@gonvex/client 0.5.2-staging.14 → 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/README.md CHANGED
@@ -259,6 +259,55 @@ Reducer also declares optimistic UI metadata. Actions are never queued.
259
259
  Deterministic server errors are never queued and always roll an optimistic
260
260
  entity overlay back when one exists.
261
261
 
262
+ ### Queued intent lifecycle
263
+
264
+ Runtimes from 0.5.2-staging.15 classify every `reducer.error` as `rejected`,
265
+ `transient`, `update_required` or `unauthenticated` (also exposed as
266
+ `GonvexClientError.errorClass` / `retryable`). The outbox acts on it:
267
+
268
+ | Class | Outbox behaviour |
269
+ | --- | --- |
270
+ | `transient` (and timeouts) | Retry with exponential backoff. After `outbox.retry.maxAttempts` (default 10, backoff capped at `maxBackoffMs`, default 60s) the intent is parked as `failed`: kept durably with its prediction, and no longer blocking later intents. |
271
+ | `unauthenticated` | Keep the intent, re-authenticate, retry. Does not spend the retry budget. |
272
+ | `update_required` | Keep the intent pending and call `onUpdateRequired`. |
273
+ | `rejected` | Roll the prediction back, rebase later local intents, fire `onReducerRejection`, and keep the intent as a `rejected` record until the app discards or retries it. |
274
+
275
+ Older runtimes send no class: their errors are treated as rejections, except
276
+ the legacy stale-artifact and "authenticate with an active tenant" messages.
277
+
278
+ ```ts
279
+ const intents = await client.listIntents(); // or subscribeIntents()/intentsSnapshot()
280
+ client.entityIntentStatus("tasks", taskId); // "syncing" | "failed" | "rejected" | undefined
281
+ await client.retryIntent(intent.id); // same idempotency key, fresh budget
282
+ await client.discardIntent(intent.id); // pending/failed/rejected only
283
+ await client.listOutboxScopes(); // includes identities that never returned
284
+ await client.purgeForeignOutboxScopes(); // never automatic
285
+ ```
286
+
287
+ `@gonvex/react` exposes the same list through `useOutboxIntents()` and a
288
+ per-row `useEntityIntentStatus(entity, id)`.
289
+
290
+ ### Resetting the local cache
291
+
292
+ `client.resetLocalReplica({ keepOutbox = true })` implements a "Clear cache"
293
+ setting without the app touching replica storage. It deletes the active
294
+ identity's persisted replica rows (IndexedDB, Expo SQLite, or the configured
295
+ storage), keeps the saved offline session, closes and re-opens every active
296
+ Replica Collection and Live Query without cursors, and re-applies the
297
+ predictions of all live intents so they reappear on top of the fresh
298
+ snapshots. Pending, failed and rejected intents are kept unless
299
+ `keepOutbox: false` is passed explicitly (inflight intents are never dropped).
300
+
301
+ It is **online only**: offline (or before the socket is authenticated) it
302
+ rejects with `GonvexClientError` `code: "disconnected"` and changes nothing,
303
+ so the user is never left with an empty cache that cannot be refilled. React
304
+ apps can use `useResetLocalReplica()` for `{ reset, isResetting, error }`.
305
+
306
+ A parked intent lets later intents proceed; the server validates each of them
307
+ on its own, so an intent that depended on the parked one is rejected rather
308
+ than applied out of order. Retrying a parked intent after later intents
309
+ committed re-applies it on top of them.
310
+
262
311
  Live Queries persist their last verified window in the Local Replica and
263
312
  resubscribe after reconnect. Call `client.retryLiveQuery(ref, args)` to force a
264
313
  re-request after a server error. `useQueryResult` is for one-shot Queries.
@@ -32,7 +32,8 @@ export function migrateClientData(snapshots, entries, chain) {
32
32
  return { ...entry, path: intent.path, args: intent.args,
33
33
  receiptPath: originalPath, patches: [],
34
34
  // Committed responses awaiting a watermark must be reconciled by their original receipt too.
35
- state: "pending", nextAttemptAt: 0 };
35
+ // Parked and rejected records wait for the user; an upgrade must not resend them.
36
+ state: entry.state === "failed" || entry.state === "rejected" ? entry.state : "pending", nextAttemptAt: 0 };
36
37
  });
