@tanstack/offline-transactions 1.1.0 → 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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/OfflineExecutor.cjs +72 -80
  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 +106 -161
  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/esm/OfflineExecutor.d.ts +6 -0
  17. package/dist/esm/OfflineExecutor.js +72 -80
  18. package/dist/esm/OfflineExecutor.js.map +1 -1
  19. package/dist/esm/api/OfflineAction.js +4 -15
  20. package/dist/esm/api/OfflineAction.js.map +1 -1
  21. package/dist/esm/api/OfflineTransaction.d.ts +2 -0
  22. package/dist/esm/api/OfflineTransaction.js +54 -51
  23. package/dist/esm/api/OfflineTransaction.js.map +1 -1
  24. package/dist/esm/executor/KeyScheduler.js +12 -38
  25. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  26. package/dist/esm/executor/TransactionExecutor.js +106 -161
  27. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  28. package/dist/esm/outbox/OutboxManager.js +48 -77
  29. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/OfflineExecutor.ts +93 -94
  32. package/src/api/OfflineAction.ts +4 -22
  33. package/src/api/OfflineTransaction.ts +78 -66
  34. package/src/executor/KeyScheduler.ts +18 -47
  35. package/src/executor/TransactionExecutor.ts +122 -186
  36. package/src/outbox/OutboxManager.ts +54 -86
  37. package/dist/cjs/retry/NonRetriableError.d.cts +0 -1
  38. package/dist/cjs/telemetry/tracer.cjs +0 -27
  39. package/dist/cjs/telemetry/tracer.cjs.map +0 -1
  40. package/dist/cjs/telemetry/tracer.d.cts +0 -30
  41. package/dist/esm/retry/NonRetriableError.d.ts +0 -1
  42. package/dist/esm/telemetry/tracer.d.ts +0 -30
  43. package/dist/esm/telemetry/tracer.js +0 -27
  44. package/dist/esm/telemetry/tracer.js.map +0 -1
  45. package/src/retry/NonRetriableError.ts +0 -1
  46. package/src/telemetry/tracer.ts +0 -66
@@ -1,77 +1,80 @@
1
1
  import { safeRandomUUID, createTransaction } from "@tanstack/db";
2
- import { NonRetriableError } from "../types.js";
3
2
  class OfflineTransaction {
4
3
  // Will be typed properly - reference to OfflineExecutor
5
4
  constructor(options, mutationFn, persistTransaction, executor) {
6
5
  this.transaction = null;
6
+ this.persisting = false;
7
7
  this.offlineId = safeRandomUUID();
8
8
  this.mutationFnName = options.mutationFnName;
9
- this.autoCommit = options.autoCommit ?? true;
9
+ this.autoCommit = options.autoCommit;
10
10
  this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID();
11
11
  this.metadata = options.metadata ?? {};
12
12
  this.persistTransaction = persistTransaction;
13
13
  this.executor = executor;
14
14
  }
15
15
  mutate(callback) {
16
- this.transaction ??= createTransaction({
17
- id: this.offlineId,
18
- autoCommit: false,
19
- mutationFn: async () => {
20
- const offlineTransaction = {
21
- id: this.offlineId,
22
- mutationFnName: this.mutationFnName,
23
- mutations: this.transaction.mutations,
24
- keys: this.extractKeys(this.transaction.mutations),
25
- idempotencyKey: this.idempotencyKey,
26
- createdAt: /* @__PURE__ */ new Date(),
27
- retryCount: 0,
28
- nextAttemptAt: Date.now(),
29
- metadata: this.metadata,
30
- spanContext: void 0,
31
- version: 1
32
- };
33
- const completionPromise = this.executor.waitForTransactionCompletion(
34
- this.offlineId
35
- );
36
- try {
37
- await Promise.race([
38
- this.persistTransaction(offlineTransaction),
39
- completionPromise
40
- ]);
41
- await completionPromise;
42
- } catch (error) {
43
- const normalizedError = error instanceof Error ? error : new Error(String(error));
44
- this.executor.rejectTransaction(this.offlineId, normalizedError);
45
- throw error;
16
+ if (!this.transaction) {
17
+ this.transaction = createTransaction({
18
+ id: this.offlineId,
19
+ autoCommit: this.autoCommit,
20
+ mutationFn: async () => {
21
+ this.persisting = true;
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;
48
+ }
49
+ return;
50
+ },
51
+ metadata: this.metadata
52
+ });
53
+ void this.transaction.when(`settled`).then(
54
+ () => {
55
+ if (!this.persisting)
56
+ this.executor.resolveTransaction(this.offlineId, void 0);
57
+ },
58
+ (error) => {
59
+ if (!this.persisting)
60
+ this.executor.rejectTransaction(
61
+ this.offlineId,
62
+ error instanceof Error ? error : new Error(`Offline transaction was rolled back`)
63
+ );
46
64
  }
47
- return;
48
- },
49
- metadata: this.metadata
50
- });
65
+ );
66
+ }
51
67
  this.transaction.mutate(() => {
52
68
  callback();
53
69
  });
54
- if (this.autoCommit) {
55
- this.commit().catch((error) => {
56
- console.error(`Auto-commit failed:`, error);
57
- throw error;
58
- });
59
- }
60
70
  return this.transaction;
61
71
  }
62
72
  async commit() {
63
73
  if (!this.transaction) {
64
74
  throw new Error(`No mutations to commit. Call mutate() first.`);
65
75
  }
66
- try {
67
- await this.transaction.commit();
68
- return this.transaction;
69
- } catch (error) {
70
- if (error instanceof NonRetriableError) {
71
- this.transaction.rollback();
72
- }
73
- throw error;
74
- }
76
+ await this.transaction.commit();
77
+ return this.transaction;
75
78
  }
76
79
  rollback() {
77
80
  if (this.transaction) {
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineTransaction.js","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 // Repeated calls add to the same transaction: one offline transaction is\n // one transaction, and its id is unique among live transactions.\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":[],"mappings":";;AASO,MAAM,mBAAmB;AAAA;AAAA,EAU9B,YACE,SACA,YACA,oBACA,UACA;AATF,SAAQ,cAAkC;AAUxC,SAAK,YAAY,eAAA;AACjB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,iBAAiB,QAAQ,kBAAkB,eAAA;AAChD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AAGxC,SAAK,gBAAgB,kBAAkB;AAAA,MACrC,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,iBAAiB,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.js","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":[],"mappings":";AAQO,MAAM,mBAAmB;AAAA;AAAA,EAY9B,YACE,SACA,YACA,oBACA,UACA;AAXF,SAAQ,cAAkC;AAE1C,SAAQ,aAAa;AAUnB,SAAK,YAAY,eAAA;AACjB,SAAK,iBAAiB,QAAQ;AAE9B,SAAK,aAAa,QAAQ;AAC1B,SAAK,iBAAiB,QAAQ,kBAAkB,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,cAAc,kBAAkB;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;"}
@@ -1,49 +1,23 @@
1
- import { withSyncSpan } from "../telemetry/tracer.js";
2
1
  class KeyScheduler {
3
2
  constructor() {
4
3
  this.pendingTransactions = [];
5
4
  }
6
5
  schedule(transaction) {
7
- return withSyncSpan(
8
- `scheduler.schedule`,
9
- {
10
- "transaction.id": transaction.id,
11
- queueLength: this.pendingTransactions.length
12
- },
13
- () => {
14
- if (this.pendingTransactions.some(
15
- (pending) => pending.id === transaction.id
16
- )) {
17
- return false;
18
- }
19
- this.pendingTransactions.push(transaction);
20
- this.pendingTransactions.sort(
21
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
22
- );
23
- return true;
24
- }
6
+ if (this.pendingTransactions.some((pending) => pending.id === transaction.id)) {
7
+ return false;
8
+ }
9
+ this.pendingTransactions.push(transaction);
10
+ this.pendingTransactions.sort(
11
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
25
12
  );
13
+ return true;
26
14
  }
27
15
  getNext() {
28
- return withSyncSpan(
29
- `scheduler.getNext`,
30
- { pendingCount: this.pendingTransactions.length },
31
- (span) => {
32
- if (this.activeTransactionId !== void 0 || this.pendingTransactions.length === 0) {
33
- span.setAttribute(`result`, `empty`);
34
- return void 0;
35
- }
36
- const firstTransaction = this.pendingTransactions[0];
37
- if (!this.isReadyToRun(firstTransaction)) {
38
- span.setAttribute(`result`, `waiting_for_first`);
39
- span.setAttribute(`transaction.id`, firstTransaction.id);
40
- return void 0;
41
- }
42
- span.setAttribute(`result`, `found`);
43
- span.setAttribute(`transaction.id`, firstTransaction.id);
44
- return firstTransaction;
45
- }
46
- );
16
+ if (this.activeTransactionId !== void 0 || this.pendingTransactions.length === 0) {
17
+ return void 0;
18
+ }
19
+ const firstTransaction = this.pendingTransactions[0];
20
+ return this.isReadyToRun(firstTransaction) ? firstTransaction : void 0;
47
21
  }
48
22
  isReadyToRun(transaction) {
49
23
  return Date.now() >= transaction.nextAttemptAt;
@@ -1 +1 @@
1
- {"version":3,"file":"KeyScheduler.js","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":[],"mappings":"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"}
1
+ {"version":3,"file":"KeyScheduler.js","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;"}
@@ -2,7 +2,6 @@ import { createTransaction, DuplicateTransactionIdError } from "@tanstack/db";
2
2
  import { OutboxTransactionNotFoundError } from "../outbox/OutboxManager.js";
3
3
  import { DefaultRetryPolicy } from "../retry/RetryPolicy.js";
4
4
  import { NonRetriableError } from "../types.js";
5
- import { withNestedSpan } from "../telemetry/tracer.js";
6
5
  const HANDLED_EXECUTION_ERROR = /* @__PURE__ */ Symbol(`HandledExecutionError`);
7
6
  function toError(value) {
8
7
  return value instanceof Error || Object.prototype.toString.call(value) === `[object Error]` ? value : new Error(String(value));
@@ -56,59 +55,37 @@ class TransactionExecutor {
56
55
  }
57
56
  async executeTransaction(transaction) {
58
57
  try {
59
- await withNestedSpan(
60
- `transaction.execute`,
61
- {
62
- "transaction.id": transaction.id,
63
- "transaction.mutationFnName": transaction.mutationFnName,
64
- "transaction.retryCount": transaction.retryCount,
65
- "transaction.keyCount": transaction.keys.length
66
- },
67
- async (span) => {
68
- const terminalError = transaction.outboxPhase === `rejection-pending` ? this.getTerminalError(transaction) : void 0;
69
- this.scheduler.markStarted(transaction);
70
- if (transaction.retryCount > 0) {
71
- span.setAttribute(`retry.attempt`, transaction.retryCount);
72
- }
73
- if (!transaction.outboxPhase) {
74
- try {
75
- await this.runMutationFn(transaction);
76
- } catch (error) {
77
- const err = toError(error);
78
- span.setAttribute(`result`, `error`);
79
- await this.handleError(transaction, err);
80
- err[HANDLED_EXECUTION_ERROR] = true;
81
- throw err;
82
- }
83
- }
84
- const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
85
- try {
86
- await this.removeSettledTransaction(
87
- deletionPending,
88
- !transaction.outboxPhase
89
- );
90
- } catch (error) {
91
- const storageError = error;
92
- span.recordException(storageError);
93
- span.setAttribute(`result`, `outbox_failure`);
94
- this.offlineExecutor.rejectTransaction(
95
- transaction.id,
96
- terminalError ?? storageError
97
- );
98
- throw storageError;
99
- }
100
- if (terminalError) {
101
- span.setAttribute(`result`, `permanent_failure`);
102
- this.offlineExecutor.rejectTransaction(
103
- transaction.id,
104
- terminalError
105
- );
106
- } else {
107
- span.setAttribute(`result`, `success`);
108
- this.offlineExecutor.resolveTransaction(transaction.id, void 0);
109
- }
58
+ const terminalError = transaction.outboxPhase === `rejection-pending` ? this.getTerminalError(transaction) : void 0;
59
+ this.scheduler.markStarted(transaction);
60
+ if (!transaction.outboxPhase) {
61
+ try {
62
+ await this.runMutationFn(transaction);
63
+ } catch (error) {
64
+ const err = toError(error);
65
+ await this.handleError(transaction, err);
66
+ err[HANDLED_EXECUTION_ERROR] = true;
67
+ throw err;
110
68
  }
111
- );
69
+ }
70
+ const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
71
+ try {
72
+ await this.removeSettledTransaction(
73
+ deletionPending,
74
+ !transaction.outboxPhase
75
+ );
76
+ } catch (error) {
77
+ const storageError = error;
78
+ this.offlineExecutor.rejectTransaction(
79
+ transaction.id,
80
+ terminalError ?? storageError
81
+ );
82
+ throw storageError;
83
+ }
84
+ if (terminalError) {
85
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
86
+ } else {
87
+ this.offlineExecutor.resolveTransaction(transaction.id, void 0);
88
+ }
112
89
  } catch (error) {
113
90
  if (error !== null && typeof error === `object` && error[HANDLED_EXECUTION_ERROR] === true) {
114
91
  return;
@@ -136,117 +113,87 @@ class TransactionExecutor {
136
113
  });
137
114
  }
138
115
  async handleError(transaction, error) {
139
- return withNestedSpan(
140
- `transaction.handleError`,
141
- {
142
- "transaction.id": transaction.id,
143
- "error.name": error.name,
144
- "error.message": error.message
145
- },
146
- async (span) => {
147
- let shouldRetry;
148
- let hookFailure;
149
- if (error instanceof NonRetriableError) {
150
- shouldRetry = false;
151
- } else {
152
- let decision;
153
- try {
154
- decision = this.config.shouldRetry?.(error, transaction.retryCount);
155
- if (decision !== void 0 && typeof decision !== `boolean`) {
156
- void Promise.resolve(decision).catch(() => {
157
- });
158
- throw new TypeError(
159
- `OfflineConfig.shouldRetry must return true, false, or undefined`
160
- );
161
- }
162
- } catch (hookError) {
163
- hookFailure = toError(hookError);
164
- }
165
- if (hookFailure) {
166
- shouldRetry = false;
167
- } else {
168
- shouldRetry = decision ?? this.retryPolicy.shouldRetry(error, transaction.retryCount);
169
- }
170
- }
171
- span.setAttribute(`shouldRetry`, shouldRetry);
172
- if (!shouldRetry) {
173
- const terminalError = hookFailure ?? error;
174
- const rejectionPending = {
175
- ...transaction,
176
- outboxPhase: `rejection-pending`,
177
- lastError: {
178
- name: terminalError.name,
179
- message: terminalError.message,
180
- stack: terminalError.stack
181
- }
182
- };
183
- if (hookFailure) {
184
- span.recordException(hookFailure);
185
- console.warn(
186
- `Retry decision failed for transaction ${transaction.id}:`,
187
- hookFailure,
188
- `Named mutation function error:`,
189
- error
190
- );
191
- } else {
192
- console.warn(
193
- `Transaction ${transaction.id} failed permanently:`,
194
- error
195
- );
196
- }
197
- try {
198
- await this.removeSettledTransaction(rejectionPending, true);
199
- } catch (storageError) {
200
- span.recordException(storageError);
201
- span.setAttribute(`result`, `outbox_failure`);
202
- this.offlineExecutor.rejectTransaction(
203
- transaction.id,
204
- terminalError
205
- );
206
- throw storageError;
207
- }
208
- span.setAttribute(
209
- `result`,
210
- hookFailure ? `retry_decision_failure` : `permanent_failure`
116
+ let shouldRetry;
117
+ let hookFailure;
118
+ if (error instanceof NonRetriableError) {
119
+ shouldRetry = false;
120
+ } else {
121
+ let decision;
122
+ try {
123
+ decision = this.config.shouldRetry?.(error, transaction.retryCount);
124
+ if (decision !== void 0 && typeof decision !== `boolean`) {
125
+ void Promise.resolve(decision).catch(() => {
126
+ });
127
+ throw new TypeError(
128
+ `OfflineConfig.shouldRetry must return true, false, or undefined`
211
129
  );
212
- this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
213
- return;
214
130
  }
215
- const delay = Math.max(
216
- 0,
217
- this.retryPolicy.calculateDelay(transaction.retryCount)
218
- );
219
- const updatedTransaction = {
220
- ...transaction,
221
- retryCount: transaction.retryCount + 1,
222
- nextAttemptAt: Date.now() + delay,
223
- lastError: {
224
- name: error.name,
225
- message: error.message,
226
- stack: error.stack
227
- }
228
- };
229
- span.setAttribute(`retryDelay`, delay);
230
- span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount);
231
- this.scheduler.updateTransaction(updatedTransaction);
232
- try {
233
- await this.outbox.update(transaction.id, updatedTransaction);
234
- span.setAttribute(`result`, `scheduled_retry`);
235
- } catch (persistError) {
236
- if (persistError instanceof OutboxTransactionNotFoundError) {
237
- this.scheduler.markCompleted(transaction);
238
- span.setAttribute(`result`, `outbox_removed`);
239
- this.offlineExecutor.rejectTransaction(transaction.id, error);
240
- return;
241
- }
242
- span.recordException(persistError);
243
- span.setAttribute(`result`, `persist_failed`);
244
- throw persistError;
245
- } finally {
246
- this.scheduler.markFailed(transaction);
131
+ } catch (hookError) {
132
+ hookFailure = toError(hookError);
133
+ }
134
+ if (hookFailure) {
135
+ shouldRetry = false;
136
+ } else {
137
+ shouldRetry = decision ?? this.retryPolicy.shouldRetry(error, transaction.retryCount);
138
+ }
139
+ }
140
+ if (!shouldRetry) {
141
+ const terminalError = hookFailure ?? error;
142
+ const rejectionPending = {
143
+ ...transaction,
144
+ outboxPhase: `rejection-pending`,
145
+ lastError: {
146
+ name: terminalError.name,
147
+ message: terminalError.message,
148
+ stack: terminalError.stack
247
149
  }
150
+ };
151
+ if (hookFailure) {
152
+ console.warn(
153
+ `Retry decision failed for transaction ${transaction.id}:`,
154
+ hookFailure,
155
+ `Named mutation function error:`,
156
+ error
157
+ );
158
+ } else {
159
+ console.warn(`Transaction ${transaction.id} failed permanently:`, error);
248
160
  }
161
+ try {
162
+ await this.removeSettledTransaction(rejectionPending, true);
163
+ } catch (storageError) {
164
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
165
+ throw storageError;
166
+ }
167
+ this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
168
+ return;
169
+ }
170
+ const delay = Math.max(
171
+ 0,
172
+ this.retryPolicy.calculateDelay(transaction.retryCount)
249
173
  );
174
+ const updatedTransaction = {
175
+ ...transaction,
176
+ retryCount: transaction.retryCount + 1,
177
+ nextAttemptAt: Date.now() + delay,
178
+ lastError: {
179
+ name: error.name,
180
+ message: error.message,
181
+ stack: error.stack
182
+ }
183
+ };
184
+ this.scheduler.updateTransaction(updatedTransaction);
185
+ try {
186
+ await this.outbox.update(transaction.id, updatedTransaction);
187
+ } catch (persistError) {
188
+ if (persistError instanceof OutboxTransactionNotFoundError) {
189
+ this.scheduler.markCompleted(transaction);
190
+ this.offlineExecutor.rejectTransaction(transaction.id, error);
191
+ return;
192
+ }
193
+ throw persistError;
194
+ } finally {
195
+ this.scheduler.markFailed(transaction);
196
+ }
250
197
  }
251
198
  async removeSettledTransaction(transaction, persistMarker) {
252
199
  try {
@@ -343,8 +290,6 @@ class TransactionExecutor {
343
290
  mutationFn: async () => {
344
291
  }
345
292
  });
346
- restorationTx.isPersisted.promise.catch(() => {
347
- });
348
293
  restorationTx.applyMutations(offlineTx.mutations);
349
294
  try {
350
295
  for (const mutation of offlineTx.mutations) {