@tanstack/offline-transactions 1.0.11 → 1.0.13

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.
@@ -18,6 +18,7 @@ let OfflineExecutor$1 = class OfflineExecutor {
18
18
  this.unsubscribeOnline = null;
19
19
  this.unsubscribeLeadership = null;
20
20
  this.pendingTransactionPromises = /* @__PURE__ */ new Map();
21
+ this.restorationTransactions = /* @__PURE__ */ new Map();
21
22
  this.config = config;
22
23
  this.scheduler = new KeyScheduler.KeyScheduler();
23
24
  this.onlineDetector = config.onlineDetector ?? new OnlineDetector.WebOnlineDetector();
@@ -196,7 +197,9 @@ let OfflineExecutor$1 = class OfflineExecutor {
196
197
  }
197
198
  try {
198
199
  await this.executor.loadPendingTransactions();
199
- await this.executor.executeAll();
200
+ this.executor.executeAll().catch((error) => {
201
+ console.warn(`Failed to execute transactions:`, error);
202
+ });
200
203
  } catch (error) {
201
204
  console.warn(`Failed to load and replay transactions:`, error);
202
205
  }
@@ -204,6 +207,13 @@ let OfflineExecutor$1 = class OfflineExecutor {
204
207
  get isOfflineEnabled() {
205
208
  return this.mode === `offline` && this.isLeaderState;
206
209
  }
210
+ /**
211
+ * Wait for the executor to fully initialize.
212
+ * This ensures that pending transactions are loaded and optimistic state is restored.
213
+ */
214
+ async waitForInit() {
215
+ return this.initPromise;
216
+ }
207
217
  createOfflineTransaction(options) {
208
218
  const mutationFn = this.config.mutationFns[options.mutationFnName];
209
219
  if (!mutationFn) {
@@ -302,6 +312,7 @@ let OfflineExecutor$1 = class OfflineExecutor {
302
312
  deferred.resolve(result);
303
313
  this.pendingTransactionPromises.delete(transactionId);
304
314
  }
315
+ this.cleanupRestorationTransaction(transactionId);
305
316
  }
306
317
  // Method for TransactionExecutor to signal failure
307
318
  rejectTransaction(transactionId, error) {
@@ -310,6 +321,39 @@ let OfflineExecutor$1 = class OfflineExecutor {
310
321
  deferred.reject(error);
311
322
  this.pendingTransactionPromises.delete(transactionId);
312
323
  }
324
+ this.cleanupRestorationTransaction(transactionId, true);
325
+ }
326
+ // Method for TransactionExecutor to register restoration transactions
327
+ registerRestorationTransaction(offlineTransactionId, restorationTransaction) {
328
+ this.restorationTransactions.set(
329
+ offlineTransactionId,
330
+ restorationTransaction
331
+ );
332
+ }
333
+ cleanupRestorationTransaction(transactionId, shouldRollback = false) {
334
+ const restorationTx = this.restorationTransactions.get(transactionId);
335
+ if (!restorationTx) {
336
+ return;
337
+ }
338
+ this.restorationTransactions.delete(transactionId);
339
+ if (shouldRollback) {
340
+ restorationTx.rollback();
341
+ return;
342
+ }
343
+ restorationTx.setState(`completed`);
344
+ const touchedCollections = /* @__PURE__ */ new Set();
345
+ for (const mutation of restorationTx.mutations) {
346
+ if (!mutation.collection) {
347
+ continue;
348
+ }
349
+ const collectionId = mutation.collection.id;
350
+ if (touchedCollections.has(collectionId)) {
351
+ continue;
352
+ }
353
+ touchedCollections.add(collectionId);
354
+ mutation.collection._state.transactions.delete(restorationTx.id);
355
+ mutation.collection._state.recomputeOptimisticState(false);
356
+ }
313
357
  }
314
358
  async removeFromOutbox(id) {
315
359
  if (!this.outbox) {
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } 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 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 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 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\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 this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\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 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\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 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 try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.mode === `offline` && this.isLeaderState\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 || crypto.randomUUID(),\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: crypto.randomUUID(),\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`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\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\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\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 notifyOnline(): void {\n this.onlineDetector.notifyOnline()\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 dispose(): void {\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","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan","OfflineExecutor"],"mappings":"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"}
1
+ {"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } 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 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 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\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 this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\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 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\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 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 try {\n // Load pending transactions and restore optimistic state\n await this.executor.loadPendingTransactions()\n\n // Start execution in the background - don't await to avoid blocking initialization\n // The transactions will execute and complete asynchronously\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return 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 || crypto.randomUUID(),\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: crypto.randomUUID(),\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`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\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 so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\n }\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 notifyOnline(): void {\n this.onlineDetector.notifyOnline()\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 dispose(): void {\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","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan","OfflineExecutor"],"mappings":"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"}
@@ -18,6 +18,7 @@ export declare class OfflineExecutor {
18
18
  private initResolve;
19
19
  private initReject;
20
20
  private pendingTransactionPromises;
21
+ private restorationTransactions;
21
22
  constructor(config: OfflineConfig);
22
23
  /**
23
24
  * Probe storage availability and create appropriate adapter.
@@ -29,12 +30,19 @@ export declare class OfflineExecutor {
29
30
  private initialize;
30
31
  private loadAndReplayTransactions;
31
32
  get isOfflineEnabled(): boolean;
33
+ /**
34
+ * Wait for the executor to fully initialize.
35
+ * This ensures that pending transactions are loaded and optimistic state is restored.
36
+ */
37
+ waitForInit(): Promise<void>;
32
38
  createOfflineTransaction(options: CreateOfflineTransactionOptions): Transaction | OfflineTransactionAPI;
33
39
  createOfflineAction<T>(options: CreateOfflineActionOptions<T>): (variables: T) => Transaction<Record<string, unknown>>;
34
40
  private persistTransaction;
35
41
  waitForTransactionCompletion(transactionId: string): Promise<any>;
36
42
  resolveTransaction(transactionId: string, result: any): void;
37
43
  rejectTransaction(transactionId: string, error: Error): void;
44
+ registerRestorationTransaction(offlineTransactionId: string, restorationTransaction: Transaction): void;
45
+ private cleanupRestorationTransaction;
38
46
  removeFromOutbox(id: string): Promise<void>;
39
47
  peekOutbox(): Promise<Array<OfflineTransaction>>;
40
48
  clearOutbox(): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const db = require("@tanstack/db");
3
4
  const RetryPolicy = require("../retry/RetryPolicy.cjs");
4
5
  const types = require("../types.cjs");
5
6
  const tracer = require("../telemetry/tracer.cjs");
@@ -163,6 +164,7 @@ class TransactionExecutor {
163
164
  for (const transaction of filteredTransactions) {
164
165
  this.scheduler.schedule(transaction);
165
166
  }
167
+ this.restoreOptimisticState(filteredTransactions);
166
168
  this.resetRetryDelays();
167
169
  this.scheduleNextRetry();
168
170
  const removedTransactions = transactions.filter(
@@ -172,6 +174,54 @@ class TransactionExecutor {
172
174
  await this.outbox.removeMany(removedTransactions.map((tx) => tx.id));
173
175
  }
174
176
  }
177
+ /**
178
+ * Restore optimistic state from loaded transactions.
179
+ * Creates internal transactions to hold the mutations so the collection's
180
+ * state manager can show optimistic data while waiting for sync.
181
+ */
182
+ restoreOptimisticState(transactions) {
183
+ for (const offlineTx of transactions) {
184
+ if (offlineTx.mutations.length === 0) {
185
+ continue;
186
+ }
187
+ try {
188
+ const restorationTx = db.createTransaction({
189
+ id: offlineTx.id,
190
+ autoCommit: false,
191
+ mutationFn: async () => {
192
+ }
193
+ });
194
+ restorationTx.isPersisted.promise.catch(() => {
195
+ });
196
+ restorationTx.applyMutations(offlineTx.mutations);
197
+ const touchedCollections = /* @__PURE__ */ new Set();
198
+ for (const mutation of offlineTx.mutations) {
199
+ if (!mutation.collection) {
200
+ continue;
201
+ }
202
+ const collectionId = mutation.collection.id;
203
+ if (touchedCollections.has(collectionId)) {
204
+ continue;
205
+ }
206
+ touchedCollections.add(collectionId);
207
+ mutation.collection._state.transactions.set(
208
+ restorationTx.id,
209
+ restorationTx
210
+ );
211
+ mutation.collection._state.recomputeOptimisticState(true);
212
+ }
213
+ this.offlineExecutor.registerRestorationTransaction(
214
+ offlineTx.id,
215
+ restorationTx
216
+ );
217
+ } catch (error) {
218
+ console.warn(
219
+ `Failed to restore optimistic state for transaction ${offlineTx.id}:`,
220
+ error
221
+ );
222
+ }
223
+ }
224
+ }
175
225
  clear() {
176
226
  this.scheduler.clear();
177
227
  this.clearRetryTimer();
@@ -1 +1 @@
1
- {"version":3,"file":"TransactionExecutor.cjs","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { DefaultRetryPolicy } from '../retry/RetryPolicy'\nimport { NonRetriableError } from '../types'\nimport { withNestedSpan } from '../telemetry/tracer'\nimport type { KeyScheduler } from './KeyScheduler'\nimport type { OutboxManager } from '../outbox/OutboxManager'\nimport type { OfflineConfig, OfflineTransaction } from '../types'\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: any // Reference to OfflineExecutor for signaling\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: any,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(10, config.jitter ?? true)\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n }\n }\n\n private async runExecution(): Promise<void> {\n const maxConcurrency = this.config.maxConcurrency ?? 3\n\n while (this.scheduler.getPendingCount() > 0) {\n const batch = this.scheduler.getNextBatch(maxConcurrency)\n\n if (batch.length === 0) {\n break\n }\n\n const executions = batch.map((transaction) =>\n this.executeTransaction(transaction),\n )\n await Promise.allSettled(executions)\n }\n\n // Schedule next retry after execution completes\n this.scheduleNextRetry()\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n try {\n await withNestedSpan(\n `transaction.execute`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.retryCount': transaction.retryCount,\n 'transaction.keyCount': transaction.keys.length,\n },\n async (span) => {\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n try {\n const result = await this.runMutationFn(transaction)\n\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(error))\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n },\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error,\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n 'transaction.id': transaction.id,\n 'error.name': error.name,\n 'error.message': error.message,\n },\n async (span) => {\n const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount,\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n\n const delay = this.retryPolicy.calculateDelay(transaction.retryCount)\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n span.setAttribute(`retryDelay`, delay)\n span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount)\n\n this.scheduler.markFailed(transaction)\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n span.setAttribute(`result`, `scheduled_retry`)\n } catch (persistError) {\n span.recordException(persistError as Error)\n span.setAttribute(`result`, `persist_failed`)\n throw persistError\n }\n\n // Schedule retry timer\n this.scheduleNextRetry()\n },\n )\n }\n\n async loadPendingTransactions(): Promise<void> {\n const transactions = await this.outbox.getAll()\n let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\n }\n\n for (const transaction of filteredTransactions) {\n this.scheduler.schedule(transaction)\n }\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n\n const removedTransactions = transactions.filter(\n (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id),\n )\n\n if (removedTransactions.length > 0) {\n await this.outbox.removeMany(removedTransactions.map((tx) => tx.id))\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n // Find the earliest retry time among pending transactions\n const earliestRetryTime = this.getEarliestRetryTime()\n\n if (earliestRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, earliestRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getEarliestRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt))\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":["DefaultRetryPolicy","withNestedSpan","NonRetriableError"],"mappings":"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"}
1
+ {"version":3,"file":"TransactionExecutor.cjs","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { createTransaction } from '@tanstack/db'\nimport { DefaultRetryPolicy } from '../retry/RetryPolicy'\nimport { NonRetriableError } from '../types'\nimport { withNestedSpan } from '../telemetry/tracer'\nimport type { KeyScheduler } from './KeyScheduler'\nimport type { OutboxManager } from '../outbox/OutboxManager'\nimport type { OfflineConfig, OfflineTransaction } from '../types'\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: any // Reference to OfflineExecutor for signaling\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: any,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(10, config.jitter ?? true)\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n }\n }\n\n private async runExecution(): Promise<void> {\n const maxConcurrency = this.config.maxConcurrency ?? 3\n\n while (this.scheduler.getPendingCount() > 0) {\n const batch = this.scheduler.getNextBatch(maxConcurrency)\n\n if (batch.length === 0) {\n break\n }\n\n const executions = batch.map((transaction) =>\n this.executeTransaction(transaction),\n )\n await Promise.allSettled(executions)\n }\n\n // Schedule next retry after execution completes\n this.scheduleNextRetry()\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n try {\n await withNestedSpan(\n `transaction.execute`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.retryCount': transaction.retryCount,\n 'transaction.keyCount': transaction.keys.length,\n },\n async (span) => {\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n try {\n const result = await this.runMutationFn(transaction)\n\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(error))\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n },\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error,\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n 'transaction.id': transaction.id,\n 'error.name': error.name,\n 'error.message': error.message,\n },\n async (span) => {\n const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount,\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n\n const delay = this.retryPolicy.calculateDelay(transaction.retryCount)\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n span.setAttribute(`retryDelay`, delay)\n span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount)\n\n this.scheduler.markFailed(transaction)\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n span.setAttribute(`result`, `scheduled_retry`)\n } catch (persistError) {\n span.recordException(persistError as Error)\n span.setAttribute(`result`, `persist_failed`)\n throw persistError\n }\n\n // Schedule retry timer\n this.scheduleNextRetry()\n },\n )\n }\n\n async loadPendingTransactions(): Promise<void> {\n const transactions = await this.outbox.getAll()\n let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\n }\n\n for (const transaction of filteredTransactions) {\n this.scheduler.schedule(transaction)\n }\n\n // Restore optimistic state for loaded transactions\n // This ensures the UI shows the optimistic data while transactions are pending\n this.restoreOptimisticState(filteredTransactions)\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n\n const removedTransactions = transactions.filter(\n (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id),\n )\n\n if (removedTransactions.length > 0) {\n await this.outbox.removeMany(removedTransactions.map((tx) => tx.id))\n }\n }\n\n /**\n * Restore optimistic state from loaded transactions.\n * Creates internal transactions to hold the mutations so the collection's\n * state manager can show optimistic data while waiting for sync.\n */\n private restoreOptimisticState(\n transactions: Array<OfflineTransaction>,\n ): void {\n for (const offlineTx of transactions) {\n if (offlineTx.mutations.length === 0) {\n continue\n }\n\n try {\n // Create a restoration transaction that holds mutations for optimistic state display.\n // It will never commit - the real mutation is handled by the offline executor.\n const restorationTx = createTransaction({\n id: offlineTx.id,\n autoCommit: false,\n mutationFn: async () => {},\n })\n\n // Prevent unhandled promise rejection when cleanup calls rollback()\n // We don't care about this promise - it's just for holding mutations\n restorationTx.isPersisted.promise.catch(() => {\n // Intentionally ignored - restoration transactions are cleaned up\n // via cleanupRestorationTransaction, not through normal commit flow\n })\n\n restorationTx.applyMutations(offlineTx.mutations)\n\n // Register with each affected collection's state manager\n const touchedCollections = new Set<string>()\n for (const mutation of offlineTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n\n mutation.collection._state.transactions.set(\n restorationTx.id,\n restorationTx,\n )\n mutation.collection._state.recomputeOptimisticState(true)\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\n console.warn(\n `Failed to restore optimistic state for transaction ${offlineTx.id}:`,\n error,\n )\n }\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n // Find the earliest retry time among pending transactions\n const earliestRetryTime = this.getEarliestRetryTime()\n\n if (earliestRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, earliestRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getEarliestRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt))\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":["DefaultRetryPolicy","withNestedSpan","NonRetriableError","createTransaction"],"mappings":"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"}
@@ -18,6 +18,12 @@ export declare class TransactionExecutor {
18
18
  private runMutationFn;
19
19
  private handleError;
20
20
  loadPendingTransactions(): Promise<void>;
21
+ /**
22
+ * Restore optimistic state from loaded transactions.
23
+ * Creates internal transactions to hold the mutations so the collection's
24
+ * state manager can show optimistic data while waiting for sync.
25
+ */
26
+ private restoreOptimisticState;
21
27
  clear(): void;
22
28
  getPendingCount(): number;
23
29
  private scheduleNextRetry;
@@ -56,18 +56,19 @@ class TransactionSerializer {
56
56
  if (!collection) {
57
57
  throw new Error(`Collection with id ${data.collectionId} not found`);
58
58
  }
59
+ const modified = this.deserializeValue(data.modified);
60
+ const key = modified ? collection.getKeyFromItem(modified) : null;
59
61
  return {
60
62
  globalKey: data.globalKey,
61
63
  type: data.type,
62
- modified: this.deserializeValue(data.modified),
64
+ modified,
63
65
  original: this.deserializeValue(data.original),
64
66
  changes: this.deserializeValue(data.changes) ?? {},
65
67
  collection,
66
68
  // These fields would need to be reconstructed by the executor
67
69
  mutationId: ``,
68
70
  // Will be regenerated
69
- key: null,
70
- // Will be extracted from the data
71
+ key,
71
72
  metadata: void 0,
72
73
  syncMetadata: {},
73
74
  optimistic: true,
@@ -1 +1 @@
1
- {"version":3,"file":"TransactionSerializer.cjs","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n createdAt: transaction.createdAt.toISOString(),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Parse without a reviver - let deserializeValue handle dates in mutation data\n // using the { __type: 'Date' } marker system\n const parsed: SerializedOfflineTransaction = JSON.parse(data)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(data: SerializedMutation): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified: this.deserializeValue(data.modified),\n original: this.deserializeValue(data.original),\n changes: this.deserializeValue(data.changes) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key: null, // Will be extracted from the data\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.serializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n private deserializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.deserializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TransactionSerializer.cjs","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n createdAt: transaction.createdAt.toISOString(),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Parse without a reviver - let deserializeValue handle dates in mutation data\n // using the { __type: 'Date' } marker system\n const parsed: SerializedOfflineTransaction = JSON.parse(data)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(data: SerializedMutation): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original),\n changes: this.deserializeValue(data.changes) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.serializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n private deserializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.deserializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
@@ -18,6 +18,7 @@ export declare class OfflineExecutor {
18
18
  private initResolve;
19
19
  private initReject;
20
20
  private pendingTransactionPromises;
21
+ private restorationTransactions;
21
22
  constructor(config: OfflineConfig);
22
23
  /**
23
24
  * Probe storage availability and create appropriate adapter.
@@ -29,12 +30,19 @@ export declare class OfflineExecutor {
29
30
  private initialize;
30
31
  private loadAndReplayTransactions;
31
32
  get isOfflineEnabled(): boolean;
33
+ /**
34
+ * Wait for the executor to fully initialize.
35
+ * This ensures that pending transactions are loaded and optimistic state is restored.
36
+ */
37
+ waitForInit(): Promise<void>;
32
38
  createOfflineTransaction(options: CreateOfflineTransactionOptions): Transaction | OfflineTransactionAPI;
33
39
  createOfflineAction<T>(options: CreateOfflineActionOptions<T>): (variables: T) => Transaction<Record<string, unknown>>;
34
40
  private persistTransaction;
35
41
  waitForTransactionCompletion(transactionId: string): Promise<any>;
36
42
  resolveTransaction(transactionId: string, result: any): void;
37
43
  rejectTransaction(transactionId: string, error: Error): void;
44
+ registerRestorationTransaction(offlineTransactionId: string, restorationTransaction: Transaction): void;
45
+ private cleanupRestorationTransaction;
38
46
  removeFromOutbox(id: string): Promise<void>;
39
47
  peekOutbox(): Promise<Array<OfflineTransaction>>;
40
48
  clearOutbox(): Promise<void>;
@@ -16,6 +16,7 @@ let OfflineExecutor$1 = class OfflineExecutor {
16
16
  this.unsubscribeOnline = null;
17
17
  this.unsubscribeLeadership = null;
18
18
  this.pendingTransactionPromises = /* @__PURE__ */ new Map();
19
+ this.restorationTransactions = /* @__PURE__ */ new Map();
19
20
  this.config = config;
20
21
  this.scheduler = new KeyScheduler();
21
22
  this.onlineDetector = config.onlineDetector ?? new WebOnlineDetector();
@@ -194,7 +195,9 @@ let OfflineExecutor$1 = class OfflineExecutor {
194
195
  }
195
196
  try {
196
197
  await this.executor.loadPendingTransactions();
197
- await this.executor.executeAll();
198
+ this.executor.executeAll().catch((error) => {
199
+ console.warn(`Failed to execute transactions:`, error);
200
+ });
198
201
  } catch (error) {
199
202
  console.warn(`Failed to load and replay transactions:`, error);
200
203
  }
@@ -202,6 +205,13 @@ let OfflineExecutor$1 = class OfflineExecutor {
202
205
  get isOfflineEnabled() {
203
206
  return this.mode === `offline` && this.isLeaderState;
204
207
  }
208
+ /**
209
+ * Wait for the executor to fully initialize.
210
+ * This ensures that pending transactions are loaded and optimistic state is restored.
211
+ */
212
+ async waitForInit() {
213
+ return this.initPromise;
214
+ }
205
215
  createOfflineTransaction(options) {
206
216
  const mutationFn = this.config.mutationFns[options.mutationFnName];
207
217
  if (!mutationFn) {
@@ -300,6 +310,7 @@ let OfflineExecutor$1 = class OfflineExecutor {
300
310
  deferred.resolve(result);
301
311
  this.pendingTransactionPromises.delete(transactionId);
302
312
  }
313
+ this.cleanupRestorationTransaction(transactionId);
303
314
  }
304
315
  // Method for TransactionExecutor to signal failure
305
316
  rejectTransaction(transactionId, error) {
@@ -308,6 +319,39 @@ let OfflineExecutor$1 = class OfflineExecutor {
308
319
  deferred.reject(error);
309
320
  this.pendingTransactionPromises.delete(transactionId);
310
321
  }
322
+ this.cleanupRestorationTransaction(transactionId, true);
323
+ }
324
+ // Method for TransactionExecutor to register restoration transactions
325
+ registerRestorationTransaction(offlineTransactionId, restorationTransaction) {
326
+ this.restorationTransactions.set(
327
+ offlineTransactionId,
328
+ restorationTransaction
329
+ );
330
+ }
331
+ cleanupRestorationTransaction(transactionId, shouldRollback = false) {
332
+ const restorationTx = this.restorationTransactions.get(transactionId);
333
+ if (!restorationTx) {
334
+ return;
335
+ }
336
+ this.restorationTransactions.delete(transactionId);
337
+ if (shouldRollback) {
338
+ restorationTx.rollback();
339
+ return;
340
+ }
341
+ restorationTx.setState(`completed`);
342
+ const touchedCollections = /* @__PURE__ */ new Set();
343
+ for (const mutation of restorationTx.mutations) {
344
+ if (!mutation.collection) {
345
+ continue;
346
+ }
347
+ const collectionId = mutation.collection.id;
348
+ if (touchedCollections.has(collectionId)) {
349
+ continue;
350
+ }
351
+ touchedCollections.add(collectionId);
352
+ mutation.collection._state.transactions.delete(restorationTx.id);
353
+ mutation.collection._state.recomputeOptimisticState(false);
354
+ }
311
355
  }
312
356
  async removeFromOutbox(id) {
313
357
  if (!this.outbox) {
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineExecutor.js","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } 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 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 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 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\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 this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\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 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\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 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 try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.mode === `offline` && this.isLeaderState\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 || crypto.randomUUID(),\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: crypto.randomUUID(),\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`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\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\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\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 notifyOnline(): void {\n this.onlineDetector.notifyOnline()\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 dispose(): void {\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":["OfflineTransactionAPI","action","OfflineExecutor"],"mappings":"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"}
1
+ {"version":3,"file":"OfflineExecutor.js","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } 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 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 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\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 this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\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 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\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 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 try {\n // Load pending transactions and restore optimistic state\n await this.executor.loadPendingTransactions()\n\n // Start execution in the background - don't await to avoid blocking initialization\n // The transactions will execute and complete asynchronously\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return 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 || crypto.randomUUID(),\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: crypto.randomUUID(),\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`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\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 so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\n }\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 notifyOnline(): void {\n this.onlineDetector.notifyOnline()\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 dispose(): void {\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":["OfflineTransactionAPI","action","OfflineExecutor"],"mappings":"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"}
@@ -18,6 +18,12 @@ export declare class TransactionExecutor {
18
18
  private runMutationFn;
19
19
  private handleError;
20
20
  loadPendingTransactions(): Promise<void>;
21
+ /**
22
+ * Restore optimistic state from loaded transactions.
23
+ * Creates internal transactions to hold the mutations so the collection's
24
+ * state manager can show optimistic data while waiting for sync.
25
+ */
26
+ private restoreOptimisticState;
21
27
  clear(): void;
22
28
  getPendingCount(): number;
23
29
  private scheduleNextRetry;
@@ -1,3 +1,4 @@
1
+ import { createTransaction } from "@tanstack/db";
1
2
  import { DefaultRetryPolicy } from "../retry/RetryPolicy.js";
2
3
  import { NonRetriableError } from "../types.js";
3
4
  import { withNestedSpan } from "../telemetry/tracer.js";
@@ -161,6 +162,7 @@ class TransactionExecutor {
161
162
  for (const transaction of filteredTransactions) {
162
163
  this.scheduler.schedule(transaction);
163
164
  }
165
+ this.restoreOptimisticState(filteredTransactions);
164
166
  this.resetRetryDelays();
165
167
  this.scheduleNextRetry();
166
168
  const removedTransactions = transactions.filter(
@@ -170,6 +172,54 @@ class TransactionExecutor {
170
172
  await this.outbox.removeMany(removedTransactions.map((tx) => tx.id));
171
173
  }
172
174
  }
175
+ /**
176
+ * Restore optimistic state from loaded transactions.
177
+ * Creates internal transactions to hold the mutations so the collection's
178
+ * state manager can show optimistic data while waiting for sync.
179
+ */
180
+ restoreOptimisticState(transactions) {
181
+ for (const offlineTx of transactions) {
182
+ if (offlineTx.mutations.length === 0) {
183
+ continue;
184
+ }
185
+ try {
186
+ const restorationTx = createTransaction({
187
+ id: offlineTx.id,
188
+ autoCommit: false,
189
+ mutationFn: async () => {
190
+ }
191
+ });
192
+ restorationTx.isPersisted.promise.catch(() => {
193
+ });
194
+ restorationTx.applyMutations(offlineTx.mutations);
195
+ const touchedCollections = /* @__PURE__ */ new Set();
196
+ for (const mutation of offlineTx.mutations) {
197
+ if (!mutation.collection) {
198
+ continue;
199
+ }
200
+ const collectionId = mutation.collection.id;
201
+ if (touchedCollections.has(collectionId)) {
202
+ continue;
203
+ }
204
+ touchedCollections.add(collectionId);
205
+ mutation.collection._state.transactions.set(
206
+ restorationTx.id,
207
+ restorationTx
208
+ );
209
+ mutation.collection._state.recomputeOptimisticState(true);
210
+ }
211
+ this.offlineExecutor.registerRestorationTransaction(
212
+ offlineTx.id,
213
+ restorationTx
214
+ );
215
+ } catch (error) {
216
+ console.warn(
217
+ `Failed to restore optimistic state for transaction ${offlineTx.id}:`,
218
+ error
219
+ );
220
+ }
221
+ }
222
+ }
173
223
  clear() {
174
224
  this.scheduler.clear();
175
225
  this.clearRetryTimer();
@@ -1 +1 @@
1
- {"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { DefaultRetryPolicy } from '../retry/RetryPolicy'\nimport { NonRetriableError } from '../types'\nimport { withNestedSpan } from '../telemetry/tracer'\nimport type { KeyScheduler } from './KeyScheduler'\nimport type { OutboxManager } from '../outbox/OutboxManager'\nimport type { OfflineConfig, OfflineTransaction } from '../types'\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: any // Reference to OfflineExecutor for signaling\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: any,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(10, config.jitter ?? true)\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n }\n }\n\n private async runExecution(): Promise<void> {\n const maxConcurrency = this.config.maxConcurrency ?? 3\n\n while (this.scheduler.getPendingCount() > 0) {\n const batch = this.scheduler.getNextBatch(maxConcurrency)\n\n if (batch.length === 0) {\n break\n }\n\n const executions = batch.map((transaction) =>\n this.executeTransaction(transaction),\n )\n await Promise.allSettled(executions)\n }\n\n // Schedule next retry after execution completes\n this.scheduleNextRetry()\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n try {\n await withNestedSpan(\n `transaction.execute`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.retryCount': transaction.retryCount,\n 'transaction.keyCount': transaction.keys.length,\n },\n async (span) => {\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n try {\n const result = await this.runMutationFn(transaction)\n\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(error))\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n },\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error,\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n 'transaction.id': transaction.id,\n 'error.name': error.name,\n 'error.message': error.message,\n },\n async (span) => {\n const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount,\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n\n const delay = this.retryPolicy.calculateDelay(transaction.retryCount)\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n span.setAttribute(`retryDelay`, delay)\n span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount)\n\n this.scheduler.markFailed(transaction)\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n span.setAttribute(`result`, `scheduled_retry`)\n } catch (persistError) {\n span.recordException(persistError as Error)\n span.setAttribute(`result`, `persist_failed`)\n throw persistError\n }\n\n // Schedule retry timer\n this.scheduleNextRetry()\n },\n )\n }\n\n async loadPendingTransactions(): Promise<void> {\n const transactions = await this.outbox.getAll()\n let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\n }\n\n for (const transaction of filteredTransactions) {\n this.scheduler.schedule(transaction)\n }\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n\n const removedTransactions = transactions.filter(\n (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id),\n )\n\n if (removedTransactions.length > 0) {\n await this.outbox.removeMany(removedTransactions.map((tx) => tx.id))\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n // Find the earliest retry time among pending transactions\n const earliestRetryTime = this.getEarliestRetryTime()\n\n if (earliestRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, earliestRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getEarliestRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt))\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { createTransaction } from '@tanstack/db'\nimport { DefaultRetryPolicy } from '../retry/RetryPolicy'\nimport { NonRetriableError } from '../types'\nimport { withNestedSpan } from '../telemetry/tracer'\nimport type { KeyScheduler } from './KeyScheduler'\nimport type { OutboxManager } from '../outbox/OutboxManager'\nimport type { OfflineConfig, OfflineTransaction } from '../types'\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: any // Reference to OfflineExecutor for signaling\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: any,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(10, config.jitter ?? true)\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n }\n }\n\n private async runExecution(): Promise<void> {\n const maxConcurrency = this.config.maxConcurrency ?? 3\n\n while (this.scheduler.getPendingCount() > 0) {\n const batch = this.scheduler.getNextBatch(maxConcurrency)\n\n if (batch.length === 0) {\n break\n }\n\n const executions = batch.map((transaction) =>\n this.executeTransaction(transaction),\n )\n await Promise.allSettled(executions)\n }\n\n // Schedule next retry after execution completes\n this.scheduleNextRetry()\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n try {\n await withNestedSpan(\n `transaction.execute`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.retryCount': transaction.retryCount,\n 'transaction.keyCount': transaction.keys.length,\n },\n async (span) => {\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n try {\n const result = await this.runMutationFn(transaction)\n\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(error))\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n },\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error,\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n 'transaction.id': transaction.id,\n 'error.name': error.name,\n 'error.message': error.message,\n },\n async (span) => {\n const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount,\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n\n const delay = this.retryPolicy.calculateDelay(transaction.retryCount)\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n span.setAttribute(`retryDelay`, delay)\n span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount)\n\n this.scheduler.markFailed(transaction)\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n span.setAttribute(`result`, `scheduled_retry`)\n } catch (persistError) {\n span.recordException(persistError as Error)\n span.setAttribute(`result`, `persist_failed`)\n throw persistError\n }\n\n // Schedule retry timer\n this.scheduleNextRetry()\n },\n )\n }\n\n async loadPendingTransactions(): Promise<void> {\n const transactions = await this.outbox.getAll()\n let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\n }\n\n for (const transaction of filteredTransactions) {\n this.scheduler.schedule(transaction)\n }\n\n // Restore optimistic state for loaded transactions\n // This ensures the UI shows the optimistic data while transactions are pending\n this.restoreOptimisticState(filteredTransactions)\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n\n const removedTransactions = transactions.filter(\n (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id),\n )\n\n if (removedTransactions.length > 0) {\n await this.outbox.removeMany(removedTransactions.map((tx) => tx.id))\n }\n }\n\n /**\n * Restore optimistic state from loaded transactions.\n * Creates internal transactions to hold the mutations so the collection's\n * state manager can show optimistic data while waiting for sync.\n */\n private restoreOptimisticState(\n transactions: Array<OfflineTransaction>,\n ): void {\n for (const offlineTx of transactions) {\n if (offlineTx.mutations.length === 0) {\n continue\n }\n\n try {\n // Create a restoration transaction that holds mutations for optimistic state display.\n // It will never commit - the real mutation is handled by the offline executor.\n const restorationTx = createTransaction({\n id: offlineTx.id,\n autoCommit: false,\n mutationFn: async () => {},\n })\n\n // Prevent unhandled promise rejection when cleanup calls rollback()\n // We don't care about this promise - it's just for holding mutations\n restorationTx.isPersisted.promise.catch(() => {\n // Intentionally ignored - restoration transactions are cleaned up\n // via cleanupRestorationTransaction, not through normal commit flow\n })\n\n restorationTx.applyMutations(offlineTx.mutations)\n\n // Register with each affected collection's state manager\n const touchedCollections = new Set<string>()\n for (const mutation of offlineTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n\n mutation.collection._state.transactions.set(\n restorationTx.id,\n restorationTx,\n )\n mutation.collection._state.recomputeOptimisticState(true)\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\n console.warn(\n `Failed to restore optimistic state for transaction ${offlineTx.id}:`,\n error,\n )\n }\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n // Find the earliest retry time among pending transactions\n const earliestRetryTime = this.getEarliestRetryTime()\n\n if (earliestRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, earliestRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getEarliestRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt))\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":[],"mappings":"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"}
@@ -54,18 +54,19 @@ class TransactionSerializer {
54
54
  if (!collection) {
55
55
  throw new Error(`Collection with id ${data.collectionId} not found`);
56
56
  }
57
+ const modified = this.deserializeValue(data.modified);
58
+ const key = modified ? collection.getKeyFromItem(modified) : null;
57
59
  return {
58
60
  globalKey: data.globalKey,
59
61
  type: data.type,
60
- modified: this.deserializeValue(data.modified),
62
+ modified,
61
63
  original: this.deserializeValue(data.original),
62
64
  changes: this.deserializeValue(data.changes) ?? {},
63
65
  collection,
64
66
  // These fields would need to be reconstructed by the executor
65
67
  mutationId: ``,
66
68
  // Will be regenerated
67
- key: null,
68
- // Will be extracted from the data
69
+ key,
69
70
  metadata: void 0,
70
71
  syncMetadata: {},
71
72
  optimistic: true,
@@ -1 +1 @@
1
- {"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n createdAt: transaction.createdAt.toISOString(),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Parse without a reviver - let deserializeValue handle dates in mutation data\n // using the { __type: 'Date' } marker system\n const parsed: SerializedOfflineTransaction = JSON.parse(data)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(data: SerializedMutation): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified: this.deserializeValue(data.modified),\n original: this.deserializeValue(data.original),\n changes: this.deserializeValue(data.changes) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key: null, // Will be extracted from the data\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.serializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n private deserializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.deserializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n createdAt: transaction.createdAt.toISOString(),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Parse without a reviver - let deserializeValue handle dates in mutation data\n // using the { __type: 'Date' } marker system\n const parsed: SerializedOfflineTransaction = JSON.parse(data)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(data: SerializedMutation): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original),\n changes: this.deserializeValue(data.changes) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.serializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n private deserializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.deserializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/offline-transactions",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Offline-first transaction capabilities for TanStack DB",
5
5
  "author": "TanStack",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "src"
51
51
  ],
52
52
  "dependencies": {
53
- "@tanstack/db": "0.5.21"
53
+ "@tanstack/db": "0.5.23"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@react-native-community/netinfo": ">=11.0.0",
@@ -69,6 +69,9 @@ export class OfflineExecutor {
69
69
  }
70
70
  > = new Map()
71
71
 
72
+ // Track restoration transactions for cleanup when offline transactions complete
73
+ private restorationTransactions: Map<string, Transaction> = new Map()
74
+
72
75
  constructor(config: OfflineConfig) {
73
76
  this.config = config
74
77
  this.scheduler = new KeyScheduler()
@@ -298,8 +301,14 @@ export class OfflineExecutor {
298
301
  }
299
302
 
300
303
  try {
304
+ // Load pending transactions and restore optimistic state
301
305
  await this.executor.loadPendingTransactions()
302
- await this.executor.executeAll()
306
+
307
+ // Start execution in the background - don't await to avoid blocking initialization
308
+ // The transactions will execute and complete asynchronously
309
+ this.executor.executeAll().catch((error) => {
310
+ console.warn(`Failed to execute transactions:`, error)
311
+ })
303
312
  } catch (error) {
304
313
  console.warn(`Failed to load and replay transactions:`, error)
305
314
  }
@@ -309,6 +318,14 @@ export class OfflineExecutor {
309
318
  return this.mode === `offline` && this.isLeaderState
310
319
  }
311
320
 
321
+ /**
322
+ * Wait for the executor to fully initialize.
323
+ * This ensures that pending transactions are loaded and optimistic state is restored.
324
+ */
325
+ async waitForInit(): Promise<void> {
326
+ return this.initPromise
327
+ }
328
+
312
329
  createOfflineTransaction(
313
330
  options: CreateOfflineTransactionOptions,
314
331
  ): Transaction | OfflineTransactionAPI {
@@ -441,6 +458,9 @@ export class OfflineExecutor {
441
458
  deferred.resolve(result)
442
459
  this.pendingTransactionPromises.delete(transactionId)
443
460
  }
461
+
462
+ // Clean up the restoration transaction - the sync will provide authoritative data
463
+ this.cleanupRestorationTransaction(transactionId)
444
464
  }
445
465
 
446
466
  // Method for TransactionExecutor to signal failure
@@ -450,6 +470,58 @@ export class OfflineExecutor {
450
470
  deferred.reject(error)
451
471
  this.pendingTransactionPromises.delete(transactionId)
452
472
  }
473
+
474
+ // Clean up the restoration transaction and rollback optimistic state
475
+ this.cleanupRestorationTransaction(transactionId, true)
476
+ }
477
+
478
+ // Method for TransactionExecutor to register restoration transactions
479
+ registerRestorationTransaction(
480
+ offlineTransactionId: string,
481
+ restorationTransaction: Transaction,
482
+ ): void {
483
+ this.restorationTransactions.set(
484
+ offlineTransactionId,
485
+ restorationTransaction,
486
+ )
487
+ }
488
+
489
+ private cleanupRestorationTransaction(
490
+ transactionId: string,
491
+ shouldRollback = false,
492
+ ): void {
493
+ const restorationTx = this.restorationTransactions.get(transactionId)
494
+ if (!restorationTx) {
495
+ return
496
+ }
497
+
498
+ this.restorationTransactions.delete(transactionId)
499
+
500
+ if (shouldRollback) {
501
+ restorationTx.rollback()
502
+ return
503
+ }
504
+
505
+ // Mark as completed so recomputeOptimisticState removes it from consideration.
506
+ // The actual data will come from the sync.
507
+ restorationTx.setState(`completed`)
508
+
509
+ // Remove from each collection's transaction map and recompute
510
+ const touchedCollections = new Set<string>()
511
+ for (const mutation of restorationTx.mutations) {
512
+ // Defensive check for corrupted deserialized data
513
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
514
+ if (!mutation.collection) {
515
+ continue
516
+ }
517
+ const collectionId = mutation.collection.id
518
+ if (touchedCollections.has(collectionId)) {
519
+ continue
520
+ }
521
+ touchedCollections.add(collectionId)
522
+ mutation.collection._state.transactions.delete(restorationTx.id)
523
+ mutation.collection._state.recomputeOptimisticState(false)
524
+ }
453
525
  }
454
526
 
455
527
  async removeFromOutbox(id: string): Promise<void> {
@@ -1,3 +1,4 @@
1
+ import { createTransaction } from '@tanstack/db'
1
2
  import { DefaultRetryPolicy } from '../retry/RetryPolicy'
2
3
  import { NonRetriableError } from '../types'
3
4
  import { withNestedSpan } from '../telemetry/tracer'
@@ -227,6 +228,10 @@ export class TransactionExecutor {
227
228
  this.scheduler.schedule(transaction)
228
229
  }
229
230
 
231
+ // Restore optimistic state for loaded transactions
232
+ // This ensures the UI shows the optimistic data while transactions are pending
233
+ this.restoreOptimisticState(filteredTransactions)
234
+
230
235
  // Reset retry delays for all loaded transactions so they can run immediately
231
236
  this.resetRetryDelays()
232
237
 
@@ -242,6 +247,71 @@ export class TransactionExecutor {
242
247
  }
243
248
  }
244
249
 
250
+ /**
251
+ * Restore optimistic state from loaded transactions.
252
+ * Creates internal transactions to hold the mutations so the collection's
253
+ * state manager can show optimistic data while waiting for sync.
254
+ */
255
+ private restoreOptimisticState(
256
+ transactions: Array<OfflineTransaction>,
257
+ ): void {
258
+ for (const offlineTx of transactions) {
259
+ if (offlineTx.mutations.length === 0) {
260
+ continue
261
+ }
262
+
263
+ try {
264
+ // Create a restoration transaction that holds mutations for optimistic state display.
265
+ // It will never commit - the real mutation is handled by the offline executor.
266
+ const restorationTx = createTransaction({
267
+ id: offlineTx.id,
268
+ autoCommit: false,
269
+ mutationFn: async () => {},
270
+ })
271
+
272
+ // Prevent unhandled promise rejection when cleanup calls rollback()
273
+ // We don't care about this promise - it's just for holding mutations
274
+ restorationTx.isPersisted.promise.catch(() => {
275
+ // Intentionally ignored - restoration transactions are cleaned up
276
+ // via cleanupRestorationTransaction, not through normal commit flow
277
+ })
278
+
279
+ restorationTx.applyMutations(offlineTx.mutations)
280
+
281
+ // Register with each affected collection's state manager
282
+ const touchedCollections = new Set<string>()
283
+ for (const mutation of offlineTx.mutations) {
284
+ // Defensive check for corrupted deserialized data
285
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
286
+ if (!mutation.collection) {
287
+ continue
288
+ }
289
+ const collectionId = mutation.collection.id
290
+ if (touchedCollections.has(collectionId)) {
291
+ continue
292
+ }
293
+ touchedCollections.add(collectionId)
294
+
295
+ mutation.collection._state.transactions.set(
296
+ restorationTx.id,
297
+ restorationTx,
298
+ )
299
+ mutation.collection._state.recomputeOptimisticState(true)
300
+ }
301
+
302
+ this.offlineExecutor.registerRestorationTransaction(
303
+ offlineTx.id,
304
+ restorationTx,
305
+ )
306
+ } catch (error) {
307
+ console.warn(
308
+ `Failed to restore optimistic state for transaction ${offlineTx.id}:`,
309
+ error,
310
+ )
311
+ }
312
+ }
313
+ }
314
+
245
315
  clear(): void {
246
316
  this.scheduler.clear()
247
317
  this.clearRetryTimer()
@@ -77,18 +77,24 @@ export class TransactionSerializer {
77
77
  throw new Error(`Collection with id ${data.collectionId} not found`)
78
78
  }
79
79
 
80
+ const modified = this.deserializeValue(data.modified)
81
+
82
+ // Extract the key from the modified data using the collection's getKey function
83
+ // This is needed for optimistic state restoration to work correctly
84
+ const key = modified ? collection.getKeyFromItem(modified) : null
85
+
80
86
  // Create a partial PendingMutation - we can't fully reconstruct it but
81
87
  // we provide what we can. The executor will need to handle the rest.
82
88
  return {
83
89
  globalKey: data.globalKey,
84
90
  type: data.type as any,
85
- modified: this.deserializeValue(data.modified),
91
+ modified,
86
92
  original: this.deserializeValue(data.original),
87
93
  changes: this.deserializeValue(data.changes) ?? {},
88
94
  collection,
89
95
  // These fields would need to be reconstructed by the executor
90
96
  mutationId: ``, // Will be regenerated
91
- key: null, // Will be extracted from the data
97
+ key,
92
98
  metadata: undefined,
93
99
  syncMetadata: {},
94
100
  optimistic: true,