@lunora/client 1.0.0-alpha.23 → 1.0.0-alpha.24

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.
@@ -1,4 +1,4 @@
1
- import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-JvtVpf8A.mjs";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-B8bdwHLr.mjs";
2
2
  import '@lunora/runtime';
3
3
  interface IdentityStore {
4
4
  /** Read the current resolved user, or `null` when signed out / not yet resolved. */
@@ -1,4 +1,4 @@
1
- import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-JvtVpf8A.js";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-B8bdwHLr.js";
2
2
  import '@lunora/runtime';
3
3
  interface IdentityStore {
4
4
  /** Read the current resolved user, or `null` when signed out / not yet resolved. */
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { c as PersistenceAdapter, B as BookmarkStorage, S as SubscriptionError, H as HttpStreamRef, d as HttpStreamArgsOf, e as HttpStreamChunkOf, f as StreamIterable, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, g as ReconnectOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-JvtVpf8A.mjs";
2
- export { type h as BatchSlot, C as CONFLICT_ERROR_CODE, type i as CachedQuery, type j as ClientMessage, type k as ClientQueryRef, type l as ClientShapeSubscribeMessage, type m as ClientShapeUnsubscribeMessage, type n as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type o as FunctionArgumentDescriptor, type p as FunctionDescriptor, type G as GlobalFacetResult, type q as GlobalFacetValue, type r as GlobalFilterClause, type s as GlobalTableInfo, type t as GlobalTablePage, type u as HttpStreamCallArgs, type v as LunoraClientError, type w as LunoraClientOptions, type x as LunoraErrorCode, type y as MutationSettledEvent, type z as OptimisticLocalStore, type E as OptimisticUpdate, type I as OutboxMutation, type J as OutboxSink, type K as PersistedMutation, type P as Preloaded, type N as RowOp, type T as RpcEnvelope, type V as RpcResponseBody, type W as ScheduleRecord, type X as SchedulerPoolStatus, type Y as SchedulerStatus, type Z as ServerMessage, type _ as ServerPokeEndMessage, type $ as ServerPokePartMessage, type a0 as ServerPokeStartMessage, type a1 as ShardTrafficEntry, type a2 as ShardTrafficResult, type a3 as StorageListPage, type a4 as StorageObject, type a5 as StreamHandle, type a6 as SubscriptionCallback, type b as SubscriptionErrorCallback, a7 as SubscriptionRegistry, type a8 as SubscriptionState, type a9 as SyncWatermark, type a as Unsubscribe, type U as User, type aa as WorkflowInstanceAction, type ab as WorkflowInstanceDetail, type ac as WorkflowInstancePage, type ad as WorkflowInstanceStatus, type ae as WorkflowInstanceSummary, type af as WorkflowStepDetail, type ag as WsTokenProvider, ah as createClientQuery, ai as createLocalStore, aj as createStream, ak as getErrorCode, al as getRetryAfterMs, am as isConflictError, an as isForbiddenError, ao as isRateLimitedError, ap as isUnauthorizedError } from "./packem_shared/lunora-client.d-JvtVpf8A.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-DKbjGN5O.mjs";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, S as SubscriptionError, H as HttpStreamRef, d as HttpStreamArgsOf, e as HttpStreamChunkOf, f as StreamIterable, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, g as ReconnectOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-B8bdwHLr.mjs";
2
+ export { type h as BatchSlot, C as CONFLICT_ERROR_CODE, type i as CachedQuery, type j as ClientMessage, type k as ClientQueryRef, type l as ClientShapeSubscribeMessage, type m as ClientShapeUnsubscribeMessage, type n as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type o as FunctionArgumentDescriptor, type p as FunctionDescriptor, type G as GlobalFacetResult, type q as GlobalFacetValue, type r as GlobalFilterClause, type s as GlobalTableInfo, type t as GlobalTablePage, type u as HttpStreamCallArgs, type v as LunoraClientError, type w as LunoraClientOptions, type x as LunoraErrorCode, type y as MutationSettledEvent, type z as OptimisticLocalStore, type E as OptimisticUpdate, type I as OutboxMutation, type J as OutboxSink, type K as PersistedMutation, type P as Preloaded, type N as RowOp, type T as RpcEnvelope, type V as RpcResponseBody, type W as ScheduleRecord, type X as SchedulerPoolStatus, type Y as SchedulerStatus, type Z as ServerMessage, type _ as ServerPokeEndMessage, type $ as ServerPokePartMessage, type a0 as ServerPokeStartMessage, type a1 as ShardTrafficEntry, type a2 as ShardTrafficResult, type a3 as StorageListPage, type a4 as StorageObject, type a5 as StreamHandle, type a6 as SubscriptionCallback, type b as SubscriptionErrorCallback, a7 as SubscriptionRegistry, type a8 as SubscriptionState, type a9 as SyncWatermark, type a as Unsubscribe, type U as User, type aa as WorkflowInstanceAction, type ab as WorkflowInstanceDetail, type ac as WorkflowInstancePage, type ad as WorkflowInstanceStatus, type ae as WorkflowInstanceSummary, type af as WorkflowStepDetail, type ag as WsTokenProvider, ah as createClientQuery, ai as createLocalStore, aj as createStream, ak as getErrorCode, al as getRetryAfterMs, am as isConflictError, an as isForbiddenError, ao as isRateLimitedError, ap as isUnauthorizedError } from "./packem_shared/lunora-client.d-B8bdwHLr.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-Ccrgw2z0.mjs";
4
4
  export type { AuthCapabilities, AuthConfigInfo, AuthImpersonation, AuthPage, AuthSession, AuthUser, AuthUserFieldSpec, CronJobInfo, KvKeyEntry, KvKeyListResult, KvNamespaceSummary, KvValueResult, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
@@ -115,6 +115,21 @@ declare class TabCoordinator {
115
115
  broadcastSubscriptionError(key: string, error: SubscriptionError): void;
116
116
  private broadcast;
117
117
  private handleMessage;
118
+ private handleClaimLeadership;
119
+ private handleHeartbeat;
120
+ /**
121
+ * Resolve two leaders existing at once — e.g. this tab was backgrounded
122
+ * and its heartbeat/health timers were throttled while a foreground
123
+ * follower's were not, so the follower's `checkLeaderHealth` timed out
124
+ * the (still-alive) leader and self-promoted. `BroadcastChannel` message
125
+ * delivery isn't subject to the same timer-throttling clamp, so even a
126
+ * backgrounded leader eventually observes the pretender's heartbeat here
127
+ * — resolve the split-brain deterministically with the same
128
+ * lexicographically-smaller-tabId rule used at claim-adoption. If the
129
+ * other tab wins, step down; if we win, reassert immediately so the
130
+ * pretender demotes itself the moment it processes our heartbeat.
131
+ */
132
+ private resolveLeaderVsLeaderTieBreak;
118
133
  private becomeLeader;
119
134
  private sendHeartbeat;
120
135
  private checkLeaderHealth;
@@ -403,6 +418,16 @@ declare class OfflineQueue {
403
418
  * gone after a reload). No-op when no persistence adapter is configured.
404
419
  * Returns the distinct shard keys of the restored writes so the caller can
405
420
  * open their sockets to trigger a flush.
421
+ *
422
+ * `hydrate()` runs post-construction (the caller awaits an async durable-store
423
+ * load), so a mutation issued while offline during that boot window is
424
+ * enqueued into `items` *before* this method's `await` resolves. Restored
425
+ * records are therefore `unshift`-ed ahead of whatever is already queued
426
+ * rather than `push`-ed to the end: the durable store's persist order is
427
+ * authoritative (a prior-session write is always older than anything from
428
+ * this session), so replaying a same-session boot-time write before an
429
+ * older restored write on the same document would let last-writer-wins
430
+ * silently clobber the newer data with the stale one.
406
431
  */
407
432
  hydrate(): Promise<(string | undefined)[]>;
408
433
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { c as PersistenceAdapter, B as BookmarkStorage, S as SubscriptionError, H as HttpStreamRef, d as HttpStreamArgsOf, e as HttpStreamChunkOf, f as StreamIterable, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, g as ReconnectOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-JvtVpf8A.js";
2
- export { type h as BatchSlot, C as CONFLICT_ERROR_CODE, type i as CachedQuery, type j as ClientMessage, type k as ClientQueryRef, type l as ClientShapeSubscribeMessage, type m as ClientShapeUnsubscribeMessage, type n as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type o as FunctionArgumentDescriptor, type p as FunctionDescriptor, type G as GlobalFacetResult, type q as GlobalFacetValue, type r as GlobalFilterClause, type s as GlobalTableInfo, type t as GlobalTablePage, type u as HttpStreamCallArgs, type v as LunoraClientError, type w as LunoraClientOptions, type x as LunoraErrorCode, type y as MutationSettledEvent, type z as OptimisticLocalStore, type E as OptimisticUpdate, type I as OutboxMutation, type J as OutboxSink, type K as PersistedMutation, type P as Preloaded, type N as RowOp, type T as RpcEnvelope, type V as RpcResponseBody, type W as ScheduleRecord, type X as SchedulerPoolStatus, type Y as SchedulerStatus, type Z as ServerMessage, type _ as ServerPokeEndMessage, type $ as ServerPokePartMessage, type a0 as ServerPokeStartMessage, type a1 as ShardTrafficEntry, type a2 as ShardTrafficResult, type a3 as StorageListPage, type a4 as StorageObject, type a5 as StreamHandle, type a6 as SubscriptionCallback, type b as SubscriptionErrorCallback, a7 as SubscriptionRegistry, type a8 as SubscriptionState, type a9 as SyncWatermark, type a as Unsubscribe, type U as User, type aa as WorkflowInstanceAction, type ab as WorkflowInstanceDetail, type ac as WorkflowInstancePage, type ad as WorkflowInstanceStatus, type ae as WorkflowInstanceSummary, type af as WorkflowStepDetail, type ag as WsTokenProvider, ah as createClientQuery, ai as createLocalStore, aj as createStream, ak as getErrorCode, al as getRetryAfterMs, am as isConflictError, an as isForbiddenError, ao as isRateLimitedError, ap as isUnauthorizedError } from "./packem_shared/lunora-client.d-JvtVpf8A.js";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-C4_d_l5v.js";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, S as SubscriptionError, H as HttpStreamRef, d as HttpStreamArgsOf, e as HttpStreamChunkOf, f as StreamIterable, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, g as ReconnectOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-B8bdwHLr.js";
2
+ export { type h as BatchSlot, C as CONFLICT_ERROR_CODE, type i as CachedQuery, type j as ClientMessage, type k as ClientQueryRef, type l as ClientShapeSubscribeMessage, type m as ClientShapeUnsubscribeMessage, type n as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type o as FunctionArgumentDescriptor, type p as FunctionDescriptor, type G as GlobalFacetResult, type q as GlobalFacetValue, type r as GlobalFilterClause, type s as GlobalTableInfo, type t as GlobalTablePage, type u as HttpStreamCallArgs, type v as LunoraClientError, type w as LunoraClientOptions, type x as LunoraErrorCode, type y as MutationSettledEvent, type z as OptimisticLocalStore, type E as OptimisticUpdate, type I as OutboxMutation, type J as OutboxSink, type K as PersistedMutation, type P as Preloaded, type N as RowOp, type T as RpcEnvelope, type V as RpcResponseBody, type W as ScheduleRecord, type X as SchedulerPoolStatus, type Y as SchedulerStatus, type Z as ServerMessage, type _ as ServerPokeEndMessage, type $ as ServerPokePartMessage, type a0 as ServerPokeStartMessage, type a1 as ShardTrafficEntry, type a2 as ShardTrafficResult, type a3 as StorageListPage, type a4 as StorageObject, type a5 as StreamHandle, type a6 as SubscriptionCallback, type b as SubscriptionErrorCallback, a7 as SubscriptionRegistry, type a8 as SubscriptionState, type a9 as SyncWatermark, type a as Unsubscribe, type U as User, type aa as WorkflowInstanceAction, type ab as WorkflowInstanceDetail, type ac as WorkflowInstancePage, type ad as WorkflowInstanceStatus, type ae as WorkflowInstanceSummary, type af as WorkflowStepDetail, type ag as WsTokenProvider, ah as createClientQuery, ai as createLocalStore, aj as createStream, ak as getErrorCode, al as getRetryAfterMs, am as isConflictError, an as isForbiddenError, ao as isRateLimitedError, ap as isUnauthorizedError } from "./packem_shared/lunora-client.d-B8bdwHLr.js";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-BPy9qajK.js";
4
4
  export type { AuthCapabilities, AuthConfigInfo, AuthImpersonation, AuthPage, AuthSession, AuthUser, AuthUserFieldSpec, CronJobInfo, KvKeyEntry, KvKeyListResult, KvNamespaceSummary, KvValueResult, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
@@ -115,6 +115,21 @@ declare class TabCoordinator {
115
115
  broadcastSubscriptionError(key: string, error: SubscriptionError): void;
116
116
  private broadcast;
117
117
  private handleMessage;
118
+ private handleClaimLeadership;
119
+ private handleHeartbeat;
120
+ /**
121
+ * Resolve two leaders existing at once — e.g. this tab was backgrounded
122
+ * and its heartbeat/health timers were throttled while a foreground
123
+ * follower's were not, so the follower's `checkLeaderHealth` timed out
124
+ * the (still-alive) leader and self-promoted. `BroadcastChannel` message
125
+ * delivery isn't subject to the same timer-throttling clamp, so even a
126
+ * backgrounded leader eventually observes the pretender's heartbeat here
127
+ * — resolve the split-brain deterministically with the same
128
+ * lexicographically-smaller-tabId rule used at claim-adoption. If the
129
+ * other tab wins, step down; if we win, reassert immediately so the
130
+ * pretender demotes itself the moment it processes our heartbeat.
131
+ */
132
+ private resolveLeaderVsLeaderTieBreak;
118
133
  private becomeLeader;
119
134
  private sendHeartbeat;
120
135
  private checkLeaderHealth;
@@ -403,6 +418,16 @@ declare class OfflineQueue {
403
418
  * gone after a reload). No-op when no persistence adapter is configured.
404
419
  * Returns the distinct shard keys of the restored writes so the caller can
405
420
  * open their sockets to trigger a flush.
421
+ *
422
+ * `hydrate()` runs post-construction (the caller awaits an async durable-store
423
+ * load), so a mutation issued while offline during that boot window is
424
+ * enqueued into `items` *before* this method's `await` resolves. Restored
425
+ * records are therefore `unshift`-ed ahead of whatever is already queued
426
+ * rather than `push`-ed to the end: the durable store's persist order is
427
+ * authoritative (a prior-session write is always older than anything from
428
+ * this session), so replaying a same-session boot-time write before an
429
+ * older restored write on the same document would let last-writer-wins
430
+ * silently clobber the newer data with the stale one.
406
431
  */
407
432
  hydrate(): Promise<(string | undefined)[]>;
408
433
  /**
package/dist/index.mjs CHANGED
@@ -1,15 +1,15 @@
1
1
  export { createAsyncStoragePersistence } from './packem_shared/createAsyncStoragePersistence-1Z5BZ8RC.mjs';
2
2
  export { default as createInMemoryBookmarkStorage } from './packem_shared/createInMemoryBookmarkStorage-BoN7a7TH.mjs';
3
- export { createClientQuery } from './packem_shared/createClientQuery-CQ51bWAE.mjs';
4
- export { TabCoordinator } from './packem_shared/TabCoordinator-BwRR8H06.mjs';
3
+ export { createClientQuery } from './packem_shared/createClientQuery-dJZg1ohm.mjs';
4
+ export { TabCoordinator } from './packem_shared/TabCoordinator-D_5oNTTt.mjs';
5
5
  export { applyDelta, isMutationDelta } from './packem_shared/applyDelta-CRKZ1PBt.mjs';
6
6
  export { CONFLICT_ERROR_CODE, getErrorCode, getRetryAfterMs, isConflictError, isForbiddenError, isRateLimitedError, isUnauthorizedError } from './packem_shared/CONFLICT_ERROR_CODE-B8gQ8tyU.mjs';
7
- export { httpStream } from './packem_shared/httpStream-BJU-aflc.mjs';
7
+ export { httpStream } from './packem_shared/httpStream-DIdL8NEw.mjs';
8
8
  export { c as createLocalStore } from './packem_shared/local-store-DIq-UWfD.mjs';
9
- export { LunoraClient } from './packem_shared/LunoraClient-BBCQjjbl.mjs';
9
+ export { LunoraClient } from './packem_shared/LunoraClient-D3h4P7hg.mjs';
10
10
  export { createMutationRunner } from './packem_shared/createMutationRunner-BqsavzvG.mjs';
11
11
  export { createMutatorRunner } from './packem_shared/createMutatorRunner-BETvCd0p.mjs';
12
- export { O as OfflineQueue } from './packem_shared/offline-queue-CF4_Co5k.mjs';
12
+ export { O as OfflineQueue } from './packem_shared/offline-queue-N-1JvYb4.mjs';
13
13
  export { createInMemoryPersistence, createIndexedDbPersistence } from './packem_shared/createInMemoryPersistence-DZ2VHWgm.mjs';
14
14
  export { preloadQuery, preloadedQueryResult } from './packem_shared/preloadQuery-lobFkD2Z.mjs';
15
15
  export { createInMemoryQueryCache, createIndexedDbQueryCache, queryCacheKey } from './packem_shared/createInMemoryQueryCache-DiaGZkA2.mjs';
@@ -1,12 +1,12 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  import { s as stableWireKey, d as decodeWire, e as encodeWire } from './wire-key-Djie6aaR.mjs';
3
3
  import createInMemoryBookmarkStorage from './createInMemoryBookmarkStorage-BoN7a7TH.mjs';
4
- import { ClientQueryStore } from './createClientQuery-CQ51bWAE.mjs';
5
- import { TabCoordinator } from './TabCoordinator-BwRR8H06.mjs';
4
+ import { ClientQueryStore } from './createClientQuery-dJZg1ohm.mjs';
5
+ import { TabCoordinator } from './TabCoordinator-D_5oNTTt.mjs';
6
6
  import { isMutationDelta, applyDelta } from './applyDelta-CRKZ1PBt.mjs';
7
- import { httpStream } from './httpStream-BJU-aflc.mjs';
7
+ import { httpStream } from './httpStream-DIdL8NEw.mjs';
8
8
  import { f as foldOptimistic, a as applyOptimisticLayer, d as dropConfirmedLayers, n as notifySubscription, c as createLocalStore } from './local-store-DIq-UWfD.mjs';
9
- import { O as OfflineQueue, n as nextId, i as isStaleVersion, r as reportPersistenceError } from './offline-queue-CF4_Co5k.mjs';
9
+ import { O as OfflineQueue, n as nextId, i as isStaleVersion, r as reportPersistenceError } from './offline-queue-N-1JvYb4.mjs';
10
10
  import { resolvePersistenceAdapter } from './createInMemoryPersistence-DZ2VHWgm.mjs';
11
11
  import { resolveQueryCacheAdapter, queryCacheKey } from './createInMemoryQueryCache-DiaGZkA2.mjs';
12
12
  import { createReconnect } from './createReconnect-Di_-oHH7.mjs';
@@ -888,6 +888,20 @@ class LunoraClient {
888
888
  onMutationSettled(listener) {
889
889
  return this.mutationSettledListeners.add(listener);
890
890
  }
891
+ /**
892
+ * The `WebSocket` implementation this client was constructed with (an
893
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
894
+ * one) — `undefined` if neither is available. This is the seam a feature
895
+ * that opens its OWN socket outside the client's multiplexed connection
896
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
897
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
898
+ * constructor to inject the auth-headers factory's credential onto the
899
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
900
+ * `new globalThis.WebSocket(url)` would silently bypass.
901
+ */
902
+ getWebSocketImpl() {
903
+ return this.WebSocketImpl;
904
+ }
891
905
  // --- Client Query (local state) ----------------------------------------
892
906
  /**
893
907
  * Read the current value for a {@link ClientQueryRef}. Returns
@@ -1 +1 @@
1
- export { O as OfflineQueue, n as nextId, r as reportPersistenceError } from './offline-queue-CF4_Co5k.mjs';
1
+ export { O as OfflineQueue, n as nextId, r as reportPersistenceError } from './offline-queue-N-1JvYb4.mjs';
@@ -142,21 +142,11 @@ class TabCoordinator {
142
142
  handleMessage(message) {
143
143
  switch (message.type) {
144
144
  case "claim-leadership": {
145
- if (message.tabId === this.tabId) {
146
- break;
147
- }
148
- if (this.leader) {
149
- this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
150
- break;
151
- }
152
- if (this.knownLeader === void 0 && message.tabId < this.tabId) {
153
- this.knownLeader = message.tabId;
154
- }
145
+ this.handleClaimLeadership(message);
155
146
  break;
156
147
  }
157
148
  case "heartbeat": {
158
- this.lastHeartbeat = message.ts;
159
- this.knownLeader = message.tabId;
149
+ this.handleHeartbeat(message);
160
150
  break;
161
151
  }
162
152
  case "subscription-data": {
@@ -181,6 +171,52 @@ class TabCoordinator {
181
171
  }
182
172
  }
183
173
  }
174
+ handleClaimLeadership(message) {
175
+ if (message.tabId === this.tabId) {
176
+ return;
177
+ }
178
+ if (this.leader) {
179
+ this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
180
+ return;
181
+ }
182
+ if (this.knownLeader === void 0 && message.tabId < this.tabId) {
183
+ this.knownLeader = message.tabId;
184
+ this.lastHeartbeat = message.ts;
185
+ }
186
+ }
187
+ handleHeartbeat(message) {
188
+ if (message.tabId === this.tabId) {
189
+ return;
190
+ }
191
+ if (this.leader) {
192
+ this.resolveLeaderVsLeaderTieBreak(message);
193
+ return;
194
+ }
195
+ this.lastHeartbeat = message.ts;
196
+ this.knownLeader = message.tabId;
197
+ }
198
+ /**
199
+ * Resolve two leaders existing at once — e.g. this tab was backgrounded
200
+ * and its heartbeat/health timers were throttled while a foreground
201
+ * follower's were not, so the follower's `checkLeaderHealth` timed out
202
+ * the (still-alive) leader and self-promoted. `BroadcastChannel` message
203
+ * delivery isn't subject to the same timer-throttling clamp, so even a
204
+ * backgrounded leader eventually observes the pretender's heartbeat here
205
+ * — resolve the split-brain deterministically with the same
206
+ * lexicographically-smaller-tabId rule used at claim-adoption. If the
207
+ * other tab wins, step down; if we win, reassert immediately so the
208
+ * pretender demotes itself the moment it processes our heartbeat.
209
+ */
210
+ resolveLeaderVsLeaderTieBreak(message) {
211
+ if (message.tabId < this.tabId) {
212
+ this.leader = false;
213
+ this.knownLeader = message.tabId;
214
+ this.lastHeartbeat = message.ts;
215
+ this.onStopBeingLeader?.();
216
+ } else {
217
+ this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
218
+ }
219
+ }
184
220
  becomeLeader() {
185
221
  if (this.leader) {
186
222
  return;
@@ -16,11 +16,16 @@ class ClientQueryStore {
16
16
  }
17
17
  /**
18
18
  * Set a new value for `ref` and notify every subscriber. Pass `undefined`
19
- * to reset the slot to `ref.defaultValue`.
19
+ * to reset the slot to `ref.defaultValue` — delegates to {@link reset} so
20
+ * `get` reports the default rather than a stored `undefined`.
20
21
  */
21
22
  set(ref, value) {
23
+ if (value === void 0) {
24
+ this.reset(ref);
25
+ return;
26
+ }
22
27
  this.values.set(ref.key, value);
23
- this.notify(ref.key);
28
+ this.notify(ref.key, value);
24
29
  }
25
30
  /**
26
31
  * Delete the stored value for `ref`, resetting to `ref.defaultValue` and
@@ -28,7 +33,7 @@ class ClientQueryStore {
28
33
  */
29
34
  reset(ref) {
30
35
  this.values.delete(ref.key);
31
- this.notify(ref.key);
36
+ this.notify(ref.key, ref.defaultValue);
32
37
  }
33
38
  /**
34
39
  * Subscribe to changes for `ref`. The callback is NOT invoked on
@@ -49,13 +54,17 @@ class ClientQueryStore {
49
54
  }
50
55
  };
51
56
  }
52
- /** Notify every subscriber of a value change for the given key. */
53
- notify(key) {
57
+ /**
58
+ * Notify every subscriber of a value change for the given key with the
59
+ * resolved value — the caller passes the just-stored value on `set` or
60
+ * `ref.defaultValue` on `reset`, so a subscriber always sees the same value
61
+ * {@link get} would return (never a stale re-read after the store mutation).
62
+ */
63
+ notify(key, value) {
54
64
  const subs = this.subscribers.get(key);
55
65
  if (!subs) {
56
66
  return;
57
67
  }
58
- const value = this.values.get(key);
59
68
  for (const callback of subs) {
60
69
  try {
61
70
  callback(value);
@@ -1,4 +1,4 @@
1
- import { LunoraClient } from './LunoraClient-BBCQjjbl.mjs';
1
+ import { LunoraClient } from './LunoraClient-D3h4P7hg.mjs';
2
2
 
3
3
  const createServerClient = (options) => {
4
4
  const client = new LunoraClient({ fetch: options.fetch, url: options.url });
@@ -104,19 +104,22 @@ const httpStream = (route, args, options = {}) => {
104
104
  }
105
105
  const url = buildHttpStreamUrl(route, args ?? {}, options.baseUrl ?? "");
106
106
  const ac = new AbortController();
107
+ let onExternalAbort;
107
108
  if (options.signal) {
108
109
  if (options.signal.aborted) {
109
110
  ac.abort();
110
111
  } else {
111
- options.signal.addEventListener(
112
- "abort",
113
- () => {
114
- ac.abort();
115
- },
116
- { once: true }
117
- );
112
+ onExternalAbort = () => {
113
+ ac.abort();
114
+ };
115
+ options.signal.addEventListener("abort", onExternalAbort, { once: true });
118
116
  }
119
117
  }
118
+ const detachExternalAbort = () => {
119
+ if (onExternalAbort) {
120
+ options.signal?.removeEventListener("abort", onExternalAbort);
121
+ }
122
+ };
120
123
  const { handle, iterable } = createStream({
121
124
  maxBuffer: options.maxBuffer,
122
125
  onCancel: () => {
@@ -124,24 +127,30 @@ const httpStream = (route, args, options = {}) => {
124
127
  }
125
128
  });
126
129
  (async () => {
127
- const response = await fetchImpl(url, {
128
- headers: { accept: "text/event-stream", ...options.headers },
129
- method: route.method,
130
- signal: ac.signal
131
- });
132
- if (!response.ok) {
133
- handle.fail(
134
- new LunoraError("HTTP_STREAM_STATUS", `httpStream: request failed (status ${response.status.toString()})`, {
135
- status: response.status
136
- })
137
- );
138
- return;
139
- }
140
- if (!response.body) {
141
- handle.fail(new LunoraError("HTTP_STREAM_NO_BODY", "httpStream: response has no body"));
142
- return;
130
+ try {
131
+ const response = await fetchImpl(url, {
132
+ headers: { accept: "text/event-stream", ...options.headers },
133
+ method: route.method,
134
+ signal: ac.signal
135
+ });
136
+ if (!response.ok) {
137
+ await response.body?.cancel().catch(() => {
138
+ });
139
+ handle.fail(
140
+ new LunoraError("HTTP_STREAM_STATUS", `httpStream: request failed (status ${response.status.toString()})`, {
141
+ status: response.status
142
+ })
143
+ );
144
+ return;
145
+ }
146
+ if (!response.body) {
147
+ handle.fail(new LunoraError("HTTP_STREAM_NO_BODY", "httpStream: response has no body"));
148
+ return;
149
+ }
150
+ await pumpSseBody(response.body, handle);
151
+ } finally {
152
+ detachExternalAbort();
143
153
  }
144
- await pumpSseBody(response.body, handle);
145
154
  })().catch((error) => {
146
155
  if (ac.signal.aborted) {
147
156
  handle.complete();
@@ -1724,6 +1724,18 @@ declare class LunoraClient {
1724
1724
  */
1725
1725
  onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
1726
1726
  /**
1727
+ * The `WebSocket` implementation this client was constructed with (an
1728
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
1729
+ * one) — `undefined` if neither is available. This is the seam a feature
1730
+ * that opens its OWN socket outside the client's multiplexed connection
1731
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
1732
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
1733
+ * constructor to inject the auth-headers factory's credential onto the
1734
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
1735
+ * `new globalThis.WebSocket(url)` would silently bypass.
1736
+ */
1737
+ getWebSocketImpl(): typeof WebSocket | undefined;
1738
+ /**
1727
1739
  * Read the current value for a {@link ClientQueryRef}. Returns
1728
1740
  * `ref.defaultValue` when no value has been explicitly set.
1729
1741
  */
@@ -1724,6 +1724,18 @@ declare class LunoraClient {
1724
1724
  */
1725
1725
  onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
1726
1726
  /**
1727
+ * The `WebSocket` implementation this client was constructed with (an
1728
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
1729
+ * one) — `undefined` if neither is available. This is the seam a feature
1730
+ * that opens its OWN socket outside the client's multiplexed connection
1731
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
1732
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
1733
+ * constructor to inject the auth-headers factory's credential onto the
1734
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
1735
+ * `new globalThis.WebSocket(url)` would silently bypass.
1736
+ */
1737
+ getWebSocketImpl(): typeof WebSocket | undefined;
1738
+ /**
1727
1739
  * Read the current value for a {@link ClientQueryRef}. Returns
1728
1740
  * `ref.defaultValue` when no value has been explicitly set.
1729
1741
  */
@@ -79,6 +79,16 @@ class OfflineQueue {
79
79
  * gone after a reload). No-op when no persistence adapter is configured.
80
80
  * Returns the distinct shard keys of the restored writes so the caller can
81
81
  * open their sockets to trigger a flush.
82
+ *
83
+ * `hydrate()` runs post-construction (the caller awaits an async durable-store
84
+ * load), so a mutation issued while offline during that boot window is
85
+ * enqueued into `items` *before* this method's `await` resolves. Restored
86
+ * records are therefore `unshift`-ed ahead of whatever is already queued
87
+ * rather than `push`-ed to the end: the durable store's persist order is
88
+ * authoritative (a prior-session write is always older than anything from
89
+ * this session), so replaying a same-session boot-time write before an
90
+ * older restored write on the same document would let last-writer-wins
91
+ * silently clobber the newer data with the stale one.
82
92
  */
83
93
  async hydrate() {
84
94
  if (!this.persistence) {
@@ -92,8 +102,9 @@ class OfflineQueue {
92
102
  throw error;
93
103
  }
94
104
  const shardKeys = /* @__PURE__ */ new Set();
105
+ const restored = [];
95
106
  for (const mutation of persisted) {
96
- if (this.items.some((item) => item.id === mutation.id)) {
107
+ if (this.items.some((item) => item.id === mutation.id) || restored.some((item) => item.id === mutation.id)) {
97
108
  continue;
98
109
  }
99
110
  if (isStaleVersion(this.version, mutation.version)) {
@@ -102,7 +113,7 @@ class OfflineQueue {
102
113
  });
103
114
  continue;
104
115
  }
105
- this.items.push({
116
+ restored.push({
106
117
  args: mutation.args,
107
118
  functionPath: mutation.functionPath,
108
119
  id: mutation.id,
@@ -113,6 +124,7 @@ class OfflineQueue {
113
124
  });
114
125
  shardKeys.add(mutation.shardKey);
115
126
  }
127
+ this.items.unshift(...restored);
116
128
  this.notifySize();
117
129
  return [...shardKeys];
118
130
  }
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-JvtVpf8A.js";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-B8bdwHLr.js";
2
2
  /**
3
3
  * Run a query once on the server (during SSR) and capture its result in a
4
4
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-JvtVpf8A.mjs";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-B8bdwHLr.mjs";
2
2
  /**
3
3
  * Run a query once on the server (during SSR) and capture its result in a
4
4
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -1,5 +1,5 @@
1
- import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-JvtVpf8A.mjs";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-JvtVpf8A.mjs";
1
+ import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-B8bdwHLr.mjs";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-B8bdwHLr.mjs";
3
3
  import '@lunora/runtime';
4
4
  /**
5
5
  * The sentinel a framework adapter resolves its reactive args to when it wants
@@ -1,5 +1,5 @@
1
- import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-JvtVpf8A.js";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-JvtVpf8A.js";
1
+ import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-B8bdwHLr.js";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-B8bdwHLr.js";
3
3
  import '@lunora/runtime';
4
4
  /**
5
5
  * The sentinel a framework adapter resolves its reactive args to when it wants
@@ -1,6 +1,6 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-JvtVpf8A.mjs";
2
- export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-JvtVpf8A.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-DKbjGN5O.mjs";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-B8bdwHLr.mjs";
2
+ export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-B8bdwHLr.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-Ccrgw2z0.mjs";
4
4
  import '@lunora/runtime';
5
5
  /**
6
6
  * Structural shape of a better-auth `getSession` call's resolved value.
@@ -1,6 +1,6 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-JvtVpf8A.js";
2
- export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-JvtVpf8A.js";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-C4_d_l5v.js";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-B8bdwHLr.js";
2
+ export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-B8bdwHLr.js";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-BPy9qajK.js";
4
4
  import '@lunora/runtime';
5
5
  /**
6
6
  * Structural shape of a better-auth `getSession` call's resolved value.
@@ -1,4 +1,4 @@
1
1
  export { getServerSession } from '../packem_shared/getServerSession-8jXewqxd.mjs';
2
2
  export { deserializePreloaded, serializePreloaded } from '../packem_shared/deserializePreloaded-C0eJTY_W.mjs';
3
- export { createServerClient } from '../packem_shared/createServerClient-CTTAmvMx.mjs';
3
+ export { createServerClient } from '../packem_shared/createServerClient-DzeC2J3A.mjs';
4
4
  export { preloadQuery, preloadedQueryResult } from '../packem_shared/preloadQuery-lobFkD2Z.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/client",
3
- "version": "1.0.0-alpha.23",
3
+ "version": "1.0.0-alpha.24",
4
4
  "description": "Lunora browser SDK: WebSocket transport, optimistic updates, and an offline mutation queue",
5
5
  "keywords": [
6
6
  "cloudflare",