@tanstack/offline-transactions 1.1.0 → 1.1.1

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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/OfflineExecutor.cjs +72 -80
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/OfflineExecutor.d.cts +6 -0
  5. package/dist/cjs/api/OfflineAction.cjs +4 -15
  6. package/dist/cjs/api/OfflineAction.cjs.map +1 -1
  7. package/dist/cjs/api/OfflineTransaction.cjs +54 -51
  8. package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
  9. package/dist/cjs/api/OfflineTransaction.d.cts +2 -0
  10. package/dist/cjs/executor/KeyScheduler.cjs +12 -38
  11. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
  12. package/dist/cjs/executor/TransactionExecutor.cjs +106 -161
  13. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
  14. package/dist/cjs/outbox/OutboxManager.cjs +48 -77
  15. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
  16. package/dist/esm/OfflineExecutor.d.ts +6 -0
  17. package/dist/esm/OfflineExecutor.js +72 -80
  18. package/dist/esm/OfflineExecutor.js.map +1 -1
  19. package/dist/esm/api/OfflineAction.js +4 -15
  20. package/dist/esm/api/OfflineAction.js.map +1 -1
  21. package/dist/esm/api/OfflineTransaction.d.ts +2 -0
  22. package/dist/esm/api/OfflineTransaction.js +54 -51
  23. package/dist/esm/api/OfflineTransaction.js.map +1 -1
  24. package/dist/esm/executor/KeyScheduler.js +12 -38
  25. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  26. package/dist/esm/executor/TransactionExecutor.js +106 -161
  27. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  28. package/dist/esm/outbox/OutboxManager.js +48 -77
  29. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/OfflineExecutor.ts +93 -94
  32. package/src/api/OfflineAction.ts +4 -22
  33. package/src/api/OfflineTransaction.ts +78 -66
  34. package/src/executor/KeyScheduler.ts +18 -47
  35. package/src/executor/TransactionExecutor.ts +122 -186
  36. package/src/outbox/OutboxManager.ts +54 -86
  37. package/dist/cjs/retry/NonRetriableError.d.cts +0 -1
  38. package/dist/cjs/telemetry/tracer.cjs +0 -27
  39. package/dist/cjs/telemetry/tracer.cjs.map +0 -1
  40. package/dist/cjs/telemetry/tracer.d.cts +0 -30
  41. package/dist/esm/retry/NonRetriableError.d.ts +0 -1
  42. package/dist/esm/telemetry/tracer.d.ts +0 -30
  43. package/dist/esm/telemetry/tracer.js +0 -27
  44. package/dist/esm/telemetry/tracer.js.map +0 -1
  45. package/src/retry/NonRetriableError.ts +0 -1
  46. package/src/telemetry/tracer.ts +0 -66
package/README.md CHANGED
@@ -222,7 +222,7 @@ mutation function after restart; an unmarked row may replay.
222
222
  #### Methods
223
223
 
224
224
  - `createOfflineTransaction(options)` - Create a manual offline transaction
225
- - `waitForTransactionCompletion(id)` - Wait for a specific transaction to complete
225
+ - `waitForTransactionCompletion(id)` - Wait for a specific transaction to complete. Call it before the transaction settles: a waiter for a transaction that this executor recently settled (one of its last 1,000) rejects. A waiter for an older settled id keeps waiting. A waiter for an id the executor does not know yet keeps waiting, because another tab can still queue that transaction.
226
226
  - `removeFromOutbox(id)` - Manually remove transaction from outbox
227
227
  - `clearOutbox()` - Manually remove all outbox transactions
228
228
  - `peekOutbox()` - View all pending transactions
