firebase / firebase/firebase-js-sdk

@firebase/firestore: ca9/b815 assertion after first cold getDoc() with memoryLocalCache

Open
#10,008 5 comments 0 reactions 0 assignees View on GitHub
api: firestore needs-attention stack:React
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.