@lunora/client 1.0.0-alpha.66 → 1.0.0-alpha.68

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.
Files changed (40) hide show
  1. package/dist/auth/index.d.mts +2 -2
  2. package/dist/auth/index.d.ts +2 -2
  3. package/dist/index.d.mts +44 -11
  4. package/dist/index.d.ts +44 -11
  5. package/dist/index.mjs +1 -1
  6. package/dist/packem_shared/LunoraClient-D-06gXR2.mjs +1 -0
  7. package/dist/packem_shared/OfflineQueue-DIhrKdlU.mjs +1 -0
  8. package/dist/packem_shared/{SubscriptionRegistry-CdHSrDqu.mjs → SubscriptionRegistry-D7n5ZE5F.mjs} +1 -1
  9. package/dist/packem_shared/applyDelta-DJmlwFnT.mjs +1 -0
  10. package/dist/packem_shared/{createAsyncStorageQueryCache-Di-S52wy.mjs → createAsyncStorageQueryCache-C0mJLuZK.mjs} +1 -1
  11. package/dist/packem_shared/createClientQuery-B8Nfj-7o.mjs +1 -0
  12. package/dist/packem_shared/createLocalStore-DtsP-CpS.mjs +1 -0
  13. package/dist/packem_shared/{createServerClient-BhfYV6Je.mjs → createServerClient-SWMORQWn.mjs} +1 -1
  14. package/dist/packem_shared/{createSnapshotPrecondition-BPMQbAbk.mjs → createSnapshotPrecondition-ZyQDet2v.mjs} +1 -1
  15. package/dist/packem_shared/{delta-merge-BoVuM-rE.mjs → delta-merge-CVSN-uoC.mjs} +1 -1
  16. package/dist/packem_shared/{local-store-tpI9VFpO.mjs → local-store-BQuKm9n6.mjs} +1 -1
  17. package/dist/packem_shared/{lunora-client.d-DAbmOzH_.d.mts → lunora-client.d-BQ0ig3ih.d.ts} +162 -2
  18. package/dist/packem_shared/{lunora-client.d-Dz6Yx4By.d.ts → lunora-client.d-DafpbiD5.d.mts} +162 -2
  19. package/dist/packem_shared/offline-queue-Bmeyg5fy.mjs +1 -0
  20. package/dist/packem_shared/{preload.d-BmPMaxYW.d.mts → preload.d-C09vDOAR.d.mts} +2 -2
  21. package/dist/packem_shared/{preload.d-CN5mOiAj.d.ts → preload.d-DGiAgCkV.d.ts} +2 -2
  22. package/dist/packem_shared/{types.d-BOB755CV.d.mts → types.d-Bsj5mGrj.d.mts} +31 -6
  23. package/dist/packem_shared/{types.d-BOB755CV.d.ts → types.d-Bsj5mGrj.d.ts} +31 -6
  24. package/dist/packem_shared/{wire-codec-D4iww4NV.mjs → wire-codec-PBOTh_2d.mjs} +1 -1
  25. package/dist/packem_shared/{wire-key-BOdKmpG3.mjs → wire-key-Dl7EFWSD.mjs} +1 -1
  26. package/dist/query/index.d.mts +3 -3
  27. package/dist/query/index.d.ts +3 -3
  28. package/dist/service.d.mts +1 -1
  29. package/dist/service.d.ts +1 -1
  30. package/dist/service.mjs +1 -1
  31. package/dist/ssr/index.d.mts +4 -4
  32. package/dist/ssr/index.d.ts +4 -4
  33. package/dist/ssr/index.mjs +1 -1
  34. package/package.json +2 -2
  35. package/dist/packem_shared/LunoraClient-BKmMPd4k.mjs +0 -1
  36. package/dist/packem_shared/OfflineQueue-DG51qWl4.mjs +0 -1
  37. package/dist/packem_shared/applyDelta-DGqMpi3N.mjs +0 -1
  38. package/dist/packem_shared/createClientQuery-TKD_52cT.mjs +0 -1
  39. package/dist/packem_shared/createLocalStore-CZwiN9F4.mjs +0 -1
  40. package/dist/packem_shared/offline-queue-CzI2lYQ8.mjs +0 -1
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClientOptions, a as Unsubscribe, U as User, W as WsTokenProvider, S as ShardTrafficResult, b as ScheduleRecord, c as SchedulerStatus, d as WorkflowInstanceStatus, e as WorkflowInstancePage, f as WorkflowInstanceDetail, g as WorkflowInstanceAction, h as FunctionDescriptor, i as StorageListPage, G as GlobalTableInfo, j as GlobalFilterClause, k as GlobalTablePage, l as GlobalFacetResult, H as HttpStreamRef, m as HttpStreamArgsOf, n as HttpStreamChunkOf } from "./types.d-BOB755CV.mjs";
1
+ import { F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClientOptions, a as Unsubscribe, U as User, W as WsTokenProvider, S as ShardTrafficResult, b as ScheduleRecord, c as SchedulerStatus, d as WorkflowInstanceStatus, e as WorkflowInstancePage, f as WorkflowInstanceDetail, g as WorkflowInstanceAction, h as FunctionDescriptor, i as StorageListPage, G as GlobalTableInfo, j as GlobalFilterClause, k as GlobalTablePage, l as GlobalFacetResult, H as HttpStreamRef, m as HttpStreamArgsOf, n as HttpStreamChunkOf } from "./types.d-Bsj5mGrj.js";
2
2
  import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
3
3
  /**
4
4
  * Reactive key-value store for local-only client state.
@@ -208,6 +208,14 @@ declare class SubscriptionRegistry {
208
208
  add(state: SubscriptionState): void;
209
209
  remove(state: SubscriptionState): void;
210
210
  all(): SubscriptionState[];
211
+ /**
212
+ * Drop every registration. Terminal — used by `LunoraClient.close()`, whose
213
+ * whole point is to release the callback closures each {@link SubscriptionState}
214
+ * holds (`callbacks`, `errorCallbacks`, `checkpointCallbacks` — React state
215
+ * setters and `@lunora/db` collection closures), which otherwise outlive the
216
+ * closed client for as long as the client object is reachable.
217
+ */
218
+ clear(): void;
211
219
  }
