Conversation
…sing
In multi-tab mode, both `synchronizeQueryViewsAndRaiseSnapshots()` and
`syncEngineApplyActiveTargetsChange()` look up targets that other clients
advertise through `WebStorage`, and guard the result with only a
`debugAssert(!!target, ...)`.
`debugAssert` is declared `asserts assertion`, so TypeScript narrows `target`
to non-null at the call site -- but its call sites are stripped from production
builds. In a release build the null therefore flows straight into
`localStoreAllocateTarget()` and `synthesizeTargetToQuery()`, where
`targetIsPipelineTarget()` dereferences it:
TypeError: Cannot read properties of null (reading 'isCorePipeline')
Because that throws inside an AsyncQueue task, the queue is marked as failed
permanently. Every later enqueue then throws
INTERNAL ASSERTION FAILED: Unexpected state (ID: b815) ... AsyncQueue is
already failed
roughly once per watch-stream message, and the client never recovers: no
snapshot resolves again, and `terminate()` and `clearPersistence()` cannot help
because they have to enqueue as well. Only a page reload restores the client.
`localStoreGetCachedTarget()` returns `Promise<TargetOrPipeline | null>`, and
null is a state that occurs in the field: `WebStorage` and this client's target
cache can drift apart (a tab that cleared its persistence, a `WebStorage` entry
written before the target cache caught up). Skip such a target and log a warning
instead, which costs this client one mirrored listener rather than the whole
Firestore instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 1c76b61 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request fixes a multi-tab crash in the Firestore client. Instead of throwing an assertion error when a target advertised by another tab is missing from the local target cache, the client now logs a warning and safely skips the target. I have no additional feedback to provide as there are no review comments.
Discussion
In multi-tab mode,
synchronizeQueryViewsAndRaiseSnapshots()andsyncEngineApplyActiveTargetsChange()resolve target IDs that other clients advertise throughWebStorage, and guard the lookup with only adebugAssert:localStoreGetCachedTarget()returnsPromise<TargetOrPipeline | null>, so the null case is part of its contract. ButdebugAssertis declaredasserts assertion, so TypeScript narrowstargetto non-null at the call site — while, per its own docblock, "the code of callsites invoking this function are stripped out in production builds". The type system is satisfied by a check that does not exist in the build users run.In a release build the null therefore flows on into
synthesizeTargetToQuery()→targetIsPipelineTarget(), which dereferences it:Because this throws inside an AsyncQueue task, the queue is marked failed permanently. Every subsequent enqueue then throws
roughly once per watch-stream message, and the client never recovers — no snapshot resolves again, and
terminate()/clearPersistence()cannot help because they must enqueue as well. Only a page reload restores Firestore. This is the fault behind a number of the open b815 reports (#10310, #10008, #9491, #9499), where b815 is consistently the flood rather than the cause; the real error is carried in itsCONTEXT.elpayload.This is not hypothetical — it is what we see in production (Samsung Internet / Android,
persistentLocalCache+persistentMultipleTabManager).WebStorageand a given client's target cache can legitimately drift apart: a tab that cleared its persistence, or aWebStorageentry written before this client's target cache caught up.Note that 4.16.0 only changed the message. Before
targetIsPipelineTarget()existed the same null hitnewQuery(target.path, …)and threwCannot read properties of null (reading 'path'), so this latent bug predates the pipeline work — which is probably why it has been hard to search for.The fix
Handle the documented null instead of asserting it away: skip the target and log a warning. That costs the client one mirrored listener rather than the entire Firestore instance, and the tab that actually owns the target is unaffected. The two
debugAsserts are the only things removed;targetIsPipelineTarget()is deliberately left alone, since with the call sites fixed a null can no longer reach it and a defensive?.there would only hide the next occurrence.Testing
yarn assertion-id:check, ESLint and Prettier all pass on the changed file, andtsc --noEmitonpackages/firestorereports exactly the same diagnostics as the unpatched tree.I did not add a spec test: the spec framework models persistence and
WebStorageas mutually consistent by construction (both are backed by the same fake, shared across simulated clients), so the drift that triggers this is not expressible in the DSL today without a new primitive for injecting an orphan active target ID intoSharedFakeWebStorage. Happy to add that, or a test in whatever form you prefer, if you'd like it covered.API Changes
None.