@tanstack/offline-transactions 1.0.58 → 1.0.59
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 +25 -5
- package/dist/cjs/OfflineExecutor.cjs +4 -3
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/executor/TransactionExecutor.cjs +103 -34
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
- package/dist/cjs/executor/TransactionExecutor.d.cts +4 -0
- package/dist/cjs/outbox/OutboxManager.cjs +4 -1
- package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
- package/dist/cjs/outbox/OutboxManager.d.cts +2 -0
- package/dist/cjs/outbox/TransactionSerializer.cjs +8 -0
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.cjs +1 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
- package/dist/cjs/storage/LocalStorageAdapter.cjs +1 -1
- package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -1
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +4 -0
- package/dist/esm/OfflineExecutor.js +4 -3
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/executor/TransactionExecutor.d.ts +4 -0
- package/dist/esm/executor/TransactionExecutor.js +103 -34
- package/dist/esm/executor/TransactionExecutor.js.map +1 -1
- package/dist/esm/outbox/OutboxManager.d.ts +2 -0
- package/dist/esm/outbox/OutboxManager.js +5 -2
- package/dist/esm/outbox/OutboxManager.js.map +1 -1
- package/dist/esm/outbox/TransactionSerializer.js +8 -0
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
- package/dist/esm/storage/IndexedDBAdapter.js +1 -0
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
- package/dist/esm/storage/LocalStorageAdapter.js +1 -1
- package/dist/esm/storage/LocalStorageAdapter.js.map +1 -1
- package/dist/esm/types.d.ts +4 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/OfflineExecutor.ts +4 -3
- package/src/executor/TransactionExecutor.ts +124 -38
- package/src/outbox/OutboxManager.ts +3 -1
- package/src/outbox/TransactionSerializer.ts +17 -1
- package/src/storage/IndexedDBAdapter.ts +1 -0
- package/src/storage/LocalStorageAdapter.ts +1 -1
- package/src/types.ts +4 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createTransaction } from "@tanstack/db";
|
|
2
|
+
import { OutboxTransactionNotFoundError } from "../outbox/OutboxManager.js";
|
|
2
3
|
import { DefaultRetryPolicy } from "../retry/RetryPolicy.js";
|
|
3
4
|
import { NonRetriableError } from "../types.js";
|
|
4
5
|
import { withNestedSpan } from "../telemetry/tracer.js";
|
|
@@ -8,6 +9,7 @@ class TransactionExecutor {
|
|
|
8
9
|
this.isExecuting = false;
|
|
9
10
|
this.executionPromise = null;
|
|
10
11
|
this.retryTimer = null;
|
|
12
|
+
this.fatalError = null;
|
|
11
13
|
this.scheduler = scheduler;
|
|
12
14
|
this.outbox = outbox;
|
|
13
15
|
this.config = config;
|
|
@@ -18,10 +20,12 @@ class TransactionExecutor {
|
|
|
18
20
|
this.offlineExecutor = offlineExecutor;
|
|
19
21
|
}
|
|
20
22
|
async execute(transaction) {
|
|
23
|
+
this.assertHealthy();
|
|
21
24
|
this.scheduler.schedule(transaction);
|
|
22
25
|
await this.executeAll();
|
|
23
26
|
}
|
|
24
27
|
async executeAll() {
|
|
28
|
+
this.assertHealthy();
|
|
25
29
|
if (this.isExecuting) {
|
|
26
30
|
return this.executionPromise;
|
|
27
31
|
}
|
|
@@ -58,31 +62,48 @@ class TransactionExecutor {
|
|
|
58
62
|
"transaction.keyCount": transaction.keys.length
|
|
59
63
|
},
|
|
60
64
|
async (span) => {
|
|
65
|
+
const terminalError = transaction.outboxPhase === `rejection-pending` ? this.getTerminalError(transaction) : void 0;
|
|
61
66
|
this.scheduler.markStarted(transaction);
|
|
62
67
|
if (transaction.retryCount > 0) {
|
|
63
68
|
span.setAttribute(`retry.attempt`, transaction.retryCount);
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
if (!transaction.outboxPhase) {
|
|
71
|
+
try {
|
|
72
|
+
await this.runMutationFn(transaction);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
75
|
+
span.setAttribute(`result`, `error`);
|
|
76
|
+
await this.handleError(transaction, err);
|
|
77
|
+
err[HANDLED_EXECUTION_ERROR] = true;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
74
80
|
}
|
|
75
|
-
|
|
81
|
+
const deletionPending = transaction.outboxPhase ? transaction : { ...transaction, outboxPhase: `deletion-pending` };
|
|
76
82
|
try {
|
|
77
|
-
await this.
|
|
83
|
+
await this.removeSettledTransaction(
|
|
84
|
+
deletionPending,
|
|
85
|
+
!transaction.outboxPhase
|
|
86
|
+
);
|
|
78
87
|
} catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
const storageError = error;
|
|
89
|
+
span.recordException(storageError);
|
|
90
|
+
span.setAttribute(`result`, `outbox_failure`);
|
|
91
|
+
this.offlineExecutor.rejectTransaction(
|
|
92
|
+
transaction.id,
|
|
93
|
+
terminalError ?? storageError
|
|
94
|
+
);
|
|
95
|
+
throw storageError;
|
|
96
|
+
}
|
|
97
|
+
if (terminalError) {
|
|
98
|
+
span.setAttribute(`result`, `permanent_failure`);
|
|
99
|
+
this.offlineExecutor.rejectTransaction(
|
|
100
|
+
transaction.id,
|
|
101
|
+
terminalError
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
span.setAttribute(`result`, `success`);
|
|
105
|
+
this.offlineExecutor.resolveTransaction(transaction.id, void 0);
|
|
82
106
|
}
|
|
83
|
-
span.setAttribute(`result`, `success`);
|
|
84
|
-
this.offlineExecutor.resolveTransaction(transaction.id, result);
|
|
85
|
-
if (removalError !== void 0) throw removalError;
|
|
86
107
|
}
|
|
87
108
|
);
|
|
88
109
|
} catch (error) {
|
|
@@ -126,21 +147,29 @@ class TransactionExecutor {
|
|
|
126
147
|
);
|
|
127
148
|
span.setAttribute(`shouldRetry`, shouldRetry);
|
|
128
149
|
if (!shouldRetry) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
const rejectionPending = {
|
|
151
|
+
...transaction,
|
|
152
|
+
outboxPhase: `rejection-pending`,
|
|
153
|
+
lastError: {
|
|
154
|
+
name: error.name,
|
|
155
|
+
message: error.message,
|
|
156
|
+
stack: error.stack
|
|
157
|
+
}
|
|
158
|
+
};
|
|
137
159
|
console.warn(
|
|
138
160
|
`Transaction ${transaction.id} failed permanently:`,
|
|
139
161
|
error
|
|
140
162
|
);
|
|
163
|
+
try {
|
|
164
|
+
await this.removeSettledTransaction(rejectionPending, true);
|
|
165
|
+
} catch (storageError) {
|
|
166
|
+
span.recordException(storageError);
|
|
167
|
+
span.setAttribute(`result`, `outbox_failure`);
|
|
168
|
+
this.offlineExecutor.rejectTransaction(transaction.id, error);
|
|
169
|
+
throw storageError;
|
|
170
|
+
}
|
|
141
171
|
span.setAttribute(`result`, `permanent_failure`);
|
|
142
172
|
this.offlineExecutor.rejectTransaction(transaction.id, error);
|
|
143
|
-
if (removalError !== void 0) throw removalError;
|
|
144
173
|
return;
|
|
145
174
|
}
|
|
146
175
|
const delay = Math.max(
|
|
@@ -173,23 +202,63 @@ class TransactionExecutor {
|
|
|
173
202
|
}
|
|
174
203
|
);
|
|
175
204
|
}
|
|
205
|
+
async removeSettledTransaction(transaction, persistMarker) {
|
|
206
|
+
try {
|
|
207
|
+
if (persistMarker) {
|
|
208
|
+
try {
|
|
209
|
+
await this.outbox.update(transaction.id, transaction);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (!(error instanceof OutboxTransactionNotFoundError)) throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await this.outbox.remove(transaction.id);
|
|
215
|
+
this.scheduler.markCompleted(transaction);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const storageError = error instanceof Error ? error : new Error(String(error));
|
|
218
|
+
this.fatalError = storageError;
|
|
219
|
+
this.scheduler.markFailed(transaction);
|
|
220
|
+
this.clearRetryTimer();
|
|
221
|
+
throw storageError;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
assertHealthy() {
|
|
225
|
+
if (this.fatalError) throw this.fatalError;
|
|
226
|
+
}
|
|
227
|
+
getTerminalError(transaction) {
|
|
228
|
+
if (!transaction.lastError)
|
|
229
|
+
throw new Error(`Terminal transaction ${transaction.id} has no error`);
|
|
230
|
+
const { name, message, stack } = transaction.lastError;
|
|
231
|
+
const error = name === `NonRetriableError` ? new NonRetriableError(message) : new Error(message);
|
|
232
|
+
error.name = name;
|
|
233
|
+
error.stack = stack;
|
|
234
|
+
return error;
|
|
235
|
+
}
|
|
176
236
|
async loadPendingTransactions() {
|
|
177
237
|
let removedIds = [];
|
|
178
238
|
await this.outbox.withAll((transactions) => {
|
|
179
239
|
const { isOfflineEnabled } = this.offlineExecutor;
|
|
180
240
|
if (!isOfflineEnabled) return;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
const providerPending = transactions.filter(
|
|
242
|
+
(transaction) => !transaction.outboxPhase
|
|
243
|
+
);
|
|
244
|
+
const selectedProvider = this.config.beforeRetry ? this.config.beforeRetry(providerPending) : providerPending;
|
|
245
|
+
const selectedById = new Map(selectedProvider.map((tx) => [tx.id, tx]));
|
|
246
|
+
const filteredTransactions = transactions.flatMap((transaction) => {
|
|
247
|
+
if (transaction.outboxPhase) return [transaction];
|
|
248
|
+
const selected = selectedById.get(transaction.id);
|
|
249
|
+
return selected ? [selected] : [];
|
|
250
|
+
});
|
|
185
251
|
if (!this.offlineExecutor.isOfflineEnabled) return;
|
|
186
252
|
const newlyLoaded = filteredTransactions.filter(
|
|
187
253
|
(transaction) => this.scheduler.schedule(transaction)
|
|
188
254
|
);
|
|
189
|
-
|
|
190
|
-
removedIds = transactions.filter(({ id }) => !retainedIds.has(id)).map(({ id }) => id);
|
|
255
|
+
removedIds = providerPending.filter(({ id }) => !selectedById.has(id)).map(({ id }) => id);
|
|
191
256
|
removedIds = this.scheduler.removePendingTransactions(removedIds);
|
|
192
|
-
this.restoreOptimisticState(
|
|
257
|
+
this.restoreOptimisticState(
|
|
258
|
+
newlyLoaded.filter(
|
|
259
|
+
(transaction) => transaction.outboxPhase !== `rejection-pending`
|
|
260
|
+
)
|
|
261
|
+
);
|
|
193
262
|
this.resetRetryDelays();
|
|
194
263
|
this.scheduleNextRetry();
|
|
195
264
|
});
|
|
@@ -271,7 +340,7 @@ class TransactionExecutor {
|
|
|
271
340
|
}
|
|
272
341
|
scheduleNextRetry() {
|
|
273
342
|
this.clearRetryTimer();
|
|
274
|
-
if (!this.canExecute()) {
|
|
343
|
+
if (this.fatalError || !this.canExecute()) {
|
|
275
344
|
return;
|
|
276
345
|
}
|
|
277
346
|
const nextRetryTime = this.getNextRetryTime();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { createTransaction } from '@tanstack/db'\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\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\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.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\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 this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n let result: void\n try {\n result = await this.runMutationFn(transaction)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(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 let removalError: unknown\n try {\n // Replay can still see this ID until durable deletion settles.\n await this.outbox.remove(transaction.id)\n } catch (error) {\n removalError = error\n } finally {\n this.scheduler.markCompleted(transaction)\n }\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n if (removalError !== undefined) throw removalError\n },\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[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 const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount,\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n let removalError: unknown\n try {\n await this.outbox.remove(transaction.id)\n } catch (cleanupError) {\n removalError = cleanupError\n } finally {\n this.scheduler.markCompleted(transaction)\n }\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n if (removalError !== undefined) throw removalError\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 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 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 let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\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 const retainedIds = new Set(filteredTransactions.map(({ id }) => id))\n removedIds = transactions\n .filter(({ id }) => !retainedIds.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(newlyLoaded)\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's state manager\n const touchedCollections = new Set<string>()\n for (const mutation of offlineTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n\n mutation.collection._state.transactions.set(\n restorationTx.id,\n restorationTx,\n )\n mutation.collection._state.recomputeOptimisticState(true)\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\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.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":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { 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\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 =\n error instanceof Error ? error : new Error(String(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 instanceof Error &&\n (error as any)[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 const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount,\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n const rejectionPending: OfflineTransaction = {\n ...transaction,\n outboxPhase: `rejection-pending`,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error,\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(transaction.id, error)\n throw storageError\n }\n\n span.setAttribute(`result`, `permanent_failure`)\n this.offlineExecutor.rejectTransaction(transaction.id, error)\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 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 =\n error instanceof Error ? error : new Error(String(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's state manager\n const touchedCollections = new Set<string>()\n for (const mutation of offlineTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n\n mutation.collection._state.transactions.set(\n restorationTx.id,\n restorationTx,\n )\n mutation.collection._state.recomputeOptimisticState(true)\n }\n\n this.offlineExecutor.registerRestorationTransaction(\n offlineTx.id,\n restorationTx,\n )\n } catch (error) {\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":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { withSpan } from "../telemetry/tracer.js";
|
|
2
2
|
import { TransactionSerializer, MissingTemporalConstructorError } from "./TransactionSerializer.js";
|
|
3
|
+
class OutboxTransactionNotFoundError extends Error {
|
|
4
|
+
}
|
|
3
5
|
class OutboxManager {
|
|
4
6
|
constructor(storage, collections) {
|
|
5
7
|
this.keyPrefix = `tx:`;
|
|
@@ -120,7 +122,7 @@ class OutboxManager {
|
|
|
120
122
|
return withSpan(`outbox.update`, {}, async () => {
|
|
121
123
|
const existing = await this.get(id);
|
|
122
124
|
if (!existing) {
|
|
123
|
-
throw new
|
|
125
|
+
throw new OutboxTransactionNotFoundError(`Transaction ${id} not found`);
|
|
124
126
|
}
|
|
125
127
|
const updated = { ...existing, ...updates };
|
|
126
128
|
await this.add(updated);
|
|
@@ -151,6 +153,7 @@ class OutboxManager {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
export {
|
|
154
|
-
OutboxManager
|
|
156
|
+
OutboxManager,
|
|
157
|
+
OutboxTransactionNotFoundError
|
|
155
158
|
};
|
|
156
159
|
//# sourceMappingURL=OutboxManager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OutboxManager.js","sources":["../../../src/outbox/OutboxManager.ts"],"sourcesContent":["import { withSpan } from '../telemetry/tracer'\nimport {\n MissingTemporalConstructorError,\n TransactionSerializer,\n} from './TransactionSerializer'\nimport type { OfflineTransaction, StorageAdapter } from '../types'\nimport type { Collection } from '@tanstack/db'\n\nexport class OutboxManager {\n private storage: StorageAdapter\n private serializer: TransactionSerializer\n private keyPrefix = `tx:`\n private removalRevision = 0\n private activeReads = new Set<{ revision: number }>()\n private removedAtRevision = new Map<string, number>()\n\n constructor(\n storage: StorageAdapter,\n\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.storage = storage\n this.serializer = new TransactionSerializer(collections)\n }\n\n private getStorageKey(id: string): string {\n return `${this.keyPrefix}${id}`\n }\n\n private recordRemovals(ids: Iterable<string>): void {\n this.removalRevision++\n if (this.activeReads.size === 0) return\n for (const id of ids) this.removedAtRevision.set(id, this.removalRevision)\n this.pruneRemovalHistory()\n }\n\n private pruneRemovalHistory(): void {\n if (this.activeReads.size === 0) {\n this.removedAtRevision.clear()\n return\n }\n let oldestRead = Number.POSITIVE_INFINITY\n for (const read of this.activeReads)\n oldestRead = Math.min(oldestRead, read.revision)\n for (const [id, revision] of this.removedAtRevision)\n if (revision <= oldestRead) this.removedAtRevision.delete(id)\n }\n\n async add(transaction: OfflineTransaction): Promise<void> {\n return withSpan(\n `outbox.add`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.keyCount': transaction.keys.length,\n },\n async () => {\n const key = this.getStorageKey(transaction.id)\n const serialized = this.serializer.serialize(transaction)\n await this.storage.set(key, serialized)\n },\n )\n }\n\n async get(id: string): Promise<OfflineTransaction | null> {\n return withSpan(`outbox.get`, { 'transaction.id': id }, async (span) => {\n const key = this.getStorageKey(id)\n const data = await this.storage.get(key)\n\n if (!data) {\n span.setAttribute(`result`, `not_found`)\n return null\n }\n\n try {\n const transaction = this.serializer.deserialize(data)\n span.setAttribute(`result`, `found`)\n return transaction\n } catch (error) {\n if (error instanceof MissingTemporalConstructorError) {\n error.message = `transaction ${id}: ${error.message}`\n throw error\n }\n console.warn(`Failed to deserialize transaction ${id}:`, error)\n span.setAttribute(`result`, `deserialize_error`)\n return null\n }\n })\n }\n\n async getAll(): Promise<Array<OfflineTransaction>> {\n return this.withAll((transactions) => transactions)\n }\n\n async withAll<T>(\n consume: (transactions: Array<OfflineTransaction>) => T,\n ): Promise<T> {\n const read = { revision: this.removalRevision }\n this.activeReads.add(read)\n try {\n return await withSpan(`outbox.getAll`, {}, async (span) => {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) =>\n key.startsWith(this.keyPrefix),\n )\n\n span.setAttribute(`transactionCount`, transactionKeys.length)\n\n const transactions: Array<OfflineTransaction> = []\n\n for (const key of transactionKeys) {\n const data = await this.storage.get(key)\n if (data) {\n try {\n const transaction = this.serializer.deserialize(data)\n transactions.push(transaction)\n } catch (error) {\n if (error instanceof MissingTemporalConstructorError) {\n error.message = `transaction ${key.slice(this.keyPrefix.length)}: ${error.message}`\n throw error\n }\n console.warn(\n `Failed to deserialize transaction from key ${key}:`,\n error,\n )\n }\n }\n }\n\n const currentTransactions = transactions\n .filter(\n ({ id }) => (this.removedAtRevision.get(id) ?? 0) <= read.revision,\n )\n .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())\n // Keep the read registered until replay admission completes. A durable\n // removal cannot land between filtering and this synchronous consumer.\n return consume(currentTransactions)\n })\n } finally {\n this.activeReads.delete(read)\n this.pruneRemovalHistory()\n }\n }\n\n async getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>> {\n const allTransactions = await this.getAll()\n const keySet = new Set(keys)\n\n return allTransactions.filter((transaction) =>\n transaction.keys.some((key) => keySet.has(key)),\n )\n }\n\n async update(\n id: string,\n updates: Partial<OfflineTransaction>,\n ): Promise<void> {\n return withSpan(`outbox.update`, { 'transaction.id': id }, async () => {\n const existing = await this.get(id)\n if (!existing) {\n throw new Error(`Transaction ${id} not found`)\n }\n\n const updated = { ...existing, ...updates }\n await this.add(updated)\n })\n }\n\n async remove(id: string): Promise<void> {\n return withSpan(`outbox.remove`, { 'transaction.id': id }, async () => {\n const key = this.getStorageKey(id)\n await this.storage.delete(key)\n this.recordRemovals([id])\n })\n }\n\n async removeMany(ids: Array<string>): Promise<void> {\n return withSpan(`outbox.removeMany`, { count: ids.length }, async () => {\n await Promise.all(ids.map((id) => this.remove(id)))\n })\n }\n\n async clear(): Promise<void> {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix))\n\n await this.removeMany(\n transactionKeys.map((key) => key.slice(this.keyPrefix.length)),\n )\n }\n\n async count(): Promise<number> {\n const keys = await this.storage.keys()\n return keys.filter((key) => key.startsWith(this.keyPrefix)).length\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OutboxManager.js","sources":["../../../src/outbox/OutboxManager.ts"],"sourcesContent":["import { withSpan } from '../telemetry/tracer'\nimport {\n MissingTemporalConstructorError,\n TransactionSerializer,\n} from './TransactionSerializer'\nimport type { OfflineTransaction, StorageAdapter } from '../types'\nimport type { Collection } from '@tanstack/db'\n\nexport class OutboxTransactionNotFoundError extends Error {}\n\nexport class OutboxManager {\n private storage: StorageAdapter\n private serializer: TransactionSerializer\n private keyPrefix = `tx:`\n private removalRevision = 0\n private activeReads = new Set<{ revision: number }>()\n private removedAtRevision = new Map<string, number>()\n\n constructor(\n storage: StorageAdapter,\n\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.storage = storage\n this.serializer = new TransactionSerializer(collections)\n }\n\n private getStorageKey(id: string): string {\n return `${this.keyPrefix}${id}`\n }\n\n private recordRemovals(ids: Iterable<string>): void {\n this.removalRevision++\n if (this.activeReads.size === 0) return\n for (const id of ids) this.removedAtRevision.set(id, this.removalRevision)\n this.pruneRemovalHistory()\n }\n\n private pruneRemovalHistory(): void {\n if (this.activeReads.size === 0) {\n this.removedAtRevision.clear()\n return\n }\n let oldestRead = Number.POSITIVE_INFINITY\n for (const read of this.activeReads)\n oldestRead = Math.min(oldestRead, read.revision)\n for (const [id, revision] of this.removedAtRevision)\n if (revision <= oldestRead) this.removedAtRevision.delete(id)\n }\n\n async add(transaction: OfflineTransaction): Promise<void> {\n return withSpan(\n `outbox.add`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n 'transaction.keyCount': transaction.keys.length,\n },\n async () => {\n const key = this.getStorageKey(transaction.id)\n const serialized = this.serializer.serialize(transaction)\n await this.storage.set(key, serialized)\n },\n )\n }\n\n async get(id: string): Promise<OfflineTransaction | null> {\n return withSpan(`outbox.get`, { 'transaction.id': id }, async (span) => {\n const key = this.getStorageKey(id)\n const data = await this.storage.get(key)\n\n if (!data) {\n span.setAttribute(`result`, `not_found`)\n return null\n }\n\n try {\n const transaction = this.serializer.deserialize(data)\n span.setAttribute(`result`, `found`)\n return transaction\n } catch (error) {\n if (error instanceof MissingTemporalConstructorError) {\n error.message = `transaction ${id}: ${error.message}`\n throw error\n }\n console.warn(`Failed to deserialize transaction ${id}:`, error)\n span.setAttribute(`result`, `deserialize_error`)\n return null\n }\n })\n }\n\n async getAll(): Promise<Array<OfflineTransaction>> {\n return this.withAll((transactions) => transactions)\n }\n\n async withAll<T>(\n consume: (transactions: Array<OfflineTransaction>) => T,\n ): Promise<T> {\n const read = { revision: this.removalRevision }\n this.activeReads.add(read)\n try {\n return await withSpan(`outbox.getAll`, {}, async (span) => {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) =>\n key.startsWith(this.keyPrefix),\n )\n\n span.setAttribute(`transactionCount`, transactionKeys.length)\n\n const transactions: Array<OfflineTransaction> = []\n\n for (const key of transactionKeys) {\n const data = await this.storage.get(key)\n if (data) {\n try {\n const transaction = this.serializer.deserialize(data)\n transactions.push(transaction)\n } catch (error) {\n if (error instanceof MissingTemporalConstructorError) {\n error.message = `transaction ${key.slice(this.keyPrefix.length)}: ${error.message}`\n throw error\n }\n console.warn(\n `Failed to deserialize transaction from key ${key}:`,\n error,\n )\n }\n }\n }\n\n const currentTransactions = transactions\n .filter(\n ({ id }) => (this.removedAtRevision.get(id) ?? 0) <= read.revision,\n )\n .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())\n // Keep the read registered until replay admission completes. A durable\n // removal cannot land between filtering and this synchronous consumer.\n return consume(currentTransactions)\n })\n } finally {\n this.activeReads.delete(read)\n this.pruneRemovalHistory()\n }\n }\n\n async getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>> {\n const allTransactions = await this.getAll()\n const keySet = new Set(keys)\n\n return allTransactions.filter((transaction) =>\n transaction.keys.some((key) => keySet.has(key)),\n )\n }\n\n async update(\n id: string,\n updates: Partial<OfflineTransaction>,\n ): Promise<void> {\n return withSpan(`outbox.update`, { 'transaction.id': id }, async () => {\n const existing = await this.get(id)\n if (!existing) {\n throw new OutboxTransactionNotFoundError(`Transaction ${id} not found`)\n }\n\n const updated = { ...existing, ...updates }\n await this.add(updated)\n })\n }\n\n async remove(id: string): Promise<void> {\n return withSpan(`outbox.remove`, { 'transaction.id': id }, async () => {\n const key = this.getStorageKey(id)\n await this.storage.delete(key)\n this.recordRemovals([id])\n })\n }\n\n async removeMany(ids: Array<string>): Promise<void> {\n return withSpan(`outbox.removeMany`, { count: ids.length }, async () => {\n await Promise.all(ids.map((id) => this.remove(id)))\n })\n }\n\n async clear(): Promise<void> {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix))\n\n await this.removeMany(\n transactionKeys.map((key) => key.slice(this.keyPrefix.length)),\n )\n }\n\n async count(): Promise<number> {\n const keys = await this.storage.keys()\n return keys.filter((key) => key.startsWith(this.keyPrefix)).length\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -51,6 +51,10 @@ function setDataProperty(object, key, value) {
|
|
|
51
51
|
writable: true
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
+
function assertOutboxPhase(phase) {
|
|
55
|
+
if (phase !== void 0 && phase !== `deletion-pending` && phase !== `rejection-pending`)
|
|
56
|
+
throw new Error(`Unsupported transaction outbox phase`);
|
|
57
|
+
}
|
|
54
58
|
class TransactionSerializer {
|
|
55
59
|
constructor(collections) {
|
|
56
60
|
this.collections = collections;
|
|
@@ -60,6 +64,7 @@ class TransactionSerializer {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
serialize(transaction) {
|
|
67
|
+
assertOutboxPhase(transaction.outboxPhase);
|
|
63
68
|
const serialized = {
|
|
64
69
|
...transaction,
|
|
65
70
|
valueEncoding: 3,
|
|
@@ -74,6 +79,7 @@ class TransactionSerializer {
|
|
|
74
79
|
deserialize(data) {
|
|
75
80
|
const {
|
|
76
81
|
valueEncoding,
|
|
82
|
+
outboxPhase,
|
|
77
83
|
...parsed
|
|
78
84
|
} = JSON.parse(data);
|
|
79
85
|
if (valueEncoding !== void 0 && valueEncoding !== 2 && valueEncoding !== 3) {
|
|
@@ -81,6 +87,7 @@ class TransactionSerializer {
|
|
|
81
87
|
`Unsupported transaction value encoding: ${valueEncoding}`
|
|
82
88
|
);
|
|
83
89
|
}
|
|
90
|
+
assertOutboxPhase(outboxPhase);
|
|
84
91
|
const createdAt = new Date(parsed.createdAt);
|
|
85
92
|
if (isNaN(createdAt.getTime())) {
|
|
86
93
|
throw new Error(
|
|
@@ -89,6 +96,7 @@ class TransactionSerializer {
|
|
|
89
96
|
}
|
|
90
97
|
return {
|
|
91
98
|
...parsed,
|
|
99
|
+
...outboxPhase === void 0 ? {} : { outboxPhase },
|
|
92
100
|
createdAt,
|
|
93
101
|
metadata: valueEncoding === 3 ? this.deserializeValue(parsed.metadata, valueEncoding) : parsed.metadata,
|
|
94
102
|
mutations: parsed.mutations.map(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nconst temporalConstructorNames = [\n `Duration`,\n `Instant`,\n `PlainDate`,\n `PlainDateTime`,\n `PlainMonthDay`,\n `PlainTime`,\n `PlainYearMonth`,\n `ZonedDateTime`,\n] as const\n\ntype TemporalConstructorName = (typeof temporalConstructorNames)[number]\ntype TemporalConstructor = {\n from: (value: string) => unknown\n prototype?: { toString?: () => string }\n}\n\nfunction getTemporalConstructorName(\n type: unknown,\n): TemporalConstructorName | undefined {\n if (typeof type !== `string` || !type.startsWith(`Temporal.`)) return\n const constructorName = type.slice(\n `Temporal.`.length,\n ) as TemporalConstructorName\n return temporalConstructorNames.includes(constructorName)\n ? constructorName\n : undefined\n}\n\nfunction requireTemporalConstructor(\n name: TemporalConstructorName,\n): TemporalConstructor {\n const constructor = (\n globalThis as {\n Temporal?: Partial<Record<TemporalConstructorName, TemporalConstructor>>\n }\n ).Temporal?.[name]\n if (typeof constructor?.from !== `function`)\n throw new MissingTemporalConstructorError(\n `Missing global Temporal.${name} constructor`,\n )\n return constructor\n}\n\nfunction serializeTemporalValue(\n value: object,\n name: TemporalConstructorName,\n): string | undefined {\n const constructor = requireTemporalConstructor(name)\n const toString = constructor.prototype?.toString\n if (typeof toString !== `function` || toString === Object.prototype.toString)\n return\n try {\n const serialized = toString.call(value)\n return typeof serialized === `string` ? serialized : undefined\n } catch {\n // Temporal prototype methods brand-check their receiver. A matching\n // Symbol.toStringTag without the corresponding internal slots is user data.\n return\n }\n}\n\nfunction mayBeNativeScalar(value: object): boolean {\n const prototype = Object.getPrototypeOf(value)\n return (\n prototype !== null &&\n prototype !== Object.prototype &&\n !Array.isArray(value)\n )\n}\n\nexport class MissingTemporalConstructorError extends Error {}\n\nfunction setDataProperty(\n object: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n if (key !== `__proto__`) object[key] = value\n else\n Object.defineProperty(object, key, {\n value,\n enumerable: true,\n configurable: true,\n writable: true,\n })\n}\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n valueEncoding: 3,\n createdAt: transaction.createdAt.toISOString(),\n metadata: this.serializeValue(transaction.metadata, `metadata`),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Old records retain their Date-marker meaning. New records also escape\n // marker-shaped user objects; the encoding tag is not application metadata.\n const {\n valueEncoding,\n ...parsed\n }: Omit<SerializedOfflineTransaction, `valueEncoding`> & {\n valueEncoding?: unknown\n } = JSON.parse(data)\n if (\n valueEncoding !== undefined &&\n valueEncoding !== 2 &&\n valueEncoding !== 3\n ) {\n throw new Error(\n `Unsupported transaction value encoding: ${valueEncoding}`,\n )\n }\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n metadata:\n valueEncoding === 3\n ? this.deserializeValue(parsed.metadata, valueEncoding)\n : parsed.metadata,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData, valueEncoding),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(\n data: SerializedMutation,\n valueEncoding: 2 | 3 | undefined,\n ): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified, valueEncoding)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original, valueEncoding),\n changes: this.deserializeValue(data.changes, valueEncoding) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(\n value: any,\n jsonKey?: string | false,\n ancestors = new WeakSet<object>(),\n ): any {\n if (value === null || typeof value !== `object`) return value\n\n if (jsonKey !== false && value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n const temporalConstructorName =\n jsonKey !== false && mayBeNativeScalar(value)\n ? getTemporalConstructorName(value[Symbol.toStringTag])\n : undefined\n if (temporalConstructorName) {\n const temporalValue = serializeTemporalValue(\n value,\n temporalConstructorName,\n )\n if (temporalValue !== undefined)\n return {\n __type: `Temporal`,\n type: `Temporal.${temporalConstructorName}`,\n value: temporalValue,\n }\n }\n\n if (ancestors.has(value))\n throw new TypeError(`Converting circular structure to JSON`)\n\n const toJSON = typeof jsonKey === `string` && value.toJSON\n if (typeof toJSON === `function`) {\n const replacement = toJSON.call(value, jsonKey)\n if (replacement === value) {\n return this.serializeValue(replacement, false, ancestors)\n }\n\n ancestors.add(value)\n try {\n return this.serializeValue(replacement, false, ancestors)\n } finally {\n ancestors.delete(value)\n }\n }\n if (\n jsonKey !== undefined &&\n (value instanceof Boolean ||\n value instanceof BigInt ||\n value instanceof Number ||\n value instanceof String)\n ) {\n return value.valueOf()\n }\n\n ancestors.add(value)\n\n const isArray = Array.isArray(value)\n const result: any = isArray ? [] : {}\n try {\n if (isArray) {\n const length = value.length\n for (let index = 0; index < length; index++) {\n const key = String(index)\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[index],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n } else {\n for (const key of Object.keys(value)) {\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[key],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n }\n } finally {\n ancestors.delete(value)\n }\n if (jsonKey === false && typeof result.toJSON === `function`)\n delete result.toJSON\n return !isArray && Object.prototype.hasOwnProperty.call(value, `__type`)\n ? { __type: `Object`, value: result }\n : result\n }\n\n private deserializeValue(value: any, valueEncoding: 2 | 3 | undefined): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (\n valueEncoding === 3 &&\n typeof value === `object` &&\n value.__type === `Temporal`\n ) {\n const constructorName = getTemporalConstructorName(value.type)\n if (!constructorName)\n throw new Error(`Corrupted Temporal marker: invalid type field`)\n if (typeof value.value !== `string`)\n throw new Error(`Corrupted Temporal marker: missing value field`)\n return requireTemporalConstructor(constructorName).from(value.value)\n }\n\n if (typeof value === `object`) {\n // Unwrap once, then decode only the fields: the object's own __type is data.\n if (valueEncoding !== undefined && value.__type === `Object`) {\n if (\n value.value === null ||\n typeof value.value !== `object` ||\n Array.isArray(value.value)\n ) {\n throw new Error(`Corrupted Object marker: expected an object value`)\n }\n value = value.value\n }\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n setDataProperty(\n result,\n key,\n this.deserializeValue(value[key], valueEncoding),\n )\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nconst temporalConstructorNames = [\n `Duration`,\n `Instant`,\n `PlainDate`,\n `PlainDateTime`,\n `PlainMonthDay`,\n `PlainTime`,\n `PlainYearMonth`,\n `ZonedDateTime`,\n] as const\n\ntype TemporalConstructorName = (typeof temporalConstructorNames)[number]\ntype TemporalConstructor = {\n from: (value: string) => unknown\n prototype?: { toString?: () => string }\n}\n\nfunction getTemporalConstructorName(\n type: unknown,\n): TemporalConstructorName | undefined {\n if (typeof type !== `string` || !type.startsWith(`Temporal.`)) return\n const constructorName = type.slice(\n `Temporal.`.length,\n ) as TemporalConstructorName\n return temporalConstructorNames.includes(constructorName)\n ? constructorName\n : undefined\n}\n\nfunction requireTemporalConstructor(\n name: TemporalConstructorName,\n): TemporalConstructor {\n const constructor = (\n globalThis as {\n Temporal?: Partial<Record<TemporalConstructorName, TemporalConstructor>>\n }\n ).Temporal?.[name]\n if (typeof constructor?.from !== `function`)\n throw new MissingTemporalConstructorError(\n `Missing global Temporal.${name} constructor`,\n )\n return constructor\n}\n\nfunction serializeTemporalValue(\n value: object,\n name: TemporalConstructorName,\n): string | undefined {\n const constructor = requireTemporalConstructor(name)\n const toString = constructor.prototype?.toString\n if (typeof toString !== `function` || toString === Object.prototype.toString)\n return\n try {\n const serialized = toString.call(value)\n return typeof serialized === `string` ? serialized : undefined\n } catch {\n // Temporal prototype methods brand-check their receiver. A matching\n // Symbol.toStringTag without the corresponding internal slots is user data.\n return\n }\n}\n\nfunction mayBeNativeScalar(value: object): boolean {\n const prototype = Object.getPrototypeOf(value)\n return (\n prototype !== null &&\n prototype !== Object.prototype &&\n !Array.isArray(value)\n )\n}\n\nexport class MissingTemporalConstructorError extends Error {}\n\nfunction setDataProperty(\n object: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n if (key !== `__proto__`) object[key] = value\n else\n Object.defineProperty(object, key, {\n value,\n enumerable: true,\n configurable: true,\n writable: true,\n })\n}\n\nfunction assertOutboxPhase(\n phase: unknown,\n): asserts phase is `deletion-pending` | `rejection-pending` | undefined {\n if (\n phase !== undefined &&\n phase !== `deletion-pending` &&\n phase !== `rejection-pending`\n )\n throw new Error(`Unsupported transaction outbox phase`)\n}\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n assertOutboxPhase(transaction.outboxPhase)\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n valueEncoding: 3,\n createdAt: transaction.createdAt.toISOString(),\n metadata: this.serializeValue(transaction.metadata, `metadata`),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Old records retain their Date-marker meaning. New records also escape\n // marker-shaped user objects; the encoding tag is not application metadata.\n const {\n valueEncoding,\n outboxPhase,\n ...parsed\n }: Omit<SerializedOfflineTransaction, `valueEncoding` | `outboxPhase`> & {\n valueEncoding?: unknown\n outboxPhase?: unknown\n } = JSON.parse(data)\n if (\n valueEncoding !== undefined &&\n valueEncoding !== 2 &&\n valueEncoding !== 3\n ) {\n throw new Error(\n `Unsupported transaction value encoding: ${valueEncoding}`,\n )\n }\n assertOutboxPhase(outboxPhase)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n ...(outboxPhase === undefined ? {} : { outboxPhase }),\n createdAt,\n metadata:\n valueEncoding === 3\n ? this.deserializeValue(parsed.metadata, valueEncoding)\n : parsed.metadata,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData, valueEncoding),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(\n data: SerializedMutation,\n valueEncoding: 2 | 3 | undefined,\n ): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified, valueEncoding)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original, valueEncoding),\n changes: this.deserializeValue(data.changes, valueEncoding) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(\n value: any,\n jsonKey?: string | false,\n ancestors = new WeakSet<object>(),\n ): any {\n if (value === null || typeof value !== `object`) return value\n\n if (jsonKey !== false && value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n const temporalConstructorName =\n jsonKey !== false && mayBeNativeScalar(value)\n ? getTemporalConstructorName(value[Symbol.toStringTag])\n : undefined\n if (temporalConstructorName) {\n const temporalValue = serializeTemporalValue(\n value,\n temporalConstructorName,\n )\n if (temporalValue !== undefined)\n return {\n __type: `Temporal`,\n type: `Temporal.${temporalConstructorName}`,\n value: temporalValue,\n }\n }\n\n if (ancestors.has(value))\n throw new TypeError(`Converting circular structure to JSON`)\n\n const toJSON = typeof jsonKey === `string` && value.toJSON\n if (typeof toJSON === `function`) {\n const replacement = toJSON.call(value, jsonKey)\n if (replacement === value) {\n return this.serializeValue(replacement, false, ancestors)\n }\n\n ancestors.add(value)\n try {\n return this.serializeValue(replacement, false, ancestors)\n } finally {\n ancestors.delete(value)\n }\n }\n if (\n jsonKey !== undefined &&\n (value instanceof Boolean ||\n value instanceof BigInt ||\n value instanceof Number ||\n value instanceof String)\n ) {\n return value.valueOf()\n }\n\n ancestors.add(value)\n\n const isArray = Array.isArray(value)\n const result: any = isArray ? [] : {}\n try {\n if (isArray) {\n const length = value.length\n for (let index = 0; index < length; index++) {\n const key = String(index)\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[index],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n } else {\n for (const key of Object.keys(value)) {\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[key],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n }\n } finally {\n ancestors.delete(value)\n }\n if (jsonKey === false && typeof result.toJSON === `function`)\n delete result.toJSON\n return !isArray && Object.prototype.hasOwnProperty.call(value, `__type`)\n ? { __type: `Object`, value: result }\n : result\n }\n\n private deserializeValue(value: any, valueEncoding: 2 | 3 | undefined): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (\n valueEncoding === 3 &&\n typeof value === `object` &&\n value.__type === `Temporal`\n ) {\n const constructorName = getTemporalConstructorName(value.type)\n if (!constructorName)\n throw new Error(`Corrupted Temporal marker: invalid type field`)\n if (typeof value.value !== `string`)\n throw new Error(`Corrupted Temporal marker: missing value field`)\n return requireTemporalConstructor(constructorName).from(value.value)\n }\n\n if (typeof value === `object`) {\n // Unwrap once, then decode only the fields: the object's own __type is data.\n if (valueEncoding !== undefined && value.__type === `Object`) {\n if (\n value.value === null ||\n typeof value.value !== `object` ||\n Array.isArray(value.value)\n ) {\n throw new Error(`Corrupted Object marker: expected an object value`)\n }\n value = value.value\n }\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n setDataProperty(\n result,\n key,\n this.deserializeValue(value[key], valueEncoding),\n )\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n /**\n * Probe IndexedDB availability by attempting to open a test database.\n * This catches private mode and other restrictions that block IndexedDB.\n */\n static async probe(): Promise<{ available: boolean; error?: Error }> {\n // Check if IndexedDB exists\n if (typeof indexedDB === `undefined`) {\n return {\n available: false,\n error: new Error(`IndexedDB is not available in this environment`),\n }\n }\n\n // Try to actually open a test database to verify it works\n try {\n const testDbName = `__offline-tx-probe__`\n const request = indexedDB.open(testDbName, 1)\n\n return new Promise((resolve) => {\n request.onerror = () => {\n const error = request.error || new Error(`IndexedDB open failed`)\n resolve({ available: false, error })\n }\n\n request.onsuccess = () => {\n // Clean up test database\n const db = request.result\n db.close()\n indexedDB.deleteDatabase(testDbName)\n resolve({ available: true })\n }\n\n request.onblocked = () => {\n resolve({\n available: false,\n error: new Error(`IndexedDB is blocked`),\n })\n }\n })\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`,\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n private waitForWrite(\n store: IDBObjectStore,\n write: () => void,\n ): Promise<void> {\n const transaction = store.transaction\n return new Promise((resolve, reject) => {\n // A successful request can still be rolled back by its transaction.\n transaction.oncomplete = () => resolve()\n transaction.onabort = () =>\n reject(\n transaction.error ??\n new DOMException(`IndexedDB transaction aborted`, `AbortError`),\n )\n write()\n })\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.put(value, key)\n })\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n throw new Error(\n `Storage quota exceeded. Consider clearing old transactions.`,\n )\n }\n throw error\n }\n }\n\n async delete(key: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.delete(key)\n })\n } catch (error) {\n console.warn(`IndexedDB delete failed:`, error)\n }\n }\n\n async keys(): Promise<Array<string>> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.getAllKeys()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result as Array<string>)\n })\n } catch (error) {\n console.warn(`IndexedDB keys failed:`, error)\n return []\n }\n }\n\n async clear(): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.clear()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n /**\n * Probe IndexedDB availability by attempting to open a test database.\n * This catches private mode and other restrictions that block IndexedDB.\n */\n static async probe(): Promise<{ available: boolean; error?: Error }> {\n // Check if IndexedDB exists\n if (typeof indexedDB === `undefined`) {\n return {\n available: false,\n error: new Error(`IndexedDB is not available in this environment`),\n }\n }\n\n // Try to actually open a test database to verify it works\n try {\n const testDbName = `__offline-tx-probe__`\n const request = indexedDB.open(testDbName, 1)\n\n return new Promise((resolve) => {\n request.onerror = () => {\n const error = request.error || new Error(`IndexedDB open failed`)\n resolve({ available: false, error })\n }\n\n request.onsuccess = () => {\n // Clean up test database\n const db = request.result\n db.close()\n indexedDB.deleteDatabase(testDbName)\n resolve({ available: true })\n }\n\n request.onblocked = () => {\n resolve({\n available: false,\n error: new Error(`IndexedDB is blocked`),\n })\n }\n })\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`,\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n private waitForWrite(\n store: IDBObjectStore,\n write: () => void,\n ): Promise<void> {\n const transaction = store.transaction\n return new Promise((resolve, reject) => {\n // A successful request can still be rolled back by its transaction.\n transaction.oncomplete = () => resolve()\n transaction.onabort = () =>\n reject(\n transaction.error ??\n new DOMException(`IndexedDB transaction aborted`, `AbortError`),\n )\n write()\n })\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.put(value, key)\n })\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n throw new Error(\n `Storage quota exceeded. Consider clearing old transactions.`,\n )\n }\n throw error\n }\n }\n\n async delete(key: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.delete(key)\n })\n } catch (error) {\n console.warn(`IndexedDB delete failed:`, error)\n throw error\n }\n }\n\n async keys(): Promise<Array<string>> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.getAllKeys()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result as Array<string>)\n })\n } catch (error) {\n console.warn(`IndexedDB keys failed:`, error)\n return []\n }\n }\n\n async clear(): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.clear()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalStorageAdapter.js","sources":["../../../src/storage/LocalStorageAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class LocalStorageAdapter extends BaseStorageAdapter {\n private prefix: string\n\n constructor(prefix = `offline-tx:`) {\n super()\n this.prefix = prefix\n }\n\n /**\n * Probe localStorage availability by attempting a test write.\n * This catches private mode and other restrictions that block localStorage.\n */\n static probe(): { available: boolean; error?: Error } {\n // Check if localStorage exists\n if (typeof localStorage === `undefined`) {\n return {\n available: false,\n error: new Error(`localStorage is not available in this environment`),\n }\n }\n\n // Try to actually write/read/delete to verify it works\n try {\n const testKey = `__offline-tx-probe__`\n const testValue = `test`\n\n localStorage.setItem(testKey, testValue)\n const retrieved = localStorage.getItem(testKey)\n localStorage.removeItem(testKey)\n\n if (retrieved !== testValue) {\n return {\n available: false,\n error: new Error(`localStorage read/write verification failed`),\n }\n }\n\n return { available: true }\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private getKey(key: string): string {\n return `${this.prefix}${key}`\n }\n\n get(key: string): Promise<string | null> {\n try {\n return Promise.resolve(localStorage.getItem(this.getKey(key)))\n } catch (error) {\n console.warn(`localStorage get failed:`, error)\n return Promise.resolve(null)\n }\n }\n\n set(key: string, value: string): Promise<void> {\n try {\n localStorage.setItem(this.getKey(key), value)\n return Promise.resolve()\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n return Promise.reject(\n new Error(\n `Storage quota exceeded. Consider clearing old transactions.`,\n ),\n )\n }\n return Promise.reject(error)\n }\n }\n\n delete(key: string): Promise<void> {\n try {\n localStorage.removeItem(this.getKey(key))\n return Promise.resolve()\n } catch (error) {\n console.warn(`localStorage delete failed:`, error)\n return Promise.
|
|
1
|
+
{"version":3,"file":"LocalStorageAdapter.js","sources":["../../../src/storage/LocalStorageAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class LocalStorageAdapter extends BaseStorageAdapter {\n private prefix: string\n\n constructor(prefix = `offline-tx:`) {\n super()\n this.prefix = prefix\n }\n\n /**\n * Probe localStorage availability by attempting a test write.\n * This catches private mode and other restrictions that block localStorage.\n */\n static probe(): { available: boolean; error?: Error } {\n // Check if localStorage exists\n if (typeof localStorage === `undefined`) {\n return {\n available: false,\n error: new Error(`localStorage is not available in this environment`),\n }\n }\n\n // Try to actually write/read/delete to verify it works\n try {\n const testKey = `__offline-tx-probe__`\n const testValue = `test`\n\n localStorage.setItem(testKey, testValue)\n const retrieved = localStorage.getItem(testKey)\n localStorage.removeItem(testKey)\n\n if (retrieved !== testValue) {\n return {\n available: false,\n error: new Error(`localStorage read/write verification failed`),\n }\n }\n\n return { available: true }\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private getKey(key: string): string {\n return `${this.prefix}${key}`\n }\n\n get(key: string): Promise<string | null> {\n try {\n return Promise.resolve(localStorage.getItem(this.getKey(key)))\n } catch (error) {\n console.warn(`localStorage get failed:`, error)\n return Promise.resolve(null)\n }\n }\n\n set(key: string, value: string): Promise<void> {\n try {\n localStorage.setItem(this.getKey(key), value)\n return Promise.resolve()\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n return Promise.reject(\n new Error(\n `Storage quota exceeded. Consider clearing old transactions.`,\n ),\n )\n }\n return Promise.reject(error)\n }\n }\n\n delete(key: string): Promise<void> {\n try {\n localStorage.removeItem(this.getKey(key))\n return Promise.resolve()\n } catch (error) {\n console.warn(`localStorage delete failed:`, error)\n return Promise.reject(error)\n }\n }\n\n keys(): Promise<Array<string>> {\n try {\n const keys: Array<string> = []\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i)\n if (key && key.startsWith(this.prefix)) {\n keys.push(key.slice(this.prefix.length))\n }\n }\n return Promise.resolve(keys)\n } catch (error) {\n console.warn(`localStorage keys failed:`, error)\n return Promise.resolve([])\n }\n }\n\n async clear(): Promise<void> {\n try {\n const keys = await this.keys()\n for (const key of keys) {\n localStorage.removeItem(this.getKey(key))\n }\n } catch (error) {\n console.warn(`localStorage clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":";AAEO,MAAM,4BAA4B,mBAAmB;AAAA,EAG1D,YAAY,SAAS,eAAe;AAClC,UAAA;AACA,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,QAA+C;AAEpD,QAAI,OAAO,iBAAiB,aAAa;AACvC,aAAO;AAAA,QACL,WAAW;AAAA,QACX,OAAO,IAAI,MAAM,mDAAmD;AAAA,MAAA;AAAA,IAExE;AAGA,QAAI;AACF,YAAM,UAAU;AAChB,YAAM,YAAY;AAElB,mBAAa,QAAQ,SAAS,SAAS;AACvC,YAAM,YAAY,aAAa,QAAQ,OAAO;AAC9C,mBAAa,WAAW,OAAO;AAE/B,UAAI,cAAc,WAAW;AAC3B,eAAO;AAAA,UACL,WAAW;AAAA,UACX,OAAO,IAAI,MAAM,6CAA6C;AAAA,QAAA;AAAA,MAElE;AAEA,aAAO,EAAE,WAAW,KAAA;AAAA,IACtB,SAAS,OAAO;AACd,aAAO;AAAA,QACL,WAAW;AAAA,QACX,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,MAAA;AAAA,IAEnE;AAAA,EACF;AAAA,EAEQ,OAAO,KAAqB;AAClC,WAAO,GAAG,KAAK,MAAM,GAAG,GAAG;AAAA,EAC7B;AAAA,EAEA,IAAI,KAAqC;AACvC,QAAI;AACF,aAAO,QAAQ,QAAQ,aAAa,QAAQ,KAAK,OAAO,GAAG,CAAC,CAAC;AAAA,IAC/D,SAAS,OAAO;AACd,cAAQ,KAAK,4BAA4B,KAAK;AAC9C,aAAO,QAAQ,QAAQ,IAAI;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,IAAI,KAAa,OAA8B;AAC7C,QAAI;AACF,mBAAa,QAAQ,KAAK,OAAO,GAAG,GAAG,KAAK;AAC5C,aAAO,QAAQ,QAAA;AAAA,IACjB,SAAS,OAAO;AACd,UACE,iBAAiB,gBACjB,MAAM,SAAS,sBACf;AACA,eAAO,QAAQ;AAAA,UACb,IAAI;AAAA,YACF;AAAA,UAAA;AAAA,QACF;AAAA,MAEJ;AACA,aAAO,QAAQ,OAAO,KAAK;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,OAAO,KAA4B;AACjC,QAAI;AACF,mBAAa,WAAW,KAAK,OAAO,GAAG,CAAC;AACxC,aAAO,QAAQ,QAAA;AAAA,IACjB,SAAS,OAAO;AACd,cAAQ,KAAK,+BAA+B,KAAK;AACjD,aAAO,QAAQ,OAAO,KAAK;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,OAA+B;AAC7B,QAAI;AACF,YAAM,OAAsB,CAAA;AAC5B,eAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,cAAM,MAAM,aAAa,IAAI,CAAC;AAC9B,YAAI,OAAO,IAAI,WAAW,KAAK,MAAM,GAAG;AACtC,eAAK,KAAK,IAAI,MAAM,KAAK,OAAO,MAAM,CAAC;AAAA,QACzC;AAAA,MACF;AACA,aAAO,QAAQ,QAAQ,IAAI;AAAA,IAC7B,SAAS,OAAO;AACd,cAAQ,KAAK,6BAA6B,KAAK;AAC/C,aAAO,QAAQ,QAAQ,EAAE;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,KAAA;AACxB,iBAAW,OAAO,MAAM;AACtB,qBAAa,WAAW,KAAK,OAAO,GAAG,CAAC;AAAA,MAC1C;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,KAAK,8BAA8B,KAAK;AAAA,IAClD;AAAA,EACF;AACF;"}
|