212
220
  /**
213
221
  * Read/write handle over the client's live query cache, handed to a mutation's
@@ -694,6 +702,31 @@ declare class LunoraClient {
694
702
  */
695
703
  private readonly streams;
696
704
  /** Live shape subscriptions (partial replication), keyed by their wire id. */
705
+ /**
706
+ * Teardown callbacks for the admin sockets {@link LunoraClient.subscribeScheduledJobs}
707
+ * opens. Those run their own reconnect loop off a closure-local `closed`
708
+ * flag rather than `this.closed` (they predate `ensureSocket`'s guard), so
709
+ * without this registry a `close()` left every one of them reconnecting on
710
+ * its backoff forever — re-minting an ephemeral admin sub-token on each
711
+ * attempt when a `WsTokenProvider` is wired.
712
+ */
713
+ private readonly adminSocketTeardowns;
714
+ /**
715
+ * The in-flight offline-queue replay per shard (`connectionKey`), while one
716
+ * is running. Two jobs:
717
+ *
718
+ * 1. It serializes overlapping flushes for the same shard — two reconnect
719
+ * events in quick succession used to drain and replay concurrently.
720
+ * 2. It is the barrier {@link LunoraClient.mutation} waits on before sending a FRESH
721
+ * write directly. The socket's `open` handler flips `wsState` to `"open"`
722
+ * first and calls the flush last, so from that instant `mutation()`'s
723
+ * offline gate is false and a new write raced straight to `/rpc` against
724
+ * the replay of the older, queued write for the same document — the newer
725
+ * one could land first and then be overwritten by the older. Ordering
726
+ * inside the replay (`replaySequential`) never covered this, because the
727
+ * racing write was never in the queue.
728
+ */
729
+ private readonly offlineFlushes;
697
730
  private readonly shapeSubscriptions;
698
731
  /**
699
732
  * In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by
@@ -813,6 +846,10 @@ declare class LunoraClient {
813
846
  * Stored per shard and replayed on every (re)connect. When a socket for the
814
847
  * shard is already open, a fresh `connect` envelope is sent immediately so the
815
848
  * server sees the new context without waiting for a reconnect.
849
+ *
850
+ * Not available on a `crossTabSync` FOLLOWER tab — the context rides the
851
+ * `connect` envelope of a socket a follower does not own, so it would be
852
+ * stored and never sent. Throws `NOT_IMPLEMENTED` there.
816
853
  */