@@ -11,8 +11,8 @@ const BroadcastChannelLeader = require("./coordination/BroadcastChannelLeader.cj
11
11
  const OnlineDetector = require("./connectivity/OnlineDetector.cjs");
12
12
  const OfflineTransaction = require("./api/OfflineTransaction.cjs");
13
13
  const OfflineAction = require("./api/OfflineAction.cjs");
14
- const tracer = require("./telemetry/tracer.cjs");
15
14
  const types = require("./types.cjs");
15
+ const SETTLED_TRANSACTIONS_REMEMBERED = 1e3;
16
16
  let OfflineExecutor$1 = class OfflineExecutor {
17
17
  constructor(config) {
18
18
  this.isLeaderState = false;
@@ -20,6 +20,7 @@ let OfflineExecutor$1 = class OfflineExecutor {
20
20
  this.unsubscribeOnline = null;
21
21
  this.unsubscribeLeadership = null;
22
22
  this.pendingTransactionPromises = /* @__PURE__ */ new Map();
23
+ this.settledTransactions = /* @__PURE__ */ new Set();
23
24
  this.restorationTransactions = /* @__PURE__ */ new Map();
24
25
  this.config = config;
25
26
  this.scheduler = new KeyScheduler.KeyScheduler();
@@ -173,60 +174,50 @@ let OfflineExecutor$1 = class OfflineExecutor {
173
174
  });
174
175
  }
175
176
  async initialize() {
176
- return tracer.withSpan(`executor.initialize`, {}, async (span) => {
177
- try {
178
- const { storage, diagnostic } = await this.createStorage();
179
- if (this.disposed) {
180
- this.initResolve();
181
- return;
182
- }
183
- ;
184
- this.storage = storage;
185
- this.storageDiagnostic = diagnostic;
186
- this.mode = diagnostic.mode;
187
- span.setAttribute(`storage.mode`, diagnostic.mode);
188
- span.setAttribute(`storage.code`, diagnostic.code);
189
- if (!storage) {
190
- if (this.config.onStorageFailure) {
191
- this.config.onStorageFailure(diagnostic);
192
- }
193
- span.setAttribute(`result`, `online-only`);
194
- this.initResolve();
195
- return;
196
- }
197
- this.outbox = new OutboxManager.OutboxManager(storage, this.config.collections);
198
- this.executor = new TransactionExecutor.TransactionExecutor(
199
- this.scheduler,
200
- this.outbox,
201
- this.config,
202
- this
203
- );
204
- this.leaderElection = this.createLeaderElection();
205
- const isLeader = await this.leaderElection.requestLeadership();
206
- if (this.disposed) {
207
- this.leaderElection.releaseLeadership();
208
- this.initResolve();
209
- return;
210
- }
211
- this.isLeaderState = isLeader;
212
- span.setAttribute(`isLeader`, isLeader);
213
- this.setupEventListeners();
214
- if (this.config.onLeadershipChange) {
215
- this.config.onLeadershipChange(isLeader);
216
- }
217
- if (isLeader) {
218
- await this.loadAndReplayTransactions();
177
+ try {
178
+ const { storage, diagnostic } = await this.createStorage();
179
+ if (this.disposed) {
180
+ this.initResolve();
181
+ return;
182
+ }
183
+ ;
184
+ this.storage = storage;
185
+ this.storageDiagnostic = diagnostic;
186
+ this.mode = diagnostic.mode;
187
+ if (!storage) {
188
+ if (this.config.onStorageFailure) {
189
+ this.config.onStorageFailure(diagnostic);
219
190
  }
220
- span.setAttribute(`result`, `offline-enabled`);
221
191
  this.initResolve();
222
- } catch (error) {
223
- console.warn(`Failed to initialize offline executor:`, error);
224
- span.setAttribute(`result`, `failed`);
225
- this.initReject(
226
- error instanceof Error ? error : new Error(String(error))
227
- );
192
+ return;
228
193
  }
229
- });
194
+ this.outbox = new OutboxManager.OutboxManager(storage, this.config.collections);
195
+ this.executor = new TransactionExecutor.TransactionExecutor(
196
+ this.scheduler,
197
+ this.outbox,
198
+ this.config,
199
+ this
200
+ );
201
+ this.leaderElection = this.createLeaderElection();
202
+ const isLeader = await this.leaderElection.requestLeadership();
203
+ if (this.disposed) {
204
+ this.leaderElection.releaseLeadership();
205
+ this.initResolve();
206
+ return;
207
+ }
208
+ this.isLeaderState = isLeader;
209
+ this.setupEventListeners();
210
+ if (this.config.onLeadershipChange) {
211
+ this.config.onLeadershipChange(isLeader);
212
+ }
213
+ if (isLeader) {
214
+ await this.loadAndReplayTransactions();
215
+ }
216
+ this.initResolve();
217
+ } catch (error) {
218
+ console.warn(`Failed to initialize offline executor:`, error);
219
+ this.initReject(error instanceof Error ? error : new Error(String(error)));
220
+ }
230
221
  }
231
222
  async loadAndReplayTransactions() {
232
223
  if (!this.executor) {
@@ -297,34 +288,29 @@ let OfflineExecutor$1 = class OfflineExecutor {
297
288
  }
298
289
  async persistTransaction(transaction) {
299
290
  await this.initPromise;
300
- return tracer.withNestedSpan(
301
- `executor.persistTransaction`,
302
- {
303
- "transaction.id": transaction.id,
304
- "transaction.mutationFnName": transaction.mutationFnName
305
- },
306
- async (span) => {
307
- if (!this.isOfflineEnabled || !this.outbox || !this.executor) {
308
- span.setAttribute(`result`, `rejected_not_admitted`);
309
- throw new types.NonRetriableError(`Offline transaction was not admitted`);
310
- }
311
- try {
312
- this.executor.assertHealthy();
313
- await this.outbox.add(transaction);
314
- this.executor.execute(transaction).catch((error) => {
315
- console.warn(`Failed to execute transactions:`, error);
316
- });
317
- span.setAttribute(`result`, `persisted`);
318
- } catch (error) {
319
- console.error(
320
- `Failed to persist offline transaction ${transaction.id}:`,
321
- error
322
- );
323
- span.setAttribute(`result`, `failed`);
324
- throw error;
325
- }
326
- }
327
- );
291
+ if (!this.isOfflineEnabled || !this.outbox || !this.executor) {
292
+ throw new types.NonRetriableError(`Offline transaction was not admitted`);
293
+ }
294
+ try {
295
+ this.executor.assertHealthy();
296
+ await this.outbox.add(transaction);
297
+ this.executor.execute(transaction).catch((error) => {
298
+ console.warn(`Failed to execute transactions:`, error);
299
+ });
300
+ } catch (error) {
301
+ console.error(
302
+ `Failed to persist offline transaction ${transaction.id}:`,
303
+ error
304
+ );
305
+ throw error;
306
+ }
307
+ }
308
+ rememberSettled(transactionId) {
309
+ this.settledTransactions.add(transactionId);
310
+ if (this.settledTransactions.size > SETTLED_TRANSACTIONS_REMEMBERED)
311
+ this.settledTransactions.delete(
312
+ this.settledTransactions.values().next().value
313
+ );
328
314
  }
329
315
  // Method for OfflineTransaction to wait for completion
330
316
  async waitForTransactionCompletion(transactionId) {
@@ -332,6 +318,10 @@ let OfflineExecutor$1 = class OfflineExecutor {
332
318
  if (existing) {
333
319
  return existing.promise;
334
320
  }
321
+ if (this.settledTransactions.has(transactionId))
322
+ throw new Error(
323
+ `Offline transaction ${transactionId} already settled; wait for it before it settles`
324
+ );
335
325
  const deferred = {};
336
326
  deferred.promise = new Promise((resolve, reject) => {
337
327
  deferred.resolve = resolve;
@@ -342,6 +332,7 @@ let OfflineExecutor$1 = class OfflineExecutor {
342
332
  }
343
333
  // Method for TransactionExecutor to signal completion
344
334
  resolveTransaction(transactionId, result) {
335
+ this.rememberSettled(transactionId);
345
336
  const deferred = this.pendingTransactionPromises.get(transactionId);
346
337
  if (deferred) {
347
338
  deferred.resolve(result);
@@ -351,6 +342,7 @@ let OfflineExecutor$1 = class OfflineExecutor {
351
342
  }
352
343
  // Method for TransactionExecutor to signal failure
353
344
  rejectTransaction(transactionId, error) {
345
+ this.rememberSettled(transactionId);
354
346
  const deferred = this.pendingTransactionPromises.get(transactionId);
355
347
  if (deferred) {
356
348
  deferred.reject(error);
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport {\n createOptimisticAction,\n createTransaction,\n safeRandomUUID,\n} from '@tanstack/db'\nimport { IndexedDBAdapter } from './storage/IndexedDBAdapter'\nimport { LocalStorageAdapter } from './storage/LocalStorageAdapter'\n\n// Core components\nimport { OutboxManager } from './outbox/OutboxManager'\nimport { KeyScheduler } from './executor/KeyScheduler'\nimport { TransactionExecutor } from './executor/TransactionExecutor'\n\n// Coordination\nimport { WebLocksLeader } from './coordination/WebLocksLeader'\nimport { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'\n\n// Connectivity\nimport { WebOnlineDetector } from './connectivity/OnlineDetector'\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'\nimport { createOfflineAction } from './api/OfflineAction'\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from './telemetry/tracer'\nimport { NonRetriableError } from './types'\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n OnlineDetector,\n StorageAdapter,\n StorageDiagnostic,\n} from './types'\nimport type { Transaction } from '@tanstack/db'\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: OnlineDetector\n private isLeaderState = false\n private disposed = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = config.onlineDetector ?? new WebOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n // Handle constructor-started rejection; waitForInit still observes it.\n void this.initPromise.catch(() => {})\n this.initialize()\n }\n\n /**\n * Probe storage availability and create appropriate adapter.\n * Returns null if no storage is available (online-only mode).\n */\n private async createStorage(): Promise<{\n storage: StorageAdapter | null\n diagnostic: StorageDiagnostic\n }> {\n // If user provided custom storage, use it without probing\n if (this.config.storage) {\n return {\n storage: this.config.storage,\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using custom storage adapter`,\n },\n }\n }\n\n // Probe IndexedDB first\n const idbProbe = await IndexedDBAdapter.probe()\n if (idbProbe.available) {\n return {\n storage: new IndexedDBAdapter(),\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using IndexedDB for offline storage`,\n },\n }\n }\n\n // IndexedDB failed, try localStorage\n const lsProbe = LocalStorageAdapter.probe()\n if (lsProbe.available) {\n return {\n storage: new LocalStorageAdapter(),\n diagnostic: {\n code: `INDEXEDDB_UNAVAILABLE`,\n mode: `offline`,\n message: `IndexedDB unavailable, using localStorage fallback`,\n error: idbProbe.error,\n },\n }\n }\n\n // Both failed - determine the diagnostic code\n const isSecurityError =\n idbProbe.error?.name === `SecurityError` ||\n lsProbe.error?.name === `SecurityError`\n const isQuotaError =\n idbProbe.error?.name === `QuotaExceededError` ||\n lsProbe.error?.name === `QuotaExceededError`\n\n let code: StorageDiagnostic[`code`]\n let message: string\n\n if (isSecurityError) {\n code = `STORAGE_BLOCKED`\n message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`\n } else if (isQuotaError) {\n code = `QUOTA_EXCEEDED`\n message = `Storage quota exceeded. Running in online-only mode.`\n } else {\n code = `UNKNOWN_ERROR`\n message = `Storage unavailable due to unknown error. Running in online-only mode.`\n }\n\n return {\n storage: null,\n diagnostic: {\n code,\n mode: `online-only`,\n message,\n error: idbProbe.error || lsProbe.error,\n },\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n // Only set up leader election listeners if we have storage\n if (this.leaderElection) {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n // A custom elector may repeat the initial result while replay is active.\n if (this.disposed || isLeader === this.isLeaderState) return\n this.isLeaderState = isLeader\n if (!isLeader) this.executor?.pause()\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions().catch((error) => {\n console.warn(`Failed to load and replay transactions:`, error)\n })\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n this.executor.resetRetryDelays()\n\n if (this.scheduler.getPendingCount() > 0) {\n const barrierPromise = this.executor.executeAll()\n\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = barrierPromise\n }\n\n barrierPromise\n .catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n .finally(() => {\n for (const collection of Object.values(this.config.collections)) {\n if (collection.deferDataRefresh === barrierPromise) {\n collection.deferDataRefresh = null\n }\n }\n })\n } else {\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n }\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n if (this.disposed) {\n this.initResolve()\n return\n }\n\n // Cast to writable to set readonly properties\n ;(this as any).storage = storage\n ;(this as any).storageDiagnostic = diagnostic\n ;(this as any).mode = diagnostic.mode\n\n span.setAttribute(`storage.mode`, diagnostic.mode)\n span.setAttribute(`storage.code`, diagnostic.code)\n\n if (!storage) {\n // Online-only mode - notify callback and skip offline setup\n if (this.config.onStorageFailure) {\n this.config.onStorageFailure(diagnostic)\n }\n span.setAttribute(`result`, `online-only`)\n this.initResolve()\n return\n }\n\n // Storage available - set up offline components\n this.outbox = new OutboxManager(storage, this.config.collections)\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this,\n )\n this.leaderElection = this.createLeaderElection()\n\n // Request leadership first\n const isLeader = await this.leaderElection.requestLeadership()\n // Disposal may have run while leadership was pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (this.disposed) {\n this.leaderElection.releaseLeadership()\n this.initResolve()\n return\n }\n this.isLeaderState = isLeader\n span.setAttribute(`isLeader`, isLeader)\n\n // Set up event listeners after leadership is established\n // This prevents the callback from being called multiple times\n this.setupEventListeners()\n\n // Notify initial leadership state\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n span.setAttribute(`result`, `offline-enabled`)\n this.initResolve()\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n span.setAttribute(`result`, `failed`)\n this.initReject(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n if (!this.executor) {\n return\n }\n\n // Startup must observe a failed read instead of reporting an empty outbox.\n await this.executor.loadPendingTransactions()\n\n // Replay completion is separate from initialization and durable admission.\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n }\n\n get isOfflineEnabled(): boolean {\n return !this.disposed && this.mode === `offline` && this.isLeaderState\n }\n\n /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions,\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || safeRandomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: safeRandomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n // Wait for initialization to complete\n await this.initPromise\n\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled || !this.outbox || !this.executor) {\n span.setAttribute(`result`, `rejected_not_admitted`)\n throw new NonRetriableError(`Offline transaction was not admitted`)\n }\n\n try {\n this.executor.assertHealthy()\n await this.outbox.add(transaction)\n // A shared queue error belongs to its execution, not every caller\n // that durably admitted a transaction. Per-ID signals settle callers.\n this.executor.execute(transaction).catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error,\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n },\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed: every Collection that tracked it recomputes and\n // releases it. The actual data will come from the sync.\n restorationTx.setState(`completed`)\n restorationTx.isPersisted.resolve(restorationTx)\n restorationTx.touchCollection()\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): OnlineDetector {\n return this.onlineDetector\n }\n\n isOnline(): boolean {\n return this.onlineDetector.isOnline()\n }\n\n dispose(): void {\n this.disposed = true\n this.executor?.pause()\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = null\n }\n\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["KeyScheduler","WebOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","OutboxManager","TransactionExecutor","createTransaction","safeRandomUUID","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan","NonRetriableError","OfflineExecutor"],"mappings":"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"}
1
+ {"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport {\n createOptimisticAction,\n createTransaction,\n safeRandomUUID,\n} from '@tanstack/db'\nimport { IndexedDBAdapter } from './storage/IndexedDBAdapter'\nimport { LocalStorageAdapter } from './storage/LocalStorageAdapter'\n\n// Core components\nimport { OutboxManager } from './outbox/OutboxManager'\nimport { KeyScheduler } from './executor/KeyScheduler'\nimport { TransactionExecutor } from './executor/TransactionExecutor'\n\n// Coordination\nimport { WebLocksLeader } from './coordination/WebLocksLeader'\nimport { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'\n\n// Connectivity\nimport { WebOnlineDetector } from './connectivity/OnlineDetector'\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'\nimport { createOfflineAction } from './api/OfflineAction'\n\n// TanStack DB primitives\n\n// Replay\nimport { NonRetriableError } from './types'\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n OnlineDetector,\n StorageAdapter,\n StorageDiagnostic,\n} from './types'\nimport type { Transaction } from '@tanstack/db'\n\n/** How many settled ids an executor remembers to reject late waiters. */\nconst SETTLED_TRANSACTIONS_REMEMBERED = 1000\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: OnlineDetector\n private isLeaderState = false\n private disposed = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n /**\n * Ids this executor settled, oldest first. A waiter for one of them has\n * nothing left to wait for. An unknown id can still arrive from another tab.\n */\n private readonly settledTransactions = new Set<string>()\n\n // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = config.onlineDetector ?? new WebOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n // Handle constructor-started rejection; waitForInit still observes it.\n void this.initPromise.catch(() => {})\n this.initialize()\n }\n\n /**\n * Probe storage availability and create appropriate adapter.\n * Returns null if no storage is available (online-only mode).\n */\n private async createStorage(): Promise<{\n storage: StorageAdapter | null\n diagnostic: StorageDiagnostic\n }> {\n // If user provided custom storage, use it without probing\n if (this.config.storage) {\n return {\n storage: this.config.storage,\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using custom storage adapter`,\n },\n }\n }\n\n // Probe IndexedDB first\n const idbProbe = await IndexedDBAdapter.probe()\n if (idbProbe.available) {\n return {\n storage: new IndexedDBAdapter(),\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using IndexedDB for offline storage`,\n },\n }\n }\n\n // IndexedDB failed, try localStorage\n const lsProbe = LocalStorageAdapter.probe()\n if (lsProbe.available) {\n return {\n storage: new LocalStorageAdapter(),\n diagnostic: {\n code: `INDEXEDDB_UNAVAILABLE`,\n mode: `offline`,\n message: `IndexedDB unavailable, using localStorage fallback`,\n error: idbProbe.error,\n },\n }\n }\n\n // Both failed - determine the diagnostic code\n const isSecurityError =\n idbProbe.error?.name === `SecurityError` ||\n lsProbe.error?.name === `SecurityError`\n const isQuotaError =\n idbProbe.error?.name === `QuotaExceededError` ||\n lsProbe.error?.name === `QuotaExceededError`\n\n let code: StorageDiagnostic[`code`]\n let message: string\n\n if (isSecurityError) {\n code = `STORAGE_BLOCKED`\n message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`\n } else if (isQuotaError) {\n code = `QUOTA_EXCEEDED`\n message = `Storage quota exceeded. Running in online-only mode.`\n } else {\n code = `UNKNOWN_ERROR`\n message = `Storage unavailable due to unknown error. Running in online-only mode.`\n }\n\n return {\n storage: null,\n diagnostic: {\n code,\n mode: `online-only`,\n message,\n error: idbProbe.error || lsProbe.error,\n },\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n // Only set up leader election listeners if we have storage\n if (this.leaderElection) {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n // A custom elector may repeat the initial result while replay is active.\n if (this.disposed || isLeader === this.isLeaderState) return\n this.isLeaderState = isLeader\n if (!isLeader) this.executor?.pause()\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions().catch((error) => {\n console.warn(`Failed to load and replay transactions:`, error)\n })\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n this.executor.resetRetryDelays()\n\n if (this.scheduler.getPendingCount() > 0) {\n const barrierPromise = this.executor.executeAll()\n\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = barrierPromise\n }\n\n barrierPromise\n .catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n .finally(() => {\n for (const collection of Object.values(this.config.collections)) {\n if (collection.deferDataRefresh === barrierPromise) {\n collection.deferDataRefresh = null\n }\n }\n })\n } else {\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n }\n }\n })\n }\n\n private async initialize(): Promise<void> {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n if (this.disposed) {\n this.initResolve()\n return\n }\n\n // Cast to writable to set readonly properties\n ;(this as any).storage = storage\n ;(this as any).storageDiagnostic = diagnostic\n ;(this as any).mode = diagnostic.mode\n\n if (!storage) {\n // Online-only mode - notify callback and skip offline setup\n if (this.config.onStorageFailure) {\n this.config.onStorageFailure(diagnostic)\n }\n this.initResolve()\n return\n }\n\n // Storage available - set up offline components\n this.outbox = new OutboxManager(storage, this.config.collections)\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this,\n )\n this.leaderElection = this.createLeaderElection()\n\n // Request leadership first\n const isLeader = await this.leaderElection.requestLeadership()\n // Disposal may have run while leadership was pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (this.disposed) {\n this.leaderElection.releaseLeadership()\n this.initResolve()\n return\n }\n this.isLeaderState = isLeader\n\n // Set up event listeners after leadership is established\n // This prevents the callback from being called multiple times\n this.setupEventListeners()\n\n // Notify initial leadership state\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n this.initResolve()\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n this.initReject(error instanceof Error ? error : new Error(String(error)))\n }\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n if (!this.executor) {\n return\n }\n\n // Startup must observe a failed read instead of reporting an empty outbox.\n await this.executor.loadPendingTransactions()\n\n // Replay completion is separate from initialization and durable admission.\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n }\n\n get isOfflineEnabled(): boolean {\n return !this.disposed && this.mode === `offline` && this.isLeaderState\n }\n\n /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions,\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || safeRandomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: safeRandomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n // Wait for initialization to complete\n await this.initPromise\n\n if (!this.isOfflineEnabled || !this.outbox || !this.executor) {\n throw new NonRetriableError(`Offline transaction was not admitted`)\n }\n\n try {\n this.executor.assertHealthy()\n await this.outbox.add(transaction)\n // A shared queue error belongs to its execution, not every caller\n // that durably admitted a transaction. Per-ID signals settle callers.\n this.executor.execute(transaction).catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error,\n )\n throw error\n }\n }\n\n private rememberSettled(transactionId: string): void {\n this.settledTransactions.add(transactionId)\n if (this.settledTransactions.size > SETTLED_TRANSACTIONS_REMEMBERED)\n this.settledTransactions.delete(\n this.settledTransactions.values().next().value!,\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n if (this.settledTransactions.has(transactionId))\n throw new Error(\n `Offline transaction ${transactionId} already settled; wait for it before it settles`,\n )\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n this.rememberSettled(transactionId)\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n this.rememberSettled(transactionId)\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed: every Collection that tracked it recomputes and\n // releases it. The actual data will come from the sync.\n restorationTx.setState(`completed`)\n restorationTx.isPersisted.resolve(restorationTx)\n restorationTx.touchCollection()\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): OnlineDetector {\n return this.onlineDetector\n }\n\n isOnline(): boolean {\n return this.onlineDetector.isOnline()\n }\n\n dispose(): void {\n this.disposed = true\n this.executor?.pause()\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = null\n }\n\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["KeyScheduler","WebOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","OutboxManager","TransactionExecutor","createTransaction","safeRandomUUID","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","NonRetriableError","OfflineExecutor"],"mappings":"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"}
@@ -19,6 +19,11 @@ export declare class OfflineExecutor {
19
19
  private initResolve;
20
20
  private initReject;
21
21
  private pendingTransactionPromises;
22
+ /**
23
+ * Ids this executor settled, oldest first. A waiter for one of them has
24
+ * nothing left to wait for. An unknown id can still arrive from another tab.
25
+ */
26
+ private readonly settledTransactions;
22
27
  private restorationTransactions;
23
28
  constructor(config: OfflineConfig);
24
29
  /**
@@ -39,6 +44,7 @@ export declare class OfflineExecutor {
39
44
  createOfflineTransaction(options: CreateOfflineTransactionOptions): Transaction | OfflineTransactionAPI;
40
45
  createOfflineAction<T>(options: CreateOfflineActionOptions<T>): (variables: T) => Transaction<Record<string, unknown>>;
41
46
  private persistTransaction;
47
+ private rememberSettled;
42
48
  waitForTransactionCompletion(transactionId: string): Promise<any>;
43
49
  resolveTransaction(transactionId: string, result: any): void;
44
50
  rejectTransaction(transactionId: string, error: Error): void;
@@ -7,35 +7,24 @@ function isPromiseLike(value) {
7
7
  }
8
8
  function createOfflineAction(options, mutationFn, persistTransaction, executor) {
9
9
  const { mutationFnName, onMutate } = options;
10
- console.log(`createOfflineAction 2`, options);
11
10
  return (variables) => {
12
11
  const offlineTransaction = new OfflineTransaction.OfflineTransaction(
13
12
  {
14
13
  mutationFnName,
15
- autoCommit: false
14
+ // The transaction commits after onMutate and reports a failure
15
+ // through `when('settled')`.
16
+ autoCommit: true
16
17
  },
17
18
  mutationFn,
18
19
  persistTransaction,
19
20
  executor
20
21
  );
21
- const transaction = offlineTransaction.mutate(() => {
22
- console.log(`mutate`);
22
+ return offlineTransaction.mutate(() => {
23
23
  const maybePromise = onMutate(variables);
24
24
  if (isPromiseLike(maybePromise)) {
25
25
  throw new db.OnMutateMustBeSynchronousError();
26
26
  }
27
27
  });
28
- const commitPromise = (async () => {
29
- try {
30
- await transaction.commit();
31
- console.log(`offlineAction committed - success`);
32
- } catch {
33
- console.log(`offlineAction commit failed - error`);
34
- }
35
- })();
36
- commitPromise.catch(() => {
37
- });
38
- return transaction;
39
28
  };
40
29
  }
41
30
  exports.createOfflineAction = createOfflineAction;
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineAction.cjs","sources":["../../../src/api/OfflineAction.ts"],"sourcesContent":["import { OnMutateMustBeSynchronousError } from '@tanstack/db'\nimport { OfflineTransaction } from './OfflineTransaction'\nimport type { Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineActionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return (\n !!value &&\n (typeof value === `object` || typeof value === `function`) &&\n typeof (value as { then?: unknown }).then === `function`\n )\n}\n\nexport function createOfflineAction<T>(\n options: CreateOfflineActionOptions<T>,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n): (variables: T) => Transaction {\n const { mutationFnName, onMutate } = options\n console.log(`createOfflineAction 2`, options)\n\n return (variables: T): Transaction => {\n const offlineTransaction = new OfflineTransaction(\n {\n mutationFnName,\n autoCommit: false,\n },\n mutationFn,\n persistTransaction,\n executor,\n )\n\n const transaction = offlineTransaction.mutate(() => {\n console.log(`mutate`)\n const maybePromise = onMutate(variables) as unknown\n\n if (isPromiseLike(maybePromise)) {\n throw new OnMutateMustBeSynchronousError()\n }\n })\n\n // Immediately commit\n const commitPromise = (async () => {\n try {\n await transaction.commit()\n console.log(`offlineAction committed - success`)\n } catch {\n console.log(`offlineAction commit failed - error`)\n }\n })()\n\n // Don't await - this is fire-and-forget for optimistic actions\n // But catch to prevent unhandled rejection\n commitPromise.catch(() => {\n // Already handled in try/catch above\n })\n\n return transaction\n }\n}\n"],"names":["OfflineTransaction","OnMutateMustBeSynchronousError"],"mappings":";;;;AASA,SAAS,cAAc,OAA+C;AACpE,SACE,CAAC,CAAC,UACD,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;AAEO,SAAS,oBACd,SACA,YACA,oBACA,UAC+B;AAC/B,QAAM,EAAE,gBAAgB,SAAA,IAAa;AACrC,UAAQ,IAAI,yBAAyB,OAAO;AAE5C,SAAO,CAAC,cAA8B;AACpC,UAAM,qBAAqB,IAAIA,mBAAAA;AAAAA,MAC7B;AAAA,QACE;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAEd;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAGF,UAAM,cAAc,mBAAmB,OAAO,MAAM;AAClD,cAAQ,IAAI,QAAQ;AACpB,YAAM,eAAe,SAAS,SAAS;AAEvC,UAAI,cAAc,YAAY,GAAG;AAC/B,cAAM,IAAIC,GAAAA,+BAAA;AAAA,MACZ;AAAA,IACF,CAAC;AAGD,UAAM,iBAAiB,YAAY;AACjC,UAAI;AACF,cAAM,YAAY,OAAA;AAClB,gBAAQ,IAAI,mCAAmC;AAAA,MACjD,QAAQ;AACN,gBAAQ,IAAI,qCAAqC;AAAA,MACnD;AAAA,IACF,GAAA;AAIA,kBAAc,MAAM,MAAM;AAAA,IAE1B,CAAC;AAED,WAAO;AAAA,EACT;AACF;;"}
1
+ {"version":3,"file":"OfflineAction.cjs","sources":["../../../src/api/OfflineAction.ts"],"sourcesContent":["import { OnMutateMustBeSynchronousError } from '@tanstack/db'\nimport { OfflineTransaction } from './OfflineTransaction'\nimport type { Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineActionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return (\n !!value &&\n (typeof value === `object` || typeof value === `function`) &&\n typeof (value as { then?: unknown }).then === `function`\n )\n}\n\nexport function createOfflineAction<T>(\n options: CreateOfflineActionOptions<T>,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n): (variables: T) => Transaction {\n const { mutationFnName, onMutate } = options\n\n return (variables: T): Transaction => {\n const offlineTransaction = new OfflineTransaction(\n {\n mutationFnName,\n // The transaction commits after onMutate and reports a failure\n // through `when('settled')`.\n autoCommit: true,\n },\n mutationFn,\n persistTransaction,\n executor,\n )\n\n return offlineTransaction.mutate(() => {\n const maybePromise = onMutate(variables) as unknown\n\n if (isPromiseLike(maybePromise)) {\n throw new OnMutateMustBeSynchronousError()\n }\n })\n }\n}\n"],"names":["OfflineTransaction","OnMutateMustBeSynchronousError"],"mappings":";;;;AASA,SAAS,cAAc,OAA+C;AACpE,SACE,CAAC,CAAC,UACD,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;AAEO,SAAS,oBACd,SACA,YACA,oBACA,UAC+B;AAC/B,QAAM,EAAE,gBAAgB,SAAA,IAAa;AAErC,SAAO,CAAC,cAA8B;AACpC,UAAM,qBAAqB,IAAIA,mBAAAA;AAAAA,MAC7B;AAAA,QACE;AAAA;AAAA;AAAA,QAGA,YAAY;AAAA,MAAA;AAAA,MAEd;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAGF,WAAO,mBAAmB,OAAO,MAAM;AACrC,YAAM,eAAe,SAAS,SAAS;AAEvC,UAAI,cAAc,YAAY,GAAG;AAC/B,cAAM,IAAIC,GAAAA,+BAAA;AAAA,MACZ;AAAA,IACF,CAAC;AAAA,EACH;AACF;;"}
@@ -1,79 +1,82 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const db = require("@tanstack/db");
4
- const types = require("../types.cjs");
5
4
  class OfflineTransaction {
6
5
  // Will be typed properly - reference to OfflineExecutor
7
6
  constructor(options, mutationFn, persistTransaction, executor) {
8
7
  this.transaction = null;
8
+ this.persisting = false;
9
9
  this.offlineId = db.safeRandomUUID();
10
10
  this.mutationFnName = options.mutationFnName;
11
- this.autoCommit = options.autoCommit ?? true;
11
+ this.autoCommit = options.autoCommit;
12
12
  this.idempotencyKey = options.idempotencyKey ?? db.safeRandomUUID();
13
13
  this.metadata = options.metadata ?? {};
14
14
  this.persistTransaction = persistTransaction;
15
15
  this.executor = executor;
16
16
  }
17
17
  mutate(callback) {
18
- this.transaction ??= db.createTransaction({
19
- id: this.offlineId,
20
- autoCommit: false,
21
- mutationFn: async () => {
22
- const offlineTransaction = {
23
- id: this.offlineId,
24
- mutationFnName: this.mutationFnName,
25
- mutations: this.transaction.mutations,
26
- keys: this.extractKeys(this.transaction.mutations),
27
- idempotencyKey: this.idempotencyKey,
28
- createdAt: /* @__PURE__ */ new Date(),
29
- retryCount: 0,
30
- nextAttemptAt: Date.now(),
31
- metadata: this.metadata,
32
- spanContext: void 0,
33
- version: 1
34
- };
35
- const completionPromise = this.executor.waitForTransactionCompletion(
36
- this.offlineId
37
- );
38
- try {
39
- await Promise.race([
40
- this.persistTransaction(offlineTransaction),
41
- completionPromise
42
- ]);
43
- await completionPromise;
44
- } catch (error) {
45
- const normalizedError = error instanceof Error ? error : new Error(String(error));
46
- this.executor.rejectTransaction(this.offlineId, normalizedError);
47
- throw error;
18
+ if (!this.transaction) {
19
+ this.transaction = db.createTransaction({
20
+ id: this.offlineId,
21
+ autoCommit: this.autoCommit,
22
+ mutationFn: async () => {
23
+ this.persisting = true;
24
+ const offlineTransaction = {
25
+ id: this.offlineId,
26
+ mutationFnName: this.mutationFnName,
27
+ mutations: this.transaction.mutations,
28
+ keys: this.extractKeys(this.transaction.mutations),
29
+ idempotencyKey: this.idempotencyKey,
30
+ createdAt: /* @__PURE__ */ new Date(),
31
+ retryCount: 0,
32
+ nextAttemptAt: Date.now(),
33
+ metadata: this.metadata,
34
+ spanContext: void 0,
35
+ version: 1
36
+ };
37
+ const completionPromise = this.executor.waitForTransactionCompletion(
38
+ this.offlineId
39
+ );
40
+ try {
41
+ await Promise.race([
42
+ this.persistTransaction(offlineTransaction),
43
+ completionPromise
44
+ ]);
45
+ await completionPromise;
46
+ } catch (error) {
47
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
48
+ this.executor.rejectTransaction(this.offlineId, normalizedError);
49
+ throw error;
50
+ }
51
+ return;
52
+ },
53
+ metadata: this.metadata
54
+ });
55
+ void this.transaction.when(`settled`).then(
56
+ () => {
57
+ if (!this.persisting)
58
+ this.executor.resolveTransaction(this.offlineId, void 0);
59
+ },
60
+ (error) => {
61
+ if (!this.persisting)
62
+ this.executor.rejectTransaction(
63
+ this.offlineId,
64
+ error instanceof Error ? error : new Error(`Offline transaction was rolled back`)
65
+ );
48
66
  }
49
- return;
50
- },
51
- metadata: this.metadata
52
- });
67
+ );
68
+ }
53
69
  this.transaction.mutate(() => {
54
70
  callback();
55
71
  });
56
- if (this.autoCommit) {
57
- this.commit().catch((error) => {
58
- console.error(`Auto-commit failed:`, error);
59
- throw error;
60
- });
61
- }
62
72
  return this.transaction;
63
73
  }
64
74
  async commit() {
65
75
  if (!this.transaction) {
66
76
  throw new Error(`No mutations to commit. Call mutate() first.`);
67
77
  }
68
- try {
69
- await this.transaction.commit();
70
- return this.transaction;
71
- } catch (error) {
72
- if (error instanceof types.NonRetriableError) {
73
- this.transaction.rollback();
74
- }
75
- throw error;
76
- }
78
+ await this.transaction.commit();
79
+ return this.transaction;
77
80
  }
78
81
  rollback() {
79
82
  if (this.transaction) {
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineTransaction.cjs","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { createTransaction, safeRandomUUID } from '@tanstack/db'\nimport { NonRetriableError } from '../types'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n ) {\n this.offlineId = safeRandomUUID()\n this.mutationFnName = options.mutationFnName\n this.autoCommit = options.autoCommit ?? true\n this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n // Repeated calls add to the same transaction: one offline transaction is\n // one transaction, and its id is unique among live transactions.\n this.transaction ??= createTransaction({\n id: this.offlineId,\n autoCommit: false,\n mutationFn: async () => {\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId,\n )\n\n try {\n // Persistence also drives the shared queue. This transaction can finish\n // before that queue drains; observe both promises from the outset.\n await Promise.race([\n this.persistTransaction(offlineTransaction),\n completionPromise,\n ])\n // A queue pause (offline or retry) is not transaction completion.\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n\n this.transaction.mutate(() => {\n callback()\n })\n\n if (this.autoCommit) {\n // Auto-commit for direct OfflineTransaction usage\n this.commit().catch((error) => {\n console.error(`Auto-commit failed:`, error)\n throw error\n })\n }\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n try {\n // Commit the TanStack DB transaction\n // This will trigger the mutationFn which handles persistence and waiting\n await this.transaction.commit()\n return this.transaction\n } catch (error) {\n // Only rollback for NonRetriableError - other errors should allow retry\n if (error instanceof NonRetriableError) {\n this.transaction.rollback()\n }\n throw error\n }\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":["safeRandomUUID","createTransaction","NonRetriableError"],"mappings":";;;;AASO,MAAM,mBAAmB;AAAA;AAAA,EAU9B,YACE,SACA,YACA,oBACA,UACA;AATF,SAAQ,cAAkC;AAUxC,SAAK,YAAYA,kBAAA;AACjB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,iBAAiB,QAAQ,kBAAkBA,GAAAA,eAAA;AAChD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AAGxC,SAAK,gBAAgBC,qBAAkB;AAAA,MACrC,IAAI,KAAK;AAAA,MACT,YAAY;AAAA,MACZ,YAAY,YAAY;AAGtB,cAAM,qBAA6C;AAAA,UACjD,IAAI,KAAK;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,WAAW,KAAK,YAAa;AAAA,UAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,UAClD,gBAAgB,KAAK;AAAA,UACrB,+BAAe,KAAA;AAAA,UACf,YAAY;AAAA,UACZ,eAAe,KAAK,IAAA;AAAA,UACpB,UAAU,KAAK;AAAA,UACf,aAAa;AAAA,UACb,SAAS;AAAA,QAAA;AAGX,cAAM,oBAAoB,KAAK,SAAS;AAAA,UACtC,KAAK;AAAA,QAAA;AAGP,YAAI;AAGF,gBAAM,QAAQ,KAAK;AAAA,YACjB,KAAK,mBAAmB,kBAAkB;AAAA,YAC1C;AAAA,UAAA,CACD;AAED,gBAAM;AAAA,QACR,SAAS,OAAO;AACd,gBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,eAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,gBAAM;AAAA,QACR;AAEA;AAAA,MACF;AAAA,MACA,UAAU,KAAK;AAAA,IAAA,CAChB;AAED,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,QAAI,KAAK,YAAY;AAEnB,WAAK,OAAA,EAAS,MAAM,CAAC,UAAU;AAC7B,gBAAQ,MAAM,uBAAuB,KAAK;AAC1C,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAEA,QAAI;AAGF,YAAM,KAAK,YAAY,OAAA;AACvB,aAAO,KAAK;AAAA,IACd,SAAS,OAAO;AAEd,UAAI,iBAAiBC,MAAAA,mBAAmB;AACtC,aAAK,YAAY,SAAA;AAAA,MACnB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;;"}
1
+ {"version":3,"file":"OfflineTransaction.cjs","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { createTransaction, safeRandomUUID } from '@tanstack/db'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean | undefined\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n /** The mutation function ran, so the executor settles the waiter. */\n private persisting = false\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n ) {\n this.offlineId = safeRandomUUID()\n this.mutationFnName = options.mutationFnName\n // The TanStack DB transaction applies the default.\n this.autoCommit = options.autoCommit\n this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n // One offline transaction is one transaction, and its id is unique among\n // live transactions. Repeated calls add to it while it is pending. With\n // autoCommit, it commits after this callback and reports a failure\n // through `when('settled')`, so a later call throws: it is no longer pending.\n if (!this.transaction) {\n this.transaction = createTransaction({\n id: this.offlineId,\n autoCommit: this.autoCommit,\n mutationFn: async () => {\n this.persisting = true\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId,\n )\n\n try {\n // Persistence also drives the shared queue. This transaction can finish\n // before that queue drains; observe both promises from the outset.\n await Promise.race([\n this.persistTransaction(offlineTransaction),\n completionPromise,\n ])\n // A queue pause (offline or retry) is not transaction completion.\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n // A transaction without mutations completes, and one rolled back before\n // it commits fails, without its mutation function, so the executor never\n // settles its waiter. Settle that waiter here with the same outcome. Once\n // the mutation function runs, only the executor settles it.\n void this.transaction.when(`settled`).then(\n () => {\n if (!this.persisting)\n this.executor.resolveTransaction(this.offlineId, undefined)\n },\n (error: unknown) => {\n if (!this.persisting)\n this.executor.rejectTransaction(\n this.offlineId,\n error instanceof Error\n ? error\n : new Error(`Offline transaction was rolled back`),\n )\n },\n )\n }\n\n this.transaction.mutate(() => {\n callback()\n })\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n // The mutationFn persists the transaction and waits for the executor. A\n // commit that rejects has already rolled the transaction back. A sibling's\n // failure can also roll it back while it waits, and then commit() resolves,\n // so `when('settled')` is the receipt.\n await this.transaction.commit()\n return this.transaction\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":["safeRandomUUID","createTransaction"],"mappings":";;;AAQO,MAAM,mBAAmB;AAAA;AAAA,EAY9B,YACE,SACA,YACA,oBACA,UACA;AAXF,SAAQ,cAAkC;AAE1C,SAAQ,aAAa;AAUnB,SAAK,YAAYA,kBAAA;AACjB,SAAK,iBAAiB,QAAQ;AAE9B,SAAK,aAAa,QAAQ;AAC1B,SAAK,iBAAiB,QAAQ,kBAAkBA,GAAAA,eAAA;AAChD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AAKxC,QAAI,CAAC,KAAK,aAAa;AACrB,WAAK,cAAcC,qBAAkB;AAAA,QACnC,IAAI,KAAK;AAAA,QACT,YAAY,KAAK;AAAA,QACjB,YAAY,YAAY;AACtB,eAAK,aAAa;AAGlB,gBAAM,qBAA6C;AAAA,YACjD,IAAI,KAAK;AAAA,YACT,gBAAgB,KAAK;AAAA,YACrB,WAAW,KAAK,YAAa;AAAA,YAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,YAClD,gBAAgB,KAAK;AAAA,YACrB,+BAAe,KAAA;AAAA,YACf,YAAY;AAAA,YACZ,eAAe,KAAK,IAAA;AAAA,YACpB,UAAU,KAAK;AAAA,YACf,aAAa;AAAA,YACb,SAAS;AAAA,UAAA;AAGX,gBAAM,oBAAoB,KAAK,SAAS;AAAA,YACtC,KAAK;AAAA,UAAA;AAGP,cAAI;AAGF,kBAAM,QAAQ,KAAK;AAAA,cACjB,KAAK,mBAAmB,kBAAkB;AAAA,cAC1C;AAAA,YAAA,CACD;AAED,kBAAM;AAAA,UACR,SAAS,OAAO;AACd,kBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,iBAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,kBAAM;AAAA,UACR;AAEA;AAAA,QACF;AAAA,QACA,UAAU,KAAK;AAAA,MAAA,CAChB;AAKD,WAAK,KAAK,YAAY,KAAK,SAAS,EAAE;AAAA,QACpC,MAAM;AACJ,cAAI,CAAC,KAAK;AACR,iBAAK,SAAS,mBAAmB,KAAK,WAAW,MAAS;AAAA,QAC9D;AAAA,QACA,CAAC,UAAmB;AAClB,cAAI,CAAC,KAAK;AACR,iBAAK,SAAS;AAAA,cACZ,KAAK;AAAA,cACL,iBAAiB,QACb,QACA,IAAI,MAAM,qCAAqC;AAAA,YAAA;AAAA,QAEzD;AAAA,MAAA;AAAA,IAEJ;AAEA,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAMA,UAAM,KAAK,YAAY,OAAA;AACvB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;;"}
@@ -7,6 +7,8 @@ export declare class OfflineTransaction {
7
7
  private idempotencyKey;
8
8
  private metadata;
9
9
  private transaction;
10
+ /** The mutation function ran, so the executor settles the waiter. */
11
+ private persisting;
10
12
  private persistTransaction;
11
13
  private executor;
12
14
  constructor(options: CreateOfflineTransactionOptions, mutationFn: OfflineMutationFn, persistTransaction: (tx: OfflineTransactionType) => Promise<void>, executor: any);