@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.
- package/README.md +81 -2
- package/dist/cjs/OfflineExecutor.cjs +74 -93
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/OfflineExecutor.d.cts +6 -0
- package/dist/cjs/api/OfflineAction.cjs +4 -15
- package/dist/cjs/api/OfflineAction.cjs.map +1 -1
- package/dist/cjs/api/OfflineTransaction.cjs +54 -51
- package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
- package/dist/cjs/api/OfflineTransaction.d.cts +2 -0
- package/dist/cjs/executor/KeyScheduler.cjs +12 -38
- package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
- package/dist/cjs/executor/TransactionExecutor.cjs +119 -134
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
- package/dist/cjs/outbox/OutboxManager.cjs +48 -77
- package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +10 -0
- package/dist/esm/OfflineExecutor.d.ts +6 -0
- package/dist/esm/OfflineExecutor.js +74 -93
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/api/OfflineAction.js +4 -15
- package/dist/esm/api/OfflineAction.js.map +1 -1
- package/dist/esm/api/OfflineTransaction.d.ts +2 -0
- package/dist/esm/api/OfflineTransaction.js +54 -51
- package/dist/esm/api/OfflineTransaction.js.map +1 -1
- package/dist/esm/executor/KeyScheduler.js +12 -38
- package/dist/esm/executor/KeyScheduler.js.map +1 -1
- package/dist/esm/executor/TransactionExecutor.js +120 -135
- package/dist/esm/executor/TransactionExecutor.js.map +1 -1
- package/dist/esm/outbox/OutboxManager.js +48 -77
- package/dist/esm/outbox/OutboxManager.js.map +1 -1
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/skills/offline/SKILL.md +73 -5
- package/src/OfflineExecutor.ts +97 -113
- package/src/api/OfflineAction.ts +4 -22
- package/src/api/OfflineTransaction.ts +78 -64
- package/src/executor/KeyScheduler.ts +18 -47
- package/src/executor/TransactionExecutor.ts +148 -162
- package/src/outbox/OutboxManager.ts +54 -86
- package/src/types.ts +10 -0
- package/dist/cjs/retry/NonRetriableError.d.cts +0 -1
- package/dist/cjs/telemetry/tracer.cjs +0 -27
- package/dist/cjs/telemetry/tracer.cjs.map +0 -1
- package/dist/cjs/telemetry/tracer.d.cts +0 -30
- package/dist/esm/retry/NonRetriableError.d.ts +0 -1
- package/dist/esm/telemetry/tracer.d.ts +0 -30
- package/dist/esm/telemetry/tracer.js +0 -27
- package/dist/esm/telemetry/tracer.js.map +0 -1
- package/src/retry/NonRetriableError.ts +0 -1
- 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
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 =
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|