@lunora/client 1.0.0-alpha.71 → 1.0.0-alpha.73

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/index.d.mts +2 -2
  3. package/dist/auth/index.d.ts +2 -2
  4. package/dist/index.d.mts +46 -23
  5. package/dist/index.d.ts +46 -23
  6. package/dist/index.mjs +1 -1
  7. package/dist/packem_shared/CONFLICT_ERROR_CODE-BOeeP_Wl.mjs +1 -0
  8. package/dist/packem_shared/LunoraClient-CQC4SiA4.mjs +1 -0
  9. package/dist/packem_shared/OfflineQueue-ZapYrAVi.mjs +1 -0
  10. package/dist/packem_shared/{SubscriptionRegistry-BQPU90ST.mjs → SubscriptionRegistry-BQTQZYoI.mjs} +1 -1
  11. package/dist/packem_shared/applyDelta-Bz-B1XB9.mjs +1 -0
  12. package/dist/packem_shared/createAsyncStoragePersistence-CeUW9NTj.mjs +1 -0
  13. package/dist/packem_shared/{createAsyncStorageQueryCache-MoM77Nwg.mjs → createAsyncStorageQueryCache-VgJNCeEn.mjs} +1 -1
  14. package/dist/packem_shared/createInMemoryPersistence-Cz2W6XVj.mjs +1 -0
  15. package/dist/packem_shared/createLocalStore-CJ0VRKPe.mjs +1 -0
  16. package/dist/packem_shared/{createServerClient-BevCicNQ.mjs → createServerClient-DsyHLN1F.mjs} +1 -1
  17. package/dist/packem_shared/{createSnapshotPrecondition-C4bSgHkg.mjs → createSnapshotPrecondition-B7kQcZUF.mjs} +1 -1
  18. package/dist/packem_shared/{delta-merge-BPM4sFID.mjs → delta-merge-Ds6tHkFX.mjs} +1 -1
  19. package/dist/packem_shared/{local-store-CNgeGW02.mjs → local-store-irMi9H2G.mjs} +1 -1
  20. package/dist/packem_shared/{lunora-client.d-BSNUC85h.d.mts → lunora-client.d-6hU156nD.d.mts} +149 -24
  21. package/dist/packem_shared/{lunora-client.d-BkDgDzTg.d.ts → lunora-client.d-B0t5v2n9.d.ts} +149 -24
  22. package/dist/packem_shared/offline-queue-xntsWiKt.mjs +1 -0
  23. package/dist/packem_shared/{preload.d-Do17SOqk.d.ts → preload.d-Dl-ja0VQ.d.ts} +1 -1
  24. package/dist/packem_shared/{preload.d-XsWPDlX6.d.mts → preload.d-M7qP6TwL.d.mts} +1 -1
  25. package/dist/packem_shared/wire-codec-BOMWQpoF.mjs +1 -0
  26. package/dist/packem_shared/{wire-key-C9UKT-C3.mjs → wire-key-CmT1XyQI.mjs} +1 -1
  27. package/dist/query/index.d.mts +2 -2
  28. package/dist/query/index.d.ts +2 -2
  29. package/dist/service.mjs +1 -1
  30. package/dist/ssr/index.d.mts +2 -2
  31. package/dist/ssr/index.d.ts +2 -2
  32. package/dist/ssr/index.mjs +1 -1
  33. package/package.json +2 -2
  34. package/dist/packem_shared/CONFLICT_ERROR_CODE-CDDneB-G.mjs +0 -1
  35. package/dist/packem_shared/LunoraClient-B9UMg5lY.mjs +0 -1
  36. package/dist/packem_shared/OfflineQueue-B6IWyX5W.mjs +0 -1
  37. package/dist/packem_shared/applyDelta-BiegnbZR.mjs +0 -1
  38. package/dist/packem_shared/createAsyncStoragePersistence-CNdM5o1u.mjs +0 -1
  39. package/dist/packem_shared/createInMemoryPersistence-C0qm7c47.mjs +0 -1
  40. package/dist/packem_shared/createLocalStore-v6oiATqG.mjs +0 -1
  41. package/dist/packem_shared/offline-queue-8mpNPwG8.mjs +0 -1
  42. package/dist/packem_shared/wire-codec-BfIZrTJe.mjs +0 -1
@@ -1,5 +1,5 @@
1
+ import { LunoraErrorCodeInput, LunoraErrorCode } from '@lunora/errors';
1
2
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.mjs";
2
- import { LunoraErrorCode } from '@lunora/errors';
3
3
  import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
4
4
  /**
5
5
  * Reactive key-value store for local-only client state.
@@ -113,7 +113,7 @@ interface ReconnectOptions {
113
113
  maxDelayMs?: number;
114
114
  }
115
115
  /** Which durable-storage operation failed, passed to {@link OfflineQueueOptions.onPersistenceError}. */
116
- type PersistenceOperation = "append" | "clear" | "load" | "remove";
116
+ type PersistenceOperation = "append" | "clear" | "load" | "remove" | "replace";
117
117
  /** Context handed to a persistence-error handler. */
