@lunora/client 1.0.0-alpha.21 → 1.0.0-alpha.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/auth/index.d.mts +1 -1
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/index.d.mts +212 -4
  4. package/dist/index.d.ts +212 -4
  5. package/dist/index.mjs +9 -4
  6. package/dist/packem_shared/ClientServiceWorker-C3PAFwy0.mjs +100 -0
  7. package/dist/packem_shared/{LunoraClient-kXpHNyaE.mjs → LunoraClient-Clb118SU.mjs} +279 -22
  8. package/dist/packem_shared/{OfflineQueue-GGYJRmhF.mjs → OfflineQueue-B4HUF7rt.mjs} +1 -1
  9. package/dist/packem_shared/SubscriptionRegistry-D4jfIzZu.mjs +31 -0
  10. package/dist/packem_shared/TabCoordinator-BwRR8H06.mjs +222 -0
  11. package/dist/packem_shared/createClientQuery-CQ51bWAE.mjs +71 -0
  12. package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +2 -0
  13. package/dist/packem_shared/createReply-lI4tVS2w.mjs +36 -0
  14. package/dist/packem_shared/{createServerClient-DF-3mLmb.mjs → createServerClient-Dxemst5C.mjs} +1 -1
  15. package/dist/packem_shared/createSnapshotPrecondition-CBwnVz6r.mjs +18 -0
  16. package/dist/packem_shared/{local-store-BveBeFEo.mjs → local-store-DtcIW4c0.mjs} +1 -1
  17. package/dist/packem_shared/{lunora-client.d-BYkEjCEJ.d.mts → lunora-client.d-pw-9sLl0.d.mts} +183 -1
  18. package/dist/packem_shared/{lunora-client.d-BYkEjCEJ.d.ts → lunora-client.d-pw-9sLl0.d.ts} +183 -1
  19. package/dist/packem_shared/{offline-queue-B9vfdSqp.mjs → offline-queue-CF4_Co5k.mjs} +29 -0
  20. package/dist/packem_shared/{preload.d-DrfuisCE.d.mts → preload.d-6ME5ubgq.d.mts} +1 -1
  21. package/dist/packem_shared/{preload.d-B-vyHnml.d.ts → preload.d-BkQr-3Vh.d.ts} +1 -1
  22. package/dist/packem_shared/{subscription-BjynOXCU.mjs → stable-key-wv6eP48B.mjs} +1 -29
  23. package/dist/query/index.d.mts +2 -2
  24. package/dist/query/index.d.ts +2 -2
  25. package/dist/ssr/index.d.mts +3 -3
  26. package/dist/ssr/index.d.ts +3 -3
  27. package/dist/ssr/index.mjs +1 -1
  28. package/package.json +1 -1
  29. package/dist/packem_shared/SubscriptionRegistry-DjGKZsqq.mjs +0 -1
  30. package/dist/packem_shared/createLocalStore-jRoqmazl.mjs +0 -2
