@tanstack/offline-transactions 0.0.0 → 0.1.0
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/LICENSE +21 -0
- package/dist/cjs/OfflineExecutor.cjs +141 -37
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/OfflineExecutor.d.cts +10 -1
- package/dist/cjs/api/OfflineAction.cjs +8 -1
- package/dist/cjs/api/OfflineAction.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.cjs +39 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.d.cts +8 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs +31 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -1
- package/dist/cjs/storage/LocalStorageAdapter.d.cts +8 -0
- package/dist/cjs/telemetry/tracer.cjs +1 -1
- package/dist/cjs/telemetry/tracer.cjs.map +1 -1
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +9 -4
- package/dist/esm/OfflineExecutor.d.ts +10 -1
- package/dist/esm/OfflineExecutor.js +141 -37
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/api/OfflineAction.js +8 -1
- package/dist/esm/api/OfflineAction.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/storage/IndexedDBAdapter.d.ts +8 -0
- package/dist/esm/storage/IndexedDBAdapter.js +39 -0
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
- package/dist/esm/storage/LocalStorageAdapter.d.ts +8 -0
- package/dist/esm/storage/LocalStorageAdapter.js +31 -0
- package/dist/esm/storage/LocalStorageAdapter.js.map +1 -1
- package/dist/esm/telemetry/tracer.js +1 -1
- package/dist/esm/telemetry/tracer.js.map +1 -1
- package/dist/esm/types.d.ts +9 -4
- package/dist/esm/types.js.map +1 -1
- package/package.json +12 -12
- package/src/OfflineExecutor.ts +200 -43
- package/src/api/OfflineAction.ts +14 -1
- package/src/index.ts +3 -0
- package/src/storage/IndexedDBAdapter.ts +47 -0
- package/src/storage/LocalStorageAdapter.ts +38 -0
- package/src/telemetry/tracer.ts +4 -9
- package/src/types.ts +19 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kyle Mathews
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -19,33 +19,85 @@ class OfflineExecutor {
|
|
|
19
19
|
this.unsubscribeLeadership = null;
|
|
20
20
|
this.pendingTransactionPromises = /* @__PURE__ */ new Map();
|
|
21
21
|
this.config = config;
|
|
22
|
-
this.storage = this.createStorage();
|
|
23
|
-
this.outbox = new OutboxManager.OutboxManager(this.storage, this.config.collections);
|
|
24
22
|
this.scheduler = new KeyScheduler.KeyScheduler();
|
|
25
|
-
this.executor = new TransactionExecutor.TransactionExecutor(
|
|
26
|
-
this.scheduler,
|
|
27
|
-
this.outbox,
|
|
28
|
-
this.config,
|
|
29
|
-
this
|
|
30
|
-
);
|
|
31
|
-
this.leaderElection = this.createLeaderElection();
|
|
32
23
|
this.onlineDetector = new OnlineDetector.DefaultOnlineDetector();
|
|
33
|
-
this.
|
|
24
|
+
this.storage = null;
|
|
25
|
+
this.outbox = null;
|
|
26
|
+
this.executor = null;
|
|
27
|
+
this.leaderElection = null;
|
|
28
|
+
this.mode = `offline`;
|
|
29
|
+
this.storageDiagnostic = {
|
|
30
|
+
code: `STORAGE_AVAILABLE`,
|
|
31
|
+
mode: `offline`,
|
|
32
|
+
message: `Initializing storage...`
|
|
33
|
+
};
|
|
34
|
+
this.initPromise = new Promise((resolve, reject) => {
|
|
35
|
+
this.initResolve = resolve;
|
|
36
|
+
this.initReject = reject;
|
|
37
|
+
});
|
|
34
38
|
this.initialize();
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Probe storage availability and create appropriate adapter.
|
|
42
|
+
* Returns null if no storage is available (online-only mode).
|
|
43
|
+
*/
|
|
44
|
+
async createStorage() {
|
|
37
45
|
if (this.config.storage) {
|
|
38
|
-
return
|
|
46
|
+
return {
|
|
47
|
+
storage: this.config.storage,
|
|
48
|
+
diagnostic: {
|
|
49
|
+
code: `STORAGE_AVAILABLE`,
|
|
50
|
+
mode: `offline`,
|
|
51
|
+
message: `Using custom storage adapter`
|
|
52
|
+
}
|
|
53
|
+
};
|
|
39
54
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
const idbProbe = await IndexedDBAdapter.IndexedDBAdapter.probe();
|
|
56
|
+
if (idbProbe.available) {
|
|
57
|
+
return {
|
|
58
|
+
storage: new IndexedDBAdapter.IndexedDBAdapter(),
|
|
59
|
+
diagnostic: {
|
|
60
|
+
code: `STORAGE_AVAILABLE`,
|
|
61
|
+
mode: `offline`,
|
|
62
|
+
message: `Using IndexedDB for offline storage`
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const lsProbe = LocalStorageAdapter.LocalStorageAdapter.probe();
|
|
67
|
+
if (lsProbe.available) {
|
|
68
|
+
return {
|
|
69
|
+
storage: new LocalStorageAdapter.LocalStorageAdapter(),
|
|
70
|
+
diagnostic: {
|
|
71
|
+
code: `INDEXEDDB_UNAVAILABLE`,
|
|
72
|
+
mode: `offline`,
|
|
73
|
+
message: `IndexedDB unavailable, using localStorage fallback`,
|
|
74
|
+
error: idbProbe.error
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const isSecurityError = idbProbe.error?.name === `SecurityError` || lsProbe.error?.name === `SecurityError`;
|
|
79
|
+
const isQuotaError = idbProbe.error?.name === `QuotaExceededError` || lsProbe.error?.name === `QuotaExceededError`;
|
|
80
|
+
let code;
|
|
81
|
+
let message;
|
|
82
|
+
if (isSecurityError) {
|
|
83
|
+
code = `STORAGE_BLOCKED`;
|
|
84
|
+
message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`;
|
|
85
|
+
} else if (isQuotaError) {
|
|
86
|
+
code = `QUOTA_EXCEEDED`;
|
|
87
|
+
message = `Storage quota exceeded. Running in online-only mode.`;
|
|
88
|
+
} else {
|
|
89
|
+
code = `UNKNOWN_ERROR`;
|
|
90
|
+
message = `Storage unavailable due to unknown error. Running in online-only mode.`;
|
|
48
91
|
}
|
|
92
|
+
return {
|
|
93
|
+
storage: null,
|
|
94
|
+
diagnostic: {
|
|
95
|
+
code,
|
|
96
|
+
mode: `online-only`,
|
|
97
|
+
message,
|
|
98
|
+
error: idbProbe.error || lsProbe.error
|
|
99
|
+
}
|
|
100
|
+
};
|
|
49
101
|
}
|
|
50
102
|
createLeaderElection() {
|
|
51
103
|
if (this.config.leaderElection) {
|
|
@@ -67,19 +119,21 @@ class OfflineExecutor {
|
|
|
67
119
|
}
|
|
68
120
|
}
|
|
69
121
|
setupEventListeners() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.config.onLeadershipChange
|
|
122
|
+
if (this.leaderElection) {
|
|
123
|
+
this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
|
|
124
|
+
(isLeader) => {
|
|
125
|
+
this.isLeaderState = isLeader;
|
|
126
|
+
if (this.config.onLeadershipChange) {
|
|
127
|
+
this.config.onLeadershipChange(isLeader);
|
|
128
|
+
}
|
|
129
|
+
if (isLeader) {
|
|
130
|
+
this.loadAndReplayTransactions();
|
|
131
|
+
}
|
|
75
132
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
);
|
|
133
|
+
);
|
|
134
|
+
}
|
|
81
135
|
this.unsubscribeOnline = this.onlineDetector.subscribe(() => {
|
|
82
|
-
if (this.isOfflineEnabled) {
|
|
136
|
+
if (this.isOfflineEnabled && this.executor) {
|
|
83
137
|
this.executor.resetRetryDelays();
|
|
84
138
|
this.executor.executeAll().catch((error) => {
|
|
85
139
|
console.warn(
|
|
@@ -93,17 +147,49 @@ class OfflineExecutor {
|
|
|
93
147
|
async initialize() {
|
|
94
148
|
return tracer.withSpan(`executor.initialize`, {}, async (span) => {
|
|
95
149
|
try {
|
|
150
|
+
const { storage, diagnostic } = await this.createStorage();
|
|
151
|
+
this.storage = storage;
|
|
152
|
+
this.storageDiagnostic = diagnostic;
|
|
153
|
+
this.mode = diagnostic.mode;
|
|
154
|
+
span.setAttribute(`storage.mode`, diagnostic.mode);
|
|
155
|
+
span.setAttribute(`storage.code`, diagnostic.code);
|
|
156
|
+
if (!storage) {
|
|
157
|
+
if (this.config.onStorageFailure) {
|
|
158
|
+
this.config.onStorageFailure(diagnostic);
|
|
159
|
+
}
|
|
160
|
+
span.setAttribute(`result`, `online-only`);
|
|
161
|
+
this.initResolve();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.outbox = new OutboxManager.OutboxManager(storage, this.config.collections);
|
|
165
|
+
this.executor = new TransactionExecutor.TransactionExecutor(
|
|
166
|
+
this.scheduler,
|
|
167
|
+
this.outbox,
|
|
168
|
+
this.config,
|
|
169
|
+
this
|
|
170
|
+
);
|
|
171
|
+
this.leaderElection = this.createLeaderElection();
|
|
96
172
|
const isLeader = await this.leaderElection.requestLeadership();
|
|
97
173
|
span.setAttribute(`isLeader`, isLeader);
|
|
174
|
+
this.setupEventListeners();
|
|
98
175
|
if (isLeader) {
|
|
99
176
|
await this.loadAndReplayTransactions();
|
|
100
177
|
}
|
|
178
|
+
span.setAttribute(`result`, `offline-enabled`);
|
|
179
|
+
this.initResolve();
|
|
101
180
|
} catch (error) {
|
|
102
181
|
console.warn(`Failed to initialize offline executor:`, error);
|
|
182
|
+
span.setAttribute(`result`, `failed`);
|
|
183
|
+
this.initReject(
|
|
184
|
+
error instanceof Error ? error : new Error(String(error))
|
|
185
|
+
);
|
|
103
186
|
}
|
|
104
187
|
});
|
|
105
188
|
}
|
|
106
189
|
async loadAndReplayTransactions() {
|
|
190
|
+
if (!this.executor) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
107
193
|
try {
|
|
108
194
|
await this.executor.loadPendingTransactions();
|
|
109
195
|
await this.executor.executeAll();
|
|
@@ -112,7 +198,7 @@ class OfflineExecutor {
|
|
|
112
198
|
}
|
|
113
199
|
}
|
|
114
200
|
get isOfflineEnabled() {
|
|
115
|
-
return this.isLeaderState;
|
|
201
|
+
return this.mode === `offline` && this.isLeaderState;
|
|
116
202
|
}
|
|
117
203
|
createOfflineTransaction(options) {
|
|
118
204
|
const mutationFn = this.config.mutationFns[options.mutationFnName];
|
|
@@ -163,6 +249,7 @@ class OfflineExecutor {
|
|
|
163
249
|
};
|
|
164
250
|
}
|
|
165
251
|
async persistTransaction(transaction) {
|
|
252
|
+
await this.initPromise;
|
|
166
253
|
return tracer.withNestedSpan(
|
|
167
254
|
`executor.persistTransaction`,
|
|
168
255
|
{
|
|
@@ -170,7 +257,7 @@ class OfflineExecutor {
|
|
|
170
257
|
"transaction.mutationFnName": transaction.mutationFnName
|
|
171
258
|
},
|
|
172
259
|
async (span) => {
|
|
173
|
-
if (!this.isOfflineEnabled) {
|
|
260
|
+
if (!this.isOfflineEnabled || !this.outbox || !this.executor) {
|
|
174
261
|
span.setAttribute(`result`, `skipped_not_leader`);
|
|
175
262
|
this.resolveTransaction(transaction.id, void 0);
|
|
176
263
|
return;
|
|
@@ -221,12 +308,21 @@ class OfflineExecutor {
|
|
|
221
308
|
}
|
|
222
309
|
}
|
|
223
310
|
async removeFromOutbox(id) {
|
|
311
|
+
if (!this.outbox) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
224
314
|
await this.outbox.remove(id);
|
|
225
315
|
}
|
|
226
316
|
async peekOutbox() {
|
|
317
|
+
if (!this.outbox) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
227
320
|
return this.outbox.getAll();
|
|
228
321
|
}
|
|
229
322
|
async clearOutbox() {
|
|
323
|
+
if (!this.outbox || !this.executor) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
230
326
|
await this.outbox.clear();
|
|
231
327
|
this.executor.clear();
|
|
232
328
|
}
|
|
@@ -234,9 +330,15 @@ class OfflineExecutor {
|
|
|
234
330
|
this.onlineDetector.notifyOnline();
|
|
235
331
|
}
|
|
236
332
|
getPendingCount() {
|
|
333
|
+
if (!this.executor) {
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
237
336
|
return this.executor.getPendingCount();
|
|
238
337
|
}
|
|
239
338
|
getRunningCount() {
|
|
339
|
+
if (!this.executor) {
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
240
342
|
return this.executor.getRunningCount();
|
|
241
343
|
}
|
|
242
344
|
getOnlineDetector() {
|
|
@@ -251,11 +353,13 @@ class OfflineExecutor {
|
|
|
251
353
|
this.unsubscribeLeadership();
|
|
252
354
|
this.unsubscribeLeadership = null;
|
|
253
355
|
}
|
|
254
|
-
this.leaderElection
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
356
|
+
if (this.leaderElection) {
|
|
357
|
+
this.leaderElection.releaseLeadership();
|
|
358
|
+
if (`dispose` in this.leaderElection) {
|
|
359
|
+
this.leaderElection.dispose();
|
|
360
|
+
}
|
|
258
361
|
}
|
|
362
|
+
this.onlineDetector.dispose();
|
|
259
363
|
}
|
|
260
364
|
}
|
|
261
365
|
function startOfflineExecutor(config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } from \"@tanstack/db\"\nimport { IndexedDBAdapter } from \"./storage/IndexedDBAdapter\"\nimport { LocalStorageAdapter } from \"./storage/LocalStorageAdapter\"\n\n// Core components\nimport { OutboxManager } from \"./outbox/OutboxManager\"\nimport { KeyScheduler } from \"./executor/KeyScheduler\"\nimport { TransactionExecutor } from \"./executor/TransactionExecutor\"\n\n// Coordination\nimport { WebLocksLeader } from \"./coordination/WebLocksLeader\"\nimport { BroadcastChannelLeader } from \"./coordination/BroadcastChannelLeader\"\n\n// Connectivity\nimport { DefaultOnlineDetector } from \"./connectivity/OnlineDetector\"\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from \"./api/OfflineTransaction\"\nimport { createOfflineAction } from \"./api/OfflineAction\"\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from \"./telemetry/tracer\"\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineTransaction,\n StorageAdapter,\n} from \"./types\"\nimport type { Transaction } from \"@tanstack/db\"\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n private storage: StorageAdapter\n private outbox: OutboxManager\n private scheduler: KeyScheduler\n private executor: TransactionExecutor\n private leaderElection: LeaderElection\n private onlineDetector: DefaultOnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.storage = this.createStorage()\n this.outbox = new OutboxManager(this.storage, this.config.collections)\n this.scheduler = new KeyScheduler()\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this\n )\n this.leaderElection = this.createLeaderElection()\n this.onlineDetector = new DefaultOnlineDetector()\n\n this.setupEventListeners()\n this.initialize()\n }\n\n private createStorage(): StorageAdapter {\n if (this.config.storage) {\n return this.config.storage\n }\n\n try {\n return new IndexedDBAdapter()\n } catch (error) {\n console.warn(\n `IndexedDB not available, falling back to localStorage:`,\n error\n )\n return new LocalStorageAdapter()\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n }\n )\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error\n )\n })\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n const isLeader = await this.leaderElection.requestLeadership()\n span.setAttribute(`isLeader`, isLeader)\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.isLeaderState\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || crypto.randomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: crypto.randomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction\n ): Promise<void> {\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n }\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n await this.outbox.clear()\n this.executor.clear()\n }\n\n notifyOnline(): void {\n this.onlineDetector.notifyOnline()\n }\n\n getPendingCount(): number {\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): DefaultOnlineDetector {\n return this.onlineDetector\n }\n\n dispose(): void {\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n this.leaderElection.releaseLeadership()\n this.onlineDetector.dispose()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["OutboxManager","KeyScheduler","TransactionExecutor","DefaultOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","createTransaction","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } from \"@tanstack/db\"\nimport { IndexedDBAdapter } from \"./storage/IndexedDBAdapter\"\nimport { LocalStorageAdapter } from \"./storage/LocalStorageAdapter\"\n\n// Core components\nimport { OutboxManager } from \"./outbox/OutboxManager\"\nimport { KeyScheduler } from \"./executor/KeyScheduler\"\nimport { TransactionExecutor } from \"./executor/TransactionExecutor\"\n\n// Coordination\nimport { WebLocksLeader } from \"./coordination/WebLocksLeader\"\nimport { BroadcastChannelLeader } from \"./coordination/BroadcastChannelLeader\"\n\n// Connectivity\nimport { DefaultOnlineDetector } from \"./connectivity/OnlineDetector\"\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from \"./api/OfflineTransaction\"\nimport { createOfflineAction } from \"./api/OfflineAction\"\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from \"./telemetry/tracer\"\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n StorageAdapter,\n StorageDiagnostic,\n} from \"./types\"\nimport type { Transaction } from \"@tanstack/db\"\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: DefaultOnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = new DefaultOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n\n this.initialize()\n }\n\n /**\n * Probe storage availability and create appropriate adapter.\n * Returns null if no storage is available (online-only mode).\n */\n private async createStorage(): Promise<{\n storage: StorageAdapter | null\n diagnostic: StorageDiagnostic\n }> {\n // If user provided custom storage, use it without probing\n if (this.config.storage) {\n return {\n storage: this.config.storage,\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using custom storage adapter`,\n },\n }\n }\n\n // Probe IndexedDB first\n const idbProbe = await IndexedDBAdapter.probe()\n if (idbProbe.available) {\n return {\n storage: new IndexedDBAdapter(),\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using IndexedDB for offline storage`,\n },\n }\n }\n\n // IndexedDB failed, try localStorage\n const lsProbe = LocalStorageAdapter.probe()\n if (lsProbe.available) {\n return {\n storage: new LocalStorageAdapter(),\n diagnostic: {\n code: `INDEXEDDB_UNAVAILABLE`,\n mode: `offline`,\n message: `IndexedDB unavailable, using localStorage fallback`,\n error: idbProbe.error,\n },\n }\n }\n\n // Both failed - determine the diagnostic code\n const isSecurityError =\n idbProbe.error?.name === `SecurityError` ||\n lsProbe.error?.name === `SecurityError`\n const isQuotaError =\n idbProbe.error?.name === `QuotaExceededError` ||\n lsProbe.error?.name === `QuotaExceededError`\n\n let code: StorageDiagnostic[`code`]\n let message: string\n\n if (isSecurityError) {\n code = `STORAGE_BLOCKED`\n message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`\n } else if (isQuotaError) {\n code = `QUOTA_EXCEEDED`\n message = `Storage quota exceeded. Running in online-only mode.`\n } else {\n code = `UNKNOWN_ERROR`\n message = `Storage unavailable due to unknown error. Running in online-only mode.`\n }\n\n return {\n storage: null,\n diagnostic: {\n code,\n mode: `online-only`,\n message,\n error: idbProbe.error || lsProbe.error,\n },\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n // Only set up leader election listeners if we have storage\n if (this.leaderElection) {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n }\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error\n )\n })\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n\n // Cast to writable to set readonly properties\n ;(this as any).storage = storage\n ;(this as any).storageDiagnostic = diagnostic\n ;(this as any).mode = diagnostic.mode\n\n span.setAttribute(`storage.mode`, diagnostic.mode)\n span.setAttribute(`storage.code`, diagnostic.code)\n\n if (!storage) {\n // Online-only mode - notify callback and skip offline setup\n if (this.config.onStorageFailure) {\n this.config.onStorageFailure(diagnostic)\n }\n span.setAttribute(`result`, `online-only`)\n this.initResolve()\n return\n }\n\n // Storage available - set up offline components\n this.outbox = new OutboxManager(storage, this.config.collections)\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this\n )\n this.leaderElection = this.createLeaderElection()\n\n // Request leadership first\n const isLeader = await this.leaderElection.requestLeadership()\n span.setAttribute(`isLeader`, isLeader)\n\n // Set up event listeners after leadership is established\n // This prevents the callback from being called multiple times\n this.setupEventListeners()\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n span.setAttribute(`result`, `offline-enabled`)\n this.initResolve()\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n span.setAttribute(`result`, `failed`)\n this.initReject(\n error instanceof Error ? error : new Error(String(error))\n )\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n if (!this.executor) {\n return\n }\n\n try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.mode === `offline` && this.isLeaderState\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || crypto.randomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: crypto.randomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction\n ): Promise<void> {\n // Wait for initialization to complete\n await this.initPromise\n\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled || !this.outbox || !this.executor) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n }\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n notifyOnline(): void {\n this.onlineDetector.notifyOnline()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): DefaultOnlineDetector {\n return this.onlineDetector\n }\n\n dispose(): void {\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["KeyScheduler","DefaultOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","OutboxManager","TransactionExecutor","createTransaction","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan"],"mappings":";;;;;;;;;;;;;;AAqCO,MAAM,gBAAgB;AAAA,EAiC3B,YAAY,QAAuB;AAvBnC,SAAQ,gBAAgB;AACxB,SAAQ,oBAAyC;AACjD,SAAQ,wBAA6C;AAYrD,SAAQ,iDAOA,IAAA;AAGN,SAAK,SAAS;AACd,SAAK,YAAY,IAAIA,0BAAA;AACrB,SAAK,iBAAiB,IAAIC,qCAAA;AAG1B,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,WAAW;AAChB,SAAK,iBAAiB;AAGtB,SAAK,OAAO;AACZ,SAAK,oBAAoB;AAAA,MACvB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,IAAA;AAIX,SAAK,cAAc,IAAI,QAAQ,CAAC,SAAS,WAAW;AAClD,WAAK,cAAc;AACnB,WAAK,aAAa;AAAA,IACpB,CAAC;AAED,SAAK,WAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,gBAGX;AAED,QAAI,KAAK,OAAO,SAAS;AACvB,aAAO;AAAA,QACL,SAAS,KAAK,OAAO;AAAA,QACrB,YAAY;AAAA,UACV,MAAM;AAAA,UACN,MAAM;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,MACX;AAAA,IAEJ;AAGA,UAAM,WAAW,MAAMC,iBAAAA,iBAAiB,MAAA;AACxC,QAAI,SAAS,WAAW;AACtB,aAAO;AAAA,QACL,SAAS,IAAIA,iBAAAA,iBAAA;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,MAAM;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,MACX;AAAA,IAEJ;AAGA,UAAM,UAAUC,oBAAAA,oBAAoB,MAAA;AACpC,QAAI,QAAQ,WAAW;AACrB,aAAO;AAAA,QACL,SAAS,IAAIA,oBAAAA,oBAAA;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO,SAAS;AAAA,QAAA;AAAA,MAClB;AAAA,IAEJ;AAGA,UAAM,kBACJ,SAAS,OAAO,SAAS,mBACzB,QAAQ,OAAO,SAAS;AAC1B,UAAM,eACJ,SAAS,OAAO,SAAS,wBACzB,QAAQ,OAAO,SAAS;AAE1B,QAAI;AACJ,QAAI;AAEJ,QAAI,iBAAiB;AACnB,aAAO;AACP,gBAAU;AAAA,IACZ,WAAW,cAAc;AACvB,aAAO;AACP,gBAAU;AAAA,IACZ,OAAO;AACL,aAAO;AACP,gBAAU;AAAA,IACZ;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,QACV;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA,OAAO,SAAS,SAAS,QAAQ;AAAA,MAAA;AAAA,IACnC;AAAA,EAEJ;AAAA,EAEQ,uBAAuC;AAC7C,QAAI,KAAK,OAAO,gBAAgB;AAC9B,aAAO,KAAK,OAAO;AAAA,IACrB;AAEA,QAAIC,eAAAA,eAAe,eAAe;AAChC,aAAO,IAAIA,eAAAA,eAAA;AAAA,IACb,WAAWC,8CAAuB,eAAe;AAC/C,aAAO,IAAIA,uBAAAA,uBAAA;AAAA,IACb,OAAO;AAEL,aAAO;AAAA,QACL,mBAAmB,MAAM,QAAQ,QAAQ,IAAI;AAAA,QAC7C,mBAAmB,MAAM;AAAA,QAAC;AAAA,QAC1B,UAAU,MAAM;AAAA,QAChB,oBAAoB,MAAM,MAAM;AAAA,QAAC;AAAA,MAAA;AAAA,IAErC;AAAA,EACF;AAAA,EAEQ,sBAA4B;AAElC,QAAI,KAAK,gBAAgB;AACvB,WAAK,wBAAwB,KAAK,eAAe;AAAA,QAC/C,CAAC,aAAa;AACZ,eAAK,gBAAgB;AAErB,cAAI,KAAK,OAAO,oBAAoB;AAClC,iBAAK,OAAO,mBAAmB,QAAQ;AAAA,UACzC;AAEA,cAAI,UAAU;AACZ,iBAAK,0BAAA;AAAA,UACP;AAAA,QACF;AAAA,MAAA;AAAA,IAEJ;AAEA,SAAK,oBAAoB,KAAK,eAAe,UAAU,MAAM;AAC3D,UAAI,KAAK,oBAAoB,KAAK,UAAU;AAE1C,aAAK,SAAS,iBAAA;AACd,aAAK,SAAS,WAAA,EAAa,MAAM,CAAC,UAAU;AAC1C,kBAAQ;AAAA,YACN;AAAA,YACA;AAAA,UAAA;AAAA,QAEJ,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,aAA4B;AACxC,WAAOC,OAAAA,SAAS,uBAAuB,CAAA,GAAI,OAAO,SAAS;AACzD,UAAI;AAEF,cAAM,EAAE,SAAS,WAAA,IAAe,MAAM,KAAK,cAAA;AAGzC,aAAa,UAAU;AACvB,aAAa,oBAAoB;AACjC,aAAa,OAAO,WAAW;AAEjC,aAAK,aAAa,gBAAgB,WAAW,IAAI;AACjD,aAAK,aAAa,gBAAgB,WAAW,IAAI;AAEjD,YAAI,CAAC,SAAS;AAEZ,cAAI,KAAK,OAAO,kBAAkB;AAChC,iBAAK,OAAO,iBAAiB,UAAU;AAAA,UACzC;AACA,eAAK,aAAa,UAAU,aAAa;AACzC,eAAK,YAAA;AACL;AAAA,QACF;AAGA,aAAK,SAAS,IAAIC,cAAAA,cAAc,SAAS,KAAK,OAAO,WAAW;AAChE,aAAK,WAAW,IAAIC,oBAAAA;AAAAA,UAClB,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL;AAAA,QAAA;AAEF,aAAK,iBAAiB,KAAK,qBAAA;AAG3B,cAAM,WAAW,MAAM,KAAK,eAAe,kBAAA;AAC3C,aAAK,aAAa,YAAY,QAAQ;AAItC,aAAK,oBAAA;AAEL,YAAI,UAAU;AACZ,gBAAM,KAAK,0BAAA;AAAA,QACb;AACA,aAAK,aAAa,UAAU,iBAAiB;AAC7C,aAAK,YAAA;AAAA,MACP,SAAS,OAAO;AACd,gBAAQ,KAAK,0CAA0C,KAAK;AAC5D,aAAK,aAAa,UAAU,QAAQ;AACpC,aAAK;AAAA,UACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,QAAA;AAAA,MAE5D;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,4BAA2C;AACvD,QAAI,CAAC,KAAK,UAAU;AAClB;AAAA,IACF;AAEA,QAAI;AACF,YAAM,KAAK,SAAS,wBAAA;AACpB,YAAM,KAAK,SAAS,WAAA;AAAA,IACtB,SAAS,OAAO;AACd,cAAQ,KAAK,2CAA2C,KAAK;AAAA,IAC/D;AAAA,EACF;AAAA,EAEA,IAAI,mBAA4B;AAC9B,WAAO,KAAK,SAAS,aAAa,KAAK;AAAA,EACzC;AAAA,EAEA,yBACE,SACqC;AACrC,UAAM,aAAa,KAAK,OAAO,YAAY,QAAQ,cAAc;AAEjE,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,8BAA8B,QAAQ,cAAc,EAAE;AAAA,IACxE;AAGA,QAAI,CAAC,KAAK,kBAAkB;AAG1B,aAAOC,qBAAkB;AAAA,QACvB,YAAY,QAAQ,cAAc;AAAA,QAClC,YAAY,CAAC,WACX,WAAW;AAAA,UACT,GAAG;AAAA,UACH,gBAAgB,QAAQ,kBAAkB,OAAO,WAAA;AAAA,QAAW,CAC7D;AAAA,QACH,UAAU,QAAQ;AAAA,MAAA,CACnB;AAAA,IACH;AAGA,WAAO,IAAIC,mBAAAA;AAAAA,MACT;AAAA,MACA;AAAA,MACA,KAAK,mBAAmB,KAAK,IAAI;AAAA,MACjC;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,oBAAuB,SAAwC;AAC7D,UAAM,aAAa,KAAK,OAAO,YAAY,QAAQ,cAAc;AAEjE,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,8BAA8B,QAAQ,cAAc,EAAE;AAAA,IACxE;AAGA,WAAO,CAAC,cAAiB;AAEvB,UAAI,CAAC,KAAK,kBAAkB;AAE1B,cAAMC,UAASC,GAAAA,uBAAuB;AAAA,UACpC,YAAY,CAAC,MAAM,WACjB,WAAW;AAAA,YACT,GAAG;AAAA,YACH,GAAG;AAAA,YACH,gBAAgB,OAAO,WAAA;AAAA,UAAW,CACnC;AAAA,UACH,UAAU,QAAQ;AAAA,QAAA,CACnB;AACD,eAAOD,QAAO,SAAS;AAAA,MACzB;AAGA,YAAM,SAASE,cAAAA;AAAAA,QACb;AAAA,QACA;AAAA,QACA,KAAK,mBAAmB,KAAK,IAAI;AAAA,QACjC;AAAA,MAAA;AAEF,aAAO,OAAO,SAAS;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAc,mBACZ,aACe;AAEf,UAAM,KAAK;AAEX,WAAOC,OAAAA;AAAAA,MACL;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,8BAA8B,YAAY;AAAA,MAAA;AAAA,MAE5C,OAAO,SAAS;AACd,YAAI,CAAC,KAAK,oBAAoB,CAAC,KAAK,UAAU,CAAC,KAAK,UAAU;AAC5D,eAAK,aAAa,UAAU,oBAAoB;AAChD,eAAK,mBAAmB,YAAY,IAAI,MAAS;AACjD;AAAA,QACF;AAEA,YAAI;AACF,gBAAM,KAAK,OAAO,IAAI,WAAW;AACjC,gBAAM,KAAK,SAAS,QAAQ,WAAW;AACvC,eAAK,aAAa,UAAU,WAAW;AAAA,QACzC,SAAS,OAAO;AACd,kBAAQ;AAAA,YACN,yCAAyC,YAAY,EAAE;AAAA,YACvD;AAAA,UAAA;AAEF,eAAK,aAAa,UAAU,QAAQ;AACpC,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IAAA;AAAA,EAEJ;AAAA;AAAA,EAGA,MAAM,6BAA6B,eAAqC;AACtE,UAAM,WAAW,KAAK,2BAA2B,IAAI,aAAa;AAClE,QAAI,UAAU;AACZ,aAAO,SAAS;AAAA,IAClB;AAEA,UAAM,WAIF,CAAA;AAEJ,aAAS,UAAU,IAAI,QAAQ,CAAC,SAAS,WAAW;AAClD,eAAS,UAAU;AACnB,eAAS,SAAS;AAAA,IACpB,CAAC;AAED,SAAK,2BAA2B,IAAI,eAAe,QAAQ;AAC3D,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA,EAGA,mBAAmB,eAAuB,QAAmB;AAC3D,UAAM,WAAW,KAAK,2BAA2B,IAAI,aAAa;AAClE,QAAI,UAAU;AACZ,eAAS,QAAQ,MAAM;AACvB,WAAK,2BAA2B,OAAO,aAAa;AAAA,IACtD;AAAA,EACF;AAAA;AAAA,EAGA,kBAAkB,eAAuB,OAAoB;AAC3D,UAAM,WAAW,KAAK,2BAA2B,IAAI,aAAa;AAClE,QAAI,UAAU;AACZ,eAAS,OAAO,KAAK;AACrB,WAAK,2BAA2B,OAAO,aAAa;AAAA,IACtD;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,IAA2B;AAChD,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AACA,UAAM,KAAK,OAAO,OAAO,EAAE;AAAA,EAC7B;AAAA,EAEA,MAAM,aAAiD;AACrD,QAAI,CAAC,KAAK,QAAQ;AAChB,aAAO,CAAA;AAAA,IACT;AACA,WAAO,KAAK,OAAO,OAAA;AAAA,EACrB;AAAA,EAEA,MAAM,cAA6B;AACjC,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,UAAU;AAClC;AAAA,IACF;AACA,UAAM,KAAK,OAAO,MAAA;AAClB,SAAK,SAAS,MAAA;AAAA,EAChB;AAAA,EAEA,eAAqB;AACnB,SAAK,eAAe,aAAA;AAAA,EACtB;AAAA,EAEA,kBAA0B;AACxB,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO;AAAA,IACT;AACA,WAAO,KAAK,SAAS,gBAAA;AAAA,EACvB;AAAA,EAEA,kBAA0B;AACxB,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO;AAAA,IACT;AACA,WAAO,KAAK,SAAS,gBAAA;AAAA,EACvB;AAAA,EAEA,oBAA2C;AACzC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,mBAAmB;AAC1B,WAAK,kBAAA;AACL,WAAK,oBAAoB;AAAA,IAC3B;AAEA,QAAI,KAAK,uBAAuB;AAC9B,WAAK,sBAAA;AACL,WAAK,wBAAwB;AAAA,IAC/B;AAEA,QAAI,KAAK,gBAAgB;AACvB,WAAK,eAAe,kBAAA;AAEpB,UAAI,aAAa,KAAK,gBAAgB;AAClC,aAAK,eAAuB,QAAA;AAAA,MAChC;AAAA,IACF;AAEA,SAAK,eAAe,QAAA;AAAA,EACtB;AACF;AAEO,SAAS,qBAAqB,QAAwC;AAC3E,SAAO,IAAI,gBAAgB,MAAM;AACnC;;;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DefaultOnlineDetector } from './connectivity/OnlineDetector.cjs';
|
|
2
2
|
import { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction.cjs';
|
|
3
|
-
import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineTransaction } from './types.cjs';
|
|
3
|
+
import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineMode, OfflineTransaction, StorageDiagnostic } from './types.cjs';
|
|
4
4
|
import { Transaction } from '@tanstack/db';
|
|
5
5
|
export declare class OfflineExecutor {
|
|
6
6
|
private config;
|
|
@@ -13,8 +13,17 @@ export declare class OfflineExecutor {
|
|
|
13
13
|
private isLeaderState;
|
|
14
14
|
private unsubscribeOnline;
|
|
15
15
|
private unsubscribeLeadership;
|
|
16
|
+
readonly mode: OfflineMode;
|
|
17
|
+
readonly storageDiagnostic: StorageDiagnostic;
|
|
18
|
+
private initPromise;
|
|
19
|
+
private initResolve;
|
|
20
|
+
private initReject;
|
|
16
21
|
private pendingTransactionPromises;
|
|
17
22
|
constructor(config: OfflineConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Probe storage availability and create appropriate adapter.
|
|
25
|
+
* Returns null if no storage is available (online-only mode).
|
|
26
|
+
*/
|
|
18
27
|
private createStorage;
|
|
19
28
|
private createLeaderElection;
|
|
20
29
|
private setupEventListeners;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const api = require("@opentelemetry/api");
|
|
4
|
+
const db = require("@tanstack/db");
|
|
4
5
|
const OfflineTransaction = require("./OfflineTransaction.cjs");
|
|
6
|
+
function isPromiseLike(value) {
|
|
7
|
+
return !!value && (typeof value === `object` || typeof value === `function`) && typeof value.then === `function`;
|
|
8
|
+
}
|
|
5
9
|
function createOfflineAction(options, mutationFn, persistTransaction, executor) {
|
|
6
10
|
const { mutationFnName, onMutate } = options;
|
|
7
11
|
console.log(`createOfflineAction 2`, options);
|
|
@@ -17,7 +21,10 @@ function createOfflineAction(options, mutationFn, persistTransaction, executor)
|
|
|
17
21
|
);
|
|
18
22
|
const transaction = offlineTransaction.mutate(() => {
|
|
19
23
|
console.log(`mutate`);
|
|
20
|
-
onMutate(variables);
|
|
24
|
+
const maybePromise = onMutate(variables);
|
|
25
|
+
if (isPromiseLike(maybePromise)) {
|
|
26
|
+
throw new db.OnMutateMustBeSynchronousError();
|
|
27
|
+
}
|
|
21
28
|
});
|
|
22
29
|
const tracer = api.trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`);
|
|
23
30
|
const span = tracer.startSpan(`offlineAction.${mutationFnName}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineAction.cjs","sources":["../../../src/api/OfflineAction.ts"],"sourcesContent":["import { SpanStatusCode, context, trace } from \"@opentelemetry/api\"\nimport { OfflineTransaction } from \"./OfflineTransaction\"\nimport type { Transaction } from \"@tanstack/db\"\nimport type {\n CreateOfflineActionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from \"../types\"\n\nexport function createOfflineAction<T>(\n options: CreateOfflineActionOptions<T>,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any\n): (variables: T) => Transaction {\n const { mutationFnName, onMutate } = options\n console.log(`createOfflineAction 2`, options)\n\n return (variables: T): Transaction => {\n const offlineTransaction = new OfflineTransaction(\n {\n mutationFnName,\n autoCommit: false,\n },\n mutationFn,\n persistTransaction,\n executor\n )\n\n const transaction = offlineTransaction.mutate(() => {\n console.log(`mutate`)\n onMutate(variables)\n })\n\n // Immediately commit with span instrumentation\n const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)\n const span = tracer.startSpan(`offlineAction.${mutationFnName}`)\n const ctx = trace.setSpan(context.active(), span)\n console.log(`starting offlineAction span`, { tracer, span, ctx })\n\n // Execute the commit within the span context\n // The key is to return the promise synchronously from context.with() so context binds to it\n const commitPromise = context.with(ctx, () => {\n // Return the promise synchronously - this is critical for context propagation in browsers\n return (async () => {\n try {\n await transaction.commit()\n span.setStatus({ code: SpanStatusCode.OK })\n span.end()\n console.log(`ended offlineAction span - success`)\n } catch (error) {\n span.recordException(error as Error)\n span.setStatus({ code: SpanStatusCode.ERROR })\n span.end()\n console.log(`ended offlineAction span - error`)\n }\n })()\n })\n\n // Don't await - this is fire-and-forget for optimistic actions\n // But catch to prevent unhandled rejection\n commitPromise.catch(() => {\n // Already handled in try/catch above\n })\n\n return transaction\n }\n}\n"],"names":["OfflineTransaction","trace","context","SpanStatusCode"],"mappings":"
|
|
1
|
+
{"version":3,"file":"OfflineAction.cjs","sources":["../../../src/api/OfflineAction.ts"],"sourcesContent":["import { SpanStatusCode, context, trace } from \"@opentelemetry/api\"\nimport { OnMutateMustBeSynchronousError } from \"@tanstack/db\"\nimport { OfflineTransaction } from \"./OfflineTransaction\"\nimport type { Transaction } from \"@tanstack/db\"\nimport type {\n CreateOfflineActionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from \"../types\"\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return (\n !!value &&\n (typeof value === `object` || typeof value === `function`) &&\n typeof (value as { then?: unknown }).then === `function`\n )\n}\n\nexport function createOfflineAction<T>(\n options: CreateOfflineActionOptions<T>,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any\n): (variables: T) => Transaction {\n const { mutationFnName, onMutate } = options\n console.log(`createOfflineAction 2`, options)\n\n return (variables: T): Transaction => {\n const offlineTransaction = new OfflineTransaction(\n {\n mutationFnName,\n autoCommit: false,\n },\n mutationFn,\n persistTransaction,\n executor\n )\n\n const transaction = offlineTransaction.mutate(() => {\n console.log(`mutate`)\n const maybePromise = onMutate(variables) as unknown\n\n if (isPromiseLike(maybePromise)) {\n throw new OnMutateMustBeSynchronousError()\n }\n })\n\n // Immediately commit with span instrumentation\n const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)\n const span = tracer.startSpan(`offlineAction.${mutationFnName}`)\n const ctx = trace.setSpan(context.active(), span)\n console.log(`starting offlineAction span`, { tracer, span, ctx })\n\n // Execute the commit within the span context\n // The key is to return the promise synchronously from context.with() so context binds to it\n const commitPromise = context.with(ctx, () => {\n // Return the promise synchronously - this is critical for context propagation in browsers\n return (async () => {\n try {\n await transaction.commit()\n span.setStatus({ code: SpanStatusCode.OK })\n span.end()\n console.log(`ended offlineAction span - success`)\n } catch (error) {\n span.recordException(error as Error)\n span.setStatus({ code: SpanStatusCode.ERROR })\n span.end()\n console.log(`ended offlineAction span - error`)\n }\n })()\n })\n\n // Don't await - this is fire-and-forget for optimistic actions\n // But catch to prevent unhandled rejection\n commitPromise.catch(() => {\n // Already handled in try/catch above\n })\n\n return transaction\n }\n}\n"],"names":["OfflineTransaction","OnMutateMustBeSynchronousError","trace","context","SpanStatusCode"],"mappings":";;;;;AAUA,SAAS,cAAc,OAA+C;AACpE,SACE,CAAC,CAAC,UACD,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;AAEO,SAAS,oBACd,SACA,YACA,oBACA,UAC+B;AAC/B,QAAM,EAAE,gBAAgB,SAAA,IAAa;AACrC,UAAQ,IAAI,yBAAyB,OAAO;AAE5C,SAAO,CAAC,cAA8B;AACpC,UAAM,qBAAqB,IAAIA,mBAAAA;AAAAA,MAC7B;AAAA,QACE;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAEd;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAGF,UAAM,cAAc,mBAAmB,OAAO,MAAM;AAClD,cAAQ,IAAI,QAAQ;AACpB,YAAM,eAAe,SAAS,SAAS;AAEvC,UAAI,cAAc,YAAY,GAAG;AAC/B,cAAM,IAAIC,GAAAA,+BAAA;AAAA,MACZ;AAAA,IACF,CAAC;AAGD,UAAM,SAASC,IAAAA,MAAM,UAAU,kCAAkC,OAAO;AACxE,UAAM,OAAO,OAAO,UAAU,iBAAiB,cAAc,EAAE;AAC/D,UAAM,MAAMA,IAAAA,MAAM,QAAQC,IAAAA,QAAQ,OAAA,GAAU,IAAI;AAChD,YAAQ,IAAI,+BAA+B,EAAE,QAAQ,MAAM,KAAK;AAIhE,UAAM,gBAAgBA,IAAAA,QAAQ,KAAK,KAAK,MAAM;AAE5C,cAAQ,YAAY;AAClB,YAAI;AACF,gBAAM,YAAY,OAAA;AAClB,eAAK,UAAU,EAAE,MAAMC,IAAAA,eAAe,IAAI;AAC1C,eAAK,IAAA;AACL,kBAAQ,IAAI,oCAAoC;AAAA,QAClD,SAAS,OAAO;AACd,eAAK,gBAAgB,KAAc;AACnC,eAAK,UAAU,EAAE,MAAMA,IAAAA,eAAe,OAAO;AAC7C,eAAK,IAAA;AACL,kBAAQ,IAAI,kCAAkC;AAAA,QAChD;AAAA,MACF,GAAA;AAAA,IACF,CAAC;AAID,kBAAc,MAAM,MAAM;AAAA,IAE1B,CAAC;AAED,WAAO;AAAA,EACT;AACF;;"}
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { OfflineExecutor, startOfflineExecutor } from './OfflineExecutor.cjs';
|
|
2
|
-
export type { OfflineTransaction, OfflineConfig, StorageAdapter, RetryPolicy, LeaderElection, OnlineDetector, CreateOfflineTransactionOptions, CreateOfflineActionOptions, SerializedError, SerializedMutation, } from './types.cjs';
|
|
2
|
+
export type { OfflineTransaction, OfflineConfig, OfflineMode, StorageAdapter, StorageDiagnostic, StorageDiagnosticCode, RetryPolicy, LeaderElection, OnlineDetector, CreateOfflineTransactionOptions, CreateOfflineActionOptions, SerializedError, SerializedMutation, } from './types.cjs';
|
|
3
3
|
export { NonRetriableError } from './types.cjs';
|
|
4
4
|
export { IndexedDBAdapter } from './storage/IndexedDBAdapter.cjs';
|
|
5
5
|
export { LocalStorageAdapter } from './storage/LocalStorageAdapter.cjs';
|
|
@@ -8,6 +8,45 @@ class IndexedDBAdapter extends StorageAdapter.BaseStorageAdapter {
|
|
|
8
8
|
this.dbName = dbName;
|
|
9
9
|
this.storeName = storeName;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Probe IndexedDB availability by attempting to open a test database.
|
|
13
|
+
* This catches private mode and other restrictions that block IndexedDB.
|
|
14
|
+
*/
|
|
15
|
+
static async probe() {
|
|
16
|
+
if (typeof indexedDB === `undefined`) {
|
|
17
|
+
return {
|
|
18
|
+
available: false,
|
|
19
|
+
error: new Error(`IndexedDB is not available in this environment`)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const testDbName = `__offline-tx-probe__`;
|
|
24
|
+
const request = indexedDB.open(testDbName, 1);
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
request.onerror = () => {
|
|
27
|
+
const error = request.error || new Error(`IndexedDB open failed`);
|
|
28
|
+
resolve({ available: false, error });
|
|
29
|
+
};
|
|
30
|
+
request.onsuccess = () => {
|
|
31
|
+
const db = request.result;
|
|
32
|
+
db.close();
|
|
33
|
+
indexedDB.deleteDatabase(testDbName);
|
|
34
|
+
resolve({ available: true });
|
|
35
|
+
};
|
|
36
|
+
request.onblocked = () => {
|
|
37
|
+
resolve({
|
|
38
|
+
available: false,
|
|
39
|
+
error: new Error(`IndexedDB is blocked`)
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
available: false,
|
|
46
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
11
50
|
async openDB() {
|
|
12
51
|
if (this.db) {
|
|
13
52
|
return this.db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexedDBAdapter.cjs","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 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 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 new Promise((resolve, reject) => {\n const request = store.put(value, key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\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 new Promise((resolve, reject) => {\n const request = store.delete(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\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 new Promise((resolve, reject) => {\n const request = store.clear()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":["BaseStorageAdapter"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"IndexedDBAdapter.cjs","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 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 new Promise((resolve, reject) => {\n const request = store.put(value, key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\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 new Promise((resolve, reject) => {\n const request = store.delete(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\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 new Promise((resolve, reject) => {\n const request = store.clear()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":["BaseStorageAdapter"],"mappings":"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"}
|
|
@@ -4,6 +4,14 @@ export declare class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
4
4
|
private storeName;
|
|
5
5
|
private db;
|
|
6
6
|
constructor(dbName?: string, storeName?: string);
|
|
7
|
+
/**
|
|
8
|
+
* Probe IndexedDB availability by attempting to open a test database.
|
|
9
|
+
* This catches private mode and other restrictions that block IndexedDB.
|
|
10
|
+
*/
|
|
11
|
+
static probe(): Promise<{
|
|
12
|
+
available: boolean;
|
|
13
|
+
error?: Error;
|
|
14
|
+
}>;
|
|
7
15
|
private openDB;
|
|
8
16
|
private getStore;
|
|
9
17
|
get(key: string): Promise<string | null>;
|
|
@@ -6,6 +6,37 @@ class LocalStorageAdapter extends StorageAdapter.BaseStorageAdapter {
|
|
|
6
6
|
super();
|
|
7
7
|
this.prefix = prefix;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Probe localStorage availability by attempting a test write.
|
|
11
|
+
* This catches private mode and other restrictions that block localStorage.
|
|
12
|
+
*/
|
|
13
|
+
static probe() {
|
|
14
|
+
if (typeof localStorage === `undefined`) {
|
|
15
|
+
return {
|
|
16
|
+
available: false,
|
|
17
|
+
error: new Error(`localStorage is not available in this environment`)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const testKey = `__offline-tx-probe__`;
|
|
22
|
+
const testValue = `test`;
|
|
23
|
+
localStorage.setItem(testKey, testValue);
|
|
24
|
+
const retrieved = localStorage.getItem(testKey);
|
|
25
|
+
localStorage.removeItem(testKey);
|
|
26
|
+
if (retrieved !== testValue) {
|
|
27
|
+
return {
|
|
28
|
+
available: false,
|
|
29
|
+
error: new Error(`localStorage read/write verification failed`)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { available: true };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
available: false,
|
|
36
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
9
40
|
getKey(key) {
|
|
10
41
|
return `${this.prefix}${key}`;
|
|
11
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalStorageAdapter.cjs","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 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.resolve()\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":["BaseStorageAdapter"],"mappings":";;;AAEO,MAAM,4BAA4BA,eAAAA,mBAAmB;AAAA,EAG1D,YAAY,SAAS,eAAe;AAClC,UAAA;AACA,SAAK,SAAS;AAAA,EAChB;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,QAAA;AAAA,IACjB;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;;"}
|
|
1
|
+
{"version":3,"file":"LocalStorageAdapter.cjs","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.resolve()\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":["BaseStorageAdapter"],"mappings":";;;AAEO,MAAM,4BAA4BA,eAAAA,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,QAAA;AAAA,IACjB;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;;"}
|
|
@@ -2,6 +2,14 @@ import { BaseStorageAdapter } from './StorageAdapter.cjs';
|
|
|
2
2
|
export declare class LocalStorageAdapter extends BaseStorageAdapter {
|
|
3
3
|
private prefix;
|
|
4
4
|
constructor(prefix?: string);
|
|
5
|
+
/**
|
|
6
|
+
* Probe localStorage availability by attempting a test write.
|
|
7
|
+
* This catches private mode and other restrictions that block localStorage.
|
|
8
|
+
*/
|
|
9
|
+
static probe(): {
|
|
10
|
+
available: boolean;
|
|
11
|
+
error?: Error;
|
|
12
|
+
};
|
|
5
13
|
private getKey;
|
|
6
14
|
get(key: string): Promise<string | null>;
|
|
7
15
|
set(key: string, value: string): Promise<void>;
|