@lunora/client 1.0.0-alpha.246 → 1.0.0-alpha.248

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 (30) hide show
  1. package/dist/auth/index.d.mts +1 -1
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/index.d.mts +10 -6
  4. package/dist/index.d.ts +10 -6
  5. package/dist/index.mjs +1 -1
  6. package/dist/packem_shared/LunoraClient-C-auNTDs.mjs +1 -0
  7. package/dist/packem_shared/OfflineQueue-Ba2qgZK1.mjs +1 -0
  8. package/dist/packem_shared/SubscriptionRegistry-BKgQtuh9.mjs +1 -0
  9. package/dist/packem_shared/createLocalStore-CsKA0HPB.mjs +1 -0
  10. package/dist/packem_shared/{createServerClient-Bf9d0dZR.mjs → createServerClient-kd9cUs8g.mjs} +1 -1
  11. package/dist/packem_shared/createSnapshotPrecondition-BXP1NPBW.mjs +1 -0
  12. package/dist/packem_shared/{local-store-fqWV2n_Z.mjs → local-store-C3bw2MPY.mjs} +1 -1
  13. package/dist/packem_shared/{lunora-client.d-D04PhNYt.d.ts → lunora-client.d-7WaMRZUK.d.ts} +144 -4
  14. package/dist/packem_shared/{lunora-client.d-CPKUKr2s.d.mts → lunora-client.d-DtTKDhtw.d.mts} +144 -4
  15. package/dist/packem_shared/offline-queue-BEqCPDmw.mjs +1 -0
  16. package/dist/packem_shared/{preload.d-C-JKibMy.d.ts → preload.d-B26QlpOB.d.ts} +1 -1
  17. package/dist/packem_shared/{preload.d-DXDa9d_p.d.mts → preload.d-CT_9YaL9.d.mts} +1 -1
  18. package/dist/packem_shared/snapshot-precondition-BEtcSbWr.mjs +1 -0
  19. package/dist/query/index.d.mts +2 -2
  20. package/dist/query/index.d.ts +2 -2
  21. package/dist/ssr/index.d.mts +2 -2
  22. package/dist/ssr/index.d.ts +2 -2
  23. package/dist/ssr/index.mjs +1 -1
  24. package/package.json +1 -1
  25. package/dist/packem_shared/LunoraClient-CFCQg1yB.mjs +0 -1
  26. package/dist/packem_shared/OfflineQueue-BLRESpAe.mjs +0 -1
  27. package/dist/packem_shared/SubscriptionRegistry-DKIXE7M7.mjs +0 -1
  28. package/dist/packem_shared/createLocalStore-BT0QPKdf.mjs +0 -1
  29. package/dist/packem_shared/createSnapshotPrecondition-CLkxVeN1.mjs +0 -1
  30. package/dist/packem_shared/offline-queue-Dt_um8sR.mjs +0 -1