118
118
  interface PersistenceErrorContext {
119
119
  readonly error: unknown;
@@ -195,6 +195,22 @@ interface PersistenceAdapter {
195
195
  load: () => Promise<PersistedMutation[]>;
196
196
  /** Remove a mutation by id once it has been replayed (resolved or rejected). */
197
197
  remove: (id: string) => Promise<void>;
198
+ /**
199
+ * Overwrite an already-persisted mutation IN PLACE, keeping its position in
200
+ * FIFO order. Used when a queued write's identity stamp is rewritten after a
201
+ * sign-in / sign-out.
202
+ *
203
+ * Must be atomic: a `remove` + `append` pair has a window where a process
204
+ * stop leaves the mutation in no durable store at all, and the in-memory
205
+ * entry has already advanced, so a reload loses the write outright. It also
206
+ * moved the record to the BACK of the queue, replaying it out of the order
207
+ * it was issued in. Implementations do the whole swap under one transaction
208
+ * (or one serialized blob write).
209
+ *
210
+ * A mutation whose id is not present is left alone — the record was drained
211
+ * concurrently and re-inserting it would replay a settled write.
212
+ */
213
+ replace: (mutation: PersistedMutation) => Promise<void>;
198
214
  }
199
215
  /**
200
216
  * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
@@ -378,10 +394,17 @@ interface LunoraClientOptions {
378
394
  */
379
395
  heartbeatIntervalMs?: number;
380
396
  /**
381
- * When `true` and a `queryCache` is active, framework hooks (React, Vue, …)
382
- * wait for the durable cache to finish hydrating before their first render
383
- * with an enabled subscription, so users see cached data instead of an
384
- * undefined flash before the socket round-trip. Defaults to `false`.
397
+ * When `true` and a `queryCache` is active, React's `useQuery` holds its
398
+ * TanStack query disabled until the durable cache has finished loading
399
+ * (`whenReady()`), so its first enabled render can seed the cached value
400
+ * instead of issuing an HTTP read that the cache would immediately
401
+ * overwrite. Defaults to `false`.
402
+ *
403
+ * It is ONLY React's `useQuery` that defers — the Vue, Svelte, Solid and
404
+ * Angular hooks subscribe at mount regardless of this flag, and so does
405
+ * React's own subscription registry. They do not need the gate: a
406
+ * subscription opened before the load completes is seeded by the load
407
+ * itself, so a cached value reaches the first subscriber either way.
385
408
  *
386
409
  * Requires `queryCache` to be set (not `false`); silently ignored otherwise.
387
410
  */
@@ -1085,7 +1108,16 @@ interface WorkflowInstancePage {
1085
1108
  type SubscriptionCallback = (data: unknown) => void;
1086
1109
  /** A subscription-scoped error the server pushed for this subscription id. */
1087
1110
  interface SubscriptionError {
1088
- code?: string;
1111
+ /**
1112
+ * The coded reason, when the frame carried one. `LunoraErrorCodeInput`, not
1113
+ * `LunoraErrorCode`: the catalog codes autocomplete for a consumer branching
1114
+ * on this (the shard sends `BAD_SUBSCRIPTION_ARGS`, `TOO_MANY_SUBSCRIPTIONS`,
1115
+ * `SUBSCRIPTION_PERSIST_FAILED`, …; the client itself adds
1116
+ * `WIRE_DECODE_FAILED` for a frame `decodeWire` refuses), but this value is
1117
+ * read verbatim off the wire and nothing validates it against the catalog,
1118
+ * so narrowing it to `LunoraErrorCode` would be a lie a newer server tells.
1119
+ */
1120
+ code?: LunoraErrorCodeInput;
1089
1121
  message: string;
1090
1122
  }
1091
1123
  type SubscriptionErrorCallback = (error: SubscriptionError) => void;
@@ -1740,6 +1772,26 @@ declare class LunoraClient {
1740
1772
  * racing write was never in the queue.
1741
1773
  */
1742
1774
  private readonly offlineFlushes;
1775
+ /**
1776
+ * Per-shard replay backoff, keyed by {@link connectionKey} exactly as the
1777
+ * flushes and their timers are: `delayMs` is the longest hint the CURRENT
1778
+ * flush of that key was given (written by
1779
+ * {@link LunoraClient.noteReplayRetryDelay}, consumed once by
1780
+ * {@link LunoraClient.scheduleRateLimitedRetry} at the end of the drain), and
1781
+ * `attempts` counts its consecutive failed flushes, which is what the
1782
+ * hintless backoff ramps on.
1783
+ *
1784
+ * Keyed, not a single field: flushes are per shard key and run concurrently,
1785
+ * so one field means one limited shard sets the wait for every other shard
1786
+ * and two flushes overwrite — then consume — each other's delay, leaving one
1787
+ * of them with nothing scheduled at all. Evicted by
1788
+ * {@link LunoraClient.scheduleRateLimitedRetry} the moment a key has nothing
1789
+ * left to retry, so an app that shards per document does not accumulate an
1790
+ * entry per document it ever wrote.
1791
+ */
1792
+ private readonly replayRetryState;
1793
+ /** Pending rate-limit retry flushes, keyed by {@link connectionKey}, so `close()` can cancel them. */
1794
+ private readonly replayRetryTimers;
1743
1795
  private readonly shapeSubscriptions;
1744
1796
  /**
1745
1797
  * In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by
@@ -2803,10 +2855,12 @@ declare class LunoraClient {
2803
2855
  * `subscribe` for the same triple joins the existing registration and shares
2804
2856
  * its value, cursor and optimistic layers.
2805
2857
  *
2806
- * Not available on a `crossTabSync` FOLLOWER tab: the cross-tab channel only
2807
- * carries the LEADER's own subscriptions outward, so a follower would receive
2808
- * this query's frames only if the leader happened to hold it too. Throws
2809
- * `NOT_IMPLEMENTED` there see {@link LunoraClientOptions.crossTabSync}.
2858
+ * Available on a `crossTabSync` FOLLOWER tab, unlike the other socket-backed
2859
+ * surfaces: registering here is exactly what lets the cross-tab relay deliver
2860
+ * the leader's broadcasts (see the note in the body). The caveat is that the
2861
+ * channel only carries the LEADER's own subscriptions outward, so a follower
2862
+ * sees this query's frames only while the leader holds it too — see
2863
+ * {@link LunoraClientOptions.crossTabSync}.
2810
2864
  */
2811
2865
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
2812
2866
  onCheckpoint?: (watermark: SyncWatermark) => void;
@@ -2988,13 +3042,35 @@ declare class LunoraClient {
2988
3042
  */
2989
3043
  private hydrateAsOutboxLeader;
2990
3044
  /**
2991
- * Load every cached query into {@link hydratedQueryCache} so the next
2992
- * `subscribe()` for each key seeds its initial value off disk. A
2993
- * subscription created before this resolves simply misses the cache (it
2994
- * gets a live snapshot as before); the gate at seed time also drops any
2995
- * entry whose stamped identity no longer matches the current one.
3045
+ * Load every cached query so a `subscribe()` for the key seeds its initial
3046
+ * value off disk.
3047
+ *
3048
+ * The load is asynchronous but every framework adapter subscribes
3049
+ * SYNCHRONOUSLY at mount, so the subscriptions that most want the cache
3050
+ * already exist by the time it lands. Those are seeded here and their entry
3051
+ * is dropped: an entry that stayed in {@link hydratedQueryCache} behind a
3052
+ * live subscription would be consumed by the NEXT subscribe of the same key
3053
+ * (a remount after navigating away) and replay the previous session's value
3054
+ * over whatever the socket had since delivered. A cache entry therefore
3055
+ * never outlives a live subscription for its key — it is either handed to
3056
+ * that subscription or discarded.
3057
+ *
3058
+ * Only keys with no live subscription are held for a later `subscribe()`;
3059
+ * the identity gate applies to both paths, so a signed-out cache never leaks
3060
+ * into a new session.
2996
3061
  */
2997
3062
  private hydrateQueryCache;
3063
+ /**
3064
+ * Hand a loaded read-cache entry to a subscription that was opened before
3065
+ * the load resolved — the same value, cursor and epoch {@link subscribe}
3066
+ * would have taken from {@link takeHydratedCache} had the load finished
3067
+ * first, so the cursor still rides the `subscribe` frame as `sinceSeq` (that
3068
+ * frame only goes out once the socket opens, well after this microtask).
3069
+ *
3070
+ * A no-op once the socket has delivered anything for this key: a live value
3071
+ * always beats the cache. Identity-gated exactly like `takeHydratedCache`.
3072
+ */
3073
+ private seedSubscriptionFromCache;
2998
3074
  /**
2999
3075
  * Consume the hydrated read-cache entry for a key (if any), gated on
3000
3076
  * identity. The entry is removed whether or not it matches — the cache only
@@ -3384,6 +3460,29 @@ declare class LunoraClient {
3384
3460
  */
3385
3461
  private flushOfflineQueue;
3386
3462
  private drainOfflineQueue;
3463
+ /**
3464
+ * Remember the longest delay this shard's flush was told (or worked out) to
3465
+ * wait, so the drain can honour it before trying again
3466
+ * ({@link LunoraClient.replayRetryState}). Counts the attempt either way:
3467
+ * that is what a hintless refusal backs off on.
3468
+ */
3469
+ private noteReplayRetryDelay;
3470
+ /**
3471
+ * Consume this shard's retry delay and re-flush it once the delay has
3472
+ * elapsed.
3473
+ *
3474
+ * Only a failure the server or an edge ANSWERED schedules anything (see
3475
+ * {@link replayRetryDelayMs}): a `fetch` that never landed is already covered
3476
+ * by the reconnect that will flush the queue, whereas a refused flush happens
3477
+ * over a socket that stays open — so without this the writes sit queued
3478
+ * indefinitely. One pending timer per shard; a second delay replaces it
3479
+ * rather than stacking flushes.
3480
+ *
3481
+ * Nothing left to retry on this key (drained, closed, or no delay) drops its
3482
+ * backoff state, which is both the reset after progress and what bounds the
3483
+ * map.
3484
+ */
3485
+ private scheduleRateLimitedRetry;
3387
3486
  /**
3388
3487
  * Partition already-gated writes into the encodable ones (returned) and reject
3389
3488
  * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
@@ -3451,11 +3550,14 @@ declare class LunoraClient {
3451
3550
  * Replay already-identity-gated writes one at a time on the single-call `/rpc`
3452
3551
  * path, preserving FIFO order (parallel `.then()` chains would race the
3453
3552
  * ordering callers depend on). Each replays under its stable `mutationId` so
3454
- * the server dedups a write it already committed (exactly-once). A coded error
3455
- * is a server verdict (drop it); a codeless (transport/transient) failure stops
3456
- * the flush and re-queues this write and every unreplayed one for the next
3457
- * reconnect their callers stay pending, and the identity guard re-applies on
3458
- * retry via each record's persisted stamp.
3553
+ * the server dedups a write it already committed (exactly-once). A server verdict
3554
+ * drops the write; a transient failure ({@link isTransientReplayFailure} — a
3555
+ * transport error, a shard the worker couldn't reach, a rate-limit refusal, or a
3556
+ * non-2xx carrying no `{ error }` envelope) stops the flush and re-queues this
3557
+ * write and every unreplayed one for the next reconnect — their callers stay
3558
+ * pending, and the identity guard re-applies on retry via each record's persisted
3559
+ * stamp. The batch path classifies a slot by the same rule, so a durable write's
3560
+ * fate never depends on how many siblings happened to be queued alongside it.
3459
3561
  */
3460
3562
  private replaySequential;
3461
3563
  /**
@@ -3465,24 +3567,47 @@ declare class LunoraClient {
3465
3567
  * per-entry `mutationId` idempotency and in-order application are inherited from
3466
3568
  * the proven path. Per-slot demux mirrors {@link replaySequential}'s
3467
3569
  * classification: success confirms the optimistic layer against the echoed
3468
- * `commitCursor`; a coded application verdict is terminal; a transient shard
3469
- * failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
3570
+ * `commitCursor`; a coded application verdict is terminal; a {@link
3571
+ * TRANSIENT_REPLAY_ERROR_CODES} code, a missing slot, or a whole-batch
3470
3572
  * transport failure re-queues for the next reconnect (never dropping a durable
3471
3573
  * write). A whole-batch coded rejection (bad request / authorization denial the
3472
3574
  * server reached a verdict on) is terminal for every entry.
3473
3575
  *
3576
+ * The body is also held under {@link MAX_BATCH_BODY_BYTES}: the worker caps a
3577
+ * batch body at 1 MiB and answers `413`, which is ONE refusal covering every
3578
+ * write in the chunk. An over-budget chunk is halved before it is sent, and a
3579
+ * `413` for a chunk of more than one write halves it and retries rather than
3580
+ * settling the whole chunk terminally — so a backlog of large writes still
3581
+ * commits, one bisection deeper.
3582
+ *
3474
3583
  * Returns the writes that must be re-queued and `stop` — `true` when the whole
3475
3584
  * chunk failed at the transport level, so the caller leaves later chunks queued
3476
3585
  * rather than sending on. The caller re-queues once, in order, so requeuing is
3477
3586
  * NOT done here.
3478
3587
  */
3479
3588
  private replayBatched;
3589
+ /**
3590
+ * Classify a `/_lunora/rpc-batch` reply that carried no per-slot results — one
3591
+ * outcome covering every entry in the chunk. A {@link TRANSIENT_REPLAY_ERROR_CODES}
3592
+ * code (an unreachable shard, a rate-limit refusal) or a
3593
+ * {@link TransportError} (an edge reply with no verdict in it) leaves every
3594
+ * write durable for the next attempt; anything else is a verdict reached on
3595
+ * the request itself, and settles all of them.
3596
+ */
3597
+ private settleWholeBatchError;
3598
+ /**
3599
+ * Split an over-large batch chunk in half and replay each half, preserving
3600
+ * FIFO order. Recurses through {@link replayBatched}, so a chunk keeps halving
3601
+ * until it fits (or reaches one write, which is then the server's verdict to
3602
+ * give). A `stop` on the first half leaves the second unsent and queued.
3603
+ */
3604
+ private replayBatchedHalves;
3480
3605
  /**
3481
3606
  * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
3482
3607
  * in input order. Each slot's envelope classifies its write the same way
3483
3608
  * {@link replaySequential} does: a success confirms the optimistic layer
3484
3609
  * against the echoed `commitCursor`; a coded application verdict is terminal;
3485
- * a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
3610
+ * a transient failure ({@link TRANSIENT_REPLAY_ERROR_CODES}) or a slot the
3486
3611
  * server never returned is returned for the caller to re-queue.
3487
3612
  * @returns the writes that must be re-queued (transient slots), in input order
3488
3613
  */
@@ -1,5 +1,5 @@
1
+ import { LunoraErrorCodeInput, LunoraErrorCode } from '@lunora/errors';
1
2
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.js";
2
- import { LunoraErrorCode } from '@lunora/errors';
3
3
  import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
4
4
  /**
5
5
  * Reactive key-value store for local-only client state.
@@ -113,7 +113,7 @@ interface ReconnectOptions {
113
113
  maxDelayMs?: number;
114
114
  }
115
115
  /** Which durable-storage operation failed, passed to {@link OfflineQueueOptions.onPersistenceError}. */
116
- type PersistenceOperation = "append" | "clear" | "load" | "remove";
116
+ type PersistenceOperation = "append" | "clear" | "load" | "remove" | "replace";
117
117
  /** Context handed to a persistence-error handler. */
118
118
  interface PersistenceErrorContext {
119
119
  readonly error: unknown;
@@ -195,6 +195,22 @@ interface PersistenceAdapter {
195
195
  load: () => Promise<PersistedMutation[]>;
196
196
  /** Remove a mutation by id once it has been replayed (resolved or rejected). */
197
197
  remove: (id: string) => Promise<void>;
198
+ /**
199
+ * Overwrite an already-persisted mutation IN PLACE, keeping its position in
200
+ * FIFO order. Used when a queued write's identity stamp is rewritten after a
201
+ * sign-in / sign-out.
202
+ *
203
+ * Must be atomic: a `remove` + `append` pair has a window where a process
204
+ * stop leaves the mutation in no durable store at all, and the in-memory
205
+ * entry has already advanced, so a reload loses the write outright. It also
206
+ * moved the record to the BACK of the queue, replaying it out of the order
207
+ * it was issued in. Implementations do the whole swap under one transaction
208
+ * (or one serialized blob write).
209
+ *
210
+ * A mutation whose id is not present is left alone — the record was drained
211
+ * concurrently and re-inserting it would replay a settled write.
212
+ */
213
+ replace: (mutation: PersistedMutation) => Promise<void>;
198
214
  }
199
215
  /**
200
216
  * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
@@ -378,10 +394,17 @@ interface LunoraClientOptions {
378
394
  */
379
395
  heartbeatIntervalMs?: number;
380
396
  /**
381
- * When `true` and a `queryCache` is active, framework hooks (React, Vue, …)
382
- * wait for the durable cache to finish hydrating before their first render
383
- * with an enabled subscription, so users see cached data instead of an
384
- * undefined flash before the socket round-trip. Defaults to `false`.
397
+ * When `true` and a `queryCache` is active, React's `useQuery` holds its
398
+ * TanStack query disabled until the durable cache has finished loading
399
+ * (`whenReady()`), so its first enabled render can seed the cached value
400
+ * instead of issuing an HTTP read that the cache would immediately
401
+ * overwrite. Defaults to `false`.
402
+ *
403
+ * It is ONLY React's `useQuery` that defers — the Vue, Svelte, Solid and
404
+ * Angular hooks subscribe at mount regardless of this flag, and so does
405
+ * React's own subscription registry. They do not need the gate: a
406
+ * subscription opened before the load completes is seeded by the load
407
+ * itself, so a cached value reaches the first subscriber either way.
385
408
  *
386
409
  * Requires `queryCache` to be set (not `false`); silently ignored otherwise.
387
410
  */
@@ -1085,7 +1108,16 @@ interface WorkflowInstancePage {
1085
1108
  type SubscriptionCallback = (data: unknown) => void;
1086
1109
  /** A subscription-scoped error the server pushed for this subscription id. */
1087
1110
  interface SubscriptionError {
1088
- code?: string;
1111
+ /**
1112
+ * The coded reason, when the frame carried one. `LunoraErrorCodeInput`, not
1113
+ * `LunoraErrorCode`: the catalog codes autocomplete for a consumer branching
1114
+ * on this (the shard sends `BAD_SUBSCRIPTION_ARGS`, `TOO_MANY_SUBSCRIPTIONS`,
1115
+ * `SUBSCRIPTION_PERSIST_FAILED`, …; the client itself adds
1116
+ * `WIRE_DECODE_FAILED` for a frame `decodeWire` refuses), but this value is
1117
+ * read verbatim off the wire and nothing validates it against the catalog,
1118
+ * so narrowing it to `LunoraErrorCode` would be a lie a newer server tells.
1119
+ */
1120
+ code?: LunoraErrorCodeInput;
1089
1121
  message: string;
1090
1122
  }
1091
1123
  type SubscriptionErrorCallback = (error: SubscriptionError) => void;
@@ -1740,6 +1772,26 @@ declare class LunoraClient {
1740
1772
  * racing write was never in the queue.
1741
1773
  */
1742
1774
  private readonly offlineFlushes;
1775
+ /**
1776
+ * Per-shard replay backoff, keyed by {@link connectionKey} exactly as the
1777
+ * flushes and their timers are: `delayMs` is the longest hint the CURRENT
1778
+ * flush of that key was given (written by
1779
+ * {@link LunoraClient.noteReplayRetryDelay}, consumed once by
1780
+ * {@link LunoraClient.scheduleRateLimitedRetry} at the end of the drain), and
1781
+ * `attempts` counts its consecutive failed flushes, which is what the
1782
+ * hintless backoff ramps on.
1783
+ *
1784
+ * Keyed, not a single field: flushes are per shard key and run concurrently,
1785
+ * so one field means one limited shard sets the wait for every other shard
1786
+ * and two flushes overwrite — then consume — each other's delay, leaving one
1787
+ * of them with nothing scheduled at all. Evicted by
1788
+ * {@link LunoraClient.scheduleRateLimitedRetry} the moment a key has nothing
1789
+ * left to retry, so an app that shards per document does not accumulate an
1790
+ * entry per document it ever wrote.
1791
+ */
1792
+ private readonly replayRetryState;
1793
+ /** Pending rate-limit retry flushes, keyed by {@link connectionKey}, so `close()` can cancel them. */
1794
+ private readonly replayRetryTimers;
1743
1795
  private readonly shapeSubscriptions;
1744
1796
  /**
1745
1797
  * In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by
@@ -2803,10 +2855,12 @@ declare class LunoraClient {
2803
2855
  * `subscribe` for the same triple joins the existing registration and shares
2804
2856
  * its value, cursor and optimistic layers.
2805
2857
  *
2806
- * Not available on a `crossTabSync` FOLLOWER tab: the cross-tab channel only
2807
- * carries the LEADER's own subscriptions outward, so a follower would receive
2808
- * this query's frames only if the leader happened to hold it too. Throws
2809
- * `NOT_IMPLEMENTED` there see {@link LunoraClientOptions.crossTabSync}.
2858
+ * Available on a `crossTabSync` FOLLOWER tab, unlike the other socket-backed
2859
+ * surfaces: registering here is exactly what lets the cross-tab relay deliver
2860
+ * the leader's broadcasts (see the note in the body). The caveat is that the
2861
+ * channel only carries the LEADER's own subscriptions outward, so a follower
2862
+ * sees this query's frames only while the leader holds it too — see
2863
+ * {@link LunoraClientOptions.crossTabSync}.
2810
2864
  */
2811
2865
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
2812
2866
  onCheckpoint?: (watermark: SyncWatermark) => void;
@@ -2988,13 +3042,35 @@ declare class LunoraClient {
2988
3042
  */
2989
3043
  private hydrateAsOutboxLeader;
2990
3044
  /**
2991
- * Load every cached query into {@link hydratedQueryCache} so the next
2992
- * `subscribe()` for each key seeds its initial value off disk. A
2993
- * subscription created before this resolves simply misses the cache (it
2994
- * gets a live snapshot as before); the gate at seed time also drops any
2995
- * entry whose stamped identity no longer matches the current one.
3045
+ * Load every cached query so a `subscribe()` for the key seeds its initial
3046
+ * value off disk.
3047
+ *
3048
+ * The load is asynchronous but every framework adapter subscribes
3049
+ * SYNCHRONOUSLY at mount, so the subscriptions that most want the cache
3050
+ * already exist by the time it lands. Those are seeded here and their entry
3051
+ * is dropped: an entry that stayed in {@link hydratedQueryCache} behind a
3052
+ * live subscription would be consumed by the NEXT subscribe of the same key
3053
+ * (a remount after navigating away) and replay the previous session's value
3054
+ * over whatever the socket had since delivered. A cache entry therefore
3055
+ * never outlives a live subscription for its key — it is either handed to
3056
+ * that subscription or discarded.
3057
+ *
3058
+ * Only keys with no live subscription are held for a later `subscribe()`;
3059
+ * the identity gate applies to both paths, so a signed-out cache never leaks
3060
+ * into a new session.
2996
3061
  */
2997
3062
  private hydrateQueryCache;
3063
+ /**
3064
+ * Hand a loaded read-cache entry to a subscription that was opened before
3065
+ * the load resolved — the same value, cursor and epoch {@link subscribe}
3066
+ * would have taken from {@link takeHydratedCache} had the load finished
3067
+ * first, so the cursor still rides the `subscribe` frame as `sinceSeq` (that
3068
+ * frame only goes out once the socket opens, well after this microtask).
3069
+ *
3070
+ * A no-op once the socket has delivered anything for this key: a live value
3071
+ * always beats the cache. Identity-gated exactly like `takeHydratedCache`.
3072
+ */
3073
+ private seedSubscriptionFromCache;
2998
3074
  /**
2999
3075
  * Consume the hydrated read-cache entry for a key (if any), gated on
3000
3076
  * identity. The entry is removed whether or not it matches — the cache only
@@ -3384,6 +3460,29 @@ declare class LunoraClient {
3384
3460
  */
3385
3461
  private flushOfflineQueue;
3386
3462
  private drainOfflineQueue;
3463
+ /**
3464
+ * Remember the longest delay this shard's flush was told (or worked out) to
3465
+ * wait, so the drain can honour it before trying again
3466
+ * ({@link LunoraClient.replayRetryState}). Counts the attempt either way:
3467
+ * that is what a hintless refusal backs off on.
3468
+ */
3469
+ private noteReplayRetryDelay;
3470
+ /**
3471
+ * Consume this shard's retry delay and re-flush it once the delay has
3472
+ * elapsed.
3473
+ *
3474
+ * Only a failure the server or an edge ANSWERED schedules anything (see
3475
+ * {@link replayRetryDelayMs}): a `fetch` that never landed is already covered
3476
+ * by the reconnect that will flush the queue, whereas a refused flush happens
3477
+ * over a socket that stays open — so without this the writes sit queued
3478
+ * indefinitely. One pending timer per shard; a second delay replaces it
3479
+ * rather than stacking flushes.
3480
+ *
3481
+ * Nothing left to retry on this key (drained, closed, or no delay) drops its
3482
+ * backoff state, which is both the reset after progress and what bounds the
3483
+ * map.
3484
+ */
3485
+ private scheduleRateLimitedRetry;
3387
3486
  /**
3388
3487
  * Partition already-gated writes into the encodable ones (returned) and reject
3389
3488
  * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
@@ -3451,11 +3550,14 @@ declare class LunoraClient {
3451
3550
  * Replay already-identity-gated writes one at a time on the single-call `/rpc`
3452
3551
  * path, preserving FIFO order (parallel `.then()` chains would race the
3453
3552
  * ordering callers depend on). Each replays under its stable `mutationId` so
3454
- * the server dedups a write it already committed (exactly-once). A coded error
3455
- * is a server verdict (drop it); a codeless (transport/transient) failure stops
3456
- * the flush and re-queues this write and every unreplayed one for the next
3457
- * reconnect their callers stay pending, and the identity guard re-applies on
3458
- * retry via each record's persisted stamp.
3553
+ * the server dedups a write it already committed (exactly-once). A server verdict
3554
+ * drops the write; a transient failure ({@link isTransientReplayFailure} — a
3555
+ * transport error, a shard the worker couldn't reach, a rate-limit refusal, or a
3556
+ * non-2xx carrying no `{ error }` envelope) stops the flush and re-queues this
3557
+ * write and every unreplayed one for the next reconnect — their callers stay
3558
+ * pending, and the identity guard re-applies on retry via each record's persisted
3559
+ * stamp. The batch path classifies a slot by the same rule, so a durable write's
3560
+ * fate never depends on how many siblings happened to be queued alongside it.
3459
3561
  */
3460
3562
  private replaySequential;
3461
3563
  /**
@@ -3465,24 +3567,47 @@ declare class LunoraClient {
3465
3567
  * per-entry `mutationId` idempotency and in-order application are inherited from
3466
3568
  * the proven path. Per-slot demux mirrors {@link replaySequential}'s
3467
3569
  * classification: success confirms the optimistic layer against the echoed
3468
- * `commitCursor`; a coded application verdict is terminal; a transient shard
3469
- * failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
3570
+ * `commitCursor`; a coded application verdict is terminal; a {@link
3571
+ * TRANSIENT_REPLAY_ERROR_CODES} code, a missing slot, or a whole-batch
3470
3572
  * transport failure re-queues for the next reconnect (never dropping a durable
3471
3573
  * write). A whole-batch coded rejection (bad request / authorization denial the
3472
3574
  * server reached a verdict on) is terminal for every entry.
3473
3575
  *
3576
+ * The body is also held under {@link MAX_BATCH_BODY_BYTES}: the worker caps a
3577
+ * batch body at 1 MiB and answers `413`, which is ONE refusal covering every
3578
+ * write in the chunk. An over-budget chunk is halved before it is sent, and a
3579
+ * `413` for a chunk of more than one write halves it and retries rather than
3580
+ * settling the whole chunk terminally — so a backlog of large writes still
3581
+ * commits, one bisection deeper.
3582
+ *
3474
3583
  * Returns the writes that must be re-queued and `stop` — `true` when the whole
3475
3584
  * chunk failed at the transport level, so the caller leaves later chunks queued
3476
3585
  * rather than sending on. The caller re-queues once, in order, so requeuing is
3477
3586
  * NOT done here.
3478
3587
  */
3479
3588
  private replayBatched;
3589
+ /**
3590
+ * Classify a `/_lunora/rpc-batch` reply that carried no per-slot results — one
3591
+ * outcome covering every entry in the chunk. A {@link TRANSIENT_REPLAY_ERROR_CODES}
3592
+ * code (an unreachable shard, a rate-limit refusal) or a
3593
+ * {@link TransportError} (an edge reply with no verdict in it) leaves every
3594
+ * write durable for the next attempt; anything else is a verdict reached on
3595
+ * the request itself, and settles all of them.
3596
+ */
3597
+ private settleWholeBatchError;
3598
+ /**
3599
+ * Split an over-large batch chunk in half and replay each half, preserving
3600
+ * FIFO order. Recurses through {@link replayBatched}, so a chunk keeps halving
3601
+ * until it fits (or reaches one write, which is then the server's verdict to
3602
+ * give). A `stop` on the first half leaves the second unsent and queued.
3603
+ */
3604
+ private replayBatchedHalves;
3480
3605
  /**
3481
3606
  * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
3482
3607
  * in input order. Each slot's envelope classifies its write the same way
3483
3608
  * {@link replaySequential} does: a success confirms the optimistic layer
3484
3609
  * against the echoed `commitCursor`; a coded application verdict is terminal;
3485
- * a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
3610
+ * a transient failure ({@link TRANSIENT_REPLAY_ERROR_CODES}) or a slot the
3486
3611
  * server never returned is returned for the caller to re-queue.
3487
3612
  * @returns the writes that must be re-queued (transient slots), in input order
3488
3613
  */
@@ -0,0 +1 @@
1
+ let h=0;const d=()=>{if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();h+=1;const r=typeof crypto<"u"&&typeof crypto.getRandomValues=="function"?Array.from(crypto.getRandomValues(new Uint8Array(8)),t=>t.toString(16).padStart(2,"0")).join(""):Math.random().toString(16).slice(2,12);return`id_${Date.now().toString(36)}_${h.toString(36)}_${r}`},f=(r,t)=>r!==void 0&&t!==r,u=d,o=(r,t,e,i)=>{if(r)try{r({error:e,mutationId:i,operation:t});return}catch{}console.warn(`[lunora] offline-queue persistence ${t} failed`,e)};class m{queueBeforeFirstConnect;maxItems;onPersistenceError;persistence;onEvict;onSizeChange;version;items=[];constructor(t={},e={}){this.maxItems=t.maxItems??1e3,this.queueBeforeFirstConnect=t.queueBeforeFirstConnect??!1,this.onPersistenceError=t.onPersistenceError,this.persistence=e.persistence,this.onEvict=e.onEvict,this.onSizeChange=e.onSizeChange,this.version=e.version}get size(){return this.items.length}enqueue(t){const e=t;e.id??=u(),this.items.push(e),this.persistence?.append({args:e.args,clientId:e.clientId,functionPath:e.functionPath,id:e.id,identity:e.identity,shardKey:e.shardKey,...this.version===void 0?{}:{version:this.version}}).catch(i=>{o(this.onPersistenceError,"append",i,e.id)}),this.evictOverflow(),this.notifySize()}async hydrate(){if(!this.persistence)return[];let t;try{t=await this.persistence.load()}catch(s){throw o(this.onPersistenceError,"load",s),s}const e=[],i=new Set(this.items.map(s=>s.id));for(const s of t)if(!i.has(s.id)){if(f(this.version,s.version)){this.persistence.remove(s.id).catch(a=>{o(this.onPersistenceError,"remove",a,s.id)});continue}i.add(s.id),e.push({args:s.args,clientId:s.clientId,functionPath:s.functionPath,id:s.id,identity:s.identity,reject:()=>{},resolve:()=>{},shardKey:s.shardKey})}this.items.unshift(...e),this.evictOverflow(),this.notifySize();const n=new Set(this.items),c=new Set;for(const s of e)n.has(s)&&c.add(s.shardKey);return[...c]}restampIdentity(t,e){for(const i of this.items){if(i.identity!==t)continue;i.identity=e;const{id:n}=i;if(!this.persistence||n===void 0)continue;const c={args:i.args,clientId:i.clientId,functionPath:i.functionPath,id:n,identity:e,shardKey:i.shardKey,...this.version===void 0?{}:{version:this.version}};this.rewriteStamp(n,c).catch(s=>{o(this.onPersistenceError,"replace",s,n)})}}drain(t){if(!t){const n=[...this.items];return this.items.length=0,this.notifySize(),n}const e=[],i=[];for(const n of this.items)(t(n)?e:i).push(n);return this.items.length=0,this.items.push(...i),this.notifySize(),e}requeue(t){t.length!==0&&(this.items.unshift(...t),this.notifySize())}drainConflict(){const t=[],e=[];for(const i of this.items)if(i.precondition!==void 0&&!i.precondition()){const n=new Error("offline mutation skipped: precondition failed before replay");n.code="OFFLINE_PRECONDITION_FAILED",i.reject(n),t.push(i)}else e.push(i);return t.length>0&&(this.items.length=0,this.items.push(...e),this.notifySize()),t}clear(){for(const t of this.items){const e=new Error("CLIENT_CLOSED");e.code="CLIENT_CLOSED",t.reject(e)}this.items.length=0,this.notifySize()}async rewriteStamp(t,e){const i=this.persistence;if(i)try{await i.replace(e)}catch(n){o(this.onPersistenceError,"replace",n,t)}}evictOverflow(){for(;this.items.length>this.maxItems;){const t=this.items.shift();if(t){t.id&&this.persistence?.remove(t.id).catch(i=>{o(this.onPersistenceError,"remove",i,t.id)});const e=new Error("offline queue overflow");e.code="OFFLINE_QUEUE_OVERFLOW",t.reject(e),this.onEvict?.(t,e)}}}notifySize(){this.onSizeChange?.(this.items.length)}}export{m as O,f as i,u as n,o as r};
@@ -1,4 +1,4 @@
1
- import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-BkDgDzTg.js";
1
+ import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-B0t5v2n9.js";
2
2
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.js";
3
3
  /**
4
4
  * Run a query once on the server (during SSR) and capture its result in a
@@ -1,4 +1,4 @@
1
- import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-BSNUC85h.mjs";
1
+ import { L as LunoraClient, P as Preloaded } from "./lunora-client.d-6hU156nD.mjs";
2
2
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.mjs";
3
3
  /**
4
4
  * Run a query once on the server (during SSR) and capture its result in a
@@ -0,0 +1 @@
1
+ const d=e=>{let t="";for(let o=0;o<e.length;o+=32768)t+=String.fromCharCode(...e.subarray(o,o+32768));return btoa(t)},p=e=>{const t=atob(e),a=new Uint8Array(t.length);for(let o=0;o<t.length;o+=1)a[o]=t.codePointAt(o)??0;return a},i="$lunora.wire$";const w="__proto__",g={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},A={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},l=e=>{if(e===null||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);return t===null||t===Object.prototype},u=(e,t=0)=>{if(t>64)throw new RangeError("wire-codec: value nesting exceeds the 64-level limit");if(e===void 0)return[i,"undefined"];if(e===null)return null;const a=typeof e;if(a==="bigint")return[i,"bigint",e.toString()];if(a==="number"){const r=e;return Number.isNaN(r)?[i,"nan"]:r===1/0?[i,"inf"]:r===-1/0?[i,"-inf"]:r}if(a!=="object")return e;if(e instanceof Date)return[i,"date",u(e.getTime(),t+1)];if(e instanceof Error){const r=e,n={};for(const s of Object.keys(r))r[s]!==void 0&&(n[s]=u(r[s],t+1));const c=[i,"error",r.name,r.message,n];return r.cause!==void 0&&c.push(u(r.cause,t+1)),c}if(e instanceof URL)return[i,"url",e.href];if(e instanceof Map)return[i,"map",[...e.entries()].map(([r,n])=>[u(r,t+1),u(n,t+1)])];if(e instanceof Set)return[i,"set",[...e].map(r=>u(r,t+1))];if(e instanceof ArrayBuffer)return[i,"bytes",d(new Uint8Array(e)),"ArrayBuffer"];if(ArrayBuffer.isView(e)){const r=e,n=r.constructor.name,c=new Uint8Array(r.buffer,r.byteOffset,r.byteLength);return n==="Uint8Array"?[i,"bytes",d(c)]:[i,"bytes",d(c),n]}if(Array.isArray(e)){const r=e.map(n=>u(n,t+1));return r.length>0&&r[0]===i?[i,"arr",r]:r}if(!l(e)){const r=e.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${r} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const o=e,f={};for(const r of Object.keys(o)){const n=o[r];if(n===void 0)continue;const c=u(n,t+1);r===w?Object.defineProperty(f,r,{configurable:!0,enumerable:!0,value:c,writable:!0}):f[r]=c}return f},y=(e,t=0)=>{if(t>64)throw new RangeError("wire-codec: value nesting exceeds the 64-level limit");if(e===null||typeof e!="object")return e;if(Array.isArray(e)){if(e[0]===i)switch(e[1]){case"-inf":return-1/0;case"arr":return e[2].map(r=>y(r,t+1));case"bigint":{const r=e[2];if(typeof r!="string"||r.length>1024||!/^-?\d+$/.test(r))throw new RangeError("wire-codec: invalid or over-long bigint (max 1024 digits)");return BigInt(r)}case"date":{const r=y(e[2],t+1);if(typeof r!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(r)}case"map":{const r=e[2];return new Map(r.map(n=>{if(!Array.isArray(n)||n.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[y(n[0],t+1),y(n[1],t+1)]}))}case"set":return new Set(e[2].map(r=>y(r,t+1)));case"url":{const r=e[2];if(typeof r!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(r)}case"error":{const r=e[2],n=e[3],c=(Object.hasOwn(A,r)?A[r]:void 0)??Error,s=new c(n);s.name!==r&&Object.defineProperty(s,"name",{configurable:!0,value:r,writable:!0});const b=y(e[4],t+1);if(b===null||typeof b!="object"||Array.isArray(b))throw new TypeError("wire-codec: malformed error — props must be an object");for(const m of Object.keys(b))m===w?Object.defineProperty(s,m,{configurable:!0,enumerable:!0,value:b[m],writable:!0}):s[m]=b[m];return e.length>5&&Object.defineProperty(s,"cause",{configurable:!0,value:y(e[5],t+1),writable:!0}),s}case"bytes":{const r=e[2];if(typeof r!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const n=p(r),c=e[3]??"Uint8Array";if(c==="ArrayBuffer")return n.buffer.byteLength===n.byteLength?n.buffer:n.slice().buffer;const s=Object.hasOwn(g,c)?g[c]:void 0;return s?new s(n.slice().buffer):n}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return e.map(r=>y(r,t+1))}return e.map(f=>y(f,t+1))}const a=e,o={};for(const f of Object.keys(a)){const r=y(a[f],t+1);f===w?Object.defineProperty(o,f,{configurable:!0,enumerable:!0,value:r,writable:!0}):o[f]=r}return o};export{y as d,u as e,l as i};
@@ -1 +1 @@
1
- import{e as a}from"./wire-codec-BfIZrTJe.mjs";const b=/["\\\u0000-\u001F\uD800-\uDFFF]/,y=t=>b.test(t)?JSON.stringify(t):`"${t}"`,i=t=>{if(t===void 0)return"null";if(typeof t=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof t=="number"){if(Number.isNaN(t))return"nan";if(t===1/0)return"inf";if(t===-1/0)return"-inf";if(Object.is(t,-0))return"-0"}if(typeof t=="string")return y(t);if(t===null||typeof t!="object")return JSON.stringify(t);if(Array.isArray(t)){let r="[";for(let e=0;e<t.length;e++)e>0&&(r+=","),r+=i(t[e]);return r+"]"}const o=Object.getPrototypeOf(t);if(o!==null&&o!==Object.prototype){const r=t.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${r} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const s=t,c=Object.keys(s).sort();let n="{",f=!0;for(const r of c){const e=s[r];e!==void 0&&(f?f=!1:n+=",",n+=y(r),n+=":",n+=i(e))}return n+"}"},u=t=>i(a(t));export{u as s};
1
+ import{e as a}from"./wire-codec-BOMWQpoF.mjs";const b=/["\\\u0000-\u001F\uD800-\uDFFF]/,y=t=>b.test(t)?JSON.stringify(t):`"${t}"`,i=t=>{if(t===void 0)return"null";if(typeof t=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof t=="number"){if(Number.isNaN(t))return"nan";if(t===1/0)return"inf";if(t===-1/0)return"-inf";if(Object.is(t,-0))return"-0"}if(typeof t=="string")return y(t);if(t===null||typeof t!="object")return JSON.stringify(t);if(Array.isArray(t)){let r="[";for(let e=0;e<t.length;e++)e>0&&(r+=","),r+=i(t[e]);return r+"]"}const o=Object.getPrototypeOf(t);if(o!==null&&o!==Object.prototype){const r=t.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${r} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const s=t,c=Object.keys(s).sort();let n="{",f=!0;for(const r of c){const e=s[r];e!==void 0&&(f?f=!1:n+=",",n+=y(r),n+=":",n+=i(e))}return n+"}"},u=t=>i(a(t));export{u as s};
@@ -1,4 +1,4 @@
1
- import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-BSNUC85h.mjs";
1
+ import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-6hU156nD.mjs";
2
2
  export type {
3
3
  /**
4
4
  * `@lunora/client/query` — the framework-neutral live-query state machine shared
@@ -10,7 +10,7 @@ export type {
10
10
  * framework — each adapter supplies thin sinks that write into its own ref /
11
11
  * store / signal / cache.
12
12
  */
13
- b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-BSNUC85h.mjs";
13
+ b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-6hU156nD.mjs";
14
14
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "../packem_shared/function-reference.d-Br_hsKje.mjs";
15
15
  import '@lunora/errors';
16
16
  import '@lunora/runtime';
@@ -1,4 +1,4 @@
1
- import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-BkDgDzTg.js";
1
+ import { S as SubscriptionError, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-B0t5v2n9.js";
2
2
  export type {
3
3
  /**
4
4
  * `@lunora/client/query` — the framework-neutral live-query state machine shared
@@ -10,7 +10,7 @@ export type {
10
10
  * framework — each adapter supplies thin sinks that write into its own ref /
11
11
  * store / signal / cache.
12
12
  */
13
- b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-BkDgDzTg.js";
13
+ b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-B0t5v2n9.js";
14
14
  import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "../packem_shared/function-reference.d-Br_hsKje.js";
15
15
  import '@lunora/errors';
16
16
  import '@lunora/runtime';
package/dist/service.mjs CHANGED
@@ -1 +1 @@
1
- import{LunoraError as u}from"@lunora/errors";import{e as w,d as f}from"./packem_shared/wire-codec-BfIZrTJe.mjs";const v="/_lunora/rpc",$="https://lunora.internal",N=e=>{const t=new Error(e.message??"request failed");return t.code=e.code,e.data!==void 0&&(t.data=f(e.data)),e.hint!==void 0&&(t.hint=e.hint),e.docsUrl!==void 0&&(t.docsUrl=e.docsUrl),t},h=async(e,t,r,a,l)=>{const s=r.__lunoraRef;let d;try{d=JSON.stringify({args:w(a??{}),functionPath:s,...l?.shardKey===void 0?{}:{shardKey:l.shardKey}})}catch(o){const p=o instanceof Error?o.message:String(o);throw new TypeError(`@lunora/client/service: cannot encode args for '${s}' — ${p}`,o instanceof Error?{cause:o}:void 0)}const n=await e.fetch(new Request(`${$}${v}`,{body:d,headers:{"content-type":"application/json"},method:"POST"}));let i;try{i=await n.json()}catch{throw new u("INTERNAL",`@lunora/client/service: ${t} "${s}" got a non-JSON response (status ${n.status.toString()}). Check the service binding points at the Lunora Worker that declares this function.`,{status:n.status})}if(i===null||typeof i!="object")throw new u("INTERNAL",`@lunora/client/service: ${t} "${s}" returned a JSON body that is not an object (status ${n.status.toString()}). Check the service binding points at the Lunora Worker that declares this function.`,{status:n.status});const c=i;if("error"in c)throw N(c.error);if(!n.ok)throw new u("INTERNAL",`@lunora/client/service: ${t} "${s}" failed with status ${n.status.toString()} and no error envelope.`,{status:n.status});return f(c.result)},E=e=>({action:async(t,r,a)=>await h(e,"action",t,r,a),mutation:async(t,r,a)=>await h(e,"mutation",t,r,a),query:async(t,r,a)=>await h(e,"query",t,r,a)});export{v as RPC_PATH,E as createServiceClient};
1
+ import{LunoraError as u}from"@lunora/errors";import{e as w,d as f}from"./packem_shared/wire-codec-BOMWQpoF.mjs";const v="/_lunora/rpc",$="https://lunora.internal",N=e=>{const t=new Error(e.message??"request failed");return t.code=e.code,e.data!==void 0&&(t.data=f(e.data)),e.hint!==void 0&&(t.hint=e.hint),e.docsUrl!==void 0&&(t.docsUrl=e.docsUrl),t},h=async(e,t,r,a,l)=>{const s=r.__lunoraRef;let d;try{d=JSON.stringify({args:w(a??{}),functionPath:s,...l?.shardKey===void 0?{}:{shardKey:l.shardKey}})}catch(o){const p=o instanceof Error?o.message:String(o);throw new TypeError(`@lunora/client/service: cannot encode args for '${s}' — ${p}`,o instanceof Error?{cause:o}:void 0)}const n=await e.fetch(new Request(`${$}${v}`,{body:d,headers:{"content-type":"application/json"},method:"POST"}));let i;try{i=await n.json()}catch{throw new u("INTERNAL",`@lunora/client/service: ${t} "${s}" got a non-JSON response (status ${n.status.toString()}). Check the service binding points at the Lunora Worker that declares this function.`,{status:n.status})}if(i===null||typeof i!="object")throw new u("INTERNAL",`@lunora/client/service: ${t} "${s}" returned a JSON body that is not an object (status ${n.status.toString()}). Check the service binding points at the Lunora Worker that declares this function.`,{status:n.status});const c=i;if("error"in c)throw N(c.error);if(!n.ok)throw new u("INTERNAL",`@lunora/client/service: ${t} "${s}" failed with status ${n.status.toString()} and no error envelope.`,{status:n.status});return f(c.result)},E=e=>({action:async(t,r,a)=>await h(e,"action",t,r,a),mutation:async(t,r,a)=>await h(e,"mutation",t,r,a),query:async(t,r,a)=>await h(e,"query",t,r,a)});export{v as RPC_PATH,E as createServiceClient};
@@ -1,4 +1,4 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-BSNUC85h.mjs";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-6hU156nD.mjs";
2
2
  export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/function-reference.d-Br_hsKje.mjs";
3
3
  export {
4
4
  /**
@@ -20,7 +20,7 @@ p as preloadQuery,
20
20
  * WebSocket or touches a UI framework, so it is safe to import from any SSR
21
21
  * loader.
22
22
  */
23
- a as preloadedQueryResult } from "../packem_shared/preload.d-XsWPDlX6.mjs";
23
+ a as preloadedQueryResult } from "../packem_shared/preload.d-M7qP6TwL.mjs";
24
24
  import '@lunora/errors';
25
25
  import '@lunora/runtime';
26
26
  /**