817
854
  setConnectionContext(context: Record<string, unknown> | undefined, options?: {
818
855
  shardKey?: string;
@@ -852,6 +889,11 @@ declare class LunoraClient {
852
889
  * do not put data on a whisper topic that some shard members shouldn't see,
853
890
  * and don't trust a whisper's `data` as authorization. Use a query/mutation
854
891
  * (with RLS) for anything privileged; whispers are for transient awareness.
892
+ *
893
+ * Not available on a `crossTabSync` FOLLOWER tab — whisper frames are not
894
+ * relayed over the cross-tab channel, so this throws `NOT_IMPLEMENTED`
895
+ * there rather than registering a handler nothing can ever reach. See
896
+ * {@link LunoraClientOptions.crossTabSync}.
855
897
  */
856
898
  whisperSubscribe(topic: string, handler: (data: unknown, from?: string) => void, options?: {
857
899
  shardKey?: string;
@@ -863,6 +905,11 @@ declare class LunoraClient {
863
905
  * queued), and the server silently drops it if the sender exceeds its
864
906
  * whisper rate budget. The sender never receives its own whisper. Omitting
865
907
  * `data` delivers JSON `null` to receivers (not `undefined`).
908
+ *
909
+ * That best-effort drop is for a socket that is momentarily down. A
910
+ * `crossTabSync` FOLLOWER tab has no socket and never will (see
911
+ * {@link LunoraClientOptions.crossTabSync}), so every whisper from it would
912
+ * be dropped forever — it throws `NOT_IMPLEMENTED` instead.
866
913
  */
867
914
  whisper(topic: string, data?: unknown, options?: {
868
915
  shardKey?: string;
@@ -1069,6 +1116,22 @@ declare class LunoraClient {
1069
1116
  * Mutations issued before the very first WS connect to a shard fail fast.
1070
1117
  * Opt into queueing-before-first-connect via
1071
1118
  * `OfflineQueueOptions.queueBeforeFirstConnect`.
1119
+ *
1120
+ * **Return-value caveat — the queued paths do not carry the server's result.**
1121
+ * The declared `Promise<ReturnOf<F>>` only holds when the write goes straight
1122
+ * to the server. Once a write is queued:
1123
+ *
1124
+ * - with a durable `outbox` configured, this resolves **immediately with
1125
+ * `undefined`** (typed as `ReturnOf<F>`) the moment the write is handed to
1126
+ * the outbox — the replay happens later, out of band, with no awaiter;
1127
+ * - with the built-in offline queue, it stays pending until the replay lands
1128
+ * and then resolves with the replayed call's value.
1129
+ *
1130
+ * So `const id = await client.mutation(api.todos.create, …)` is `undefined`
1131
+ * for every write issued while offline under an outbox. Generate ids
1132
+ * client-side (or read them back from a subscription) rather than depending
1133
+ * on a mutation's return value in an offline-capable app —
1134
+ * {@link LunoraClient.importRows} documents the same caveat for its counts.
1072
1135
  */
1073
1136
  mutation<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>): Promise<ReturnOf<F>>;
1074
1137
  action<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: ActionCallOptions): Promise<ReturnOf<F>>;
@@ -1309,6 +1372,11 @@ declare class LunoraClient {
1309
1372
  * `DELETE /_lunora/admin/storage?key=…` endpoint — the worker must be built
1310
1373
  * with a `storageDelete` function and `adminToken`. Powers the studio file
1311
1374
  * browser's per-row delete; resolves `{ deleted, key }`.
1375
+ *
1376
+ * An absent `deleted` field reads as `false`, matching every sibling admin
1377
+ * verb (`runCronJob`'s `ran`, …): the studio renders this value as the row's
1378
+ * outcome, so defaulting a missing field to success would report a delete
1379
+ * that a mismatched/older worker never performed.
1312
1380
  */
1313
1381
  deleteStorageObject(key: string, options?: {
1314
1382
  bucket?: string;
@@ -1700,6 +1768,20 @@ declare class LunoraClient {
1700
1768
  offset?: number;
1701
1769
  userId?: string;
1702
1770
  }): Promise<AuthPage<AuthSession>>;
1771
+ /**
1772
+ * Subscribe to a live query. The callback fires with the current value (from
1773
+ * the durable read cache, when one is hydrated) and again on every server
1774
+ * frame; the returned function unsubscribes.
1775
+ *
1776
+ * Subscriptions are deduped by `(functionPath, args, shardKey)` — a second
1777
+ * `subscribe` for the same triple joins the existing registration and shares
1778
+ * its value, cursor and optimistic layers.
1779
+ *
1780
+ * Not available on a `crossTabSync` FOLLOWER tab: the cross-tab channel only
1781
+ * carries the LEADER's own subscriptions outward, so a follower would receive
1782
+ * this query's frames only if the leader happened to hold it too. Throws
1783
+ * `NOT_IMPLEMENTED` there — see {@link LunoraClientOptions.crossTabSync}.
1784
+ */
1703
1785
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
1704
1786
  onCheckpoint?: (watermark: SyncWatermark) => void;
1705
1787
  onError?: SubscriptionErrorCallback;
@@ -1716,6 +1798,11 @@ declare class LunoraClient {
1716
1798
  * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
1717
1799
  * (name, args): the server resolves them under the socket's verified identity,
1718
1800
  * so every call gets its own id + view. The returned function unsubscribes.
1801
+ *
1802
+ * Not available on a `crossTabSync` FOLLOWER tab: shape pokes are not part of
1803
+ * the leader→follower broadcast set, so a follower's shape could never
1804
+ * resolve. Throws `NOT_IMPLEMENTED` there — see
1805
+ * {@link LunoraClientOptions.crossTabSync}.
1719
1806
  */
1720
1807
  subscribeShape(shape: {
1721
1808
  args?: Record<string, unknown>;
@@ -1770,6 +1857,53 @@ declare class LunoraClient {
1770
1857
  close(): void;
1771
1858
  /** Guard shared by every public entry point: a closed client accepts no further calls. */
1772
1859
  private assertOpen;
1860
+ /**
1861
+ * `true` when this tab is a cross-tab FOLLOWER of a live leader — i.e. it
1862
+ * will not open a socket of its own and another tab is known to hold one.
1863
+ *
1864
+ * Deliberately NOT just `!isLeader()`. Every `crossTabSync` client is a
1865
+ * non-leader for the first `leaderTimeout` of its life, while its
1866
+ * claim-leadership probe is outstanding; a lone tab self-promotes at the end
1867
+ * of that window and `onBecomeLeader` opens the sockets and replays every
1868
+ * registered subscription. That window is a legitimate, self-healing defer,
1869
+ * not a failure. A KNOWN leader on another tab is the state that never heals.
1870
+ */
1871
+ private followsAnotherTab;
1872
+ /**
1873
+ * Reject a call that needs a socket this tab will never have.
1874
+ *
1875
+ * The cross-tab protocol is one-directional: a leader broadcasts
1876
+ * `subscription-data` / `-error` / `-settled` / `connection-status` to
1877
+ * followers, and a follower has no frame with which to tell the leader what
1878
+ * it needs (see `cross-tab.ts`'s `WsFollowerMessage`, which is exactly
1879
+ * heartbeat / claim-leadership / yield-leadership). `subscribeShape` /
1880
+ * `whisper*` / `setConnectionContext` / `acquireConnectionContext` — none of
1881
+ * which the leader broadcasts at all — therefore never worked on a follower
1882
+ * under any circumstances: each returned a handle that looked live, fired no
1883
+ * callback, raised no error, and reported `connectionStatus() ===
1884
+ * "connected"` (mirrored from the leader).
1885
+ *
1886
+ * `subscribe` is deliberately NOT in that set. A follower's `subscribe`
1887
+ * registers the key the leader's broadcast is matched against, so it is the
1888
+ * mechanism the relay is built on rather than a surface that silently fails.
1889
+ * A follower sees a query only while the leader holds the same
1890
+ * `(fn, args, shardKey)` — that is the documented shape of the option, not a
1891
+ * defect.
1892
+ *
1893
+ * `stream()` reaches the same outcome by a different route: it fails the
1894
+ * handle it returns rather than throwing at the call. See
1895
+ * {@link LunoraClientOptions.crossTabSync} for what the option does and does
1896
+ * not cover.
1897
+ */
1898
+ private assertLeaderOwnedSurface;
1899
+ /**
1900
+ * Clear every timer a {@link ShardConnection} can have armed.
1901
+ *
1902
+ * One function because both teardown paths must clear all three and a fourth
1903
+ * timer would otherwise have to be remembered in two places — which is how a
1904
+ * leak gets added rather than written.
1905
+ */
1906
+ private clearConnectionTimers;
1773
1907
  /**
1774
1908
  * Tear down one {@link ShardConnection}'s live state: clear its reconnect/
1775
1909
  * connect timers, stop its heartbeat, and close its socket (if any).
@@ -1777,6 +1911,15 @@ declare class LunoraClient {
1777
1911
  * handler (demoted, but still alive) so a demoted leader can't leak a
1778
1912
  * pending `reconnectTimer` or an open socket's `heartbeatTimer` the way
1779
1913
  * an inline `conn.socket?.close()` — which skips both — used to.
1914
+ *
1915
+ * Settles this shard's in-flight streams first. The teardown clears
1916
+ * `conn.socket` BEFORE the real `close` event fires, so that event trips
1917
+ * `openManagedSocket`'s identity guard (`conn.socket !== socket`) and
1918
+ * returns — meaning `handleDisconnect`, the only other place that settles a
1919
+ * shard's streams, never runs for this connection again. `close()` already
1920
+ * failed and cleared `this.streams` before it gets here, so this is a no-op
1921
+ * on that path; the cross-tab demotion path is the one where a consumer's
1922
+ * `for await` used to block forever with no error and no completion.
1780
1923
  */
1781
1924
  private teardownConnection;
1782
1925
  /**
@@ -2155,12 +2298,21 @@ declare class LunoraClient {
2155
2298
  */
2156
2299
  private rejectQueuedForIdentityChange;
2157
2300
  /**
2158
- * Migrate every live identity stamp from `from` to `to` — used when the auth
2301
+ * Migrate every identity stamp from `from` to `to` — used when the auth
2159
2302
  * identity label changes but the underlying credential (token) does NOT, e.g.
2160
2303
  * the user id resolves a tick after the token was set. The in-memory
2161
2304
  * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
2162
2305
  * keeps the in-flight writes replayable under the new (more stable) identity
2163
2306
  * instead of the flush guard discarding them as a mismatch.
2307
+ *
2308
+ * That map alone was not enough: it is consumed and DELETED on the first
2309
+ * flush attempt (`passesReplayIdentityGate`), while the queue entry and its
2310
+ * persisted record keep the original stamp. So a reload, or a requeue after a
2311
+ * transient failure, fell back to the old token hash — and once the token had
2312
+ * been refreshed, `isSameCredentialUnderTokenHash` no longer recognised it
2313
+ * and the write was rejected `OFFLINE_IDENTITY_CHANGED` for the very user
2314
+ * `setAuthToken`'s sticky-`subject` contract promises to protect. The queue's
2315
+ * own re-stamp covers both the entry and its durable record.
2164
2316
  */
2165
2317
  private restampQueuedIdentity;
2166
2318
  /**
@@ -2197,7 +2349,15 @@ declare class LunoraClient {
2197
2349
  * nothing), so over-inclusion here is harmless.
2198
2350
  */
2199
2351
  private flushAllOfflineQueues;
2352
+ /**
2353
+ * Replay a shard's queued writes, serialized per shard and published as
2354
+ * {@link offlineFlushes} so a concurrent `mutation()` can wait behind it.
2355
+ * Never rejects: every entry's outcome is settled individually inside
2356
+ * {@link drainOfflineQueue}, and a poisoned chain would strand every later
2357
+ * flush AND every write waiting on the barrier.
2358
+ */
2200
2359
  private flushOfflineQueue;
2360
+ private drainOfflineQueue;
2201
2361
  /**
2202
2362
  * Partition already-gated writes into the encodable ones (returned) and reject
2203
2363
  * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClientOptions, a as Unsubscribe, U as User, W as WsTokenProvider, S as ShardTrafficResult, b as ScheduleRecord, c as SchedulerStatus, d as WorkflowInstanceStatus, e as WorkflowInstancePage, f as WorkflowInstanceDetail, g as WorkflowInstanceAction, h as FunctionDescriptor, i as StorageListPage, G as GlobalTableInfo, j as GlobalFilterClause, k as GlobalTablePage, l as GlobalFacetResult, H as HttpStreamRef, m as HttpStreamArgsOf, n as HttpStreamChunkOf } from "./types.d-BOB755CV.js";
1
+ import { F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClientOptions, a as Unsubscribe, U as User, W as WsTokenProvider, S as ShardTrafficResult, b as ScheduleRecord, c as SchedulerStatus, d as WorkflowInstanceStatus, e as WorkflowInstancePage, f as WorkflowInstanceDetail, g as WorkflowInstanceAction, h as FunctionDescriptor, i as StorageListPage, G as GlobalTableInfo, j as GlobalFilterClause, k as GlobalTablePage, l as GlobalFacetResult, H as HttpStreamRef, m as HttpStreamArgsOf, n as HttpStreamChunkOf } from "./types.d-Bsj5mGrj.mjs";
2
2
  import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
3
3
  /**
4
4
  * Reactive key-value store for local-only client state.
@@ -208,6 +208,14 @@ declare class SubscriptionRegistry {
208
208
  add(state: SubscriptionState): void;
209
209
  remove(state: SubscriptionState): void;
210
210
  all(): SubscriptionState[];
211
+ /**
212
+ * Drop every registration. Terminal — used by `LunoraClient.close()`, whose
213
+ * whole point is to release the callback closures each {@link SubscriptionState}
214
+ * holds (`callbacks`, `errorCallbacks`, `checkpointCallbacks` — React state
215
+ * setters and `@lunora/db` collection closures), which otherwise outlive the
216
+ * closed client for as long as the client object is reachable.
217
+ */
218
+ clear(): void;
211
219
  }
212
220
  /**
213
221
  * Read/write handle over the client's live query cache, handed to a mutation's
@@ -694,6 +702,31 @@ declare class LunoraClient {
694
702
  */
695
703
  private readonly streams;
696
704
  /** Live shape subscriptions (partial replication), keyed by their wire id. */
705
+ /**
706
+ * Teardown callbacks for the admin sockets {@link LunoraClient.subscribeScheduledJobs}
707
+ * opens. Those run their own reconnect loop off a closure-local `closed`
708
+ * flag rather than `this.closed` (they predate `ensureSocket`'s guard), so
709
+ * without this registry a `close()` left every one of them reconnecting on
710
+ * its backoff forever — re-minting an ephemeral admin sub-token on each
711
+ * attempt when a `WsTokenProvider` is wired.
712
+ */
713
+ private readonly adminSocketTeardowns;
714
+ /**
715
+ * The in-flight offline-queue replay per shard (`connectionKey`), while one
716
+ * is running. Two jobs:
717
+ *
718
+ * 1. It serializes overlapping flushes for the same shard — two reconnect
719
+ * events in quick succession used to drain and replay concurrently.
720
+ * 2. It is the barrier {@link LunoraClient.mutation} waits on before sending a FRESH
721
+ * write directly. The socket's `open` handler flips `wsState` to `"open"`
722
+ * first and calls the flush last, so from that instant `mutation()`'s
723
+ * offline gate is false and a new write raced straight to `/rpc` against
724
+ * the replay of the older, queued write for the same document — the newer
725
+ * one could land first and then be overwritten by the older. Ordering
726
+ * inside the replay (`replaySequential`) never covered this, because the
727
+ * racing write was never in the queue.
728
+ */
729
+ private readonly offlineFlushes;
697
730
  private readonly shapeSubscriptions;
698
731
  /**
699
732
  * In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by
@@ -813,6 +846,10 @@ declare class LunoraClient {
813
846
  * Stored per shard and replayed on every (re)connect. When a socket for the
814
847
  * shard is already open, a fresh `connect` envelope is sent immediately so the
815
848
  * server sees the new context without waiting for a reconnect.
849
+ *
850
+ * Not available on a `crossTabSync` FOLLOWER tab — the context rides the
851
+ * `connect` envelope of a socket a follower does not own, so it would be
852
+ * stored and never sent. Throws `NOT_IMPLEMENTED` there.
816
853
  */
817
854
  setConnectionContext(context: Record<string, unknown> | undefined, options?: {
818
855
  shardKey?: string;
@@ -852,6 +889,11 @@ declare class LunoraClient {
852
889
  * do not put data on a whisper topic that some shard members shouldn't see,
853
890
  * and don't trust a whisper's `data` as authorization. Use a query/mutation
854
891
  * (with RLS) for anything privileged; whispers are for transient awareness.
892
+ *
893
+ * Not available on a `crossTabSync` FOLLOWER tab — whisper frames are not
894
+ * relayed over the cross-tab channel, so this throws `NOT_IMPLEMENTED`
895
+ * there rather than registering a handler nothing can ever reach. See
896
+ * {@link LunoraClientOptions.crossTabSync}.
855
897
  */
856
898
  whisperSubscribe(topic: string, handler: (data: unknown, from?: string) => void, options?: {
857
899
  shardKey?: string;
@@ -863,6 +905,11 @@ declare class LunoraClient {
863
905
  * queued), and the server silently drops it if the sender exceeds its
864
906
  * whisper rate budget. The sender never receives its own whisper. Omitting
865
907
  * `data` delivers JSON `null` to receivers (not `undefined`).
908
+ *
909
+ * That best-effort drop is for a socket that is momentarily down. A
910
+ * `crossTabSync` FOLLOWER tab has no socket and never will (see
911
+ * {@link LunoraClientOptions.crossTabSync}), so every whisper from it would
912
+ * be dropped forever — it throws `NOT_IMPLEMENTED` instead.
866
913
  */
867
914
  whisper(topic: string, data?: unknown, options?: {
868
915
  shardKey?: string;
@@ -1069,6 +1116,22 @@ declare class LunoraClient {
1069
1116
  * Mutations issued before the very first WS connect to a shard fail fast.
1070
1117
  * Opt into queueing-before-first-connect via
1071
1118
  * `OfflineQueueOptions.queueBeforeFirstConnect`.
1119
+ *
1120
+ * **Return-value caveat — the queued paths do not carry the server's result.**
1121
+ * The declared `Promise<ReturnOf<F>>` only holds when the write goes straight
1122
+ * to the server. Once a write is queued:
1123
+ *
1124
+ * - with a durable `outbox` configured, this resolves **immediately with
1125
+ * `undefined`** (typed as `ReturnOf<F>`) the moment the write is handed to
1126
+ * the outbox — the replay happens later, out of band, with no awaiter;
1127
+ * - with the built-in offline queue, it stays pending until the replay lands
1128
+ * and then resolves with the replayed call's value.
1129
+ *
1130
+ * So `const id = await client.mutation(api.todos.create, …)` is `undefined`
1131
+ * for every write issued while offline under an outbox. Generate ids
1132
+ * client-side (or read them back from a subscription) rather than depending
1133
+ * on a mutation's return value in an offline-capable app —
1134
+ * {@link LunoraClient.importRows} documents the same caveat for its counts.
1072
1135
  */
1073
1136
  mutation<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>): Promise<ReturnOf<F>>;
1074
1137
  action<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: ActionCallOptions): Promise<ReturnOf<F>>;
@@ -1309,6 +1372,11 @@ declare class LunoraClient {
1309
1372
  * `DELETE /_lunora/admin/storage?key=…` endpoint — the worker must be built
1310
1373
  * with a `storageDelete` function and `adminToken`. Powers the studio file
1311
1374
  * browser's per-row delete; resolves `{ deleted, key }`.
1375
+ *
1376
+ * An absent `deleted` field reads as `false`, matching every sibling admin
1377
+ * verb (`runCronJob`'s `ran`, …): the studio renders this value as the row's
1378
+ * outcome, so defaulting a missing field to success would report a delete
1379
+ * that a mismatched/older worker never performed.
1312
1380
  */
1313
1381
  deleteStorageObject(key: string, options?: {
1314
1382
  bucket?: string;
@@ -1700,6 +1768,20 @@ declare class LunoraClient {
1700
1768
  offset?: number;
1701
1769
  userId?: string;
1702
1770
  }): Promise<AuthPage<AuthSession>>;
1771
+ /**
1772
+ * Subscribe to a live query. The callback fires with the current value (from
1773
+ * the durable read cache, when one is hydrated) and again on every server
1774
+ * frame; the returned function unsubscribes.
1775
+ *
1776
+ * Subscriptions are deduped by `(functionPath, args, shardKey)` — a second
1777
+ * `subscribe` for the same triple joins the existing registration and shares
1778
+ * its value, cursor and optimistic layers.
1779
+ *
1780
+ * Not available on a `crossTabSync` FOLLOWER tab: the cross-tab channel only
1781
+ * carries the LEADER's own subscriptions outward, so a follower would receive
1782
+ * this query's frames only if the leader happened to hold it too. Throws
1783
+ * `NOT_IMPLEMENTED` there — see {@link LunoraClientOptions.crossTabSync}.
1784
+ */
1703
1785
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
1704
1786
  onCheckpoint?: (watermark: SyncWatermark) => void;
1705
1787
  onError?: SubscriptionErrorCallback;
@@ -1716,6 +1798,11 @@ declare class LunoraClient {
1716
1798
  * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
1717
1799
  * (name, args): the server resolves them under the socket's verified identity,
1718
1800
  * so every call gets its own id + view. The returned function unsubscribes.
1801
+ *
1802
+ * Not available on a `crossTabSync` FOLLOWER tab: shape pokes are not part of
1803
+ * the leader→follower broadcast set, so a follower's shape could never
1804
+ * resolve. Throws `NOT_IMPLEMENTED` there — see
1805
+ * {@link LunoraClientOptions.crossTabSync}.
1719
1806
  */
1720
1807
  subscribeShape(shape: {
1721
1808
  args?: Record<string, unknown>;
@@ -1770,6 +1857,53 @@ declare class LunoraClient {
1770
1857
  close(): void;
1771
1858
  /** Guard shared by every public entry point: a closed client accepts no further calls. */
1772
1859
  private assertOpen;
1860
+ /**
1861
+ * `true` when this tab is a cross-tab FOLLOWER of a live leader — i.e. it
1862
+ * will not open a socket of its own and another tab is known to hold one.
1863
+ *
1864
+ * Deliberately NOT just `!isLeader()`. Every `crossTabSync` client is a
1865
+ * non-leader for the first `leaderTimeout` of its life, while its
1866
+ * claim-leadership probe is outstanding; a lone tab self-promotes at the end
1867
+ * of that window and `onBecomeLeader` opens the sockets and replays every
1868
+ * registered subscription. That window is a legitimate, self-healing defer,
1869
+ * not a failure. A KNOWN leader on another tab is the state that never heals.
1870
+ */
1871
+ private followsAnotherTab;
1872
+ /**
1873
+ * Reject a call that needs a socket this tab will never have.
1874
+ *
1875
+ * The cross-tab protocol is one-directional: a leader broadcasts
1876
+ * `subscription-data` / `-error` / `-settled` / `connection-status` to
1877
+ * followers, and a follower has no frame with which to tell the leader what
1878
+ * it needs (see `cross-tab.ts`'s `WsFollowerMessage`, which is exactly
1879
+ * heartbeat / claim-leadership / yield-leadership). `subscribeShape` /
1880
+ * `whisper*` / `setConnectionContext` / `acquireConnectionContext` — none of
1881
+ * which the leader broadcasts at all — therefore never worked on a follower
1882
+ * under any circumstances: each returned a handle that looked live, fired no
1883
+ * callback, raised no error, and reported `connectionStatus() ===
1884
+ * "connected"` (mirrored from the leader).
1885
+ *
1886
+ * `subscribe` is deliberately NOT in that set. A follower's `subscribe`
1887
+ * registers the key the leader's broadcast is matched against, so it is the
1888
+ * mechanism the relay is built on rather than a surface that silently fails.
1889
+ * A follower sees a query only while the leader holds the same
1890
+ * `(fn, args, shardKey)` — that is the documented shape of the option, not a
1891
+ * defect.
1892
+ *
1893
+ * `stream()` reaches the same outcome by a different route: it fails the
1894
+ * handle it returns rather than throwing at the call. See
1895
+ * {@link LunoraClientOptions.crossTabSync} for what the option does and does
1896
+ * not cover.
1897
+ */
1898
+ private assertLeaderOwnedSurface;
1899
+ /**
1900
+ * Clear every timer a {@link ShardConnection} can have armed.
1901
+ *
1902
+ * One function because both teardown paths must clear all three and a fourth
1903
+ * timer would otherwise have to be remembered in two places — which is how a
1904
+ * leak gets added rather than written.
1905
+ */
1906
+ private clearConnectionTimers;
1773
1907
  /**
1774
1908
  * Tear down one {@link ShardConnection}'s live state: clear its reconnect/
1775
1909
  * connect timers, stop its heartbeat, and close its socket (if any).
@@ -1777,6 +1911,15 @@ declare class LunoraClient {
1777
1911
  * handler (demoted, but still alive) so a demoted leader can't leak a
1778
1912
  * pending `reconnectTimer` or an open socket's `heartbeatTimer` the way
1779
1913
  * an inline `conn.socket?.close()` — which skips both — used to.
1914
+ *
1915
+ * Settles this shard's in-flight streams first. The teardown clears
1916
+ * `conn.socket` BEFORE the real `close` event fires, so that event trips
1917
+ * `openManagedSocket`'s identity guard (`conn.socket !== socket`) and
1918
+ * returns — meaning `handleDisconnect`, the only other place that settles a
1919
+ * shard's streams, never runs for this connection again. `close()` already
1920
+ * failed and cleared `this.streams` before it gets here, so this is a no-op
1921
+ * on that path; the cross-tab demotion path is the one where a consumer's
1922
+ * `for await` used to block forever with no error and no completion.
1780
1923
  */
1781
1924
  private teardownConnection;
1782
1925
  /**
@@ -2155,12 +2298,21 @@ declare class LunoraClient {
2155
2298
  */
2156
2299
  private rejectQueuedForIdentityChange;
2157
2300
  /**
2158
- * Migrate every live identity stamp from `from` to `to` — used when the auth
2301
+ * Migrate every identity stamp from `from` to `to` — used when the auth
2159
2302
  * identity label changes but the underlying credential (token) does NOT, e.g.
2160
2303
  * the user id resolves a tick after the token was set. The in-memory
2161
2304
  * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
2162
2305
  * keeps the in-flight writes replayable under the new (more stable) identity
2163
2306
  * instead of the flush guard discarding them as a mismatch.
2307
+ *
2308
+ * That map alone was not enough: it is consumed and DELETED on the first
2309
+ * flush attempt (`passesReplayIdentityGate`), while the queue entry and its
2310
+ * persisted record keep the original stamp. So a reload, or a requeue after a
2311
+ * transient failure, fell back to the old token hash — and once the token had
2312
+ * been refreshed, `isSameCredentialUnderTokenHash` no longer recognised it
2313
+ * and the write was rejected `OFFLINE_IDENTITY_CHANGED` for the very user
2314
+ * `setAuthToken`'s sticky-`subject` contract promises to protect. The queue's
2315
+ * own re-stamp covers both the entry and its durable record.
2164
2316
  */
2165
2317
  private restampQueuedIdentity;
2166
2318
  /**
@@ -2197,7 +2349,15 @@ declare class LunoraClient {
2197
2349
  * nothing), so over-inclusion here is harmless.
2198
2350
  */
2199
2351
  private flushAllOfflineQueues;
2352
+ /**
2353
+ * Replay a shard's queued writes, serialized per shard and published as
2354
+ * {@link offlineFlushes} so a concurrent `mutation()` can wait behind it.
2355
+ * Never rejects: every entry's outcome is settled individually inside
2356
+ * {@link drainOfflineQueue}, and a poisoned chain would strand every later
2357
+ * flush AND every write waiting on the barrier.
2358
+ */
2200
2359
  private flushOfflineQueue;
2360
+ private drainOfflineQueue;
2201
2361
  /**
2202
2362
  * Partition already-gated writes into the encodable ones (returned) and reject
2203
2363
  * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
@@ -0,0 +1 @@
1
+ let h=0;const a=()=>{if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();h+=1;const r=typeof crypto<"u"&&typeof crypto.getRandomValues=="function"?Array.from(crypto.getRandomValues(new Uint8Array(8)),t=>t.toString(16).padStart(2,"0")).join(""):Math.random().toString(16).slice(2,12);return`id_${Date.now().toString(36)}_${h.toString(36)}_${r}`},f=(r,t)=>r!==void 0&&t!==r,u=a,o=(r,t,e,i)=>{if(r){r({error:e,mutationId:i,operation:t});return}console.warn(`[lunora] offline-queue persistence ${t} failed`,e)};class m{queueBeforeFirstConnect;maxItems;onPersistenceError;persistence;onEvict;onSizeChange;version;items=[];constructor(t={},e={}){this.maxItems=t.maxItems??1e3,this.queueBeforeFirstConnect=t.queueBeforeFirstConnect??!1,this.onPersistenceError=t.onPersistenceError,this.persistence=e.persistence,this.onEvict=e.onEvict,this.onSizeChange=e.onSizeChange,this.version=e.version}get size(){return this.items.length}enqueue(t){const e=t;e.id??=u(),this.items.push(e),this.persistence?.append({args:e.args,clientId:e.clientId,functionPath:e.functionPath,id:e.id,identity:e.identity,shardKey:e.shardKey,...this.version===void 0?{}:{version:this.version}}).catch(i=>{o(this.onPersistenceError,"append",i,e.id)}),this.evictOverflow(),this.notifySize()}async hydrate(){if(!this.persistence)return[];let t;try{t=await this.persistence.load()}catch(s){throw o(this.onPersistenceError,"load",s),s}const e=[],i=new Set(this.items.map(s=>s.id));for(const s of t)if(!i.has(s.id)){if(f(this.version,s.version)){this.persistence.remove(s.id).catch(d=>{o(this.onPersistenceError,"remove",d,s.id)});continue}i.add(s.id),e.push({args:s.args,clientId:s.clientId,functionPath:s.functionPath,id:s.id,identity:s.identity,reject:()=>{},resolve:()=>{},shardKey:s.shardKey})}this.items.unshift(...e),this.evictOverflow(),this.notifySize();const n=new Set(this.items),c=new Set;for(const s of e)n.has(s)&&c.add(s.shardKey);return[...c]}restampIdentity(t,e){for(const i of this.items){if(i.identity!==t)continue;i.identity=e;const{id:n}=i;if(!this.persistence||n===void 0)continue;const c={args:i.args,clientId:i.clientId,functionPath:i.functionPath,id:n,identity:e,shardKey:i.shardKey,...this.version===void 0?{}:{version:this.version}};this.persistence.remove(n).then(async()=>this.persistence?.append(c)).catch(s=>{o(this.onPersistenceError,"append",s,n)})}}drain(t){if(!t){const n=[...this.items];return this.items.length=0,this.notifySize(),n}const e=[],i=[];for(const n of this.items)(t(n)?e:i).push(n);return this.items.length=0,this.items.push(...i),this.notifySize(),e}requeue(t){t.length!==0&&(this.items.unshift(...t),this.notifySize())}drainConflict(){const t=[],e=[];for(const i of this.items)if(i.precondition!==void 0&&!i.precondition()){const n=new Error("offline mutation skipped: precondition failed before replay");n.code="OFFLINE_PRECONDITION_FAILED",i.reject(n),t.push(i)}else e.push(i);return t.length>0&&(this.items.length=0,this.items.push(...e),this.notifySize()),t}clear(){for(const t of this.items){const e=new Error("CLIENT_CLOSED");e.code="CLIENT_CLOSED",t.reject(e)}this.items.length=0,this.notifySize()}evictOverflow(){for(;this.items.length>this.maxItems;){const t=this.items.shift();if(t){t.id&&this.persistence?.remove(t.id).catch(i=>{o(this.onPersistenceError,"remove",i,t.id)});const e=new Error("offline queue overflow");e.code="OFFLINE_QUEUE_OVERFLOW",t.reject(e),this.onEvict?.(t,e)}}}notifySize(){this.onSizeChange?.(this.items.length)}}export{m as O,f as i,u as n,o as r};
@@ -1,5 +1,5 @@
1
- import { L as LunoraClient } from "./lunora-client.d-DAbmOzH_.mjs";
2
- import { F as FunctionReference, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./types.d-BOB755CV.mjs";
1
+ import { L as LunoraClient } from "./lunora-client.d-DafpbiD5.mjs";
2
+ import { F as FunctionReference, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./types.d-Bsj5mGrj.mjs";
3
3
  /**
4
4
  * Run a query once on the server (during SSR) and capture its result in a
5
5
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -1,5 +1,5 @@
1
- import { L as LunoraClient } from "./lunora-client.d-Dz6Yx4By.js";
2
- import { F as FunctionReference, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./types.d-BOB755CV.js";
1
+ import { L as LunoraClient } from "./lunora-client.d-BQ0ig3ih.js";
2
+ import { F as FunctionReference, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./types.d-Bsj5mGrj.js";
3
3
  /**
4
4
  * Run a query once on the server (during SSR) and capture its result in a
5
5
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -322,13 +322,38 @@ interface LunoraClientOptions {
322
322
  */
323
323
  connectTimeoutMs?: number;
324
324
  /**
325
- * When `true`, tabs sharing the same origin coordinate via BroadcastChannel
326
- * so only one tab (the "leader") opens WebSocket connections to the server.
327
- * Follower tabs receive subscription data through the channel instead.
325
+ * When `true`, tabs sharing the same origin (and the same signed-in identity)
326
+ * coordinate via BroadcastChannel so only one tab the "leader" opens
327
+ * WebSocket connections to the server. Reduces simultaneous WS connections,
328
+ * bandwidth, and cross-tab state drift. Requires `BroadcastChannel`
329
+ * (browser-only); silently ignored otherwise. Defaults to `false`.
328
330
  *
329
- * Reduces simultaneous WS connections, bandwidth, and cross-tab state drift.
330
- * Requires `BroadcastChannel` (browser-only); silently ignored otherwise.
331
- * Defaults to `false`.
331
+ * **The channel is one-directional: leader follower.** The leader
332
+ * broadcasts the values, errors, checkpoints and connection status of the
333
+ * subscriptions *it* holds; there is no frame with which a follower can ask
334
+ * the leader for anything.
335
+ *
336
+ * `subscribe` works on a follower and is how the relay delivers: the
337
+ * registration is what the leader's broadcast key is matched against, so a
338
+ * follower sees a value while the leader independently holds the same
339
+ * `(fn, args, shardKey)`.
340
+ *
341
+ * Nothing else is served, because the leader broadcasts nothing for it and a
342
+ * follower cannot ask. `subscribeShape` and `acquireConnectionContext` are
343
+ * inert on a follower — framework code (`@lunora/db`'s shape sync, every
344
+ * `usePresence` adapter) calls them from an effect the app cannot opt out
345
+ * of, so throwing would unwind the tab rather than degrade one feature.
346
+ * `whisper`, `whisperSubscribe`, `setConnectionContext` and `stream` are
347
+ * only ever called by app code, which can handle a failure, so those throw
348
+ * `NOT_IMPLEMENTED` rather than returning a handle that never fires. (The
349
+ * brief window every tab spends claiming leadership at startup is not a
350
+ * follower state: a lone tab self-promotes and its registered subscriptions
351
+ * are sent then.) HTTP surfaces — `query`, `mutation`, `action`, the offline
352
+ * queue's replay — are unaffected on every tab.
353
+ *
354
+ * So: enable this when your tabs run the SAME app views over plain
355
+ * `subscribe`, and leave it off if tabs can sit on different routes or you
356
+ * use shapes, whispers, streams, or connection context.
332
357
  */
333
358
  crossTabSync?: boolean;
334
359
  fetch?: typeof fetch;
@@ -322,13 +322,38 @@ interface LunoraClientOptions {
322
322
  */
323
323
  connectTimeoutMs?: number;
324
324
  /**
325
- * When `true`, tabs sharing the same origin coordinate via BroadcastChannel
326
- * so only one tab (the "leader") opens WebSocket connections to the server.
327
- * Follower tabs receive subscription data through the channel instead.
325
+ * When `true`, tabs sharing the same origin (and the same signed-in identity)
326
+ * coordinate via BroadcastChannel so only one tab the "leader" opens
327
+ * WebSocket connections to the server. Reduces simultaneous WS connections,
328
+ * bandwidth, and cross-tab state drift. Requires `BroadcastChannel`
329
+ * (browser-only); silently ignored otherwise. Defaults to `false`.
328
330
  *
329
- * Reduces simultaneous WS connections, bandwidth, and cross-tab state drift.
330
- * Requires `BroadcastChannel` (browser-only); silently ignored otherwise.
331
- * Defaults to `false`.
331
+ * **The channel is one-directional: leader follower.** The leader
332
+ * broadcasts the values, errors, checkpoints and connection status of the
333
+ * subscriptions *it* holds; there is no frame with which a follower can ask
334
+ * the leader for anything.
335
+ *
336
+ * `subscribe` works on a follower and is how the relay delivers: the
337
+ * registration is what the leader's broadcast key is matched against, so a
338
+ * follower sees a value while the leader independently holds the same
339
+ * `(fn, args, shardKey)`.
340
+ *
341
+ * Nothing else is served, because the leader broadcasts nothing for it and a
342
+ * follower cannot ask. `subscribeShape` and `acquireConnectionContext` are
343
+ * inert on a follower — framework code (`@lunora/db`'s shape sync, every
344
+ * `usePresence` adapter) calls them from an effect the app cannot opt out
345
+ * of, so throwing would unwind the tab rather than degrade one feature.
346
+ * `whisper`, `whisperSubscribe`, `setConnectionContext` and `stream` are
347
+ * only ever called by app code, which can handle a failure, so those throw
348
+ * `NOT_IMPLEMENTED` rather than returning a handle that never fires. (The
349
+ * brief window every tab spends claiming leadership at startup is not a
350
+ * follower state: a lone tab self-promotes and its registered subscriptions
351
+ * are sent then.) HTTP surfaces — `query`, `mutation`, `action`, the offline
352
+ * queue's replay — are unaffected on every tab.
353
+ *
354
+ * So: enable this when your tabs run the SAME app views over plain
355
+ * `subscribe`, and leave it off if tabs can sit on different routes or you
356
+ * use shapes, whispers, streams, or connection context.
332
357
  */
333
358
  crossTabSync?: boolean;
334
359
  fetch?: typeof fetch;