@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.
- package/README.md +1 -1
- package/dist/cjs/OfflineExecutor.cjs +72 -80
- 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 +106 -161
- 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/esm/OfflineExecutor.d.ts +6 -0
- package/dist/esm/OfflineExecutor.js +72 -80
- 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 +106 -161
- 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/package.json +2 -2
- package/src/OfflineExecutor.ts +93 -94
- package/src/api/OfflineAction.ts +4 -22
- package/src/api/OfflineTransaction.ts +78 -66
- package/src/executor/KeyScheduler.ts +18 -47
- package/src/executor/TransactionExecutor.ts +122 -186
- package/src/outbox/OutboxManager.ts +54 -86
- 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,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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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) {
|