@tanstack/offline-transactions 1.0.53 → 1.0.56
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/dist/cjs/OfflineExecutor.cjs +26 -11
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/OfflineExecutor.d.cts +1 -0
- package/dist/cjs/api/OfflineTransaction.cjs +4 -1
- package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
- package/dist/cjs/executor/KeyScheduler.cjs +7 -1
- package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
- package/dist/cjs/executor/KeyScheduler.d.cts +1 -1
- package/dist/cjs/executor/TransactionExecutor.cjs +22 -15
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
- package/dist/cjs/executor/TransactionExecutor.d.cts +3 -2
- package/dist/cjs/outbox/TransactionSerializer.cjs +39 -10
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.cjs +16 -12
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.d.cts +1 -0
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +3 -0
- package/dist/esm/OfflineExecutor.d.ts +1 -0
- package/dist/esm/OfflineExecutor.js +26 -11
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/api/OfflineTransaction.js +4 -1
- package/dist/esm/api/OfflineTransaction.js.map +1 -1
- package/dist/esm/executor/KeyScheduler.d.ts +1 -1
- package/dist/esm/executor/KeyScheduler.js +7 -1
- package/dist/esm/executor/KeyScheduler.js.map +1 -1
- package/dist/esm/executor/TransactionExecutor.d.ts +3 -2
- package/dist/esm/executor/TransactionExecutor.js +22 -15
- package/dist/esm/executor/TransactionExecutor.js.map +1 -1
- package/dist/esm/outbox/TransactionSerializer.js +39 -10
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
- package/dist/esm/storage/IndexedDBAdapter.d.ts +1 -0
- package/dist/esm/storage/IndexedDBAdapter.js +16 -12
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
- package/dist/esm/types.d.ts +3 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/OfflineExecutor.ts +32 -14
- package/src/api/OfflineTransaction.ts +7 -2
- package/src/executor/KeyScheduler.ts +10 -2
- package/src/executor/TransactionExecutor.ts +30 -16
- package/src/outbox/TransactionSerializer.ts +59 -12
- package/src/storage/IndexedDBAdapter.ts +23 -12
- package/src/types.ts +3 -0
package/src/OfflineExecutor.ts
CHANGED
|
@@ -51,6 +51,7 @@ export class OfflineExecutor {
|
|
|
51
51
|
private leaderElection: LeaderElection | null
|
|
52
52
|
private onlineDetector: OnlineDetector
|
|
53
53
|
private isLeaderState = false
|
|
54
|
+
private disposed = false
|
|
54
55
|
private unsubscribeOnline: (() => void) | null = null
|
|
55
56
|
private unsubscribeLeadership: (() => void) | null = null
|
|
56
57
|
|
|
@@ -209,14 +210,19 @@ export class OfflineExecutor {
|
|
|
209
210
|
if (this.leaderElection) {
|
|
210
211
|
this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
|
|
211
212
|
(isLeader) => {
|
|
213
|
+
// A custom elector may repeat the initial result while replay is active.
|
|
214
|
+
if (this.disposed || isLeader === this.isLeaderState) return
|
|
212
215
|
this.isLeaderState = isLeader
|
|
216
|
+
if (!isLeader) this.executor?.pause()
|
|
213
217
|
|
|
214
218
|
if (this.config.onLeadershipChange) {
|
|
215
219
|
this.config.onLeadershipChange(isLeader)
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
if (isLeader) {
|
|
219
|
-
this.loadAndReplayTransactions()
|
|
223
|
+
this.loadAndReplayTransactions().catch((error) => {
|
|
224
|
+
console.warn(`Failed to load and replay transactions:`, error)
|
|
225
|
+
})
|
|
220
226
|
}
|
|
221
227
|
},
|
|
222
228
|
)
|
|
@@ -264,6 +270,10 @@ export class OfflineExecutor {
|
|
|
264
270
|
try {
|
|
265
271
|
// Probe storage and create adapter
|
|
266
272
|
const { storage, diagnostic } = await this.createStorage()
|
|
273
|
+
if (this.disposed) {
|
|
274
|
+
this.initResolve()
|
|
275
|
+
return
|
|
276
|
+
}
|
|
267
277
|
|
|
268
278
|
// Cast to writable to set readonly properties
|
|
269
279
|
;(this as any).storage = storage
|
|
@@ -295,6 +305,13 @@ export class OfflineExecutor {
|
|
|
295
305
|
|
|
296
306
|
// Request leadership first
|
|
297
307
|
const isLeader = await this.leaderElection.requestLeadership()
|
|
308
|
+
// Disposal may have run while leadership was pending.
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
310
|
+
if (this.disposed) {
|
|
311
|
+
this.leaderElection.releaseLeadership()
|
|
312
|
+
this.initResolve()
|
|
313
|
+
return
|
|
314
|
+
}
|
|
298
315
|
this.isLeaderState = isLeader
|
|
299
316
|
span.setAttribute(`isLeader`, isLeader)
|
|
300
317
|
|
|
@@ -327,22 +344,17 @@ export class OfflineExecutor {
|
|
|
327
344
|
return
|
|
328
345
|
}
|
|
329
346
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await this.executor.loadPendingTransactions()
|
|
347
|
+
// Startup must observe a failed read instead of reporting an empty outbox.
|
|
348
|
+
await this.executor.loadPendingTransactions()
|
|
333
349
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
})
|
|
339
|
-
} catch (error) {
|
|
340
|
-
console.warn(`Failed to load and replay transactions:`, error)
|
|
341
|
-
}
|
|
350
|
+
// Replay completion is separate from initialization and durable admission.
|
|
351
|
+
this.executor.executeAll().catch((error) => {
|
|
352
|
+
console.warn(`Failed to execute transactions:`, error)
|
|
353
|
+
})
|
|
342
354
|
}
|
|
343
355
|
|
|
344
356
|
get isOfflineEnabled(): boolean {
|
|
345
|
-
return this.mode === `offline` && this.isLeaderState
|
|
357
|
+
return !this.disposed && this.mode === `offline` && this.isLeaderState
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
/**
|
|
@@ -442,7 +454,11 @@ export class OfflineExecutor {
|
|
|
442
454
|
|
|
443
455
|
try {
|
|
444
456
|
await this.outbox.add(transaction)
|
|
445
|
-
|
|
457
|
+
// A shared queue error belongs to its execution, not every caller
|
|
458
|
+
// that durably admitted a transaction. Per-ID signals settle callers.
|
|
459
|
+
this.executor.execute(transaction).catch((error) => {
|
|
460
|
+
console.warn(`Failed to execute transactions:`, error)
|
|
461
|
+
})
|
|
446
462
|
span.setAttribute(`result`, `persisted`)
|
|
447
463
|
} catch (error) {
|
|
448
464
|
console.error(
|
|
@@ -596,6 +612,8 @@ export class OfflineExecutor {
|
|
|
596
612
|
}
|
|
597
613
|
|
|
598
614
|
dispose(): void {
|
|
615
|
+
this.disposed = true
|
|
616
|
+
this.executor?.pause()
|
|
599
617
|
for (const collection of Object.values(this.config.collections)) {
|
|
600
618
|
collection.deferDataRefresh = null
|
|
601
619
|
}
|
|
@@ -58,8 +58,13 @@ export class OfflineTransaction {
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
try {
|
|
61
|
-
|
|
62
|
-
//
|
|
61
|
+
// Persistence also drives the shared queue. This transaction can finish
|
|
62
|
+
// before that queue drains; observe both promises from the outset.
|
|
63
|
+
await Promise.race([
|
|
64
|
+
this.persistTransaction(offlineTransaction),
|
|
65
|
+
completionPromise,
|
|
66
|
+
])
|
|
67
|
+
// A queue pause (offline or retry) is not transaction completion.
|
|
63
68
|
await completionPromise
|
|
64
69
|
} catch (error) {
|
|
65
70
|
const normalizedError =
|
|
@@ -5,19 +5,27 @@ export class KeyScheduler {
|
|
|
5
5
|
private pendingTransactions: Array<OfflineTransaction> = []
|
|
6
6
|
private isRunning = false
|
|
7
7
|
|
|
8
|
-
schedule(transaction: OfflineTransaction):
|
|
9
|
-
withSyncSpan(
|
|
8
|
+
schedule(transaction: OfflineTransaction): boolean {
|
|
9
|
+
return withSyncSpan(
|
|
10
10
|
`scheduler.schedule`,
|
|
11
11
|
{
|
|
12
12
|
'transaction.id': transaction.id,
|
|
13
13
|
queueLength: this.pendingTransactions.length,
|
|
14
14
|
},
|
|
15
15
|
() => {
|
|
16
|
+
if (
|
|
17
|
+
this.pendingTransactions.some(
|
|
18
|
+
(pending) => pending.id === transaction.id,
|
|
19
|
+
)
|
|
20
|
+
) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
16
23
|
this.pendingTransactions.push(transaction)
|
|
17
24
|
// Sort by creation time to maintain FIFO order
|
|
18
25
|
this.pendingTransactions.sort(
|
|
19
26
|
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
|
|
20
27
|
)
|
|
28
|
+
return true
|
|
21
29
|
},
|
|
22
30
|
)
|
|
23
31
|
}
|
|
@@ -61,7 +61,7 @@ export class TransactionExecutor {
|
|
|
61
61
|
|
|
62
62
|
private async runExecution(): Promise<void> {
|
|
63
63
|
while (this.scheduler.getPendingCount() > 0) {
|
|
64
|
-
if (!this.
|
|
64
|
+
if (!this.canExecute()) {
|
|
65
65
|
break
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -100,8 +100,12 @@ export class TransactionExecutor {
|
|
|
100
100
|
try {
|
|
101
101
|
const result = await this.runMutationFn(transaction)
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
try {
|
|
104
|
+
// Replay can still see this ID until durable deletion settles.
|
|
105
|
+
await this.outbox.remove(transaction.id)
|
|
106
|
+
} finally {
|
|
107
|
+
this.scheduler.markCompleted(transaction)
|
|
108
|
+
}
|
|
105
109
|
|
|
106
110
|
span.setAttribute(`result`, `success`)
|
|
107
111
|
this.offlineExecutor.resolveTransaction(transaction.id, result)
|
|
@@ -233,13 +237,16 @@ export class TransactionExecutor {
|
|
|
233
237
|
filteredTransactions = this.config.beforeRetry(transactions)
|
|
234
238
|
}
|
|
235
239
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
240
|
+
// The outbox read or retry hook may outlive this owner's right to replay.
|
|
241
|
+
if (!this.offlineExecutor.isOfflineEnabled) return
|
|
242
|
+
|
|
243
|
+
const newlyLoaded = filteredTransactions.filter((transaction) =>
|
|
244
|
+
this.scheduler.schedule(transaction),
|
|
245
|
+
)
|
|
239
246
|
|
|
240
247
|
// Restore optimistic state for loaded transactions
|
|
241
248
|
// This ensures the UI shows the optimistic data while transactions are pending
|
|
242
|
-
this.restoreOptimisticState(
|
|
249
|
+
this.restoreOptimisticState(newlyLoaded)
|
|
243
250
|
|
|
244
251
|
// Reset retry delays for all loaded transactions so they can run immediately
|
|
245
252
|
this.resetRetryDelays()
|
|
@@ -326,6 +333,11 @@ export class TransactionExecutor {
|
|
|
326
333
|
this.clearRetryTimer()
|
|
327
334
|
}
|
|
328
335
|
|
|
336
|
+
pause(): void {
|
|
337
|
+
// Retain queued work and let the issued call finish its acknowledgment.
|
|
338
|
+
this.clearRetryTimer()
|
|
339
|
+
}
|
|
340
|
+
|
|
329
341
|
getPendingCount(): number {
|
|
330
342
|
return this.scheduler.getPendingCount()
|
|
331
343
|
}
|
|
@@ -334,18 +346,17 @@ export class TransactionExecutor {
|
|
|
334
346
|
// Clear existing timer
|
|
335
347
|
this.clearRetryTimer()
|
|
336
348
|
|
|
337
|
-
if (!this.
|
|
349
|
+
if (!this.canExecute()) {
|
|
338
350
|
return
|
|
339
351
|
}
|
|
340
352
|
|
|
341
|
-
|
|
342
|
-
const earliestRetryTime = this.getEarliestRetryTime()
|
|
353
|
+
const nextRetryTime = this.getNextRetryTime()
|
|
343
354
|
|
|
344
|
-
if (
|
|
355
|
+
if (nextRetryTime === null) {
|
|
345
356
|
return // No transactions pending retry
|
|
346
357
|
}
|
|
347
358
|
|
|
348
|
-
const delay = Math.max(0,
|
|
359
|
+
const delay = Math.max(0, nextRetryTime - Date.now())
|
|
349
360
|
|
|
350
361
|
this.retryTimer = setTimeout(() => {
|
|
351
362
|
this.executeAll().catch((error) => {
|
|
@@ -354,14 +365,15 @@ export class TransactionExecutor {
|
|
|
354
365
|
}, delay)
|
|
355
366
|
}
|
|
356
367
|
|
|
357
|
-
private
|
|
368
|
+
private getNextRetryTime(): number | null {
|
|
358
369
|
const allTransactions = this.scheduler.getAllPendingTransactions()
|
|
359
370
|
|
|
360
371
|
if (allTransactions.length === 0) {
|
|
361
372
|
return null
|
|
362
373
|
}
|
|
363
374
|
|
|
364
|
-
|
|
375
|
+
// Later transactions cannot overtake the FIFO head, even if they are ready.
|
|
376
|
+
return allTransactions[0]!.nextAttemptAt
|
|
365
377
|
}
|
|
366
378
|
|
|
367
379
|
private clearRetryTimer(): void {
|
|
@@ -371,8 +383,10 @@ export class TransactionExecutor {
|
|
|
371
383
|
}
|
|
372
384
|
}
|
|
373
385
|
|
|
374
|
-
private
|
|
375
|
-
return
|
|
386
|
+
private canExecute(): boolean {
|
|
387
|
+
return (
|
|
388
|
+
this.offlineExecutor.isOfflineEnabled && this.offlineExecutor.isOnline()
|
|
389
|
+
)
|
|
376
390
|
}
|
|
377
391
|
|
|
378
392
|
getRunningCount(): number {
|
|
@@ -6,6 +6,21 @@ import type {
|
|
|
6
6
|
} from '../types'
|
|
7
7
|
import type { Collection, PendingMutation } from '@tanstack/db'
|
|
8
8
|
|
|
9
|
+
function setDataProperty(
|
|
10
|
+
object: Record<string, unknown>,
|
|
11
|
+
key: string,
|
|
12
|
+
value: unknown,
|
|
13
|
+
): void {
|
|
14
|
+
if (key !== `__proto__`) object[key] = value
|
|
15
|
+
else
|
|
16
|
+
Object.defineProperty(object, key, {
|
|
17
|
+
value,
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
export class TransactionSerializer {
|
|
10
25
|
private collections: Record<string, Collection<any, any, any, any, any>>
|
|
11
26
|
private collectionIdToKey: Map<string, string>
|
|
@@ -24,6 +39,7 @@ export class TransactionSerializer {
|
|
|
24
39
|
serialize(transaction: OfflineTransaction): string {
|
|
25
40
|
const serialized: SerializedOfflineTransaction = {
|
|
26
41
|
...transaction,
|
|
42
|
+
valueEncoding: 2,
|
|
27
43
|
createdAt: transaction.createdAt.toISOString(),
|
|
28
44
|
mutations: transaction.mutations.map((mutation) =>
|
|
29
45
|
this.serializeMutation(mutation),
|
|
@@ -33,9 +49,19 @@ export class TransactionSerializer {
|
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
deserialize(data: string): OfflineTransaction {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
const
|
|
52
|
+
// Old records retain their Date-marker meaning. New records also escape
|
|
53
|
+
// marker-shaped user objects; the encoding tag is not application metadata.
|
|
54
|
+
const {
|
|
55
|
+
valueEncoding,
|
|
56
|
+
...parsed
|
|
57
|
+
}: Omit<SerializedOfflineTransaction, `valueEncoding`> & {
|
|
58
|
+
valueEncoding?: unknown
|
|
59
|
+
} = JSON.parse(data)
|
|
60
|
+
if (valueEncoding !== undefined && valueEncoding !== 2) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Unsupported transaction value encoding: ${valueEncoding}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
39
65
|
|
|
40
66
|
const createdAt = new Date(parsed.createdAt)
|
|
41
67
|
if (isNaN(createdAt.getTime())) {
|
|
@@ -48,7 +74,7 @@ export class TransactionSerializer {
|
|
|
48
74
|
...parsed,
|
|
49
75
|
createdAt,
|
|
50
76
|
mutations: parsed.mutations.map((mutationData) =>
|
|
51
|
-
this.deserializeMutation(mutationData),
|
|
77
|
+
this.deserializeMutation(mutationData, valueEncoding === 2),
|
|
52
78
|
),
|
|
53
79
|
}
|
|
54
80
|
}
|
|
@@ -71,13 +97,16 @@ export class TransactionSerializer {
|
|
|
71
97
|
}
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
private deserializeMutation(
|
|
100
|
+
private deserializeMutation(
|
|
101
|
+
data: SerializedMutation,
|
|
102
|
+
escapedObjects: boolean,
|
|
103
|
+
): PendingMutation {
|
|
75
104
|
const collection = this.collections[data.collectionId]
|
|
76
105
|
if (!collection) {
|
|
77
106
|
throw new Error(`Collection with id ${data.collectionId} not found`)
|
|
78
107
|
}
|
|
79
108
|
|
|
80
|
-
const modified = this.deserializeValue(data.modified)
|
|
109
|
+
const modified = this.deserializeValue(data.modified, escapedObjects)
|
|
81
110
|
|
|
82
111
|
// Extract the key from the modified data using the collection's getKey function
|
|
83
112
|
// This is needed for optimistic state restoration to work correctly
|
|
@@ -89,8 +118,8 @@ export class TransactionSerializer {
|
|
|
89
118
|
globalKey: data.globalKey,
|
|
90
119
|
type: data.type as any,
|
|
91
120
|
modified,
|
|
92
|
-
original: this.deserializeValue(data.original),
|
|
93
|
-
changes: this.deserializeValue(data.changes) ?? {},
|
|
121
|
+
original: this.deserializeValue(data.original, escapedObjects),
|
|
122
|
+
changes: this.deserializeValue(data.changes, escapedObjects) ?? {},
|
|
94
123
|
collection,
|
|
95
124
|
// These fields would need to be reconstructed by the executor
|
|
96
125
|
mutationId: ``, // Will be regenerated
|
|
@@ -116,16 +145,19 @@ export class TransactionSerializer {
|
|
|
116
145
|
const result: any = Array.isArray(value) ? [] : {}
|
|
117
146
|
for (const key in value) {
|
|
118
147
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
119
|
-
result
|
|
148
|
+
setDataProperty(result, key, this.serializeValue(value[key]))
|
|
120
149
|
}
|
|
121
150
|
}
|
|
122
|
-
return
|
|
151
|
+
return !Array.isArray(value) &&
|
|
152
|
+
Object.prototype.hasOwnProperty.call(value, `__type`)
|
|
153
|
+
? { __type: `Object`, value: result }
|
|
154
|
+
: result
|
|
123
155
|
}
|
|
124
156
|
|
|
125
157
|
return value
|
|
126
158
|
}
|
|
127
159
|
|
|
128
|
-
private deserializeValue(value: any): any {
|
|
160
|
+
private deserializeValue(value: any, escapedObjects: boolean): any {
|
|
129
161
|
if (value === null || value === undefined) {
|
|
130
162
|
return value
|
|
131
163
|
}
|
|
@@ -144,10 +176,25 @@ export class TransactionSerializer {
|
|
|
144
176
|
}
|
|
145
177
|
|
|
146
178
|
if (typeof value === `object`) {
|
|
179
|
+
// Unwrap once, then decode only the fields: the object's own __type is data.
|
|
180
|
+
if (escapedObjects && value.__type === `Object`) {
|
|
181
|
+
if (
|
|
182
|
+
value.value === null ||
|
|
183
|
+
typeof value.value !== `object` ||
|
|
184
|
+
Array.isArray(value.value)
|
|
185
|
+
) {
|
|
186
|
+
throw new Error(`Corrupted Object marker: expected an object value`)
|
|
187
|
+
}
|
|
188
|
+
value = value.value
|
|
189
|
+
}
|
|
147
190
|
const result: any = Array.isArray(value) ? [] : {}
|
|
148
191
|
for (const key in value) {
|
|
149
192
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
150
|
-
|
|
193
|
+
setDataProperty(
|
|
194
|
+
result,
|
|
195
|
+
key,
|
|
196
|
+
this.deserializeValue(value[key], escapedObjects),
|
|
197
|
+
)
|
|
151
198
|
}
|
|
152
199
|
}
|
|
153
200
|
return result
|
|
@@ -89,6 +89,23 @@ export class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
89
89
|
return transaction.objectStore(this.storeName)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
private waitForWrite(
|
|
93
|
+
store: IDBObjectStore,
|
|
94
|
+
write: () => void,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const transaction = store.transaction
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
// A successful request can still be rolled back by its transaction.
|
|
99
|
+
transaction.oncomplete = () => resolve()
|
|
100
|
+
transaction.onabort = () =>
|
|
101
|
+
reject(
|
|
102
|
+
transaction.error ??
|
|
103
|
+
new DOMException(`IndexedDB transaction aborted`, `AbortError`),
|
|
104
|
+
)
|
|
105
|
+
write()
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
async get(key: string): Promise<string | null> {
|
|
93
110
|
try {
|
|
94
111
|
const store = await this.getStore(`readonly`)
|
|
@@ -106,10 +123,8 @@ export class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
106
123
|
async set(key: string, value: string): Promise<void> {
|
|
107
124
|
try {
|
|
108
125
|
const store = await this.getStore(`readwrite`)
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
request.onerror = () => reject(request.error)
|
|
112
|
-
request.onsuccess = () => resolve()
|
|
126
|
+
return this.waitForWrite(store, () => {
|
|
127
|
+
store.put(value, key)
|
|
113
128
|
})
|
|
114
129
|
} catch (error) {
|
|
115
130
|
if (
|
|
@@ -127,10 +142,8 @@ export class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
127
142
|
async delete(key: string): Promise<void> {
|
|
128
143
|
try {
|
|
129
144
|
const store = await this.getStore(`readwrite`)
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
request.onerror = () => reject(request.error)
|
|
133
|
-
request.onsuccess = () => resolve()
|
|
145
|
+
return this.waitForWrite(store, () => {
|
|
146
|
+
store.delete(key)
|
|
134
147
|
})
|
|
135
148
|
} catch (error) {
|
|
136
149
|
console.warn(`IndexedDB delete failed:`, error)
|
|
@@ -154,10 +167,8 @@ export class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
154
167
|
async clear(): Promise<void> {
|
|
155
168
|
try {
|
|
156
169
|
const store = await this.getStore(`readwrite`)
|
|
157
|
-
return
|
|
158
|
-
|
|
159
|
-
request.onerror = () => reject(request.error)
|
|
160
|
-
request.onsuccess = () => resolve()
|
|
170
|
+
return this.waitForWrite(store, () => {
|
|
171
|
+
store.clear()
|
|
161
172
|
})
|
|
162
173
|
} catch (error) {
|
|
163
174
|
console.warn(`IndexedDB clear failed:`, error)
|
package/src/types.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface OfflineTransaction {
|
|
|
57
57
|
|
|
58
58
|
// Serialized representation for storage
|
|
59
59
|
export interface SerializedOfflineTransaction {
|
|
60
|
+
/** Absent for the original Date-marker format. */
|
|
61
|
+
valueEncoding?: 2
|
|
60
62
|
id: string
|
|
61
63
|
mutationFnName: string
|
|
62
64
|
mutations: Array<SerializedMutation>
|
|
@@ -131,6 +133,7 @@ export interface LeaderElection {
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
export interface TransactionSignaler {
|
|
136
|
+
readonly isOfflineEnabled: boolean
|
|
134
137
|
resolveTransaction: (transactionId: string, result: any) => void
|
|
135
138
|
rejectTransaction: (transactionId: string, error: Error) => void
|
|
136
139
|
registerRestorationTransaction: (
|