@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,79 +1,82 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const db = require("@tanstack/db");
4
- const types = require("../types.cjs");
5
4
  class OfflineTransaction {
6
5
  // Will be typed properly - reference to OfflineExecutor
7
6
  constructor(options, mutationFn, persistTransaction, executor) {
8
7
  this.transaction = null;
8
+ this.persisting = false;
9
9
  this.offlineId = db.safeRandomUUID();
10
10
  this.mutationFnName = options.mutationFnName;
11
- this.autoCommit = options.autoCommit ?? true;
11
+ this.autoCommit = options.autoCommit;
12
12
  this.idempotencyKey = options.idempotencyKey ?? db.safeRandomUUID();
13
13
  this.metadata = options.metadata ?? {};
14
14
  this.persistTransaction = persistTransaction;
15
15
  this.executor = executor;
16
16
  }
17
17
  mutate(callback) {
18
- this.transaction = db.createTransaction({
19
- id: this.offlineId,
20
- autoCommit: false,
21
- mutationFn: async () => {
22
- const offlineTransaction = {
23
- id: this.offlineId,
24
- mutationFnName: this.mutationFnName,
25
- mutations: this.transaction.mutations,
26
- keys: this.extractKeys(this.transaction.mutations),
27
- idempotencyKey: this.idempotencyKey,
28
- createdAt: /* @__PURE__ */ new Date(),
29
- retryCount: 0,
30
- nextAttemptAt: Date.now(),
31
- metadata: this.metadata,
32
- spanContext: void 0,
33
- version: 1
34
- };
35
- const completionPromise = this.executor.waitForTransactionCompletion(
36
- this.offlineId
37
- );
38
- try {
39
- await Promise.race([
40
- this.persistTransaction(offlineTransaction),
41
- completionPromise
42
- ]);
43
- await completionPromise;
44
- } catch (error) {
45
- const normalizedError = error instanceof Error ? error : new Error(String(error));
46
- this.executor.rejectTransaction(this.offlineId, normalizedError);
47
- throw error;
18
+ if (!this.transaction) {
19
+ this.transaction = db.createTransaction({
20
+ id: this.offlineId,
21
+ autoCommit: this.autoCommit,
22
+ mutationFn: async () => {
23
+ this.persisting = true;
24
+ const offlineTransaction = {
25
+ id: this.offlineId,
26
+ mutationFnName: this.mutationFnName,
27
+ mutations: this.transaction.mutations,
28
+ keys: this.extractKeys(this.transaction.mutations),
29
+ idempotencyKey: this.idempotencyKey,
30
+ createdAt: /* @__PURE__ */ new Date(),
31
+ retryCount: 0,
32
+ nextAttemptAt: Date.now(),
33
+ metadata: this.metadata,
34
+ spanContext: void 0,
35
+ version: 1
36
+ };
37
+ const completionPromise = this.executor.waitForTransactionCompletion(
38
+ this.offlineId
39
+ );
40
+ try {
41
+ await Promise.race([
42
+ this.persistTransaction(offlineTransaction),
43
+ completionPromise
44
+ ]);
45
+ await completionPromise;
46
+ } catch (error) {
47
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
48
+ this.executor.rejectTransaction(this.offlineId, normalizedError);
49
+ throw error;
50
+ }
51
+ return;
52
+ },
53
+ metadata: this.metadata
54
+ });
55
+ void this.transaction.when(`settled`).then(
56
+ () => {
57
+ if (!this.persisting)
58
+ this.executor.resolveTransaction(this.offlineId, void 0);
59
+ },
60
+ (error) => {
61
+ if (!this.persisting)
62
+ this.executor.rejectTransaction(
63
+ this.offlineId,
64
+ error instanceof Error ? error : new Error(`Offline transaction was rolled back`)
65
+ );
48
66
  }
49
- return;
50
- },
51
- metadata: this.metadata
52
- });
67
+ );
68
+ }
53
69
  this.transaction.mutate(() => {
54
70
  callback();
55
71
  });
56
- if (this.autoCommit) {
57
- this.commit().catch((error) => {
58
- console.error(`Auto-commit failed:`, error);
59
- throw error;
60
- });
61
- }
62
72
  return this.transaction;
63
73
  }
64
74
  async commit() {
65
75
  if (!this.transaction) {
66
76
  throw new Error(`No mutations to commit. Call mutate() first.`);
67
77
  }
68
- try {
69
- await this.transaction.commit();
70
- return this.transaction;
71
- } catch (error) {
72
- if (error instanceof types.NonRetriableError) {
73
- this.transaction.rollback();
74
- }
75
- throw error;
76
- }
78
+ await this.transaction.commit();
79
+ return this.transaction;
77
80
  }
78
81
  rollback() {
79
82
  if (this.transaction) {
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineTransaction.cjs","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { createTransaction, safeRandomUUID } from '@tanstack/db'\nimport { NonRetriableError } from '../types'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n ) {\n this.offlineId = safeRandomUUID()\n this.mutationFnName = options.mutationFnName\n this.autoCommit = options.autoCommit ?? true\n this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n this.transaction = createTransaction({\n id: this.offlineId,\n autoCommit: false,\n mutationFn: async () => {\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId,\n )\n\n try {\n // Persistence also drives the shared queue. This transaction can finish\n // before that queue drains; observe both promises from the outset.\n await Promise.race([\n this.persistTransaction(offlineTransaction),\n completionPromise,\n ])\n // A queue pause (offline or retry) is not transaction completion.\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n\n this.transaction.mutate(() => {\n callback()\n })\n\n if (this.autoCommit) {\n // Auto-commit for direct OfflineTransaction usage\n this.commit().catch((error) => {\n console.error(`Auto-commit failed:`, error)\n throw error\n })\n }\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n try {\n // Commit the TanStack DB transaction\n // This will trigger the mutationFn which handles persistence and waiting\n await this.transaction.commit()\n return this.transaction\n } catch (error) {\n // Only rollback for NonRetriableError - other errors should allow retry\n if (error instanceof NonRetriableError) {\n this.transaction.rollback()\n }\n throw error\n }\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":["safeRandomUUID","createTransaction","NonRetriableError"],"mappings":";;;;AASO,MAAM,mBAAmB;AAAA;AAAA,EAU9B,YACE,SACA,YACA,oBACA,UACA;AATF,SAAQ,cAAkC;AAUxC,SAAK,YAAYA,kBAAA;AACjB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,iBAAiB,QAAQ,kBAAkBA,GAAAA,eAAA;AAChD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AACxC,SAAK,cAAcC,qBAAkB;AAAA,MACnC,IAAI,KAAK;AAAA,MACT,YAAY;AAAA,MACZ,YAAY,YAAY;AAGtB,cAAM,qBAA6C;AAAA,UACjD,IAAI,KAAK;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,WAAW,KAAK,YAAa;AAAA,UAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,UAClD,gBAAgB,KAAK;AAAA,UACrB,+BAAe,KAAA;AAAA,UACf,YAAY;AAAA,UACZ,eAAe,KAAK,IAAA;AAAA,UACpB,UAAU,KAAK;AAAA,UACf,aAAa;AAAA,UACb,SAAS;AAAA,QAAA;AAGX,cAAM,oBAAoB,KAAK,SAAS;AAAA,UACtC,KAAK;AAAA,QAAA;AAGP,YAAI;AAGF,gBAAM,QAAQ,KAAK;AAAA,YACjB,KAAK,mBAAmB,kBAAkB;AAAA,YAC1C;AAAA,UAAA,CACD;AAED,gBAAM;AAAA,QACR,SAAS,OAAO;AACd,gBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,eAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,gBAAM;AAAA,QACR;AAEA;AAAA,MACF;AAAA,MACA,UAAU,KAAK;AAAA,IAAA,CAChB;AAED,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,QAAI,KAAK,YAAY;AAEnB,WAAK,OAAA,EAAS,MAAM,CAAC,UAAU;AAC7B,gBAAQ,MAAM,uBAAuB,KAAK;AAC1C,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAEA,QAAI;AAGF,YAAM,KAAK,YAAY,OAAA;AACvB,aAAO,KAAK;AAAA,IACd,SAAS,OAAO;AAEd,UAAI,iBAAiBC,MAAAA,mBAAmB;AACtC,aAAK,YAAY,SAAA;AAAA,MACnB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;;"}
1
+ {"version":3,"file":"OfflineTransaction.cjs","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { createTransaction, safeRandomUUID } from '@tanstack/db'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean | undefined\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n /** The mutation function ran, so the executor settles the waiter. */\n private persisting = false\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n ) {\n this.offlineId = safeRandomUUID()\n this.mutationFnName = options.mutationFnName\n // The TanStack DB transaction applies the default.\n this.autoCommit = options.autoCommit\n this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n // One offline transaction is one transaction, and its id is unique among\n // live transactions. Repeated calls add to it while it is pending. With\n // autoCommit, it commits after this callback and reports a failure\n // through `when('settled')`, so a later call throws: it is no longer pending.\n if (!this.transaction) {\n this.transaction = createTransaction({\n id: this.offlineId,\n autoCommit: this.autoCommit,\n mutationFn: async () => {\n this.persisting = true\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId,\n )\n\n try {\n // Persistence also drives the shared queue. This transaction can finish\n // before that queue drains; observe both promises from the outset.\n await Promise.race([\n this.persistTransaction(offlineTransaction),\n completionPromise,\n ])\n // A queue pause (offline or retry) is not transaction completion.\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n // A transaction without mutations completes, and one rolled back before\n // it commits fails, without its mutation function, so the executor never\n // settles its waiter. Settle that waiter here with the same outcome. Once\n // the mutation function runs, only the executor settles it.\n void this.transaction.when(`settled`).then(\n () => {\n if (!this.persisting)\n this.executor.resolveTransaction(this.offlineId, undefined)\n },\n (error: unknown) => {\n if (!this.persisting)\n this.executor.rejectTransaction(\n this.offlineId,\n error instanceof Error\n ? error\n : new Error(`Offline transaction was rolled back`),\n )\n },\n )\n }\n\n this.transaction.mutate(() => {\n callback()\n })\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n // The mutationFn persists the transaction and waits for the executor. A\n // commit that rejects has already rolled the transaction back. A sibling's\n // failure can also roll it back while it waits, and then commit() resolves,\n // so `when('settled')` is the receipt.\n await this.transaction.commit()\n return this.transaction\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":["safeRandomUUID","createTransaction"],"mappings":";;;AAQO,MAAM,mBAAmB;AAAA;AAAA,EAY9B,YACE,SACA,YACA,oBACA,UACA;AAXF,SAAQ,cAAkC;AAE1C,SAAQ,aAAa;AAUnB,SAAK,YAAYA,kBAAA;AACjB,SAAK,iBAAiB,QAAQ;AAE9B,SAAK,aAAa,QAAQ;AAC1B,SAAK,iBAAiB,QAAQ,kBAAkBA,GAAAA,eAAA;AAChD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AAKxC,QAAI,CAAC,KAAK,aAAa;AACrB,WAAK,cAAcC,qBAAkB;AAAA,QACnC,IAAI,KAAK;AAAA,QACT,YAAY,KAAK;AAAA,QACjB,YAAY,YAAY;AACtB,eAAK,aAAa;AAGlB,gBAAM,qBAA6C;AAAA,YACjD,IAAI,KAAK;AAAA,YACT,gBAAgB,KAAK;AAAA,YACrB,WAAW,KAAK,YAAa;AAAA,YAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,YAClD,gBAAgB,KAAK;AAAA,YACrB,+BAAe,KAAA;AAAA,YACf,YAAY;AAAA,YACZ,eAAe,KAAK,IAAA;AAAA,YACpB,UAAU,KAAK;AAAA,YACf,aAAa;AAAA,YACb,SAAS;AAAA,UAAA;AAGX,gBAAM,oBAAoB,KAAK,SAAS;AAAA,YACtC,KAAK;AAAA,UAAA;AAGP,cAAI;AAGF,kBAAM,QAAQ,KAAK;AAAA,cACjB,KAAK,mBAAmB,kBAAkB;AAAA,cAC1C;AAAA,YAAA,CACD;AAED,kBAAM;AAAA,UACR,SAAS,OAAO;AACd,kBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,iBAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,kBAAM;AAAA,UACR;AAEA;AAAA,QACF;AAAA,QACA,UAAU,KAAK;AAAA,MAAA,CAChB;AAKD,WAAK,KAAK,YAAY,KAAK,SAAS,EAAE;AAAA,QACpC,MAAM;AACJ,cAAI,CAAC,KAAK;AACR,iBAAK,SAAS,mBAAmB,KAAK,WAAW,MAAS;AAAA,QAC9D;AAAA,QACA,CAAC,UAAmB;AAClB,cAAI,CAAC,KAAK;AACR,iBAAK,SAAS;AAAA,cACZ,KAAK;AAAA,cACL,iBAAiB,QACb,QACA,IAAI,MAAM,qCAAqC;AAAA,YAAA;AAAA,QAEzD;AAAA,MAAA;AAAA,IAEJ;AAEA,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAMA,UAAM,KAAK,YAAY,OAAA;AACvB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;;"}
@@ -7,6 +7,8 @@ export declare class OfflineTransaction {
7
7
  private idempotencyKey;
8
8
  private metadata;
9
9
  private transaction;
10
+ /** The mutation function ran, so the executor settles the waiter. */
11
+ private persisting;
10
12
  private persistTransaction;
11
13
  private executor;
12
14
  constructor(options: CreateOfflineTransactionOptions, mutationFn: OfflineMutationFn, persistTransaction: (tx: OfflineTransactionType) => Promise<void>, executor: any);
@@ -1,51 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const tracer = require("../telemetry/tracer.cjs");
4
3
  class KeyScheduler {
5
4
  constructor() {
6
5
  this.pendingTransactions = [];
7
6
  }
8
7
  schedule(transaction) {
9
- return tracer.withSyncSpan(
10
- `scheduler.schedule`,
11
- {
12
- "transaction.id": transaction.id,
13
- queueLength: this.pendingTransactions.length
14
- },
15
- () => {
16
- if (this.pendingTransactions.some(
17
- (pending) => pending.id === transaction.id
18
- )) {
19
- return false;
20
- }
21
- this.pendingTransactions.push(transaction);
22
- this.pendingTransactions.sort(
23
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
24
- );
25
- return true;
26
- }
8
+ if (this.pendingTransactions.some((pending) => pending.id === transaction.id)) {
9
+ return false;
10
+ }
11
+ this.pendingTransactions.push(transaction);
12
+ this.pendingTransactions.sort(
13
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
27
14
  );
15
+ return true;
28
16
  }
29
17
  getNext() {
30
- return tracer.withSyncSpan(
31
- `scheduler.getNext`,
32
- { pendingCount: this.pendingTransactions.length },
33
- (span) => {
34
- if (this.activeTransactionId !== void 0 || this.pendingTransactions.length === 0) {
35
- span.setAttribute(`result`, `empty`);
36
- return void 0;
37
- }
38
- const firstTransaction = this.pendingTransactions[0];
39
- if (!this.isReadyToRun(firstTransaction)) {
40
- span.setAttribute(`result`, `waiting_for_first`);
41
- span.setAttribute(`transaction.id`, firstTransaction.id);
42
- return void 0;
43
- }
44
- span.setAttribute(`result`, `found`);
45
- span.setAttribute(`transaction.id`, firstTransaction.id);
46
- return firstTransaction;
47
- }
48
- );
18
+ if (this.activeTransactionId !== void 0 || this.pendingTransactions.length === 0) {
19
+ return void 0;
20
+ }
21
+ const firstTransaction = this.pendingTransactions[0];
22
+ return this.isReadyToRun(firstTransaction) ? firstTransaction : void 0;
49
23
  }
50
24
  isReadyToRun(transaction) {
51
25
  return Date.now() >= transaction.nextAttemptAt;
@@ -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 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
+ {"version":3,"file":"KeyScheduler.cjs","sources":["../../../src/executor/KeyScheduler.ts"],"sourcesContent":["import 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 if (\n this.pendingTransactions.some((pending) => pending.id === transaction.id)\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 getNext(): OfflineTransaction | undefined {\n if (\n this.activeTransactionId !== undefined ||\n this.pendingTransactions.length === 0\n ) {\n return undefined\n }\n const firstTransaction = this.pendingTransactions[0]!\n return this.isReadyToRun(firstTransaction) ? firstTransaction : undefined\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":[],"mappings":";;AAEO,MAAM,aAAa;AAAA,EAAnB,cAAA;AACL,SAAQ,sBAAiD,CAAA;AAAA,EAAC;AAAA,EAG1D,SAAS,aAA0C;AACjD,QACE,KAAK,oBAAoB,KAAK,CAAC,YAAY,QAAQ,OAAO,YAAY,EAAE,GACxE;AACA,aAAO;AAAA,IACT;AACA,SAAK,oBAAoB,KAAK,WAAW;AAEzC,SAAK,oBAAoB;AAAA,MACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,IAAQ;AAExD,WAAO;AAAA,EACT;AAAA,EAEA,UAA0C;AACxC,QACE,KAAK,wBAAwB,UAC7B,KAAK,oBAAoB,WAAW,GACpC;AACA,aAAO;AAAA,IACT;AACA,UAAM,mBAAmB,KAAK,oBAAoB,CAAC;AACnD,WAAO,KAAK,aAAa,gBAAgB,IAAI,mBAAmB;AAAA,EAClE;AAAA,EAEQ,aAAa,aAA0C;AAC7D,WAAO,KAAK,SAAS,YAAY;AAAA,EACnC;AAAA,EAEA,YAAY,aAAuC;AACjD,SAAK,sBAAsB,YAAY;AAAA,EACzC;AAAA,EAEA,cAAc,aAAuC;AACnD,SAAK,kBAAkB,WAAW;AAClC,SAAK,sBAAsB;AAAA,EAC7B;AAAA,EAEA,WAAW,cAAwC;AACjD,SAAK,sBAAsB;AAAA,EAC7B;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,SAAK,mBAAmB,CAAC,WAAW,CAAC;AAAA,EACvC;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,oBAAoB;AAAA,EAClC;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,wBAAwB,SAAY,IAAI;AAAA,EACtD;AAAA,EAEA,QAAc;AACZ,SAAK,sBAAsB,CAAA;AAC3B,SAAK,sBAAsB;AAAA,EAC7B;AAAA;AAAA,EAGA,0BAA0B,gBAAiD;AACzE,UAAM,MAAM,IAAI,IAAI,cAAc;AAClC,QAAI,KAAK,wBAAwB;AAC/B,UAAI,OAAO,KAAK,mBAAmB;AACrC,SAAK,sBAAsB,KAAK,oBAAoB;AAAA,MAClD,CAAC,EAAE,GAAA,MAAS,CAAC,IAAI,IAAI,EAAE;AAAA,IAAA;AAEzB,WAAO,CAAC,GAAG,GAAG;AAAA,EAChB;AAAA,EAEA,4BAAuD;AACrD,WAAO,CAAC,GAAG,KAAK,mBAAmB;AAAA,EACrC;AAAA,EAEA,mBAAmB,qBAAsD;AACvE,UAAM,cAAc,IAAI;AAAA,MACtB,oBAAoB,IAAI,CAAC,gBAAgB,CAAC,YAAY,IAAI,WAAW,CAAC;AAAA,IAAA;AAExE,SAAK,sBAAsB,KAAK,oBAAoB;AAAA,MAClD,CAAC,gBAAgB,YAAY,IAAI,YAAY,EAAE,KAAK;AAAA,IAAA;AAGtD,SAAK,oBAAoB;AAAA,MACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,IAAQ;AAAA,EAE1D;AACF;;"}
@@ -4,8 +4,10 @@ const db = require("@tanstack/db");
4
4
  const OutboxManager = require("../outbox/OutboxManager.cjs");
5
5
  const RetryPolicy = require("../retry/RetryPolicy.cjs");
6
6
  const types = require("../types.cjs");
7
- const tracer = require("../telemetry/tracer.cjs");
8
7
  const HANDLED_EXECUTION_ERROR = /* @__PURE__ */ Symbol(`HandledExecutionError`);
8
+ function toError(value) {
9
+ return value instanceof Error || Object.prototype.toString.call(value) === `[object Error]` ? value : new Error(String(value));
10
+ }
9
11
  class TransactionExecutor {
10
12
  constructor(scheduler, outbox, config, offlineExecutor) {
11
13
  this.isExecuting = false;
@@ -55,61 +57,39 @@ class TransactionExecutor {
55
57
  }
56
58
  async executeTransaction(transaction) {
57
59
  try {
58
- await tracer.withNestedSpan(
59
- `transaction.execute`,
60
- {
61
- "transaction.id": transaction.id,
62
- "transaction.mutationFnName": transaction.mutationFnName,
63
- "transaction.retryCount": transaction.retryCount,
64
- "transaction.keyCount": transaction.keys.length
65
- },
66
- async (span) => {
67
- const terminalError = transaction.outboxPhase === `rejection-pending` ? this.getTerminalError(transaction) : void 0;
68
- this.scheduler.markStarted(transaction);
69
- if (transaction.retryCount > 0) {
70
- span.setAttribute(`retry.attempt`, transaction.retryCount);
71
- }
72
- if (!transaction.outboxPhase) {
73
- try {
74
- await this.runMutationFn(transaction);
75
- } catch (error) {
76
- const err = error instanceof Error ? error : new Error(String(error));
77
- span.setAttribute(`result`, `error`);
78
- await this.handleError(transaction, err);
79
- err[HANDLED_EXECUTION_ERROR] = true;
80
- throw err;
81
- }
82
- }
83
- const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
84
- try {
85
- await this.removeSettledTransaction(
86
- deletionPending,
87
- !transaction.outboxPhase
88
- );
89
- } catch (error) {
90
- const storageError = error;
91
- span.recordException(storageError);
92
- span.setAttribute(`result`, `outbox_failure`);
93
- this.offlineExecutor.rejectTransaction(
94
- transaction.id,
95
- terminalError ?? storageError
96
- );
97
- throw storageError;
98
- }
99
- if (terminalError) {
100
- span.setAttribute(`result`, `permanent_failure`);
101
- this.offlineExecutor.rejectTransaction(
102
- transaction.id,
103
- terminalError
104
- );
105
- } else {
106
- span.setAttribute(`result`, `success`);
107
- this.offlineExecutor.resolveTransaction(transaction.id, void 0);
108
- }
60
+ const terminalError = transaction.outboxPhase === `rejection-pending` ? this.getTerminalError(transaction) : void 0;
61
+ this.scheduler.markStarted(transaction);
62
+ if (!transaction.outboxPhase) {
63
+ try {
64
+ await this.runMutationFn(transaction);
65
+ } catch (error) {
66
+ const err = toError(error);
67
+ await this.handleError(transaction, err);
68
+ err[HANDLED_EXECUTION_ERROR] = true;
69
+ throw err;
109
70
  }
110
- );
71
+ }
72
+ const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
73
+ try {
74
+ await this.removeSettledTransaction(
75
+ deletionPending,
76
+ !transaction.outboxPhase
77
+ );
78
+ } catch (error) {
79
+ const storageError = error;
80
+ this.offlineExecutor.rejectTransaction(
81
+ transaction.id,
82
+ terminalError ?? storageError
83
+ );
84
+ throw storageError;
85
+ }
86
+ if (terminalError) {
87
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
88
+ } else {
89
+ this.offlineExecutor.resolveTransaction(transaction.id, void 0);
90
+ }
111
91
  } catch (error) {
112
- if (error instanceof Error && error[HANDLED_EXECUTION_ERROR] === true) {
92
+ if (error !== null && typeof error === `object` && error[HANDLED_EXECUTION_ERROR] === true) {
113
93
  return;
114
94
  }
115
95
  throw error;
@@ -135,74 +115,87 @@ class TransactionExecutor {
135
115
  });
136
116
  }
137
117
  async handleError(transaction, error) {
138
- return tracer.withNestedSpan(
139
- `transaction.handleError`,
140
- {
141
- "transaction.id": transaction.id,
142
- "error.name": error.name,
143
- "error.message": error.message
144
- },
145
- async (span) => {
146
- const shouldRetry = this.retryPolicy.shouldRetry(
147
- error,
148
- transaction.retryCount
149
- );
150
- span.setAttribute(`shouldRetry`, shouldRetry);
151
- if (!shouldRetry) {
152
- const rejectionPending = {
153
- ...transaction,
154
- outboxPhase: `rejection-pending`,
155
- lastError: {
156
- name: error.name,
157
- message: error.message,
158
- stack: error.stack
159
- }
160
- };
161
- console.warn(
162
- `Transaction ${transaction.id} failed permanently:`,
163
- error
118
+ let shouldRetry;
119
+ let hookFailure;
120
+ if (error instanceof types.NonRetriableError) {
121
+ shouldRetry = false;
122
+ } else {
123
+ let decision;
124
+ try {
125
+ decision = this.config.shouldRetry?.(error, transaction.retryCount);
126
+ if (decision !== void 0 && typeof decision !== `boolean`) {
127
+ void Promise.resolve(decision).catch(() => {
128
+ });
129
+ throw new TypeError(
130
+ `OfflineConfig.shouldRetry must return true, false, or undefined`
164
131
  );
165
- try {
166
- await this.removeSettledTransaction(rejectionPending, true);
167
- } catch (storageError) {
168
- span.recordException(storageError);
169
- span.setAttribute(`result`, `outbox_failure`);
170
- this.offlineExecutor.rejectTransaction(transaction.id, error);
171
- throw storageError;
172
- }
173
- span.setAttribute(`result`, `permanent_failure`);
174
- this.offlineExecutor.rejectTransaction(transaction.id, error);
175
- return;
176
132
  }
177
- const delay = Math.max(
178
- 0,
179
- this.retryPolicy.calculateDelay(transaction.retryCount)
180
- );
181
- const updatedTransaction = {
182
- ...transaction,
183
- retryCount: transaction.retryCount + 1,
184
- nextAttemptAt: Date.now() + delay,
185
- lastError: {
186
- name: error.name,
187
- message: error.message,
188
- stack: error.stack
189
- }
190
- };
191
- span.setAttribute(`retryDelay`, delay);
192
- span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount);
193
- this.scheduler.updateTransaction(updatedTransaction);
194
- try {
195
- await this.outbox.update(transaction.id, updatedTransaction);
196
- span.setAttribute(`result`, `scheduled_retry`);
197
- } catch (persistError) {
198
- span.recordException(persistError);
199
- span.setAttribute(`result`, `persist_failed`);
200
- throw persistError;
201
- } finally {
202
- this.scheduler.markFailed(transaction);
133
+ } catch (hookError) {
134
+ hookFailure = toError(hookError);
135
+ }
136
+ if (hookFailure) {
137
+ shouldRetry = false;
138
+ } else {
139
+ shouldRetry = decision ?? this.retryPolicy.shouldRetry(error, transaction.retryCount);
140
+ }
141
+ }
142
+ if (!shouldRetry) {
143
+ const terminalError = hookFailure ?? error;
144
+ const rejectionPending = {
145
+ ...transaction,
146
+ outboxPhase: `rejection-pending`,
147
+ lastError: {
148
+ name: terminalError.name,
149
+ message: terminalError.message,
150
+ stack: terminalError.stack
203
151
  }
152
+ };
153
+ if (hookFailure) {
154
+ console.warn(
155
+ `Retry decision failed for transaction ${transaction.id}:`,
156
+ hookFailure,
157
+ `Named mutation function error:`,
158
+ error
159
+ );
160
+ } else {
161
+ console.warn(`Transaction ${transaction.id} failed permanently:`, error);
204
162
  }
163
+ try {
164
+ await this.removeSettledTransaction(rejectionPending, true);
165
+ } catch (storageError) {
166
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
167
+ throw storageError;
168
+ }
169
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
170
+ return;
171
+ }
172
+ const delay = Math.max(
173
+ 0,
174
+ this.retryPolicy.calculateDelay(transaction.retryCount)
205
175
  );
176
+ const updatedTransaction = {
177
+ ...transaction,
178
+ retryCount: transaction.retryCount + 1,
179
+ nextAttemptAt: Date.now() + delay,
180
+ lastError: {
181
+ name: error.name,
182
+ message: error.message,
183
+ stack: error.stack
184
+ }
185
+ };
186
+ this.scheduler.updateTransaction(updatedTransaction);
187
+ try {
188
+ await this.outbox.update(transaction.id, updatedTransaction);
189
+ } catch (persistError) {
190
+ if (persistError instanceof OutboxManager.OutboxTransactionNotFoundError) {
191
+ this.scheduler.markCompleted(transaction);
192
+ this.offlineExecutor.rejectTransaction(transaction.id, error);
193
+ return;
194
+ }
195
+ throw persistError;
196
+ } finally {
197
+ this.scheduler.markFailed(transaction);
198
+ }
206
199
  }
207
200
  async removeSettledTransaction(transaction, persistMarker) {
208
201
  try {
@@ -216,7 +209,7 @@ class TransactionExecutor {
216
209
  await this.outbox.remove(transaction.id);
217
210
  this.scheduler.markCompleted(transaction);
218
211
  } catch (error) {
219
- const storageError = error instanceof Error ? error : new Error(String(error));
212
+ const storageError = toError(error);
220
213
  this.fatalError = storageError;
221
214
  this.scheduler.markFailed(transaction);
222
215
  this.clearRetryTimer();
@@ -299,30 +292,22 @@ class TransactionExecutor {
299
292
  mutationFn: async () => {
300
293
  }
301
294
  });
302
- restorationTx.isPersisted.promise.catch(() => {
303
- });
304
295
  restorationTx.applyMutations(offlineTx.mutations);
305
- const touchedCollections = /* @__PURE__ */ new Set();
306
- for (const mutation of offlineTx.mutations) {
307
- if (!mutation.collection) {
308
- continue;
309
- }
310
- const collectionId = mutation.collection.id;
311
- if (touchedCollections.has(collectionId)) {
312
- continue;
296
+ try {
297
+ for (const mutation of offlineTx.mutations) {
298
+ if (mutation.collection._state.trackTransaction(restorationTx))
299
+ mutation.collection._state.recomputeOptimisticState(true);
313
300
  }
314
- touchedCollections.add(collectionId);
315
- mutation.collection._state.transactions.set(
316
- restorationTx.id,
317
- restorationTx
318
- );
319
- mutation.collection._state.recomputeOptimisticState(true);
301
+ } catch (error) {
302
+ restorationTx.rollback({ isSecondaryRollback: true });
303
+ throw error;
320
304
  }
321
305
  this.offlineExecutor.registerRestorationTransaction(
322
306
  offlineTx.id,
323
307
  restorationTx
324
308
  );
325
309
  } catch (error) {
310
+ if (error instanceof db.DuplicateTransactionIdError) continue;
326
311
  console.warn(
327
312
  `Failed to restore optimistic state for transaction ${offlineTx.id}:`,
328
313
  error