Skip to content

fix(firestore): don't fail the AsyncQueue when a multi-tab cached target is missing - #10381

Open
bruno63 wants to merge 1 commit into
firebase:mainfrom
bruno63:fix/multitab-missing-cached-target
Open

bruno63 wants to merge 1 commit into
firebase:mainfrom
bruno63:fix/multitab-missing-cached-target

Conversation

@bruno63

@bruno63 bruno63 commented Sep 13, 2026

Copy link
Copy Markdown

Discussion

In multi-tab mode, synchronizeQueryViewsAndRaiseSnapshots() and syncEngineApplyActiveTargetsChange() resolve target IDs that other clients advertise through WebStorage, and guard the lookup with only a debugAssert:

const target = await localStoreGetCachedTarget(syncEngineImpl.localStore, targetId);
debugAssert(!!target, `Query data for active target ${targetId} not found`);
const targetData = await localStoreAllocateTarget(syncEngineImpl.localStore, target);

localStoreGetCachedTarget() returns Promise<TargetOrPipeline | null>, so the null case is part of its contract. But debugAssert is declared asserts assertion, so TypeScript narrows target to 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:

export function targetIsPipelineTarget(target: TargetOrPipeline): target is CorePipeline {
  // Workaround for circular dependency
  return !!(target as CorePipeline).isCorePipeline;
}
TypeError: Cannot read properties of null (reading 'isCorePipeline')

Because this throws inside an AsyncQueue task, the queue is marked failed permanently. Every subsequent enqueue then throws

FIRESTORE (12.16.0) INTERNAL ASSERTION FAILED: Unexpected state (ID: b815)
CONTEXT: {"el":"TypeError: Cannot read properties of null (reading 'isCorePipeline')..."}

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 its CONTEXT.el payload.

This is not hypothetical — it is what we see in production (Samsung Internet / Android, persistentLocalCache + persistentMultipleTabManager). WebStorage and a given client's target cache can legitimately drift apart: a tab that cleared its persistence, or a WebStorage entry written before this client's target cache caught up.

Note that 4.16.0 only changed the message. Before targetIsPipelineTarget() existed the same null hit newQuery(target.path, …) and threw Cannot 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, and tsc --noEmit on packages/firestore reports exactly the same diagnostics as the unpatched tree.

I did not add a spec test: the spec framework models persistence and WebStorage as 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 into SharedFakeWebStorage. Happy to add that, or a test in whatever form you prefer, if you'd like it covered.

API Changes

None.

…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>
@bruno63
bruno63 requested review from a team as code owners September 13, 2026 19:11
@changeset-bot

changeset-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1c76b61

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@firebase/firestore Patch
firebase Patch
@firebase/firestore-compat Patch

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

@google-cla

google-cla Bot commented Sep 13, 2026

Copy link
Copy Markdown

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant