@tanstack/offline-transactions 1.0.54 → 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 (64) hide show
  1. package/README.md +137 -23
  2. package/dist/cjs/OfflineExecutor.cjs +28 -11
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/OfflineExecutor.d.cts +1 -0
  5. package/dist/cjs/api/OfflineTransaction.cjs +4 -1
  6. package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
  7. package/dist/cjs/connectivity/OnlineDetector.cjs +1 -1
  8. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
  9. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +1 -7
  10. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -1
  11. package/dist/cjs/executor/KeyScheduler.cjs +31 -26
  12. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
  13. package/dist/cjs/executor/KeyScheduler.d.cts +5 -3
  14. package/dist/cjs/executor/TransactionExecutor.cjs +72 -35
  15. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
  16. package/dist/cjs/executor/TransactionExecutor.d.cts +3 -2
  17. package/dist/cjs/outbox/OutboxManager.cjs +66 -24
  18. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
  19. package/dist/cjs/outbox/OutboxManager.d.cts +6 -0
  20. package/dist/cjs/outbox/TransactionSerializer.cjs +159 -20
  21. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
  22. package/dist/cjs/outbox/TransactionSerializer.d.cts +2 -0
  23. package/dist/cjs/storage/IndexedDBAdapter.cjs +16 -12
  24. package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
  25. package/dist/cjs/storage/IndexedDBAdapter.d.cts +1 -0
  26. package/dist/cjs/types.cjs.map +1 -1
  27. package/dist/cjs/types.d.cts +3 -0
  28. package/dist/esm/OfflineExecutor.d.ts +1 -0
  29. package/dist/esm/OfflineExecutor.js +28 -11
  30. package/dist/esm/OfflineExecutor.js.map +1 -1
  31. package/dist/esm/api/OfflineTransaction.js +4 -1
  32. package/dist/esm/api/OfflineTransaction.js.map +1 -1
  33. package/dist/esm/connectivity/OnlineDetector.js +1 -1
  34. package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
  35. package/dist/esm/connectivity/ReactNativeOnlineDetector.js +1 -7
  36. package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -1
  37. package/dist/esm/executor/KeyScheduler.d.ts +5 -3
  38. package/dist/esm/executor/KeyScheduler.js +31 -26
  39. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  40. package/dist/esm/executor/TransactionExecutor.d.ts +3 -2
  41. package/dist/esm/executor/TransactionExecutor.js +72 -35
  42. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  43. package/dist/esm/outbox/OutboxManager.d.ts +6 -0
  44. package/dist/esm/outbox/OutboxManager.js +67 -25
  45. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  46. package/dist/esm/outbox/TransactionSerializer.d.ts +2 -0
  47. package/dist/esm/outbox/TransactionSerializer.js +159 -20
  48. package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
  49. package/dist/esm/storage/IndexedDBAdapter.d.ts +1 -0
  50. package/dist/esm/storage/IndexedDBAdapter.js +16 -12
  51. package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
  52. package/dist/esm/types.d.ts +3 -0
  53. package/dist/esm/types.js.map +1 -1
  54. package/package.json +2 -2
  55. package/src/OfflineExecutor.ts +34 -15
  56. package/src/api/OfflineTransaction.ts +7 -2
  57. package/src/connectivity/OnlineDetector.ts +6 -1
  58. package/src/connectivity/ReactNativeOnlineDetector.ts +1 -11
  59. package/src/executor/KeyScheduler.ts +39 -28
  60. package/src/executor/TransactionExecutor.ts +88 -46
  61. package/src/outbox/OutboxManager.ts +82 -28
  62. package/src/outbox/TransactionSerializer.ts +236 -22
  63. package/src/storage/IndexedDBAdapter.ts +23 -12
  64. package/src/types.ts +3 -0
