firebase / firebase/firebase-js-sdk
@firebase/firestore: ca9/b815 assertion after first cold getDoc() with memoryLocalCache
- Dominant language
- TypeScript
- Stars
- 5.1k
- Forks
- 1k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 37
Description
### Operating System
Not OS-specific so far. Observed in a browser SPA environment.
### Environment (if applicable)
- React 19
- Vite SPA
- Firestore Web SDK initialized with `memoryLocalCache()`
- `experimentalAutoDetectLongPolling` enabled
### Firebase SDK Version
`firebase@12.13.0`
Also reproduced across `12.2.0` through `12.13.0`, so this does not appear to be specific to `12.3.0`.
### Firebase SDK Product(s)
Firestore
### Project Tooling
- React 19
- Vite
- Browser client app
### Detailed Problem Description
This is a follow-up to https://github.com/firebase/firebase-js-sdk/issues/9267#issuecomment-4540009355 after #9267 was closed as fixed by #9842. We are still seeing the same `ca9` / `b815` failure on `firebase@12.13.0`.
In our app, the deterministic trigger is that the **first Firestore operation after page load** is a single server `getDoc()` against a freshly-opened WebChannel:
```js
const db = initializeFirestore(app, {
localCache: memoryLocalCache(),
experimentalAutoDetectLongPolling: true,
});
// First op on the cold stream:
await getDoc(doc(db, 'users', uid));
```
The `ca9` assertion fires roughly 60ms after that `getDoc()` resolves, which points to the transient listen teardown path rather than the add path. The stack is consistently in the watch path:
```text
__PRIVATE_TargetState.recordTargetResponse
hardAssert(this.outstandingResponses >= 0, 0xca9)
__PRIVATE_WatchChangeAggregator.handleTargetChange
... forEachTarget -> onWatchStreamChange
```
The observed internal state is `TargetState.outstandingResponses === -1`, which suggests a watch `Removed` / `Added` target-change is being processed without a matching `recordPendingTargetRequest()`.
The more severe behavior is the cascade after the initial assertion:
1. `0xca9` is thrown from the watch target bookkeeping path.
2. The throw propagates into `AsyncQueue`.
3. `AsyncQueue` permanently fails.
4. Every subsequent Firestore operation for the page lifetime throws `b815` via `verifyNotFailed`.
So a single unexpected target response effectively bricks the entire Firestore client until the page is reloaded.
### Expected Behavior
A transient watch target bookkeeping mismatch should not permanently fail the Firestore client. At worst, the affected target should be dropped, recovered, or re-listened.
### Actual Behavior
The SDK hard-asserts with `ca9`, then the entire `AsyncQueue` remains failed and later reads/listeners/writes fail with `b815`.
### Workaround
Replacing the first transient `getDoc()` with a persistent `onSnapshot()` eliminated the issue across many cold loads in our app. The listener resolves the first server snapshot but remains open, avoiding the immediate remove-target teardown race.
Practical workaround:
```js
// Avoid making a transient getDoc() the first operation on a cold WebChannel.
// Use a kept-open listener for the first read instead.
const unsubscribe = onSnapshot(doc(db, 'users', uid), snapshot => {
// Resolve first server snapshot, but keep listener open.
});
```
### Suggested SDK Fix
Treat an unexpected extra target response as a recoverable inconsistency instead of a hard assertion that kills the shared `AsyncQueue`. Recovering the affected target or re-listening would turn this into a localized read/listen failure instead of a total page-lifetime Firestore outage.
### Steps and code to reproduce issue
The minimal trigger in our app is:
1. Initialize Firestore with `memoryLocalCache()` and `experimentalAutoDetectLongPolling`.
2. On a fresh page load / cold Firestore client, make the first Firestore operation a single `getDoc()`.
3. Allow the transient listen to resolve and tear down.
4. Observe `ca9` shortly after the `getDoc()` resolves.
5. Observe subsequent operations fail with `b815` for the rest of the page lifetime.
```js
import { initializeApp } from 'firebase/app';
import {
doc,
getDoc,
initializeFirestore,
memoryLocalCache,
} from 'firebase/firestore';
const app = initializeApp(firebaseConfig);
const db = initializeFirestore(app, {
localCache: memoryLocalCache(),
experimentalAutoDetectLongPolling: true,
});
await getDoc(doc(db, 'users', uid));
// ca9 fires shortly after this resolves; subsequent Firestore operations fail with b815.
```
I can't reproduce the issue in a test repo, will try again this week.
Contributor guide
Assessment
This issue has not been assessed yet.