@tanstack/offline-transactions 1.0.65 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +81 -2
  2. package/dist/cjs/OfflineExecutor.cjs +74 -93
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/OfflineExecutor.d.cts +6 -0
  5. package/dist/cjs/api/OfflineAction.cjs +4 -15
  6. package/dist/cjs/api/OfflineAction.cjs.map +1 -1
  7. package/dist/cjs/api/OfflineTransaction.cjs +54 -51
  8. package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
  9. package/dist/cjs/api/OfflineTransaction.d.cts +2 -0
  10. package/dist/cjs/executor/KeyScheduler.cjs +12 -38
  11. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
  12. package/dist/cjs/executor/TransactionExecutor.cjs +119 -134
  13. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
  14. package/dist/cjs/outbox/OutboxManager.cjs +48 -77
  15. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
  16. package/dist/cjs/types.cjs.map +1 -1
  17. package/dist/cjs/types.d.cts +10 -0
  18. package/dist/esm/OfflineExecutor.d.ts +6 -0
  19. package/dist/esm/OfflineExecutor.js +74 -93
  20. package/dist/esm/OfflineExecutor.js.map +1 -1
  21. package/dist/esm/api/OfflineAction.js +4 -15
  22. package/dist/esm/api/OfflineAction.js.map +1 -1
  23. package/dist/esm/api/OfflineTransaction.d.ts +2 -0
  24. package/dist/esm/api/OfflineTransaction.js +54 -51
  25. package/dist/esm/api/OfflineTransaction.js.map +1 -1
  26. package/dist/esm/executor/KeyScheduler.js +12 -38
  27. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  28. package/dist/esm/executor/TransactionExecutor.js +120 -135
  29. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  30. package/dist/esm/outbox/OutboxManager.js +48 -77
  31. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  32. package/dist/esm/types.d.ts +10 -0
  33. package/dist/esm/types.js.map +1 -1
  34. package/package.json +2 -2
  35. package/skills/offline/SKILL.md +73 -5
  36. package/src/OfflineExecutor.ts +97 -113
  37. package/src/api/OfflineAction.ts +4 -22
  38. package/src/api/OfflineTransaction.ts +78 -64
  39. package/src/executor/KeyScheduler.ts +18 -47
  40. package/src/executor/TransactionExecutor.ts +148 -162
  41. package/src/outbox/OutboxManager.ts +54 -86
  42. package/src/types.ts +10 -0
  43. package/dist/cjs/retry/NonRetriableError.d.cts +0 -1
  44. package/dist/cjs/telemetry/tracer.cjs +0 -27
  45. package/dist/cjs/telemetry/tracer.cjs.map +0 -1
  46. package/dist/cjs/telemetry/tracer.d.cts +0 -30
  47. package/dist/esm/retry/NonRetriableError.d.ts +0 -1
  48. package/dist/esm/telemetry/tracer.d.ts +0 -30
  49. package/dist/esm/telemetry/tracer.js +0 -27
  50. package/dist/esm/telemetry/tracer.js.map +0 -1
  51. package/src/retry/NonRetriableError.ts +0 -1
  52. package/src/telemetry/tracer.ts +0 -66
@@ -1,9 +1,11 @@
1
- import { createTransaction } from "@tanstack/db";
1
+ import { createTransaction, DuplicateTransactionIdError } from "@tanstack/db";
2
2
  import { OutboxTransactionNotFoundError } from "../outbox/OutboxManager.js";
3
3
  import { DefaultRetryPolicy } from "../retry/RetryPolicy.js";
4
4
  import { NonRetriableError } from "../types.js";
5
- import { withNestedSpan } from "../telemetry/tracer.js";
6
5
  const HANDLED_EXECUTION_ERROR = /* @__PURE__ */ Symbol(`HandledExecutionError`);
