@tanstack/offline-transactions 1.0.56 → 1.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +137 -23
  2. package/dist/cjs/OfflineExecutor.cjs +2 -0
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/connectivity/OnlineDetector.cjs +1 -1
  5. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
  6. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +1 -7
  7. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -1
  8. package/dist/cjs/executor/KeyScheduler.cjs +24 -25
  9. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
  10. package/dist/cjs/executor/KeyScheduler.d.cts +4 -2
  11. package/dist/cjs/executor/TransactionExecutor.cjs +60 -30
  12. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
  13. package/dist/cjs/outbox/OutboxManager.cjs +66 -24
  14. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
  15. package/dist/cjs/outbox/OutboxManager.d.cts +6 -0
  16. package/dist/cjs/outbox/TransactionSerializer.cjs +132 -22
  17. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
  18. package/dist/cjs/outbox/TransactionSerializer.d.cts +2 -0
  19. package/dist/cjs/types.cjs.map +1 -1
  20. package/dist/cjs/types.d.cts +1 -1
  21. package/dist/esm/OfflineExecutor.js +2 -0
  22. package/dist/esm/OfflineExecutor.js.map +1 -1
  23. package/dist/esm/connectivity/OnlineDetector.js +1 -1
  24. package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
  25. package/dist/esm/connectivity/ReactNativeOnlineDetector.js +1 -7
  26. package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -1
  27. package/dist/esm/executor/KeyScheduler.d.ts +4 -2
  28. package/dist/esm/executor/KeyScheduler.js +24 -25
  29. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  30. package/dist/esm/executor/TransactionExecutor.js +60 -30
  31. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  32. package/dist/esm/outbox/OutboxManager.d.ts +6 -0
  33. package/dist/esm/outbox/OutboxManager.js +67 -25
  34. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  35. package/dist/esm/outbox/TransactionSerializer.d.ts +2 -0
  36. package/dist/esm/outbox/TransactionSerializer.js +132 -22
  37. package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
  38. package/dist/esm/types.d.ts +1 -1
  39. package/dist/esm/types.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/OfflineExecutor.ts +2 -1
  42. package/src/connectivity/OnlineDetector.ts +6 -1
  43. package/src/connectivity/ReactNativeOnlineDetector.ts +1 -11
  44. package/src/executor/KeyScheduler.ts +29 -26
  45. package/src/executor/TransactionExecutor.ts +71 -43
  46. package/src/outbox/OutboxManager.ts +82 -28
  47. package/src/outbox/TransactionSerializer.ts +192 -25
  48. package/src/types.ts +1 -1
@@ -34,6 +34,7 @@ class TransactionExecutor {
34
34
  } finally {
35
35
  this.isExecuting = false;
36
36
  this.executionPromise = null;
37
+ this.scheduleNextRetry();
37
38
  }
38
39
  }
39
40
  async runExecution() {
@@ -47,7 +48,6 @@ class TransactionExecutor {
47
48
  }
48
49
  await this.executeTransaction(transaction);
49
50
  }
50
- this.scheduleNextRetry();
51
51
  }
52
52
  async executeTransaction(transaction) {
53
53
  try {
@@ -64,15 +64,9 @@ class TransactionExecutor {
64
64
  if (transaction.retryCount > 0) {
65
65
  span.setAttribute(`retry.attempt`, transaction.retryCount);
66
66
  }
67
+ let result;
67
68
  try {
68
- const result = await this.runMutationFn(transaction);
69
- try {
70
- await this.outbox.remove(transaction.id);
71
- } finally {
72
- this.scheduler.markCompleted(transaction);
73
- }
74
- span.setAttribute(`result`, `success`);
75
- this.offlineExecutor.resolveTransaction(transaction.id, result);
69
+ result = await this.runMutationFn(transaction);
76
70
  } catch (error) {
77
71
  const err = error instanceof Error ? error : new Error(String(error));
78
72
  span.setAttribute(`result`, `error`);
@@ -80,6 +74,17 @@ class TransactionExecutor {
80
74
  err[HANDLED_EXECUTION_ERROR] = true;
81
75
  throw err;
82
76
  }
77
+ let removalError;
78
+ try {
79
+ await this.outbox.remove(transaction.id);
80
+ } catch (error) {
81
+ removalError = error;
82
+ } finally {
83
+ this.scheduler.markCompleted(transaction);
84
+ }
85
+ span.setAttribute(`result`, `success`);
86
+ this.offlineExecutor.resolveTransaction(transaction.id, result);
87
+ if (removalError !== void 0) throw removalError;
83
88
  }
84
89
  );
85
90
  } catch (error) {
@@ -123,14 +128,21 @@ class TransactionExecutor {
123
128
  );
124
129
  span.setAttribute(`shouldRetry`, shouldRetry);
125
130
  if (!shouldRetry) {
126
- this.scheduler.markCompleted(transaction);
127
- await this.outbox.remove(transaction.id);
131
+ let removalError;
132
+ try {
133
+ await this.outbox.remove(transaction.id);
134
+ } catch (cleanupError) {
135
+ removalError = cleanupError;
136
+ } finally {
137
+ this.scheduler.markCompleted(transaction);
138
+ }
128
139
  console.warn(
129
140
  `Transaction ${transaction.id} failed permanently:`,
130
141
  error
131
142
  );
132
143
  span.setAttribute(`result`, `permanent_failure`);
133
144
  this.offlineExecutor.rejectTransaction(transaction.id, error);
145
+ if (removalError !== void 0) throw removalError;
134
146
  return;
135
147
  }
136
148
  const delay = Math.max(
@@ -149,7 +161,6 @@ class TransactionExecutor {
149
161
  };
150
162
  span.setAttribute(`retryDelay`, delay);
151
163
  span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount);
152
- this.scheduler.markFailed(transaction);
153
164
  this.scheduler.updateTransaction(updatedTransaction);
154
165
  try {
155
166
  await this.outbox.update(transaction.id, updatedTransaction);
@@ -158,29 +169,48 @@ class TransactionExecutor {
158
169
  span.recordException(persistError);
159
170
  span.setAttribute(`result`, `persist_failed`);
160
171
  throw persistError;
172
+ } finally {
173
+ this.scheduler.markFailed(transaction);
161
174
  }
162
- this.scheduleNextRetry();
163
175
  }
164
176
  );
165
177
  }
166
178
  async loadPendingTransactions() {
167
- const transactions = await this.outbox.getAll();
168
- let filteredTransactions = transactions;
169
- if (this.config.beforeRetry) {
170
- filteredTransactions = this.config.beforeRetry(transactions);
171
- }
172
- if (!this.offlineExecutor.isOfflineEnabled) return;
173
- const newlyLoaded = filteredTransactions.filter(
174
- (transaction) => this.scheduler.schedule(transaction)
175
- );
176
- this.restoreOptimisticState(newlyLoaded);
177
- this.resetRetryDelays();
178
- this.scheduleNextRetry();
179
- const removedTransactions = transactions.filter(
180
- (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id)
181
- );
182
- if (removedTransactions.length > 0) {
183
- await this.outbox.removeMany(removedTransactions.map((tx) => tx.id));
179
+ let removedIds = [];
180
+ await this.outbox.withAll((transactions) => {
181
+ const { isOfflineEnabled } = this.offlineExecutor;
182
+ if (!isOfflineEnabled) return;
183
+ let filteredTransactions = transactions;
184
+ if (this.config.beforeRetry) {
185
+ filteredTransactions = this.config.beforeRetry(transactions);
186
+ }
187
+ if (!this.offlineExecutor.isOfflineEnabled) return;
188
+ const newlyLoaded = filteredTransactions.filter(
189
+ (transaction) => this.scheduler.schedule(transaction)
190
+ );
191
+ const retainedIds = new Set(filteredTransactions.map(({ id }) => id));
192
+ removedIds = transactions.filter(({ id }) => !retainedIds.has(id)).map(({ id }) => id);
193
+ removedIds = this.scheduler.removePendingTransactions(removedIds);
194
+ this.restoreOptimisticState(newlyLoaded);
195
+ this.resetRetryDelays();
196
+ this.scheduleNextRetry();
197
+ });
198
+ if (removedIds.length > 0) {
199
+ const error = new types.NonRetriableError(`Transaction excluded by beforeRetry`);
200
+ await Promise.all(
201
+ removedIds.map(async (id) => {
202
+ try {
203
+ await this.outbox.remove(id);
204
+ this.offlineExecutor.rejectTransaction(id, error);
205
+ } catch (cleanupError) {
206
+ console.warn(
207
+ `Failed to remove transaction excluded by beforeRetry:`,
208
+ id,
209
+ cleanupError
210
+ );
211
+ }
212
+ })
213
+ );
184
214
  }
185
215
  }
186
216
  /**
@@ -1 +1 @@
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 {\n OfflineConfig,\n OfflineTransaction,\n TransactionSignaler,\n} 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: TransactionSignaler\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: TransactionSignaler,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(\n Number.POSITIVE_INFINITY,\n config.jitter ?? true,\n )\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 while (this.scheduler.getPendingCount() > 0) {\n if (!this.canExecute()) {\n break\n }\n\n const transaction = this.scheduler.getNext()\n\n if (!transaction) {\n break\n }\n\n await this.executeTransaction(transaction)\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 try {\n // Replay can still see this ID until durable deletion settles.\n await this.outbox.remove(transaction.id)\n } finally {\n this.scheduler.markCompleted(transaction)\n }\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 = Math.max(\n 0,\n this.retryPolicy.calculateDelay(transaction.retryCount),\n )\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 // The outbox read or retry hook may outlive this owner's right to replay.\n if (!this.offlineExecutor.isOfflineEnabled) return\n\n const newlyLoaded = filteredTransactions.filter((transaction) =>\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(newlyLoaded)\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 pause(): void {\n // Retain queued work and let the issued call finish its acknowledgment.\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 if (!this.canExecute()) {\n return\n }\n\n const nextRetryTime = this.getNextRetryTime()\n\n if (nextRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, nextRetryTime - 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 getNextRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n // Later transactions cannot overtake the FIFO head, even if they are ready.\n return allTransactions[0]!.nextAttemptAt\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n private canExecute(): boolean {\n return (\n this.offlineExecutor.isOfflineEnabled && this.offlineExecutor.isOnline()\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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"}
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 {\n OfflineConfig,\n OfflineTransaction,\n TransactionSignaler,\n} 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: TransactionSignaler\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: TransactionSignaler,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(\n Number.POSITIVE_INFINITY,\n config.jitter ?? true,\n )\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 this.scheduleNextRetry()\n }\n }\n\n private async runExecution(): Promise<void> {\n while (this.scheduler.getPendingCount() > 0) {\n if (!this.canExecute()) {\n break\n }\n\n const transaction = this.scheduler.getNext()\n\n if (!transaction) {\n break\n }\n\n await this.executeTransaction(transaction)\n }\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 let result: void\n try {\n result = await this.runMutationFn(transaction)\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 let removalError: unknown\n try {\n // Replay can still see this ID until durable deletion settles.\n await this.outbox.remove(transaction.id)\n } catch (error) {\n removalError = error\n } finally {\n this.scheduler.markCompleted(transaction)\n }\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n if (removalError !== undefined) throw removalError\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 let removalError: unknown\n try {\n await this.outbox.remove(transaction.id)\n } catch (cleanupError) {\n removalError = cleanupError\n } finally {\n this.scheduler.markCompleted(transaction)\n }\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 if (removalError !== undefined) throw removalError\n return\n }\n\n const delay = Math.max(\n 0,\n this.retryPolicy.calculateDelay(transaction.retryCount),\n )\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.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 } finally {\n this.scheduler.markFailed(transaction)\n }\n },\n )\n }\n\n async loadPendingTransactions(): Promise<void> {\n let removedIds: Array<string> = []\n await this.outbox.withAll((transactions) => {\n const { isOfflineEnabled } = this.offlineExecutor\n if (!isOfflineEnabled) return\n let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\n }\n\n // The retry hook is user code and may synchronously revoke replay rights.\n if (!this.offlineExecutor.isOfflineEnabled) return\n\n const newlyLoaded = filteredTransactions.filter((transaction) =>\n this.scheduler.schedule(transaction),\n )\n\n const retainedIds = new Set(filteredTransactions.map(({ id }) => id))\n removedIds = transactions\n .filter(({ id }) => !retainedIds.has(id))\n .map(({ id }) => id)\n removedIds = this.scheduler.removePendingTransactions(removedIds)\n\n // Restore optimistic state for loaded transactions\n // This ensures the UI shows the optimistic data while transactions are pending\n this.restoreOptimisticState(newlyLoaded)\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\n if (removedIds.length > 0) {\n const error = new NonRetriableError(`Transaction excluded by beforeRetry`)\n await Promise.all(\n removedIds.map(async (id) => {\n try {\n await this.outbox.remove(id)\n this.offlineExecutor.rejectTransaction(id, error)\n } catch (cleanupError) {\n console.warn(\n `Failed to remove transaction excluded by beforeRetry:`,\n id,\n cleanupError,\n )\n }\n }),\n )\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 pause(): void {\n // Retain queued work and let the issued call finish its acknowledgment.\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 if (!this.canExecute()) {\n return\n }\n\n const nextRetryTime = this.getNextRetryTime()\n\n if (nextRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, nextRetryTime - 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 getNextRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n // Later transactions cannot overtake the FIFO head, even if they are ready.\n return allTransactions[0]!.nextAttemptAt\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n private canExecute(): boolean {\n return (\n this.offlineExecutor.isOfflineEnabled && this.offlineExecutor.isOnline()\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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"}
@@ -5,12 +5,32 @@ const TransactionSerializer = require("./TransactionSerializer.cjs");
5
5
  class OutboxManager {
6
6
  constructor(storage, collections) {
7
7
  this.keyPrefix = `tx:`;
8
+ this.removalRevision = 0;
9
+ this.activeReads = /* @__PURE__ */ new Set();
10
+ this.removedAtRevision = /* @__PURE__ */ new Map();
8
11
  this.storage = storage;
9
12
  this.serializer = new TransactionSerializer.TransactionSerializer(collections);
10
13
  }
11
14
  getStorageKey(id) {
12
15
  return `${this.keyPrefix}${id}`;
13
16
  }
17
+ recordRemovals(ids) {
18
+ this.removalRevision++;
19
+ if (this.activeReads.size === 0) return;
20
+ for (const id of ids) this.removedAtRevision.set(id, this.removalRevision);
21
+ this.pruneRemovalHistory();
22
+ }
23
+ pruneRemovalHistory() {
24
+ if (this.activeReads.size === 0) {
25
+ this.removedAtRevision.clear();
26
+ return;
27
+ }
28
+ let oldestRead = Number.POSITIVE_INFINITY;
29
+ for (const read of this.activeReads)
30
+ oldestRead = Math.min(oldestRead, read.revision);
31
+ for (const [id, revision] of this.removedAtRevision)
32
+ if (revision <= oldestRead) this.removedAtRevision.delete(id);
33
+ }
14
34
  async add(transaction) {
15
35
  return tracer.withSpan(
16
36
  `outbox.add`,
@@ -39,6 +59,10 @@ class OutboxManager {
39
59
  span.setAttribute(`result`, `found`);
40
60
  return transaction;
41
61
  } catch (error) {
62
+ if (error instanceof TransactionSerializer.MissingTemporalConstructorError) {
63
+ error.message = `transaction ${id}: ${error.message}`;
64
+ throw error;
65
+ }
42
66
  console.warn(`Failed to deserialize transaction ${id}:`, error);
43
67
  span.setAttribute(`result`, `deserialize_error`);
44
68
  return null;
@@ -46,31 +70,46 @@ class OutboxManager {
46
70
  });
47
71
  }
48
72
  async getAll() {
49
- return tracer.withSpan(`outbox.getAll`, {}, async (span) => {
50
- const keys = await this.storage.keys();
51
- const transactionKeys = keys.filter(
52
- (key) => key.startsWith(this.keyPrefix)
53
- );
54
- span.setAttribute(`transactionCount`, transactionKeys.length);
55
- const transactions = [];
56
- for (const key of transactionKeys) {
57
- const data = await this.storage.get(key);
58
- if (data) {
59
- try {
60
- const transaction = this.serializer.deserialize(data);
61
- transactions.push(transaction);
62
- } catch (error) {
63
- console.warn(
64
- `Failed to deserialize transaction from key ${key}:`,
65
- error
66
- );
73
+ return this.withAll((transactions) => transactions);
74
+ }
75
+ async withAll(consume) {
76
+ const read = { revision: this.removalRevision };
77
+ this.activeReads.add(read);
78
+ try {
79
+ return await tracer.withSpan(`outbox.getAll`, {}, async (span) => {
80
+ const keys = await this.storage.keys();
81
+ const transactionKeys = keys.filter(
82
+ (key) => key.startsWith(this.keyPrefix)
83
+ );
84
+ span.setAttribute(`transactionCount`, transactionKeys.length);
85
+ const transactions = [];
86
+ for (const key of transactionKeys) {
87
+ const data = await this.storage.get(key);
88
+ if (data) {
89
+ try {
90
+ const transaction = this.serializer.deserialize(data);
91
+ transactions.push(transaction);
92
+ } catch (error) {
93
+ if (error instanceof TransactionSerializer.MissingTemporalConstructorError) {
94
+ error.message = `transaction ${key.slice(this.keyPrefix.length)}: ${error.message}`;
95
+ throw error;
96
+ }
97
+ console.warn(
98
+ `Failed to deserialize transaction from key ${key}:`,
99
+ error
100
+ );
101
+ }
67
102
  }
68
103
  }
69
- }
70
- return transactions.sort(
71
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
72
- );
73
- });
104
+ const currentTransactions = transactions.filter(
105
+ ({ id }) => (this.removedAtRevision.get(id) ?? 0) <= read.revision
106
+ ).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
107
+ return consume(currentTransactions);
108
+ });
109
+ } finally {
110
+ this.activeReads.delete(read);
111
+ this.pruneRemovalHistory();
112
+ }
74
113
  }
75
114
  async getByKeys(keys) {
76
115
  const allTransactions = await this.getAll();
@@ -93,6 +132,7 @@ class OutboxManager {
93
132
  return tracer.withSpan(`outbox.remove`, {}, async () => {
94
133
  const key = this.getStorageKey(id);
95
134
  await this.storage.delete(key);
135
+ this.recordRemovals([id]);
96
136
  });
97
137
  }
98
138
  async removeMany(ids) {
@@ -103,7 +143,9 @@ class OutboxManager {
103
143
  async clear() {
104
144
  const keys = await this.storage.keys();
105
145
  const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix));
106
- await Promise.all(transactionKeys.map((key) => this.storage.delete(key)));
146
+ await this.removeMany(
147
+ transactionKeys.map((key) => key.slice(this.keyPrefix.length))
148
+ );
107
149
  }
108
150
  async count() {
109
151
  const keys = await this.storage.keys();
@@ -1 +1 @@
1
- {"version":3,"file":"OutboxManager.cjs","sources":["../../../src/outbox/OutboxManager.ts"],"sourcesContent":["import { withSpan } from '../telemetry/tracer'\nimport { TransactionSerializer } from './TransactionSerializer'\nimport type { OfflineTransaction, StorageAdapter } from '../types'\nimport type { Collection } from '@tanstack/db'\n\nexport class OutboxManager {\n private storage: StorageAdapter\n private serializer: TransactionSerializer\n private keyPrefix = `tx:`\n\n constructor(\n storage: StorageAdapter,\n\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.storage = storage\n this.serializer = new TransactionSerializer(collections)\n }\n\n private getStorageKey(id: string): string {\n return `${this.keyPrefix}${id}`\n }\n\n async add(transaction: OfflineTransaction): Promise<void> {\n return withSpan(\n `outbox.add`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.keyCount': transaction.keys.length,\n },\n async () => {\n const key = this.getStorageKey(transaction.id)\n const serialized = this.serializer.serialize(transaction)\n await this.storage.set(key, serialized)\n },\n )\n }\n\n async get(id: string): Promise<OfflineTransaction | null> {\n return withSpan(`outbox.get`, { 'transaction.id': id }, async (span) => {\n const key = this.getStorageKey(id)\n const data = await this.storage.get(key)\n\n if (!data) {\n span.setAttribute(`result`, `not_found`)\n return null\n }\n\n try {\n const transaction = this.serializer.deserialize(data)\n span.setAttribute(`result`, `found`)\n return transaction\n } catch (error) {\n console.warn(`Failed to deserialize transaction ${id}:`, error)\n span.setAttribute(`result`, `deserialize_error`)\n return null\n }\n })\n }\n\n async getAll(): Promise<Array<OfflineTransaction>> {\n return withSpan(`outbox.getAll`, {}, async (span) => {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) =>\n key.startsWith(this.keyPrefix),\n )\n\n span.setAttribute(`transactionCount`, transactionKeys.length)\n\n const transactions: Array<OfflineTransaction> = []\n\n for (const key of transactionKeys) {\n const data = await this.storage.get(key)\n if (data) {\n try {\n const transaction = this.serializer.deserialize(data)\n transactions.push(transaction)\n } catch (error) {\n console.warn(\n `Failed to deserialize transaction from key ${key}:`,\n error,\n )\n }\n }\n }\n\n return transactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n )\n })\n }\n\n async getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>> {\n const allTransactions = await this.getAll()\n const keySet = new Set(keys)\n\n return allTransactions.filter((transaction) =>\n transaction.keys.some((key) => keySet.has(key)),\n )\n }\n\n async update(\n id: string,\n updates: Partial<OfflineTransaction>,\n ): Promise<void> {\n return withSpan(`outbox.update`, { 'transaction.id': id }, async () => {\n const existing = await this.get(id)\n if (!existing) {\n throw new Error(`Transaction ${id} not found`)\n }\n\n const updated = { ...existing, ...updates }\n await this.add(updated)\n })\n }\n\n async remove(id: string): Promise<void> {\n return withSpan(`outbox.remove`, { 'transaction.id': id }, async () => {\n const key = this.getStorageKey(id)\n await this.storage.delete(key)\n })\n }\n\n async removeMany(ids: Array<string>): Promise<void> {\n return withSpan(`outbox.removeMany`, { count: ids.length }, async () => {\n await Promise.all(ids.map((id) => this.remove(id)))\n })\n }\n\n async clear(): Promise<void> {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix))\n\n await Promise.all(transactionKeys.map((key) => this.storage.delete(key)))\n }\n\n async count(): Promise<number> {\n const keys = await this.storage.keys()\n return keys.filter((key) => key.startsWith(this.keyPrefix)).length\n }\n}\n"],"names":["TransactionSerializer","withSpan"],"mappings":";;;;AAKO,MAAM,cAAc;AAAA,EAKzB,YACE,SAEA,aACA;AANF,SAAQ,YAAY;AAOlB,SAAK,UAAU;AACf,SAAK,aAAa,IAAIA,sBAAAA,sBAAsB,WAAW;AAAA,EACzD;AAAA,EAEQ,cAAc,IAAoB;AACxC,WAAO,GAAG,KAAK,SAAS,GAAG,EAAE;AAAA,EAC/B;AAAA,EAEA,MAAM,IAAI,aAAgD;AACxD,WAAOC,OAAAA;AAAAA,MACL;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,8BAA8B,YAAY;AAAA,QAC1C,wBAAwB,YAAY,KAAK;AAAA,MAAA;AAAA,MAE3C,YAAY;AACV,cAAM,MAAM,KAAK,cAAc,YAAY,EAAE;AAC7C,cAAM,aAAa,KAAK,WAAW,UAAU,WAAW;AACxD,cAAM,KAAK,QAAQ,IAAI,KAAK,UAAU;AAAA,MACxC;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAM,IAAI,IAAgD;AACxD,WAAOA,OAAAA,SAAS,cAAc,CAAuB,GAAG,OAAO,SAAS;AACtE,YAAM,MAAM,KAAK,cAAc,EAAE;AACjC,YAAM,OAAO,MAAM,KAAK,QAAQ,IAAI,GAAG;AAEvC,UAAI,CAAC,MAAM;AACT,aAAK,aAAa,UAAU,WAAW;AACvC,eAAO;AAAA,MACT;AAEA,UAAI;AACF,cAAM,cAAc,KAAK,WAAW,YAAY,IAAI;AACpD,aAAK,aAAa,UAAU,OAAO;AACnC,eAAO;AAAA,MACT,SAAS,OAAO;AACd,gBAAQ,KAAK,qCAAqC,EAAE,KAAK,KAAK;AAC9D,aAAK,aAAa,UAAU,mBAAmB;AAC/C,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,SAA6C;AACjD,WAAOA,OAAAA,SAAS,iBAAiB,CAAA,GAAI,OAAO,SAAS;AACnD,YAAM,OAAO,MAAM,KAAK,QAAQ,KAAA;AAChC,YAAM,kBAAkB,KAAK;AAAA,QAAO,CAAC,QACnC,IAAI,WAAW,KAAK,SAAS;AAAA,MAAA;AAG/B,WAAK,aAAa,oBAAoB,gBAAgB,MAAM;AAE5D,YAAM,eAA0C,CAAA;AAEhD,iBAAW,OAAO,iBAAiB;AACjC,cAAM,OAAO,MAAM,KAAK,QAAQ,IAAI,GAAG;AACvC,YAAI,MAAM;AACR,cAAI;AACF,kBAAM,cAAc,KAAK,WAAW,YAAY,IAAI;AACpD,yBAAa,KAAK,WAAW;AAAA,UAC/B,SAAS,OAAO;AACd,oBAAQ;AAAA,cACN,8CAA8C,GAAG;AAAA,cACjD;AAAA,YAAA;AAAA,UAEJ;AAAA,QACF;AAAA,MACF;AAEA,aAAO,aAAa;AAAA,QAClB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,MAAQ;AAAA,IAE1D,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,UAAU,MAAyD;AACvE,UAAM,kBAAkB,MAAM,KAAK,OAAA;AACnC,UAAM,SAAS,IAAI,IAAI,IAAI;AAE3B,WAAO,gBAAgB;AAAA,MAAO,CAAC,gBAC7B,YAAY,KAAK,KAAK,CAAC,QAAQ,OAAO,IAAI,GAAG,CAAC;AAAA,IAAA;AAAA,EAElD;AAAA,EAEA,MAAM,OACJ,IACA,SACe;AACf,WAAOA,OAAAA,SAAS,iBAAiB,CAAuB,GAAG,YAAY;AACrE,YAAM,WAAW,MAAM,KAAK,IAAI,EAAE;AAClC,UAAI,CAAC,UAAU;AACb,cAAM,IAAI,MAAM,eAAe,EAAE,YAAY;AAAA,MAC/C;AAEA,YAAM,UAAU,EAAE,GAAG,UAAU,GAAG,QAAA;AAClC,YAAM,KAAK,IAAI,OAAO;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,IAA2B;AACtC,WAAOA,OAAAA,SAAS,iBAAiB,CAAuB,GAAG,YAAY;AACrE,YAAM,MAAM,KAAK,cAAc,EAAE;AACjC,YAAM,KAAK,QAAQ,OAAO,GAAG;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,WAAW,KAAmC;AAClD,WAAOA,OAAAA,SAAS,qBAAqB,EAAE,OAAO,IAAI,OAAA,GAAU,YAAY;AACtE,YAAM,QAAQ,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC,CAAC;AAAA,IACpD,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,OAAO,MAAM,KAAK,QAAQ,KAAA;AAChC,UAAM,kBAAkB,KAAK,OAAO,CAAC,QAAQ,IAAI,WAAW,KAAK,SAAS,CAAC;AAE3E,UAAM,QAAQ,IAAI,gBAAgB,IAAI,CAAC,QAAQ,KAAK,QAAQ,OAAO,GAAG,CAAC,CAAC;AAAA,EAC1E;AAAA,EAEA,MAAM,QAAyB;AAC7B,UAAM,OAAO,MAAM,KAAK,QAAQ,KAAA;AAChC,WAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,WAAW,KAAK,SAAS,CAAC,EAAE;AAAA,EAC9D;AACF;;"}
1
+ {"version":3,"file":"OutboxManager.cjs","sources":["../../../src/outbox/OutboxManager.ts"],"sourcesContent":["import { withSpan } from '../telemetry/tracer'\nimport {\n MissingTemporalConstructorError,\n TransactionSerializer,\n} from './TransactionSerializer'\nimport type { OfflineTransaction, StorageAdapter } from '../types'\nimport type { Collection } from '@tanstack/db'\n\nexport class OutboxManager {\n private storage: StorageAdapter\n private serializer: TransactionSerializer\n private keyPrefix = `tx:`\n private removalRevision = 0\n private activeReads = new Set<{ revision: number }>()\n private removedAtRevision = new Map<string, number>()\n\n constructor(\n storage: StorageAdapter,\n\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.storage = storage\n this.serializer = new TransactionSerializer(collections)\n }\n\n private getStorageKey(id: string): string {\n return `${this.keyPrefix}${id}`\n }\n\n private recordRemovals(ids: Iterable<string>): void {\n this.removalRevision++\n if (this.activeReads.size === 0) return\n for (const id of ids) this.removedAtRevision.set(id, this.removalRevision)\n this.pruneRemovalHistory()\n }\n\n private pruneRemovalHistory(): void {\n if (this.activeReads.size === 0) {\n this.removedAtRevision.clear()\n return\n }\n let oldestRead = Number.POSITIVE_INFINITY\n for (const read of this.activeReads)\n oldestRead = Math.min(oldestRead, read.revision)\n for (const [id, revision] of this.removedAtRevision)\n if (revision <= oldestRead) this.removedAtRevision.delete(id)\n }\n\n async add(transaction: OfflineTransaction): Promise<void> {\n return withSpan(\n `outbox.add`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.keyCount': transaction.keys.length,\n },\n async () => {\n const key = this.getStorageKey(transaction.id)\n const serialized = this.serializer.serialize(transaction)\n await this.storage.set(key, serialized)\n },\n )\n }\n\n async get(id: string): Promise<OfflineTransaction | null> {\n return withSpan(`outbox.get`, { 'transaction.id': id }, async (span) => {\n const key = this.getStorageKey(id)\n const data = await this.storage.get(key)\n\n if (!data) {\n span.setAttribute(`result`, `not_found`)\n return null\n }\n\n try {\n const transaction = this.serializer.deserialize(data)\n span.setAttribute(`result`, `found`)\n return transaction\n } catch (error) {\n if (error instanceof MissingTemporalConstructorError) {\n error.message = `transaction ${id}: ${error.message}`\n throw error\n }\n console.warn(`Failed to deserialize transaction ${id}:`, error)\n span.setAttribute(`result`, `deserialize_error`)\n return null\n }\n })\n }\n\n async getAll(): Promise<Array<OfflineTransaction>> {\n return this.withAll((transactions) => transactions)\n }\n\n async withAll<T>(\n consume: (transactions: Array<OfflineTransaction>) => T,\n ): Promise<T> {\n const read = { revision: this.removalRevision }\n this.activeReads.add(read)\n try {\n return await withSpan(`outbox.getAll`, {}, async (span) => {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) =>\n key.startsWith(this.keyPrefix),\n )\n\n span.setAttribute(`transactionCount`, transactionKeys.length)\n\n const transactions: Array<OfflineTransaction> = []\n\n for (const key of transactionKeys) {\n const data = await this.storage.get(key)\n if (data) {\n try {\n const transaction = this.serializer.deserialize(data)\n transactions.push(transaction)\n } catch (error) {\n if (error instanceof MissingTemporalConstructorError) {\n error.message = `transaction ${key.slice(this.keyPrefix.length)}: ${error.message}`\n throw error\n }\n console.warn(\n `Failed to deserialize transaction from key ${key}:`,\n error,\n )\n }\n }\n }\n\n const currentTransactions = transactions\n .filter(\n ({ id }) => (this.removedAtRevision.get(id) ?? 0) <= read.revision,\n )\n .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())\n // Keep the read registered until replay admission completes. A durable\n // removal cannot land between filtering and this synchronous consumer.\n return consume(currentTransactions)\n })\n } finally {\n this.activeReads.delete(read)\n this.pruneRemovalHistory()\n }\n }\n\n async getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>> {\n const allTransactions = await this.getAll()\n const keySet = new Set(keys)\n\n return allTransactions.filter((transaction) =>\n transaction.keys.some((key) => keySet.has(key)),\n )\n }\n\n async update(\n id: string,\n updates: Partial<OfflineTransaction>,\n ): Promise<void> {\n return withSpan(`outbox.update`, { 'transaction.id': id }, async () => {\n const existing = await this.get(id)\n if (!existing) {\n throw new Error(`Transaction ${id} not found`)\n }\n\n const updated = { ...existing, ...updates }\n await this.add(updated)\n })\n }\n\n async remove(id: string): Promise<void> {\n return withSpan(`outbox.remove`, { 'transaction.id': id }, async () => {\n const key = this.getStorageKey(id)\n await this.storage.delete(key)\n this.recordRemovals([id])\n })\n }\n\n async removeMany(ids: Array<string>): Promise<void> {\n return withSpan(`outbox.removeMany`, { count: ids.length }, async () => {\n await Promise.all(ids.map((id) => this.remove(id)))\n })\n }\n\n async clear(): Promise<void> {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix))\n\n await this.removeMany(\n transactionKeys.map((key) => key.slice(this.keyPrefix.length)),\n )\n }\n\n async count(): Promise<number> {\n const keys = await this.storage.keys()\n return keys.filter((key) => key.startsWith(this.keyPrefix)).length\n }\n}\n"],"names":["TransactionSerializer","withSpan","MissingTemporalConstructorError"],"mappings":";;;;AAQO,MAAM,cAAc;AAAA,EAQzB,YACE,SAEA,aACA;AATF,SAAQ,YAAY;AACpB,SAAQ,kBAAkB;AAC1B,SAAQ,kCAAkB,IAAA;AAC1B,SAAQ,wCAAwB,IAAA;AAO9B,SAAK,UAAU;AACf,SAAK,aAAa,IAAIA,sBAAAA,sBAAsB,WAAW;AAAA,EACzD;AAAA,EAEQ,cAAc,IAAoB;AACxC,WAAO,GAAG,KAAK,SAAS,GAAG,EAAE;AAAA,EAC/B;AAAA,EAEQ,eAAe,KAA6B;AAClD,SAAK;AACL,QAAI,KAAK,YAAY,SAAS,EAAG;AACjC,eAAW,MAAM,IAAK,MAAK,kBAAkB,IAAI,IAAI,KAAK,eAAe;AACzE,SAAK,oBAAA;AAAA,EACP;AAAA,EAEQ,sBAA4B;AAClC,QAAI,KAAK,YAAY,SAAS,GAAG;AAC/B,WAAK,kBAAkB,MAAA;AACvB;AAAA,IACF;AACA,QAAI,aAAa,OAAO;AACxB,eAAW,QAAQ,KAAK;AACtB,mBAAa,KAAK,IAAI,YAAY,KAAK,QAAQ;AACjD,eAAW,CAAC,IAAI,QAAQ,KAAK,KAAK;AAChC,UAAI,YAAY,WAAY,MAAK,kBAAkB,OAAO,EAAE;AAAA,EAChE;AAAA,EAEA,MAAM,IAAI,aAAgD;AACxD,WAAOC,OAAAA;AAAAA,MACL;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,8BAA8B,YAAY;AAAA,QAC1C,wBAAwB,YAAY,KAAK;AAAA,MAAA;AAAA,MAE3C,YAAY;AACV,cAAM,MAAM,KAAK,cAAc,YAAY,EAAE;AAC7C,cAAM,aAAa,KAAK,WAAW,UAAU,WAAW;AACxD,cAAM,KAAK,QAAQ,IAAI,KAAK,UAAU;AAAA,MACxC;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAM,IAAI,IAAgD;AACxD,WAAOA,OAAAA,SAAS,cAAc,CAAuB,GAAG,OAAO,SAAS;AACtE,YAAM,MAAM,KAAK,cAAc,EAAE;AACjC,YAAM,OAAO,MAAM,KAAK,QAAQ,IAAI,GAAG;AAEvC,UAAI,CAAC,MAAM;AACT,aAAK,aAAa,UAAU,WAAW;AACvC,eAAO;AAAA,MACT;AAEA,UAAI;AACF,cAAM,cAAc,KAAK,WAAW,YAAY,IAAI;AACpD,aAAK,aAAa,UAAU,OAAO;AACnC,eAAO;AAAA,MACT,SAAS,OAAO;AACd,YAAI,iBAAiBC,sBAAAA,iCAAiC;AACpD,gBAAM,UAAU,eAAe,EAAE,KAAK,MAAM,OAAO;AACnD,gBAAM;AAAA,QACR;AACA,gBAAQ,KAAK,qCAAqC,EAAE,KAAK,KAAK;AAC9D,aAAK,aAAa,UAAU,mBAAmB;AAC/C,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,SAA6C;AACjD,WAAO,KAAK,QAAQ,CAAC,iBAAiB,YAAY;AAAA,EACpD;AAAA,EAEA,MAAM,QACJ,SACY;AACZ,UAAM,OAAO,EAAE,UAAU,KAAK,gBAAA;AAC9B,SAAK,YAAY,IAAI,IAAI;AACzB,QAAI;AACF,aAAO,MAAMD,OAAAA,SAAS,iBAAiB,CAAA,GAAI,OAAO,SAAS;AACzD,cAAM,OAAO,MAAM,KAAK,QAAQ,KAAA;AAChC,cAAM,kBAAkB,KAAK;AAAA,UAAO,CAAC,QACnC,IAAI,WAAW,KAAK,SAAS;AAAA,QAAA;AAG/B,aAAK,aAAa,oBAAoB,gBAAgB,MAAM;AAE5D,cAAM,eAA0C,CAAA;AAEhD,mBAAW,OAAO,iBAAiB;AACjC,gBAAM,OAAO,MAAM,KAAK,QAAQ,IAAI,GAAG;AACvC,cAAI,MAAM;AACR,gBAAI;AACF,oBAAM,cAAc,KAAK,WAAW,YAAY,IAAI;AACpD,2BAAa,KAAK,WAAW;AAAA,YAC/B,SAAS,OAAO;AACd,kBAAI,iBAAiBC,sBAAAA,iCAAiC;AACpD,sBAAM,UAAU,eAAe,IAAI,MAAM,KAAK,UAAU,MAAM,CAAC,KAAK,MAAM,OAAO;AACjF,sBAAM;AAAA,cACR;AACA,sBAAQ;AAAA,gBACN,8CAA8C,GAAG;AAAA,gBACjD;AAAA,cAAA;AAAA,YAEJ;AAAA,UACF;AAAA,QACF;AAEA,cAAM,sBAAsB,aACzB;AAAA,UACC,CAAC,EAAE,UAAU,KAAK,kBAAkB,IAAI,EAAE,KAAK,MAAM,KAAK;AAAA,QAAA,EAE3D,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA,CAAS;AAG/D,eAAO,QAAQ,mBAAmB;AAAA,MACpC,CAAC;AAAA,IACH,UAAA;AACE,WAAK,YAAY,OAAO,IAAI;AAC5B,WAAK,oBAAA;AAAA,IACP;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,MAAyD;AACvE,UAAM,kBAAkB,MAAM,KAAK,OAAA;AACnC,UAAM,SAAS,IAAI,IAAI,IAAI;AAE3B,WAAO,gBAAgB;AAAA,MAAO,CAAC,gBAC7B,YAAY,KAAK,KAAK,CAAC,QAAQ,OAAO,IAAI,GAAG,CAAC;AAAA,IAAA;AAAA,EAElD;AAAA,EAEA,MAAM,OACJ,IACA,SACe;AACf,WAAOD,OAAAA,SAAS,iBAAiB,CAAuB,GAAG,YAAY;AACrE,YAAM,WAAW,MAAM,KAAK,IAAI,EAAE;AAClC,UAAI,CAAC,UAAU;AACb,cAAM,IAAI,MAAM,eAAe,EAAE,YAAY;AAAA,MAC/C;AAEA,YAAM,UAAU,EAAE,GAAG,UAAU,GAAG,QAAA;AAClC,YAAM,KAAK,IAAI,OAAO;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,IAA2B;AACtC,WAAOA,OAAAA,SAAS,iBAAiB,CAAuB,GAAG,YAAY;AACrE,YAAM,MAAM,KAAK,cAAc,EAAE;AACjC,YAAM,KAAK,QAAQ,OAAO,GAAG;AAC7B,WAAK,eAAe,CAAC,EAAE,CAAC;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,WAAW,KAAmC;AAClD,WAAOA,OAAAA,SAAS,qBAAqB,EAAE,OAAO,IAAI,OAAA,GAAU,YAAY;AACtE,YAAM,QAAQ,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC,CAAC;AAAA,IACpD,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,OAAO,MAAM,KAAK,QAAQ,KAAA;AAChC,UAAM,kBAAkB,KAAK,OAAO,CAAC,QAAQ,IAAI,WAAW,KAAK,SAAS,CAAC;AAE3E,UAAM,KAAK;AAAA,MACT,gBAAgB,IAAI,CAAC,QAAQ,IAAI,MAAM,KAAK,UAAU,MAAM,CAAC;AAAA,IAAA;AAAA,EAEjE;AAAA,EAEA,MAAM,QAAyB;AAC7B,UAAM,OAAO,MAAM,KAAK,QAAQ,KAAA;AAChC,WAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,WAAW,KAAK,SAAS,CAAC,EAAE;AAAA,EAC9D;AACF;;"}
@@ -4,11 +4,17 @@ export declare class OutboxManager {
4
4
  private storage;
5
5
  private serializer;
6
6
  private keyPrefix;
7
+ private removalRevision;
8
+ private activeReads;
9
+ private removedAtRevision;
7
10
  constructor(storage: StorageAdapter, collections: Record<string, Collection<any, any, any, any, any>>);
8
11
  private getStorageKey;
12
+ private recordRemovals;
13
+ private pruneRemovalHistory;
9
14
  add(transaction: OfflineTransaction): Promise<void>;
10
15
  get(id: string): Promise<OfflineTransaction | null>;
11
16
  getAll(): Promise<Array<OfflineTransaction>>;
17
+ withAll<T>(consume: (transactions: Array<OfflineTransaction>) => T): Promise<T>;
12
18
  getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>>;
13
19
  update(id: string, updates: Partial<OfflineTransaction>): Promise<void>;
14
20
  remove(id: string): Promise<void>;