@@ -1,4 +1,4 @@
1
- import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-BYkEjCEJ.mjs";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-pw-9sLl0.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-BYkEjCEJ.js";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-pw-9sLl0.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-BYkEjCEJ.mjs";
2
- export { type e as BatchSlot, C as CONFLICT_ERROR_CODE, type f as CachedQuery, type g as ClientMessage, type h as ClientShapeSubscribeMessage, type i as ClientShapeUnsubscribeMessage, type j as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type k as FunctionArgumentDescriptor, type l as FunctionDescriptor, type G as GlobalFacetResult, type m as GlobalFacetValue, type n as GlobalFilterClause, type o as GlobalTableInfo, type p as GlobalTablePage, L as LunoraClient, type q as LunoraClientError, type r as LunoraClientOptions, type s as LunoraErrorCode, type t as MutationSettledEvent, type u as OptimisticLocalStore, type v as OptimisticUpdate, type w as OutboxMutation, type x as OutboxSink, type y as PersistedMutation, type P as Preloaded, type z as RowOp, type E as RpcEnvelope, type H as RpcResponseBody, type I as ScheduleRecord, type J as SchedulerPoolStatus, type K as SchedulerStatus, type N as ServerMessage, type T as ServerPokeEndMessage, type V as ServerPokePartMessage, type W as ServerPokeStartMessage, type X as ShardTrafficEntry, type Y as ShardTrafficResult, type Z as StorageListPage, type _ as StorageObject, type $ as StreamHandle, type a0 as StreamIterable, type a1 as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, a2 as SubscriptionRegistry, type a3 as SubscriptionState, type a4 as SyncWatermark, type a as Unsubscribe, type U as User, type a5 as WorkflowInstanceAction, type a6 as WorkflowInstanceDetail, type a7 as WorkflowInstancePage, type a8 as WorkflowInstanceStatus, type a9 as WorkflowInstanceSummary, type aa as WorkflowStepDetail, ab as createLocalStore, ac as createStream, ad as getErrorCode, ae as getRetryAfterMs, af as isConflictError, ag as isForbiddenError, ah as isRateLimitedError, ai as isUnauthorizedError } from "./packem_shared/lunora-client.d-BYkEjCEJ.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-DrfuisCE.mjs";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, S as SubscriptionError, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-pw-9sLl0.mjs";
2
+ export { type e as BatchSlot, C as CONFLICT_ERROR_CODE, type f as CachedQuery, type g as ClientMessage, type h as ClientQueryRef, type i as ClientShapeSubscribeMessage, type j as ClientShapeUnsubscribeMessage, type k as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type l as FunctionArgumentDescriptor, type m as FunctionDescriptor, type G as GlobalFacetResult, type n as GlobalFacetValue, type o as GlobalFilterClause, type p as GlobalTableInfo, type q as GlobalTablePage, type r as LunoraClientError, type s as LunoraClientOptions, type t as LunoraErrorCode, type u as MutationSettledEvent, type v as OptimisticLocalStore, type w as OptimisticUpdate, type x as OutboxMutation, type y as OutboxSink, type z as PersistedMutation, type P as Preloaded, type E as RowOp, type H as RpcEnvelope, type I as RpcResponseBody, type J as ScheduleRecord, type K as SchedulerPoolStatus, type N as SchedulerStatus, type T as ServerMessage, type V as ServerPokeEndMessage, type W as ServerPokePartMessage, type X as ServerPokeStartMessage, type Y as ShardTrafficEntry, type Z as ShardTrafficResult, type _ as StorageListPage, type $ as StorageObject, type a0 as StreamHandle, type a1 as StreamIterable, type a2 as SubscriptionCallback, type b as SubscriptionErrorCallback, a3 as SubscriptionRegistry, type a4 as SubscriptionState, type a5 as SyncWatermark, type a as Unsubscribe, type U as User, type a6 as WorkflowInstanceAction, type a7 as WorkflowInstanceDetail, type a8 as WorkflowInstancePage, type a9 as WorkflowInstanceStatus, type aa as WorkflowInstanceSummary, type ab as WorkflowStepDetail, ac as createClientQuery, ad as createLocalStore, ae as createStream, af as getErrorCode, ag as getRetryAfterMs, ah as isConflictError, ai as isForbiddenError, aj as isRateLimitedError, ak as isUnauthorizedError } from "./packem_shared/lunora-client.d-pw-9sLl0.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-6ME5ubgq.mjs";
4
4
  export type { AuthCapabilities, AuthConfigInfo, AuthImpersonation, AuthPage, AuthSession, AuthUser, AuthUserFieldSpec, CronJobInfo, KvKeyEntry, KvKeyListResult, KvNamespaceSummary, KvValueResult, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
@@ -33,6 +33,92 @@ interface AsyncStoragePersistenceOptions {
33
33
  declare const createAsyncStoragePersistence: (options: AsyncStoragePersistenceOptions) => PersistenceAdapter;
34
34
  /** Default in-memory bookmark store. Survives the lifetime of the client. */
35
35
  declare const createInMemoryBookmarkStorage: () => BookmarkStorage;
36
+ interface TabCoordinatorOptions {
37
+ /**
38
+ * BroadcastChannel name. Defaults to `"lunora-bridge"`.
39
+ */
40
+ channelName?: string;
41
+ /**
42
+ * Interval (ms) between leader heartbeats. Defaults to 1000.
43
+ */
44
+ heartbeatInterval?: number;
45
+ /**
46
+ * Milliseconds without a heartbeat to consider the leader dead. Must be
47
+ * larger than `heartbeatInterval`. Defaults to 3000.
48
+ */
49
+ leaderTimeout?: number;
50
+ /**
51
+ * Called when this tab becomes the leader (should open WS connections).
52
+ */
53
+ onBecomeLeader?: () => void;
54
+ /**
55
+ * Called when this tab loses leadership (should close WS connections).
56
+ */
57
+ onStopBeingLeader?: () => void;
58
+ /**
59
+ * Called when the leader broadcasts subscription data.
60
+ */
61
+ onSubscriptionData?: (key: string, data: unknown) => void;
62
+ /**
63
+ * Called when the leader broadcasts a subscription error.
64
+ */
65
+ onSubscriptionError?: (key: string, error: SubscriptionError) => void;
66
+ }
67
+ declare class TabCoordinator {
68
+ private readonly bc;
69
+ private readonly tabId;
70
+ private readonly heartbeatInterval;
71
+ private readonly leaderTimeout;
72
+ /** The tab id of the current known leader, or `undefined` if no leader. */
73
+ private knownLeader;
74
+ /** `true` when this tab believes it is the leader. */
75
+ private leader;
76
+ /** `true` once `start()` has been called. */
77
+ private running;
78
+ /** Timestamp of the most recent leader heartbeat. */
79
+ private lastHeartbeat;
80
+ private heartbeatTimer;
81
+ private leaderCheckTimer;
82
+ /** Callbacks set via constructor options. */
83
+ private readonly onBecomeLeader;
84
+ private readonly onStopBeingLeader;
85
+ private readonly onSubscriptionData;
86
+ private readonly onSubscriptionError;
87
+ constructor(options?: TabCoordinatorOptions);
88
+ /**
89
+ * Start the coordinator: attempt to claim leadership and begin the
90
+ * heartbeat/leader-check cycle. Safe to call multiple times.
91
+ */
92
+ start(): void;
93
+ /**
94
+ * Stop the coordinator: yield leadership (if held), close the channel, and
95
+ * clear all timers. Safe to call multiple times.
96
+ */
97
+ stop(): void;
98
+ /** `true` when this tab is the current WebSocket leader. */
99
+ isLeader(): boolean;
100
+ /** The tab id of the current leader, or `undefined` if unknown / no leader. */
101
+ get leaderTabId(): string | undefined;
102
+ /** The id of this tab. */
103
+ get id(): string;
104
+ /** `true` when the coordinator has been started and is not yet stopped. */
105
+ get isRunning(): boolean;
106
+ /**
107
+ * Broadcast subscription data to all follower tabs. Only the leader should
108
+ * call this.
109
+ */
110
+ broadcastSubscriptionData(key: string, data: unknown): void;
111
+ /**
112
+ * Broadcast a subscription error to all follower tabs. Only the leader
113
+ * should call this.
114
+ */
115
+ broadcastSubscriptionError(key: string, error: SubscriptionError): void;
116
+ private broadcast;
117
+ private handleMessage;
118
+ private becomeLeader;
119
+ private sendHeartbeat;
120
+ private checkLeaderHealth;
121
+ }
36
122
  /**
37
123
  * Client-side incremental merging of structured mutation deltas.
38
124
  *
@@ -202,6 +288,14 @@ interface QueuedMutation<T = unknown> {
202
288
  * Absent on hydrated records (the optimistic write lived in a prior session).
203
289
  */
204
290
  readonly onCommit?: (commitCursor: number | undefined) => void;
291
+ /**
292
+ * Optional sync predicate evaluated just before replay. When it returns
293
+ * `false` the write is dropped instead of replaying, handling the case
294
+ * where the mutation's preconditions are no longer valid (e.g. the
295
+ * document it referred to was deleted while offline). Absent or `true`
296
+ * means "ok to replay".
297
+ */
298
+ readonly precondition?: () => boolean;
205
299
  /** Rejects if the mutation can no longer be replayed. */
206
300
  readonly reject: (error: unknown) => void;
207
301
  /** Resolves once the mutation has been replayed against the server. */
@@ -288,6 +382,16 @@ declare class OfflineQueue {
288
382
  * transport failure: the unreplayed writes stay queued for the next reconnect.
289
383
  */
290
384
  requeue(items: QueuedMutation[]): void;
385
+ /**
386
+ * Remove mutations whose precondition evaluates to `false` (stale/dirty
387
+ * writes that should not replay) and reject each with an
388
+ * `OFFLINE_PRECONDITION_FAILED` error. The valid (admitted) mutations stay
389
+ * queued in FIFO order. Returns the drained stale entries.
390
+ *
391
+ * Called during reconnect before the flush cycle to weed out writes whose
392
+ * assumptions no longer hold (e.g. a document was deleted by another client).
393
+ */
394
+ drainConflict(): QueuedMutation[];
291
395
  clear(): void;
292
396
  /** Notify the size observer (the client's pending-sync count) after any change. */
293
397
  private notifySize;
@@ -398,4 +502,108 @@ interface ReconnectCalculator {
398
502
  reset: () => void;
399
503
  }
400
504
  declare const createReconnect: (options?: ReconnectOptions, random?: () => number) => ReconnectCalculator;
401
- export { type ArgsOf, type AsyncStorageLike, type AsyncStoragePersistenceOptions, type BookmarkStorage, type FunctionReference, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, type MutationCallOptions, type MutationDelta, type MutationRunnerSinks, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, type ReconnectCalculator, type ReconnectOptions, type ReturnOf, applyDelta, createAsyncStoragePersistence, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutationRunner, createMutatorRunner, createReconnect, isMutationDelta, queryCacheKey };
505
+ /**
506
+ * Capture a snapshot of the current live query value at call time and produce a
507
+ * `() => boolean` precondition that compares it against the value at replay time.
508
+ *
509
+ * When the precondition is checked (on queue drain / reconnect) it re-reads the
510
+ * query's current value via {@link LunoraClient.peekActiveQueryValue}. If the
511
+ * value differs from what was captured at call time the precondition returns
512
+ * `false` and the offline mutation is dropped as stale.
513
+ * @example
514
+ * ```ts
515
+ * client.mutation(api.todos.update, { id, text }, {
516
+ * precondition: createSnapshotPrecondition(client, api.todos.list, { userId }),
517
+ * });
518
+ * ```
519
+ */
520
+ declare const createSnapshotPrecondition: (client: LunoraClient, functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string) => (() => boolean);
521
+ /**
522
+ * Client-side service-worker registration and lifecycle management.
523
+ *
524
+ * Usage:
525
+ * ```ts
526
+ * const sw = new ClientServiceWorker({ swUrl: "/sw.js" });
527
+ * await sw.register();
528
+ *
529
+ * if (sw.active) {
530
+ * sw.postMessage({ type: "sync" });
531
+ * }
532
+ * ```
533
+ */
534
+ type ServiceWorkerStatus = "unsupported" | "unregistered" | "registering" | "active" | "error";
535
+ interface ClientSwOptions {
536
+ /** Called when the status changes. */
537
+ onStatusChange?: (status: ServiceWorkerStatus) => void;
538
+ /** Optional registration scope. Defaults to `/`. */
539
+ scope?: string;
540
+ /** URL of the service worker script (relative to origin). */
541
+ swUrl: string;
542
+ }
543
+ /**
544
+ * Manages service-worker registration and provides a simple API for
545
+ * sending messages and listening for responses.
546
+ */
547
+ declare class ClientServiceWorker {
548
+ #private;
549
+ readonly swUrl: string;
550
+ readonly scope: string;
551
+ constructor(options: ClientSwOptions);
552
+ /** Current registration status. */
553
+ get status(): ServiceWorkerStatus;
554
+ /** The underlying `ServiceWorkerRegistration`, if registered. */
555
+ get registration(): ServiceWorkerRegistration | undefined;
556
+ /** Whether the SW is currently controlling this page. */
557
+ get active(): boolean;
558
+ /**
559
+ * Register the service worker.
560
+ *
561
+ * Returns `false` when the browser does not support service workers.
562
+ */
563
+ register(): Promise<boolean>;
564
+ /**
565
+ * Remove the active service worker registration and clear listeners.
566
+ *
567
+ * Returns `false` when no registration is currently held.
568
+ */
569
+ unregister(): Promise<boolean>;
570
+ /**
571
+ * Send a message to the active service worker.
572
+ */
573
+ postMessage(message: unknown): void;
574
+ /**
575
+ * Register a handler for messages **from** the service worker.
576
+ * @returns Unsubscribe function.
577
+ */
578
+ onMessage(handler: (event: MessageEvent) => void): () => void;
579
+ }
580
+ /**
581
+ * Outbound message from the client to the SW.
582
+ */
583
+ interface ClientToSwMessage {
584
+ /** Opaque correlation ID for request/response patterns. */
585
+ correlationId?: string;
586
+ payload?: unknown;
587
+ type: string;
588
+ }
589
+ /**
590
+ * Inbound message from the SW to the client.
591
+ */
592
+ interface SwToClientMessage {
593
+ /** Echoes the correlation ID from the client request, if any. */
594
+ correlationId?: string;
595
+ payload?: unknown;
596
+ type: string;
597
+ }
598
+ /**
599
+ * Send a typed message to the service worker and optionally await a
600
+ * matching response.
601
+ * @returns A promise that resolves when the SW sends a reply with the
602
+ * same `correlationId` (if `expectResponse` is true).
603
+ */
604
+ declare const sendToSw: (sw: ServiceWorker | null, message: ClientToSwMessage, expectResponse?: boolean) => Promise<unknown>;
605
+ /**
606
+ * Create a reply for a client message (call from inside the SW).
607
+ */
608
+ declare const createReply: (original: ClientToSwMessage, payload?: unknown) => SwToClientMessage;
609
+ export { type ArgsOf, type AsyncStorageLike, type AsyncStoragePersistenceOptions, type BookmarkStorage, ClientServiceWorker, type ClientSwOptions, type ClientToSwMessage, type FunctionReference, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, LunoraClient, type MutationCallOptions, type MutationDelta, type MutationRunnerSinks, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, type ReconnectCalculator, type ReconnectOptions, type ReturnOf, type ServiceWorkerStatus, type SubscriptionError, type SwToClientMessage, TabCoordinator, applyDelta, createAsyncStoragePersistence, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutationRunner, createMutatorRunner, createReconnect, createReply, createSnapshotPrecondition, isMutationDelta, queryCacheKey, sendToSw };
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-BYkEjCEJ.js";
2
- export { type e as BatchSlot, C as CONFLICT_ERROR_CODE, type f as CachedQuery, type g as ClientMessage, type h as ClientShapeSubscribeMessage, type i as ClientShapeUnsubscribeMessage, type j as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type k as FunctionArgumentDescriptor, type l as FunctionDescriptor, type G as GlobalFacetResult, type m as GlobalFacetValue, type n as GlobalFilterClause, type o as GlobalTableInfo, type p as GlobalTablePage, L as LunoraClient, type q as LunoraClientError, type r as LunoraClientOptions, type s as LunoraErrorCode, type t as MutationSettledEvent, type u as OptimisticLocalStore, type v as OptimisticUpdate, type w as OutboxMutation, type x as OutboxSink, type y as PersistedMutation, type P as Preloaded, type z as RowOp, type E as RpcEnvelope, type H as RpcResponseBody, type I as ScheduleRecord, type J as SchedulerPoolStatus, type K as SchedulerStatus, type N as ServerMessage, type T as ServerPokeEndMessage, type V as ServerPokePartMessage, type W as ServerPokeStartMessage, type X as ShardTrafficEntry, type Y as ShardTrafficResult, type Z as StorageListPage, type _ as StorageObject, type $ as StreamHandle, type a0 as StreamIterable, type a1 as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, a2 as SubscriptionRegistry, type a3 as SubscriptionState, type a4 as SyncWatermark, type a as Unsubscribe, type U as User, type a5 as WorkflowInstanceAction, type a6 as WorkflowInstanceDetail, type a7 as WorkflowInstancePage, type a8 as WorkflowInstanceStatus, type a9 as WorkflowInstanceSummary, type aa as WorkflowStepDetail, ab as createLocalStore, ac as createStream, ad as getErrorCode, ae as getRetryAfterMs, af as isConflictError, ag as isForbiddenError, ah as isRateLimitedError, ai as isUnauthorizedError } from "./packem_shared/lunora-client.d-BYkEjCEJ.js";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-B-vyHnml.js";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, S as SubscriptionError, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-pw-9sLl0.js";
2
+ export { type e as BatchSlot, C as CONFLICT_ERROR_CODE, type f as CachedQuery, type g as ClientMessage, type h as ClientQueryRef, type i as ClientShapeSubscribeMessage, type j as ClientShapeUnsubscribeMessage, type k as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type l as FunctionArgumentDescriptor, type m as FunctionDescriptor, type G as GlobalFacetResult, type n as GlobalFacetValue, type o as GlobalFilterClause, type p as GlobalTableInfo, type q as GlobalTablePage, type r as LunoraClientError, type s as LunoraClientOptions, type t as LunoraErrorCode, type u as MutationSettledEvent, type v as OptimisticLocalStore, type w as OptimisticUpdate, type x as OutboxMutation, type y as OutboxSink, type z as PersistedMutation, type P as Preloaded, type E as RowOp, type H as RpcEnvelope, type I as RpcResponseBody, type J as ScheduleRecord, type K as SchedulerPoolStatus, type N as SchedulerStatus, type T as ServerMessage, type V as ServerPokeEndMessage, type W as ServerPokePartMessage, type X as ServerPokeStartMessage, type Y as ShardTrafficEntry, type Z as ShardTrafficResult, type _ as StorageListPage, type $ as StorageObject, type a0 as StreamHandle, type a1 as StreamIterable, type a2 as SubscriptionCallback, type b as SubscriptionErrorCallback, a3 as SubscriptionRegistry, type a4 as SubscriptionState, type a5 as SyncWatermark, type a as Unsubscribe, type U as User, type a6 as WorkflowInstanceAction, type a7 as WorkflowInstanceDetail, type a8 as WorkflowInstancePage, type a9 as WorkflowInstanceStatus, type aa as WorkflowInstanceSummary, type ab as WorkflowStepDetail, ac as createClientQuery, ad as createLocalStore, ae as createStream, af as getErrorCode, ag as getRetryAfterMs, ah as isConflictError, ai as isForbiddenError, aj as isRateLimitedError, ak as isUnauthorizedError } from "./packem_shared/lunora-client.d-pw-9sLl0.js";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-BkQr-3Vh.js";
4
4
  export type { AuthCapabilities, AuthConfigInfo, AuthImpersonation, AuthPage, AuthSession, AuthUser, AuthUserFieldSpec, CronJobInfo, KvKeyEntry, KvKeyListResult, KvNamespaceSummary, KvValueResult, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
@@ -33,6 +33,92 @@ interface AsyncStoragePersistenceOptions {
33
33
  declare const createAsyncStoragePersistence: (options: AsyncStoragePersistenceOptions) => PersistenceAdapter;
34
34
  /** Default in-memory bookmark store. Survives the lifetime of the client. */
35
35
  declare const createInMemoryBookmarkStorage: () => BookmarkStorage;
36
+ interface TabCoordinatorOptions {
37
+ /**
38
+ * BroadcastChannel name. Defaults to `"lunora-bridge"`.
39
+ */
40
+ channelName?: string;
41
+ /**
42
+ * Interval (ms) between leader heartbeats. Defaults to 1000.
43
+ */
44
+ heartbeatInterval?: number;
45
+ /**
46
+ * Milliseconds without a heartbeat to consider the leader dead. Must be
47
+ * larger than `heartbeatInterval`. Defaults to 3000.
48
+ */
49
+ leaderTimeout?: number;
50
+ /**
51
+ * Called when this tab becomes the leader (should open WS connections).
52
+ */
53
+ onBecomeLeader?: () => void;
54
+ /**
55
+ * Called when this tab loses leadership (should close WS connections).
56
+ */
57
+ onStopBeingLeader?: () => void;
58
+ /**
59
+ * Called when the leader broadcasts subscription data.
60
+ */
61
+ onSubscriptionData?: (key: string, data: unknown) => void;
62
+ /**
63
+ * Called when the leader broadcasts a subscription error.
64
+ */
65
+ onSubscriptionError?: (key: string, error: SubscriptionError) => void;
66
+ }
67
+ declare class TabCoordinator {
68
+ private readonly bc;
69
+ private readonly tabId;
70
+ private readonly heartbeatInterval;
71
+ private readonly leaderTimeout;
72
+ /** The tab id of the current known leader, or `undefined` if no leader. */
73
+ private knownLeader;
74
+ /** `true` when this tab believes it is the leader. */
75
+ private leader;
76
+ /** `true` once `start()` has been called. */
77
+ private running;
78
+ /** Timestamp of the most recent leader heartbeat. */
79
+ private lastHeartbeat;
80
+ private heartbeatTimer;
81
+ private leaderCheckTimer;
82
+ /** Callbacks set via constructor options. */
83
+ private readonly onBecomeLeader;
84
+ private readonly onStopBeingLeader;
85
+ private readonly onSubscriptionData;
86
+ private readonly onSubscriptionError;
87
+ constructor(options?: TabCoordinatorOptions);
88
+ /**
89
+ * Start the coordinator: attempt to claim leadership and begin the
90
+ * heartbeat/leader-check cycle. Safe to call multiple times.
91
+ */
92
+ start(): void;
93
+ /**
94
+ * Stop the coordinator: yield leadership (if held), close the channel, and
95
+ * clear all timers. Safe to call multiple times.
96
+ */
97
+ stop(): void;
98
+ /** `true` when this tab is the current WebSocket leader. */
99
+ isLeader(): boolean;
100
+ /** The tab id of the current leader, or `undefined` if unknown / no leader. */
101
+ get leaderTabId(): string | undefined;
102
+ /** The id of this tab. */
103
+ get id(): string;
104
+ /** `true` when the coordinator has been started and is not yet stopped. */
105
+ get isRunning(): boolean;
106
+ /**
107
+ * Broadcast subscription data to all follower tabs. Only the leader should
108
+ * call this.
109
+ */
110
+ broadcastSubscriptionData(key: string, data: unknown): void;
111
+ /**
112
+ * Broadcast a subscription error to all follower tabs. Only the leader
113
+ * should call this.
114
+ */
115
+ broadcastSubscriptionError(key: string, error: SubscriptionError): void;
116
+ private broadcast;
117
+ private handleMessage;
118
+ private becomeLeader;
119
+ private sendHeartbeat;
120
+ private checkLeaderHealth;
121
+ }
36
122
  /**
37
123
  * Client-side incremental merging of structured mutation deltas.
38
124
  *
@@ -202,6 +288,14 @@ interface QueuedMutation<T = unknown> {
202
288
  * Absent on hydrated records (the optimistic write lived in a prior session).
203
289
  */
204
290
  readonly onCommit?: (commitCursor: number | undefined) => void;
291
+ /**
292
+ * Optional sync predicate evaluated just before replay. When it returns
293
+ * `false` the write is dropped instead of replaying, handling the case
294
+ * where the mutation's preconditions are no longer valid (e.g. the
295
+ * document it referred to was deleted while offline). Absent or `true`
296
+ * means "ok to replay".
297
+ */
298
+ readonly precondition?: () => boolean;
205
299
  /** Rejects if the mutation can no longer be replayed. */
206
300
  readonly reject: (error: unknown) => void;
207
301
  /** Resolves once the mutation has been replayed against the server. */
@@ -288,6 +382,16 @@ declare class OfflineQueue {
288
382
  * transport failure: the unreplayed writes stay queued for the next reconnect.
289
383
  */
290
384
  requeue(items: QueuedMutation[]): void;
385
+ /**
386
+ * Remove mutations whose precondition evaluates to `false` (stale/dirty
387
+ * writes that should not replay) and reject each with an
388
+ * `OFFLINE_PRECONDITION_FAILED` error. The valid (admitted) mutations stay
389
+ * queued in FIFO order. Returns the drained stale entries.
390
+ *
391
+ * Called during reconnect before the flush cycle to weed out writes whose
392
+ * assumptions no longer hold (e.g. a document was deleted by another client).
393
+ */
394
+ drainConflict(): QueuedMutation[];
291
395
  clear(): void;
292
396
  /** Notify the size observer (the client's pending-sync count) after any change. */
293
397
  private notifySize;
@@ -398,4 +502,108 @@ interface ReconnectCalculator {
398
502
  reset: () => void;
399
503
  }
400
504
  declare const createReconnect: (options?: ReconnectOptions, random?: () => number) => ReconnectCalculator;
401
- export { type ArgsOf, type AsyncStorageLike, type AsyncStoragePersistenceOptions, type BookmarkStorage, type FunctionReference, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, type MutationCallOptions, type MutationDelta, type MutationRunnerSinks, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, type ReconnectCalculator, type ReconnectOptions, type ReturnOf, applyDelta, createAsyncStoragePersistence, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutationRunner, createMutatorRunner, createReconnect, isMutationDelta, queryCacheKey };
505
+ /**
506
+ * Capture a snapshot of the current live query value at call time and produce a
507
+ * `() => boolean` precondition that compares it against the value at replay time.
508
+ *
509
+ * When the precondition is checked (on queue drain / reconnect) it re-reads the
510
+ * query's current value via {@link LunoraClient.peekActiveQueryValue}. If the
511
+ * value differs from what was captured at call time the precondition returns
512
+ * `false` and the offline mutation is dropped as stale.
513
+ * @example
514
+ * ```ts
515
+ * client.mutation(api.todos.update, { id, text }, {
516
+ * precondition: createSnapshotPrecondition(client, api.todos.list, { userId }),
517
+ * });
518
+ * ```
519
+ */
520
+ declare const createSnapshotPrecondition: (client: LunoraClient, functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string) => (() => boolean);
521
+ /**
522
+ * Client-side service-worker registration and lifecycle management.
523
+ *
524
+ * Usage:
525
+ * ```ts
526
+ * const sw = new ClientServiceWorker({ swUrl: "/sw.js" });
527
+ * await sw.register();
528
+ *
529
+ * if (sw.active) {
530
+ * sw.postMessage({ type: "sync" });
531
+ * }
532
+ * ```
533
+ */
534
+ type ServiceWorkerStatus = "unsupported" | "unregistered" | "registering" | "active" | "error";
535
+ interface ClientSwOptions {
536
+ /** Called when the status changes. */
537
+ onStatusChange?: (status: ServiceWorkerStatus) => void;
538
+ /** Optional registration scope. Defaults to `/`. */
539
+ scope?: string;
540
+ /** URL of the service worker script (relative to origin). */
541
+ swUrl: string;
542
+ }
543
+ /**
544
+ * Manages service-worker registration and provides a simple API for
545
+ * sending messages and listening for responses.
546
+ */
547
+ declare class ClientServiceWorker {
548
+ #private;
549
+ readonly swUrl: string;
550
+ readonly scope: string;
551
+ constructor(options: ClientSwOptions);
552
+ /** Current registration status. */
553
+ get status(): ServiceWorkerStatus;
554
+ /** The underlying `ServiceWorkerRegistration`, if registered. */
555
+ get registration(): ServiceWorkerRegistration | undefined;
556
+ /** Whether the SW is currently controlling this page. */
557
+ get active(): boolean;
558
+ /**
559
+ * Register the service worker.
560
+ *
561
+ * Returns `false` when the browser does not support service workers.
562
+ */
563
+ register(): Promise<boolean>;
564
+ /**
565
+ * Remove the active service worker registration and clear listeners.
566
+ *
567
+ * Returns `false` when no registration is currently held.
568
+ */
569
+ unregister(): Promise<boolean>;
570
+ /**
571
+ * Send a message to the active service worker.
572
+ */
573
+ postMessage(message: unknown): void;
574
+ /**
575
+ * Register a handler for messages **from** the service worker.
576
+ * @returns Unsubscribe function.
577
+ */
578
+ onMessage(handler: (event: MessageEvent) => void): () => void;
579
+ }
580
+ /**
581
+ * Outbound message from the client to the SW.
582
+ */
583
+ interface ClientToSwMessage {
584
+ /** Opaque correlation ID for request/response patterns. */
585
+ correlationId?: string;
586
+ payload?: unknown;
587
+ type: string;
588
+ }
589
+ /**
590
+ * Inbound message from the SW to the client.
591
+ */
592
+ interface SwToClientMessage {
593
+ /** Echoes the correlation ID from the client request, if any. */
594
+ correlationId?: string;
595
+ payload?: unknown;
596
+ type: string;
597
+ }
598
+ /**
599
+ * Send a typed message to the service worker and optionally await a
600
+ * matching response.
601
+ * @returns A promise that resolves when the SW sends a reply with the
602
+ * same `correlationId` (if `expectResponse` is true).
603
+ */
604
+ declare const sendToSw: (sw: ServiceWorker | null, message: ClientToSwMessage, expectResponse?: boolean) => Promise<unknown>;
605
+ /**
606
+ * Create a reply for a client message (call from inside the SW).
607
+ */
608
+ declare const createReply: (original: ClientToSwMessage, payload?: unknown) => SwToClientMessage;
609
+ export { type ArgsOf, type AsyncStorageLike, type AsyncStoragePersistenceOptions, type BookmarkStorage, ClientServiceWorker, type ClientSwOptions, type ClientToSwMessage, type FunctionReference, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, LunoraClient, type MutationCallOptions, type MutationDelta, type MutationRunnerSinks, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, type ReconnectCalculator, type ReconnectOptions, type ReturnOf, type ServiceWorkerStatus, type SubscriptionError, type SwToClientMessage, TabCoordinator, applyDelta, createAsyncStoragePersistence, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutationRunner, createMutatorRunner, createReconnect, createReply, createSnapshotPrecondition, isMutationDelta, queryCacheKey, sendToSw };
package/dist/index.mjs CHANGED
@@ -1,15 +1,20 @@
1
1
  export { createAsyncStoragePersistence } from './packem_shared/createAsyncStoragePersistence-1Z5BZ8RC.mjs';
2
2
  export { default as createInMemoryBookmarkStorage } from './packem_shared/createInMemoryBookmarkStorage-BoN7a7TH.mjs';
3
+ export { createClientQuery } from './packem_shared/createClientQuery-CQ51bWAE.mjs';
4
+ export { TabCoordinator } from './packem_shared/TabCoordinator-BwRR8H06.mjs';
3
5
  export { applyDelta, isMutationDelta } from './packem_shared/applyDelta-CRKZ1PBt.mjs';
4
6
  export { CONFLICT_ERROR_CODE, getErrorCode, getRetryAfterMs, isConflictError, isForbiddenError, isRateLimitedError, isUnauthorizedError } from './packem_shared/CONFLICT_ERROR_CODE-B8gQ8tyU.mjs';
5
- export { c as createLocalStore } from './packem_shared/local-store-BveBeFEo.mjs';
6
- export { LunoraClient } from './packem_shared/LunoraClient-kXpHNyaE.mjs';
7
+ export { c as createLocalStore } from './packem_shared/local-store-DtcIW4c0.mjs';
8
+ export { LunoraClient } from './packem_shared/LunoraClient-Clb118SU.mjs';
7
9
  export { createMutationRunner } from './packem_shared/createMutationRunner-BqsavzvG.mjs';
8
10
  export { createMutatorRunner } from './packem_shared/createMutatorRunner-BETvCd0p.mjs';
9
- export { O as OfflineQueue } from './packem_shared/offline-queue-B9vfdSqp.mjs';
11
+ export { O as OfflineQueue } from './packem_shared/offline-queue-CF4_Co5k.mjs';
10
12
  export { createInMemoryPersistence, createIndexedDbPersistence } from './packem_shared/createInMemoryPersistence-DZ2VHWgm.mjs';
11
13
  export { preloadQuery, preloadedQueryResult } from './packem_shared/preloadQuery-lobFkD2Z.mjs';
12
14
  export { createInMemoryQueryCache, createIndexedDbQueryCache, queryCacheKey } from './packem_shared/createInMemoryQueryCache-DiaGZkA2.mjs';
13
15
  export { createReconnect } from './packem_shared/createReconnect-Di_-oHH7.mjs';
16
+ export { default as createSnapshotPrecondition } from './packem_shared/createSnapshotPrecondition-CBwnVz6r.mjs';
14
17
  export { DEFAULT_MAX_BUFFER, createStream } from './packem_shared/DEFAULT_MAX_BUFFER-7hFnzNk9.mjs';
15
- export { S as SubscriptionRegistry } from './packem_shared/subscription-BjynOXCU.mjs';
18
+ export { SubscriptionRegistry } from './packem_shared/SubscriptionRegistry-D4jfIzZu.mjs';
19
+ export { ClientServiceWorker } from './packem_shared/ClientServiceWorker-C3PAFwy0.mjs';
20
+ export { createReply, sendToSw } from './packem_shared/createReply-lI4tVS2w.mjs';
@@ -0,0 +1,100 @@
1
+ class ClientServiceWorker {
2
+ swUrl;
3
+ scope;
4
+ #registration = void 0;
5
+ #status = "unregistered";
6
+ #onStatusChange;
7
+ #messageHandlers = /* @__PURE__ */ new Set();
8
+ constructor(options) {
9
+ this.swUrl = options.swUrl;
10
+ this.scope = options.scope ?? "/";
11
+ this.#onStatusChange = options.onStatusChange;
12
+ }
13
+ // ── Status ──────────────────────────────────────────────────────────
14
+ /** Current registration status. */
15
+ get status() {
16
+ return this.#status;
17
+ }
18
+ /** The underlying `ServiceWorkerRegistration`, if registered. */
19
+ get registration() {
20
+ return this.#registration;
21
+ }
22
+ /** Whether the SW is currently controlling this page. */
23
+ get active() {
24
+ return this.#registration?.active !== null && this.#registration?.active !== void 0;
25
+ }
26
+ // ── Registration ────────────────────────────────────────────────────
27
+ /**
28
+ * Register the service worker.
29
+ *
30
+ * Returns `false` when the browser does not support service workers.
31
+ */
32
+ async register() {
33
+ if (!("serviceWorker" in navigator)) {
34
+ this.#setStatus("unsupported");
35
+ return false;
36
+ }
37
+ this.#setStatus("registering");
38
+ try {
39
+ this.#registration = await navigator.serviceWorker.register(this.swUrl, {
40
+ scope: this.scope
41
+ });
42
+ this.#setStatus("active");
43
+ navigator.serviceWorker.addEventListener("message", this.#onMessage);
44
+ } catch {
45
+ this.#setStatus("error");
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+ /**
51
+ * Remove the active service worker registration and clear listeners.
52
+ *
53
+ * Returns `false` when no registration is currently held.
54
+ */
55
+ async unregister() {
56
+ if (!this.#registration) {
57
+ return false;
58
+ }
59
+ navigator.serviceWorker.removeEventListener("message", this.#onMessage);
60
+ const ok = await this.#registration.unregister();
61
+ this.#registration = void 0;
62
+ this.#setStatus("unregistered");
63
+ return ok;
64
+ }
65
+ // ── Messaging ───────────────────────────────────────────────────────
66
+ /**
67
+ * Send a message to the active service worker.
68
+ */
69
+ postMessage(message) {
70
+ const active = this.#registration?.active;
71
+ if (active) {
72
+ active.postMessage(message);
73
+ }
74
+ }
75
+ /**
76
+ * Register a handler for messages **from** the service worker.
77
+ * @returns Unsubscribe function.
78
+ */
79
+ onMessage(handler) {
80
+ this.#messageHandlers.add(handler);
81
+ return () => {
82
+ this.#messageHandlers.delete(handler);
83
+ };
84
+ }
85
+ // ── Internals ───────────────────────────────────────────────────────
86
+ #setStatus(status) {
87
+ this.#status = status;
88
+ this.#onStatusChange?.(status);
89
+ }
90
+ #onMessage = (event) => {
91
+ for (const handler of this.#messageHandlers) {
92
+ try {
93
+ handler(event);
94
+ } catch {
95
+ }
96
+ }
97
+ };
98
+ }
99
+
100
+ export { ClientServiceWorker };