6
+ function toError(value) {
7
+ return value instanceof Error || Object.prototype.toString.call(value) === `[object Error]` ? value : new Error(String(value));
8
+ }
7
9
  class TransactionExecutor {
8
10
  constructor(scheduler, outbox, config, offlineExecutor) {
9
11
  this.isExecuting = false;
@@ -53,61 +55,39 @@ class TransactionExecutor {
53
55
  }
54
56
  async executeTransaction(transaction) {
55
57
  try {
56
- await withNestedSpan(
57
- `transaction.execute`,
58
- {
59
- "transaction.id": transaction.id,
60
- "transaction.mutationFnName": transaction.mutationFnName,
61
- "transaction.retryCount": transaction.retryCount,
62
- "transaction.keyCount": transaction.keys.length
63
- },
64
- async (span) => {
65
- const terminalError = transaction.outboxPhase === `rejection-pending` ? this.getTerminalError(transaction) : void 0;
66
- this.scheduler.markStarted(transaction);
67
- if (transaction.retryCount > 0) {
68
- span.setAttribute(`retry.attempt`, transaction.retryCount);
69
- }
70
- if (!transaction.outboxPhase) {
71
- try {
72
- await this.runMutationFn(transaction);
73
- } catch (error) {
74
- const err = error instanceof Error ? error : new Error(String(error));
75
- span.setAttribute(`result`, `error`);
76
- await this.handleError(transaction, err);
77
- err[HANDLED_EXECUTION_ERROR] = true;
78
- throw err;
79
- }
80
- }
81
- const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
82
- try {
83
- await this.removeSettledTransaction(
84
- deletionPending,
85
- !transaction.outboxPhase
86
- );
87
- } catch (error) {
88
- const storageError = error;
89
- span.recordException(storageError);
90
- span.setAttribute(`result`, `outbox_failure`);
91
- this.offlineExecutor.rejectTransaction(
92
- transaction.id,
93
- terminalError ?? storageError
94
- );
95
- throw storageError;
96
- }
97
- if (terminalError) {
98
- span.setAttribute(`result`, `permanent_failure`);
99
- this.offlineExecutor.rejectTransaction(
100
- transaction.id,
101
- terminalError
102
- );
103
- } else {
104
- span.setAttribute(`result`, `success`);
105
- this.offlineExecutor.resolveTransaction(transaction.id, void 0);
106
- }
58
+ const terminalError = transaction.outboxPhase === `rejection-pending` ? this.getTerminalError(transaction) : void 0;
59
+ this.scheduler.markStarted(transaction);
60
+ if (!transaction.outboxPhase) {
61
+ try {
62
+ await this.runMutationFn(transaction);
63
+ } catch (error) {
64
+ const err = toError(error);
65
+ await this.handleError(transaction, err);
66
+ err[HANDLED_EXECUTION_ERROR] = true;
67
+ throw err;
107
68
  }
108
- );
69
+ }
70
+ const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
71
+ try {
72
+ await this.removeSettledTransaction(
73
+ deletionPending,
74
+ !transaction.outboxPhase
75
+ );
76
+ } catch (error) {
77
+ const storageError = error;
78
+ this.offlineExecutor.rejectTransaction(
79
+ transaction.id,
80
+ terminalError ?? storageError
81
+ );
82
+ throw storageError;
83
+ }
84
+ if (terminalError) {
85
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
86
+ } else {
87
+ this.offlineExecutor.resolveTransaction(transaction.id, void 0);
88
+ }
109
89
  } catch (error) {
110
- if (error instanceof Error && error[HANDLED_EXECUTION_ERROR] === true) {
90
+ if (error !== null && typeof error === `object` && error[HANDLED_EXECUTION_ERROR] === true) {
111
91
  return;
112
92
  }
113
93
  throw error;
@@ -133,74 +113,87 @@ class TransactionExecutor {
133
113
  });
134
114
  }
135
115
  async handleError(transaction, error) {
136
- return withNestedSpan(
137
- `transaction.handleError`,
138
- {
139
- "transaction.id": transaction.id,
140
- "error.name": error.name,
141
- "error.message": error.message
142
- },
143
- async (span) => {
144
- const shouldRetry = this.retryPolicy.shouldRetry(
145
- error,
146
- transaction.retryCount
147
- );
148
- span.setAttribute(`shouldRetry`, shouldRetry);
149
- if (!shouldRetry) {
150
- const rejectionPending = {
151
- ...transaction,
152
- outboxPhase: `rejection-pending`,
153
- lastError: {
154
- name: error.name,
155
- message: error.message,
156
- stack: error.stack
157
- }
158
- };
159
- console.warn(
160
- `Transaction ${transaction.id} failed permanently:`,
161
- error
116
+ let shouldRetry;
117
+ let hookFailure;
118
+ if (error instanceof NonRetriableError) {
119
+ shouldRetry = false;
120
+ } else {
121
+ let decision;
122
+ try {
123
+ decision = this.config.shouldRetry?.(error, transaction.retryCount);
124
+ if (decision !== void 0 && typeof decision !== `boolean`) {
125
+ void Promise.resolve(decision).catch(() => {
126
+ });
127
+ throw new TypeError(
128
+ `OfflineConfig.shouldRetry must return true, false, or undefined`
162
129
  );
163
- try {
164
- await this.removeSettledTransaction(rejectionPending, true);
165
- } catch (storageError) {
166
- span.recordException(storageError);
167
- span.setAttribute(`result`, `outbox_failure`);
168
- this.offlineExecutor.rejectTransaction(transaction.id, error);
169
- throw storageError;
170
- }
171
- span.setAttribute(`result`, `permanent_failure`);
172
- this.offlineExecutor.rejectTransaction(transaction.id, error);
173
- return;
174
130
  }
175
- const delay = Math.max(
176
- 0,
177
- this.retryPolicy.calculateDelay(transaction.retryCount)
178
- );
179
- const updatedTransaction = {
180
- ...transaction,
181
- retryCount: transaction.retryCount + 1,
182
- nextAttemptAt: Date.now() + delay,
183
- lastError: {
184
- name: error.name,
185
- message: error.message,
186
- stack: error.stack
187
- }
188
- };
189
- span.setAttribute(`retryDelay`, delay);
190
- span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount);
191
- this.scheduler.updateTransaction(updatedTransaction);
192
- try {
193
- await this.outbox.update(transaction.id, updatedTransaction);
194
- span.setAttribute(`result`, `scheduled_retry`);
195
- } catch (persistError) {
196
- span.recordException(persistError);
197
- span.setAttribute(`result`, `persist_failed`);
198
- throw persistError;
199
- } finally {
200
- this.scheduler.markFailed(transaction);
131
+ } catch (hookError) {
132
+ hookFailure = toError(hookError);
133
+ }
134
+ if (hookFailure) {
135
+ shouldRetry = false;
136
+ } else {
137
+ shouldRetry = decision ?? this.retryPolicy.shouldRetry(error, transaction.retryCount);
138
+ }
139
+ }
140
+ if (!shouldRetry) {
141
+ const terminalError = hookFailure ?? error;
142
+ const rejectionPending = {
143
+ ...transaction,
144
+ outboxPhase: `rejection-pending`,
145
+ lastError: {
146
+ name: terminalError.name,
147
+ message: terminalError.message,
148
+ stack: terminalError.stack
201
149
  }
150
+ };
151
+ if (hookFailure) {
152
+ console.warn(
153
+ `Retry decision failed for transaction ${transaction.id}:`,
154
+ hookFailure,
155
+ `Named mutation function error:`,
156
+ error
157
+ );
158
+ } else {
159
+ console.warn(`Transaction ${transaction.id} failed permanently:`, error);
202
160
  }
161
+ try {
162
+ await this.removeSettledTransaction(rejectionPending, true);
163
+ } catch (storageError) {
164
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
165
+ throw storageError;
166
+ }
167
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
168
+ return;
169
+ }
170
+ const delay = Math.max(
171
+ 0,
172
+ this.retryPolicy.calculateDelay(transaction.retryCount)
203
173
  );
174
+ const updatedTransaction = {
175
+ ...transaction,
176
+ retryCount: transaction.retryCount + 1,
177
+ nextAttemptAt: Date.now() + delay,
178
+ lastError: {
179
+ name: error.name,
180
+ message: error.message,
181
+ stack: error.stack
182
+ }
183
+ };
184
+ this.scheduler.updateTransaction(updatedTransaction);
185
+ try {
186
+ await this.outbox.update(transaction.id, updatedTransaction);
187
+ } catch (persistError) {
188
+ if (persistError instanceof OutboxTransactionNotFoundError) {
189
+ this.scheduler.markCompleted(transaction);
190
+ this.offlineExecutor.rejectTransaction(transaction.id, error);
191
+ return;
192
+ }
193
+ throw persistError;
194
+ } finally {
195
+ this.scheduler.markFailed(transaction);
196
+ }
204
197
  }
205
198
  async removeSettledTransaction(transaction, persistMarker) {
206
199
  try {
@@ -214,7 +207,7 @@ class TransactionExecutor {
214
207
  await this.outbox.remove(transaction.id);
215
208
  this.scheduler.markCompleted(transaction);
216
209
  } catch (error) {
217
- const storageError = error instanceof Error ? error : new Error(String(error));
210
+ const storageError = toError(error);
218
211
  this.fatalError = storageError;
219
212
  this.scheduler.markFailed(transaction);
220
213
  this.clearRetryTimer();
@@ -297,30 +290,22 @@ class TransactionExecutor {
297
290
  mutationFn: async () => {
298
291
  }
299
292
  });
300
- restorationTx.isPersisted.promise.catch(() => {
301
- });
302
293
  restorationTx.applyMutations(offlineTx.mutations);
303
- const touchedCollections = /* @__PURE__ */ new Set();
304
- for (const mutation of offlineTx.mutations) {
305
- if (!mutation.collection) {
306
- continue;
307
- }
308
- const collectionId = mutation.collection.id;
309
- if (touchedCollections.has(collectionId)) {
310
- continue;
294
+ try {
295
+ for (const mutation of offlineTx.mutations) {
296
+ if (mutation.collection._state.trackTransaction(restorationTx))
297
+ mutation.collection._state.recomputeOptimisticState(true);
311
298
  }
312
- touchedCollections.add(collectionId);
313
- mutation.collection._state.transactions.set(
314
- restorationTx.id,
315
- restorationTx
316
- );
317
- mutation.collection._state.recomputeOptimisticState(true);
299
+ } catch (error) {
300
+ restorationTx.rollback({ isSecondaryRollback: true });
301
+ throw error;
318
302
  }
319
303
  this.offlineExecutor.registerRestorationTransaction(
320
304
  offlineTx.id,
321
305
  restorationTx
322
306
  );
323
307
  } catch (error) {
308
+ if (error instanceof DuplicateTransactionIdError) continue;
324
309
  console.warn(
325
310
  `Failed to restore optimistic state for transaction ${offlineTx.id}:`,
326
311
  error
@@ -1 +1 @@
1
- {"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { createTransaction } from '@tanstack/db'\nimport { OutboxTransactionNotFoundError } from '../outbox/OutboxManager'\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 private fatalError: Error | 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.assertHealthy()\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n this.assertHealthy()\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 const terminalError =\n transaction.outboxPhase === `rejection-pending`\n ? this.getTerminalError(transaction)\n : undefined\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n if (!transaction.outboxPhase) {\n try {\n 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\n // A successful provider call can outlive this executor. Persist that\n // fact before removal so a restarted executor only retries deletion.\n const deletionPending: OfflineTransaction = transaction.outboxPhase\n ? transaction\n : { ...transaction, outboxPhase: `deletion-pending` }\n try {\n await this.removeSettledTransaction(\n deletionPending,\n !transaction.outboxPhase,\n )\n } catch (error) {\n const storageError = error as Error\n span.recordException(storageError)\n span.setAttribute(`result`, `outbox_failure`)\n this.offlineExecutor.rejectTransaction(\n transaction.id,\n terminalError ?? storageError,\n )\n throw storageError\n }\n\n if (terminalError) {\n span.setAttribute(`result`, `permanent_failure`)\n this.offlineExecutor.rejectTransaction(\n transaction.id,\n terminalError,\n )\n } else {\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, undefined)\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 const rejectionPending: OfflineTransaction = {\n ...transaction,\n outboxPhase: `rejection-pending`,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n try {\n await this.removeSettledTransaction(rejectionPending, true)\n } catch (storageError) {\n span.recordException(storageError as Error)\n span.setAttribute(`result`, `outbox_failure`)\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n throw storageError\n }\n\n span.setAttribute(`result`, `permanent_failure`)\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.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 private async removeSettledTransaction(\n transaction: OfflineTransaction,\n persistMarker: boolean,\n ): Promise<void> {\n try {\n if (persistMarker) {\n try {\n await this.outbox.update(transaction.id, transaction)\n } catch (error) {\n // A public removal can finish while the provider is still running.\n if (!(error instanceof OutboxTransactionNotFoundError)) throw error\n }\n }\n await this.outbox.remove(transaction.id)\n this.scheduler.markCompleted(transaction)\n } catch (error) {\n const storageError =\n error instanceof Error ? error : new Error(String(error))\n this.fatalError = storageError\n this.scheduler.markFailed(transaction)\n this.clearRetryTimer()\n throw storageError\n }\n }\n\n assertHealthy(): void {\n if (this.fatalError) throw this.fatalError\n }\n\n private getTerminalError(transaction: OfflineTransaction): Error {\n if (!transaction.lastError)\n throw new Error(`Terminal transaction ${transaction.id} has no error`)\n const { name, message, stack } = transaction.lastError\n const error =\n name === `NonRetriableError`\n ? new NonRetriableError(message)\n : new Error(message)\n error.name = name\n error.stack = stack\n return error\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 const providerPending = transactions.filter(\n (transaction) => !transaction.outboxPhase,\n )\n const selectedProvider = this.config.beforeRetry\n ? this.config.beforeRetry(providerPending)\n : providerPending\n const selectedById = new Map(selectedProvider.map((tx) => [tx.id, tx]))\n const filteredTransactions = transactions.flatMap((transaction) => {\n if (transaction.outboxPhase) return [transaction]\n const selected = selectedById.get(transaction.id)\n return selected ? [selected] : []\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 removedIds = providerPending\n .filter(({ id }) => !selectedById.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(\n newlyLoaded.filter(\n (transaction) => transaction.outboxPhase !== `rejection-pending`,\n ),\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\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.fatalError || !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":[],"mappings":";;;;;AAaA,MAAM,iDAAiC,uBAAuB;AAEvD,MAAM,oBAAoB;AAAA,EAW/B,YACE,WACA,QACA,QACA,iBACA;AAXF,SAAQ,cAAc;AACtB,SAAQ,mBAAyC;AAEjD,SAAQ,aAAmD;AAC3D,SAAQ,aAA2B;AAQjC,SAAK,YAAY;AACjB,SAAK,SAAS;AACd,SAAK,SAAS;AACd,SAAK,cAAc,IAAI;AAAA,MACrB,OAAO;AAAA,MACP,OAAO,UAAU;AAAA,IAAA;AAEnB,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,MAAM,QAAQ,aAAgD;AAC5D,SAAK,cAAA;AACL,SAAK,UAAU,SAAS,WAAW;AACnC,UAAM,KAAK,WAAA;AAAA,EACb;AAAA,EAEA,MAAM,aAA4B;AAChC,SAAK,cAAA;AACL,QAAI,KAAK,aAAa;AACpB,aAAO,KAAK;AAAA,IACd;AAEA,SAAK,cAAc;AACnB,SAAK,mBAAmB,KAAK,aAAA;AAE7B,QAAI;AACF,YAAM,KAAK;AAAA,IACb,UAAA;AACE,WAAK,cAAc;AACnB,WAAK,mBAAmB;AACxB,WAAK,kBAAA;AAAA,IACP;AAAA,EACF;AAAA,EAEA,MAAc,eAA8B;AAC1C,WAAO,KAAK,UAAU,gBAAA,IAAoB,GAAG;AAC3C,UAAI,CAAC,KAAK,cAAc;AACtB;AAAA,MACF;AAEA,YAAM,cAAc,KAAK,UAAU,QAAA;AAEnC,UAAI,CAAC,aAAa;AAChB;AAAA,MACF;AAEA,YAAM,KAAK,mBAAmB,WAAW;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAc,mBACZ,aACe;AACf,QAAI;AACF,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,UACE,kBAAkB,YAAY;AAAA,UAC9B,8BAA8B,YAAY;AAAA,UAC1C,0BAA0B,YAAY;AAAA,UACtC,wBAAwB,YAAY,KAAK;AAAA,QAAA;AAAA,QAE3C,OAAO,SAAS;AACd,gBAAM,gBACJ,YAAY,gBAAgB,sBACxB,KAAK,iBAAiB,WAAW,IACjC;AACN,eAAK,UAAU,YAAY,WAAW;AAEtC,cAAI,YAAY,aAAa,GAAG;AAC9B,iBAAK,aAAa,iBAAiB,YAAY,UAAU;AAAA,UAC3D;AAEA,cAAI,CAAC,YAAY,aAAa;AAC5B,gBAAI;AACF,oBAAM,KAAK,cAAc,WAAW;AAAA,YACtC,SAAS,OAAO;AACd,oBAAM,MACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAE1D,mBAAK,aAAa,UAAU,OAAO;AAEnC,oBAAM,KAAK,YAAY,aAAa,GAAG;AACrC,kBAAY,uBAAuB,IAAI;AACzC,oBAAM;AAAA,YACR;AAAA,UACF;AAIA,gBAAM,kBAAsC,YAAY,cACpD,cACA,EAAE,GAAG,aAAa,aAAa,mBAAA;AACnC,cAAI;AACF,kBAAM,KAAK;AAAA,cACT;AAAA,cACA,CAAC,YAAY;AAAA,YAAA;AAAA,UAEjB,SAAS,OAAO;AACd,kBAAM,eAAe;AACrB,iBAAK,gBAAgB,YAAY;AACjC,iBAAK,aAAa,UAAU,gBAAgB;AAC5C,iBAAK,gBAAgB;AAAA,cACnB,YAAY;AAAA,cACZ,iBAAiB;AAAA,YAAA;AAEnB,kBAAM;AAAA,UACR;AAEA,cAAI,eAAe;AACjB,iBAAK,aAAa,UAAU,mBAAmB;AAC/C,iBAAK,gBAAgB;AAAA,cACnB,YAAY;AAAA,cACZ;AAAA,YAAA;AAAA,UAEJ,OAAO;AACL,iBAAK,aAAa,UAAU,SAAS;AACrC,iBAAK,gBAAgB,mBAAmB,YAAY,IAAI,MAAS;AAAA,UACnE;AAAA,QACF;AAAA,MAAA;AAAA,IAEJ,SAAS,OAAO;AACd,UACE,iBAAiB,SAChB,MAAc,uBAAuB,MAAM,MAC5C;AACA;AAAA,MACF;AAEA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAc,cAAc,aAAgD;AAC1E,UAAM,aAAa,KAAK,OAAO,YAAY,YAAY,cAAc;AAErE,QAAI,CAAC,YAAY;AACf,YAAM,eAAe,8BAA8B,YAAY,cAAc;AAE7E,UAAI,KAAK,OAAO,qBAAqB;AACnC,aAAK,OAAO,oBAAoB,YAAY,gBAAgB,WAAW;AAAA,MACzE;AAEA,YAAM,IAAI,kBAAkB,YAAY;AAAA,IAC1C;AAIA,UAAM,2BAA2B;AAAA,MAC/B,IAAI,YAAY;AAAA,MAChB,WAAW,YAAY;AAAA,MACvB,UAAU,YAAY,YAAY,CAAA;AAAA,IAAC;AAGrC,UAAM,WAAW;AAAA,MACf,aAAa;AAAA,MACb,gBAAgB,YAAY;AAAA,IAAA,CAC7B;AAAA,EACH;AAAA,EAEA,MAAc,YACZ,aACA,OACe;AACf,WAAO;AAAA,MACL;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,cAAc,MAAM;AAAA,QACpB,iBAAiB,MAAM;AAAA,MAAA;AAAA,MAEzB,OAAO,SAAS;AACd,cAAM,cAAc,KAAK,YAAY;AAAA,UACnC;AAAA,UACA,YAAY;AAAA,QAAA;AAGd,aAAK,aAAa,eAAe,WAAW;AAE5C,YAAI,CAAC,aAAa;AAChB,gBAAM,mBAAuC;AAAA,YAC3C,GAAG;AAAA,YACH,aAAa;AAAA,YACb,WAAW;AAAA,cACT,MAAM,MAAM;AAAA,cACZ,SAAS,MAAM;AAAA,cACf,OAAO,MAAM;AAAA,YAAA;AAAA,UACf;AAEF,kBAAQ;AAAA,YACN,eAAe,YAAY,EAAE;AAAA,YAC7B;AAAA,UAAA;AAEF,cAAI;AACF,kBAAM,KAAK,yBAAyB,kBAAkB,IAAI;AAAA,UAC5D,SAAS,cAAc;AACrB,iBAAK,gBAAgB,YAAqB;AAC1C,iBAAK,aAAa,UAAU,gBAAgB;AAC5C,iBAAK,gBAAgB,kBAAkB,YAAY,IAAI,KAAK;AAC5D,kBAAM;AAAA,UACR;AAEA,eAAK,aAAa,UAAU,mBAAmB;AAC/C,eAAK,gBAAgB,kBAAkB,YAAY,IAAI,KAAK;AAC5D;AAAA,QACF;AAEA,cAAM,QAAQ,KAAK;AAAA,UACjB;AAAA,UACA,KAAK,YAAY,eAAe,YAAY,UAAU;AAAA,QAAA;AAExD,cAAM,qBAAyC;AAAA,UAC7C,GAAG;AAAA,UACH,YAAY,YAAY,aAAa;AAAA,UACrC,eAAe,KAAK,IAAA,IAAQ;AAAA,UAC5B,WAAW;AAAA,YACT,MAAM,MAAM;AAAA,YACZ,SAAS,MAAM;AAAA,YACf,OAAO,MAAM;AAAA,UAAA;AAAA,QACf;AAGF,aAAK,aAAa,cAAc,KAAK;AACrC,aAAK,aAAa,kBAAkB,mBAAmB,UAAU;AAEjE,aAAK,UAAU,kBAAkB,kBAAkB;AAEnD,YAAI;AACF,gBAAM,KAAK,OAAO,OAAO,YAAY,IAAI,kBAAkB;AAC3D,eAAK,aAAa,UAAU,iBAAiB;AAAA,QAC/C,SAAS,cAAc;AACrB,eAAK,gBAAgB,YAAqB;AAC1C,eAAK,aAAa,UAAU,gBAAgB;AAC5C,gBAAM;AAAA,QACR,UAAA;AACE,eAAK,UAAU,WAAW,WAAW;AAAA,QACvC;AAAA,MACF;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAc,yBACZ,aACA,eACe;AACf,QAAI;AACF,UAAI,eAAe;AACjB,YAAI;AACF,gBAAM,KAAK,OAAO,OAAO,YAAY,IAAI,WAAW;AAAA,QACtD,SAAS,OAAO;AAEd,cAAI,EAAE,iBAAiB,gCAAiC,OAAM;AAAA,QAChE;AAAA,MACF;AACA,YAAM,KAAK,OAAO,OAAO,YAAY,EAAE;AACvC,WAAK,UAAU,cAAc,WAAW;AAAA,IAC1C,SAAS,OAAO;AACd,YAAM,eACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,WAAK,aAAa;AAClB,WAAK,UAAU,WAAW,WAAW;AACrC,WAAK,gBAAA;AACL,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,gBAAsB;AACpB,QAAI,KAAK,WAAY,OAAM,KAAK;AAAA,EAClC;AAAA,EAEQ,iBAAiB,aAAwC;AAC/D,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,wBAAwB,YAAY,EAAE,eAAe;AACvE,UAAM,EAAE,MAAM,SAAS,MAAA,IAAU,YAAY;AAC7C,UAAM,QACJ,SAAS,sBACL,IAAI,kBAAkB,OAAO,IAC7B,IAAI,MAAM,OAAO;AACvB,UAAM,OAAO;AACb,UAAM,QAAQ;AACd,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,0BAAyC;AAC7C,QAAI,aAA4B,CAAA;AAChC,UAAM,KAAK,OAAO,QAAQ,CAAC,iBAAiB;AAC1C,YAAM,EAAE,qBAAqB,KAAK;AAClC,UAAI,CAAC,iBAAkB;AACvB,YAAM,kBAAkB,aAAa;AAAA,QACnC,CAAC,gBAAgB,CAAC,YAAY;AAAA,MAAA;AAEhC,YAAM,mBAAmB,KAAK,OAAO,cACjC,KAAK,OAAO,YAAY,eAAe,IACvC;AACJ,YAAM,eAAe,IAAI,IAAI,iBAAiB,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;AACtE,YAAM,uBAAuB,aAAa,QAAQ,CAAC,gBAAgB;AACjE,YAAI,YAAY,YAAa,QAAO,CAAC,WAAW;AAChD,cAAM,WAAW,aAAa,IAAI,YAAY,EAAE;AAChD,eAAO,WAAW,CAAC,QAAQ,IAAI,CAAA;AAAA,MACjC,CAAC;AAGD,UAAI,CAAC,KAAK,gBAAgB,iBAAkB;AAE5C,YAAM,cAAc,qBAAqB;AAAA,QAAO,CAAC,gBAC/C,KAAK,UAAU,SAAS,WAAW;AAAA,MAAA;AAGrC,mBAAa,gBACV,OAAO,CAAC,EAAE,GAAA,MAAS,CAAC,aAAa,IAAI,EAAE,CAAC,EACxC,IAAI,CAAC,EAAE,GAAA,MAAS,EAAE;AACrB,mBAAa,KAAK,UAAU,0BAA0B,UAAU;AAIhE,WAAK;AAAA,QACH,YAAY;AAAA,UACV,CAAC,gBAAgB,YAAY,gBAAgB;AAAA,QAAA;AAAA,MAC/C;AAIF,WAAK,iBAAA;AAGL,WAAK,kBAAA;AAAA,IACP,CAAC;AAED,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,QAAQ,IAAI,kBAAkB,qCAAqC;AACzE,YAAM,QAAQ;AAAA,QACZ,WAAW,IAAI,OAAO,OAAO;AAC3B,cAAI;AACF,kBAAM,KAAK,OAAO,OAAO,EAAE;AAC3B,iBAAK,gBAAgB,kBAAkB,IAAI,KAAK;AAAA,UAClD,SAAS,cAAc;AACrB,oBAAQ;AAAA,cACN;AAAA,cACA;AAAA,cACA;AAAA,YAAA;AAAA,UAEJ;AAAA,QACF,CAAC;AAAA,MAAA;AAAA,IAEL;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,uBACN,cACM;AACN,eAAW,aAAa,cAAc;AACpC,UAAI,UAAU,UAAU,WAAW,GAAG;AACpC;AAAA,MACF;AAEA,UAAI;AAGF,cAAM,gBAAgB,kBAAkB;AAAA,UACtC,IAAI,UAAU;AAAA,UACd,YAAY;AAAA,UACZ,YAAY,YAAY;AAAA,UAAC;AAAA,QAAA,CAC1B;AAID,sBAAc,YAAY,QAAQ,MAAM,MAAM;AAAA,QAG9C,CAAC;AAED,sBAAc,eAAe,UAAU,SAAS;AAGhD,cAAM,yCAAyB,IAAA;AAC/B,mBAAW,YAAY,UAAU,WAAW;AAG1C,cAAI,CAAC,SAAS,YAAY;AACxB;AAAA,UACF;AACA,gBAAM,eAAe,SAAS,WAAW;AACzC,cAAI,mBAAmB,IAAI,YAAY,GAAG;AACxC;AAAA,UACF;AACA,6BAAmB,IAAI,YAAY;AAEnC,mBAAS,WAAW,OAAO,aAAa;AAAA,YACtC,cAAc;AAAA,YACd;AAAA,UAAA;AAEF,mBAAS,WAAW,OAAO,yBAAyB,IAAI;AAAA,QAC1D;AAEA,aAAK,gBAAgB;AAAA,UACnB,UAAU;AAAA,UACV;AAAA,QAAA;AAAA,MAEJ,SAAS,OAAO;AACd,gBAAQ;AAAA,UACN,sDAAsD,UAAU,EAAE;AAAA,UAClE;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,SAAK,UAAU,MAAA;AACf,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,QAAc;AAEZ,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,UAAU,gBAAA;AAAA,EACxB;AAAA,EAEQ,oBAA0B;AAEhC,SAAK,gBAAA;AAEL,QAAI,KAAK,cAAc,CAAC,KAAK,cAAc;AACzC;AAAA,IACF;AAEA,UAAM,gBAAgB,KAAK,iBAAA;AAE3B,QAAI,kBAAkB,MAAM;AAC1B;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,IAAI,GAAG,gBAAgB,KAAK,KAAK;AAEpD,SAAK,aAAa,WAAW,MAAM;AACjC,WAAK,WAAA,EAAa,MAAM,CAAC,UAAU;AACjC,gBAAQ,KAAK,kCAAkC,KAAK;AAAA,MACtD,CAAC;AAAA,IACH,GAAG,KAAK;AAAA,EACV;AAAA,EAEQ,mBAAkC;AACxC,UAAM,kBAAkB,KAAK,UAAU,0BAAA;AAEvC,QAAI,gBAAgB,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AAGA,WAAO,gBAAgB,CAAC,EAAG;AAAA,EAC7B;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA,EAEQ,aAAsB;AAC5B,WACE,KAAK,gBAAgB,oBAAoB,KAAK,gBAAgB,SAAA;AAAA,EAElE;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,UAAU,gBAAA;AAAA,EACxB;AAAA,EAEA,mBAAyB;AACvB,UAAM,kBAAkB,KAAK,UAAU,0BAAA;AACvC,UAAM,sBAAsB,gBAAgB,IAAI,CAAC,iBAAiB;AAAA,MAChE,GAAG;AAAA,MACH,eAAe,KAAK,IAAA;AAAA,IAAI,EACxB;AAEF,SAAK,UAAU,mBAAmB,mBAAmB;AAAA,EACvD;AACF;"}
1
+ {"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { DuplicateTransactionIdError, createTransaction } from '@tanstack/db'\nimport { OutboxTransactionNotFoundError } from '../outbox/OutboxManager'\nimport { DefaultRetryPolicy } from '../retry/RetryPolicy'\nimport { NonRetriableError } from '../types'\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\nfunction toError(value: unknown): Error {\n return value instanceof Error ||\n Object.prototype.toString.call(value) === `[object Error]`\n ? (value as Error)\n : new Error(String(value))\n}\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 private fatalError: Error | 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.assertHealthy()\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n this.assertHealthy()\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 const terminalError =\n transaction.outboxPhase === `rejection-pending`\n ? this.getTerminalError(transaction)\n : undefined\n this.scheduler.markStarted(transaction)\n\n if (!transaction.outboxPhase) {\n try {\n await this.runMutationFn(transaction)\n } catch (error) {\n const err = toError(error)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n }\n\n // A successful provider call can outlive this executor. Persist that\n // fact before removal so a restarted executor only retries deletion.\n const deletionPending: OfflineTransaction = transaction.outboxPhase\n ? transaction\n : { ...transaction, outboxPhase: `deletion-pending` }\n try {\n await this.removeSettledTransaction(\n deletionPending,\n !transaction.outboxPhase,\n )\n } catch (error) {\n const storageError = error as Error\n this.offlineExecutor.rejectTransaction(\n transaction.id,\n terminalError ?? storageError,\n )\n throw storageError\n }\n\n if (terminalError) {\n this.offlineExecutor.rejectTransaction(transaction.id, terminalError)\n } else {\n this.offlineExecutor.resolveTransaction(transaction.id, undefined)\n }\n } catch (error) {\n if (\n error !== null &&\n typeof error === `object` &&\n (error as Record<symbol, unknown>)[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 let shouldRetry: boolean\n let hookFailure: Error | undefined\n if (error instanceof NonRetriableError) {\n shouldRetry = false\n } else {\n let decision: boolean | undefined\n try {\n decision = this.config.shouldRetry?.(error, transaction.retryCount)\n if (decision !== undefined && typeof decision !== `boolean`) {\n // An async hook is invalid, but its rejection must stay row-local.\n void Promise.resolve(decision).catch(() => {})\n throw new TypeError(\n `OfflineConfig.shouldRetry must return true, false, or undefined`,\n )\n }\n } catch (hookError) {\n hookFailure = toError(hookError)\n }\n if (hookFailure) {\n shouldRetry = false\n } else {\n shouldRetry =\n decision ??\n this.retryPolicy.shouldRetry(error, transaction.retryCount)\n }\n }\n\n if (!shouldRetry) {\n const terminalError = hookFailure ?? error\n const rejectionPending: OfflineTransaction = {\n ...transaction,\n outboxPhase: `rejection-pending`,\n lastError: {\n name: terminalError.name,\n message: terminalError.message,\n stack: terminalError.stack,\n },\n }\n if (hookFailure) {\n console.warn(\n `Retry decision failed for transaction ${transaction.id}:`,\n hookFailure,\n `Named mutation function error:`,\n error,\n )\n } else {\n console.warn(`Transaction ${transaction.id} failed permanently:`, error)\n }\n try {\n await this.removeSettledTransaction(rejectionPending, true)\n } catch (storageError) {\n this.offlineExecutor.rejectTransaction(transaction.id, terminalError)\n throw storageError\n }\n\n this.offlineExecutor.rejectTransaction(transaction.id, terminalError)\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 this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n } catch (persistError) {\n // Public removal can finish while the named mutation function runs.\n // The removed row cannot be retried, but its caller still owns the\n // provider failure and later queued work must be able to run.\n if (persistError instanceof OutboxTransactionNotFoundError) {\n this.scheduler.markCompleted(transaction)\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n throw persistError\n } finally {\n this.scheduler.markFailed(transaction)\n }\n }\n\n private async removeSettledTransaction(\n transaction: OfflineTransaction,\n persistMarker: boolean,\n ): Promise<void> {\n try {\n if (persistMarker) {\n try {\n await this.outbox.update(transaction.id, transaction)\n } catch (error) {\n // A public removal can finish while the provider is still running.\n if (!(error instanceof OutboxTransactionNotFoundError)) throw error\n }\n }\n await this.outbox.remove(transaction.id)\n this.scheduler.markCompleted(transaction)\n } catch (error) {\n const storageError = toError(error)\n this.fatalError = storageError\n this.scheduler.markFailed(transaction)\n this.clearRetryTimer()\n throw storageError\n }\n }\n\n assertHealthy(): void {\n if (this.fatalError) throw this.fatalError\n }\n\n private getTerminalError(transaction: OfflineTransaction): Error {\n if (!transaction.lastError)\n throw new Error(`Terminal transaction ${transaction.id} has no error`)\n const { name, message, stack } = transaction.lastError\n const error =\n name === `NonRetriableError`\n ? new NonRetriableError(message)\n : new Error(message)\n error.name = name\n error.stack = stack\n return error\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 const providerPending = transactions.filter(\n (transaction) => !transaction.outboxPhase,\n )\n const selectedProvider = this.config.beforeRetry\n ? this.config.beforeRetry(providerPending)\n : providerPending\n const selectedById = new Map(selectedProvider.map((tx) => [tx.id, tx]))\n const filteredTransactions = transactions.flatMap((transaction) => {\n if (transaction.outboxPhase) return [transaction]\n const selected = selectedById.get(transaction.id)\n return selected ? [selected] : []\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 removedIds = providerPending\n .filter(({ id }) => !selectedById.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(\n newlyLoaded.filter(\n (transaction) => transaction.outboxPhase !== `rejection-pending`,\n ),\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\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 restorationTx.applyMutations(offlineTx.mutations)\n\n // Register with each affected Collection. If one cannot track it, roll\n // it back, so no Collection keeps a restoration nothing will settle.\n try {\n // The serializer resolves every mutation's Collection or throws.\n for (const mutation of offlineTx.mutations) {\n if (mutation.collection._state.trackTransaction(restorationTx))\n mutation.collection._state.recomputeOptimisticState(true)\n }\n } catch (error) {\n // A secondary rollback settles only this restoration; it must not\n // roll back the user's live transactions on the same keys.\n restorationTx.rollback({ isSecondaryRollback: true })\n throw error\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\n // A live transaction with this id already shows these mutations.\n if (error instanceof DuplicateTransactionIdError) continue\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.fatalError || !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":[],"mappings":"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"}
@@ -1,4 +1,3 @@
1
- import { withSpan } from "../telemetry/tracer.js";
2
1
  import { TransactionSerializer, MissingTemporalConstructorError } from "./TransactionSerializer.js";
3
2
  class OutboxTransactionNotFoundError extends Error {
4
3
  }
@@ -32,42 +31,24 @@ class OutboxManager {
32
31
  if (revision <= oldestRead) this.removedAtRevision.delete(id);
33
32
  }
34
33
  async add(transaction) {
35
- return withSpan(
36
- `outbox.add`,
37
- {
38
- "transaction.id": transaction.id,
39
- "transaction.mutationFnName": transaction.mutationFnName,
40
- "transaction.keyCount": transaction.keys.length
41
- },
42
- async () => {
43
- const key = this.getStorageKey(transaction.id);
44
- const serialized = this.serializer.serialize(transaction);
45
- await this.storage.set(key, serialized);
46
- }
47
- );
34
+ const key = this.getStorageKey(transaction.id);
35
+ const serialized = this.serializer.serialize(transaction);
36
+ await this.storage.set(key, serialized);
48
37
  }
49
38
  async get(id) {
50
- return withSpan(`outbox.get`, {}, async (span) => {
51
- const key = this.getStorageKey(id);
52
- const data = await this.storage.get(key);
53
- if (!data) {
54
- span.setAttribute(`result`, `not_found`);
55
- return null;
56
- }
57
- try {
58
- const transaction = this.serializer.deserialize(data);
59
- span.setAttribute(`result`, `found`);
60
- return transaction;
61
- } catch (error) {
62
- if (error instanceof MissingTemporalConstructorError) {
63
- error.message = `transaction ${id}: ${error.message}`;
64
- throw error;
65
- }
66
- console.warn(`Failed to deserialize transaction ${id}:`, error);
67
- span.setAttribute(`result`, `deserialize_error`);
68
- return null;
39
+ const key = this.getStorageKey(id);
40
+ const data = await this.storage.get(key);
41
+ if (!data) return null;
42
+ try {
43
+ return this.serializer.deserialize(data);
44
+ } catch (error) {
45
+ if (error instanceof MissingTemporalConstructorError) {
46
+ error.message = `transaction ${id}: ${error.message}`;
47
+ throw error;
69
48
  }
70
- });
49
+ console.warn(`Failed to deserialize transaction ${id}:`, error);
50
+ return null;
51
+ }
71
52
  }
72
53
  async getAll() {
73
54
  return this.withAll((transactions) => transactions);
@@ -76,36 +57,33 @@ class OutboxManager {
76
57
  const read = { revision: this.removalRevision };
77
58
  this.activeReads.add(read);
78
59
  try {
79
- return await 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 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
- );
60
+ const keys = await this.storage.keys();
61
+ const transactionKeys = keys.filter(
62
+ (key) => key.startsWith(this.keyPrefix)
63
+ );
64
+ const transactions = [];
65
+ for (const key of transactionKeys) {
66
+ const data = await this.storage.get(key);
67
+ if (data) {
68
+ try {
69
+ const transaction = this.serializer.deserialize(data);
70
+ transactions.push(transaction);
71
+ } catch (error) {
72
+ if (error instanceof MissingTemporalConstructorError) {
73
+ error.message = `transaction ${key.slice(this.keyPrefix.length)}: ${error.message}`;
74
+ throw error;
101
75
  }
76
+ console.warn(
77
+ `Failed to deserialize transaction from key ${key}:`,
78
+ error
79
+ );
102
80
  }
103
81
  }
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
- });
82
+ }
83
+ const currentTransactions = transactions.filter(
84
+ ({ id }) => (this.removedAtRevision.get(id) ?? 0) <= read.revision
85
+ ).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
86
+ return await consume(currentTransactions);
109
87
  } finally {
110
88
  this.activeReads.delete(read);
111
89
  this.pruneRemovalHistory();
@@ -119,26 +97,19 @@ class OutboxManager {
119
97
  );
120
98
  }
121
99
  async update(id, updates) {
122
- return withSpan(`outbox.update`, {}, async () => {
123
- const existing = await this.get(id);
124
- if (!existing) {
125
- throw new OutboxTransactionNotFoundError(`Transaction ${id} not found`);
126
- }
127
- const updated = { ...existing, ...updates };
128
- await this.add(updated);
129
- });
100
+ const existing = await this.get(id);
101
+ if (!existing) {
102
+ throw new OutboxTransactionNotFoundError(`Transaction ${id} not found`);
103
+ }
104
+ await this.add({ ...existing, ...updates });
130
105
  }
131
106
  async remove(id) {
132
- return withSpan(`outbox.remove`, {}, async () => {
133
- const key = this.getStorageKey(id);
134
- await this.storage.delete(key);
135
- this.recordRemovals([id]);
136
- });
107
+ const key = this.getStorageKey(id);
108
+ await this.storage.delete(key);
109
+ this.recordRemovals([id]);
137
110
  }
138
111
  async removeMany(ids) {
139
- return withSpan(`outbox.removeMany`, { count: ids.length }, async () => {
140
- await Promise.all(ids.map((id) => this.remove(id)));
141
- });
112
+ await Promise.all(ids.map((id) => this.remove(id)));
142
113
  }
143
114
  async clear() {
144
115
  const keys = await this.storage.keys();
@@ -1 +1 @@
1
- {"version":3,"file":"OutboxManager.js","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 OutboxTransactionNotFoundError extends Error {}\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 OutboxTransactionNotFoundError(`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":[],"mappings":"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"}
1
+ {"version":3,"file":"OutboxManager.js","sources":["../../../src/outbox/OutboxManager.ts"],"sourcesContent":["import {\n MissingTemporalConstructorError,\n TransactionSerializer,\n} from './TransactionSerializer'\nimport type { OfflineTransaction, StorageAdapter } from '../types'\nimport type { Collection } from '@tanstack/db'\n\nexport class OutboxTransactionNotFoundError extends Error {}\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 const key = this.getStorageKey(transaction.id)\n const serialized = this.serializer.serialize(transaction)\n await this.storage.set(key, serialized)\n }\n\n async get(id: string): Promise<OfflineTransaction | null> {\n const key = this.getStorageKey(id)\n const data = await this.storage.get(key)\n if (!data) return null\n\n try {\n return this.serializer.deserialize(data)\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 return null\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 const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) =>\n key.startsWith(this.keyPrefix),\n )\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 await consume(currentTransactions)\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 const existing = await this.get(id)\n if (!existing) {\n throw new OutboxTransactionNotFoundError(`Transaction ${id} not found`)\n }\n await this.add({ ...existing, ...updates })\n }\n\n async remove(id: string): Promise<void> {\n const key = this.getStorageKey(id)\n await this.storage.delete(key)\n this.recordRemovals([id])\n }\n\n async removeMany(ids: Array<string>): Promise<void> {\n await Promise.all(ids.map((id) => this.remove(id)))\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":[],"mappings":"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"}
@@ -70,6 +70,16 @@ export interface OfflineConfig {
70
70
  storage?: StorageAdapter;
71
71
  maxConcurrency?: number;
72
72
  jitter?: boolean;
73
+ /**
74
+ * Override retry decisions for errors from all named mutation functions.
75
+ * Return synchronously; undefined uses the default decision. Put any
76
+ * function-specific context needed by this shared hook on the Error.
77
+ * NonRetriableError always stops retry.
78
+ * A throw or invalid return fails only that outbox row and rejects its
79
+ * waiters with the hook failure. Queued work runs after terminal cleanup.
80
+ * The default policy still supplies the delay and configured jitter.
81
+ */
82
+ shouldRetry?: (error: Error, retryCount: number) => boolean | undefined;
73
83
  beforeRetry?: (transactions: Array<OfflineTransaction>) => Array<OfflineTransaction>;
74
84
  onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void;
75
85
  onLeadershipChange?: (isLeader: boolean) => void;