@lunora/client 1.0.0-alpha.11 → 1.0.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) 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 -10
  4. package/dist/index.d.ts +10 -10
  5. package/dist/index.mjs +5 -5
  6. package/dist/packem_shared/{LunoraClient-6J-4BGWK.mjs → LunoraClient-cxtxdYiI.mjs} +546 -39
  7. package/dist/packem_shared/{OfflineQueue-BI0FNNvc.mjs → OfflineQueue-GGYJRmhF.mjs} +1 -1
  8. package/dist/packem_shared/SubscriptionRegistry-Cxr70og-.mjs +1 -0
  9. package/dist/packem_shared/{createInMemoryPersistence-DlFjWtOm.mjs → createInMemoryPersistence-CY6F0ZvQ.mjs} +1 -1
  10. package/dist/packem_shared/{createInMemoryQueryCache-4AkTV38-.mjs → createInMemoryQueryCache-Bv4Ruv6n.mjs} +2 -2
  11. package/dist/packem_shared/{createServerClient-CtklXrs2.mjs → createServerClient-DQu_BVen.mjs} +1 -1
  12. package/dist/packem_shared/{lunora-client.d-Dm1BYq19.d.mts → lunora-client.d-DTwbfUWF.d.mts} +106 -1
  13. package/dist/packem_shared/{lunora-client.d-Dm1BYq19.d.ts → lunora-client.d-DTwbfUWF.d.ts} +106 -1
  14. package/dist/packem_shared/{offline-queue-7Wc4onA0.mjs → offline-queue-B9vfdSqp.mjs} +7 -1
  15. package/dist/packem_shared/{preload.d-BfWzGJWV.d.mts → preload.d-BEp0JJeu.d.mts} +1 -1
  16. package/dist/packem_shared/{preload.d-Sp_Ef-_u.d.ts → preload.d-CK2IYf5B.d.ts} +1 -1
  17. package/dist/packem_shared/{subscription-C1Jy7HiF.mjs → subscription-DoyO04-2.mjs} +10 -0
  18. package/dist/query/index.d.mts +2 -2
  19. package/dist/query/index.d.ts +2 -2
  20. package/dist/ssr/index.d.mts +3 -3
  21. package/dist/ssr/index.d.ts +3 -3
  22. package/dist/ssr/index.mjs +1 -1
  23. package/package.json +1 -1
  24. package/dist/packem_shared/SubscriptionRegistry-Dn-7k7eo.mjs +0 -1
@@ -1,4 +1,4 @@
1
- import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-Dm1BYq19.mjs";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-DTwbfUWF.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-Dm1BYq19.js";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-DTwbfUWF.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, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-Dm1BYq19.mjs";
2
- export { type C as CachedQuery, type e as ClientMessage, type f as ClientShapeSubscribeMessage, type g as ClientShapeUnsubscribeMessage, type h as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type i as FunctionArgumentDescriptor, type j as FunctionDescriptor, type G as GlobalFacetResult, type k as GlobalFacetValue, type l as GlobalFilterClause, type m as GlobalTableInfo, type n as GlobalTablePage, L as LunoraClient, type o as LunoraClientOptions, type p as MutationSettledEvent, type q as OptimisticLocalStore, type r as OptimisticUpdate, type s as OutboxMutation, type t as OutboxSink, type u as PersistedMutation, type P as Preloaded, type v as RowOp, type w as RpcEnvelope, type x as RpcResponseBody, type y as ScheduleRecord, type z as SchedulerPoolStatus, type E as SchedulerStatus, type H as ServerMessage, type I as ServerPokeEndMessage, type J as ServerPokePartMessage, type K as ServerPokeStartMessage, type N as ShardTrafficEntry, type T as ShardTrafficResult, type V as StorageListPage, type W as StorageObject, type X as StreamHandle, type Y as StreamIterable, type Z as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, _ as SubscriptionRegistry, type $ as SubscriptionState, type a0 as SyncWatermark, type a as Unsubscribe, type U as User, type a1 as WorkflowInstanceAction, type a2 as WorkflowInstanceDetail, type a3 as WorkflowInstancePage, type a4 as WorkflowInstanceStatus, type a5 as WorkflowInstanceSummary, type a6 as WorkflowStepDetail, a7 as createLocalStore, a8 as createStream } from "./packem_shared/lunora-client.d-Dm1BYq19.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-BfWzGJWV.mjs";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-DTwbfUWF.mjs";
2
+ export { type e as BatchSlot, type C as CachedQuery, type f as ClientMessage, type g as ClientShapeSubscribeMessage, type h as ClientShapeUnsubscribeMessage, type i as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type j as FunctionArgumentDescriptor, type k as FunctionDescriptor, type G as GlobalFacetResult, type l as GlobalFacetValue, type m as GlobalFilterClause, type n as GlobalTableInfo, type o as GlobalTablePage, L as LunoraClient, type p as LunoraClientError, type q as LunoraClientOptions, type r as MutationSettledEvent, type s as OptimisticLocalStore, type t as OptimisticUpdate, type u as OutboxMutation, type v as OutboxSink, type w as PersistedMutation, type P as Preloaded, type x as RowOp, type y as RpcEnvelope, type z as RpcResponseBody, type E as ScheduleRecord, type H as SchedulerPoolStatus, type I as SchedulerStatus, type J as ServerMessage, type K as ServerPokeEndMessage, type N as ServerPokePartMessage, type T as ServerPokeStartMessage, type V as ShardTrafficEntry, type W as ShardTrafficResult, type X as StorageListPage, type Y as StorageObject, type Z as StreamHandle, type _ as StreamIterable, type $ as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, a0 as SubscriptionRegistry, type a1 as SubscriptionState, type a2 as SyncWatermark, type a as Unsubscribe, type U as User, type a3 as WorkflowInstanceAction, type a4 as WorkflowInstanceDetail, type a5 as WorkflowInstancePage, type a6 as WorkflowInstanceStatus, type a7 as WorkflowInstanceSummary, type a8 as WorkflowStepDetail, a9 as createLocalStore, aa as createStream } from "./packem_shared/lunora-client.d-DTwbfUWF.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-BEp0JJeu.mjs";
4
4
  export type { AuthCapabilities, AuthImpersonation, AuthPage, AuthSession, AuthUser, CronJobInfo, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
@@ -311,7 +311,7 @@ declare class OfflineQueue {
311
311
  */
312
312
  declare const createInMemoryPersistence: () => PersistenceAdapter;
313
313
  interface IndexedDbPersistenceOptions {
314
- /** Database name; defaults to `"lunora"`. */
314
+ /** Database name; defaults to `"lunora-outbox"` (its own DB, separate from the read cache). */
315
315
  databaseName?: string;
316
316
  /** Injectable `IDBFactory` (e.g. `fake-indexeddb` in tests); defaults to the global `indexedDB`. */
317
317
  indexedDB?: IDBFactory;
@@ -364,7 +364,7 @@ declare const createInMemoryQueryCache: (options?: {
364
364
  maxEntries?: number;
365
365
  }) => QueryCacheAdapter;
366
366
  interface IndexedDbQueryCacheOptions {
367
- /** Database name; defaults to `"lunora"` (shared with the offline-mutation store). */
367
+ /** Database name; defaults to `"lunora-query-cache"` (its own DB, separate from the offline outbox). */
368
368
  databaseName?: string;
369
369
  /** Injectable `IDBFactory` (e.g. `fake-indexeddb` in tests); defaults to the global `indexedDB`. */
370
370
  indexedDB?: IDBFactory;
@@ -379,11 +379,11 @@ interface IndexedDbQueryCacheOptions {
379
379
  * LRU eviction. The store handle is opened lazily and cached, so repeated ops
380
380
  * reuse one connection.
381
381
  *
382
- * The store lives in the same `lunora` database as the offline-mutation queue
383
- * (bumped to schema v2). Opening it upgrades a v1 database in place, adding the
384
- * `query-cache` store without touching `offline-mutations`. Throws eagerly if no
385
- * `IDBFactory` is available — callers in non-browser environments should use
386
- * {@link createInMemoryQueryCache}.
382
+ * The store lives in its own `lunora-query-cache` database deliberately
383
+ * separate from the offline-mutation outbox's `lunora-outbox` database so the two
384
+ * independently-toggleable adapters never share (and drift on) a schema version.
385
+ * Throws eagerly if no `IDBFactory` is available — callers in non-browser
386
+ * environments should use {@link createInMemoryQueryCache}.
387
387
  */
388
388
  declare const createIndexedDbQueryCache: (options?: IndexedDbQueryCacheOptions) => QueryCacheAdapter;
389
389
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { c as PersistenceAdapter, B as BookmarkStorage, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-Dm1BYq19.js";
2
- export { type C as CachedQuery, type e as ClientMessage, type f as ClientShapeSubscribeMessage, type g as ClientShapeUnsubscribeMessage, type h as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type i as FunctionArgumentDescriptor, type j as FunctionDescriptor, type G as GlobalFacetResult, type k as GlobalFacetValue, type l as GlobalFilterClause, type m as GlobalTableInfo, type n as GlobalTablePage, L as LunoraClient, type o as LunoraClientOptions, type p as MutationSettledEvent, type q as OptimisticLocalStore, type r as OptimisticUpdate, type s as OutboxMutation, type t as OutboxSink, type u as PersistedMutation, type P as Preloaded, type v as RowOp, type w as RpcEnvelope, type x as RpcResponseBody, type y as ScheduleRecord, type z as SchedulerPoolStatus, type E as SchedulerStatus, type H as ServerMessage, type I as ServerPokeEndMessage, type J as ServerPokePartMessage, type K as ServerPokeStartMessage, type N as ShardTrafficEntry, type T as ShardTrafficResult, type V as StorageListPage, type W as StorageObject, type X as StreamHandle, type Y as StreamIterable, type Z as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, _ as SubscriptionRegistry, type $ as SubscriptionState, type a0 as SyncWatermark, type a as Unsubscribe, type U as User, type a1 as WorkflowInstanceAction, type a2 as WorkflowInstanceDetail, type a3 as WorkflowInstancePage, type a4 as WorkflowInstanceStatus, type a5 as WorkflowInstanceSummary, type a6 as WorkflowStepDetail, a7 as createLocalStore, a8 as createStream } from "./packem_shared/lunora-client.d-Dm1BYq19.js";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-Sp_Ef-_u.js";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-DTwbfUWF.js";
2
+ export { type e as BatchSlot, type C as CachedQuery, type f as ClientMessage, type g as ClientShapeSubscribeMessage, type h as ClientShapeUnsubscribeMessage, type i as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type j as FunctionArgumentDescriptor, type k as FunctionDescriptor, type G as GlobalFacetResult, type l as GlobalFacetValue, type m as GlobalFilterClause, type n as GlobalTableInfo, type o as GlobalTablePage, L as LunoraClient, type p as LunoraClientError, type q as LunoraClientOptions, type r as MutationSettledEvent, type s as OptimisticLocalStore, type t as OptimisticUpdate, type u as OutboxMutation, type v as OutboxSink, type w as PersistedMutation, type P as Preloaded, type x as RowOp, type y as RpcEnvelope, type z as RpcResponseBody, type E as ScheduleRecord, type H as SchedulerPoolStatus, type I as SchedulerStatus, type J as ServerMessage, type K as ServerPokeEndMessage, type N as ServerPokePartMessage, type T as ServerPokeStartMessage, type V as ShardTrafficEntry, type W as ShardTrafficResult, type X as StorageListPage, type Y as StorageObject, type Z as StreamHandle, type _ as StreamIterable, type $ as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, a0 as SubscriptionRegistry, type a1 as SubscriptionState, type a2 as SyncWatermark, type a as Unsubscribe, type U as User, type a3 as WorkflowInstanceAction, type a4 as WorkflowInstanceDetail, type a5 as WorkflowInstancePage, type a6 as WorkflowInstanceStatus, type a7 as WorkflowInstanceSummary, type a8 as WorkflowStepDetail, a9 as createLocalStore, aa as createStream } from "./packem_shared/lunora-client.d-DTwbfUWF.js";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-CK2IYf5B.js";
4
4
  export type { AuthCapabilities, AuthImpersonation, AuthPage, AuthSession, AuthUser, CronJobInfo, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
@@ -311,7 +311,7 @@ declare class OfflineQueue {
311
311
  */
312
312
  declare const createInMemoryPersistence: () => PersistenceAdapter;
313
313
  interface IndexedDbPersistenceOptions {
314
- /** Database name; defaults to `"lunora"`. */
314
+ /** Database name; defaults to `"lunora-outbox"` (its own DB, separate from the read cache). */
315
315
  databaseName?: string;
316
316
  /** Injectable `IDBFactory` (e.g. `fake-indexeddb` in tests); defaults to the global `indexedDB`. */
317
317
  indexedDB?: IDBFactory;
@@ -364,7 +364,7 @@ declare const createInMemoryQueryCache: (options?: {
364
364
  maxEntries?: number;
365
365
  }) => QueryCacheAdapter;
366
366
  interface IndexedDbQueryCacheOptions {
367
- /** Database name; defaults to `"lunora"` (shared with the offline-mutation store). */
367
+ /** Database name; defaults to `"lunora-query-cache"` (its own DB, separate from the offline outbox). */
368
368
  databaseName?: string;
369
369
  /** Injectable `IDBFactory` (e.g. `fake-indexeddb` in tests); defaults to the global `indexedDB`. */
370
370
  indexedDB?: IDBFactory;
@@ -379,11 +379,11 @@ interface IndexedDbQueryCacheOptions {
379
379
  * LRU eviction. The store handle is opened lazily and cached, so repeated ops
380
380
  * reuse one connection.
381
381
  *
382
- * The store lives in the same `lunora` database as the offline-mutation queue
383
- * (bumped to schema v2). Opening it upgrades a v1 database in place, adding the
384
- * `query-cache` store without touching `offline-mutations`. Throws eagerly if no
385
- * `IDBFactory` is available — callers in non-browser environments should use
386
- * {@link createInMemoryQueryCache}.
382
+ * The store lives in its own `lunora-query-cache` database deliberately
383
+ * separate from the offline-mutation outbox's `lunora-outbox` database so the two
384
+ * independently-toggleable adapters never share (and drift on) a schema version.
385
+ * Throws eagerly if no `IDBFactory` is available — callers in non-browser
386
+ * environments should use {@link createInMemoryQueryCache}.
387
387
  */
388
388
  declare const createIndexedDbQueryCache: (options?: IndexedDbQueryCacheOptions) => QueryCacheAdapter;
389
389
  /**
package/dist/index.mjs CHANGED
@@ -3,13 +3,13 @@ export { default as createInMemoryBookmarkStorage } from './packem_shared/create
3
3
  export { applyDelta, isMutationDelta } from './packem_shared/applyDelta-4jFGTPA3.mjs';
4
4
  export { CONFLICT_ERROR_CODE, isConflictError } from './packem_shared/CONFLICT_ERROR_CODE-aUdVbEDw.mjs';
5
5
  export { c as createLocalStore } from './packem_shared/local-store-BNgN3Dw3.mjs';
6
- export { LunoraClient } from './packem_shared/LunoraClient-6J-4BGWK.mjs';
6
+ export { LunoraClient } from './packem_shared/LunoraClient-cxtxdYiI.mjs';
7
7
  export { createMutationRunner } from './packem_shared/createMutationRunner-BqsavzvG.mjs';
8
8
  export { createMutatorRunner } from './packem_shared/createMutatorRunner-BETvCd0p.mjs';
9
- export { O as OfflineQueue } from './packem_shared/offline-queue-7Wc4onA0.mjs';
10
- export { createInMemoryPersistence, createIndexedDbPersistence } from './packem_shared/createInMemoryPersistence-DlFjWtOm.mjs';
9
+ export { O as OfflineQueue } from './packem_shared/offline-queue-B9vfdSqp.mjs';
10
+ export { createInMemoryPersistence, createIndexedDbPersistence } from './packem_shared/createInMemoryPersistence-CY6F0ZvQ.mjs';
11
11
  export { preloadQuery, preloadedQueryResult } from './packem_shared/preloadQuery-lobFkD2Z.mjs';
12
- export { createInMemoryQueryCache, createIndexedDbQueryCache, queryCacheKey } from './packem_shared/createInMemoryQueryCache-4AkTV38-.mjs';
12
+ export { createInMemoryQueryCache, createIndexedDbQueryCache, queryCacheKey } from './packem_shared/createInMemoryQueryCache-Bv4Ruv6n.mjs';
13
13
  export { createReconnect } from './packem_shared/createReconnect-Di_-oHH7.mjs';
14
14
  export { DEFAULT_MAX_BUFFER, createStream } from './packem_shared/DEFAULT_MAX_BUFFER-BDkqO5PW.mjs';
15
- export { S as SubscriptionRegistry } from './packem_shared/subscription-C1Jy7HiF.mjs';
15
+ export { S as SubscriptionRegistry } from './packem_shared/subscription-DoyO04-2.mjs';
@@ -1,13 +1,231 @@
1
- import { S as SubscriptionRegistry, s as stableStringify } from './subscription-C1Jy7HiF.mjs';
1
+ import { S as SubscriptionRegistry, s as stableStringify } from './subscription-DoyO04-2.mjs';
2
2
  import createInMemoryBookmarkStorage from './createInMemoryBookmarkStorage-BoN7a7TH.mjs';
3
3
  import { isMutationDelta, applyDelta } from './applyDelta-4jFGTPA3.mjs';
4
4
  import { a as applyOptimisticLayer, d as dropConfirmedLayers, n as notifySubscription, f as foldOptimistic, c as createLocalStore } from './local-store-BNgN3Dw3.mjs';
5
- import { O as OfflineQueue, n as nextId, i as isStaleVersion, r as reportPersistenceError } from './offline-queue-7Wc4onA0.mjs';
6
- import { resolvePersistenceAdapter } from './createInMemoryPersistence-DlFjWtOm.mjs';
7
- import { resolveQueryCacheAdapter, queryCacheKey } from './createInMemoryQueryCache-4AkTV38-.mjs';
5
+ import { O as OfflineQueue, n as nextId, i as isStaleVersion, r as reportPersistenceError } from './offline-queue-B9vfdSqp.mjs';
6
+ import { resolvePersistenceAdapter } from './createInMemoryPersistence-CY6F0ZvQ.mjs';
7
+ import { resolveQueryCacheAdapter, queryCacheKey } from './createInMemoryQueryCache-Bv4Ruv6n.mjs';
8
8
  import { createReconnect } from './createReconnect-Di_-oHH7.mjs';
9
9
  import { createStream } from './DEFAULT_MAX_BUFFER-BDkqO5PW.mjs';
10
10
 
11
+ const MAX_BATCH_ENTRIES = 500;
12
+
13
+ const TAG = "$lunora.wire$";
14
+ const MAX_DEPTH = 64;
15
+ const MAX_BIGINT_DIGITS = 1024;
16
+ const UNSAFE_KEY = "__proto__";
17
+ const TYPED_ARRAY_CTORS = {
18
+ BigInt64Array,
19
+ BigUint64Array,
20
+ Float32Array,
21
+ Float64Array,
22
+ Int8Array,
23
+ Int16Array,
24
+ Int32Array,
25
+ Uint8Array,
26
+ Uint8ClampedArray,
27
+ Uint16Array,
28
+ Uint32Array
29
+ };
30
+ const ERROR_CTORS = {
31
+ Error,
32
+ EvalError,
33
+ RangeError,
34
+ ReferenceError,
35
+ SyntaxError,
36
+ TypeError,
37
+ URIError
38
+ };
39
+ const toBase64 = (bytes) => {
40
+ let binary = "";
41
+ const chunk = 32768;
42
+ for (let index = 0; index < bytes.length; index += chunk) {
43
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
44
+ }
45
+ return btoa(binary);
46
+ };
47
+ const fromBase64 = (base64) => {
48
+ const binary = atob(base64);
49
+ const bytes = new Uint8Array(binary.length);
50
+ for (let index = 0; index < binary.length; index += 1) {
51
+ bytes[index] = binary.codePointAt(index) ?? 0;
52
+ }
53
+ return bytes;
54
+ };
55
+ const encodeWire = (value, depth = 0) => {
56
+ if (depth > MAX_DEPTH) {
57
+ throw new RangeError(`wire-codec: value nesting exceeds the ${MAX_DEPTH}-level limit`);
58
+ }
59
+ if (value === void 0) {
60
+ return [TAG, "undefined"];
61
+ }
62
+ if (value === null) {
63
+ return null;
64
+ }
65
+ const kind = typeof value;
66
+ if (kind === "bigint") {
67
+ return [TAG, "bigint", value.toString()];
68
+ }
69
+ if (kind === "number") {
70
+ const numeric = value;
71
+ if (Number.isNaN(numeric)) {
72
+ return [TAG, "nan"];
73
+ }
74
+ if (numeric === Infinity) {
75
+ return [TAG, "inf"];
76
+ }
77
+ if (numeric === -Infinity) {
78
+ return [TAG, "-inf"];
79
+ }
80
+ return numeric;
81
+ }
82
+ if (kind !== "object") {
83
+ return value;
84
+ }
85
+ if (value instanceof Date) {
86
+ return [TAG, "date", encodeWire(value.getTime(), depth + 1)];
87
+ }
88
+ if (value instanceof Error) {
89
+ const error = value;
90
+ const properties = {};
91
+ for (const key of Object.keys(error)) {
92
+ if (error[key] !== void 0) {
93
+ properties[key] = encodeWire(error[key], depth + 1);
94
+ }
95
+ }
96
+ const encodedError = [TAG, "error", error.name, error.message, properties];
97
+ if (error.cause !== void 0) {
98
+ encodedError.push(encodeWire(error.cause, depth + 1));
99
+ }
100
+ return encodedError;
101
+ }
102
+ if (value instanceof URL) {
103
+ return [TAG, "url", value.href];
104
+ }
105
+ if (value instanceof Map) {
106
+ return [TAG, "map", [...value.entries()].map(([k, v]) => [encodeWire(k, depth + 1), encodeWire(v, depth + 1)])];
107
+ }
108
+ if (value instanceof Set) {
109
+ return [TAG, "set", [...value].map((item) => encodeWire(item, depth + 1))];
110
+ }
111
+ if (value instanceof ArrayBuffer) {
112
+ return [TAG, "bytes", toBase64(new Uint8Array(value)), "ArrayBuffer"];
113
+ }
114
+ if (ArrayBuffer.isView(value)) {
115
+ const view = value;
116
+ const ctorName = view.constructor.name;
117
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
118
+ return ctorName === "Uint8Array" ? [TAG, "bytes", toBase64(bytes)] : [TAG, "bytes", toBase64(bytes), ctorName];
119
+ }
120
+ if (Array.isArray(value)) {
121
+ const encoded = value.map((item) => encodeWire(item, depth + 1));
122
+ return encoded.length > 0 && encoded[0] === TAG ? [TAG, "arr", encoded] : encoded;
123
+ }
124
+ const proto = Object.getPrototypeOf(value);
125
+ if (proto !== null && proto !== Object.prototype) {
126
+ const name = value.constructor?.name ?? "value";
127
+ throw new TypeError(
128
+ `wire-codec: cannot encode a ${name} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`
129
+ );
130
+ }
131
+ const source = value;
132
+ const result = {};
133
+ for (const key of Object.keys(source)) {
134
+ const field = source[key];
135
+ if (field !== void 0) {
136
+ result[key] = encodeWire(field, depth + 1);
137
+ }
138
+ }
139
+ return result;
140
+ };
141
+ const decodeWire = (value, depth = 0) => {
142
+ if (depth > MAX_DEPTH) {
143
+ throw new RangeError(`wire-codec: value nesting exceeds the ${MAX_DEPTH}-level limit`);
144
+ }
145
+ if (value === null || typeof value !== "object") {
146
+ return value;
147
+ }
148
+ if (Array.isArray(value)) {
149
+ if (value[0] === TAG) {
150
+ const tag = value[1];
151
+ switch (tag) {
152
+ case "-inf": {
153
+ return -Infinity;
154
+ }
155
+ case "arr": {
156
+ return value[2].map((item) => decodeWire(item, depth + 1));
157
+ }
158
+ case "bigint": {
159
+ const raw = value[2];
160
+ if (typeof raw !== "string" || raw.length > MAX_BIGINT_DIGITS || !/^-?\d+$/.test(raw)) {
161
+ throw new RangeError(`wire-codec: invalid or over-long bigint (max ${MAX_BIGINT_DIGITS} digits)`);
162
+ }
163
+ return BigInt(raw);
164
+ }
165
+ case "date": {
166
+ return new Date(decodeWire(value[2], depth + 1));
167
+ }
168
+ case "map": {
169
+ return new Map(value[2].map(([k, v]) => [decodeWire(k, depth + 1), decodeWire(v, depth + 1)]));
170
+ }
171
+ case "set": {
172
+ return new Set(value[2].map((item) => decodeWire(item, depth + 1)));
173
+ }
174
+ case "url": {
175
+ return new URL(value[2]);
176
+ }
177
+ case "error": {
178
+ const name = value[2];
179
+ const message = value[3];
180
+ const Ctor = (Object.hasOwn(ERROR_CTORS, name) ? ERROR_CTORS[name] : void 0) ?? Error;
181
+ const error = new Ctor(message);
182
+ if (error.name !== name) {
183
+ Object.defineProperty(error, "name", { configurable: true, value: name, writable: true });
184
+ }
185
+ Object.assign(error, decodeWire(value[4], depth + 1));
186
+ if (value.length > 5) {
187
+ Object.defineProperty(error, "cause", { configurable: true, value: decodeWire(value[5], depth + 1), writable: true });
188
+ }
189
+ return error;
190
+ }
191
+ case "bytes": {
192
+ const bytes = fromBase64(value[2]);
193
+ const ctorName = value[3] ?? "Uint8Array";
194
+ if (ctorName === "ArrayBuffer") {
195
+ return bytes.buffer.byteLength === bytes.byteLength ? bytes.buffer : bytes.slice().buffer;
196
+ }
197
+ const Ctor = Object.hasOwn(TYPED_ARRAY_CTORS, ctorName) ? TYPED_ARRAY_CTORS[ctorName] : void 0;
198
+ return Ctor ? new Ctor(bytes.slice().buffer) : bytes;
199
+ }
200
+ case "inf": {
201
+ return Infinity;
202
+ }
203
+ case "nan": {
204
+ return Number.NaN;
205
+ }
206
+ case "undefined": {
207
+ return void 0;
208
+ }
209
+ default: {
210
+ return value.map((item) => decodeWire(item, depth + 1));
211
+ }
212
+ }
213
+ }
214
+ return value.map((item) => decodeWire(item, depth + 1));
215
+ }
216
+ const source = value;
217
+ const result = {};
218
+ for (const key of Object.keys(source)) {
219
+ const decoded = decodeWire(source[key], depth + 1);
220
+ if (key === UNSAFE_KEY) {
221
+ Object.defineProperty(result, key, { configurable: true, enumerable: true, value: decoded, writable: true });
222
+ } else {
223
+ result[key] = decoded;
224
+ }
225
+ }
226
+ return result;
227
+ };
228
+
11
229
  class Listeners {
12
230
  listeners = /* @__PURE__ */ new Set();
13
231
  add(listener) {
@@ -34,6 +252,7 @@ class Listeners {
34
252
  }
35
253
 
36
254
  const RPC_PATH = "/_lunora/rpc";
255
+ const RPC_BATCH_PATH = "/_lunora/rpc-batch";
37
256
  const WS_PATH = "/_lunora/ws";
38
257
  const bucketQuery = (bucket) => bucket === void 0 || bucket === "" ? "" : `&bucket=${encodeURIComponent(bucket)}`;
39
258
  const rollbackOptimistic = (optimisticRollbacks) => {
@@ -171,6 +390,34 @@ const sendOn = (conn, message) => {
171
390
  return false;
172
391
  }
173
392
  };
393
+ const reconstructError = (errorBody) => {
394
+ const error = new Error(errorBody.message ?? "request failed");
395
+ error.code = errorBody.code;
396
+ if (errorBody.data !== void 0) {
397
+ error.data = decodeWire(errorBody.data);
398
+ }
399
+ return error;
400
+ };
401
+ const encodeCallArgs = (payload, label) => {
402
+ try {
403
+ return encodeWire(payload);
404
+ } catch (error) {
405
+ const reason = error instanceof Error ? error.message : String(error);
406
+ throw new TypeError(`LunoraClient: cannot encode ${label} — ${reason}`, error instanceof Error ? { cause: error } : void 0);
407
+ }
408
+ };
409
+ const demuxBatchResults = (rawResults, count) => {
410
+ const slots = Array.from({ length: count });
411
+ for (const entry of rawResults) {
412
+ if (typeof entry.id !== "number" || entry.id < 0 || entry.id >= count) {
413
+ continue;
414
+ }
415
+ const inner = entry.body;
416
+ slots[entry.id] = inner && "error" in inner && inner.error ? { error: reconstructError(inner.error), ok: false } : { ok: true, value: decodeWire(inner?.result) };
417
+ }
418
+ return slots.map((slot) => slot ?? { error: new Error("batch call returned no result"), ok: false });
419
+ };
420
+ const TRANSIENT_BATCH_ERROR_CODES = /* @__PURE__ */ new Set(["SHARD_ERROR", "SHARD_UNAVAILABLE"]);
174
421
  class LunoraClient {
175
422
  /** Hard cap on concurrently-buffered pokes — a backstop that reclaims buffers abandoned by a mid-poke disconnect (no `pokeEnd`). Far above any real concurrent-in-flight count. */
176
423
  static MAX_POKE_BUFFERS = 256;
@@ -650,7 +897,7 @@ class LunoraClient {
650
897
  this.ensureSocket(options.shardKey);
651
898
  const conn = this.getConnection(options.shardKey);
652
899
  if (conn) {
653
- sendOn(conn, { data, topic, type: "whisper" });
900
+ sendOn(conn, { data: encodeCallArgs(data ?? null, `whisper data for topic '${topic}'`), topic, type: "whisper" });
654
901
  }
655
902
  }
656
903
  /**
@@ -723,6 +970,62 @@ class LunoraClient {
723
970
  }
724
971
  return await this.rpc(function_.__lunoraRef, args, options.shardKey, { attachBookmark: true });
725
972
  }
973
+ /**
974
+ * Batch several independent calls into ONE round trip (plan 088). Each call is
975
+ * dispatched server-side exactly as an individual RPC — per-shard
976
+ * authorization, `(identity, mutationId)` idempotency, and custom-mutator
977
+ * watermark ordering are all preserved — and the worker splits the batch by
978
+ * shard so calls to different shards fan out to their own DOs. Results are
979
+ * demuxed back in input order; a failing call does NOT fail the batch (its
980
+ * slot carries `{ ok: false, error }`, with `.code`/`.data` reconstructed like
981
+ * a single call). Args/results ride the value codec (bytes/bigint survive).
982
+ *
983
+ * No promise pipelining and no capability passing — a call's args cannot
984
+ * reference another call's result (see plan 088 §fence; capabilities are
985
+ * incompatible with DO hibernation).
986
+ */
987
+ async batch(calls) {
988
+ if (this.closed) {
989
+ throw new Error("LunoraClient is closed");
990
+ }
991
+ if (!this.fetchImpl) {
992
+ throw new Error("LunoraClient: no `fetch` implementation available");
993
+ }
994
+ if (calls.length === 0) {
995
+ return [];
996
+ }
997
+ const response = await this.fetchImpl(joinUrl(this.url, RPC_BATCH_PATH), {
998
+ body: JSON.stringify({
999
+ calls: calls.map((call, index) => {
1000
+ return {
1001
+ args: encodeCallArgs(call.args ?? {}, `args for batch call '${call.fn.__lunoraRef}'`),
1002
+ functionPath: call.fn.__lunoraRef,
1003
+ id: index,
1004
+ shardKey: call.shardKey
1005
+ };
1006
+ })
1007
+ }),
1008
+ headers: this.rpcRequestHeaders({ attachBookmark: true }),
1009
+ method: "POST"
1010
+ });
1011
+ const bookmark = response.headers.get("x-d1-bookmark");
1012
+ if (bookmark) {
1013
+ this.bookmark.set(bookmark);
1014
+ }
1015
+ let body;
1016
+ try {
1017
+ body = await response.json();
1018
+ } catch {
1019
+ throw new Error(`LunoraClient: batch response was not JSON (status ${response.status.toString()})`);
1020
+ }
1021
+ if (!response.ok || body.error && !body.results) {
1022
+ if (body.error) {
1023
+ throw reconstructError(body.error);
1024
+ }
1025
+ throw new Error(`LunoraClient: batch request failed (status ${response.status.toString()})`);
1026
+ }
1027
+ return demuxBatchResults(body.results ?? [], calls.length);
1028
+ }
726
1029
  /**
727
1030
  * Invoke a mutation. Errors propagate as rejections.
728
1031
  *
@@ -1549,7 +1852,14 @@ class LunoraClient {
1549
1852
  const conn = this.getConnection(shardKey);
1550
1853
  const message = {
1551
1854
  id,
1552
- query: { args: argsRecord, functionPath: function_.__lunoraRef, shardKey },
1855
+ // Wire-encode the stream args so `bigint`/bytes survive the send (raw
1856
+ // `JSON.stringify` throws on a bigint); the shard `decodeWire`s them
1857
+ // before invoking the stream handler.
1858
+ query: {
1859
+ args: encodeCallArgs(argsRecord, `stream args for '${function_.__lunoraRef}'`),
1860
+ functionPath: function_.__lunoraRef,
1861
+ shardKey
1862
+ },
1553
1863
  type: "stream"
1554
1864
  };
1555
1865
  const sentImmediately = conn?.wsState === "open" && sendOn(conn, message);
@@ -1975,7 +2285,10 @@ class LunoraClient {
1975
2285
  }
1976
2286
  const headers = this.rpcRequestHeaders(flags);
1977
2287
  const response = await this.fetchImpl(joinUrl(this.url, RPC_PATH), {
1978
- body: JSON.stringify({ args, functionPath, shardKey }),
2288
+ // `encodeWire` tags leaves plain JSON can't carry (`bigint`,
2289
+ // `ArrayBuffer`/typed arrays, `NaN`/±Infinity); a pure-JSON `args`
2290
+ // encodes byte-identically, so a pre-codec server still interops.
2291
+ body: JSON.stringify({ args: encodeCallArgs(args, `args for '${functionPath}'`), functionPath, shardKey }),
1979
2292
  headers,
1980
2293
  method: "POST"
1981
2294
  });
@@ -1993,9 +2306,7 @@ class LunoraClient {
1993
2306
  throw new Error(`LunoraClient: response was not JSON (status ${response.status.toString()}${statusText})`);
1994
2307
  }
1995
2308
  if ("error" in body) {
1996
- const error = new Error(body.error.message);
1997
- error.code = body.error.code;
1998
- throw error;
2309
+ throw reconstructError(body.error);
1999
2310
  }
2000
2311
  if (!response.ok) {
2001
2312
  const statusText = response.statusText ? ` ${response.statusText}` : "";
@@ -2003,7 +2314,7 @@ class LunoraClient {
2003
2314
  }
2004
2315
  flags.onMutationAck?.(body.lastMutationId);
2005
2316
  flags.onCommitCursor?.(body.commitCursor);
2006
- return body.result;
2317
+ return decodeWire(body.result);
2007
2318
  }
2008
2319
  /**
2009
2320
  * Authenticated request to a non-RPC admin endpoint (the scheduler list /
@@ -2325,7 +2636,7 @@ class LunoraClient {
2325
2636
  case "chunk": {
2326
2637
  const { data, id } = message;
2327
2638
  const stream = this.streams.get(id);
2328
- stream?.handle.push(data);
2639
+ stream?.handle.push(decodeWire(data));
2329
2640
  return;
2330
2641
  }
2331
2642
  case "complete": {
@@ -2405,7 +2716,9 @@ class LunoraClient {
2405
2716
  return;
2406
2717
  }
2407
2718
  const existing = buffer.parts.get(message.shapeId) ?? [];
2408
- existing.push(...message.rowsPatch);
2719
+ for (const op of message.rowsPatch) {
2720
+ existing.push(op.value === void 0 ? op : { ...op, value: decodeWire(op.value) });
2721
+ }
2409
2722
  buffer.parts.set(message.shapeId, existing);
2410
2723
  if (message.lastMutationId !== void 0) {
2411
2724
  buffer.lastMutationId.set(message.shapeId, message.lastMutationId);
@@ -2551,9 +2864,9 @@ class LunoraClient {
2551
2864
  // eslint-disable-next-line class-methods-use-this -- instance method for symmetry with the other message handlers; reads no shared client state
2552
2865
  resolveDataPayload(message, state) {
2553
2866
  if ("data" in message && message.data !== void 0) {
2554
- return message.data;
2867
+ return decodeWire(message.data);
2555
2868
  }
2556
- const { delta } = message;
2869
+ const delta = decodeWire(message.delta);
2557
2870
  if (isMutationDelta(delta) && state.serverBase !== void 0) {
2558
2871
  const merged = applyDelta(state.serverBase, delta);
2559
2872
  if (merged !== void 0) {
@@ -2568,9 +2881,10 @@ class LunoraClient {
2568
2881
  if (!handlers) {
2569
2882
  return;
2570
2883
  }
2884
+ const data = decodeWire(message.data);
2571
2885
  for (const handler of handlers) {
2572
2886
  try {
2573
- handler(message.data, message.from);
2887
+ handler(data, message.from);
2574
2888
  } catch {
2575
2889
  }
2576
2890
  }
@@ -2674,24 +2988,123 @@ class LunoraClient {
2674
2988
  async flushOfflineQueue(shardKey) {
2675
2989
  const key = connectionKey(shardKey);
2676
2990
  const drained = this.offlineQueue.drain((item) => connectionKey(item.shardKey) === key);
2677
- for (let index = 0; index < drained.length; index += 1) {
2678
- const item = drained[index];
2679
- if (!item) {
2680
- continue;
2991
+ if (drained.length === 0) {
2992
+ return;
2993
+ }
2994
+ const currentIdentity = this.identityFingerprint();
2995
+ const sendable = [];
2996
+ for (const item of drained) {
2997
+ if (this.passesReplayIdentityGate(item, currentIdentity)) {
2998
+ sendable.push(item);
2681
2999
  }
2682
- const currentIdentity = this.identityFingerprint();
2683
- const liveStamp = item.id === void 0 ? void 0 : this.queuedIdentities.get(item.id);
2684
- const stamped = liveStamp === void 0 ? item.identity : liveStamp;
2685
- if (stamped !== void 0 && stamped !== currentIdentity) {
2686
- this.queuedIdentities.delete(item.id ?? "");
2687
- this.unpersist(item.id);
2688
- const error = new Error("offline mutation skipped: auth identity changed before replay");
2689
- error.code = "OFFLINE_IDENTITY_CHANGED";
2690
- item.reject(error);
2691
- this.emitItemSettled(item, "rejected", error);
2692
- continue;
3000
+ }
3001
+ if (sendable.length === 0) {
3002
+ return;
3003
+ }
3004
+ const encodable = this.encodableOrSettleTerminal(sendable);
3005
+ if (encodable.length === 0) {
3006
+ return;
3007
+ }
3008
+ if (encodable.length === 1) {
3009
+ await this.replaySequential(encodable);
3010
+ return;
3011
+ }
3012
+ const toRequeue = [];
3013
+ for (let start = 0; start < encodable.length; start += MAX_BATCH_ENTRIES) {
3014
+ const chunk = encodable.slice(start, start + MAX_BATCH_ENTRIES);
3015
+ const outcome = await this.replayBatched(chunk);
3016
+ toRequeue.push(...outcome.requeue);
3017
+ if (outcome.stop) {
3018
+ toRequeue.push(...encodable.slice(start + MAX_BATCH_ENTRIES));
3019
+ break;
3020
+ }
3021
+ }
3022
+ if (toRequeue.length > 0) {
3023
+ this.offlineQueue.requeue(toRequeue);
3024
+ }
3025
+ }
3026
+ /**
3027
+ * Partition already-gated writes into the encodable ones (returned) and reject
3028
+ * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
3029
+ * or class instance in a `v.any()` field) can NEVER replay — the codec failure
3030
+ * is deterministic, not transient. Rejecting here is essential: otherwise
3031
+ * `encodeWire` throws mid-flush, is classified as transient (a codec error has
3032
+ * no `.code`), and re-queues forever — a silent hang where the caller's Promise
3033
+ * never settles and the optimistic write never rolls back. Encoding is cheap;
3034
+ * the flush is the slow reconnect path.
3035
+ */
3036
+ encodableOrSettleTerminal(items) {
3037
+ const encodable = [];
3038
+ for (const item of items) {
3039
+ try {
3040
+ encodeCallArgs(item.args, `args for '${item.functionPath}'`);
3041
+ encodable.push(item);
3042
+ } catch (error) {
3043
+ this.settleReplayTerminal(item, error instanceof Error ? error : new Error(String(error)));
2693
3044
  }
3045
+ }
3046
+ return encodable;
3047
+ }
3048
+ /**
3049
+ * Identity guard for one queued write about to replay: a write stamped under
3050
+ * one identity must never replay under another. The live `queuedIdentities`
3051
+ * map is the source of truth for the current session; a hydrated write whose
3052
+ * id isn't in the map falls back to the stamp persisted with the record
3053
+ * (`item.identity`), so a reload can't replay another user's queued writes.
3054
+ * Only legacy records (persisted before stamps were durable —
3055
+ * `item.identity === undefined`) replay under whatever identity is current.
3056
+ *
3057
+ * `Map.get` returns `undefined` for unstamped/hydrated ids and `item.identity`
3058
+ * is `undefined` for legacy records; a persisted `null` (queued while signed
3059
+ * out) is a real value that must not collapse into `undefined` — hence the
3060
+ * explicit `=== undefined` check rather than `??`. Returns `true` when the
3061
+ * write may replay; otherwise settles it `OFFLINE_IDENTITY_CHANGED` and returns
3062
+ * `false`. Either way the live stamp is consumed.
3063
+ */
3064
+ passesReplayIdentityGate(item, currentIdentity) {
3065
+ const liveStamp = item.id === void 0 ? void 0 : this.queuedIdentities.get(item.id);
3066
+ const stamped = liveStamp === void 0 ? item.identity : liveStamp;
3067
+ if (stamped !== void 0 && stamped !== currentIdentity) {
2694
3068
  this.queuedIdentities.delete(item.id ?? "");
3069
+ this.unpersist(item.id);
3070
+ const error = new Error("offline mutation skipped: auth identity changed before replay");
3071
+ error.code = "OFFLINE_IDENTITY_CHANGED";
3072
+ item.reject(error);
3073
+ this.emitItemSettled(item, "rejected", error);
3074
+ return false;
3075
+ }
3076
+ this.queuedIdentities.delete(item.id ?? "");
3077
+ return true;
3078
+ }
3079
+ /** Settle a write that replayed successfully: confirm its optimistic layer against the echoed commit cursor BEFORE resolving, so the gapless drop is in place when the awaiter (and any confirming frame) observes the settle. */
3080
+ settleReplaySuccess(item, value, commitCursor) {
3081
+ this.unpersist(item.id);
3082
+ item.onCommit?.(commitCursor);
3083
+ item.resolve(value);
3084
+ this.emitItemSettled(item, "committed");
3085
+ }
3086
+ /** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
3087
+ settleReplayTerminal(item, error) {
3088
+ this.unpersist(item.id);
3089
+ item.reject(error);
3090
+ this.emitItemSettled(item, "rejected", error);
3091
+ }
3092
+ /**
3093
+ * Replay already-identity-gated writes one at a time on the single-call `/rpc`
3094
+ * path, preserving FIFO order (parallel `.then()` chains would race the
3095
+ * ordering callers depend on). Each replays under its stable `mutationId` so
3096
+ * the server dedups a write it already committed (exactly-once). A coded error
3097
+ * is a server verdict (drop it); a codeless (transport/transient) failure stops
3098
+ * the flush and re-queues this write and every unreplayed one for the next
3099
+ * reconnect — their callers stay pending, and the identity guard re-applies on
3100
+ * retry via each record's persisted stamp.
3101
+ */
3102
+ async replaySequential(items) {
3103
+ for (let index = 0; index < items.length; index += 1) {
3104
+ const item = items[index];
3105
+ if (!item) {
3106
+ continue;
3107
+ }
2695
3108
  try {
2696
3109
  let commitCursor;
2697
3110
  const value = await this.rpc(item.functionPath, item.args, item.shardKey, {
@@ -2701,22 +3114,116 @@ class LunoraClient {
2701
3114
  commitCursor = cursor;
2702
3115
  }
2703
3116
  });
2704
- this.unpersist(item.id);
2705
- item.onCommit?.(commitCursor);
2706
- item.resolve(value);
2707
- this.emitItemSettled(item, "committed");
3117
+ this.settleReplaySuccess(item, value, commitCursor);
2708
3118
  } catch (error) {
2709
3119
  if (error.code !== void 0) {
2710
- this.unpersist(item.id);
2711
- item.reject(error);
2712
- this.emitItemSettled(item, "rejected", error);
3120
+ this.settleReplayTerminal(item, error);
2713
3121
  continue;
2714
3122
  }
2715
- this.offlineQueue.requeue(drained.slice(index));
3123
+ this.offlineQueue.requeue(items.slice(index));
2716
3124
  return;
2717
3125
  }
2718
3126
  }
2719
3127
  }
3128
+ /**
3129
+ * Coalesce already-identity-gated writes for a single shard into ONE
3130
+ * `/_lunora/rpc-batch` round trip (plan 088 follow-on). The worker forwards
3131
+ * them to the shard DO, which replays each through its single-call dispatch, so
3132
+ * per-entry `mutationId` idempotency and in-order application are inherited from
3133
+ * the proven path. Per-slot demux mirrors {@link replaySequential}'s
3134
+ * classification: success confirms the optimistic layer against the echoed
3135
+ * `commitCursor`; a coded application verdict is terminal; a transient shard
3136
+ * failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
3137
+ * transport failure re-queues for the next reconnect (never dropping a durable
3138
+ * write). A whole-batch coded rejection (bad request / authorization denial the
3139
+ * server reached a verdict on) is terminal for every entry.
3140
+ *
3141
+ * Returns the writes that must be re-queued and `stop` — `true` when the whole
3142
+ * chunk failed at the transport level, so the caller leaves later chunks queued
3143
+ * rather than sending on. The caller re-queues once, in order, so requeuing is
3144
+ * NOT done here.
3145
+ */
3146
+ async replayBatched(items) {
3147
+ if (!this.fetchImpl) {
3148
+ return { requeue: items, stop: true };
3149
+ }
3150
+ let response;
3151
+ try {
3152
+ response = await this.fetchImpl(joinUrl(this.url, RPC_BATCH_PATH), {
3153
+ body: JSON.stringify({
3154
+ calls: items.map((item, index) => {
3155
+ return {
3156
+ args: encodeCallArgs(item.args, `args for '${item.functionPath}'`),
3157
+ functionPath: item.functionPath,
3158
+ id: index,
3159
+ // Stable per-write key so the DO dedups a write it already
3160
+ // committed (exactly-once), exactly as the single-call replay.
3161
+ mutationId: item.id,
3162
+ shardKey: item.shardKey
3163
+ };
3164
+ })
3165
+ }),
3166
+ headers: this.rpcRequestHeaders({ attachBookmark: true }),
3167
+ method: "POST"
3168
+ });
3169
+ } catch {
3170
+ return { requeue: items, stop: true };
3171
+ }
3172
+ const bookmark = response.headers.get("x-d1-bookmark");
3173
+ if (bookmark) {
3174
+ this.bookmark.set(bookmark);
3175
+ }
3176
+ let body;
3177
+ try {
3178
+ body = await response.json();
3179
+ } catch {
3180
+ return { requeue: items, stop: true };
3181
+ }
3182
+ if (!body.results) {
3183
+ if (body.error) {
3184
+ const error = reconstructError(body.error);
3185
+ for (const item of items) {
3186
+ this.settleReplayTerminal(item, error);
3187
+ }
3188
+ return { requeue: [], stop: false };
3189
+ }
3190
+ return { requeue: items, stop: true };
3191
+ }
3192
+ return { requeue: this.settleReplayBatchSlots(items, body.results), stop: false };
3193
+ }
3194
+ /**
3195
+ * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
3196
+ * in input order. Each slot's envelope classifies its write the same way
3197
+ * {@link replaySequential} does: a success confirms the optimistic layer
3198
+ * against the echoed `commitCursor`; a coded application verdict is terminal;
3199
+ * a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
3200
+ * server never returned is returned for the caller to re-queue.
3201
+ * @returns the writes that must be re-queued (transient slots), in input order
3202
+ */
3203
+ settleReplayBatchSlots(items, results) {
3204
+ const bySlot = /* @__PURE__ */ new Map();
3205
+ for (const entry of results) {
3206
+ if (typeof entry.id === "number" && entry.body !== void 0) {
3207
+ bySlot.set(entry.id, entry.body);
3208
+ }
3209
+ }
3210
+ const requeue = [];
3211
+ for (const [index, item] of items.entries()) {
3212
+ const inner = bySlot.get(index);
3213
+ if (inner === void 0) {
3214
+ requeue.push(item);
3215
+ } else if ("error" in inner) {
3216
+ if (TRANSIENT_BATCH_ERROR_CODES.has(inner.error.code)) {
3217
+ requeue.push(item);
3218
+ } else {
3219
+ this.settleReplayTerminal(item, reconstructError(inner.error));
3220
+ }
3221
+ } else {
3222
+ this.settleReplaySuccess(item, decodeWire(inner.result), inner.commitCursor);
3223
+ }
3224
+ }
3225
+ return requeue;
3226
+ }
2720
3227
  }
2721
3228
 
2722
3229
  export { LunoraClient };
@@ -1 +1 @@
1
- export { O as OfflineQueue, n as nextId, r as reportPersistenceError } from './offline-queue-7Wc4onA0.mjs';
1
+ export { O as OfflineQueue, n as nextId, r as reportPersistenceError } from './offline-queue-B9vfdSqp.mjs';
@@ -0,0 +1 @@
1
+ export { S as SubscriptionRegistry } from './subscription-DoyO04-2.mjs';
@@ -25,7 +25,7 @@ const createInMemoryPersistence = () => {
25
25
  }
26
26
  };
27
27
  };
28
- const DEFAULT_DATABASE = "lunora";
28
+ const DEFAULT_DATABASE = "lunora-outbox";
29
29
  const DEFAULT_STORE = "offline-mutations";
30
30
  const ID_INDEX = "by_id";
31
31
  const promisifyRequest = (request) => new Promise((resolve, reject) => {
@@ -35,10 +35,10 @@ const createInMemoryQueryCache = (options = {}) => {
35
35
  }
36
36
  };
37
37
  };
38
- const DEFAULT_DATABASE = "lunora";
38
+ const DEFAULT_DATABASE = "lunora-query-cache";
39
39
  const DEFAULT_STORE = "query-cache";
40
40
  const TS_INDEX = "by_ts";
41
- const DATABASE_VERSION = 2;
41
+ const DATABASE_VERSION = 1;
42
42
  const promisifyRequest = (request) => new Promise((resolve, reject) => {
43
43
  request.addEventListener("success", () => {
44
44
  resolve(request.result);
@@ -1,4 +1,4 @@
1
- import { LunoraClient } from './LunoraClient-6J-4BGWK.mjs';
1
+ import { LunoraClient } from './LunoraClient-cxtxdYiI.mjs';
2
2
 
3
3
  const createServerClient = (options) => {
4
4
  const client = new LunoraClient({ fetch: options.fetch, url: options.url });
@@ -358,6 +358,7 @@ interface RpcEnvelope {
358
358
  type RpcResponseBody = {
359
359
  error: {
360
360
  code: string;
361
+ data?: unknown;
361
362
  message: string;
362
363
  };
363
364
  } | {
@@ -1151,6 +1152,19 @@ interface SyncWatermark {
1151
1152
  checkpoint?: number;
1152
1153
  mutationId?: number;
1153
1154
  }
1155
+ /** An `Error` carrying the server's machine-readable `code` and (for a `LunoraError`) structured `data`. The client's public error contract for RPC/batch failures. */
1156
+ type LunoraClientError = Error & {
1157
+ code?: string;
1158
+ data?: unknown;
1159
+ };
1160
+ /** One demuxed result slot of a {@link LunoraClient.batch} call (plan 088). */
1161
+ type BatchSlot = {
1162
+ error: LunoraClientError;
1163
+ ok: false;
1164
+ } | {
1165
+ ok: true;
1166
+ value: unknown;
1167
+ };
1154
1168
  /**
1155
1169
  * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
1156
1170
  * a single multiplexed WebSocket.
@@ -1498,6 +1512,25 @@ declare class LunoraClient {
1498
1512
  shardKey?: string;
1499
1513
  }): Promise<ReturnOf<F>>;
1500
1514
  /**
1515
+ * Batch several independent calls into ONE round trip (plan 088). Each call is
1516
+ * dispatched server-side exactly as an individual RPC — per-shard
1517
+ * authorization, `(identity, mutationId)` idempotency, and custom-mutator
1518
+ * watermark ordering are all preserved — and the worker splits the batch by
1519
+ * shard so calls to different shards fan out to their own DOs. Results are
1520
+ * demuxed back in input order; a failing call does NOT fail the batch (its
1521
+ * slot carries `{ ok: false, error }`, with `.code`/`.data` reconstructed like
1522
+ * a single call). Args/results ride the value codec (bytes/bigint survive).
1523
+ *
1524
+ * No promise pipelining and no capability passing — a call's args cannot
1525
+ * reference another call's result (see plan 088 §fence; capabilities are
1526
+ * incompatible with DO hibernation).
1527
+ */
1528
+ batch(calls: ReadonlyArray<{
1529
+ args?: Record<string, unknown>;
1530
+ fn: FunctionReference;
1531
+ shardKey?: string;
1532
+ }>): Promise<BatchSlot[]>;
1533
+ /**
1501
1534
  * Invoke a mutation. Errors propagate as rejections.
1502
1535
  *
1503
1536
  * Offline-queue semantics: a mutation is queued (and replayed on reconnect)
@@ -2202,5 +2235,77 @@ declare class LunoraClient {
2202
2235
  */
2203
2236
  private clearQueryCacheForIdentityChange;
2204
2237
  private flushOfflineQueue;
2238
+ /**
2239
+ * Partition already-gated writes into the encodable ones (returned) and reject
2240
+ * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
2241
+ * or class instance in a `v.any()` field) can NEVER replay — the codec failure
2242
+ * is deterministic, not transient. Rejecting here is essential: otherwise
2243
+ * `encodeWire` throws mid-flush, is classified as transient (a codec error has
2244
+ * no `.code`), and re-queues forever — a silent hang where the caller's Promise
2245
+ * never settles and the optimistic write never rolls back. Encoding is cheap;
2246
+ * the flush is the slow reconnect path.
2247
+ */
2248
+ private encodableOrSettleTerminal;
2249
+ /**
2250
+ * Identity guard for one queued write about to replay: a write stamped under
2251
+ * one identity must never replay under another. The live `queuedIdentities`
2252
+ * map is the source of truth for the current session; a hydrated write whose
2253
+ * id isn't in the map falls back to the stamp persisted with the record
2254
+ * (`item.identity`), so a reload can't replay another user's queued writes.
2255
+ * Only legacy records (persisted before stamps were durable —
2256
+ * `item.identity === undefined`) replay under whatever identity is current.
2257
+ *
2258
+ * `Map.get` returns `undefined` for unstamped/hydrated ids and `item.identity`
2259
+ * is `undefined` for legacy records; a persisted `null` (queued while signed
2260
+ * out) is a real value that must not collapse into `undefined` — hence the
2261
+ * explicit `=== undefined` check rather than `??`. Returns `true` when the
2262
+ * write may replay; otherwise settles it `OFFLINE_IDENTITY_CHANGED` and returns
2263
+ * `false`. Either way the live stamp is consumed.
2264
+ */
2265
+ private passesReplayIdentityGate;
2266
+ /** Settle a write that replayed successfully: confirm its optimistic layer against the echoed commit cursor BEFORE resolving, so the gapless drop is in place when the awaiter (and any confirming frame) observes the settle. */
2267
+ private settleReplaySuccess;
2268
+ /** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
2269
+ private settleReplayTerminal;
2270
+ /**
2271
+ * Replay already-identity-gated writes one at a time on the single-call `/rpc`
2272
+ * path, preserving FIFO order (parallel `.then()` chains would race the
2273
+ * ordering callers depend on). Each replays under its stable `mutationId` so
2274
+ * the server dedups a write it already committed (exactly-once). A coded error
2275
+ * is a server verdict (drop it); a codeless (transport/transient) failure stops
2276
+ * the flush and re-queues this write and every unreplayed one for the next
2277
+ * reconnect — their callers stay pending, and the identity guard re-applies on
2278
+ * retry via each record's persisted stamp.
2279
+ */
2280
+ private replaySequential;
2281
+ /**
2282
+ * Coalesce already-identity-gated writes for a single shard into ONE
2283
+ * `/_lunora/rpc-batch` round trip (plan 088 follow-on). The worker forwards
2284
+ * them to the shard DO, which replays each through its single-call dispatch, so
2285
+ * per-entry `mutationId` idempotency and in-order application are inherited from
2286
+ * the proven path. Per-slot demux mirrors {@link replaySequential}'s
2287
+ * classification: success confirms the optimistic layer against the echoed
2288
+ * `commitCursor`; a coded application verdict is terminal; a transient shard
2289
+ * failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
2290
+ * transport failure re-queues for the next reconnect (never dropping a durable
2291
+ * write). A whole-batch coded rejection (bad request / authorization denial the
2292
+ * server reached a verdict on) is terminal for every entry.
2293
+ *
2294
+ * Returns the writes that must be re-queued and `stop` — `true` when the whole
2295
+ * chunk failed at the transport level, so the caller leaves later chunks queued
2296
+ * rather than sending on. The caller re-queues once, in order, so requeuing is
2297
+ * NOT done here.
2298
+ */
2299
+ private replayBatched;
2300
+ /**
2301
+ * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
2302
+ * in input order. Each slot's envelope classifies its write the same way
2303
+ * {@link replaySequential} does: a success confirms the optimistic layer
2304
+ * against the echoed `commitCursor`; a coded application verdict is terminal;
2305
+ * a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
2306
+ * server never returned is returned for the caller to re-queue.
2307
+ * @returns the writes that must be re-queued (transient slots), in input order
2308
+ */
2309
+ private settleReplayBatchSlots;
2205
2310
  }
2206
- export { SubscriptionState as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, SchedulerStatus as E, FunctionReference as F, GlobalFacetResult as G, ServerMessage as H, ServerPokeEndMessage as I, ServerPokePartMessage as J, ServerPokeStartMessage as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficEntry as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ShardTrafficResult as T, User as U, StorageListPage as V, StorageObject as W, StreamHandle as X, StreamIterable as Y, SubscriptionCallback as Z, SubscriptionRegistry as _, Unsubscribe as a, SyncWatermark as a0, WorkflowInstanceAction as a1, WorkflowInstanceDetail as a2, WorkflowInstancePage as a3, WorkflowInstanceStatus as a4, WorkflowInstanceSummary as a5, WorkflowStepDetail as a6, createLocalStore as a7, createStream as a8, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ClientShapeSubscribeMessage as f, ClientShapeUnsubscribeMessage as g, ConnectionStatus as h, FunctionArgumentDescriptor as i, FunctionDescriptor as j, GlobalFacetValue as k, GlobalFilterClause as l, GlobalTableInfo as m, GlobalTablePage as n, LunoraClientOptions as o, MutationSettledEvent as p, OptimisticLocalStore as q, OptimisticUpdate as r, OutboxMutation as s, OutboxSink as t, PersistedMutation as u, RowOp as v, RpcEnvelope as w, RpcResponseBody as x, ScheduleRecord as y, SchedulerPoolStatus as z };
2311
+ export { SubscriptionCallback as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, ScheduleRecord as E, FunctionReference as F, GlobalFacetResult as G, SchedulerPoolStatus as H, SchedulerStatus as I, ServerMessage as J, ServerPokeEndMessage as K, LunoraClient as L, MutationCallOptions as M, ServerPokePartMessage as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ServerPokeStartMessage as T, User as U, ShardTrafficEntry as V, ShardTrafficResult as W, StorageListPage as X, StorageObject as Y, StreamHandle as Z, StreamIterable as _, Unsubscribe as a, SubscriptionRegistry as a0, SubscriptionState as a1, SyncWatermark as a2, WorkflowInstanceAction as a3, WorkflowInstanceDetail as a4, WorkflowInstancePage as a5, WorkflowInstanceStatus as a6, WorkflowInstanceSummary as a7, WorkflowStepDetail as a8, createLocalStore as a9, createStream as aa, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, BatchSlot as e, ClientMessage as f, ClientShapeSubscribeMessage as g, ClientShapeUnsubscribeMessage as h, ConnectionStatus as i, FunctionArgumentDescriptor as j, FunctionDescriptor as k, GlobalFacetValue as l, GlobalFilterClause as m, GlobalTableInfo as n, GlobalTablePage as o, LunoraClientError as p, LunoraClientOptions as q, MutationSettledEvent as r, OptimisticLocalStore as s, OptimisticUpdate as t, OutboxMutation as u, OutboxSink as v, PersistedMutation as w, RowOp as x, RpcEnvelope as y, RpcResponseBody as z };
@@ -358,6 +358,7 @@ interface RpcEnvelope {
358
358
  type RpcResponseBody = {
359
359
  error: {
360
360
  code: string;
361
+ data?: unknown;
361
362
  message: string;
362
363
  };
363
364
  } | {
@@ -1151,6 +1152,19 @@ interface SyncWatermark {
1151
1152
  checkpoint?: number;
1152
1153
  mutationId?: number;
1153
1154
  }
1155
+ /** An `Error` carrying the server's machine-readable `code` and (for a `LunoraError`) structured `data`. The client's public error contract for RPC/batch failures. */
1156
+ type LunoraClientError = Error & {
1157
+ code?: string;
1158
+ data?: unknown;
1159
+ };
1160
+ /** One demuxed result slot of a {@link LunoraClient.batch} call (plan 088). */
1161
+ type BatchSlot = {
1162
+ error: LunoraClientError;
1163
+ ok: false;
1164
+ } | {
1165
+ ok: true;
1166
+ value: unknown;
1167
+ };
1154
1168
  /**
1155
1169
  * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
1156
1170
  * a single multiplexed WebSocket.
@@ -1498,6 +1512,25 @@ declare class LunoraClient {
1498
1512
  shardKey?: string;
1499
1513
  }): Promise<ReturnOf<F>>;
1500
1514
  /**
1515
+ * Batch several independent calls into ONE round trip (plan 088). Each call is
1516
+ * dispatched server-side exactly as an individual RPC — per-shard
1517
+ * authorization, `(identity, mutationId)` idempotency, and custom-mutator
1518
+ * watermark ordering are all preserved — and the worker splits the batch by
1519
+ * shard so calls to different shards fan out to their own DOs. Results are
1520
+ * demuxed back in input order; a failing call does NOT fail the batch (its
1521
+ * slot carries `{ ok: false, error }`, with `.code`/`.data` reconstructed like
1522
+ * a single call). Args/results ride the value codec (bytes/bigint survive).
1523
+ *
1524
+ * No promise pipelining and no capability passing — a call's args cannot
1525
+ * reference another call's result (see plan 088 §fence; capabilities are
1526
+ * incompatible with DO hibernation).
1527
+ */
1528
+ batch(calls: ReadonlyArray<{
1529
+ args?: Record<string, unknown>;
1530
+ fn: FunctionReference;
1531
+ shardKey?: string;
1532
+ }>): Promise<BatchSlot[]>;
1533
+ /**
1501
1534
  * Invoke a mutation. Errors propagate as rejections.
1502
1535
  *
1503
1536
  * Offline-queue semantics: a mutation is queued (and replayed on reconnect)
@@ -2202,5 +2235,77 @@ declare class LunoraClient {
2202
2235
  */
2203
2236
  private clearQueryCacheForIdentityChange;
2204
2237
  private flushOfflineQueue;
2238
+ /**
2239
+ * Partition already-gated writes into the encodable ones (returned) and reject
2240
+ * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
2241
+ * or class instance in a `v.any()` field) can NEVER replay — the codec failure
2242
+ * is deterministic, not transient. Rejecting here is essential: otherwise
2243
+ * `encodeWire` throws mid-flush, is classified as transient (a codec error has
2244
+ * no `.code`), and re-queues forever — a silent hang where the caller's Promise
2245
+ * never settles and the optimistic write never rolls back. Encoding is cheap;
2246
+ * the flush is the slow reconnect path.
2247
+ */
2248
+ private encodableOrSettleTerminal;
2249
+ /**
2250
+ * Identity guard for one queued write about to replay: a write stamped under
2251
+ * one identity must never replay under another. The live `queuedIdentities`
2252
+ * map is the source of truth for the current session; a hydrated write whose
2253
+ * id isn't in the map falls back to the stamp persisted with the record
2254
+ * (`item.identity`), so a reload can't replay another user's queued writes.
2255
+ * Only legacy records (persisted before stamps were durable —
2256
+ * `item.identity === undefined`) replay under whatever identity is current.
2257
+ *
2258
+ * `Map.get` returns `undefined` for unstamped/hydrated ids and `item.identity`
2259
+ * is `undefined` for legacy records; a persisted `null` (queued while signed
2260
+ * out) is a real value that must not collapse into `undefined` — hence the
2261
+ * explicit `=== undefined` check rather than `??`. Returns `true` when the
2262
+ * write may replay; otherwise settles it `OFFLINE_IDENTITY_CHANGED` and returns
2263
+ * `false`. Either way the live stamp is consumed.
2264
+ */
2265
+ private passesReplayIdentityGate;
2266
+ /** Settle a write that replayed successfully: confirm its optimistic layer against the echoed commit cursor BEFORE resolving, so the gapless drop is in place when the awaiter (and any confirming frame) observes the settle. */
2267
+ private settleReplaySuccess;
2268
+ /** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
2269
+ private settleReplayTerminal;
2270
+ /**
2271
+ * Replay already-identity-gated writes one at a time on the single-call `/rpc`
2272
+ * path, preserving FIFO order (parallel `.then()` chains would race the
2273
+ * ordering callers depend on). Each replays under its stable `mutationId` so
2274
+ * the server dedups a write it already committed (exactly-once). A coded error
2275
+ * is a server verdict (drop it); a codeless (transport/transient) failure stops
2276
+ * the flush and re-queues this write and every unreplayed one for the next
2277
+ * reconnect — their callers stay pending, and the identity guard re-applies on
2278
+ * retry via each record's persisted stamp.
2279
+ */
2280
+ private replaySequential;
2281
+ /**
2282
+ * Coalesce already-identity-gated writes for a single shard into ONE
2283
+ * `/_lunora/rpc-batch` round trip (plan 088 follow-on). The worker forwards
2284
+ * them to the shard DO, which replays each through its single-call dispatch, so
2285
+ * per-entry `mutationId` idempotency and in-order application are inherited from
2286
+ * the proven path. Per-slot demux mirrors {@link replaySequential}'s
2287
+ * classification: success confirms the optimistic layer against the echoed
2288
+ * `commitCursor`; a coded application verdict is terminal; a transient shard
2289
+ * failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
2290
+ * transport failure re-queues for the next reconnect (never dropping a durable
2291
+ * write). A whole-batch coded rejection (bad request / authorization denial the
2292
+ * server reached a verdict on) is terminal for every entry.
2293
+ *
2294
+ * Returns the writes that must be re-queued and `stop` — `true` when the whole
2295
+ * chunk failed at the transport level, so the caller leaves later chunks queued
2296
+ * rather than sending on. The caller re-queues once, in order, so requeuing is
2297
+ * NOT done here.
2298
+ */
2299
+ private replayBatched;
2300
+ /**
2301
+ * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
2302
+ * in input order. Each slot's envelope classifies its write the same way
2303
+ * {@link replaySequential} does: a success confirms the optimistic layer
2304
+ * against the echoed `commitCursor`; a coded application verdict is terminal;
2305
+ * a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
2306
+ * server never returned is returned for the caller to re-queue.
2307
+ * @returns the writes that must be re-queued (transient slots), in input order
2308
+ */
2309
+ private settleReplayBatchSlots;
2205
2310
  }
2206
- export { SubscriptionState as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, SchedulerStatus as E, FunctionReference as F, GlobalFacetResult as G, ServerMessage as H, ServerPokeEndMessage as I, ServerPokePartMessage as J, ServerPokeStartMessage as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficEntry as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ShardTrafficResult as T, User as U, StorageListPage as V, StorageObject as W, StreamHandle as X, StreamIterable as Y, SubscriptionCallback as Z, SubscriptionRegistry as _, Unsubscribe as a, SyncWatermark as a0, WorkflowInstanceAction as a1, WorkflowInstanceDetail as a2, WorkflowInstancePage as a3, WorkflowInstanceStatus as a4, WorkflowInstanceSummary as a5, WorkflowStepDetail as a6, createLocalStore as a7, createStream as a8, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ClientShapeSubscribeMessage as f, ClientShapeUnsubscribeMessage as g, ConnectionStatus as h, FunctionArgumentDescriptor as i, FunctionDescriptor as j, GlobalFacetValue as k, GlobalFilterClause as l, GlobalTableInfo as m, GlobalTablePage as n, LunoraClientOptions as o, MutationSettledEvent as p, OptimisticLocalStore as q, OptimisticUpdate as r, OutboxMutation as s, OutboxSink as t, PersistedMutation as u, RowOp as v, RpcEnvelope as w, RpcResponseBody as x, ScheduleRecord as y, SchedulerPoolStatus as z };
2311
+ export { SubscriptionCallback as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, ScheduleRecord as E, FunctionReference as F, GlobalFacetResult as G, SchedulerPoolStatus as H, SchedulerStatus as I, ServerMessage as J, ServerPokeEndMessage as K, LunoraClient as L, MutationCallOptions as M, ServerPokePartMessage as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ServerPokeStartMessage as T, User as U, ShardTrafficEntry as V, ShardTrafficResult as W, StorageListPage as X, StorageObject as Y, StreamHandle as Z, StreamIterable as _, Unsubscribe as a, SubscriptionRegistry as a0, SubscriptionState as a1, SyncWatermark as a2, WorkflowInstanceAction as a3, WorkflowInstanceDetail as a4, WorkflowInstancePage as a5, WorkflowInstanceStatus as a6, WorkflowInstanceSummary as a7, WorkflowStepDetail as a8, createLocalStore as a9, createStream as aa, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, BatchSlot as e, ClientMessage as f, ClientShapeSubscribeMessage as g, ClientShapeUnsubscribeMessage as h, ConnectionStatus as i, FunctionArgumentDescriptor as j, FunctionDescriptor as k, GlobalFacetValue as l, GlobalFilterClause as m, GlobalTableInfo as n, GlobalTablePage as o, LunoraClientError as p, LunoraClientOptions as q, MutationSettledEvent as r, OptimisticLocalStore as s, OptimisticUpdate as t, OutboxMutation as u, OutboxSink as v, PersistedMutation as w, RowOp as x, RpcEnvelope as y, RpcResponseBody as z };
@@ -84,7 +84,13 @@ class OfflineQueue {
84
84
  if (!this.persistence) {
85
85
  return [];
86
86
  }
87
- const persisted = await this.persistence.load();
87
+ let persisted;
88
+ try {
89
+ persisted = await this.persistence.load();
90
+ } catch (error) {
91
+ reportPersistenceError(this.onPersistenceError, "load", error);
92
+ throw error;
93
+ }
88
94
  const shardKeys = /* @__PURE__ */ new Set();
89
95
  for (const mutation of persisted) {
90
96
  if (this.items.some((item) => item.id === mutation.id)) {
@@ -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-Dm1BYq19.mjs";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-DTwbfUWF.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,4 +1,4 @@
1
- import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-Dm1BYq19.js";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-DTwbfUWF.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
@@ -8,12 +8,22 @@ const stableStringify = (value) => {
8
8
  if (value === void 0) {
9
9
  return "null";
10
10
  }
11
+ if (typeof value === "bigint") {
12
+ throw new TypeError("stableStringify: cannot use a bigint in a cache key (query/subscription/shape args) — pass it as a string");
13
+ }
11
14
  if (value === null || typeof value !== "object") {
12
15
  return JSON.stringify(value);
13
16
  }
14
17
  if (Array.isArray(value)) {
15
18
  return `[${value.map((item) => stableStringify(item)).join(",")}]`;
16
19
  }
20
+ const proto = Object.getPrototypeOf(value);
21
+ if (proto !== null && proto !== Object.prototype) {
22
+ const name = value.constructor?.name ?? "value";
23
+ throw new TypeError(
24
+ `stableStringify: cannot use a ${name} in a cache key (query/subscription/shape args) — only plain objects, arrays, and JSON primitives are supported`
25
+ );
26
+ }
17
27
  const record = value;
18
28
  const keys = Object.keys(record).toSorted(compareKeys);
19
29
  const parts = [];
@@ -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-Dm1BYq19.mjs";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-Dm1BYq19.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-DTwbfUWF.mjs";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-DTwbfUWF.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-Dm1BYq19.js";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-Dm1BYq19.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-DTwbfUWF.js";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-DTwbfUWF.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-Dm1BYq19.mjs";
2
- export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-Dm1BYq19.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-BfWzGJWV.mjs";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-DTwbfUWF.mjs";
2
+ export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-DTwbfUWF.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-BEp0JJeu.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-Dm1BYq19.js";
2
- export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-Dm1BYq19.js";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-Sp_Ef-_u.js";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-DTwbfUWF.js";
2
+ export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-DTwbfUWF.js";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-CK2IYf5B.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-CtklXrs2.mjs';
3
+ export { createServerClient } from '../packem_shared/createServerClient-DQu_BVen.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.11",
3
+ "version": "1.0.0-alpha.13",
4
4
  "description": "Lunora browser SDK: WebSocket transport, optimistic updates, and an offline mutation queue",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- export { S as SubscriptionRegistry } from './subscription-C1Jy7HiF.mjs';