@gonvex/client 0.5.2-staging.13 → 0.5.2-staging.15

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/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { BrowserTelemetryInfo, ExecutionScope, JsonValue, MessageTrace, ServerCapabilities, ServerMessage } from "@gonvex/protocol";
1
+ import type { BrowserTelemetryInfo, ExecutionScope, JsonValue, MessageTrace, ServerCapabilities, ServerMessage, ReducerErrorClass } from "@gonvex/protocol";
2
2
  import type { LocalRuntimeBinding } from "@gonvex/local-runtime/worker-client";
3
3
  import { type ErrorReporterOptions } from "./error-reporter.js";
4
4
  export { GonvexErrorReporter } from "./error-reporter.js";
5
5
  export type { ErrorReporterOptions, ErrorEventPayload, ErrorContext, ErrorAccount } from "./error-reporter.js";
6
6
  import { type OptimisticPatch, type OptimisticTransactionDefinition } from "./optimistic.js";
7
- import { type OutboxStore } from "./outbox.js";
7
+ import { type OutboxErrorClass, type ReducerOutboxScopeSummary, type ReducerOutboxState, type OutboxStore } from "./outbox.js";
8
8
  import { type LocalReplicaStorage, type LocalReplicaView, type ReplicaRow, type LiveQueryResult, type ReplicaCollectionState, type ReplicaCollectionSubscriptionState, type ReplicaCollectionPlan } from "./local-replica.js";
9
9
  import { type LiveQueryPlan, type OfflineLiveQueryResult } from "./query-expression.js";
10
10
  export * from "./error-reporter.js";
@@ -14,6 +14,8 @@ export * from "./kv-stores.js";
14
14
  export * from "./client-upgrades.js";
15
15
  export * from "./browser-upgrades.js";
16
16
  export * from "./signals.js";
17
+ export * from "./paged-read-view.js";
18
+ export { mergeReplicaRecord, type ReplicaRecordVersion, type VersionedReplicaRecord } from './replica-record.js';
17
19
  export * from "./external-auth.js";
18
20
  export { MemoryLocalReplicaStorage, type LocalReplicaStorage, type LocalReplicaSession, type LocalReplicaView, type ReplicaChange, type ReplicaFreshness, type ReplicaRow, type ReplicaScope, type ReplicaSnapshot, type ReplicaTransaction, type ReplicaWindow, type LiveQueryResult, type ReplicaCollectionState, type ReplicaCollectionSubscriptionState, type ReplicaCollectionPlan, } from "./local-replica.js";
19
21
  export * from "./query-expression.js";
@@ -93,12 +95,86 @@ export declare class GonvexClientError extends Error {
93
95
  readonly code: GonvexClientErrorCode;
94
96
  readonly path?: string;
95
97
  readonly operation?: "query" | "reducer" | "action";
98
+ /**
99
+ * For `server` Reducer errors: the runtime's classification, or a legacy
100
+ * inference when an older runtime sent none. Undefined means a legacy
101
+ * runtime's unclassified error, which is handled as a rejection.
102
+ */
103
+ readonly errorClass?: ReducerErrorClass;
104
+ /** True when the same call (same idempotency key) may succeed later. */
105
+ readonly retryable?: boolean;
96
106
  constructor(message: string, options: {
97
107
  code: GonvexClientErrorCode;
98
108
  path?: string;
99
109
  operation?: "query" | "reducer" | "action";
110
+ errorClass?: ReducerErrorClass;
111
+ retryable?: boolean;
100
112
  });
101
113
  }
