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

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 (28) hide show
  1. package/dist/auth/index.d.mts +10 -10
  2. package/dist/auth/index.d.ts +10 -10
  3. package/dist/index.d.mts +344 -357
  4. package/dist/index.d.ts +344 -357
  5. package/dist/index.mjs +5 -5
  6. package/dist/packem_shared/{LunoraClient-BBCQjjbl.mjs → LunoraClient-D3h4P7hg.mjs} +18 -4
  7. package/dist/packem_shared/{OfflineQueue-B4HUF7rt.mjs → OfflineQueue-BgarnAub.mjs} +1 -1
  8. package/dist/packem_shared/{TabCoordinator-BwRR8H06.mjs → TabCoordinator-D_5oNTTt.mjs} +48 -12
  9. package/dist/packem_shared/{createClientQuery-CQ51bWAE.mjs → createClientQuery-dJZg1ohm.mjs} +15 -6
  10. package/dist/packem_shared/{createServerClient-CTTAmvMx.mjs → createServerClient-DzeC2J3A.mjs} +1 -1
  11. package/dist/packem_shared/{httpStream-BJU-aflc.mjs → httpStream-DIdL8NEw.mjs} +33 -24
  12. package/dist/packem_shared/lunora-client.d-C4ud8bej.d.mts +2834 -0
  13. package/dist/packem_shared/lunora-client.d-C4ud8bej.d.ts +2834 -0
  14. package/dist/packem_shared/{offline-queue-CF4_Co5k.mjs → offline-queue-N-1JvYb4.mjs} +14 -2
  15. package/dist/packem_shared/preload.d-B6-lqUf2.d.ts +20 -0
  16. package/dist/packem_shared/preload.d-Dvk8zg6m.d.mts +20 -0
  17. package/dist/pagination/index.d.mts +42 -42
  18. package/dist/pagination/index.d.ts +42 -42
  19. package/dist/query/index.d.mts +42 -42
  20. package/dist/query/index.d.ts +42 -42
  21. package/dist/ssr/index.d.mts +79 -79
  22. package/dist/ssr/index.d.ts +79 -79
  23. package/dist/ssr/index.mjs +1 -1
  24. package/package.json +2 -2
  25. package/dist/packem_shared/lunora-client.d-JvtVpf8A.d.mts +0 -2824
  26. package/dist/packem_shared/lunora-client.d-JvtVpf8A.d.ts +0 -2824
  27. package/dist/packem_shared/preload.d-C4_d_l5v.d.ts +0 -20
  28. package/dist/packem_shared/preload.d-DKbjGN5O.d.mts +0 -20
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();