@@ -4,20 +4,25 @@ const tracer = require("../telemetry/tracer.cjs");
4
4
  class KeyScheduler {
5
5
  constructor() {
6
6
  this.pendingTransactions = [];
7
- this.isRunning = false;
8
7
  }
9
8
  schedule(transaction) {
10
- tracer.withSyncSpan(
9
+ return tracer.withSyncSpan(
11
10
  `scheduler.schedule`,
12
11
  {
13
12
  "transaction.id": transaction.id,
14
13
  queueLength: this.pendingTransactions.length
15
14
  },
16
15
  () => {
16
+ if (this.pendingTransactions.some(
17
+ (pending) => pending.id === transaction.id
18
+ )) {
19
+ return false;
20
+ }
17
21
  this.pendingTransactions.push(transaction);
18
22
  this.pendingTransactions.sort(
19
23
  (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
20
24
  );
25
+ return true;
21
26
  }
22
27
  );
23
28
  }
@@ -26,7 +31,7 @@ class KeyScheduler {
26
31
  `scheduler.getNext`,
27
32
  { pendingCount: this.pendingTransactions.length },
28
33
  (span) => {
29
- if (this.isRunning || this.pendingTransactions.length === 0) {
34
+ if (this.activeTransactionId !== void 0 || this.pendingTransactions.length === 0) {
30
35
  span.setAttribute(`result`, `empty`);
31
36
  return void 0;
32
37
  }
@@ -45,15 +50,15 @@ class KeyScheduler {
45
50
  isReadyToRun(transaction) {
46
51
  return Date.now() >= transaction.nextAttemptAt;
47
52
  }
48
- markStarted(_transaction) {
49
- this.isRunning = true;
53
+ markStarted(transaction) {
54
+ this.activeTransactionId = transaction.id;
50
55
  }
51
56
  markCompleted(transaction) {
52
57
  this.removeTransaction(transaction);
53
- this.isRunning = false;
58
+ this.activeTransactionId = void 0;
54
59
  }
55
60
  markFailed(_transaction) {
56
- this.isRunning = false;
61
+ this.activeTransactionId = void 0;
57
62
  }
58
63
  removeTransaction(transaction) {
59
64
  const index = this.pendingTransactions.findIndex(
@@ -64,38 +69,38 @@ class KeyScheduler {
64
69
  }
65
70
  }
66
71
  updateTransaction(transaction) {
67
- const index = this.pendingTransactions.findIndex(
68
- (tx) => tx.id === transaction.id
69
- );
70
- if (index >= 0) {
71
- this.pendingTransactions[index] = transaction;
72
- this.pendingTransactions.sort(
73
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
74
- );
75
- }
72
+ this.updateTransactions([transaction]);
76
73
  }
77
74
  getPendingCount() {
78
75
  return this.pendingTransactions.length;
79
76
  }
80
77
  getRunningCount() {
81
- return this.isRunning ? 1 : 0;
78
+ return this.activeTransactionId === void 0 ? 0 : 1;
82
79
  }
83
80
  clear() {
84
81
  this.pendingTransactions = [];
85
- this.isRunning = false;
82
+ this.activeTransactionId = void 0;
83
+ }
84
+ /** @internal Reconcile one replay snapshot without canceling issued work. */
85
+ removePendingTransactions(transactionIds) {
86
+ const ids = new Set(transactionIds);
87
+ if (this.activeTransactionId !== void 0)
88
+ ids.delete(this.activeTransactionId);
89
+ this.pendingTransactions = this.pendingTransactions.filter(
90
+ ({ id }) => !ids.has(id)
91
+ );
92
+ return [...ids];
86
93
  }
87
94
  getAllPendingTransactions() {
88
95
  return [...this.pendingTransactions];
89
96
  }
90
97
  updateTransactions(updatedTransactions) {
91
- for (const updatedTx of updatedTransactions) {
92
- const index = this.pendingTransactions.findIndex(
93
- (tx) => tx.id === updatedTx.id
94
- );
95
- if (index >= 0) {
96
- this.pendingTransactions[index] = updatedTx;
97
- }
98
- }
98
+ const updatedById = new Map(
99
+ updatedTransactions.map((transaction) => [transaction.id, transaction])
100
+ );
101
+ this.pendingTransactions = this.pendingTransactions.map(
102
+ (transaction) => updatedById.get(transaction.id) ?? transaction
103
+ );
99
104
  this.pendingTransactions.sort(
100
105
  (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
101
106
  );
@@ -1 +1 @@
1
- {"version":3,"file":"KeyScheduler.cjs","sources":["../../../src/executor/KeyScheduler.ts"],"sourcesContent":["import { withSyncSpan } from '../telemetry/tracer'\nimport type { OfflineTransaction } from '../types'\n\nexport class KeyScheduler {\n private pendingTransactions: Array<OfflineTransaction> = []\n private isRunning = false\n\n schedule(transaction: OfflineTransaction): void {\n withSyncSpan(\n `scheduler.schedule`,\n {\n 'transaction.id': transaction.id,\n queueLength: this.pendingTransactions.length,\n },\n () => {\n this.pendingTransactions.push(transaction)\n // Sort by creation time to maintain FIFO order\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n )\n },\n )\n }\n\n getNext(): OfflineTransaction | undefined {\n return withSyncSpan(\n `scheduler.getNext`,\n { pendingCount: this.pendingTransactions.length },\n (span) => {\n if (this.isRunning || this.pendingTransactions.length === 0) {\n span.setAttribute(`result`, `empty`)\n return undefined\n }\n\n const firstTransaction = this.pendingTransactions[0]!\n\n if (!this.isReadyToRun(firstTransaction)) {\n span.setAttribute(`result`, `waiting_for_first`)\n span.setAttribute(`transaction.id`, firstTransaction.id)\n return undefined\n }\n\n span.setAttribute(`result`, `found`)\n span.setAttribute(`transaction.id`, firstTransaction.id)\n return firstTransaction\n },\n )\n }\n\n private isReadyToRun(transaction: OfflineTransaction): boolean {\n return Date.now() >= transaction.nextAttemptAt\n }\n\n markStarted(_transaction: OfflineTransaction): void {\n this.isRunning = true\n }\n\n markCompleted(transaction: OfflineTransaction): void {\n this.removeTransaction(transaction)\n this.isRunning = false\n }\n\n markFailed(_transaction: OfflineTransaction): void {\n this.isRunning = false\n }\n\n private removeTransaction(transaction: OfflineTransaction): void {\n const index = this.pendingTransactions.findIndex(\n (tx) => tx.id === transaction.id,\n )\n if (index >= 0) {\n this.pendingTransactions.splice(index, 1)\n }\n }\n\n updateTransaction(transaction: OfflineTransaction): void {\n const index = this.pendingTransactions.findIndex(\n (tx) => tx.id === transaction.id,\n )\n if (index >= 0) {\n this.pendingTransactions[index] = transaction\n // Re-sort to maintain FIFO order after update\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n )\n }\n }\n\n getPendingCount(): number {\n return this.pendingTransactions.length\n }\n\n getRunningCount(): number {\n return this.isRunning ? 1 : 0\n }\n\n clear(): void {\n this.pendingTransactions = []\n this.isRunning = false\n }\n\n getAllPendingTransactions(): Array<OfflineTransaction> {\n return [...this.pendingTransactions]\n }\n\n updateTransactions(updatedTransactions: Array<OfflineTransaction>): void {\n for (const updatedTx of updatedTransactions) {\n const index = this.pendingTransactions.findIndex(\n (tx) => tx.id === updatedTx.id,\n )\n if (index >= 0) {\n this.pendingTransactions[index] = updatedTx\n }\n }\n // Re-sort to maintain FIFO order after updates\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n )\n }\n}\n"],"names":["withSyncSpan"],"mappings":";;;AAGO,MAAM,aAAa;AAAA,EAAnB,cAAA;AACL,SAAQ,sBAAiD,CAAA;AACzD,SAAQ,YAAY;AAAA,EAAA;AAAA,EAEpB,SAAS,aAAuC;AAC9CA,WAAAA;AAAAA,MACE;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,aAAa,KAAK,oBAAoB;AAAA,MAAA;AAAA,MAExC,MAAM;AACJ,aAAK,oBAAoB,KAAK,WAAW;AAEzC,aAAK,oBAAoB;AAAA,UACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,QAAQ;AAAA,MAE1D;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,UAA0C;AACxC,WAAOA,OAAAA;AAAAA,MACL;AAAA,MACA,EAAE,cAAc,KAAK,oBAAoB,OAAA;AAAA,MACzC,CAAC,SAAS;AACR,YAAI,KAAK,aAAa,KAAK,oBAAoB,WAAW,GAAG;AAC3D,eAAK,aAAa,UAAU,OAAO;AACnC,iBAAO;AAAA,QACT;AAEA,cAAM,mBAAmB,KAAK,oBAAoB,CAAC;AAEnD,YAAI,CAAC,KAAK,aAAa,gBAAgB,GAAG;AACxC,eAAK,aAAa,UAAU,mBAAmB;AAC/C,eAAK,aAAa,kBAAkB,iBAAiB,EAAE;AACvD,iBAAO;AAAA,QACT;AAEA,aAAK,aAAa,UAAU,OAAO;AACnC,aAAK,aAAa,kBAAkB,iBAAiB,EAAE;AACvD,eAAO;AAAA,MACT;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,aAAa,aAA0C;AAC7D,WAAO,KAAK,SAAS,YAAY;AAAA,EACnC;AAAA,EAEA,YAAY,cAAwC;AAClD,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,cAAc,aAAuC;AACnD,SAAK,kBAAkB,WAAW;AAClC,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,WAAW,cAAwC;AACjD,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,kBAAkB,aAAuC;AAC/D,UAAM,QAAQ,KAAK,oBAAoB;AAAA,MACrC,CAAC,OAAO,GAAG,OAAO,YAAY;AAAA,IAAA;AAEhC,QAAI,SAAS,GAAG;AACd,WAAK,oBAAoB,OAAO,OAAO,CAAC;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,kBAAkB,aAAuC;AACvD,UAAM,QAAQ,KAAK,oBAAoB;AAAA,MACrC,CAAC,OAAO,GAAG,OAAO,YAAY;AAAA,IAAA;AAEhC,QAAI,SAAS,GAAG;AACd,WAAK,oBAAoB,KAAK,IAAI;AAElC,WAAK,oBAAoB;AAAA,QACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,MAAQ;AAAA,IAE1D;AAAA,EACF;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,oBAAoB;AAAA,EAClC;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,YAAY,IAAI;AAAA,EAC9B;AAAA,EAEA,QAAc;AACZ,SAAK,sBAAsB,CAAA;AAC3B,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,4BAAuD;AACrD,WAAO,CAAC,GAAG,KAAK,mBAAmB;AAAA,EACrC;AAAA,EAEA,mBAAmB,qBAAsD;AACvE,eAAW,aAAa,qBAAqB;AAC3C,YAAM,QAAQ,KAAK,oBAAoB;AAAA,QACrC,CAAC,OAAO,GAAG,OAAO,UAAU;AAAA,MAAA;AAE9B,UAAI,SAAS,GAAG;AACd,aAAK,oBAAoB,KAAK,IAAI;AAAA,MACpC;AAAA,IACF;AAEA,SAAK,oBAAoB;AAAA,MACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,IAAQ;AAAA,EAE1D;AACF;;"}
1
+ {"version":3,"file":"KeyScheduler.cjs","sources":["../../../src/executor/KeyScheduler.ts"],"sourcesContent":["import { withSyncSpan } from '../telemetry/tracer'\nimport type { OfflineTransaction } from '../types'\n\nexport class KeyScheduler {\n private pendingTransactions: Array<OfflineTransaction> = []\n private activeTransactionId: string | undefined\n\n schedule(transaction: OfflineTransaction): boolean {\n return withSyncSpan(\n `scheduler.schedule`,\n {\n 'transaction.id': transaction.id,\n queueLength: this.pendingTransactions.length,\n },\n () => {\n if (\n this.pendingTransactions.some(\n (pending) => pending.id === transaction.id,\n )\n ) {\n return false\n }\n this.pendingTransactions.push(transaction)\n // Sort by creation time to maintain FIFO order\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n )\n return true\n },\n )\n }\n\n getNext(): OfflineTransaction | undefined {\n return withSyncSpan(\n `scheduler.getNext`,\n { pendingCount: this.pendingTransactions.length },\n (span) => {\n if (\n this.activeTransactionId !== undefined ||\n this.pendingTransactions.length === 0\n ) {\n span.setAttribute(`result`, `empty`)\n return undefined\n }\n\n const firstTransaction = this.pendingTransactions[0]!\n\n if (!this.isReadyToRun(firstTransaction)) {\n span.setAttribute(`result`, `waiting_for_first`)\n span.setAttribute(`transaction.id`, firstTransaction.id)\n return undefined\n }\n\n span.setAttribute(`result`, `found`)\n span.setAttribute(`transaction.id`, firstTransaction.id)\n return firstTransaction\n },\n )\n }\n\n private isReadyToRun(transaction: OfflineTransaction): boolean {\n return Date.now() >= transaction.nextAttemptAt\n }\n\n markStarted(transaction: OfflineTransaction): void {\n this.activeTransactionId = transaction.id\n }\n\n markCompleted(transaction: OfflineTransaction): void {\n this.removeTransaction(transaction)\n this.activeTransactionId = undefined\n }\n\n markFailed(_transaction: OfflineTransaction): void {\n this.activeTransactionId = undefined\n }\n\n private removeTransaction(transaction: OfflineTransaction): void {\n const index = this.pendingTransactions.findIndex(\n (tx) => tx.id === transaction.id,\n )\n if (index >= 0) {\n this.pendingTransactions.splice(index, 1)\n }\n }\n\n updateTransaction(transaction: OfflineTransaction): void {\n this.updateTransactions([transaction])\n }\n\n getPendingCount(): number {\n return this.pendingTransactions.length\n }\n\n getRunningCount(): number {\n return this.activeTransactionId === undefined ? 0 : 1\n }\n\n clear(): void {\n this.pendingTransactions = []\n this.activeTransactionId = undefined\n }\n\n /** @internal Reconcile one replay snapshot without canceling issued work. */\n removePendingTransactions(transactionIds: Iterable<string>): Array<string> {\n const ids = new Set(transactionIds)\n if (this.activeTransactionId !== undefined)\n ids.delete(this.activeTransactionId)\n this.pendingTransactions = this.pendingTransactions.filter(\n ({ id }) => !ids.has(id),\n )\n return [...ids]\n }\n\n getAllPendingTransactions(): Array<OfflineTransaction> {\n return [...this.pendingTransactions]\n }\n\n updateTransactions(updatedTransactions: Array<OfflineTransaction>): void {\n const updatedById = new Map(\n updatedTransactions.map((transaction) => [transaction.id, transaction]),\n )\n this.pendingTransactions = this.pendingTransactions.map(\n (transaction) => updatedById.get(transaction.id) ?? transaction,\n )\n // Re-sort to maintain FIFO order after updates\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n )\n }\n}\n"],"names":["withSyncSpan"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { OfflineTransaction } from '../types.cjs';
2
2
  export declare class KeyScheduler {
3
3
  private pendingTransactions;
4
- private isRunning;
5
- schedule(transaction: OfflineTransaction): void;
4
+ private activeTransactionId;
5
+ schedule(transaction: OfflineTransaction): boolean;
6
6
  getNext(): OfflineTransaction | undefined;
7
7
  private isReadyToRun;
8
- markStarted(_transaction: OfflineTransaction): void;
8
+ markStarted(transaction: OfflineTransaction): void;
9
9
  markCompleted(transaction: OfflineTransaction): void;
10
10
  markFailed(_transaction: OfflineTransaction): void;
11
11
  private removeTransaction;
@@ -13,6 +13,8 @@ export declare class KeyScheduler {
13
13
  getPendingCount(): number;
14
14
  getRunningCount(): number;
15
15
  clear(): void;
16
+ /** @internal Reconcile one replay snapshot without canceling issued work. */
17
+ removePendingTransactions(transactionIds: Iterable<string>): Array<string>;
16
18
  getAllPendingTransactions(): Array<OfflineTransaction>;
17
19
  updateTransactions(updatedTransactions: Array<OfflineTransaction>): void;
18
20
  }
@@ -34,11 +34,12 @@ 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() {
40
41
  while (this.scheduler.getPendingCount() > 0) {
41
- if (!this.isOnline()) {
42
+ if (!this.canExecute()) {
42
43
  break;
43
44
  }
44
45
  const transaction = this.scheduler.getNext();
@@ -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,12 +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
- this.scheduler.markCompleted(transaction);
70
- await this.outbox.remove(transaction.id);
71
- span.setAttribute(`result`, `success`);
72
- this.offlineExecutor.resolveTransaction(transaction.id, result);
69
+ result = await this.runMutationFn(transaction);
73
70
  } catch (error) {
74
71
  const err = error instanceof Error ? error : new Error(String(error));
75
72
  span.setAttribute(`result`, `error`);
@@ -77,6 +74,17 @@ class TransactionExecutor {
77
74
  err[HANDLED_EXECUTION_ERROR] = true;
78
75
  throw err;
79
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;
80
88
  }
81
89
  );
82
90
  } catch (error) {
@@ -120,14 +128,21 @@ class TransactionExecutor {
120
128
  );
121
129
  span.setAttribute(`shouldRetry`, shouldRetry);
122
130
  if (!shouldRetry) {
123
- this.scheduler.markCompleted(transaction);
124
- 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
+ }
125
139
  console.warn(
126
140
  `Transaction ${transaction.id} failed permanently:`,
127
141
  error
128
142
  );
129
143
  span.setAttribute(`result`, `permanent_failure`);
130
144
  this.offlineExecutor.rejectTransaction(transaction.id, error);
145
+ if (removalError !== void 0) throw removalError;
131
146
  return;
132
147
  }
133
148
  const delay = Math.max(
@@ -146,7 +161,6 @@ class TransactionExecutor {
146
161
  };
147
162
  span.setAttribute(`retryDelay`, delay);
148
163
  span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount);
149
- this.scheduler.markFailed(transaction);
150
164
  this.scheduler.updateTransaction(updatedTransaction);
151
165
  try {
152
166
  await this.outbox.update(transaction.id, updatedTransaction);
@@ -155,28 +169,48 @@ class TransactionExecutor {
155
169
  span.recordException(persistError);
156
170
  span.setAttribute(`result`, `persist_failed`);
157
171
  throw persistError;
172
+ } finally {
173
+ this.scheduler.markFailed(transaction);
158
174
  }
159
- this.scheduleNextRetry();
160
175
  }
161
176
  );
162
177
  }
163
178
  async loadPendingTransactions() {
164
- const transactions = await this.outbox.getAll();
165
- let filteredTransactions = transactions;
166
- if (this.config.beforeRetry) {
167
- filteredTransactions = this.config.beforeRetry(transactions);
168
- }
169
- for (const transaction of filteredTransactions) {
170
- this.scheduler.schedule(transaction);
171
- }
172
- this.restoreOptimisticState(filteredTransactions);
173
- this.resetRetryDelays();
174
- this.scheduleNextRetry();
175
- const removedTransactions = transactions.filter(
176
- (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id)
177
- );
178
- if (removedTransactions.length > 0) {
179
- 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
+ );
180
214
  }
181
215
  }
182
216
  /**
@@ -231,31 +265,34 @@ class TransactionExecutor {
231
265
  this.scheduler.clear();
232
266
  this.clearRetryTimer();
233
267
  }
268
+ pause() {
269
+ this.clearRetryTimer();
270
+ }
234
271
  getPendingCount() {
235
272
  return this.scheduler.getPendingCount();
236
273
  }
237
274
  scheduleNextRetry() {
238
275
  this.clearRetryTimer();
239
- if (!this.isOnline()) {
276
+ if (!this.canExecute()) {
240
277
  return;
241
278
  }
242
- const earliestRetryTime = this.getEarliestRetryTime();
243
- if (earliestRetryTime === null) {
279
+ const nextRetryTime = this.getNextRetryTime();
280
+ if (nextRetryTime === null) {
244
281
  return;
245
282
  }
246
- const delay = Math.max(0, earliestRetryTime - Date.now());
283
+ const delay = Math.max(0, nextRetryTime - Date.now());
247
284
  this.retryTimer = setTimeout(() => {
248
285
  this.executeAll().catch((error) => {
249
286
  console.warn(`Failed to execute retry batch:`, error);
250
287
  });
251
288
  }, delay);
252
289
  }
253
- getEarliestRetryTime() {
290
+ getNextRetryTime() {
254
291
  const allTransactions = this.scheduler.getAllPendingTransactions();
255
292
  if (allTransactions.length === 0) {
256
293
  return null;
257
294
  }
258
- return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt));
295
+ return allTransactions[0].nextAttemptAt;
259
296
  }
260
297
  clearRetryTimer() {
261
298
  if (this.retryTimer) {
@@ -263,8 +300,8 @@ class TransactionExecutor {
263
300
  this.retryTimer = null;
264
301
  }
265
302
  }
266
- isOnline() {
267
- return this.offlineExecutor.isOnline();
303
+ canExecute() {
304
+ return this.offlineExecutor.isOfflineEnabled && this.offlineExecutor.isOnline();
268
305
  }
269
306
  getRunningCount() {
270
307
  return this.scheduler.getRunningCount();
@@ -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.isOnline()) {\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 this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(error))\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n },\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error,\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n 'transaction.id': transaction.id,\n 'error.name': error.name,\n 'error.message': error.message,\n },\n async (span) => {\n const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount,\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n\n const delay = 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 for (const transaction of filteredTransactions) {\n this.scheduler.schedule(transaction)\n }\n\n // Restore optimistic state for loaded transactions\n // This ensures the UI shows the optimistic data while transactions are pending\n this.restoreOptimisticState(filteredTransactions)\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n\n const removedTransactions = transactions.filter(\n (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id),\n )\n\n if (removedTransactions.length > 0) {\n await this.outbox.removeMany(removedTransactions.map((tx) => tx.id))\n }\n }\n\n /**\n * Restore optimistic state from loaded transactions.\n * Creates internal transactions to hold the mutations so the collection's\n * state manager can show optimistic data while waiting for sync.\n */\n private restoreOptimisticState(\n transactions: Array<OfflineTransaction>,\n ): void {\n for (const offlineTx of transactions) {\n if (offlineTx.mutations.length === 0) {\n continue\n }\n\n try {\n // Create a restoration transaction that holds mutations for optimistic state display.\n // It will never commit - the real mutation is handled by the offline executor.\n const restorationTx = createTransaction({\n id: offlineTx.id,\n autoCommit: false,\n mutationFn: async () => {},\n })\n\n // Prevent unhandled promise rejection when cleanup calls rollback()\n // We don't care about this promise - it's just for holding mutations\n restorationTx.isPersisted.promise.catch(() => {\n // Intentionally ignored - restoration transactions are cleaned up\n // via cleanupRestorationTransaction, not through normal commit flow\n })\n\n restorationTx.applyMutations(offlineTx.mutations)\n\n // Register with each affected collection's state manager\n const touchedCollections = new Set<string>()\n for (const mutation of offlineTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n\n mutation.collection._state.transactions.set(\n restorationTx.id,\n restorationTx,\n )\n mutation.collection._state.recomputeOptimisticState(true)\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\n console.warn(\n `Failed to restore optimistic state for transaction ${offlineTx.id}:`,\n error,\n )\n }\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n if (!this.isOnline()) {\n return\n }\n\n // Find the earliest retry time among pending transactions\n const earliestRetryTime = this.getEarliestRetryTime()\n\n if (earliestRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, earliestRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getEarliestRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt))\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n private isOnline(): boolean {\n return this.offlineExecutor.isOnline()\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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"}
@@ -25,11 +25,12 @@ export declare class TransactionExecutor {
25
25
  */
26
26
  private restoreOptimisticState;
27
27
  clear(): void;
28
+ pause(): void;
28
29
  getPendingCount(): number;
29
30
  private scheduleNextRetry;
30
- private getEarliestRetryTime;
31
+ private getNextRetryTime;
31
32
  private clearRetryTimer;
32
- private isOnline;
33
+ private canExecute;
33
34
  getRunningCount(): number;
34
35
  resetRetryDelays(): void;
35
36
  }
@@ -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();