37
38
  }
38
39
  return { snapshots: nextSnapshots, entries: nextEntries };
@@ -1 +1 @@
1
- {"version":3,"file":"client-upgrades.js","sourceRoot":"","sources":["../src/client-upgrades.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client-upgrades.js","sourceRoot":"","sources":["../src/client-upgrades.ts"],"names":[],"mappings":"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"}
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";
@@ -95,12 +95,86 @@ export declare class GonvexClientError extends Error {
95
95
  readonly code: GonvexClientErrorCode;
96
96
  readonly path?: string;
97
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;
98
106
  constructor(message: string, options: {
99
107
  code: GonvexClientErrorCode;
100
108
  path?: string;
101
109
  operation?: "query" | "reducer" | "action";
110
+ errorClass?: ReducerErrorClass;
111
+ retryable?: boolean;
102
112
  });
103
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;
104
178
  export type ConnectionState = {
105
179
  isWebSocketConnected: boolean;
106
180
  hasEverConnected: boolean;
@@ -191,6 +265,7 @@ export type GonvexClientOptions = GonvexClientAuth & {
191
265
  databaseName?: string;
192
266
  enabled?: boolean;
193
267
  store?: OutboxStore;
268
+ retry?: OutboxRetryOptions;
194
269
  };
195
270
  /** Transactional normalized store used by Replica Collections and Live Queries. */
196
271
  localReplica?: {
@@ -231,6 +306,12 @@ export declare class GonvexClient {
231
306
  private readonly localCollectionKeys;
232
307
  private readonly localExecutionTables;
233
308
  private readonly reducerRejectionHandlers;
309
+ private readonly outboxRetry;
310
+ private unauthenticatedRetries;
311
+ private intentsSnapshotValue;
312
+ private readonly intentListeners;
313
+ private intentsRefreshRunning;
314
+ private intentsRefreshDirty;
234
315
  private socket;
235
316
  private readonly handlers;
236
317
  private readonly querySubscriptions;
@@ -283,6 +364,8 @@ export declare class GonvexClient {
283
364
  private peerRefreshDirty;
284
365
  private readonly unsubscribeBrowserOnline;
285
366
  private drainingOutbox;
367
+ /** A wake-up (timer, enqueue, reconnect) that arrived while a drain was running. */
368
+ private outboxDrainRequested;
286
369
  private outboxDrainTimer;
287
370
  private readonly sessionScopeHandlers;
288
371
  private readonly errorReporter;
@@ -304,12 +387,68 @@ export declare class GonvexClient {
304
387
  private updateRequired;
305
388
  private lastOnlineAtMs;
306
389
  constructor(url: string, options?: GonvexClientOptions);
307
- /** Authoritative sync failures arrive after an offline call returned locally. */
308
- onReducerRejection(listener: (event: {
309
- reducerId: string;
310
- path: string;
311
- error: string;
312
- }) => 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;
313
452
  private restoreLocalSession;
314
453
  private ensureLocalCollections;
315
454
  private hydrateMissingLocalTable;
@@ -344,7 +483,7 @@ export declare class GonvexClient {
344
483
  offlineLiveQuery<T extends ReplicaRow = ReplicaRow>(ref: FunctionReference, args?: JsonValue, options?: {
345
484
  window?: boolean;
346
485
  }): OfflineLiveQueryResult<T>;
347
- /** Number of reducers waiting for a definitive server result. */
486
+ /** Number of reducers still queued for delivery (excludes failed and rejected intents). */
348
487
  outboxCount(): Promise<number>;
349
488
  connectionState(): ConnectionState;
350
489
  /** Metadata advertised by the runtime in its latest session.ready frame. */
@@ -443,6 +582,25 @@ export declare class GonvexClient {
443
582
  private rejectOptimisticReducer;
444
583
  private ackOptimisticReducer;
445
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;
446
604
  private scheduleOutboxDrain;
447
605
  reducer<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args: Args, options: CallOptions & {
448
606
  offline: "queue";
@@ -521,3 +679,5 @@ export declare class GonvexClient {
521
679
  private sendNow;
522
680
  private flushPendingMessages;
523
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;