@@ -382,6 +382,14 @@ interface LunoraClientOptions {
382
382
  * `/api/auth` (matching `@lunora/auth`'s `DEFAULT_AUTH_BASE_PATH`).
383
383
  */
384
384
  authBasePath?: string;
385
+ /**
386
+ * Whether the `WebSocket` transport sends the `setAuthToken` bearer on the
387
+ * upgrade, as `@lunora/react-native` does on native. Then a
388
+ * sign-in from signed-out reconnects the shard sockets so the server sees the
389
+ * bearer. A browser socket cannot carry it, so the default `false` leaves its
390
+ * sockets (and their live values) alone.
391
+ */
392
+ authTokenOnUpgrade?: boolean;
385
393
  bookmarkStorage?: BookmarkStorage;
386
394
  /**
387
395
  * Stable per-client id backing the custom-mutator watermark. Sent on the
@@ -1320,8 +1328,13 @@ interface OptimisticLayer {
1320
1328
  readonly transform: (current: unknown) => unknown;
1321
1329
  }
1322
1330
  interface SubscriptionState {
1323
- /** True once the server has acked the subscription on the current socket. */
1324
- acked: boolean;
1331
+ /**
1332
+ * Where this subscription's `subscribe` frame stands on the current socket:
1333
+ * none outstanding (`"idle"`), sent and not yet answered (`"inflight"`, so a
1334
+ * second subscriber joins it instead of sending another), or acknowledged by
1335
+ * the server (`"acked"`). Written only through {@link setSubscriptionAck}.
1336
+ */
1337
+ ack: "acked" | "idle" | "inflight";
1325
1338
  readonly args: Record<string, unknown>;
1326
1339
  /**
1327
1340
  * Stable wire-key of `args` (`stableWireKey`), computed once at subscribe
@@ -1929,6 +1942,8 @@ declare class LunoraClient {
1929
1942
  private readonly pollingFallbackAfterFailedAttempts;
1930
1943
  /** Polling-fallback cadence (ms); `0` disables it. See {@link LunoraClientOptions.pollingFallback}. */
1931
1944
  private readonly pollingFallbackIntervalMs;
1945
+ /** See {@link LunoraClientOptions.authTokenOnUpgrade}. */
1946
+ private readonly authTokenOnUpgrade;
1932
1947
  private readonly offlineQueue;
1933
1948
  /**
1934
1949
  * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
@@ -2080,6 +2095,8 @@ declare class LunoraClient {
2080
2095
  */
2081
2096
  private readonly connectionContextHolders;
2082
2097
  private authToken;
2098
+ /** Hash of the current bearer, keyed on the token so a new token misses once. `setAuthToken` drops it on every change, so a replaced or signed-out bearer is not retained. */
2099
+ private tokenFingerprint;
2083
2100
  /**
2084
2101
  * Optional STABLE identity subject (a user id), the basis of the offline-queue
2085
2102
  * identity stamp when supplied. Keeps a same-user token *refresh* from looking
@@ -2273,6 +2290,27 @@ declare class LunoraClient {
2273
2290
  * racing write was never in the queue.
2274
2291
  */
2275
2292
  private readonly offlineFlushes;
2293
+ /**
2294
+ * Subscriptions a socket `open` un-answered that no value-bearing frame has
2295
+ * answered since. What they hold is the previous socket's value, or a blank
2296
+ * after a cold reconnect, and the server acks a `subscribe` before it seeds
2297
+ * it, so neither `lastValue` nor `ack` says the value is current. A queued
2298
+ * write whose snapshot precondition reads one of them is not evaluated yet:
2299
+ * see {@link LunoraClient.snapshotAnswer}. On a follower the open is the
2300
+ * leader's, see {@link LunoraClient.awaitLeaderAnswers}.
2301
+ */
2302
+ private readonly awaitingAnswer;
2303
+ /**
2304
+ * Subscriptions whose wait ended without a value to read: the seed was
2305
+ * refused or undecodable, or the resubscribe was given up on. Still acked and
2306
+ * blank after a cold reconnect, so a snapshot comparison would read the blank
2307
+ * as a change; a guard over one of them is no evidence either way.
2308
+ */
2309
+ private readonly answeredUnreadable;
2310
+ /** Connection keys whose last flush left a write waiting on {@link LunoraClient.awaitingAnswer}; an answer there flushes again. */
2311
+ private readonly answerHeldShards;
2312
+ /** Gives up on the leader relaying an answer, one per subscription, see {@link LunoraClient.awaitLeaderAnswer}. */
2313
+ private readonly leaderAnswerTimers;
2276
2314
  /**
2277
2315
  * Per-shard replay backoff, keyed by {@link connectionKey} exactly as the
2278
2316
  * flushes and their timers are: `delayMs` is the longest hint the CURRENT
@@ -2328,7 +2366,10 @@ declare class LunoraClient {
2328
2366
  * Does NOT update the WebSocket auth — a socket keeps the session credential
2329
2367
  * it connected with for its life. To refresh live WS auth, call
2330
2368
  * {@link setWsToken} explicitly, which closes existing shard sockets to
2331
- * force a reconnect with the new credential.
2369
+ * force a reconnect with the new credential. With
2370
+ * {@link LunoraClientOptions.authTokenOnUpgrade} (React Native's
2371
+ * `Authorization` header), a sign-in from signed-out (a token where there was
2372
+ * none) reconnects the sockets, cold, so the upgrade presents it.
2332
2373
  */
2333
2374
  setAuthToken(token: string | null, subject?: string | null): void;
2334
2375
  getAuthToken(): string | null;
@@ -2849,6 +2890,25 @@ declare class LunoraClient {
2849
2890
  * `new globalThis.WebSocket(url)` would silently bypass.
2850
2891
  */
2851
2892
  getWebSocketImpl(): typeof WebSocket | undefined;
2893
+ /**
2894
+ * `true` while {@link LunoraClientOptions.crossTabSync} is on and this tab
2895
+ * is not the elected leader, including the election window before it knows
2896
+ * whether another tab leads. Such a tab holds no socket: its subscriptions
2897
+ * see only the frames the leader relays for queries the leader holds too,
2898
+ * so a read that must resolve goes over HTTP instead of waiting on a push.
2899
+ */
2900
+ isCrossTabFollower(): boolean;
2901
+ /**
2902
+ * Whether a live query on `shardKey` can expect its value to be pushed: by a
2903
+ * socket that is open or still on its first connect attempt, or by the
2904
+ * polling fallback. `false` once a connect attempt has failed to reach `open`
2905
+ * since the socket last did and polling is not delivering instead, where a
2906
+ * read that must resolve should go over HTTP rather than wait on a push. A
2907
+ * shard with no connection yet expects one: subscribing opens it.
2908
+ */
2909
+ expectsPush(options?: {
2910
+ shardKey?: string;
2911
+ }): boolean;
2852
2912
  /**
2853
2913
  * Read the current value for a {@link ClientQueryRef}. Returns
2854
2914
  * `ref.defaultValue` when no value has been explicitly set.
@@ -2931,7 +2991,9 @@ declare class LunoraClient {
2931
2991
  * whether one exists at all rather than just its value. `present` is `false`
2932
2992
  * when no subscription is open for the given `(functionPath, args, shardKey)`;
2933
2993
  * `value` is the subscription's `lastValue`, which includes any optimistic
2934
- * overlay.
2994
+ * overlay. `answered` is `false` while an open subscription holds no answer
2995
+ * (never answered, or blanked by a cold reconnect), when `value` is absence
2996
+ * rather than evidence.
2935
2997
  *
2936
2998
  * The two are separate because a caller like the snapshot precondition has to
2937
2999
  * tell "no subscription is active, so this read knows nothing" apart from
@@ -2944,6 +3006,7 @@ declare class LunoraClient {
2944
3006
  * in-memory state of an already-opened subscription.
2945
3007
  */
2946
3008
  peekActiveQuerySnapshot(functionPath: string, args: Record<string, unknown>, shardKey?: string): {
3009
+ answered: boolean;
2947
3010
  present: boolean;
2948
3011
  value: unknown;
2949
3012
  };
@@ -3790,6 +3853,29 @@ declare class LunoraClient {
3790
3853
  signal?: AbortSignal;
3791
3854
  }): StreamIterable<HttpStreamChunkOf<Ref>>;
3792
3855
  close(): void;
3856
+ /**
3857
+ * A genuine credential change: reject the queued writes that can no longer
3858
+ * replay as the incoming identity, and evict the session the previous one
3859
+ * left live. `true` when it evicted, which reconnects every socket cold.
3860
+ */
3861
+ private retirePreviousIdentity;
3862
+ /**
3863
+ * How the open sockets must follow a bearer change when the transport
3864
+ * presents the bearer on the upgrade (React Native's `Authorization`
3865
+ * header). An open upgrade keeps the token it was made with for its whole
3866
+ * life, and a browser socket never carries it, so without
3867
+ * `authTokenOnUpgrade` nothing reconnects.
3868
+ *
3869
+ * A sign-in from signed-out is `"cold"`: the sockets upgraded anonymously,
3870
+ * and no view may resume from an anonymous cursor. A refreshed bearer under
3871
+ * an unchanged identity is `"warm"`: the subscriptions resume from their
3872
+ * cursors.
3873
+ *
3874
+ * A sign-out or an account switch leaves a previous identity behind and is
3875
+ * evicted by `setAuthToken` before this is asked.
3876
+ */
3877
+ private reconcileUpgradeBearer;
3878
+ private applyUpgradeBearer;
3793
3879
  /** Guard shared by every public entry point: a closed client accepts no further calls. */
3794
3880
  private assertOpen;
3795
3881
  /**
@@ -4379,6 +4465,11 @@ declare class LunoraClient {
4379
4465
  * shard still waking, a swallowed message) would otherwise leave the
4380
4466
  * subscription un-acked on a perfectly live socket until the next reconnect.
4381
4467
  * One retry, not a loop: a query that is merely slow must not be re-run repeatedly.
4468
+ *
4469
+ * The retry gets one ack window of its own (`final`), after which the frame is
4470
+ * stood down so a later `subscribe()` can send again; otherwise a lost retry
4471
+ * refuses every send until the next reconnect. Kept in `unackedWatchdogs`, so
4472
+ * an ack or a teardown cancels either phase.
4382
4473
  */
4383
4474
  private armUnackedWatchdog;
4384
4475
  private sendShapeSubscribeIfOpen;
@@ -4864,6 +4955,14 @@ declare class LunoraClient {
4864
4955
  private restartTabCoordinatorForIdentity;
4865
4956
  /** Drop every room send held before the socket opened: they were made under the identity that is now going away. */
4866
4957
  private resetRoomSends;
4958
+ /**
4959
+ * Blank every live subscription and shape, forget their resume cursors, and
4960
+ * reconnect every socket, so each resubscribe is a cold one the server
4961
+ * re-seeds for the identity the new socket carries. Used when the socket's
4962
+ * credential changes: resuming from a cursor taken under the previous one
4963
+ * would splice a diff onto rows that identity was never shown.
4964
+ */
4965
+ private reconnectCold;
4867
4966
  /**
4868
4967
  * Close every open shard socket so each reconnects carrying the credential
4869
4968
  * in effect now.
@@ -4894,6 +4993,47 @@ declare class LunoraClient {
4894
4993
  * flush AND every write waiting on the barrier.
4895
4994
  */
4896
4995
  private flushOfflineQueue;
4996
+ /**
4997
+ * Where a snapshot precondition's subscription stands on its socket, when
4998
+ * comparing would mislead: `"awaiting"` while a reconnect has un-answered it
4999
+ * and no value has answered since (the comparison would judge the previous
5000
+ * socket's value, or a blank), `"unreadable"` once that wait ended with no
5001
+ * value to read. `undefined` for any other precondition or subscription.
5002
+ */
5003
+ private snapshotAnswer;
5004
+ /**
5005
+ * The precondition a queued write carries. A snapshot precondition reads as
5006
+ * holding while its subscription is awaited, so `drainConflict` keeps the
5007
+ * write rather than drop it on a blank the server acked but has not seeded
5008
+ * yet (the drain then holds it back, {@link drainOfflineQueue}), and once the
5009
+ * wait ended unreadable, which is no evidence the value changed.
5010
+ */
5011
+ private holdWhileUnanswered;
5012
+ /**
5013
+ * A follower's counterpart of a socket open in {@link markShardPendingAck}: every
5014
+ * subscription awaits the leader's next relay. A follower cannot ask the leader for
5015
+ * one, and the leader relays only the queries it holds itself, so the wait is bounded
5016
+ * by the leader's own (an ack window and one retry) and ends unreadable, as the
5017
+ * leader's does when it gives up.
5018
+ */
5019
+ private awaitLeaderAnswers;
5020
+ /**
5021
+ * Holds `state` for the leader's next relay. Its timer is its own, so settling
5022
+ * one subscription early never releases another.
5023
+ */
5024
+ private awaitLeaderAnswer;
5025
+ /**
5026
+ * `state` has been answered on its socket (`readable`), or will not be: release
5027
+ * any write held for it. Not called for an `ack`, which the server sends
5028
+ * before the seed.
5029
+ */
5030
+ private settleAwaitedAnswer;
5031
+ /**
5032
+ * Put back a drained write whose snapshot cannot be compared yet, and every
5033
+ * write behind it (replaying those first would reorder the shard), recording
5034
+ * the hold so the answer flushes again. Returns the writes that may go on.
5035
+ */
5036
+ private holdUndecidedWrites;
4897
5037
  private drainOfflineQueue;
4898
5038
  /**
4899
5039
  * Ask for another flush of this shard after a backoff, because every entry it
@@ -382,6 +382,14 @@ interface LunoraClientOptions {
382
382
  * `/api/auth` (matching `@lunora/auth`'s `DEFAULT_AUTH_BASE_PATH`).
383
383
  */
384
384
  authBasePath?: string;
385
+ /**
386
+ * Whether the `WebSocket` transport sends the `setAuthToken` bearer on the
387
+ * upgrade, as `@lunora/react-native` does on native. Then a
388
+ * sign-in from signed-out reconnects the shard sockets so the server sees the
389
+ * bearer. A browser socket cannot carry it, so the default `false` leaves its
390
+ * sockets (and their live values) alone.
391
+ */
392
+ authTokenOnUpgrade?: boolean;
385
393
  bookmarkStorage?: BookmarkStorage;
386
394
  /**
387
395
  * Stable per-client id backing the custom-mutator watermark. Sent on the
@@ -1320,8 +1328,13 @@ interface OptimisticLayer {
1320
1328
  readonly transform: (current: unknown) => unknown;
1321
1329
  }
1322
1330
  interface SubscriptionState {
1323
- /** True once the server has acked the subscription on the current socket. */
1324
- acked: boolean;
1331
+ /**
1332
+ * Where this subscription's `subscribe` frame stands on the current socket:
1333
+ * none outstanding (`"idle"`), sent and not yet answered (`"inflight"`, so a
1334
+ * second subscriber joins it instead of sending another), or acknowledged by
1335
+ * the server (`"acked"`). Written only through {@link setSubscriptionAck}.
1336
+ */
1337
+ ack: "acked" | "idle" | "inflight";
1325
1338
  readonly args: Record<string, unknown>;
1326
1339
  /**
1327
1340
  * Stable wire-key of `args` (`stableWireKey`), computed once at subscribe
@@ -1929,6 +1942,8 @@ declare class LunoraClient {
1929
1942
  private readonly pollingFallbackAfterFailedAttempts;
1930
1943
  /** Polling-fallback cadence (ms); `0` disables it. See {@link LunoraClientOptions.pollingFallback}. */
1931
1944
  private readonly pollingFallbackIntervalMs;
1945
+ /** See {@link LunoraClientOptions.authTokenOnUpgrade}. */
1946
+ private readonly authTokenOnUpgrade;
1932
1947
  private readonly offlineQueue;
1933
1948
  /**
1934
1949
  * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
@@ -2080,6 +2095,8 @@ declare class LunoraClient {
2080
2095
  */
2081
2096
  private readonly connectionContextHolders;
2082
2097
  private authToken;
2098
+ /** Hash of the current bearer, keyed on the token so a new token misses once. `setAuthToken` drops it on every change, so a replaced or signed-out bearer is not retained. */
2099
+ private tokenFingerprint;
2083
2100
  /**
2084
2101
  * Optional STABLE identity subject (a user id), the basis of the offline-queue
2085
2102
  * identity stamp when supplied. Keeps a same-user token *refresh* from looking
@@ -2273,6 +2290,27 @@ declare class LunoraClient {
2273
2290
  * racing write was never in the queue.
2274
2291
  */
2275
2292
  private readonly offlineFlushes;
2293
+ /**
2294
+ * Subscriptions a socket `open` un-answered that no value-bearing frame has
2295
+ * answered since. What they hold is the previous socket's value, or a blank
2296
+ * after a cold reconnect, and the server acks a `subscribe` before it seeds
2297
+ * it, so neither `lastValue` nor `ack` says the value is current. A queued
2298
+ * write whose snapshot precondition reads one of them is not evaluated yet:
2299
+ * see {@link LunoraClient.snapshotAnswer}. On a follower the open is the
2300
+ * leader's, see {@link LunoraClient.awaitLeaderAnswers}.
2301
+ */
2302
+ private readonly awaitingAnswer;
2303
+ /**
2304
+ * Subscriptions whose wait ended without a value to read: the seed was
2305
+ * refused or undecodable, or the resubscribe was given up on. Still acked and
2306
+ * blank after a cold reconnect, so a snapshot comparison would read the blank
2307
+ * as a change; a guard over one of them is no evidence either way.
2308
+ */
2309
+ private readonly answeredUnreadable;
2310
+ /** Connection keys whose last flush left a write waiting on {@link LunoraClient.awaitingAnswer}; an answer there flushes again. */
2311
+ private readonly answerHeldShards;
2312
+ /** Gives up on the leader relaying an answer, one per subscription, see {@link LunoraClient.awaitLeaderAnswer}. */
2313
+ private readonly leaderAnswerTimers;
2276
2314
  /**
2277
2315
  * Per-shard replay backoff, keyed by {@link connectionKey} exactly as the
2278
2316
  * flushes and their timers are: `delayMs` is the longest hint the CURRENT
@@ -2328,7 +2366,10 @@ declare class LunoraClient {
2328
2366
  * Does NOT update the WebSocket auth — a socket keeps the session credential
2329
2367
  * it connected with for its life. To refresh live WS auth, call
2330
2368
  * {@link setWsToken} explicitly, which closes existing shard sockets to
2331
- * force a reconnect with the new credential.
2369
+ * force a reconnect with the new credential. With
2370
+ * {@link LunoraClientOptions.authTokenOnUpgrade} (React Native's
2371
+ * `Authorization` header), a sign-in from signed-out (a token where there was
2372
+ * none) reconnects the sockets, cold, so the upgrade presents it.
2332
2373
  */
2333
2374
  setAuthToken(token: string | null, subject?: string | null): void;
2334
2375
  getAuthToken(): string | null;
@@ -2849,6 +2890,25 @@ declare class LunoraClient {
2849
2890
  * `new globalThis.WebSocket(url)` would silently bypass.
2850
2891
  */
2851
2892
  getWebSocketImpl(): typeof WebSocket | undefined;
2893
+ /**
2894
+ * `true` while {@link LunoraClientOptions.crossTabSync} is on and this tab
2895
+ * is not the elected leader, including the election window before it knows
2896
+ * whether another tab leads. Such a tab holds no socket: its subscriptions
2897
+ * see only the frames the leader relays for queries the leader holds too,
2898
+ * so a read that must resolve goes over HTTP instead of waiting on a push.
2899
+ */
2900
+ isCrossTabFollower(): boolean;
2901
+ /**
2902
+ * Whether a live query on `shardKey` can expect its value to be pushed: by a
2903
+ * socket that is open or still on its first connect attempt, or by the
2904
+ * polling fallback. `false` once a connect attempt has failed to reach `open`
2905
+ * since the socket last did and polling is not delivering instead, where a
2906
+ * read that must resolve should go over HTTP rather than wait on a push. A
2907
+ * shard with no connection yet expects one: subscribing opens it.
2908
+ */
2909
+ expectsPush(options?: {
2910
+ shardKey?: string;
2911
+ }): boolean;
2852
2912
  /**
2853
2913
  * Read the current value for a {@link ClientQueryRef}. Returns
2854
2914
  * `ref.defaultValue` when no value has been explicitly set.
@@ -2931,7 +2991,9 @@ declare class LunoraClient {
2931
2991
  * whether one exists at all rather than just its value. `present` is `false`
2932
2992
  * when no subscription is open for the given `(functionPath, args, shardKey)`;
2933
2993
  * `value` is the subscription's `lastValue`, which includes any optimistic
2934
- * overlay.
2994
+ * overlay. `answered` is `false` while an open subscription holds no answer
2995
+ * (never answered, or blanked by a cold reconnect), when `value` is absence
2996
+ * rather than evidence.
2935
2997
  *
2936
2998
  * The two are separate because a caller like the snapshot precondition has to
2937
2999
  * tell "no subscription is active, so this read knows nothing" apart from
@@ -2944,6 +3006,7 @@ declare class LunoraClient {
2944
3006
  * in-memory state of an already-opened subscription.
2945
3007
  */
2946
3008
  peekActiveQuerySnapshot(functionPath: string, args: Record<string, unknown>, shardKey?: string): {
3009
+ answered: boolean;
2947
3010
  present: boolean;
2948
3011
  value: unknown;
2949
3012
  };
@@ -3790,6 +3853,29 @@ declare class LunoraClient {
3790
3853
  signal?: AbortSignal;
3791
3854
  }): StreamIterable<HttpStreamChunkOf<Ref>>;
3792
3855
  close(): void;
3856
+ /**
3857
+ * A genuine credential change: reject the queued writes that can no longer
3858
+ * replay as the incoming identity, and evict the session the previous one
3859
+ * left live. `true` when it evicted, which reconnects every socket cold.
3860
+ */
3861
+ private retirePreviousIdentity;
3862
+ /**
3863
+ * How the open sockets must follow a bearer change when the transport
3864
+ * presents the bearer on the upgrade (React Native's `Authorization`
3865
+ * header). An open upgrade keeps the token it was made with for its whole
3866
+ * life, and a browser socket never carries it, so without
3867
+ * `authTokenOnUpgrade` nothing reconnects.
3868
+ *
3869
+ * A sign-in from signed-out is `"cold"`: the sockets upgraded anonymously,
3870
+ * and no view may resume from an anonymous cursor. A refreshed bearer under
3871
+ * an unchanged identity is `"warm"`: the subscriptions resume from their
3872
+ * cursors.
3873
+ *
3874
+ * A sign-out or an account switch leaves a previous identity behind and is
3875
+ * evicted by `setAuthToken` before this is asked.
3876
+ */
3877
+ private reconcileUpgradeBearer;
3878
+ private applyUpgradeBearer;
3793
3879
  /** Guard shared by every public entry point: a closed client accepts no further calls. */
3794
3880
  private assertOpen;
3795
3881
  /**
@@ -4379,6 +4465,11 @@ declare class LunoraClient {
4379
4465
  * shard still waking, a swallowed message) would otherwise leave the
4380
4466
  * subscription un-acked on a perfectly live socket until the next reconnect.
4381
4467
  * One retry, not a loop: a query that is merely slow must not be re-run repeatedly.
4468
+ *
4469
+ * The retry gets one ack window of its own (`final`), after which the frame is
4470
+ * stood down so a later `subscribe()` can send again; otherwise a lost retry
4471
+ * refuses every send until the next reconnect. Kept in `unackedWatchdogs`, so
4472
+ * an ack or a teardown cancels either phase.
4382
4473
  */
4383
4474
  private armUnackedWatchdog;
4384
4475
  private sendShapeSubscribeIfOpen;
@@ -4864,6 +4955,14 @@ declare class LunoraClient {
4864
4955
  private restartTabCoordinatorForIdentity;
4865
4956
  /** Drop every room send held before the socket opened: they were made under the identity that is now going away. */
4866
4957
  private resetRoomSends;
4958
+ /**
4959
+ * Blank every live subscription and shape, forget their resume cursors, and
4960
+ * reconnect every socket, so each resubscribe is a cold one the server
4961
+ * re-seeds for the identity the new socket carries. Used when the socket's
4962
+ * credential changes: resuming from a cursor taken under the previous one
4963
+ * would splice a diff onto rows that identity was never shown.
4964
+ */
4965
+ private reconnectCold;
4867
4966
  /**
4868
4967
  * Close every open shard socket so each reconnects carrying the credential
4869
4968
  * in effect now.
@@ -4894,6 +4993,47 @@ declare class LunoraClient {
4894
4993
  * flush AND every write waiting on the barrier.
4895
4994
  */
4896
4995
  private flushOfflineQueue;
4996
+ /**
4997
+ * Where a snapshot precondition's subscription stands on its socket, when
4998
+ * comparing would mislead: `"awaiting"` while a reconnect has un-answered it
4999
+ * and no value has answered since (the comparison would judge the previous
5000
+ * socket's value, or a blank), `"unreadable"` once that wait ended with no
5001
+ * value to read. `undefined` for any other precondition or subscription.
5002
+ */
5003
+ private snapshotAnswer;
5004
+ /**
5005
+ * The precondition a queued write carries. A snapshot precondition reads as
5006
+ * holding while its subscription is awaited, so `drainConflict` keeps the
5007
+ * write rather than drop it on a blank the server acked but has not seeded
5008
+ * yet (the drain then holds it back, {@link drainOfflineQueue}), and once the
5009
+ * wait ended unreadable, which is no evidence the value changed.
5010
+ */
5011
+ private holdWhileUnanswered;
5012
+ /**
5013
+ * A follower's counterpart of a socket open in {@link markShardPendingAck}: every
5014
+ * subscription awaits the leader's next relay. A follower cannot ask the leader for
5015
+ * one, and the leader relays only the queries it holds itself, so the wait is bounded
5016
+ * by the leader's own (an ack window and one retry) and ends unreadable, as the
5017
+ * leader's does when it gives up.
5018
+ */
5019
+ private awaitLeaderAnswers;
5020
+ /**
5021
+ * Holds `state` for the leader's next relay. Its timer is its own, so settling
5022
+ * one subscription early never releases another.
5023
+ */
5024
+ private awaitLeaderAnswer;
5025
+ /**
5026
+ * `state` has been answered on its socket (`readable`), or will not be: release
5027
+ * any write held for it. Not called for an `ack`, which the server sends
5028
+ * before the seed.
5029
+ */
5030
+ private settleAwaitedAnswer;
5031
+ /**
5032
+ * Put back a drained write whose snapshot cannot be compared yet, and every
5033
+ * write behind it (replaying those first would reorder the shard), recording
5034
+ * the hold so the answer flushes again. Returns the writes that may go on.
5035
+ */
5036
+ private holdUndecidedWrites;
4897
5037
  private drainOfflineQueue;
4898
5038
  /**
4899
5039
  * Ask for another flush of this shard after a backoff, because every entry it
@@ -0,0 +1 @@
1
+ let a=0;const d=()=>{if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();a+=1;const o=typeof crypto<"u"&&typeof crypto.getRandomValues=="function"?Array.from(crypto.getRandomValues(new Uint8Array(8)),e=>e.toString(16).padStart(2,"0")).join(""):Math.random().toString(16).slice(2,12);return`id_${Date.now().toString(36)}_${a.toString(36)}_${o}`},l=(o,e)=>o!==void 0&&e!==o,f=d,h=(o,e,t,i)=>{if(o)try{o({error:t,mutationId:i,operation:e});return}catch{}console.warn(`[lunora] offline-queue persistence ${e} failed`,t)};class p{queueBeforeFirstConnect;maxItems;onPersistenceError;persistence;onEvict;onSizeChange;onReplayingChange;version;items=[];replayingIds=new Set;appending=new Map;unsettled=new Map;unpersisted=new Set;constructor(e={},t={}){this.maxItems=e.maxItems??1e3,this.queueBeforeFirstConnect=e.queueBeforeFirstConnect??!1,this.onPersistenceError=e.onPersistenceError,this.persistence=t.persistence,this.onEvict=t.onEvict,this.onSizeChange=t.onSizeChange,this.onReplayingChange=t.onReplayingChange,this.version=t.version}get size(){return this.items.length}get replaying(){return this.replayingIds.size}enqueue(e){const t=e;t.id??=f();let i=this.items.length;for(;t.issueSeq!==void 0&&i>0&&(this.items[i-1]?.issueSeq??-1)>t.issueSeq;)i-=1;this.items.splice(i,0,t),this.unsettled.set(t.id,t),this.persist(this.recordOf(t,t.id)),this.evictOverflow(),this.notifySize()}discard(e){this.unsettled.delete(e),this.unpersisted.delete(e),this.replayingIds.delete(e)&&this.onReplayingChange?.();const{persistence:t}=this;t&&(this.appending.get(e)??Promise.resolve()).then(async()=>t.remove(e)).catch(i=>{h(this.onPersistenceError,"remove",i,e)})}flushUnpersisted(){for(const e of this.unpersisted){const t=this.unsettled.get(e);t&&!this.appending.has(e)&&this.persist(this.recordOf(t,e))}}async hydrate(){if(!this.persistence)return[];let e;try{e=await this.persistence.load()}catch(s){throw h(this.onPersistenceError,"load",s),s}const t=[],i=new Set(this.items.map(s=>s.id));for(const s of e)if(!i.has(s.id)){if(l(this.version,s.version)){this.persistence.remove(s.id).catch(c=>{h(this.onPersistenceError,"remove",c,s.id)});continue}i.add(s.id),t.push({args:s.args,baselineSeq:s.baselineSeq,clientId:s.clientId,functionPath:s.functionPath,id:s.id,identity:s.identity,reject:()=>{},resolve:()=>{},shardKey:s.shardKey})}this.items.unshift(...t),this.evictOverflow(),this.notifySize();const n=new Set(this.items),r=new Set;for(const s of t)n.has(s)&&r.add(s.shardKey);return[...r]}restampIdentity(e,t){for(const i of this.items){if(i.identity!==e)continue;i.identity=t;const{id:n}=i;!this.persistence||n===void 0||this.rewriteStamp(n,this.recordOf(i,n)).catch(r=>{h(this.onPersistenceError,"replace",r,n)})}}hasPending(e){return this.items.some(t=>e(t))}drain(e){if(!e){const n=[...this.items];return this.items.length=0,this.markReplaying(n),this.notifySize(),n}const t=[],i=[];for(const n of this.items)(e(n)?t:i).push(n);return this.items.length=0,this.items.push(...i),this.markReplaying(t),this.notifySize(),t}requeue(e){if(e.length!==0){for(const t of e)t.id!==void 0&&this.replayingIds.delete(t.id);this.items.unshift(...e),this.notifySize()}}drainConflict(){const e=[],t=[];for(const i of this.items){let n=!0,r;if(i.precondition!==void 0)try{n=i.precondition()}catch(s){n=!1,r=s}if(n)t.push(i);else{const s=new Error("offline mutation skipped: precondition failed before replay",r===void 0?void 0:{cause:r});s.code="OFFLINE_PRECONDITION_FAILED",i.reject(s),e.push(i)}}return e.length>0&&(this.items.length=0,this.items.push(...t),this.notifySize()),e}clear(){for(const e of new Set([...this.items,...this.unsettled.values()])){const t=new Error("CLIENT_CLOSED"),i=e.id??"",n=s=>{t.code="CLIENT_CLOSED",t.persisted=s,e.reject(t)},r=this.appending.get(i);this.replayingIds.has(i)?n(!1):r===void 0?n(this.persistence!==void 0&&!this.unpersisted.has(i)):r.catch(()=>!1).then(n).catch(()=>{})}this.items.length=0,this.replayingIds.clear(),this.unsettled.clear(),this.unpersisted.clear(),this.notifySize()}recordOf(e,t){return{args:e.args,...e.baselineSeq===void 0?{}:{baselineSeq:e.baselineSeq},clientId:e.clientId,functionPath:e.functionPath,id:t,identity:e.identity,shardKey:e.shardKey,...this.version===void 0?{}:{version:this.version}}}persist(e){const{persistence:t}=this;if(!t)return;const i=(async()=>{try{return await t.append(e),this.unpersisted.delete(e.id),!0}catch(n){return this.unsettled.has(e.id)&&this.unpersisted.add(e.id),h(this.onPersistenceError,"append",n,e.id),!1}})();this.appending.set(e.id,i),i.finally(()=>{this.appending.get(e.id)===i&&this.appending.delete(e.id)}).catch(()=>{})}async rewriteStamp(e,t){const i=this.persistence;if(i)try{await this.appending.get(e),await i.replace(t)}catch(n){h(this.onPersistenceError,"replace",n,e)}}evictOverflow(){for(;this.items.length>this.maxItems;){const e=this.items.shift();if(e){e.id&&this.discard(e.id);const t=new Error("offline queue overflow");t.code="OFFLINE_QUEUE_OVERFLOW",e.reject(t),this.onEvict?.(e,t)}}}markReplaying(e){for(const t of e)t.id!==void 0&&this.replayingIds.add(t.id)}notifySize(){this.onSizeChange?.(this.items.length)}}export{p as O,l as i,f as n,h as r};
@@ -1,4 +1,4 @@
1
- import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-D04PhNYt.js";
1
+ import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-7WaMRZUK.js";
2
2
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.js";
3
3
  /**
4
4
  * Run a query once on the server (during SSR) and capture its result in a
@@ -1,4 +1,4 @@
1
- import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-CPKUKr2s.mjs";
1
+ import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-DtTKDhtw.mjs";
2
2
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.mjs";
3
3
  /**
4
4
  * Run a query once on the server (during SSR) and capture its result in a
@@ -0,0 +1 @@
1
+ import{stableWireKey as p}from"@lunora/wire";const u=new WeakMap,i=(e,t)=>{u.set(e,t)},d=e=>e===void 0?void 0:u.get(e),h=(e,t,s,n)=>{const o=e.peekActiveQuerySnapshot(t.__lunoraRef,s,n),a=o.present&&o.answered?p(o.value):void 0,c=()=>{const r=e.peekActiveQuerySnapshot(t.__lunoraRef,s,n);return!o.present||!o.answered||!r.present||!r.answered?!0:p(r.value)===a};return a!==void 0&&i(c,{args:s,functionPath:t.__lunoraRef,shardKey:n}),c};export{h as c,i as r,d as s};
@@ -1,4 +1,4 @@
1
- import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-CPKUKr2s.mjs";
1
+ import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-DtTKDhtw.mjs";
2
2
  export type {
3
3
  /**
4
4
  * `@lunora/client/query` — the framework-neutral live-query state machine shared
@@ -10,7 +10,7 @@ export type {
10
10
  * framework — each adapter supplies thin sinks that write into its own ref /
11
11
  * store / signal / cache.
12
12
  */
13
- b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-CPKUKr2s.mjs";
13
+ b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-DtTKDhtw.mjs";
14
14
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "../packem_shared/function-reference.d-Br_hsKje.mjs";
15
15
  import '@lunora/errors';
16
16
  import '@lunora/runtime';
@@ -1,4 +1,4 @@
1
- import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-D04PhNYt.js";
1
+ import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-7WaMRZUK.js";
2
2
  export type {
3
3
  /**
4
4
  * `@lunora/client/query` — the framework-neutral live-query state machine shared
@@ -10,7 +10,7 @@ export type {
10
10
  * framework — each adapter supplies thin sinks that write into its own ref /
11
11
  * store / signal / cache.
12
12
  */
13
- b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-D04PhNYt.js";
13
+ b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-7WaMRZUK.js";
14
14
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "../packem_shared/function-reference.d-Br_hsKje.js";
15
15
  import '@lunora/errors';
16
16
  import '@lunora/runtime';
@@ -1,4 +1,4 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-CPKUKr2s.mjs";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-DtTKDhtw.mjs";
2
2
  export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/function-reference.d-Br_hsKje.mjs";
3
3
  export {
4
4
  /**
@@ -20,7 +20,7 @@ p as preloadQuery,
20
20
  * WebSocket or touches a UI framework, so it is safe to import from any SSR
21
21
  * loader.
22
22
  */
23
- a as preloadedQueryResult } from "../packem_shared/preload.d-DXDa9d_p.mjs";
23
+ a as preloadedQueryResult } from "../packem_shared/preload.d-CT_9YaL9.mjs";
24
24
  import '@lunora/errors';
25
25
  import '@lunora/runtime';
26
26
  /**
@@ -1,4 +1,4 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-D04PhNYt.js";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-7WaMRZUK.js";
2
2
  export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/function-reference.d-Br_hsKje.js";
3
3
  export {
4
4
  /**
@@ -20,7 +20,7 @@ p as preloadQuery,
20
20
  * WebSocket or touches a UI framework, so it is safe to import from any SSR
21
21
  * loader.
22
22
  */
23
- a as preloadedQueryResult } from "../packem_shared/preload.d-C-JKibMy.js";
23
+ a as preloadedQueryResult } from "../packem_shared/preload.d-B26QlpOB.js";
24
24
  import '@lunora/errors';
25
25
  import '@lunora/runtime';
26
26
  /**
@@ -1 +1 @@
1
- import{getServerSession as o}from"../packem_shared/getServerSession-BaOoo1p6.mjs";import{deserializePreloaded as l,serializePreloaded as t}from"../packem_shared/deserializePreloaded-CyFK_cv6.mjs";import{createServerClient as i}from"../packem_shared/createServerClient-Bf9d0dZR.mjs";import{preloadQuery as s,preloadedQueryResult as f}from"../packem_shared/preloadQuery-uFy24PCR.mjs";export{i as createServerClient,l as deserializePreloaded,o as getServerSession,s as preloadQuery,f as preloadedQueryResult,t as serializePreloaded};
1
+ import{getServerSession as o}from"../packem_shared/getServerSession-BaOoo1p6.mjs";import{deserializePreloaded as l,serializePreloaded as t}from"../packem_shared/deserializePreloaded-CyFK_cv6.mjs";import{createServerClient as i}from"../packem_shared/createServerClient-kd9cUs8g.mjs";import{preloadQuery as s,preloadedQueryResult as f}from"../packem_shared/preloadQuery-uFy24PCR.mjs";export{i as createServerClient,l as deserializePreloaded,o as getServerSession,s as preloadQuery,f as preloadedQueryResult,t as serializePreloaded};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/client",
3
- "version": "1.0.0-alpha.246",
3
+ "version": "1.0.0-alpha.248",
4
4
  "description": "Lunora browser SDK: WebSocket transport, optimistic updates, and an offline mutation queue",
5
5
  "keywords": [
6
6
  "cloudflare",