js-bao-wss-client 2.2.0-alpha.12 → 2.2.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,9 +123,10 @@ const client = await initializeClient({
123
123
  appId: "my-app",
124
124
  models: [Task, Project],
125
125
 
126
- // Yjs document persistence for Node.js
127
- yjsPersistence: (docId, ydoc, { appId, userId }) => {
128
- return new SqlitePersistence(docId, ydoc, {
126
+ // Yjs document persistence for Node.js. The first argument is the store
127
+ // name — see "What the first argument is" below.
128
+ yjsPersistence: (storeName, ydoc, { appId, userId }) => {
129
+ return new SqlitePersistence(storeName, ydoc, {
129
130
  dbPath: `${process.env.HOME}/.my-app/${appId}/${userId}/yjs.sqlite`
130
131
  });
131
132
  },
@@ -137,6 +138,34 @@ const { ydoc } = await client.documents.open("doc-123", {
137
138
  });
138
139
  ```
139
140
 
141
+ **What the first argument is**
142
+
143
+ The factory's first argument is the **store name** to persist this Yjs state
144
+ under, not necessarily the document id. For an ordinary document, and for a
145
+ large document (`documentFormat: 2`) that has not rotated yet, the two are the
146
+ same value.
147
+
148
+ A large document's Yjs state is an epoch *overlay* rather than the document
149
+ itself, so once it rotates each epoch gets its own store name,
150
+ `{documentId}@{epoch}`. A rotation attaches a provider for the fresh epoch and
151
+ then clears the sealed one, so a factory that put both epochs under one name
152
+ would lose every write made since the rotation to that clear. Keying your
153
+ storage on the first argument — as the example above does — is correct for both
154
+ kinds of document, and needs no change when a document grows large.
155
+
156
+ The third argument carries the rest of what the store is for:
157
+
158
+ | Field | Meaning |
159
+ |-------|---------|
160
+ | `appId` | The application id |
161
+ | `userId` | The authenticated user, or `"anon"` |
162
+ | `documentId` | The plain document id, without the epoch suffix |
163
+ | `documentFormat` | `2` for a large document, `1` for an ordinary one — absent on the very first open of a document created elsewhere, whose format nothing local knows yet |
164
+ | `epoch` | The epoch the store name is scoped to, or `null` when the name is the plain document id |
165
+
166
+ Use `context.documentId` when you want to group every store belonging to one
167
+ document; use the first argument whenever you are naming storage.
168
+
140
169
  **Why two storage systems?**
141
170
 
142
171
  - `storageConfig` → General key-value data (auth, cache, metadata)
@@ -14,6 +14,7 @@ import { type GoogleClientsConfig } from "./internal/authController";
14
14
  import { type RequestOptions } from "./internal/httpClient";
15
15
  import { type DocumentPermission as DocumentAccessLevel, type LocalDocumentEntry, type LocalMetadataEntry, type DocumentDebugSnapshot } from "./internal/documentManager";
16
16
  import { type OfflineReplayNotice } from "js-bao";
17
+ import type { DocumentOfflineWindowError } from "js-bao";
17
18
  import { DocumentsAPI, DocumentContext, type DocumentInfo, type ResolveAliasParams, type CreateDocumentOptions } from "./api/documentsApi";
18
19
  import { type CacheFacade } from "./api/cacheFacade";
19
20
  import { MeAPI } from "./api/meApi";
@@ -420,6 +421,15 @@ export interface ExecutePromptResult<R = unknown> {
420
421
  * Absent when the provider reports none.
421
422
  */
422
423
  reasoningTokens?: number;
424
+ /**
425
+ * What this call cost, in USD, as the provider reported it.
426
+ *
427
+ * A decisions model's case is largely economic — a fraction of a chat
428
+ * model's cost on the same workload — so a result that reported tokens
429
+ * and not price would leave the number the choice turns on off the wire.
430
+ * Absent when the provider reports none.
431
+ */
432
+ cost?: number;
423
433
  };
424
434
  rawResponse: R;
425
435
  configId: string;
@@ -519,6 +529,18 @@ export interface JsBaoClientOptions {
519
529
  * - Browser: uses y-indexeddb (built-in)
520
530
  * - Node.js: no Yjs persistence (documents only synced via server)
521
531
  *
532
+ * The factory's first argument is the STORE NAME to persist this Yjs state
533
+ * under, not necessarily the document id. For an ordinary document, and for a
534
+ * large document (`documentFormat: 2`) that has not rotated yet, the two are
535
+ * the same. A large document's Yjs state is an epoch OVERLAY, so once it
536
+ * rotates each epoch gets its own name (`{documentId}@{epoch}`): a rotation
537
+ * attaches a provider for the fresh epoch and then clears the sealed one, so
538
+ * a factory that put both epochs under one name would lose the writes made
539
+ * since the rotation to that clear. Keying storage on the first argument, as
540
+ * the example below does, is therefore correct for both kinds of document;
541
+ * `context.documentId` is the plain document id when you want to group a
542
+ * document's stores.
543
+ *
522
544
  * For Node.js persistence, use y-sqlite3:
523
545
  * @example
524
546
  * ```typescript
@@ -526,8 +548,8 @@ export interface JsBaoClientOptions {
526
548
  *
527
549
  * const client = new JsBaoClient({
528
550
  * // ...
529
- * yjsPersistence: (docId, ydoc, { appId, userId }) => {
530
- * return new SqlitePersistence(docId, ydoc, {
551
+ * yjsPersistence: (storeName, ydoc, { appId, userId }) => {
552
+ * return new SqlitePersistence(storeName, ydoc, {
531
553
  * dbPath: `~/.my-app/${appId}/${userId}/yjs.sqlite`
532
554
  * });
533
555
  * }
@@ -615,6 +637,32 @@ export interface DocumentOfflineWritesResolvedEvent {
615
637
  epoch: number;
616
638
  notices: OfflineReplayNotice[];
617
639
  }
640
+ /**
641
+ * Payload of the `document:write-refused` event
642
+ * (`client.on("document:write-refused", ...)`).
643
+ *
644
+ * Fired for EVERY local write a large document refuses because it is past its
645
+ * offline write window — the same event, by the same name, the Swift client
646
+ * emits. A call that can throw throws {@link error} as well, so the throw
647
+ * stays the call site's signal; this is the channel for handling the refusal
648
+ * once, in one place, on either client.
649
+ *
650
+ * Nothing is emitted for an accepted write, for an ordinary (format-1)
651
+ * document, or for a refusal of another kind.
652
+ */
653
+ export interface DocumentWriteRefusedEvent {
654
+ documentId: string;
655
+ /** The model the refused write was on. */
656
+ model: string;
657
+ /** The record it was on. */
658
+ recordId: string;
659
+ /**
660
+ * The very error the call threw: `code` is
661
+ * `DOCUMENT_OFFLINE_WINDOW_EXPIRED`, and `details` carries `documentId`,
662
+ * `lastSyncAt`, `windowDays` and `overdueMs`.
663
+ */
664
+ error: DocumentOfflineWindowError;
665
+ }
618
666
  /**
619
667
  * Options for `client.syncMetadata()`. Controls whether the sync covers all
620
668
  * of the user's document metadata or a single document (`scope` /
@@ -1649,6 +1697,13 @@ export interface ListLocalDocumentsOptions {
1649
1697
  export interface EvictAllLocalOptions {
1650
1698
  /** If true, only evicts documents that are fully synced with the server. */
1651
1699
  onlySynced?: boolean;
1700
+ /**
1701
+ * Whose local stores to remove, when it is not whoever is signed in.
1702
+ *
1703
+ * @internal The sign-out path captures the departing user before the token is
1704
+ * cleared and passes it here; nothing else needs to.
1705
+ */
1706
+ storageUser?: string | null;
1652
1707
  }
1653
1708
  /**
1654
1709
  * Map of every event the client emits to its payload type. This is the
@@ -1738,6 +1793,7 @@ export interface JsBaoEvents {
1738
1793
  sync: SyncEvent;
1739
1794
  awareness: AwarenessEvent;
1740
1795
  "document:snapshot-load": DocumentSnapshotLoadEvent;
1796
+ "document:write-refused": DocumentWriteRefusedEvent;
1741
1797
  "blobs:upload-progress": BlobUploadProgressEvent;
1742
1798
  "blobs:upload-completed": BlobUploadCompletedEvent;
1743
1799
  "blobs:upload-failed": BlobUploadFailedEvent;
@@ -2005,6 +2061,17 @@ export declare class JsBaoClient extends Observable<any> {
2005
2061
  private jsBaoFormat2NoteDiscontinuity;
2006
2062
  /** #3435 — the convergence landed: forget what it reconciled. */
2007
2063
  private jsBaoFormat2ClearDiscontinuities;
2064
+ /**
2065
+ * #3755 — record durably that this client owes a deferred judgement.
2066
+ *
2067
+ * Written BEFORE the overlay swap and again inside the epoch mark's own
2068
+ * transaction: the client keeps two marks (the document metadata row and the
2069
+ * store's `_epoch`) and the bind raises the second to the first, so a crash
2070
+ * between them reproduces the window the note exists to close.
2071
+ */
2072
+ private jsBaoFormat2NoteDeferredReplay;
2073
+ /** #3755 — the deferred judgement has been made: forget the note. */
2074
+ private jsBaoFormat2ClearDeferredReplay;
2008
2075
  /**
2009
2076
  * #3435 — the models this client holds rows of, which is the inventory a
2010
2077
  * convergence plan decides its whole-model prunes against.
@@ -2088,12 +2155,33 @@ export declare class JsBaoClient extends Observable<any> {
2088
2155
  * behavior 15).
2089
2156
  */
2090
2157
  private jsBaoFormat2LoadSnapshot;
2158
+ /**
2159
+ * Remove one large document's local record store (#3754).
2160
+ *
2161
+ * The three doors an app removes local data through — `documents.evict`,
2162
+ * `closeDocument({ evictLocal: true })` and `logout({ wipeLocal: true })` —
2163
+ * cleared the Yjs overlay and the metadata row and nothing else, so a large
2164
+ * document's records, its unacknowledged writes and its projected query rows
2165
+ * stayed on disk and opened read-only from the same profile. This is what
2166
+ * removes them, and the Swift client has had its twin since #3436.
2167
+ */
2168
+ private jsBaoFormat2Purge;
2169
+ /** The same for every large document of one scope except `keep` (#3754). */
2170
+ private jsBaoFormat2PurgeAll;
2091
2171
  /**
2092
2172
  * Runs an epoch move with the document's local writes held back: a write
2093
2173
  * that lands on the sealed overlay after what is owed has been read off it
2094
2174
  * goes down with that document (#2816, behavior 10).
2095
2175
  */
2096
2176
  private jsBaoFormat2HoldWrites;
2177
+ /**
2178
+ * How to stop hearing this ORM instance's refused writes.
2179
+ *
2180
+ * Held so the subscription goes down with the instance it belongs to: an
2181
+ * ORM torn down and replaced would otherwise leave a listener re-emitting
2182
+ * on a client that has moved on.
2183
+ */
2184
+ private jsBaoFormat2WriteRefusedUnsubscribe;
2097
2185
  /**
2098
2186
  * Documents whose writes this client is already holding.
2099
2187
  *
@@ -2181,6 +2269,33 @@ export declare class JsBaoClient extends Observable<any> {
2181
2269
  * the bind. They stay silent until it has (#2816, behavior 12).
2182
2270
  */
2183
2271
  private readonly format2OwedCatchUps;
2272
+ /**
2273
+ * Documents whose last move was REFUSED because its deferred judgement
2274
+ * could not be recorded durably (#3755, finding 3755-R1).
2275
+ *
2276
+ * The debt stays owed and the document stays held behind the room, so the
2277
+ * existing owed-catch-up path retries the catch-up and the move on the next
2278
+ * `epoch.info` or `snapshot.ready`. Deliberately not a stop: nothing is
2279
+ * broken, one write did not land.
2280
+ */
2281
+ private readonly format2DeferredNoteRefusals;
2282
+ /**
2283
+ * Documents whose deferred judgement is being planned right now (#3755).
2284
+ *
2285
+ * Both triggers — the bind and the document's `epoch.info` — can land while
2286
+ * the other's archive reads are in flight, and two resumes over one note
2287
+ * would judge the same writes twice.
2288
+ */
2289
+ private readonly format2ResumingJudgements;
2290
+ /**
2291
+ * Test-only (#3755): hold a deferred judgement instead of settling it.
2292
+ *
2293
+ * `null` in every build an application runs, and read in exactly one place
2294
+ * ({@link format2SettlePendingReplay}). Not on the package's public surface:
2295
+ * an integrated test reaches it the way it reaches the other private fields
2296
+ * it drives, and nothing an app can call installs it.
2297
+ */
2298
+ private format2ReplaySettleGate;
2184
2299
  /**
2185
2300
  * Large documents stopped on a superseded epoch: their overlay belongs to an
2186
2301
  * epoch the room has left behind, so nothing of their local state may be
@@ -2829,6 +2944,21 @@ export declare class JsBaoClient extends Observable<any> {
2829
2944
  private getServiceWorkerApiBase;
2830
2945
  private handleServiceWorkerAuthChange;
2831
2946
  private emitAuthSuccess;
2947
+ /**
2948
+ * What a large document's browser store is named by, for one NAMED user
2949
+ * (#3754).
2950
+ *
2951
+ * The same three parts every open uses. Taking the user as an argument is the
2952
+ * whole point: at logout the token has already gone, so anything that asked
2953
+ * "who is signed in?" would answer `anon` and name stores this user never
2954
+ * wrote (finding R1).
2955
+ */
2956
+ private largeDocumentScopeFor;
2957
+ /**
2958
+ * The purge hook as an eviction context wants it: the scope filled in from
2959
+ * the user the caller captured (#3754).
2960
+ */
2961
+ private largeDocumentPurgeContext;
2832
2962
  private getCurrentUserIdForStorage;
2833
2963
  private resolveAnalyticsUserUlid;
2834
2964
  private resolveAnalyticsPlan;
@@ -3209,6 +3339,30 @@ export declare class JsBaoClient extends Observable<any> {
3209
3339
  * next frame on THIS connection reads, before any store has been bound.
3210
3340
  */
3211
3341
  private format2NoteDiscontinuity;
3342
+ /**
3343
+ * Record durably that this document owes a deferred judgement (#3755).
3344
+ *
3345
+ * Written BEFORE the overlay swap, and again inside the epoch mark's own
3346
+ * transaction: the client keeps TWO epoch marks — the document metadata row
3347
+ * (moved by `swapEpochDocument`) and the store's `_epoch` (moved after it) —
3348
+ * and the bind raises the second to the first. A crash between them would
3349
+ * otherwise leave a store whose mark reads current with nothing saying a
3350
+ * judgement is owed, which is the very defect this note closes. One row, one
3351
+ * idempotent upsert, whichever of the two lands.
3352
+ *
3353
+ * REJECTS on failure rather than logging and carrying on (finding 3755-R1):
3354
+ * the caller refuses the move, so nothing advances without the note.
3355
+ */
3356
+ private format2NoteDeferredReplay;
3357
+ /**
3358
+ * The deferred judgement has been made: forget the note (#3755).
3359
+ *
3360
+ * Awaited before the withhold is released, so a flush that fires during the
3361
+ * clear still finds the covered sequences held back. A failure leaves the
3362
+ * note standing — the survivors are stated and safe; only their claim waits
3363
+ * for the next bind to read the note again.
3364
+ */
3365
+ private format2ClearDeferredReplay;
3212
3366
  /**
3213
3367
  * Run `run` with this document's local writes held back, once.
3214
3368
  *
@@ -3229,6 +3383,23 @@ export declare class JsBaoClient extends Observable<any> {
3229
3383
  * prevent.
3230
3384
  */
3231
3385
  private format2WithholdSequences;
3386
+ /**
3387
+ * Hold back every sequence a DURABLE deferred judgement still covers
3388
+ * (#3755, finding 3755-R3).
3389
+ *
3390
+ * Applied by every caller of `claimCatchUp` before it claims, because there
3391
+ * is no barrier between the ORM's adoption and the post-bind hook:
3392
+ * `handleSyncStep1` reads these very marks and claims straight off them, and
3393
+ * a handshake landing in that window would otherwise claim sequences whose
3394
+ * content is on no frame. One synchronous read of the mirror the adoption
3395
+ * refreshed, on a path the client already takes.
3396
+ *
3397
+ * The floor is the lowest UNACKNOWLEDGED sequence, not the note's own span:
3398
+ * the server's mark is contiguous, so nothing above a covered sequence can be
3399
+ * acknowledged anyway, and claiming it would only invite the prune this
3400
+ * exists to prevent.
3401
+ */
3402
+ private format2ApplyDurableWithhold;
3232
3403
  /** The withheld writes have been replayed: their sequences may travel. */
3233
3404
  private format2ReleaseWithheldSequences;
3234
3405
  /** The highest bulk-load epoch this document has not converged past (#3435). */
@@ -3396,6 +3567,107 @@ export declare class JsBaoClient extends Observable<any> {
3396
3567
  * about and classified by presence alone (finding 3435-R07).
3397
3568
  */
3398
3569
  private format2SettleConvergenceReplay;
3570
+ /**
3571
+ * Resume a judgement a restart interrupted (#3755, C4).
3572
+ *
3573
+ * The debt is durable and this is where it is paid. Runs when BOTH halves
3574
+ * are in — the store is bound (so the note and the owed writes are readable)
3575
+ * and the document's `epoch.info` has arrived (so the chain and the seal
3576
+ * times are) — whichever comes second.
3577
+ *
3578
+ * The evidence is rebuilt into the DOCUMENT's own conflict ledger, never a
3579
+ * replacement (finding 3755-R2): `format2NoteClock` has already seeded it
3580
+ * with the handshake's seal times, and a fresh ledger without them would
3581
+ * make a provably outdated write merely ambiguous and replay it. Each
3582
+ * archive of `[from_epoch, to_epoch)` is NOTED, never folded — the merged
3583
+ * view already holds those epochs; what is missing is the record of what
3584
+ * they touched.
3585
+ *
3586
+ * The judgement itself is then held exactly as an in-session deferral is, so
3587
+ * it runs on the joined epoch's `syncComplete` (with the open epoch's content
3588
+ * noted first and the dropped-delete question asked) or on the same 15 s
3589
+ * fallback.
3590
+ */
3591
+ private format2MaybeResumeDeferredJudgement;
3592
+ /**
3593
+ * Pay a durable judgement NOW, from the recorded mutations (#3755, findings
3594
+ * 3755-C01 and 3755-C03).
3595
+ *
3596
+ * A note stands exactly where the writes it covers are on no overlay: a
3597
+ * deferral installed a fresh one without them, or a bind kept them off it on
3598
+ * purpose. So no carry read off an overlay can ever settle this debt —
3599
+ * `carryUnackedOverlay` finds nothing to carry and the note would be cleared
3600
+ * over writes that reached no frame. It is settled here instead, from the
3601
+ * durable log, and closed in the order an uninterrupted judgement closes it.
3602
+ *
3603
+ * Unlike {@link format2MaybeResumeDeferredJudgement} this does not wait for
3604
+ * the joined epoch to sync: every caller is already at the point where that
3605
+ * epoch's content is in (a judgement that has just run, or a rebuild that
3606
+ * has just landed its base).
3607
+ *
3608
+ * Answers whether there was a debt; a caller told `false` still owes its own
3609
+ * close-out.
3610
+ */
3611
+ private format2SettleDurableDebt;
3612
+ /**
3613
+ * The durable debt this document is standing under, if any (#3755).
3614
+ *
3615
+ * The same reading the bind makes, off the same marks: a note is honoured
3616
+ * only where it still names writes this store has not had acknowledged.
3617
+ */
3618
+ private format2DurableDebtNote;
3619
+ /**
3620
+ * The writes a resumed judgement is about (#3755).
3621
+ *
3622
+ * This client's ops inside the note's span, MINUS the ones a bulk load's
3623
+ * convergence owns: the discontinuity path judges everything at or below its
3624
+ * boundary (#3435, finding FROZEN-01), so the two sets are disjoint and no
3625
+ * write is judged twice (edge E4).
3626
+ */
3627
+ private format2OwedForJudgement;
3628
+ /**
3629
+ * Note the archives of `[from_epoch, to_epoch)` as conflict evidence, without
3630
+ * folding any of them (#3755, behavior 13).
3631
+ *
3632
+ * The merged view already holds those epochs — the interrupted session
3633
+ * applied them — so folding would re-apply state the document has. What was
3634
+ * lost is the RECORD of which records and fields each of them touched, which
3635
+ * is what a recency judgement is made of. An archive a concurrent catch-up
3636
+ * has already noted is not fetched again.
3637
+ */
3638
+ private format2NoteResumedChain;
3639
+ /** The document's conflict ledger, created only if it has none. */
3640
+ private format2EnsureConflictLedger;
3641
+ /**
3642
+ * Judge a resumed deferral and state what survives (#3755, C4 steps 1-3).
3643
+ *
3644
+ * The same three things an uninterrupted judgement does, in the same order,
3645
+ * which is why the verdict an app receives is the same either way.
3646
+ */
3647
+ private format2RunResumedJudgement;
3648
+ /**
3649
+ * Steps 1 and 2 of a resumed judgement: resolve, and state what survives.
3650
+ *
3651
+ * Apart from the close-out, so a caller that has more to do before the note
3652
+ * may be cleared can take the same two steps (#3755, finding 3755-C01).
3653
+ */
3654
+ private format2StateResumedJudgement;
3655
+ /**
3656
+ * Close out a judgement, in the order that keeps a flush honest (#3755,
3657
+ * finding 3755-R5).
3658
+ *
3659
+ * Clear the note durably, THEN release the withhold, THEN send the
3660
+ * state-complete claim. A flush that fires during the awaited clear still
3661
+ * finds the covered sequences held back, so it carries the survivors and
3662
+ * claims nothing — and the claim that follows covers them.
3663
+ *
3664
+ * The claim is not conditional on anything having been dropped: the
3665
+ * survivors are STATED from their recorded mutations rather than committed,
3666
+ * so no sequence is noted for them and an ordinary frame's claim starts at
3667
+ * the session floor. Only a state-complete frame may claim what this session
3668
+ * did not write, and this is that frame.
3669
+ */
3670
+ private format2FinishDeferredJudgement;
3399
3671
  /**
3400
3672
  * The owed writes a RESTART left for the convergence to judge (#3435,
3401
3673
  * finding FROZEN-01).
@@ -4478,26 +4750,39 @@ export declare class JsBaoClient extends Observable<any> {
4478
4750
  private metadataHydratedFromIdb;
4479
4751
  private _loadAllMetadataFromIdb;
4480
4752
  /**
4481
- * Seed the local-only marker for one document from its persisted metadata.
4753
+ * Seed what the open path must know about a document BEFORE it acts on it,
4754
+ * from that document's persisted metadata row.
4482
4755
  *
4483
- * `openDocument` can be handed the id of a document created with
4484
- * `localOnly: true` in an earlier session, and nothing on the open path
4485
- * reads local storage first — the in-memory metadata index is only ever
4486
- * filled by a listing call, which an app that stores document ids need never
4487
- * make. Without this, the document opens looking ordinary and its first edit
4488
- * goes out as an `update` frame (#2691).
4756
+ * Two facts, one keyed read:
4489
4757
  *
4490
- * One keyed read, and only while the flag could still be unknown: once the
4491
- * whole index has been hydrated this session, or the document is already
4492
- * known local-only, there is nothing to learn.
4758
+ * - Local-only-ness (#2691). `openDocument` can be handed the id of a
4759
+ * document created with `localOnly: true` in an earlier session, and
4760
+ * nothing else on the open path reads local storage first — the in-memory
4761
+ * metadata index is only ever filled by a listing call, which an app that
4762
+ * stores document ids need never make. Without this the document opens
4763
+ * looking ordinary and its first edit goes out as an `update` frame.
4764
+ * - The naming fields (#3669). A large document's local overlay is stored
4765
+ * under `documentId@format2Epoch`, and `format2Epoch` lives in that row.
4766
+ * A by-id open in an un-hydrated session would otherwise attach the plain
4767
+ * name — an empty overlay — and the whole-row metadata write that follows
4768
+ * would erase the fields that said which store was the real one.
4493
4769
  *
4494
4770
  * "Hydrated" means a read that actually reached the store
4495
4771
  * (`metadataHydratedFromIdb`), not one that was merely attempted: the bulk
4496
4772
  * read gives up after 2s and swallows its own failures, and taking that
4497
4773
  * empty result as an answer would put a stored local-only document back on
4498
4774
  * the wire on exactly the sessions where storage was slow or unavailable.
4775
+ * The naming half is read even on a hydrated session when the cache says
4776
+ * this is a large document, because another tab may have rotated since the
4777
+ * hydration — one keyed get per open of a large document, and nothing at all
4778
+ * for an ordinary one.
4779
+ *
4780
+ * @returns whether the read produced the durable ROW. There is no definite
4781
+ * null to be had below this call — every layer answers a failure by
4782
+ * resolving `null` — so an absent row is read as a naming that is not
4783
+ * durable, and no store is reclaimed on one.
4499
4784
  */
4500
- private _primeLocalOnlyFromLocalMetadata;
4785
+ private _primeFromLocalMetadata;
4501
4786
  /**
4502
4787
  * The format of a document being opened (#2816).
4503
4788
  *
@@ -4512,7 +4797,30 @@ export declare class JsBaoClient extends Observable<any> {
4512
4797
  * opens as legacy, exactly as it does today.
4513
4798
  */
4514
4799
  private _resolveDocumentFormat;
4800
+ /**
4801
+ * The durable naming read the manager uses before it chooses a store name
4802
+ * (#3745), or `undefined` when there is nothing a read could add.
4803
+ *
4804
+ * Only worth a keyed read when the in-memory index is NOT already the whole
4805
+ * picture: a hydration that reached the store answered for every document in
4806
+ * it, so an ordinary open must not pay a second read. The same gate
4807
+ * `_resolveDocumentFormat` applies to its own persisted source.
4808
+ *
4809
+ * A LARGE document is the one exception, and it is why this takes the
4810
+ * document rather than answering for the session (#3669): another tab can
4811
+ * rotate it after the hydration, so the epoch the index holds for it can be
4812
+ * behind the row on disk, and the sweep on open re-reads that row under the
4813
+ * epoch lock. An ordinary document on a hydrated session still pays nothing.
4814
+ */
4815
+ private _persistedNamingReader;
4515
4816
  private _putMetadataToIdb;
4817
+ /**
4818
+ * Persist a DELTA of one document's metadata row (#3746) — the fields named
4819
+ * and nothing else, merged into whatever is stored. What the document-open
4820
+ * path writes with, so opening one document in one tab cannot regress a field
4821
+ * another tab's rotation or a listing wrote.
4822
+ */
4823
+ private _mergeMetadataToIdb;
4516
4824
  private _deleteMetadataFromIdb;
4517
4825
  private _enforceRetentionPolicy;
4518
4826
  /** Get the current network status including connectivity and transport state.
@@ -4635,7 +4943,28 @@ export declare class JsBaoClient extends Observable<any> {
4635
4943
  evictLocalDocument(documentId: string, opts?: {
4636
4944
  force?: boolean;
4637
4945
  suppressMetadataEvent?: boolean;
4946
+ /**
4947
+ * Whose local stores this is about. Defaults to the signed-in user; the
4948
+ * sign-out path names the user who is signing OUT, because by then
4949
+ * nobody is.
4950
+ *
4951
+ * @internal
4952
+ */
4953
+ storageUser?: string | null;
4638
4954
  }): Promise<void>;
4955
+ /**
4956
+ * Whose stores an eviction is about, decided NOW (#3754).
4957
+ *
4958
+ * The sign-out path names the user explicitly, because by the time it evicts
4959
+ * the token is gone and nobody is signed in (finding R1). Every other caller
4960
+ * names nobody — and "nobody" used to mean "whoever is signed in when the
4961
+ * purge eventually runs", which is a different user for the one eviction that
4962
+ * does not run at once: evicting a document that is OPEN defers its purge to
4963
+ * the close, and a sign-out or a sign-in as somebody else can happen in
4964
+ * between. Resolved at the door, the scope the close purges under is the one
4965
+ * the eviction was asked for (criterion 5, finding 3754-C01).
4966
+ */
4967
+ private capturedStorageUser;
4639
4968
  /** Remove all locally stored document data.
4640
4969
  * @param opts - Optional eviction behavior
4641
4970
  * @group Offline & Sync */