114
+ /** One durable reducer intent, as exposed to application UI. */
115
+ export type OutboxIntent = {
116
+ /** The reducer id / idempotency key. Stable across retries, reloads and tabs. */
117
+ id: string;
118
+ /** Local queue sequence number. Lower numbers were issued first. */
119
+ entryId: number;
120
+ reducer: string;
121
+ state: ReducerOutboxState;
122
+ /** Failed deliveries counted toward the retry budget. */
123
+ attempts: number;
124
+ lastError?: string;
125
+ errorClass?: OutboxErrorClass;
126
+ createdAt: number;
127
+ nextAttemptAt: number;
128
+ /** When the intent became `failed` or `rejected`. */
129
+ settledAt?: number;
130
+ args: unknown;
131
+ /** Short, display-safe JSON preview of the arguments. */
132
+ argsSummary: string;
133
+ /** Rows this intent's optimistic prediction touched (best effort). */
134
+ entities: Array<{
135
+ entity: string;
136
+ id: string;
137
+ }>;
138
+ };
139
+ /** Per-row delivery status derived from the outbox. */
140
+ export type EntityIntentStatus = "syncing" | "failed" | "rejected";
141
+ export type ReducerRejectionEvent = {
142
+ reducerId: string;
143
+ path: string;
144
+ error: string;
145
+ errorClass?: OutboxErrorClass;
146
+ };
147
+ export type OutboxScope = ReducerOutboxScopeSummary & {
148
+ /** True for the identity this client is currently signed in as. */
149
+ current: boolean;
150
+ };
151
+ export type OutboxRetryOptions = {
152
+ /**
153
+ * Counted delivery failures (transient server errors and timeouts) before an
154
+ * intent is parked as `failed`. Default 10. `Infinity` retries forever.
155
+ */
156
+ maxAttempts?: number;
157
+ /** Cap for the exponential backoff between attempts. Default 60s. */
158
+ maxBackoffMs?: number;
159
+ };
160
+ export type ResetLocalReplicaOptions = {
161
+ /**
162
+ * Keep the durable outbox (default true). Pending, failed and rejected
163
+ * intents survive the reset and their predictions are re-applied on top of
164
+ * the rehydrated data. `false` also deletes every intent of the active
165
+ * identity that is not already inflight or committed; use it only for an
166
+ * explicit "discard unsynced changes" action.
167
+ */
168
+ keepOutbox?: boolean;
169
+ };
170
+ export type ResetLocalReplicaResult = {
171
+ /** Intents deleted because `keepOutbox: false` was requested. */
172
+ discardedIntents: number;
173
+ /** Replica Collections and Live Queries re-requested from the server. */
174
+ resubscribed: number;
175
+ };
176
+ export declare const DEFAULT_OUTBOX_MAX_ATTEMPTS = 10;
177
+ export declare const DEFAULT_OUTBOX_RETRY_MAX_BACKOFF_MS = 60000;
102
178
  export type ConnectionState = {
103
179
  isWebSocketConnected: boolean;
104
180
  hasEverConnected: boolean;
@@ -189,10 +265,13 @@ export type GonvexClientOptions = GonvexClientAuth & {
189
265
  databaseName?: string;
190
266
  enabled?: boolean;
191
267
  store?: OutboxStore;
268
+ retry?: OutboxRetryOptions;
192
269
  };
193
270
  /** Transactional normalized store used by Replica Collections and Live Queries. */
194
271
  localReplica?: {
195
272
  storage?: LocalReplicaStorage;
273
+ maxResidentRows?: number;
274
+ maxResidentBytes?: number;
196
275
  };
197
276
  errorReporting?: false | Omit<ErrorReporterOptions, "transport" | "project" | "tenant">;
198
277
  timeouts?: GonvexTimeoutOptions;
@@ -212,20 +291,32 @@ export type GonvexTelemetryEvent = {
212
291
  };
213
292
  export declare class GonvexClient {
214
293
  private readonly url;
294
+ private readonly localTableBindings;
215
295
  private readonly localBinding?;
216
296
  private readonly localExecutor?;
217
297
  private readonly localStorage?;
218
298
  private localIdentity?;
219
299
  private localLane;
300
+ private preparationLane;
220
301
  private localReplayScheduled;
302
+ private waitingLocalEdits;
221
303
  private replacingLocal;
222
304
  private readonly unsubscribeLocal?;
223
305
  private readonly localCollectionClosers;
306
+ private readonly localCollectionKeys;
307
+ private readonly localExecutionTables;
224
308
  private readonly reducerRejectionHandlers;
309
+ private readonly outboxRetry;
310
+ private unauthenticatedRetries;
311
+ private intentsSnapshotValue;
312
+ private readonly intentListeners;
313
+ private intentsRefreshRunning;
314
+ private intentsRefreshDirty;
225
315
  private socket;
226
316
  private readonly handlers;
227
317
  private readonly querySubscriptions;
228
318
  private readonly replicaSubscriptions;
319
+ private readonly replicaMetadataListeners;
229
320
  private readonly oneShotQueries;
230
321
  private readonly telemetryHandlers;
231
322
  private readonly pendingMessages;
@@ -266,8 +357,15 @@ export declare class GonvexClient {
266
357
  private processedReplicaWatermarkRevision;
267
358
  private readonly pendingReplicaTransactions;
268
359
  private readonly unsubscribeOutbox;
360
+ private readonly sharedOutboxStore?;
361
+ private readonly unsubscribePeerOutbox?;
362
+ private readonly unsubscribePeerReplica?;
363
+ private peerRefreshScheduled;
364
+ private peerRefreshDirty;
269
365
  private readonly unsubscribeBrowserOnline;
270
366
  private drainingOutbox;
367
+ /** A wake-up (timer, enqueue, reconnect) that arrived while a drain was running. */
368
+ private outboxDrainRequested;
271
369
  private outboxDrainTimer;
272
370
  private readonly sessionScopeHandlers;
273
371
  private readonly errorReporter;
@@ -289,20 +387,86 @@ export declare class GonvexClient {
289
387
  private updateRequired;
290
388
  private lastOnlineAtMs;
291
389
  constructor(url: string, options?: GonvexClientOptions);
292
- /** Authoritative sync failures arrive after an offline call returned locally. */
293
- onReducerRejection(listener: (event: {
294
- reducerId: string;
295
- path: string;
296
- error: string;
297
- }) => void): () => void;
390
+ /**
391
+ * Authoritative sync failures arrive after an offline call returned locally.
392
+ * The intent also stays in the outbox as `rejected` until it is discarded
393
+ * or retried, so a UI that mounts later can still show it.
394
+ */
395
+ onReducerRejection(listener: (event: ReducerRejectionEvent) => void): () => void;
396
+ /** Every durable intent of the current identity, oldest first. */
397
+ listIntents(): Promise<OutboxIntent[]>;
398
+ /**
399
+ * Synchronous snapshot for external stores (React `useSyncExternalStore`).
400
+ * Kept current while at least one {@link subscribeIntents} listener exists.
401
+ */
402
+ intentsSnapshot(): readonly OutboxIntent[];
403
+ /** Observe intent changes; call {@link intentsSnapshot} for the new value. */
404
+ subscribeIntents(listener: () => void): () => void;
405
+ /**
406
+ * Delivery status for one row from the current snapshot: `failed` or
407
+ * `rejected` when an intent touching it needs attention, `syncing` while one
408
+ * is still queued, otherwise undefined.
409
+ */
410
+ entityIntentStatus(entity: string, id: string): EntityIntentStatus | undefined;
411
+ /**
412
+ * Re-arm a `failed` or `rejected` intent with a fresh retry budget. The
413
+ * original idempotency key is reused, so an attempt that had actually
414
+ * committed is replayed by the server rather than applied twice.
415
+ */
416
+ retryIntent(id: string): Promise<boolean>;
417
+ /**
418
+ * Drop a `pending`, `failed` or `rejected` intent: it will never be sent,
419
+ * its optimistic prediction is removed and later local intents are rebased.
420
+ * Inflight and committed intents cannot be discarded. Discarding an intent
421
+ * whose earlier attempt timed out does not undo a commit that the server
422
+ * may already have applied; the replica then shows the server's truth.
423
+ */
424
+ discardIntent(id: string): Promise<boolean>;
425
+ /**
426
+ * Discard the persisted Local Replica of the active identity (IndexedDB,
427
+ * Expo SQLite or any configured storage) and rehydrate it from the server.
428
+ *
429
+ * Online only: without an authenticated connection the call rejects with a
430
+ * `GonvexClientError` (`code: "disconnected"`) and changes nothing, so the
431
+ * user is never left with an empty cache that cannot be refilled. The saved
432
+ * offline session (identity and replica directive) is kept.
433
+ *
434
+ * By default the outbox is untouched: pending, failed and rejected intents
435
+ * stay durable, and the predictions of every live intent are re-applied
436
+ * immediately and recomputed as server data arrives. Active Replica
437
+ * Collections and Live Queries are re-requested without their old cursors,
438
+ * so the server sends full snapshots.
439
+ */
440
+ resetLocalReplica(options?: ResetLocalReplicaOptions): Promise<ResetLocalReplicaResult>;
441
+ /** Outbox owners with durable entries, including identities that never returned. */
442
+ listOutboxScopes(): Promise<OutboxScope[]>;
443
+ /**
444
+ * Permanently delete another identity's durable intents. The active scope
445
+ * cannot be purged this way; use {@link discardIntent} for its entries.
446
+ */
447
+ purgeOutboxScope(scope: string): Promise<number>;
448
+ /** Purge every outbox scope except the current identity's. Never runs automatically. */
449
+ purgeForeignOutboxScopes(): Promise<number>;
450
+ private refreshIntents;
451
+ private publishIntents;
298
452
  private restoreLocalSession;
299
453
  private ensureLocalCollections;
454
+ private hydrateMissingLocalTable;
300
455
  private localSnapshot;
456
+ private localReadCoverage;
457
+ private isUncoveredLocalRead;
458
+ private executeLocal;
301
459
  private inLocalLane;
460
+ private replaceLocalPredictions;
461
+ private coordinateOutbox;
462
+ private refreshPeerOutbox;
302
463
  private scheduleLocalReplay;
303
464
  private reportLocalRejection;
304
465
  private rebaseLocalEntries;
466
+ private rebaseLocalEntriesLocked;
305
467
  private runLocalReducer;
468
+ /** Preload code for a mounted control without reading data or executing an intent. */
469
+ prepareReducer(ref: FunctionReference): Promise<void>;
306
470
  /** The single normalized authoritative + optimistic application data store. */
307
471
  get localReplica(): LocalReplicaView;
308
472
  replicaSignature(ref: FunctionReference, args?: JsonValue): string;
@@ -310,11 +474,16 @@ export declare class GonvexClient {
310
474
  retainedLiveQuery<T extends ReplicaRow = ReplicaRow>(signature: string): LiveQueryResult<T>;
311
475
  /** Resolve an ordered ID batch from one normalized entity store. */
312
476
  replicaEntities<T extends ReplicaRow = ReplicaRow>(entity: string, ids: readonly string[]): Array<T | undefined>;
477
+ /** Retain only the data an active view observes; cold rows remain on disk. */
478
+ retainReplicaEntities(entity: string, ids: readonly string[]): () => void;
479
+ retainReplicaWindow(signature: string): () => void;
313
480
  /** Read rows and server-owned completeness for a persisted Replica Collection. */
314
481
  replicaCollectionState<T extends ReplicaRow = ReplicaRow>(ref: FunctionReference, args?: JsonValue): ReplicaCollectionState<T>;
315
482
  /** Run the generated Live Query plan over the bounded normalized cache. */
316
- offlineLiveQuery<T extends ReplicaRow = ReplicaRow>(ref: FunctionReference, args?: JsonValue): OfflineLiveQueryResult<T>;
317
- /** Number of reducers waiting for a definitive server result. */
483
+ offlineLiveQuery<T extends ReplicaRow = ReplicaRow>(ref: FunctionReference, args?: JsonValue, options?: {
484
+ window?: boolean;
485
+ }): OfflineLiveQueryResult<T>;
486
+ /** Number of reducers still queued for delivery (excludes failed and rejected intents). */
318
487
  outboxCount(): Promise<number>;
319
488
  connectionState(): ConnectionState;
320
489
  /** Metadata advertised by the runtime in its latest session.ready frame. */
@@ -362,7 +531,9 @@ export declare class GonvexClient {
362
531
  /** Subscribe to a bounded Replica Collection. */
363
532
  subscribeReplica<Args extends JsonValue = JsonValue, Result extends JsonValue = JsonValue>(ref: FunctionReference<Args, Result>, args: Args | undefined, onMessage: ReplicaSubscriptionHandler): () => void;
364
533
  /** Watch a bounded Replica Collection through the normalized Local Replica. */
365
- watchReplica<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args?: Args): {
534
+ watchReplica<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args?: Args, options?: {
535
+ deferStart?: boolean;
536
+ }): {
366
537
  localReplicaResult: () => T[] | undefined;
367
538
  localReplicaState: () => ReplicaCollectionSubscriptionState | undefined;
368
539
  status: () => {
@@ -376,7 +547,9 @@ export declare class GonvexClient {
376
547
  * latest query result is retained only as the transport-shaped skeleton;
377
548
  * its row window is always rebuilt from LocalReplica membership/entities.
378
549
  */
379
- watchLiveQuery<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args?: Args): {
550
+ watchLiveQuery<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args?: Args, options?: {
551
+ deferStart?: boolean;
552
+ }): {
380
553
  localLiveQueryResult: () => T | undefined;
381
554
  onUpdate(handler: WatchUpdateHandler): () => void;
382
555
  };
@@ -409,6 +582,25 @@ export declare class GonvexClient {
409
582
  private rejectOptimisticReducer;
410
583
  private ackOptimisticReducer;
411
584
  private drainOutbox;
585
+ /**
586
+ * Record a non-rejection delivery failure and arm the next attempt.
587
+ * - update_required: keep the intent pending and stop for an app update.
588
+ * - unauthenticated: keep the intent, re-authenticate, retry with backoff
589
+ * that never spends the retry budget.
590
+ * - network: connectivity loss; retried on reconnect without spending budget.
591
+ * - transient (and timeouts): exponential backoff, parked as `failed` once
592
+ * the retry budget is spent.
593
+ */
594
+ private recordDeliveryFailure;
595
+ /**
596
+ * Arm a timer for the earliest backed-off entry. Timer clocks and
597
+ * `Date.now()` can disagree by a millisecond, so a wake-up may find its
598
+ * entry not quite due; without this the queue would wait for an unrelated
599
+ * event to resume.
600
+ */
601
+ private scheduleNextOutboxAttempt;
602
+ /** Re-send auth on the open socket so a lost tenant session is restored. */
603
+ private requestReauthentication;
412
604
  private scheduleOutboxDrain;
413
605
  reducer<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args: Args, options: CallOptions & {
414
606
  offline: "queue";
@@ -487,3 +679,5 @@ export declare class GonvexClient {
487
679
  private sendNow;
488
680
  private flushPendingMessages;
489
681
  }
682
+ /** Row status from a list of intents: failed > rejected > syncing. */
683
+ export declare function entityStatusFromIntents(intents: readonly OutboxIntent[], entity: string, id: string): EntityIntentStatus | undefined;