@lunora/client 1.0.0-alpha.7 → 1.0.0-alpha.9
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.
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/index.d.mts +29 -4
- package/dist/index.d.ts +29 -4
- package/dist/index.mjs +3 -3
- package/dist/packem_shared/{LunoraClient-Bsv8immC.mjs → LunoraClient-C4eFp92b.mjs} +157 -105
- package/dist/packem_shared/{OfflineQueue-D-ASeqL7.mjs → OfflineQueue-eJBiWPfg.mjs} +4 -1
- package/dist/packem_shared/createLocalStore-IOur0jHF.mjs +1 -0
- package/dist/packem_shared/{createServerClient-DucOyOz3.mjs → createServerClient-PHoMdNGJ.mjs} +1 -1
- package/dist/packem_shared/local-store-BNgN3Dw3.mjs +111 -0
- package/dist/packem_shared/{lunora-client.d-DLbxPGH9.d.mts → lunora-client.d-CfwxuJo8.d.mts} +123 -23
- package/dist/packem_shared/{lunora-client.d-DLbxPGH9.d.ts → lunora-client.d-CfwxuJo8.d.ts} +123 -23
- package/dist/packem_shared/{preload.d-BAT6GeSD.d.mts → preload.d-Bbn8plix.d.mts} +1 -1
- package/dist/packem_shared/{preload.d-CSkjbfEq.d.ts → preload.d-DuMetDdU.d.ts} +1 -1
- package/dist/query/index.d.mts +2 -2
- package/dist/query/index.d.ts +2 -2
- package/dist/ssr/index.d.mts +3 -3
- package/dist/ssr/index.d.ts +3 -3
- package/dist/ssr/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/packem_shared/createLocalStore-DSUfoLqY.mjs +0 -36
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-
|
|
1
|
+
import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-CfwxuJo8.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. */
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-
|
|
1
|
+
import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-CfwxuJo8.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-
|
|
2
|
-
export { type C as CachedQuery, type e as ClientMessage, type f as ClientShapeSubscribeMessage, type g as ClientShapeUnsubscribeMessage, type h as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type i as FunctionArgumentDescriptor, type j as FunctionDescriptor, type G as GlobalFacetResult, type k as GlobalFacetValue, type l as GlobalFilterClause, type m as GlobalTableInfo, type n as GlobalTablePage, L as LunoraClient, type o as LunoraClientOptions, type p as
|
|
3
|
-
export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-
|
|
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-CfwxuJo8.mjs";
|
|
2
|
+
export { type C as CachedQuery, type e as ClientMessage, type f as ClientShapeSubscribeMessage, type g as ClientShapeUnsubscribeMessage, type h as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type i as FunctionArgumentDescriptor, type j as FunctionDescriptor, type G as GlobalFacetResult, type k as GlobalFacetValue, type l as GlobalFilterClause, type m as GlobalTableInfo, type n as GlobalTablePage, L as LunoraClient, type o as LunoraClientOptions, type p as MutationSettledEvent, type q as OptimisticLocalStore, type r as OptimisticUpdate, type s as OutboxMutation, type t as OutboxSink, type u as PersistedMutation, type P as Preloaded, type v as RowOp, type w as RpcEnvelope, type x as RpcResponseBody, type y as ScheduleRecord, type z as SchedulerPoolStatus, type E as SchedulerStatus, type H as ServerMessage, type I as ServerPokeEndMessage, type J as ServerPokePartMessage, type K as ServerPokeStartMessage, type N as ShardTrafficEntry, type T as ShardTrafficResult, type V as StorageListPage, type W as StorageObject, type X as StreamHandle, type Y as StreamIterable, type Z as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, _ as SubscriptionRegistry, type $ as SubscriptionState, type a0 as SyncWatermark, type a as Unsubscribe, type U as User, type a1 as WorkflowInstanceAction, type a2 as WorkflowInstanceDetail, type a3 as WorkflowInstancePage, type a4 as WorkflowInstanceStatus, type a5 as WorkflowInstanceSummary, type a6 as WorkflowStepDetail, a7 as createLocalStore, a8 as createStream } from "./packem_shared/lunora-client.d-CfwxuJo8.mjs";
|
|
3
|
+
export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-Bbn8plix.mjs";
|
|
4
4
|
export type { AuthCapabilities, AuthImpersonation, AuthPage, AuthSession, AuthUser, CronJobInfo, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
|
|
5
5
|
/**
|
|
6
6
|
* The slice of React Native's `AsyncStorage` (or any async key/value store —
|
|
@@ -199,6 +199,20 @@ interface QueuedMutation<T = unknown> {
|
|
|
199
199
|
* signed out). Absent on hydrated legacy records, which replay ambiently.
|
|
200
200
|
*/
|
|
201
201
|
readonly identity?: string | null;
|
|
202
|
+
/**
|
|
203
|
+
* `true` when a live caller is still awaiting this write's `mutation()`
|
|
204
|
+
* Promise; `false`/absent for a write restored from durable storage after a
|
|
205
|
+
* reload (its original awaiter is gone). Carried so terminal-verdict
|
|
206
|
+
* observers can distinguish "the caller already saw this" from "nothing else
|
|
207
|
+
* will report this". Maps to the public `MutationSettledEvent.hadAwaiter`.
|
|
208
|
+
*/
|
|
209
|
+
liveAwaiter?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Invoked on a successful replay with the server's echoed commit CDC cursor,
|
|
212
|
+
* so a live per-call optimistic layer drops gaplessly once a frame reaches it.
|
|
213
|
+
* Absent on hydrated records (the optimistic write lived in a prior session).
|
|
214
|
+
*/
|
|
215
|
+
readonly onCommit?: (commitCursor: number | undefined) => void;
|
|
202
216
|
/** Rejects if the mutation can no longer be replayed. */
|
|
203
217
|
readonly reject: (error: unknown) => void;
|
|
204
218
|
/** Resolves once the mutation has been replayed against the server. */
|
|
@@ -206,6 +220,15 @@ interface QueuedMutation<T = unknown> {
|
|
|
206
220
|
readonly shardKey?: string;
|
|
207
221
|
}
|
|
208
222
|
/**
|
|
223
|
+
* Invoked when the queue itself discards an entry on overflow (capacity
|
|
224
|
+
* eviction), so the client can surface the dropped write on its
|
|
225
|
+
* terminal-verdict observer even when the entry has no live awaiter (a hydrated
|
|
226
|
+
* record). The `error` carries the `OFFLINE_QUEUE_OVERFLOW` code.
|
|
227
|
+
*/
|
|
228
|
+
type EvictHandler = (entry: QueuedMutation, error: Error & {
|
|
229
|
+
code?: string;
|
|
230
|
+
}) => void;
|
|
231
|
+
/**
|
|
209
232
|
* A process-unique id, used both per-mutation and as the fallback `clientId`. It
|
|
210
233
|
* MUST be globally unique: the server scopes a custom mutator's replay watermark
|
|
211
234
|
* by `(verifiedIdentity, clientId)`, and an anonymous push has no verified
|
|
@@ -215,6 +238,7 @@ interface QueuedMutation<T = unknown> {
|
|
|
215
238
|
* crypto-quality (or `Math.random`) entropy with the timestamp + counter so it
|
|
216
239
|
* can't collide across two clients started in the same millisecond.
|
|
217
240
|
*/
|
|
241
|
+
|
|
218
242
|
/**
|
|
219
243
|
* Bounded FIFO queue. Mutations issued while the client is offline are
|
|
220
244
|
* enqueued and replayed in the order they were submitted once the WS
|
|
@@ -233,8 +257,9 @@ declare class OfflineQueue {
|
|
|
233
257
|
private readonly maxItems;
|
|
234
258
|
private readonly onPersistenceError;
|
|
235
259
|
private readonly persistence;
|
|
260
|
+
private readonly onEvict;
|
|
236
261
|
private readonly items;
|
|
237
|
-
constructor(options?: OfflineQueueOptions, persistence?: PersistenceAdapter);
|
|
262
|
+
constructor(options?: OfflineQueueOptions, persistence?: PersistenceAdapter, onEvict?: EvictHandler);
|
|
238
263
|
get size(): number;
|
|
239
264
|
enqueue<T>(entry: QueuedMutation<T>): void;
|
|
240
265
|
/**
|
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-
|
|
2
|
-
export { type C as CachedQuery, type e as ClientMessage, type f as ClientShapeSubscribeMessage, type g as ClientShapeUnsubscribeMessage, type h as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type i as FunctionArgumentDescriptor, type j as FunctionDescriptor, type G as GlobalFacetResult, type k as GlobalFacetValue, type l as GlobalFilterClause, type m as GlobalTableInfo, type n as GlobalTablePage, L as LunoraClient, type o as LunoraClientOptions, type p as
|
|
3
|
-
export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-
|
|
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-CfwxuJo8.js";
|
|
2
|
+
export { type C as CachedQuery, type e as ClientMessage, type f as ClientShapeSubscribeMessage, type g as ClientShapeUnsubscribeMessage, type h as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type i as FunctionArgumentDescriptor, type j as FunctionDescriptor, type G as GlobalFacetResult, type k as GlobalFacetValue, type l as GlobalFilterClause, type m as GlobalTableInfo, type n as GlobalTablePage, L as LunoraClient, type o as LunoraClientOptions, type p as MutationSettledEvent, type q as OptimisticLocalStore, type r as OptimisticUpdate, type s as OutboxMutation, type t as OutboxSink, type u as PersistedMutation, type P as Preloaded, type v as RowOp, type w as RpcEnvelope, type x as RpcResponseBody, type y as ScheduleRecord, type z as SchedulerPoolStatus, type E as SchedulerStatus, type H as ServerMessage, type I as ServerPokeEndMessage, type J as ServerPokePartMessage, type K as ServerPokeStartMessage, type N as ShardTrafficEntry, type T as ShardTrafficResult, type V as StorageListPage, type W as StorageObject, type X as StreamHandle, type Y as StreamIterable, type Z as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, _ as SubscriptionRegistry, type $ as SubscriptionState, type a0 as SyncWatermark, type a as Unsubscribe, type U as User, type a1 as WorkflowInstanceAction, type a2 as WorkflowInstanceDetail, type a3 as WorkflowInstancePage, type a4 as WorkflowInstanceStatus, type a5 as WorkflowInstanceSummary, type a6 as WorkflowStepDetail, a7 as createLocalStore, a8 as createStream } from "./packem_shared/lunora-client.d-CfwxuJo8.js";
|
|
3
|
+
export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-DuMetDdU.js";
|
|
4
4
|
export type { AuthCapabilities, AuthImpersonation, AuthPage, AuthSession, AuthUser, CronJobInfo, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
|
|
5
5
|
/**
|
|
6
6
|
* The slice of React Native's `AsyncStorage` (or any async key/value store —
|
|
@@ -199,6 +199,20 @@ interface QueuedMutation<T = unknown> {
|
|
|
199
199
|
* signed out). Absent on hydrated legacy records, which replay ambiently.
|
|
200
200
|
*/
|
|
201
201
|
readonly identity?: string | null;
|
|
202
|
+
/**
|
|
203
|
+
* `true` when a live caller is still awaiting this write's `mutation()`
|
|
204
|
+
* Promise; `false`/absent for a write restored from durable storage after a
|
|
205
|
+
* reload (its original awaiter is gone). Carried so terminal-verdict
|
|
206
|
+
* observers can distinguish "the caller already saw this" from "nothing else
|
|
207
|
+
* will report this". Maps to the public `MutationSettledEvent.hadAwaiter`.
|
|
208
|
+
*/
|
|
209
|
+
liveAwaiter?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Invoked on a successful replay with the server's echoed commit CDC cursor,
|
|
212
|
+
* so a live per-call optimistic layer drops gaplessly once a frame reaches it.
|
|
213
|
+
* Absent on hydrated records (the optimistic write lived in a prior session).
|
|
214
|
+
*/
|
|
215
|
+
readonly onCommit?: (commitCursor: number | undefined) => void;
|
|
202
216
|
/** Rejects if the mutation can no longer be replayed. */
|
|
203
217
|
readonly reject: (error: unknown) => void;
|
|
204
218
|
/** Resolves once the mutation has been replayed against the server. */
|
|
@@ -206,6 +220,15 @@ interface QueuedMutation<T = unknown> {
|
|
|
206
220
|
readonly shardKey?: string;
|
|
207
221
|
}
|
|
208
222
|
/**
|
|
223
|
+
* Invoked when the queue itself discards an entry on overflow (capacity
|
|
224
|
+
* eviction), so the client can surface the dropped write on its
|
|
225
|
+
* terminal-verdict observer even when the entry has no live awaiter (a hydrated
|
|
226
|
+
* record). The `error` carries the `OFFLINE_QUEUE_OVERFLOW` code.
|
|
227
|
+
*/
|
|
228
|
+
type EvictHandler = (entry: QueuedMutation, error: Error & {
|
|
229
|
+
code?: string;
|
|
230
|
+
}) => void;
|
|
231
|
+
/**
|
|
209
232
|
* A process-unique id, used both per-mutation and as the fallback `clientId`. It
|
|
210
233
|
* MUST be globally unique: the server scopes a custom mutator's replay watermark
|
|
211
234
|
* by `(verifiedIdentity, clientId)`, and an anonymous push has no verified
|
|
@@ -215,6 +238,7 @@ interface QueuedMutation<T = unknown> {
|
|
|
215
238
|
* crypto-quality (or `Math.random`) entropy with the timestamp + counter so it
|
|
216
239
|
* can't collide across two clients started in the same millisecond.
|
|
217
240
|
*/
|
|
241
|
+
|
|
218
242
|
/**
|
|
219
243
|
* Bounded FIFO queue. Mutations issued while the client is offline are
|
|
220
244
|
* enqueued and replayed in the order they were submitted once the WS
|
|
@@ -233,8 +257,9 @@ declare class OfflineQueue {
|
|
|
233
257
|
private readonly maxItems;
|
|
234
258
|
private readonly onPersistenceError;
|
|
235
259
|
private readonly persistence;
|
|
260
|
+
private readonly onEvict;
|
|
236
261
|
private readonly items;
|
|
237
|
-
constructor(options?: OfflineQueueOptions, persistence?: PersistenceAdapter);
|
|
262
|
+
constructor(options?: OfflineQueueOptions, persistence?: PersistenceAdapter, onEvict?: EvictHandler);
|
|
238
263
|
get size(): number;
|
|
239
264
|
enqueue<T>(entry: QueuedMutation<T>): void;
|
|
240
265
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -2,11 +2,11 @@ export { createAsyncStoragePersistence } from './packem_shared/createAsyncStorag
|
|
|
2
2
|
export { default as createInMemoryBookmarkStorage } from './packem_shared/createInMemoryBookmarkStorage-BoN7a7TH.mjs';
|
|
3
3
|
export { applyDelta, isMutationDelta } from './packem_shared/applyDelta-4jFGTPA3.mjs';
|
|
4
4
|
export { CONFLICT_ERROR_CODE, isConflictError } from './packem_shared/CONFLICT_ERROR_CODE-aUdVbEDw.mjs';
|
|
5
|
-
export { createLocalStore } from './packem_shared/
|
|
6
|
-
export { LunoraClient } from './packem_shared/LunoraClient-
|
|
5
|
+
export { c as createLocalStore } from './packem_shared/local-store-BNgN3Dw3.mjs';
|
|
6
|
+
export { LunoraClient } from './packem_shared/LunoraClient-C4eFp92b.mjs';
|
|
7
7
|
export { createMutationRunner } from './packem_shared/createMutationRunner-BqsavzvG.mjs';
|
|
8
8
|
export { createMutatorRunner } from './packem_shared/createMutatorRunner-BETvCd0p.mjs';
|
|
9
|
-
export { OfflineQueue } from './packem_shared/OfflineQueue-
|
|
9
|
+
export { OfflineQueue } from './packem_shared/OfflineQueue-eJBiWPfg.mjs';
|
|
10
10
|
export { createInMemoryPersistence, createIndexedDbPersistence } from './packem_shared/createInMemoryPersistence-CW82inU5.mjs';
|
|
11
11
|
export { preloadQuery, preloadedQueryResult } from './packem_shared/preloadQuery-lobFkD2Z.mjs';
|
|
12
12
|
export { createInMemoryQueryCache, createIndexedDbQueryCache, queryCacheKey } from './packem_shared/createInMemoryQueryCache-B1PQ9Twl.mjs';
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { S as SubscriptionRegistry, s as stableStringify } from './subscription-C1Jy7HiF.mjs';
|
|
2
2
|
import createInMemoryBookmarkStorage from './createInMemoryBookmarkStorage-BoN7a7TH.mjs';
|
|
3
3
|
import { isMutationDelta, applyDelta } from './applyDelta-4jFGTPA3.mjs';
|
|
4
|
-
import { createLocalStore } from './
|
|
5
|
-
import { OfflineQueue, nextId, reportPersistenceError } from './OfflineQueue-
|
|
4
|
+
import { a as applyOptimisticLayer, d as dropConfirmedLayers, n as notifySubscription, f as foldOptimistic, c as createLocalStore } from './local-store-BNgN3Dw3.mjs';
|
|
5
|
+
import { OfflineQueue, nextId, reportPersistenceError } from './OfflineQueue-eJBiWPfg.mjs';
|
|
6
6
|
import { queryCacheKey } from './createInMemoryQueryCache-B1PQ9Twl.mjs';
|
|
7
7
|
import { createReconnect } from './createReconnect-Di_-oHH7.mjs';
|
|
8
8
|
import { createStream } from './DEFAULT_MAX_BUFFER-BDkqO5PW.mjs';
|
|
9
9
|
|
|
10
|
+
class Listeners {
|
|
11
|
+
listeners = /* @__PURE__ */ new Set();
|
|
12
|
+
add(listener) {
|
|
13
|
+
this.listeners.add(listener);
|
|
14
|
+
return () => {
|
|
15
|
+
this.listeners.delete(listener);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// The conditional rest tuple makes `emit()` argument-free for a
|
|
19
|
+
// `Listeners<void>` and one-argument for every other payload.
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- `[T] extends [void]` is the discriminant for the payload-free overload, not a value-position void
|
|
21
|
+
emit(...args) {
|
|
22
|
+
const [value] = args;
|
|
23
|
+
for (const listener of this.listeners) {
|
|
24
|
+
try {
|
|
25
|
+
listener(value);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
clear() {
|
|
31
|
+
this.listeners.clear();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
const RPC_PATH = "/_lunora/rpc";
|
|
11
36
|
const WS_PATH = "/_lunora/ws";
|
|
12
37
|
const bucketQuery = (bucket) => bucket === void 0 || bucket === "" ? "" : `&bucket=${encodeURIComponent(bucket)}`;
|
|
@@ -102,41 +127,6 @@ const withQuery = (path, params) => {
|
|
|
102
127
|
return query === "" ? path : `${path}?${query}`;
|
|
103
128
|
};
|
|
104
129
|
const connectionKey = (shardKey) => shardKey ?? "";
|
|
105
|
-
const writeOptimisticToState = (state, next) => {
|
|
106
|
-
const previous = state.lastValue;
|
|
107
|
-
const versionAtApply = state.serverVersion;
|
|
108
|
-
state.lastValue = next;
|
|
109
|
-
for (const callback of state.callbacks) {
|
|
110
|
-
try {
|
|
111
|
-
callback(next);
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return () => {
|
|
116
|
-
if (state.serverVersion > versionAtApply) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (state.lastValue !== next) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
state.lastValue = previous;
|
|
123
|
-
for (const callback of state.callbacks) {
|
|
124
|
-
try {
|
|
125
|
-
callback(previous);
|
|
126
|
-
} catch {
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
};
|
|
131
|
-
const applyOptimisticToState = (state, optimistic) => {
|
|
132
|
-
let next;
|
|
133
|
-
try {
|
|
134
|
-
next = optimistic(state.lastValue);
|
|
135
|
-
} catch {
|
|
136
|
-
return void 0;
|
|
137
|
-
}
|
|
138
|
-
return writeOptimisticToState(state, next);
|
|
139
|
-
};
|
|
140
130
|
const buildStreamError = (message) => {
|
|
141
131
|
const errorEnvelope = message.error;
|
|
142
132
|
const code = typeof errorEnvelope?.code === "string" ? errorEnvelope.code : void 0;
|
|
@@ -273,11 +263,13 @@ class LunoraClient {
|
|
|
273
263
|
queuedIdentities = /* @__PURE__ */ new Map();
|
|
274
264
|
closed = false;
|
|
275
265
|
/** Subscribers to auth-token changes (see `onAuthTokenChange`). */
|
|
276
|
-
authTokenListeners =
|
|
266
|
+
authTokenListeners = new Listeners();
|
|
277
267
|
/** Subscribers to aggregate connection-status changes (see `onConnectionStatus`). */
|
|
278
|
-
statusListeners =
|
|
268
|
+
statusListeners = new Listeners();
|
|
279
269
|
/** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
|
|
280
|
-
tokenExpiredListeners =
|
|
270
|
+
tokenExpiredListeners = new Listeners();
|
|
271
|
+
/** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
|
|
272
|
+
mutationSettledListeners = new Listeners();
|
|
281
273
|
/**
|
|
282
274
|
* Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
|
|
283
275
|
* of callbacks. Membership doubles as the resubscribe set replayed on every
|
|
@@ -316,7 +308,9 @@ class LunoraClient {
|
|
|
316
308
|
this.persistence = options.persistence;
|
|
317
309
|
this.queryCache = options.queryCache === false ? void 0 : options.queryCache;
|
|
318
310
|
this.onPersistenceError = options.offlineQueue?.onPersistenceError;
|
|
319
|
-
this.offlineQueue = new OfflineQueue(options.offlineQueue, options.persistence)
|
|
311
|
+
this.offlineQueue = new OfflineQueue(options.offlineQueue, options.persistence, (entry, error) => {
|
|
312
|
+
this.emitItemSettled(entry, "rejected", error);
|
|
313
|
+
});
|
|
320
314
|
this.outbox = options.outbox;
|
|
321
315
|
this.clientId = options.clientId ?? `client-${nextId()}`;
|
|
322
316
|
if (this.persistence) {
|
|
@@ -347,12 +341,7 @@ class LunoraClient {
|
|
|
347
341
|
}
|
|
348
342
|
this.authToken = token;
|
|
349
343
|
this.rejectQueuedForIdentityChange();
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
listener(token);
|
|
353
|
-
} catch {
|
|
354
|
-
}
|
|
355
|
-
}
|
|
344
|
+
this.authTokenListeners.emit(token);
|
|
356
345
|
}
|
|
357
346
|
getAuthToken() {
|
|
358
347
|
return this.authToken;
|
|
@@ -424,10 +413,7 @@ class LunoraClient {
|
|
|
424
413
|
* the current value.
|
|
425
414
|
*/
|
|
426
415
|
onAuthTokenChange(listener) {
|
|
427
|
-
this.authTokenListeners.add(listener);
|
|
428
|
-
return () => {
|
|
429
|
-
this.authTokenListeners.delete(listener);
|
|
430
|
-
};
|
|
416
|
+
return this.authTokenListeners.add(listener);
|
|
431
417
|
}
|
|
432
418
|
/**
|
|
433
419
|
* Fetch the currently authenticated user from better-auth's `get-session`
|
|
@@ -630,10 +616,7 @@ class LunoraClient {
|
|
|
630
616
|
* with a freshly minted one. Returns an unsubscribe function.
|
|
631
617
|
*/
|
|
632
618
|
onTokenExpired(listener) {
|
|
633
|
-
this.tokenExpiredListeners.add(listener);
|
|
634
|
-
return () => {
|
|
635
|
-
this.tokenExpiredListeners.delete(listener);
|
|
636
|
-
};
|
|
619
|
+
return this.tokenExpiredListeners.add(listener);
|
|
637
620
|
}
|
|
638
621
|
// --- Connection status --------------------------------------------------
|
|
639
622
|
/**
|
|
@@ -649,11 +632,23 @@ class LunoraClient {
|
|
|
649
632
|
* unsubscribe function.
|
|
650
633
|
*/
|
|
651
634
|
onConnectionStatus(listener) {
|
|
652
|
-
this.statusListeners.add(listener);
|
|
635
|
+
const unsubscribe = this.statusListeners.add(listener);
|
|
653
636
|
listener(this.computeStatus());
|
|
654
|
-
return
|
|
655
|
-
|
|
656
|
-
|
|
637
|
+
return unsubscribe;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Subscribe to terminal verdicts for offline-queued mutations. The listener
|
|
641
|
+
* fires once per queued write that commits or is rejected — including a write
|
|
642
|
+
* restored from durable storage after a reload, whose original `mutation()`
|
|
643
|
+
* Promise no longer exists (`hadAwaiter: false`), and a write the queue
|
|
644
|
+
* evicts on overflow or discards on an identity change. This is the durable
|
|
645
|
+
* channel for surfacing a rolled-back optimistic write to the UI; an online
|
|
646
|
+
* mutation that never queued still surfaces through the Promise `mutation()`
|
|
647
|
+
* returns. The listener is NOT invoked on registration. Returns an
|
|
648
|
+
* unsubscribe function. See {@link MutationSettledEvent}.
|
|
649
|
+
*/
|
|
650
|
+
onMutationSettled(listener) {
|
|
651
|
+
return this.mutationSettledListeners.add(listener);
|
|
657
652
|
}
|
|
658
653
|
// --- RPC ---------------------------------------------------------------
|
|
659
654
|
async query(function_, args, options = {}) {
|
|
@@ -678,9 +673,14 @@ class LunoraClient {
|
|
|
678
673
|
}
|
|
679
674
|
const argsRecord = args;
|
|
680
675
|
const mutationId = options.mutationId ?? nextId();
|
|
681
|
-
const optimisticRollbacks = this.applyOptimisticUpdates(
|
|
676
|
+
const { confirms: optimisticConfirms, rollbacks: optimisticRollbacks } = this.applyOptimisticUpdates(
|
|
677
|
+
function_.__lunoraRef,
|
|
678
|
+
argsRecord,
|
|
679
|
+
options.shardKey,
|
|
680
|
+
options.optimistic
|
|
681
|
+
);
|
|
682
682
|
if (options.optimisticUpdate) {
|
|
683
|
-
this.applyOptimisticUpdate(options.optimisticUpdate, args, options.shardKey, optimisticRollbacks);
|
|
683
|
+
this.applyOptimisticUpdate(options.optimisticUpdate, args, options.shardKey, optimisticRollbacks, optimisticConfirms);
|
|
684
684
|
}
|
|
685
685
|
const conn = this.getConnection(options.shardKey);
|
|
686
686
|
const wsState = conn?.wsState ?? "idle";
|
|
@@ -691,10 +691,21 @@ class LunoraClient {
|
|
|
691
691
|
const shouldQueueOffline = this.WebSocketImpl !== void 0 && connectedGate;
|
|
692
692
|
const midReconnect = wsState === "connecting" && connectedGate;
|
|
693
693
|
if (wsState !== "open" && !hasSocket && shouldQueueOffline || midReconnect) {
|
|
694
|
-
return this.enqueueOfflineMutation(function_, argsRecord, options.shardKey, mutationId, optimisticRollbacks);
|
|
694
|
+
return this.enqueueOfflineMutation(function_, argsRecord, options.shardKey, mutationId, optimisticRollbacks, optimisticConfirms);
|
|
695
695
|
}
|
|
696
696
|
try {
|
|
697
|
-
|
|
697
|
+
let commitCursor;
|
|
698
|
+
const result = await this.rpc(function_.__lunoraRef, argsRecord, options.shardKey, {
|
|
699
|
+
captureBookmark: true,
|
|
700
|
+
mutationId,
|
|
701
|
+
onCommitCursor: (cursor) => {
|
|
702
|
+
commitCursor = cursor;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
for (const confirm of optimisticConfirms) {
|
|
706
|
+
confirm(commitCursor);
|
|
707
|
+
}
|
|
708
|
+
return result;
|
|
698
709
|
} catch (error) {
|
|
699
710
|
rollbackOptimistic(optimisticRollbacks);
|
|
700
711
|
throw error;
|
|
@@ -1346,8 +1357,9 @@ class LunoraClient {
|
|
|
1346
1357
|
fn: function_,
|
|
1347
1358
|
id,
|
|
1348
1359
|
lastValue: cached?.value,
|
|
1360
|
+
optimisticLayers: [],
|
|
1361
|
+
serverBase: cached?.value,
|
|
1349
1362
|
serverCursor: cached?.serverCursor,
|
|
1350
|
-
serverVersion: 0,
|
|
1351
1363
|
shardKey: options.shardKey,
|
|
1352
1364
|
...cached?.serverEpoch === void 0 ? {} : { serverEpoch: cached.serverEpoch }
|
|
1353
1365
|
};
|
|
@@ -1529,6 +1541,7 @@ class LunoraClient {
|
|
|
1529
1541
|
this.authTokenListeners.clear();
|
|
1530
1542
|
this.statusListeners.clear();
|
|
1531
1543
|
this.tokenExpiredListeners.clear();
|
|
1544
|
+
this.mutationSettledListeners.clear();
|
|
1532
1545
|
this.whisperHandlers.clear();
|
|
1533
1546
|
this.shapeSubscriptions.clear();
|
|
1534
1547
|
this.pokeBuffers.clear();
|
|
@@ -1545,7 +1558,7 @@ class LunoraClient {
|
|
|
1545
1558
|
* optimistically (confirmation rides the synced view). Otherwise the
|
|
1546
1559
|
* built-in `OfflineQueue` resolves/rejects the returned promise on replay.
|
|
1547
1560
|
*/
|
|
1548
|
-
async enqueueOfflineMutation(function_, argsRecord, shardKey, mutationId, optimisticRollbacks) {
|
|
1561
|
+
async enqueueOfflineMutation(function_, argsRecord, shardKey, mutationId, optimisticRollbacks, optimisticConfirms) {
|
|
1549
1562
|
const issuingIdentity = this.identityFingerprint();
|
|
1550
1563
|
if (this.outbox) {
|
|
1551
1564
|
this.outboxMutationCounter += 1;
|
|
@@ -1564,18 +1577,32 @@ class LunoraClient {
|
|
|
1564
1577
|
rollbackOptimistic(optimisticRollbacks);
|
|
1565
1578
|
throw error instanceof Error ? error : new Error(String(error));
|
|
1566
1579
|
}
|
|
1580
|
+
for (const confirm of optimisticConfirms) {
|
|
1581
|
+
confirm(void 0);
|
|
1582
|
+
}
|
|
1567
1583
|
return void 0;
|
|
1568
1584
|
}
|
|
1569
1585
|
return new Promise((resolve, reject) => {
|
|
1570
1586
|
const entry = {
|
|
1571
1587
|
args: argsRecord,
|
|
1572
1588
|
functionPath: function_.__lunoraRef,
|
|
1589
|
+
// A live caller is awaiting this Promise, so a terminal verdict
|
|
1590
|
+
// reaches them directly; the observer event carries
|
|
1591
|
+
// `hadAwaiter: true`. Hydrated replays leave this unset.
|
|
1592
|
+
liveAwaiter: true,
|
|
1573
1593
|
// Reuse the call's idempotency key as the queue id so the replay
|
|
1574
1594
|
// carries the same `x-lunora-mutation-id` the server dedups on.
|
|
1575
1595
|
id: mutationId,
|
|
1576
1596
|
// Persist the stamp alongside the record so a hydrated write can
|
|
1577
1597
|
// only replay under the identity that queued it.
|
|
1578
1598
|
identity: issuingIdentity,
|
|
1599
|
+
// Confirm the per-call optimistic layer(s) against the commit cursor
|
|
1600
|
+
// the flush replay echoes (see flushOfflineQueue).
|
|
1601
|
+
onCommit: (commitCursor) => {
|
|
1602
|
+
for (const confirm of optimisticConfirms) {
|
|
1603
|
+
confirm(commitCursor);
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1579
1606
|
reject: (error) => {
|
|
1580
1607
|
this.queuedIdentities.delete(mutationId);
|
|
1581
1608
|
rollbackOptimistic(optimisticRollbacks);
|
|
@@ -1646,7 +1673,8 @@ class LunoraClient {
|
|
|
1646
1673
|
* (nothing to render offline).
|
|
1647
1674
|
*/
|
|
1648
1675
|
persistQueryValue(state) {
|
|
1649
|
-
|
|
1676
|
+
const authoritative = state.serverBase;
|
|
1677
|
+
if (!this.queryCache || authoritative === void 0) {
|
|
1650
1678
|
return;
|
|
1651
1679
|
}
|
|
1652
1680
|
const key = queryCacheKey(state.fn.__lunoraRef, state.argsKey, state.shardKey);
|
|
@@ -1654,7 +1682,7 @@ class LunoraClient {
|
|
|
1654
1682
|
identity: this.identityFingerprint(),
|
|
1655
1683
|
serverCursor: state.serverCursor,
|
|
1656
1684
|
ts: Date.now(),
|
|
1657
|
-
value:
|
|
1685
|
+
value: authoritative,
|
|
1658
1686
|
...state.serverEpoch === void 0 ? {} : { serverEpoch: state.serverEpoch }
|
|
1659
1687
|
});
|
|
1660
1688
|
this.cacheFlushTimer ??= setTimeout(() => {
|
|
@@ -1694,12 +1722,25 @@ class LunoraClient {
|
|
|
1694
1722
|
return;
|
|
1695
1723
|
}
|
|
1696
1724
|
this.lastStatus = next;
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1725
|
+
this.statusListeners.emit(next);
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Build a {@link MutationSettledEvent} from a queued entry and emit it on the
|
|
1729
|
+
* {@link onMutationSettled} channel. `item.id` is always assigned by the time
|
|
1730
|
+
* a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
|
|
1731
|
+
* is unreachable — present only to satisfy the optional queue-id type.
|
|
1732
|
+
*/
|
|
1733
|
+
emitItemSettled(item, status, error) {
|
|
1734
|
+
this.mutationSettledListeners.emit({
|
|
1735
|
+
args: item.args,
|
|
1736
|
+
code: error === void 0 ? void 0 : error.code,
|
|
1737
|
+
error,
|
|
1738
|
+
functionPath: item.functionPath,
|
|
1739
|
+
hadAwaiter: item.liveAwaiter ?? false,
|
|
1740
|
+
id: item.id ?? "",
|
|
1741
|
+
shardKey: item.shardKey,
|
|
1742
|
+
status
|
|
1743
|
+
});
|
|
1703
1744
|
}
|
|
1704
1745
|
/**
|
|
1705
1746
|
* Apply an optimistic update to the subscription that matches the mutation's
|
|
@@ -1716,31 +1757,35 @@ class LunoraClient {
|
|
|
1716
1757
|
* of whether the caller passed `undefined` or omitted the field.
|
|
1717
1758
|
*/
|
|
1718
1759
|
applyOptimisticUpdates(functionRef, argsRecord, mutationShardKey, optimistic) {
|
|
1719
|
-
const
|
|
1760
|
+
const confirms = [];
|
|
1761
|
+
const rollbacks = [];
|
|
1720
1762
|
if (!optimistic) {
|
|
1721
|
-
return
|
|
1763
|
+
return { confirms, rollbacks };
|
|
1722
1764
|
}
|
|
1723
1765
|
const matchKey = SubscriptionRegistry.key(functionRef, argsRecord, mutationShardKey);
|
|
1724
1766
|
const state = this.subscriptions.get(matchKey);
|
|
1725
1767
|
if (state) {
|
|
1726
|
-
const
|
|
1727
|
-
if (
|
|
1728
|
-
|
|
1768
|
+
const handle = applyOptimisticLayer(state, optimistic);
|
|
1769
|
+
if (handle) {
|
|
1770
|
+
confirms.push(handle.confirm);
|
|
1771
|
+
rollbacks.push(handle.rollback);
|
|
1729
1772
|
}
|
|
1730
1773
|
}
|
|
1731
|
-
return
|
|
1774
|
+
return { confirms, rollbacks };
|
|
1732
1775
|
}
|
|
1733
1776
|
/**
|
|
1734
1777
|
* Run a Convex-parity `optimisticUpdate` callback against a localStore bound
|
|
1735
|
-
* to the live subscription registry
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1778
|
+
* to the live subscription registry. Each `setQuery` registers a constant
|
|
1779
|
+
* optimistic LAYER on its target subscription (via the same engine the
|
|
1780
|
+
* per-call `optimistic` path uses), so the multi-query patch rebases onto
|
|
1781
|
+
* incoming deltas and drops gaplessly on its commit cursor — its `confirm` /
|
|
1782
|
+
* `rollback` closures are appended to the mutation's settle lists. A throwing
|
|
1783
|
+
* callback unwinds its own partial writes — LIFO over just the rollbacks it
|
|
1784
|
+
* produced — and is swallowed, so a buggy optimistic update can never fail the
|
|
1785
|
+
* mutation or leave a partial patch live.
|
|
1786
|
+
*/
|
|
1787
|
+
applyOptimisticUpdate(optimisticUpdate, args, shardKey, optimisticRollbacks, optimisticConfirms) {
|
|
1788
|
+
const { confirms, rollbacks, store } = createLocalStore(this.subscriptions, shardKey, stableStringify);
|
|
1744
1789
|
try {
|
|
1745
1790
|
optimisticUpdate(store, args);
|
|
1746
1791
|
} catch {
|
|
@@ -1750,6 +1795,7 @@ class LunoraClient {
|
|
|
1750
1795
|
return;
|
|
1751
1796
|
}
|
|
1752
1797
|
optimisticRollbacks.push(...rollbacks);
|
|
1798
|
+
optimisticConfirms.push(...confirms);
|
|
1753
1799
|
}
|
|
1754
1800
|
getConnection(shardKey) {
|
|
1755
1801
|
return this.connections.get(connectionKey(shardKey));
|
|
@@ -1850,6 +1896,7 @@ class LunoraClient {
|
|
|
1850
1896
|
throw new Error(`LunoraClient: request failed (status ${response.status.toString()}${statusText})`);
|
|
1851
1897
|
}
|
|
1852
1898
|
flags.onMutationAck?.(body.lastMutationId);
|
|
1899
|
+
flags.onCommitCursor?.(body.commitCursor);
|
|
1853
1900
|
return body.result;
|
|
1854
1901
|
}
|
|
1855
1902
|
/**
|
|
@@ -2312,8 +2359,7 @@ class LunoraClient {
|
|
|
2312
2359
|
return;
|
|
2313
2360
|
}
|
|
2314
2361
|
const payload = this.resolveDataPayload(message, state);
|
|
2315
|
-
state.
|
|
2316
|
-
state.serverVersion += 1;
|
|
2362
|
+
state.serverBase = payload;
|
|
2317
2363
|
if (message.cursor !== void 0) {
|
|
2318
2364
|
state.serverCursor = message.cursor;
|
|
2319
2365
|
}
|
|
@@ -2321,12 +2367,8 @@ class LunoraClient {
|
|
|
2321
2367
|
state.serverEpoch = message.epoch;
|
|
2322
2368
|
}
|
|
2323
2369
|
this.persistQueryValue(state);
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
callback(payload);
|
|
2327
|
-
} catch {
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2370
|
+
dropConfirmedLayers(state, state.serverCursor);
|
|
2371
|
+
notifySubscription(state, state.optimisticLayers.length === 0 ? payload : foldOptimistic(payload, state.optimisticLayers));
|
|
2330
2372
|
}
|
|
2331
2373
|
/**
|
|
2332
2374
|
* Handle a `resume` frame (Pillar 1b): the server proved nothing the
|
|
@@ -2383,6 +2425,9 @@ class LunoraClient {
|
|
|
2383
2425
|
state.serverEpoch = epoch;
|
|
2384
2426
|
}
|
|
2385
2427
|
this.persistQueryValue(state);
|
|
2428
|
+
if (dropConfirmedLayers(state, state.serverCursor)) {
|
|
2429
|
+
notifySubscription(state, foldOptimistic(state.serverBase, state.optimisticLayers));
|
|
2430
|
+
}
|
|
2386
2431
|
}
|
|
2387
2432
|
}
|
|
2388
2433
|
/**
|
|
@@ -2403,8 +2448,8 @@ class LunoraClient {
|
|
|
2403
2448
|
return message.data;
|
|
2404
2449
|
}
|
|
2405
2450
|
const { delta } = message;
|
|
2406
|
-
if (isMutationDelta(delta) && state.
|
|
2407
|
-
const merged = applyDelta(state.
|
|
2451
|
+
if (isMutationDelta(delta) && state.serverBase !== void 0) {
|
|
2452
|
+
const merged = applyDelta(state.serverBase, delta);
|
|
2408
2453
|
if (merged !== void 0) {
|
|
2409
2454
|
return merged;
|
|
2410
2455
|
}
|
|
@@ -2426,12 +2471,7 @@ class LunoraClient {
|
|
|
2426
2471
|
}
|
|
2427
2472
|
/** Notify every {@link onTokenExpired} listener (best-effort, listener throws swallowed). */
|
|
2428
2473
|
notifyTokenExpired() {
|
|
2429
|
-
|
|
2430
|
-
try {
|
|
2431
|
-
listener();
|
|
2432
|
-
} catch {
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2474
|
+
this.tokenExpiredListeners.emit();
|
|
2435
2475
|
}
|
|
2436
2476
|
handleCompleteMessage(id) {
|
|
2437
2477
|
const stream = this.streams.get(id);
|
|
@@ -2488,6 +2528,7 @@ class LunoraClient {
|
|
|
2488
2528
|
const error = new Error("offline mutation discarded: auth identity changed before replay");
|
|
2489
2529
|
error.code = "OFFLINE_IDENTITY_CHANGED";
|
|
2490
2530
|
item.reject(error);
|
|
2531
|
+
this.emitItemSettled(item, "rejected", error);
|
|
2491
2532
|
}
|
|
2492
2533
|
this.clearQueryCacheForIdentityChange();
|
|
2493
2534
|
}
|
|
@@ -2523,17 +2564,28 @@ class LunoraClient {
|
|
|
2523
2564
|
const error = new Error("offline mutation skipped: auth identity changed before replay");
|
|
2524
2565
|
error.code = "OFFLINE_IDENTITY_CHANGED";
|
|
2525
2566
|
item.reject(error);
|
|
2567
|
+
this.emitItemSettled(item, "rejected", error);
|
|
2526
2568
|
continue;
|
|
2527
2569
|
}
|
|
2528
2570
|
this.queuedIdentities.delete(item.id ?? "");
|
|
2529
2571
|
try {
|
|
2530
|
-
|
|
2572
|
+
let commitCursor;
|
|
2573
|
+
const value = await this.rpc(item.functionPath, item.args, item.shardKey, {
|
|
2574
|
+
captureBookmark: true,
|
|
2575
|
+
mutationId: item.id,
|
|
2576
|
+
onCommitCursor: (cursor) => {
|
|
2577
|
+
commitCursor = cursor;
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2531
2580
|
this.unpersist(item.id);
|
|
2581
|
+
item.onCommit?.(commitCursor);
|
|
2532
2582
|
item.resolve(value);
|
|
2583
|
+
this.emitItemSettled(item, "committed");
|
|
2533
2584
|
} catch (error) {
|
|
2534
2585
|
if (error.code !== void 0) {
|
|
2535
2586
|
this.unpersist(item.id);
|
|
2536
2587
|
item.reject(error);
|
|
2588
|
+
this.emitItemSettled(item, "rejected", error);
|
|
2537
2589
|
continue;
|
|
2538
2590
|
}
|
|
2539
2591
|
this.offlineQueue.requeue(drained.slice(index));
|