@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,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,7 +4,6 @@ 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`);
|
|
9
8
|
function toError(value) {
|
|
10
9
|
return value instanceof Error || Object.prototype.toString.call(value) === `[object Error]` ? value : new Error(String(value));
|
|
@@ -58,59 +57,37 @@ class TransactionExecutor {
|
|
|
58
57
|
}
|
|
59
58
|
async executeTransaction(transaction) {
|
|
60
59
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.scheduler.markStarted(transaction);
|
|
72
|
-
if (transaction.retryCount > 0) {
|
|
73
|
-
span.setAttribute(`retry.attempt`, transaction.retryCount);
|
|
74
|
-
}
|
|
75
|
-
if (!transaction.outboxPhase) {
|
|
76
|
-
try {
|
|
77
|
-
await this.runMutationFn(transaction);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
const err = toError(error);
|
|
80
|
-
span.setAttribute(`result`, `error`);
|
|
81
|
-
await this.handleError(transaction, err);
|
|
82
|
-
err[HANDLED_EXECUTION_ERROR] = true;
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
|
|
87
|
-
try {
|
|
88
|
-
await this.removeSettledTransaction(
|
|
89
|
-
deletionPending,
|
|
90
|
-
!transaction.outboxPhase
|
|
91
|
-
);
|
|
92
|
-
} catch (error) {
|
|
93
|
-
const storageError = error;
|
|
94
|
-
span.recordException(storageError);
|
|
95
|
-
span.setAttribute(`result`, `outbox_failure`);
|
|
96
|
-
this.offlineExecutor.rejectTransaction(
|
|
97
|
-
transaction.id,
|
|
98
|
-
terminalError ?? storageError
|
|
99
|
-
);
|
|
100
|
-
throw storageError;
|
|
101
|
-
}
|
|
102
|
-
if (terminalError) {
|
|
103
|
-
span.setAttribute(`result`, `permanent_failure`);
|
|
104
|
-
this.offlineExecutor.rejectTransaction(
|
|
105
|
-
transaction.id,
|
|
106
|
-
terminalError
|
|
107
|
-
);
|
|
108
|
-
} else {
|
|
109
|
-
span.setAttribute(`result`, `success`);
|
|
110
|
-
this.offlineExecutor.resolveTransaction(transaction.id, void 0);
|
|
111
|
-
}
|
|
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;
|
|
112
70
|
}
|
|
113
|
-
|
|
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
|
+
}
|
|
114
91
|
} catch (error) {
|
|
115
92
|
if (error !== null && typeof error === `object` && error[HANDLED_EXECUTION_ERROR] === true) {
|
|
116
93
|
return;
|
|
@@ -138,117 +115,87 @@ class TransactionExecutor {
|
|
|
138
115
|
});
|
|
139
116
|
}
|
|
140
117
|
async handleError(transaction, error) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let decision;
|
|
155
|
-
try {
|
|
156
|
-
decision = this.config.shouldRetry?.(error, transaction.retryCount);
|
|
157
|
-
if (decision !== void 0 && typeof decision !== `boolean`) {
|
|
158
|
-
void Promise.resolve(decision).catch(() => {
|
|
159
|
-
});
|
|
160
|
-
throw new TypeError(
|
|
161
|
-
`OfflineConfig.shouldRetry must return true, false, or undefined`
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
} catch (hookError) {
|
|
165
|
-
hookFailure = toError(hookError);
|
|
166
|
-
}
|
|
167
|
-
if (hookFailure) {
|
|
168
|
-
shouldRetry = false;
|
|
169
|
-
} else {
|
|
170
|
-
shouldRetry = decision ?? this.retryPolicy.shouldRetry(error, transaction.retryCount);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
span.setAttribute(`shouldRetry`, shouldRetry);
|
|
174
|
-
if (!shouldRetry) {
|
|
175
|
-
const terminalError = hookFailure ?? error;
|
|
176
|
-
const rejectionPending = {
|
|
177
|
-
...transaction,
|
|
178
|
-
outboxPhase: `rejection-pending`,
|
|
179
|
-
lastError: {
|
|
180
|
-
name: terminalError.name,
|
|
181
|
-
message: terminalError.message,
|
|
182
|
-
stack: terminalError.stack
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
if (hookFailure) {
|
|
186
|
-
span.recordException(hookFailure);
|
|
187
|
-
console.warn(
|
|
188
|
-
`Retry decision failed for transaction ${transaction.id}:`,
|
|
189
|
-
hookFailure,
|
|
190
|
-
`Named mutation function error:`,
|
|
191
|
-
error
|
|
192
|
-
);
|
|
193
|
-
} else {
|
|
194
|
-
console.warn(
|
|
195
|
-
`Transaction ${transaction.id} failed permanently:`,
|
|
196
|
-
error
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
await this.removeSettledTransaction(rejectionPending, true);
|
|
201
|
-
} catch (storageError) {
|
|
202
|
-
span.recordException(storageError);
|
|
203
|
-
span.setAttribute(`result`, `outbox_failure`);
|
|
204
|
-
this.offlineExecutor.rejectTransaction(
|
|
205
|
-
transaction.id,
|
|
206
|
-
terminalError
|
|
207
|
-
);
|
|
208
|
-
throw storageError;
|
|
209
|
-
}
|
|
210
|
-
span.setAttribute(
|
|
211
|
-
`result`,
|
|
212
|
-
hookFailure ? `retry_decision_failure` : `permanent_failure`
|
|
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`
|
|
213
131
|
);
|
|
214
|
-
this.offlineExecutor.rejectTransaction(transaction.id, terminalError);
|
|
215
|
-
return;
|
|
216
132
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await this.outbox.update(transaction.id, updatedTransaction);
|
|
236
|
-
span.setAttribute(`result`, `scheduled_retry`);
|
|
237
|
-
} catch (persistError) {
|
|
238
|
-
if (persistError instanceof OutboxManager.OutboxTransactionNotFoundError) {
|
|
239
|
-
this.scheduler.markCompleted(transaction);
|
|
240
|
-
span.setAttribute(`result`, `outbox_removed`);
|
|
241
|
-
this.offlineExecutor.rejectTransaction(transaction.id, error);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
span.recordException(persistError);
|
|
245
|
-
span.setAttribute(`result`, `persist_failed`);
|
|
246
|
-
throw persistError;
|
|
247
|
-
} finally {
|
|
248
|
-
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
|
|
249
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);
|
|
250
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)
|
|
251
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
|
+
}
|
|
252
199
|
}
|
|
253
200
|
async removeSettledTransaction(transaction, persistMarker) {
|
|
254
201
|
try {
|
|
@@ -345,8 +292,6 @@ class TransactionExecutor {
|
|
|
345
292
|
mutationFn: async () => {
|
|
346
293
|
}
|
|
347
294
|
});
|
|
348
|
-
restorationTx.isPersisted.promise.catch(() => {
|
|
349
|
-
});
|
|
350
295
|
restorationTx.applyMutations(offlineTx.mutations);
|
|
351
296
|
try {
|
|
352
297
|
for (const mutation of offlineTx.mutations) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransactionExecutor.cjs","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { DuplicateTransactionIdError, createTransaction } from '@tanstack/db'\nimport { OutboxTransactionNotFoundError } from '../outbox/OutboxManager'\nimport { DefaultRetryPolicy } from '../retry/RetryPolicy'\nimport { NonRetriableError } from '../types'\nimport { withNestedSpan } from '../telemetry/tracer'\nimport type { KeyScheduler } from './KeyScheduler'\nimport type { OutboxManager } from '../outbox/OutboxManager'\nimport type {\n OfflineConfig,\n OfflineTransaction,\n TransactionSignaler,\n} from '../types'\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ||\n Object.prototype.toString.call(value) === `[object Error]`\n ? (value as Error)\n : new Error(String(value))\n}\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: TransactionSignaler\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n private fatalError: Error | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: TransactionSignaler,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(\n Number.POSITIVE_INFINITY,\n config.jitter ?? true,\n )\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.assertHealthy()\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n this.assertHealthy()\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n this.scheduleNextRetry()\n }\n }\n\n private async runExecution(): Promise<void> {\n while (this.scheduler.getPendingCount() > 0) {\n if (!this.canExecute()) {\n break\n }\n\n const transaction = this.scheduler.getNext()\n\n if (!transaction) {\n break\n }\n\n await this.executeTransaction(transaction)\n }\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n try {\n await withNestedSpan(\n `transaction.execute`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.retryCount': transaction.retryCount,\n 'transaction.keyCount': transaction.keys.length,\n },\n async (span) => {\n const terminalError =\n transaction.outboxPhase === `rejection-pending`\n ? this.getTerminalError(transaction)\n : undefined\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n if (!transaction.outboxPhase) {\n try {\n await this.runMutationFn(transaction)\n } catch (error) {\n const err = toError(error)\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n }\n\n // A successful provider call can outlive this executor. Persist that\n // fact before removal so a restarted executor only retries deletion.\n const deletionPending: OfflineTransaction = transaction.outboxPhase\n ? transaction\n : { ...transaction, outboxPhase: `deletion-pending` }\n try {\n await this.removeSettledTransaction(\n deletionPending,\n !transaction.outboxPhase,\n )\n } catch (error) {\n const storageError = error as Error\n span.recordException(storageError)\n span.setAttribute(`result`, `outbox_failure`)\n this.offlineExecutor.rejectTransaction(\n transaction.id,\n terminalError ?? storageError,\n )\n throw storageError\n }\n\n if (terminalError) {\n span.setAttribute(`result`, `permanent_failure`)\n this.offlineExecutor.rejectTransaction(\n transaction.id,\n terminalError,\n )\n } else {\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, undefined)\n }\n },\n )\n } catch (error) {\n if (\n error !== null &&\n typeof error === `object` &&\n (error as Record<symbol, unknown>)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error,\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n 'transaction.id': transaction.id,\n 'error.name': error.name,\n 'error.message': error.message,\n },\n async (span) => {\n let shouldRetry: boolean\n let hookFailure: Error | undefined\n if (error instanceof NonRetriableError) {\n shouldRetry = false\n } else {\n let decision: boolean | undefined\n try {\n decision = this.config.shouldRetry?.(error, transaction.retryCount)\n if (decision !== undefined && typeof decision !== `boolean`) {\n // An async hook is invalid, but its rejection must stay row-local.\n void Promise.resolve(decision).catch(() => {})\n throw new TypeError(\n `OfflineConfig.shouldRetry must return true, false, or undefined`,\n )\n }\n } catch (hookError) {\n hookFailure = toError(hookError)\n }\n if (hookFailure) {\n shouldRetry = false\n } else {\n shouldRetry =\n decision ??\n this.retryPolicy.shouldRetry(error, transaction.retryCount)\n }\n }\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n const terminalError = hookFailure ?? error\n const rejectionPending: OfflineTransaction = {\n ...transaction,\n outboxPhase: `rejection-pending`,\n lastError: {\n name: terminalError.name,\n message: terminalError.message,\n stack: terminalError.stack,\n },\n }\n if (hookFailure) {\n span.recordException(hookFailure)\n console.warn(\n `Retry decision failed for transaction ${transaction.id}:`,\n hookFailure,\n `Named mutation function error:`,\n error,\n )\n } else {\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n }\n try {\n await this.removeSettledTransaction(rejectionPending, true)\n } catch (storageError) {\n span.recordException(storageError as Error)\n span.setAttribute(`result`, `outbox_failure`)\n this.offlineExecutor.rejectTransaction(\n transaction.id,\n terminalError,\n )\n throw storageError\n }\n\n span.setAttribute(\n `result`,\n hookFailure ? `retry_decision_failure` : `permanent_failure`,\n )\n this.offlineExecutor.rejectTransaction(transaction.id, terminalError)\n return\n }\n\n const delay = Math.max(\n 0,\n this.retryPolicy.calculateDelay(transaction.retryCount),\n )\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n span.setAttribute(`retryDelay`, delay)\n span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount)\n\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n span.setAttribute(`result`, `scheduled_retry`)\n } catch (persistError) {\n // Public removal can finish while the named mutation function runs.\n // The removed row cannot be retried, but its caller still owns the\n // provider failure and later queued work must be able to run.\n if (persistError instanceof OutboxTransactionNotFoundError) {\n this.scheduler.markCompleted(transaction)\n span.setAttribute(`result`, `outbox_removed`)\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n span.recordException(persistError as Error)\n span.setAttribute(`result`, `persist_failed`)\n throw persistError\n } finally {\n this.scheduler.markFailed(transaction)\n }\n },\n )\n }\n\n private async removeSettledTransaction(\n transaction: OfflineTransaction,\n persistMarker: boolean,\n ): Promise<void> {\n try {\n if (persistMarker) {\n try {\n await this.outbox.update(transaction.id, transaction)\n } catch (error) {\n // A public removal can finish while the provider is still running.\n if (!(error instanceof OutboxTransactionNotFoundError)) throw error\n }\n }\n await this.outbox.remove(transaction.id)\n this.scheduler.markCompleted(transaction)\n } catch (error) {\n const storageError = toError(error)\n this.fatalError = storageError\n this.scheduler.markFailed(transaction)\n this.clearRetryTimer()\n throw storageError\n }\n }\n\n assertHealthy(): void {\n if (this.fatalError) throw this.fatalError\n }\n\n private getTerminalError(transaction: OfflineTransaction): Error {\n if (!transaction.lastError)\n throw new Error(`Terminal transaction ${transaction.id} has no error`)\n const { name, message, stack } = transaction.lastError\n const error =\n name === `NonRetriableError`\n ? new NonRetriableError(message)\n : new Error(message)\n error.name = name\n error.stack = stack\n return error\n }\n\n async loadPendingTransactions(): Promise<void> {\n let removedIds: Array<string> = []\n await this.outbox.withAll((transactions) => {\n const { isOfflineEnabled } = this.offlineExecutor\n if (!isOfflineEnabled) return\n const providerPending = transactions.filter(\n (transaction) => !transaction.outboxPhase,\n )\n const selectedProvider = this.config.beforeRetry\n ? this.config.beforeRetry(providerPending)\n : providerPending\n const selectedById = new Map(selectedProvider.map((tx) => [tx.id, tx]))\n const filteredTransactions = transactions.flatMap((transaction) => {\n if (transaction.outboxPhase) return [transaction]\n const selected = selectedById.get(transaction.id)\n return selected ? [selected] : []\n })\n\n // The retry hook is user code and may synchronously revoke replay rights.\n if (!this.offlineExecutor.isOfflineEnabled) return\n\n const newlyLoaded = filteredTransactions.filter((transaction) =>\n this.scheduler.schedule(transaction),\n )\n\n removedIds = providerPending\n .filter(({ id }) => !selectedById.has(id))\n .map(({ id }) => id)\n removedIds = this.scheduler.removePendingTransactions(removedIds)\n\n // Restore optimistic state for loaded transactions\n // This ensures the UI shows the optimistic data while transactions are pending\n this.restoreOptimisticState(\n newlyLoaded.filter(\n (transaction) => transaction.outboxPhase !== `rejection-pending`,\n ),\n )\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n })\n\n if (removedIds.length > 0) {\n const error = new NonRetriableError(`Transaction excluded by beforeRetry`)\n await Promise.all(\n removedIds.map(async (id) => {\n try {\n await this.outbox.remove(id)\n this.offlineExecutor.rejectTransaction(id, error)\n } catch (cleanupError) {\n console.warn(\n `Failed to remove transaction excluded by beforeRetry:`,\n id,\n cleanupError,\n )\n }\n }),\n )\n }\n }\n\n /**\n * Restore optimistic state from loaded transactions.\n * Creates internal transactions to hold the mutations so the collection's\n * state manager can show optimistic data while waiting for sync.\n */\n private restoreOptimisticState(\n transactions: Array<OfflineTransaction>,\n ): void {\n for (const offlineTx of transactions) {\n if (offlineTx.mutations.length === 0) {\n continue\n }\n\n try {\n // Create a restoration transaction that holds mutations for optimistic state display.\n // It will never commit - the real mutation is handled by the offline executor.\n const restorationTx = createTransaction({\n id: offlineTx.id,\n autoCommit: false,\n mutationFn: async () => {},\n })\n\n // Prevent unhandled promise rejection when cleanup calls rollback()\n // We don't care about this promise - it's just for holding mutations\n restorationTx.isPersisted.promise.catch(() => {\n // Intentionally ignored - restoration transactions are cleaned up\n // via cleanupRestorationTransaction, not through normal commit flow\n })\n\n restorationTx.applyMutations(offlineTx.mutations)\n\n // Register with each affected Collection. If one cannot track it, roll\n // it back, so no Collection keeps a restoration nothing will settle.\n try {\n // The serializer resolves every mutation's Collection or throws.\n for (const mutation of offlineTx.mutations) {\n if (mutation.collection._state.trackTransaction(restorationTx))\n mutation.collection._state.recomputeOptimisticState(true)\n }\n } catch (error) {\n // A secondary rollback settles only this restoration; it must not\n // roll back the user's live transactions on the same keys.\n restorationTx.rollback({ isSecondaryRollback: true })\n throw error\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\n // A live transaction with this id already shows these mutations.\n if (error instanceof DuplicateTransactionIdError) continue\n console.warn(\n `Failed to restore optimistic state for transaction ${offlineTx.id}:`,\n error,\n )\n }\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n pause(): void {\n // Retain queued work and let the issued call finish its acknowledgment.\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n if (this.fatalError || !this.canExecute()) {\n return\n }\n\n const nextRetryTime = this.getNextRetryTime()\n\n if (nextRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, nextRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getNextRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n // Later transactions cannot overtake the FIFO head, even if they are ready.\n return allTransactions[0]!.nextAttemptAt\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n private canExecute(): boolean {\n return (\n this.offlineExecutor.isOfflineEnabled && this.offlineExecutor.isOnline()\n )\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":["DefaultRetryPolicy","withNestedSpan","NonRetriableError","OutboxTransactionNotFoundError","createTransaction","DuplicateTransactionIdError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TransactionExecutor.cjs","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { DuplicateTransactionIdError, createTransaction } from '@tanstack/db'\nimport { OutboxTransactionNotFoundError } from '../outbox/OutboxManager'\nimport { DefaultRetryPolicy } from '../retry/RetryPolicy'\nimport { NonRetriableError } from '../types'\nimport type { KeyScheduler } from './KeyScheduler'\nimport type { OutboxManager } from '../outbox/OutboxManager'\nimport type {\n OfflineConfig,\n OfflineTransaction,\n TransactionSignaler,\n} from '../types'\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ||\n Object.prototype.toString.call(value) === `[object Error]`\n ? (value as Error)\n : new Error(String(value))\n}\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: TransactionSignaler\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n private fatalError: Error | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: TransactionSignaler,\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(\n Number.POSITIVE_INFINITY,\n config.jitter ?? true,\n )\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.assertHealthy()\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n this.assertHealthy()\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n this.scheduleNextRetry()\n }\n }\n\n private async runExecution(): Promise<void> {\n while (this.scheduler.getPendingCount() > 0) {\n if (!this.canExecute()) {\n break\n }\n\n const transaction = this.scheduler.getNext()\n\n if (!transaction) {\n break\n }\n\n await this.executeTransaction(transaction)\n }\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n try {\n const terminalError =\n transaction.outboxPhase === `rejection-pending`\n ? this.getTerminalError(transaction)\n : undefined\n this.scheduler.markStarted(transaction)\n\n if (!transaction.outboxPhase) {\n try {\n await this.runMutationFn(transaction)\n } catch (error) {\n const err = toError(error)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n }\n\n // A successful provider call can outlive this executor. Persist that\n // fact before removal so a restarted executor only retries deletion.\n const deletionPending: OfflineTransaction = transaction.outboxPhase\n ? transaction\n : { ...transaction, outboxPhase: `deletion-pending` }\n try {\n await this.removeSettledTransaction(\n deletionPending,\n !transaction.outboxPhase,\n )\n } catch (error) {\n const storageError = error as Error\n this.offlineExecutor.rejectTransaction(\n transaction.id,\n terminalError ?? storageError,\n )\n throw storageError\n }\n\n if (terminalError) {\n this.offlineExecutor.rejectTransaction(transaction.id, terminalError)\n } else {\n this.offlineExecutor.resolveTransaction(transaction.id, undefined)\n }\n } catch (error) {\n if (\n error !== null &&\n typeof error === `object` &&\n (error as Record<symbol, unknown>)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error,\n ): Promise<void> {\n let shouldRetry: boolean\n let hookFailure: Error | undefined\n if (error instanceof NonRetriableError) {\n shouldRetry = false\n } else {\n let decision: boolean | undefined\n try {\n decision = this.config.shouldRetry?.(error, transaction.retryCount)\n if (decision !== undefined && typeof decision !== `boolean`) {\n // An async hook is invalid, but its rejection must stay row-local.\n void Promise.resolve(decision).catch(() => {})\n throw new TypeError(\n `OfflineConfig.shouldRetry must return true, false, or undefined`,\n )\n }\n } catch (hookError) {\n hookFailure = toError(hookError)\n }\n if (hookFailure) {\n shouldRetry = false\n } else {\n shouldRetry =\n decision ??\n this.retryPolicy.shouldRetry(error, transaction.retryCount)\n }\n }\n\n if (!shouldRetry) {\n const terminalError = hookFailure ?? error\n const rejectionPending: OfflineTransaction = {\n ...transaction,\n outboxPhase: `rejection-pending`,\n lastError: {\n name: terminalError.name,\n message: terminalError.message,\n stack: terminalError.stack,\n },\n }\n if (hookFailure) {\n console.warn(\n `Retry decision failed for transaction ${transaction.id}:`,\n hookFailure,\n `Named mutation function error:`,\n error,\n )\n } else {\n console.warn(`Transaction ${transaction.id} failed permanently:`, error)\n }\n try {\n await this.removeSettledTransaction(rejectionPending, true)\n } catch (storageError) {\n this.offlineExecutor.rejectTransaction(transaction.id, terminalError)\n throw storageError\n }\n\n this.offlineExecutor.rejectTransaction(transaction.id, terminalError)\n return\n }\n\n const delay = Math.max(\n 0,\n this.retryPolicy.calculateDelay(transaction.retryCount),\n )\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n } catch (persistError) {\n // Public removal can finish while the named mutation function runs.\n // The removed row cannot be retried, but its caller still owns the\n // provider failure and later queued work must be able to run.\n if (persistError instanceof OutboxTransactionNotFoundError) {\n this.scheduler.markCompleted(transaction)\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n throw persistError\n } finally {\n this.scheduler.markFailed(transaction)\n }\n }\n\n private async removeSettledTransaction(\n transaction: OfflineTransaction,\n persistMarker: boolean,\n ): Promise<void> {\n try {\n if (persistMarker) {\n try {\n await this.outbox.update(transaction.id, transaction)\n } catch (error) {\n // A public removal can finish while the provider is still running.\n if (!(error instanceof OutboxTransactionNotFoundError)) throw error\n }\n }\n await this.outbox.remove(transaction.id)\n this.scheduler.markCompleted(transaction)\n } catch (error) {\n const storageError = toError(error)\n this.fatalError = storageError\n this.scheduler.markFailed(transaction)\n this.clearRetryTimer()\n throw storageError\n }\n }\n\n assertHealthy(): void {\n if (this.fatalError) throw this.fatalError\n }\n\n private getTerminalError(transaction: OfflineTransaction): Error {\n if (!transaction.lastError)\n throw new Error(`Terminal transaction ${transaction.id} has no error`)\n const { name, message, stack } = transaction.lastError\n const error =\n name === `NonRetriableError`\n ? new NonRetriableError(message)\n : new Error(message)\n error.name = name\n error.stack = stack\n return error\n }\n\n async loadPendingTransactions(): Promise<void> {\n let removedIds: Array<string> = []\n await this.outbox.withAll((transactions) => {\n const { isOfflineEnabled } = this.offlineExecutor\n if (!isOfflineEnabled) return\n const providerPending = transactions.filter(\n (transaction) => !transaction.outboxPhase,\n )\n const selectedProvider = this.config.beforeRetry\n ? this.config.beforeRetry(providerPending)\n : providerPending\n const selectedById = new Map(selectedProvider.map((tx) => [tx.id, tx]))\n const filteredTransactions = transactions.flatMap((transaction) => {\n if (transaction.outboxPhase) return [transaction]\n const selected = selectedById.get(transaction.id)\n return selected ? [selected] : []\n })\n\n // The retry hook is user code and may synchronously revoke replay rights.\n if (!this.offlineExecutor.isOfflineEnabled) return\n\n const newlyLoaded = filteredTransactions.filter((transaction) =>\n this.scheduler.schedule(transaction),\n )\n\n removedIds = providerPending\n .filter(({ id }) => !selectedById.has(id))\n .map(({ id }) => id)\n removedIds = this.scheduler.removePendingTransactions(removedIds)\n\n // Restore optimistic state for loaded transactions\n // This ensures the UI shows the optimistic data while transactions are pending\n this.restoreOptimisticState(\n newlyLoaded.filter(\n (transaction) => transaction.outboxPhase !== `rejection-pending`,\n ),\n )\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n })\n\n if (removedIds.length > 0) {\n const error = new NonRetriableError(`Transaction excluded by beforeRetry`)\n await Promise.all(\n removedIds.map(async (id) => {\n try {\n await this.outbox.remove(id)\n this.offlineExecutor.rejectTransaction(id, error)\n } catch (cleanupError) {\n console.warn(\n `Failed to remove transaction excluded by beforeRetry:`,\n id,\n cleanupError,\n )\n }\n }),\n )\n }\n }\n\n /**\n * Restore optimistic state from loaded transactions.\n * Creates internal transactions to hold the mutations so the collection's\n * state manager can show optimistic data while waiting for sync.\n */\n private restoreOptimisticState(\n transactions: Array<OfflineTransaction>,\n ): void {\n for (const offlineTx of transactions) {\n if (offlineTx.mutations.length === 0) {\n continue\n }\n\n try {\n // Create a restoration transaction that holds mutations for optimistic state display.\n // It will never commit - the real mutation is handled by the offline executor.\n const restorationTx = createTransaction({\n id: offlineTx.id,\n autoCommit: false,\n mutationFn: async () => {},\n })\n\n restorationTx.applyMutations(offlineTx.mutations)\n\n // Register with each affected Collection. If one cannot track it, roll\n // it back, so no Collection keeps a restoration nothing will settle.\n try {\n // The serializer resolves every mutation's Collection or throws.\n for (const mutation of offlineTx.mutations) {\n if (mutation.collection._state.trackTransaction(restorationTx))\n mutation.collection._state.recomputeOptimisticState(true)\n }\n } catch (error) {\n // A secondary rollback settles only this restoration; it must not\n // roll back the user's live transactions on the same keys.\n restorationTx.rollback({ isSecondaryRollback: true })\n throw error\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\n // A live transaction with this id already shows these mutations.\n if (error instanceof DuplicateTransactionIdError) continue\n console.warn(\n `Failed to restore optimistic state for transaction ${offlineTx.id}:`,\n error,\n )\n }\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n pause(): void {\n // Retain queued work and let the issued call finish its acknowledgment.\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n if (this.fatalError || !this.canExecute()) {\n return\n }\n\n const nextRetryTime = this.getNextRetryTime()\n\n if (nextRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, nextRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getNextRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n // Later transactions cannot overtake the FIFO head, even if they are ready.\n return allTransactions[0]!.nextAttemptAt\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n private canExecute(): boolean {\n return (\n this.offlineExecutor.isOfflineEnabled && this.offlineExecutor.isOnline()\n )\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":["DefaultRetryPolicy","NonRetriableError","OutboxTransactionNotFoundError","createTransaction","DuplicateTransactionIdError"],"mappings":"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"}
|