@tanstack/db-sqlite-persistence-core 0.2.23 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -7
- package/dist/cjs/broadcast-coordinator.cjs +1689 -0
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -0
- package/dist/cjs/broadcast-coordinator.d.cts +116 -0
- package/dist/cjs/errors.cjs +73 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +44 -0
- package/dist/cjs/index.cjs +19 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/persisted.cjs +1782 -394
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +176 -11
- package/dist/cjs/remote-subset-owner.cjs +20 -0
- package/dist/cjs/remote-subset-owner.cjs.map +1 -0
- package/dist/cjs/remote-subset-owner.d.cts +7 -0
- package/dist/cjs/remote-subset-wire.cjs +681 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -0
- package/dist/cjs/remote-subset-wire.d.cts +59 -0
- package/dist/cjs/sqlite-core-adapter.cjs +969 -364
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +45 -1
- package/dist/cjs/sqlite-value.cjs +31 -0
- package/dist/cjs/sqlite-value.cjs.map +1 -0
- package/dist/cjs/sqlite-value.d.cts +10 -0
- package/dist/esm/broadcast-coordinator.d.ts +116 -0
- package/dist/esm/broadcast-coordinator.js +1689 -0
- package/dist/esm/broadcast-coordinator.js.map +1 -0
- package/dist/esm/errors.d.ts +44 -0
- package/dist/esm/errors.js +74 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +21 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/persisted.d.ts +176 -11
- package/dist/esm/persisted.js +1784 -396
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-owner.d.ts +7 -0
- package/dist/esm/remote-subset-owner.js +20 -0
- package/dist/esm/remote-subset-owner.js.map +1 -0
- package/dist/esm/remote-subset-wire.d.ts +59 -0
- package/dist/esm/remote-subset-wire.js +681 -0
- package/dist/esm/remote-subset-wire.js.map +1 -0
- package/dist/esm/sqlite-core-adapter.d.ts +45 -1
- package/dist/esm/sqlite-core-adapter.js +955 -350
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/dist/esm/sqlite-value.d.ts +10 -0
- package/dist/esm/sqlite-value.js +31 -0
- package/dist/esm/sqlite-value.js.map +1 -0
- package/package.json +12 -2
- package/src/broadcast-coordinator.ts +2456 -0
- package/src/errors.ts +120 -0
- package/src/index.ts +2 -0
- package/src/persisted.ts +2617 -442
- package/src/remote-subset-owner.ts +36 -0
- package/src/remote-subset-wire.ts +946 -0
- package/src/sqlite-core-adapter.ts +1297 -410
- package/src/sqlite-value.ts +30 -0
package/src/persisted.ts
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import {
|
|
2
|
+
NoPendingSyncTransactionCommitError,
|
|
3
|
+
NoPendingSyncTransactionWriteError,
|
|
4
|
+
PERSISTED_READINESS,
|
|
5
|
+
SYNC_PERSISTENCE_PROTOCOL,
|
|
6
|
+
SYNC_PERSISTENCE_VERSION,
|
|
2
7
|
SyncTransactionAbortedError,
|
|
3
8
|
compileSingleRowExpression,
|
|
9
|
+
getLoadSubsetDemandKey,
|
|
4
10
|
safeRandomUUID,
|
|
5
11
|
toBooleanPredicate,
|
|
6
12
|
withCollectionConfigFactory,
|
|
7
13
|
} from '@tanstack/db'
|
|
8
14
|
import {
|
|
15
|
+
DuplicateRemoteSubsetOwnerError,
|
|
9
16
|
InvalidPersistedCollectionConfigError,
|
|
10
17
|
InvalidPersistedCollectionCoordinatorError,
|
|
11
18
|
InvalidPersistedStorageKeyEncodingError,
|
|
12
19
|
InvalidPersistedStorageKeyError,
|
|
13
20
|
InvalidPersistenceAdapterError,
|
|
14
21
|
InvalidSyncConfigError,
|
|
22
|
+
PersistedCollectionDurabilityError,
|
|
23
|
+
toPersistedCollectionDurabilityError,
|
|
15
24
|
} from './errors'
|
|
25
|
+
import { serializeSQLiteBigInt } from './sqlite-value'
|
|
26
|
+
import {
|
|
27
|
+
toProcessLocalLoadSubsetOptions,
|
|
28
|
+
toTransportedLoadSubsetOptions,
|
|
29
|
+
} from './remote-subset-wire'
|
|
30
|
+
import {
|
|
31
|
+
reportRemoteSubsetOwnerError,
|
|
32
|
+
unloadRemoteSubsetOwner,
|
|
33
|
+
} from './remote-subset-owner'
|
|
34
|
+
import type { TransportedLoadSubsetOptions } from './remote-subset-wire'
|
|
35
|
+
import type { RemoteSubsetOwner } from './remote-subset-owner'
|
|
16
36
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
17
37
|
import type {
|
|
18
38
|
ChangeMessageOrDeleteKeyMessage,
|
|
@@ -20,14 +40,20 @@ import type {
|
|
|
20
40
|
CollectionConfig,
|
|
21
41
|
CollectionIndexMetadata,
|
|
22
42
|
DeleteMutationFnParams,
|
|
43
|
+
InferSchemaOutput,
|
|
23
44
|
InsertMutationFnParams,
|
|
24
45
|
LoadSubsetFn,
|
|
25
46
|
LoadSubsetOptions,
|
|
26
47
|
PendingMutation,
|
|
48
|
+
PersistedReadinessSnapshot,
|
|
49
|
+
PersistedReadinessSource,
|
|
27
50
|
SyncAppliedReceipt,
|
|
28
51
|
SyncConfig,
|
|
29
52
|
SyncConfigRes,
|
|
30
53
|
SyncMetadataApi,
|
|
54
|
+
SyncPersistenceCapabilityV1,
|
|
55
|
+
SyncPersistenceKeySetEvidence,
|
|
56
|
+
SyncPersistenceScanOptions,
|
|
31
57
|
UpdateMutationFnParams,
|
|
32
58
|
UtilsRecord,
|
|
33
59
|
} from '@tanstack/db'
|
|
@@ -38,12 +64,20 @@ export type PersistedMutationEnvelope =
|
|
|
38
64
|
type: `insert`
|
|
39
65
|
key: string | number
|
|
40
66
|
value: Record<string, unknown>
|
|
67
|
+
/** Persisted row metadata, not optimistic-transaction metadata. */
|
|
68
|
+
metadata?: unknown
|
|
69
|
+
/** Whether this envelope replaces or deletes the persisted row metadata. */
|
|
70
|
+
metadataChanged?: boolean
|
|
41
71
|
}
|
|
42
72
|
| {
|
|
43
73
|
mutationId: string
|
|
44
74
|
type: `update`
|
|
45
75
|
key: string | number
|
|
46
76
|
value: Record<string, unknown>
|
|
77
|
+
/** Persisted row metadata, not optimistic-transaction metadata. */
|
|
78
|
+
metadata?: unknown
|
|
79
|
+
/** Whether this envelope replaces or deletes the persisted row metadata. */
|
|
80
|
+
metadataChanged?: boolean
|
|
47
81
|
}
|
|
48
82
|
| {
|
|
49
83
|
mutationId: string
|
|
@@ -96,7 +130,8 @@ export type TxCommitted = {
|
|
|
96
130
|
export type EnsureRemoteSubsetRequest = {
|
|
97
131
|
type: `rpc:ensureRemoteSubset:req`
|
|
98
132
|
rpcId: string
|
|
99
|
-
|
|
133
|
+
acquisitionId: string
|
|
134
|
+
options: TransportedLoadSubsetOptions
|
|
100
135
|
}
|
|
101
136
|
|
|
102
137
|
export type EnsureRemoteSubsetResponse =
|
|
@@ -104,12 +139,33 @@ export type EnsureRemoteSubsetResponse =
|
|
|
104
139
|
type: `rpc:ensureRemoteSubset:res`
|
|
105
140
|
rpcId: string
|
|
106
141
|
ok: true
|
|
142
|
+
leaderId: string
|
|
107
143
|
}
|
|
108
144
|
| {
|
|
109
145
|
type: `rpc:ensureRemoteSubset:res`
|
|
110
146
|
rpcId: string
|
|
111
147
|
ok: false
|
|
112
148
|
error: string
|
|
149
|
+
retryable?: true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type ReleaseRemoteSubsetRequest = {
|
|
153
|
+
type: `rpc:releaseRemoteSubset:req`
|
|
154
|
+
rpcId: string
|
|
155
|
+
acquisitionId: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type ReleaseRemoteSubsetResponse =
|
|
159
|
+
| {
|
|
160
|
+
type: `rpc:releaseRemoteSubset:res`
|
|
161
|
+
rpcId: string
|
|
162
|
+
ok: true
|
|
163
|
+
}
|
|
164
|
+
| {
|
|
165
|
+
type: `rpc:releaseRemoteSubset:res`
|
|
166
|
+
rpcId: string
|
|
167
|
+
ok: false
|
|
168
|
+
error: string
|
|
113
169
|
}
|
|
114
170
|
|
|
115
171
|
export type ApplyLocalMutationsRequest = {
|
|
@@ -136,6 +192,48 @@ export type ApplyLocalMutationsResponse =
|
|
|
136
192
|
code: `NOT_LEADER` | `VALIDATION_ERROR` | `CONFLICT` | `TIMEOUT`
|
|
137
193
|
error: string
|
|
138
194
|
}
|
|
195
|
+
| {
|
|
196
|
+
type: `rpc:applyLocalMutations:res`
|
|
197
|
+
rpcId: string
|
|
198
|
+
ok: false
|
|
199
|
+
code: `PERSISTENCE_ERROR`
|
|
200
|
+
error: string
|
|
201
|
+
sourceCode?: string | number
|
|
202
|
+
path?: string | ReadonlyArray<string | number>
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type ApplyCommittedTxRequest = {
|
|
206
|
+
type: `rpc:applyCommittedTx:req`
|
|
207
|
+
rpcId: string
|
|
208
|
+
envelopeId: string
|
|
209
|
+
tx: PersistedTx
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type ApplyCommittedTxResponse =
|
|
213
|
+
| {
|
|
214
|
+
type: `rpc:applyCommittedTx:res`
|
|
215
|
+
rpcId: string
|
|
216
|
+
ok: true
|
|
217
|
+
term: number
|
|
218
|
+
seq: number
|
|
219
|
+
latestRowVersion: number
|
|
220
|
+
}
|
|
221
|
+
| {
|
|
222
|
+
type: `rpc:applyCommittedTx:res`
|
|
223
|
+
rpcId: string
|
|
224
|
+
ok: false
|
|
225
|
+
code: `NOT_LEADER` | `CONFLICT` | `TIMEOUT`
|
|
226
|
+
error: string
|
|
227
|
+
}
|
|
228
|
+
| {
|
|
229
|
+
type: `rpc:applyCommittedTx:res`
|
|
230
|
+
rpcId: string
|
|
231
|
+
ok: false
|
|
232
|
+
code: `PERSISTENCE_ERROR`
|
|
233
|
+
error: string
|
|
234
|
+
sourceCode?: string | number
|
|
235
|
+
path?: string | ReadonlyArray<string | number>
|
|
236
|
+
}
|
|
139
237
|
|
|
140
238
|
export type PullSinceRequest = {
|
|
141
239
|
type: `rpc:pullSince:req`
|
|
@@ -215,10 +313,32 @@ export type PersistedScannedRow<
|
|
|
215
313
|
metadata?: unknown
|
|
216
314
|
}
|
|
217
315
|
|
|
218
|
-
export type PersistedRowScanOptions =
|
|
219
|
-
|
|
316
|
+
export type PersistedRowScanOptions = SyncPersistenceScanOptions
|
|
317
|
+
|
|
318
|
+
export type PersistedKeySetEvidence = SyncPersistenceKeySetEvidence
|
|
319
|
+
|
|
320
|
+
type PersistedResumeGeneration = {
|
|
321
|
+
latestTerm: number
|
|
322
|
+
latestSeq: number
|
|
323
|
+
latestRowVersion: number
|
|
324
|
+
resetEpoch: number
|
|
220
325
|
}
|
|
221
326
|
|
|
327
|
+
export type PersistencePullSinceResult =
|
|
328
|
+
| {
|
|
329
|
+
latestRowVersion: number
|
|
330
|
+
requiresFullReload: true
|
|
331
|
+
}
|
|
332
|
+
| {
|
|
333
|
+
latestRowVersion: number
|
|
334
|
+
requiresFullReload: false
|
|
335
|
+
changedKeys: Array<string | number>
|
|
336
|
+
deletedKeys: Array<string | number>
|
|
337
|
+
deltas?: Array<
|
|
338
|
+
ReplayableTxDelta<Record<string, unknown>, string | number>
|
|
339
|
+
>
|
|
340
|
+
}
|
|
341
|
+
|
|
222
342
|
export type PersistedTx<
|
|
223
343
|
T extends object = Record<string, unknown>,
|
|
224
344
|
TKey extends string | number = string | number,
|
|
@@ -249,6 +369,21 @@ export type PersistedTx<
|
|
|
249
369
|
collectionMetadataMutations?: Array<PersistedCollectionMetadataMutation>
|
|
250
370
|
}
|
|
251
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Opaque identity shared by every adapter over the same physical SQLite driver.
|
|
374
|
+
* The core adapter uses it to serialize non-preemptible logical operations
|
|
375
|
+
* while alternating one regular operation between queued hydrations.
|
|
376
|
+
*
|
|
377
|
+
* Delegating drivers must forward this property before they are passed to a
|
|
378
|
+
* SQLite persistence adapter. A driver may also brand each returned Promise
|
|
379
|
+
* with the same key for late capability discovery, but a wrapper must return
|
|
380
|
+
* that exact Promise: discovering the key after a call cannot retroactively
|
|
381
|
+
* schedule the wrapper's first logical operation.
|
|
382
|
+
*/
|
|
383
|
+
export const SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY = Symbol.for(
|
|
384
|
+
`tanstack-db.sqlite-driver-supports-shared-logical-scheduling`,
|
|
385
|
+
)
|
|
386
|
+
|
|
252
387
|
export interface PersistenceAdapter {
|
|
253
388
|
loadSubset: (
|
|
254
389
|
collectionId: string,
|
|
@@ -261,6 +396,25 @@ export interface PersistenceAdapter {
|
|
|
261
396
|
metadata?: unknown
|
|
262
397
|
}>
|
|
263
398
|
>
|
|
399
|
+
loadResumeSnapshot: (
|
|
400
|
+
collectionId: string,
|
|
401
|
+
ctx?: {
|
|
402
|
+
requiredIndexSignatures?: ReadonlyArray<string>
|
|
403
|
+
includeRows?: boolean
|
|
404
|
+
},
|
|
405
|
+
) => Promise<{
|
|
406
|
+
rows: Array<{
|
|
407
|
+
key: string | number
|
|
408
|
+
value: Record<string, unknown>
|
|
409
|
+
metadata?: unknown
|
|
410
|
+
}>
|
|
411
|
+
keySet?: PersistedKeySetEvidence
|
|
412
|
+
collectionMetadata: Array<{ key: string; value: unknown }>
|
|
413
|
+
latestTerm: number
|
|
414
|
+
latestSeq: number
|
|
415
|
+
latestRowVersion: number
|
|
416
|
+
resetEpoch: number
|
|
417
|
+
}>
|
|
264
418
|
applyCommittedTx: (collectionId: string, tx: PersistedTx) => Promise<void>
|
|
265
419
|
loadCollectionMetadata?: (
|
|
266
420
|
collectionId: string,
|
|
@@ -280,9 +434,41 @@ export interface PersistenceAdapter {
|
|
|
280
434
|
latestSeq: number
|
|
281
435
|
latestRowVersion: number
|
|
282
436
|
}>
|
|
437
|
+
/**
|
|
438
|
+
* Runs one complete logical hydrate as a non-preemptible scheduler unit.
|
|
439
|
+
* The callback must use the supplied unscheduled adapter for all nested
|
|
440
|
+
* persistence work and must not retain it after the callback settles.
|
|
441
|
+
*/
|
|
442
|
+
runInHydrationScope?: <T>(
|
|
443
|
+
task: (adapter: HydrationPersistenceAdapter) => Promise<T>,
|
|
444
|
+
) => Promise<T>
|
|
445
|
+
/** Schedule one regular operation before it acquires external writer locks. */
|
|
446
|
+
runInRegularScope?: <T>(
|
|
447
|
+
task: (adapter: HydrationPersistenceAdapter) => Promise<T>,
|
|
448
|
+
) => Promise<T>
|
|
449
|
+
/** Whether hydration scopes currently enter a shared driver scheduler. */
|
|
450
|
+
isHydrationScopeScheduled?: () => boolean
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export type HydrationPersistenceAdapter = PersistenceAdapter & {
|
|
454
|
+
pullSince?: (
|
|
455
|
+
collectionId: string,
|
|
456
|
+
fromRowVersion: number,
|
|
457
|
+
) => Promise<PersistencePullSinceResult>
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export type { RemoteSubsetOwner } from './remote-subset-owner'
|
|
461
|
+
|
|
462
|
+
type SingleProcessRemoteSubsetAcquisition = {
|
|
463
|
+
owner: RemoteSubsetOwner
|
|
464
|
+
options: TransportedLoadSubsetOptions
|
|
465
|
+
load: Promise<void>
|
|
283
466
|
}
|
|
284
467
|
|
|
285
468
|
export interface SQLiteDriver {
|
|
469
|
+
readonly [SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY]?: object
|
|
470
|
+
/** Host-specific limit on bound parameters in one query, when lower than SQLite's. */
|
|
471
|
+
readonly maxBoundParameters?: number
|
|
286
472
|
exec: (sql: string) => Promise<void>
|
|
287
473
|
query: <T>(
|
|
288
474
|
sql: string,
|
|
@@ -297,8 +483,30 @@ export interface SQLiteDriver {
|
|
|
297
483
|
) => Promise<T>
|
|
298
484
|
}
|
|
299
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Forwards a driver's shared logical scheduling identity to a transparent
|
|
488
|
+
* delegating driver before the wrapper is used by a persistence adapter.
|
|
489
|
+
*/
|
|
490
|
+
export function forwardSQLiteDriverSharedLogicalScheduling<
|
|
491
|
+
TDriver extends SQLiteDriver,
|
|
492
|
+
>(source: SQLiteDriver, wrapper: TDriver): TDriver {
|
|
493
|
+
const key = source[SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY]
|
|
494
|
+
if (key) {
|
|
495
|
+
Object.defineProperty(
|
|
496
|
+
wrapper,
|
|
497
|
+
SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
|
|
498
|
+
{ value: key },
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
return wrapper
|
|
502
|
+
}
|
|
503
|
+
|
|
300
504
|
export interface PersistedCollectionCoordinator {
|
|
301
505
|
getNodeId: () => string
|
|
506
|
+
setAdapterForCollection?: (
|
|
507
|
+
collectionId: string,
|
|
508
|
+
adapter: PersistenceAdapter,
|
|
509
|
+
) => void
|
|
302
510
|
subscribe: (
|
|
303
511
|
collectionId: string,
|
|
304
512
|
onMessage: (message: ProtocolEnvelope<unknown>) => void,
|
|
@@ -306,22 +514,44 @@ export interface PersistedCollectionCoordinator {
|
|
|
306
514
|
publish: (collectionId: string, message: ProtocolEnvelope<unknown>) => void
|
|
307
515
|
isLeader: (collectionId: string) => boolean
|
|
308
516
|
ensureLeadership: (collectionId: string) => Promise<void>
|
|
309
|
-
requestEnsureRemoteSubset
|
|
517
|
+
requestEnsureRemoteSubset: (
|
|
518
|
+
collectionId: string,
|
|
519
|
+
options: LoadSubsetOptions,
|
|
520
|
+
) => Promise<void>
|
|
521
|
+
requestReleaseRemoteSubset: (
|
|
310
522
|
collectionId: string,
|
|
311
523
|
options: LoadSubsetOptions,
|
|
312
524
|
) => Promise<void>
|
|
525
|
+
registerRemoteSubsetOwner: (
|
|
526
|
+
collectionId: string,
|
|
527
|
+
owner: RemoteSubsetOwner,
|
|
528
|
+
) => () => void
|
|
529
|
+
/**
|
|
530
|
+
* Requests leader-side index creation. The scoped adapter is leader-local
|
|
531
|
+
* and is never serialized to a follower. `localEnsureCompleted` lets the
|
|
532
|
+
* built-in leader avoid repeating successful local work.
|
|
533
|
+
*/
|
|
313
534
|
requestEnsurePersistedIndex: (
|
|
314
535
|
collectionId: string,
|
|
315
536
|
signature: string,
|
|
316
537
|
spec: PersistedIndexSpec,
|
|
538
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
539
|
+
localEnsureCompleted?: boolean,
|
|
317
540
|
) => Promise<void>
|
|
318
541
|
requestApplyLocalMutations?: (
|
|
319
542
|
collectionId: string,
|
|
320
543
|
mutations: Array<PersistedMutationEnvelope>,
|
|
321
544
|
) => Promise<ApplyLocalMutationsResponse>
|
|
545
|
+
requestApplyCommittedTx: (
|
|
546
|
+
collectionId: string,
|
|
547
|
+
tx: PersistedTx,
|
|
548
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
549
|
+
) => Promise<ApplyCommittedTxResponse>
|
|
550
|
+
/** The scoped adapter is leader-local and is never serialized to a follower. */
|
|
322
551
|
pullSince?: (
|
|
323
552
|
collectionId: string,
|
|
324
553
|
fromRowVersion: number,
|
|
554
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
325
555
|
) => Promise<PullSinceResponse>
|
|
326
556
|
}
|
|
327
557
|
|
|
@@ -356,6 +586,74 @@ export interface PersistedCollectionUtils extends UtilsRecord {
|
|
|
356
586
|
forceReloadSubset?: (options: LoadSubsetOptions) => Promise<void> | void
|
|
357
587
|
}
|
|
358
588
|
|
|
589
|
+
class PersistedReadinessTracker implements PersistedReadinessSource {
|
|
590
|
+
private snapshot: PersistedReadinessSnapshot = { status: `loading` }
|
|
591
|
+
private networkDeadlineAt: number | undefined
|
|
592
|
+
private readonly listeners = new Set<() => void>()
|
|
593
|
+
|
|
594
|
+
constructor(readonly networkTimeoutMs: number) {}
|
|
595
|
+
|
|
596
|
+
getSnapshot = (): PersistedReadinessSnapshot => this.snapshot
|
|
597
|
+
|
|
598
|
+
getOrStartNetworkDeadline = (): number => {
|
|
599
|
+
return (this.networkDeadlineAt ??= Date.now() + this.networkTimeoutMs)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
603
|
+
this.listeners.add(listener)
|
|
604
|
+
return () => this.listeners.delete(listener)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
set(snapshot: PersistedReadinessSnapshot): void {
|
|
608
|
+
const deadlineRestarted =
|
|
609
|
+
snapshot.status === `loading` && this.networkDeadlineAt !== undefined
|
|
610
|
+
if (deadlineRestarted) {
|
|
611
|
+
this.networkDeadlineAt = Date.now() + this.networkTimeoutMs
|
|
612
|
+
}
|
|
613
|
+
if (
|
|
614
|
+
this.snapshot.status === snapshot.status &&
|
|
615
|
+
this.snapshot.error === snapshot.error &&
|
|
616
|
+
!deadlineRestarted
|
|
617
|
+
) {
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
this.snapshot = snapshot
|
|
621
|
+
for (const listener of this.listeners) listener()
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export type PersistedInitialRenderOptions = {
|
|
626
|
+
strategy: `network-first`
|
|
627
|
+
/** Time to prefer network before restored rows may render; defaults to 3s. */
|
|
628
|
+
networkTimeoutMs?: number
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const DEFAULT_NETWORK_TIMEOUT_MS = 3_000
|
|
632
|
+
const MAX_TIMER_TIMEOUT_MS = 2_147_483_647
|
|
633
|
+
|
|
634
|
+
function getNetworkTimeoutMs(
|
|
635
|
+
initialRender: PersistedInitialRenderOptions | undefined,
|
|
636
|
+
): number | undefined {
|
|
637
|
+
if (initialRender === undefined) return undefined
|
|
638
|
+
const strategy = (initialRender as { strategy: unknown }).strategy
|
|
639
|
+
if (strategy !== `network-first`) {
|
|
640
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
641
|
+
`initialRender.strategy must be "network-first"`,
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
const timeoutMs = initialRender.networkTimeoutMs ?? DEFAULT_NETWORK_TIMEOUT_MS
|
|
645
|
+
if (
|
|
646
|
+
!Number.isFinite(timeoutMs) ||
|
|
647
|
+
timeoutMs < 0 ||
|
|
648
|
+
timeoutMs > MAX_TIMER_TIMEOUT_MS
|
|
649
|
+
) {
|
|
650
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
651
|
+
`initialRender.networkTimeoutMs must be between 0 and ${MAX_TIMER_TIMEOUT_MS}`,
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
return timeoutMs
|
|
655
|
+
}
|
|
656
|
+
|
|
359
657
|
export type PersistedSyncWrappedOptions<
|
|
360
658
|
T extends object,
|
|
361
659
|
TKey extends string | number,
|
|
@@ -365,6 +663,8 @@ export type PersistedSyncWrappedOptions<
|
|
|
365
663
|
sync: SyncConfig<T, TKey>
|
|
366
664
|
persistence: PersistedCollectionPersistence
|
|
367
665
|
schemaVersion?: number
|
|
666
|
+
/** Opt into network-first rendering with a persisted fallback. */
|
|
667
|
+
initialRender?: PersistedInitialRenderOptions
|
|
368
668
|
}
|
|
369
669
|
|
|
370
670
|
export type PersistedLocalOnlyOptions<
|
|
@@ -375,6 +675,8 @@ export type PersistedLocalOnlyOptions<
|
|
|
375
675
|
> = Omit<CollectionConfig<T, TKey, TSchema, TUtils>, `sync`> & {
|
|
376
676
|
persistence: PersistedCollectionPersistence
|
|
377
677
|
schemaVersion?: number
|
|
678
|
+
/** Opt into network-first rendering with a persisted fallback. */
|
|
679
|
+
initialRender?: PersistedInitialRenderOptions
|
|
378
680
|
}
|
|
379
681
|
|
|
380
682
|
type PersistedSyncOptionsResult<
|
|
@@ -405,7 +707,11 @@ const REQUIRED_COORDINATOR_METHODS: ReadonlyArray<
|
|
|
405
707
|
| `publish`
|
|
406
708
|
| `isLeader`
|
|
407
709
|
| `ensureLeadership`
|
|
710
|
+
| `requestEnsureRemoteSubset`
|
|
711
|
+
| `requestReleaseRemoteSubset`
|
|
712
|
+
| `registerRemoteSubsetOwner`
|
|
408
713
|
| `requestEnsurePersistedIndex`
|
|
714
|
+
| `requestApplyCommittedTx`
|
|
409
715
|
>
|
|
410
716
|
> = [
|
|
411
717
|
`getNodeId`,
|
|
@@ -413,15 +719,19 @@ const REQUIRED_COORDINATOR_METHODS: ReadonlyArray<
|
|
|
413
719
|
`publish`,
|
|
414
720
|
`isLeader`,
|
|
415
721
|
`ensureLeadership`,
|
|
722
|
+
`requestEnsureRemoteSubset`,
|
|
723
|
+
`requestReleaseRemoteSubset`,
|
|
724
|
+
`registerRemoteSubsetOwner`,
|
|
416
725
|
`requestEnsurePersistedIndex`,
|
|
726
|
+
`requestApplyCommittedTx`,
|
|
417
727
|
]
|
|
418
728
|
|
|
419
729
|
const REQUIRED_ADAPTER_METHODS: ReadonlyArray<
|
|
420
730
|
keyof Pick<
|
|
421
731
|
PersistenceAdapter,
|
|
422
|
-
`loadSubset` | `applyCommittedTx` | `ensureIndex`
|
|
732
|
+
`loadSubset` | `loadResumeSnapshot` | `applyCommittedTx` | `ensureIndex`
|
|
423
733
|
>
|
|
424
|
-
> = [`loadSubset`, `applyCommittedTx`, `ensureIndex`]
|
|
734
|
+
> = [`loadSubset`, `loadResumeSnapshot`, `applyCommittedTx`, `ensureIndex`]
|
|
425
735
|
|
|
426
736
|
const TARGETED_INVALIDATION_KEY_LIMIT = 128
|
|
427
737
|
const DEFAULT_DB_NAME = `tanstack-db`
|
|
@@ -440,6 +750,7 @@ type SyncControlFns<T extends object, TKey extends string | number> = {
|
|
|
440
750
|
commit: ((signal?: AbortSignal) => SyncAppliedReceipt) | null
|
|
441
751
|
truncate: (() => void) | null
|
|
442
752
|
metadata: SyncMetadataApi<TKey> | null
|
|
753
|
+
markError: ((error: unknown) => void) | null
|
|
443
754
|
}
|
|
444
755
|
|
|
445
756
|
/**
|
|
@@ -448,6 +759,12 @@ type SyncControlFns<T extends object, TKey extends string | number> = {
|
|
|
448
759
|
*/
|
|
449
760
|
export class SingleProcessCoordinator implements PersistedCollectionCoordinator {
|
|
450
761
|
private readonly nodeId: string
|
|
762
|
+
private readonly collectionAdapters = new Map<string, PersistenceAdapter>()
|
|
763
|
+
private readonly remoteSubsetOwners = new Map<string, RemoteSubsetOwner>()
|
|
764
|
+
private readonly remoteSubsetAcquisitions = new Map<
|
|
765
|
+
string,
|
|
766
|
+
Map<LoadSubsetOptions, SingleProcessRemoteSubsetAcquisition>
|
|
767
|
+
>()
|
|
451
768
|
|
|
452
769
|
constructor(nodeId: string = safeRandomUUID()) {
|
|
453
770
|
this.nodeId = nodeId
|
|
@@ -469,10 +786,129 @@ export class SingleProcessCoordinator implements PersistedCollectionCoordinator
|
|
|
469
786
|
|
|
470
787
|
public async ensureLeadership(): Promise<void> {}
|
|
471
788
|
|
|
472
|
-
public async requestEnsureRemoteSubset(
|
|
789
|
+
public async requestEnsureRemoteSubset(
|
|
790
|
+
collectionId: string,
|
|
791
|
+
options: LoadSubsetOptions,
|
|
792
|
+
): Promise<void> {
|
|
793
|
+
const transported = toTransportedLoadSubsetOptions(options)
|
|
794
|
+
const localOptions = toProcessLocalLoadSubsetOptions(options, transported)
|
|
795
|
+
const owner = this.remoteSubsetOwners.get(collectionId)
|
|
796
|
+
if (!owner) {
|
|
797
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
798
|
+
`SingleProcessCoordinator has no remote subset owner configured for collection "${collectionId}"`,
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
let acquisitions = this.remoteSubsetAcquisitions.get(collectionId)
|
|
802
|
+
if (!acquisitions) {
|
|
803
|
+
acquisitions = new Map()
|
|
804
|
+
this.remoteSubsetAcquisitions.set(collectionId, acquisitions)
|
|
805
|
+
}
|
|
806
|
+
const existing = acquisitions.get(options)
|
|
807
|
+
if (existing) {
|
|
808
|
+
await existing.load
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
let resolveLoad!: () => void
|
|
813
|
+
let rejectLoad!: (error: unknown) => void
|
|
814
|
+
const load = new Promise<void>((resolve, reject) => {
|
|
815
|
+
resolveLoad = resolve
|
|
816
|
+
rejectLoad = reject
|
|
817
|
+
})
|
|
818
|
+
acquisitions.set(options, { owner, options: localOptions, load })
|
|
819
|
+
try {
|
|
820
|
+
const ownerLoad = owner(localOptions)
|
|
821
|
+
void Promise.resolve(ownerLoad).then(resolveLoad, (error) => {
|
|
822
|
+
reportRemoteSubsetOwnerError(owner, error)
|
|
823
|
+
rejectLoad(error)
|
|
824
|
+
})
|
|
825
|
+
} catch (error) {
|
|
826
|
+
reportRemoteSubsetOwnerError(owner, error)
|
|
827
|
+
rejectLoad(error)
|
|
828
|
+
}
|
|
829
|
+
await load
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
public async requestReleaseRemoteSubset(
|
|
833
|
+
collectionId: string,
|
|
834
|
+
options: LoadSubsetOptions,
|
|
835
|
+
): Promise<void> {
|
|
836
|
+
const acquisitions = this.remoteSubsetAcquisitions.get(collectionId)
|
|
837
|
+
const acquisition = acquisitions?.get(options)
|
|
838
|
+
if (!acquisition) return
|
|
839
|
+
acquisitions!.delete(options)
|
|
840
|
+
if (acquisitions!.size === 0) {
|
|
841
|
+
this.remoteSubsetAcquisitions.delete(collectionId)
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
await acquisition.load
|
|
845
|
+
} catch {
|
|
846
|
+
// Calling the owner transferred the lease even when its load rejected.
|
|
847
|
+
}
|
|
848
|
+
await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
public registerRemoteSubsetOwner(
|
|
852
|
+
collectionId: string,
|
|
853
|
+
owner: RemoteSubsetOwner,
|
|
854
|
+
): () => void {
|
|
855
|
+
if (this.remoteSubsetOwners.has(collectionId)) {
|
|
856
|
+
throw new DuplicateRemoteSubsetOwnerError(collectionId)
|
|
857
|
+
}
|
|
858
|
+
this.remoteSubsetOwners.set(collectionId, owner)
|
|
859
|
+
return () => {
|
|
860
|
+
if (this.remoteSubsetOwners.get(collectionId) !== owner) return
|
|
861
|
+
const acquisitions = this.remoteSubsetAcquisitions.get(collectionId)
|
|
862
|
+
this.remoteSubsetAcquisitions.delete(collectionId)
|
|
863
|
+
this.remoteSubsetOwners.delete(collectionId)
|
|
864
|
+
for (const acquisition of acquisitions?.values() ?? []) {
|
|
865
|
+
void (async () => {
|
|
866
|
+
try {
|
|
867
|
+
await acquisition.load
|
|
868
|
+
} catch {
|
|
869
|
+
// Calling the owner transferred the lease even when its load rejected.
|
|
870
|
+
}
|
|
871
|
+
await unloadRemoteSubsetOwner(owner, acquisition.options)
|
|
872
|
+
})().catch(() => undefined)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
473
876
|
|
|
474
877
|
public async requestEnsurePersistedIndex(): Promise<void> {}
|
|
475
878
|
|
|
879
|
+
public setAdapterForCollection(
|
|
880
|
+
collectionId: string,
|
|
881
|
+
adapter: PersistenceAdapter,
|
|
882
|
+
): void {
|
|
883
|
+
this.collectionAdapters.set(collectionId, adapter)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
public async requestApplyCommittedTx(
|
|
887
|
+
collectionId: string,
|
|
888
|
+
tx: PersistedTx,
|
|
889
|
+
): Promise<ApplyCommittedTxResponse> {
|
|
890
|
+
const adapter = this.collectionAdapters.get(collectionId)
|
|
891
|
+
if (!adapter) {
|
|
892
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
893
|
+
`SingleProcessCoordinator has no persistence adapter configured for collection "${collectionId}"`,
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
try {
|
|
898
|
+
await adapter.applyCommittedTx(collectionId, tx)
|
|
899
|
+
} catch (error) {
|
|
900
|
+
throw toPersistedCollectionDurabilityError(collectionId, error)
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
type: `rpc:applyCommittedTx:res`,
|
|
904
|
+
rpcId: safeRandomUUID(),
|
|
905
|
+
ok: true,
|
|
906
|
+
term: tx.term,
|
|
907
|
+
seq: tx.seq,
|
|
908
|
+
latestRowVersion: tx.rowVersion,
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
476
912
|
public pullSince(): Promise<PullSinceResponse> {
|
|
477
913
|
return Promise.resolve({
|
|
478
914
|
type: `rpc:pullSince:res`,
|
|
@@ -509,10 +945,14 @@ function validatePersistenceAdapter(adapter: PersistenceAdapter): void {
|
|
|
509
945
|
|
|
510
946
|
function resolvePersistence(
|
|
511
947
|
persistence: PersistedCollectionPersistence,
|
|
948
|
+
collectionId: string,
|
|
512
949
|
): PersistedResolvedPersistence {
|
|
513
950
|
validatePersistenceAdapter(persistence.adapter)
|
|
514
951
|
|
|
515
952
|
const coordinator = persistence.coordinator ?? new SingleProcessCoordinator()
|
|
953
|
+
if (coordinator instanceof SingleProcessCoordinator) {
|
|
954
|
+
coordinator.setAdapterForCollection(collectionId, persistence.adapter)
|
|
955
|
+
}
|
|
516
956
|
validatePersistedCollectionCoordinator(coordinator)
|
|
517
957
|
|
|
518
958
|
return {
|
|
@@ -524,9 +964,13 @@ function resolvePersistence(
|
|
|
524
964
|
function resolvePersistenceForMode(
|
|
525
965
|
persistence: PersistedCollectionPersistence,
|
|
526
966
|
mode: PersistedCollectionMode,
|
|
967
|
+
collectionId: string,
|
|
527
968
|
): PersistedResolvedPersistence {
|
|
528
969
|
const modeSpecificPersistence = persistence.resolvePersistenceForMode?.(mode)
|
|
529
|
-
return resolvePersistence(
|
|
970
|
+
return resolvePersistence(
|
|
971
|
+
modeSpecificPersistence ?? persistence,
|
|
972
|
+
collectionId,
|
|
973
|
+
)
|
|
530
974
|
}
|
|
531
975
|
|
|
532
976
|
function resolvePersistenceForCollection(
|
|
@@ -540,10 +984,17 @@ function resolvePersistenceForCollection(
|
|
|
540
984
|
const collectionSpecificPersistence =
|
|
541
985
|
persistence.resolvePersistenceForCollection?.(options)
|
|
542
986
|
if (collectionSpecificPersistence) {
|
|
543
|
-
return resolvePersistence(
|
|
987
|
+
return resolvePersistence(
|
|
988
|
+
collectionSpecificPersistence,
|
|
989
|
+
options.collectionId,
|
|
990
|
+
)
|
|
544
991
|
}
|
|
545
992
|
|
|
546
|
-
return resolvePersistenceForMode(
|
|
993
|
+
return resolvePersistenceForMode(
|
|
994
|
+
persistence,
|
|
995
|
+
options.mode,
|
|
996
|
+
options.collectionId,
|
|
997
|
+
)
|
|
547
998
|
}
|
|
548
999
|
|
|
549
1000
|
function hasOwnSyncKey(options: object): options is { sync: unknown } {
|
|
@@ -580,6 +1031,7 @@ type NormalizedSyncOperation<T extends object, TKey extends string | number> =
|
|
|
580
1031
|
|
|
581
1032
|
type BufferedSyncTransaction<T extends object, TKey extends string | number> = {
|
|
582
1033
|
operations: Array<NormalizedSyncOperation<T, TKey>>
|
|
1034
|
+
partialUpdateOperationIndexes: Set<number>
|
|
583
1035
|
rowMetadataWrites: Map<
|
|
584
1036
|
TKey,
|
|
585
1037
|
{ type: `set`; value: unknown } | { type: `delete` }
|
|
@@ -588,44 +1040,95 @@ type BufferedSyncTransaction<T extends object, TKey extends string | number> = {
|
|
|
588
1040
|
string,
|
|
589
1041
|
{ type: `set`; value: unknown } | { type: `delete` }
|
|
590
1042
|
>
|
|
1043
|
+
deferredHydrationMetadataDeleteKeys: Set<TKey>
|
|
1044
|
+
hydrationContext?: { suppliedRowKeys: Set<TKey> }
|
|
1045
|
+
hydrationSequence?: number
|
|
591
1046
|
truncate: boolean
|
|
592
1047
|
internal: boolean
|
|
1048
|
+
lifecycleGeneration: number
|
|
1049
|
+
beginOptions?: { immediate?: boolean }
|
|
1050
|
+
expectedResumeGenerationOwner?: symbol
|
|
593
1051
|
signal?: AbortSignal
|
|
1052
|
+
prependHydrationRows: (
|
|
1053
|
+
rows: Array<{ key: TKey; value: T; metadata?: unknown }>,
|
|
1054
|
+
) => void
|
|
1055
|
+
applyToCollection: () => SyncAppliedReceipt
|
|
1056
|
+
shouldFailStopOnAbort?: () => boolean
|
|
594
1057
|
resolveApplied?: () => void
|
|
595
1058
|
rejectApplied?: (error: unknown) => void
|
|
596
1059
|
}
|
|
597
1060
|
|
|
598
|
-
type OpenSyncTransaction<
|
|
599
|
-
T
|
|
600
|
-
|
|
601
|
-
>
|
|
1061
|
+
type OpenSyncTransaction<T extends object, TKey extends string | number> = Omit<
|
|
1062
|
+
BufferedSyncTransaction<T, TKey>,
|
|
1063
|
+
`applyToCollection` | `prependHydrationRows` | `shouldFailStopOnAbort`
|
|
1064
|
+
> & {
|
|
1065
|
+
signal?: AbortSignal
|
|
1066
|
+
applicationReceipt?: SyncAppliedReceipt
|
|
1067
|
+
operationKeys: Set<TKey>
|
|
602
1068
|
queuedBecauseHydrating: boolean
|
|
1069
|
+
hasDependentSuccessor: boolean
|
|
1070
|
+
publicationAdmissionWaiters?: Set<{
|
|
1071
|
+
resolve: () => void
|
|
1072
|
+
reject: (error: unknown) => void
|
|
1073
|
+
}>
|
|
1074
|
+
terminalFailure?: { error: unknown }
|
|
603
1075
|
}
|
|
604
1076
|
|
|
605
1077
|
type SyncWriteNormalization<T extends object, TKey extends string | number> = {
|
|
606
|
-
forwardMessage:
|
|
607
|
-
| {
|
|
608
|
-
type: `update`
|
|
609
|
-
value: T
|
|
610
|
-
metadata?: Record<string, unknown>
|
|
611
|
-
}
|
|
612
|
-
| {
|
|
613
|
-
type: `delete`
|
|
614
|
-
key: TKey
|
|
615
|
-
}
|
|
616
1078
|
operation: NormalizedSyncOperation<T, TKey>
|
|
617
1079
|
}
|
|
618
1080
|
|
|
619
1081
|
class ApplyMutex {
|
|
620
1082
|
private queue: Promise<void> = Promise.resolve()
|
|
1083
|
+
private pending = 0
|
|
621
1084
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1085
|
+
reserve(): {
|
|
1086
|
+
run: <T>(task: () => Promise<T> | T) => Promise<T>
|
|
1087
|
+
} {
|
|
1088
|
+
const runImmediately = this.pending === 0
|
|
1089
|
+
const predecessor = this.queue
|
|
1090
|
+
this.pending++
|
|
1091
|
+
|
|
1092
|
+
let releaseReservation!: () => void
|
|
1093
|
+
const reservation = new Promise<void>((resolve) => {
|
|
1094
|
+
releaseReservation = resolve
|
|
1095
|
+
})
|
|
1096
|
+
this.queue = runImmediately
|
|
1097
|
+
? reservation
|
|
1098
|
+
: predecessor.then(() => reservation)
|
|
1099
|
+
|
|
1100
|
+
let used = false
|
|
1101
|
+
return {
|
|
1102
|
+
run: <T>(task: () => Promise<T> | T): Promise<T> => {
|
|
1103
|
+
if (used) {
|
|
1104
|
+
return Promise.reject(
|
|
1105
|
+
new Error(`an apply-mutex reservation can only run once`),
|
|
1106
|
+
)
|
|
1107
|
+
}
|
|
1108
|
+
used = true
|
|
1109
|
+
|
|
1110
|
+
let taskPromise: Promise<T>
|
|
1111
|
+
if (runImmediately) {
|
|
1112
|
+
try {
|
|
1113
|
+
taskPromise = Promise.resolve(task())
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
taskPromise = Promise.reject(error)
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
taskPromise = predecessor.then(() => task())
|
|
1119
|
+
}
|
|
1120
|
+
const markComplete = () => {
|
|
1121
|
+
this.pending--
|
|
1122
|
+
releaseReservation()
|
|
1123
|
+
}
|
|
1124
|
+
void taskPromise.then(markComplete, markComplete)
|
|
1125
|
+
return taskPromise
|
|
1126
|
+
},
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
run<T>(task: () => Promise<T> | T): Promise<T> {
|
|
1131
|
+
return this.reserve().run(task)
|
|
629
1132
|
}
|
|
630
1133
|
}
|
|
631
1134
|
|
|
@@ -640,7 +1143,7 @@ function toStableSerializable(value: unknown): unknown {
|
|
|
640
1143
|
case `boolean`:
|
|
641
1144
|
return value
|
|
642
1145
|
case `bigint`:
|
|
643
|
-
return value
|
|
1146
|
+
return serializeSQLiteBigInt(value)
|
|
644
1147
|
case `function`:
|
|
645
1148
|
case `symbol`:
|
|
646
1149
|
case `undefined`:
|
|
@@ -769,6 +1272,8 @@ function toPersistedMutationEnvelope(
|
|
|
769
1272
|
? (mutation.original as Record<string, unknown>)
|
|
770
1273
|
: mutation.modified
|
|
771
1274
|
|
|
1275
|
+
// PendingMutation.metadata belongs to the optimistic transaction and is
|
|
1276
|
+
// consumed by mutation handlers. It must not overwrite persisted row metadata.
|
|
772
1277
|
return {
|
|
773
1278
|
mutationId: mutation.mutationId,
|
|
774
1279
|
type: mutation.type,
|
|
@@ -783,6 +1288,10 @@ class PersistedCollectionRuntime<
|
|
|
783
1288
|
> {
|
|
784
1289
|
private readonly applyMutex = new ApplyMutex()
|
|
785
1290
|
private readonly activeSubsets = new Map<string, LoadSubsetOptions>()
|
|
1291
|
+
private activeHydrationContext: { suppliedRowKeys: Set<TKey> } | undefined
|
|
1292
|
+
private hydrationSequence = 0
|
|
1293
|
+
// An unscheduled startup read may finish after a coordinator reset publishes.
|
|
1294
|
+
private resetSequence = 0
|
|
786
1295
|
private readonly pendingRemoteSubsetEnsures = new Map<
|
|
787
1296
|
string,
|
|
788
1297
|
LoadSubsetOptions
|
|
@@ -790,8 +1299,12 @@ class PersistedCollectionRuntime<
|
|
|
790
1299
|
private readonly queuedHydrationTransactions: Array<
|
|
791
1300
|
BufferedSyncTransaction<T, TKey>
|
|
792
1301
|
> = []
|
|
793
|
-
private readonly queuedTxCommitted: Array<
|
|
1302
|
+
private readonly queuedTxCommitted: Array<{
|
|
1303
|
+
txCommitted: TxCommitted
|
|
1304
|
+
lifecycleGeneration: number
|
|
1305
|
+
}> = []
|
|
794
1306
|
private readonly requestIds = new WeakMap<LoadSubsetOptions, string>()
|
|
1307
|
+
private readonly hydratedDemands = new Set<string | undefined>()
|
|
795
1308
|
|
|
796
1309
|
private collection: Collection<T, TKey, PersistedCollectionUtils> | null =
|
|
797
1310
|
null
|
|
@@ -801,21 +1314,35 @@ class PersistedCollectionRuntime<
|
|
|
801
1314
|
commit: null,
|
|
802
1315
|
truncate: null,
|
|
803
1316
|
metadata: null,
|
|
1317
|
+
markError: null,
|
|
804
1318
|
}
|
|
805
|
-
private started = false
|
|
806
1319
|
private startupMetadataPromise: Promise<void> | null = null
|
|
807
1320
|
private startPromise: Promise<void> | null = null
|
|
1321
|
+
private hasAttemptedStartup = false
|
|
808
1322
|
private resumeBaselinePromise: Promise<void> | null = null
|
|
1323
|
+
private resumeCertificationPromise: Promise<void> | null = null
|
|
1324
|
+
private persistedKeySetEvidence: PersistedKeySetEvidence | undefined
|
|
1325
|
+
private persistedResumeGeneration: PersistedResumeGeneration | undefined
|
|
1326
|
+
private resumeGenerationOwner = Symbol(`persisted resume generation owner`)
|
|
809
1327
|
private lifecycleGeneration = 0
|
|
810
1328
|
private internalApplyDepth = 0
|
|
1329
|
+
private sourcePublicationWaitDepth = 0
|
|
811
1330
|
private appliedReceiptSequence = 0
|
|
1331
|
+
private syncErrorReported = false
|
|
1332
|
+
private reportedSyncError: unknown
|
|
812
1333
|
private readonly pendingAppliedReceipts = new Map<number, Promise<void>>()
|
|
813
1334
|
private hydratingGeneration: number | null = null
|
|
1335
|
+
private terminalFailure:
|
|
1336
|
+
| { lifecycleGeneration: number; error: unknown }
|
|
1337
|
+
| undefined
|
|
814
1338
|
private coordinatorUnsubscribe: (() => void) | null = null
|
|
1339
|
+
private remoteSubsetOwnerUnsubscribe: (() => void) | null = null
|
|
815
1340
|
private indexAddedUnsubscribe: (() => void) | null = null
|
|
816
1341
|
private indexRemovedUnsubscribe: (() => void) | null = null
|
|
817
1342
|
private remoteEnsureRetryTimer: ReturnType<typeof setTimeout> | null = null
|
|
818
1343
|
private nextRequestId = 0
|
|
1344
|
+
private startupSettled = false
|
|
1345
|
+
private sourceTruncateGeneration = 0
|
|
819
1346
|
|
|
820
1347
|
private latestTerm = 0
|
|
821
1348
|
private latestSeq = 0
|
|
@@ -830,10 +1357,13 @@ class PersistedCollectionRuntime<
|
|
|
830
1357
|
private readonly persistence: PersistedResolvedPersistence,
|
|
831
1358
|
private readonly syncMode: `eager` | `on-demand`,
|
|
832
1359
|
private readonly dbName: string,
|
|
1360
|
+
private readonly persistedReadiness?: PersistedReadinessTracker,
|
|
833
1361
|
) {}
|
|
834
1362
|
|
|
835
1363
|
setSyncControls(syncControls: SyncControlFns<T, TKey>): void {
|
|
836
1364
|
this.advanceLifecycle()
|
|
1365
|
+
this.syncErrorReported = false
|
|
1366
|
+
this.reportedSyncError = undefined
|
|
837
1367
|
|
|
838
1368
|
const commit = syncControls.commit
|
|
839
1369
|
this.syncControls = {
|
|
@@ -844,6 +1374,30 @@ class PersistedCollectionRuntime<
|
|
|
844
1374
|
}
|
|
845
1375
|
}
|
|
846
1376
|
|
|
1377
|
+
reportSyncError(error: unknown): unknown {
|
|
1378
|
+
const markError = this.syncControls.markError
|
|
1379
|
+
if (this.syncErrorReported) return this.reportedSyncError
|
|
1380
|
+
if (!markError) return error
|
|
1381
|
+
|
|
1382
|
+
this.syncErrorReported = true
|
|
1383
|
+
this.reportedSyncError = error
|
|
1384
|
+
try {
|
|
1385
|
+
markError(error)
|
|
1386
|
+
} catch {
|
|
1387
|
+
// Reporting must not replace the original asynchronous failure.
|
|
1388
|
+
}
|
|
1389
|
+
return error
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
registerRemoteSubsetOwner(owner: RemoteSubsetOwner): void {
|
|
1393
|
+
this.remoteSubsetOwnerUnsubscribe?.()
|
|
1394
|
+
this.remoteSubsetOwnerUnsubscribe =
|
|
1395
|
+
this.persistence.coordinator.registerRemoteSubsetOwner(
|
|
1396
|
+
this.collectionId,
|
|
1397
|
+
owner,
|
|
1398
|
+
)
|
|
1399
|
+
}
|
|
1400
|
+
|
|
847
1401
|
private trackAppliedReceipt(receipt: SyncAppliedReceipt): SyncAppliedReceipt {
|
|
848
1402
|
const sequence = ++this.appliedReceiptSequence
|
|
849
1403
|
if (receipt === true) {
|
|
@@ -863,13 +1417,14 @@ class PersistedCollectionRuntime<
|
|
|
863
1417
|
)
|
|
864
1418
|
}
|
|
865
1419
|
|
|
866
|
-
clearSyncControls(): void {
|
|
1420
|
+
private clearSyncControls(): void {
|
|
867
1421
|
this.syncControls = {
|
|
868
1422
|
begin: null,
|
|
869
1423
|
write: null,
|
|
870
1424
|
commit: null,
|
|
871
1425
|
truncate: null,
|
|
872
1426
|
metadata: null,
|
|
1427
|
+
markError: null,
|
|
873
1428
|
}
|
|
874
1429
|
}
|
|
875
1430
|
|
|
@@ -877,6 +1432,62 @@ class PersistedCollectionRuntime<
|
|
|
877
1432
|
return this.hydratingGeneration === this.lifecycleGeneration
|
|
878
1433
|
}
|
|
879
1434
|
|
|
1435
|
+
getActiveHydrationContext(): { suppliedRowKeys: Set<TKey> } | undefined {
|
|
1436
|
+
return this.activeHydrationContext
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
getHydrationSequence(): number {
|
|
1440
|
+
return this.hydrationSequence
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
getCurrentTerminalFailure(): { error: unknown } | undefined {
|
|
1444
|
+
const failure = this.terminalFailure
|
|
1445
|
+
return failure?.lifecycleGeneration === this.lifecycleGeneration
|
|
1446
|
+
? failure
|
|
1447
|
+
: undefined
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
getLifecycleGeneration(): number {
|
|
1451
|
+
return this.lifecycleGeneration
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private throwIfLifecycleReplaced(lifecycleGeneration: number): void {
|
|
1455
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) {
|
|
1456
|
+
throw new SyncTransactionAbortedError()
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
private throwIfTerminal(): void {
|
|
1461
|
+
const failure = this.getCurrentTerminalFailure()
|
|
1462
|
+
if (failure) throw failure.error
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private markTerminalFailure(
|
|
1466
|
+
error: unknown,
|
|
1467
|
+
lifecycleGeneration = this.lifecycleGeneration,
|
|
1468
|
+
): unknown {
|
|
1469
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) {
|
|
1470
|
+
return error
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const existing = this.getCurrentTerminalFailure()
|
|
1474
|
+
if (existing) {
|
|
1475
|
+
return existing.error
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
this.terminalFailure = { lifecycleGeneration, error }
|
|
1479
|
+
this.persistedReadiness?.set({ status: `error`, error })
|
|
1480
|
+
this.pendingRemoteSubsetEnsures.clear()
|
|
1481
|
+
this.queuedTxCommitted.length = 0
|
|
1482
|
+
if (this.remoteEnsureRetryTimer !== null) {
|
|
1483
|
+
clearTimeout(this.remoteEnsureRetryTimer)
|
|
1484
|
+
this.remoteEnsureRetryTimer = null
|
|
1485
|
+
}
|
|
1486
|
+
this.rejectQueuedHydrationTransactions(error)
|
|
1487
|
+
this.reportSyncError(error)
|
|
1488
|
+
return error
|
|
1489
|
+
}
|
|
1490
|
+
|
|
880
1491
|
isApplyingInternally(): boolean {
|
|
881
1492
|
return this.internalApplyDepth > 0
|
|
882
1493
|
}
|
|
@@ -899,17 +1510,137 @@ class PersistedCollectionRuntime<
|
|
|
899
1510
|
}
|
|
900
1511
|
}
|
|
901
1512
|
|
|
1513
|
+
private runInHydrationScope<TResult>(
|
|
1514
|
+
task: (adapter: HydrationPersistenceAdapter) => Promise<TResult>,
|
|
1515
|
+
adapter: HydrationPersistenceAdapter = this.persistence.adapter,
|
|
1516
|
+
): Promise<TResult> {
|
|
1517
|
+
if (adapter.runInHydrationScope) {
|
|
1518
|
+
return adapter.runInHydrationScope(task)
|
|
1519
|
+
}
|
|
1520
|
+
return Promise.resolve().then(() => task(adapter))
|
|
1521
|
+
}
|
|
1522
|
+
|
|
902
1523
|
async ensureStarted(): Promise<void> {
|
|
1524
|
+
this.throwIfTerminal()
|
|
903
1525
|
if (this.startPromise) {
|
|
904
1526
|
return this.startPromise
|
|
905
1527
|
}
|
|
906
1528
|
|
|
907
1529
|
const lifecycleGeneration = this.lifecycleGeneration
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1530
|
+
const isRestart = this.hasAttemptedStartup
|
|
1531
|
+
this.hasAttemptedStartup = true
|
|
1532
|
+
let resolveStartupMetadata!: () => void
|
|
1533
|
+
let rejectStartupMetadata!: (error: unknown) => void
|
|
1534
|
+
this.startupMetadataPromise = new Promise<void>((resolve, reject) => {
|
|
1535
|
+
resolveStartupMetadata = resolve
|
|
1536
|
+
rejectStartupMetadata = reject
|
|
1537
|
+
})
|
|
1538
|
+
void this.startupMetadataPromise.catch(() => undefined)
|
|
1539
|
+
|
|
1540
|
+
this.startPromise = (async () => {
|
|
1541
|
+
const loadStartupMetadata = async (
|
|
1542
|
+
adapter: HydrationPersistenceAdapter,
|
|
1543
|
+
) => {
|
|
1544
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) {
|
|
1545
|
+
resolveStartupMetadata()
|
|
1546
|
+
return false
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
try {
|
|
1550
|
+
await this.loadStartupMetadataInternal(lifecycleGeneration, adapter)
|
|
1551
|
+
return lifecycleGeneration === this.lifecycleGeneration
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
rejectStartupMetadata(error)
|
|
1554
|
+
throw error
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
let startup:
|
|
1559
|
+
| {
|
|
1560
|
+
appliedCursor: number | undefined
|
|
1561
|
+
indexBootstrapSnapshot: Array<CollectionIndexMetadata>
|
|
1562
|
+
completedLocalIndexSignatures: Set<string>
|
|
1563
|
+
}
|
|
1564
|
+
| undefined
|
|
1565
|
+
const scheduleStartupAsOneHydrate =
|
|
1566
|
+
this.persistence.adapter.runInHydrationScope !== undefined &&
|
|
1567
|
+
(!isRestart ||
|
|
1568
|
+
(this.persistence.adapter.isHydrationScopeScheduled?.() ?? true))
|
|
1569
|
+
if (scheduleStartupAsOneHydrate) {
|
|
1570
|
+
startup = await this.applyMutex.run(async () => {
|
|
1571
|
+
const result = await this.runInHydrationScope(async (adapter) => {
|
|
1572
|
+
if (!(await loadStartupMetadata(adapter))) return undefined
|
|
1573
|
+
return this.startInternal(
|
|
1574
|
+
lifecycleGeneration,
|
|
1575
|
+
adapter,
|
|
1576
|
+
resolveStartupMetadata,
|
|
1577
|
+
)
|
|
1578
|
+
})
|
|
1579
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
1580
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
1581
|
+
}
|
|
1582
|
+
return result
|
|
1583
|
+
})
|
|
1584
|
+
} else {
|
|
1585
|
+
// Preserve the existing unscheduled-adapter lifecycle contract: a
|
|
1586
|
+
// replacement upstream may start while stale hydration is settling.
|
|
1587
|
+
if (await loadStartupMetadata(this.persistence.adapter)) {
|
|
1588
|
+
if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
|
|
1589
|
+
startup = await this.applyMutex.run(async () => {
|
|
1590
|
+
const result = await this.runInHydrationScope((adapter) =>
|
|
1591
|
+
this.startInternal(
|
|
1592
|
+
lifecycleGeneration,
|
|
1593
|
+
adapter,
|
|
1594
|
+
resolveStartupMetadata,
|
|
1595
|
+
),
|
|
1596
|
+
)
|
|
1597
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
1598
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
1599
|
+
}
|
|
1600
|
+
return result
|
|
1601
|
+
})
|
|
1602
|
+
} else {
|
|
1603
|
+
startup = await this.startInternal(
|
|
1604
|
+
lifecycleGeneration,
|
|
1605
|
+
this.persistence.adapter,
|
|
1606
|
+
resolveStartupMetadata,
|
|
1607
|
+
)
|
|
1608
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
1609
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (
|
|
1615
|
+
startup !== undefined &&
|
|
1616
|
+
lifecycleGeneration === this.lifecycleGeneration
|
|
1617
|
+
) {
|
|
1618
|
+
await this.requestCoordinatorPersistedIndexes(
|
|
1619
|
+
startup.indexBootstrapSnapshot,
|
|
1620
|
+
startup.completedLocalIndexSignatures,
|
|
1621
|
+
)
|
|
1622
|
+
if (
|
|
1623
|
+
startup.appliedCursor !== undefined &&
|
|
1624
|
+
lifecycleGeneration === this.lifecycleGeneration
|
|
1625
|
+
) {
|
|
1626
|
+
await this.waitForAppliedReceiptsAfter(startup.appliedCursor)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
1630
|
+
this.startupSettled = true
|
|
1631
|
+
this.persistedReadiness?.set({ status: `ready` })
|
|
1632
|
+
}
|
|
1633
|
+
resolveStartupMetadata()
|
|
1634
|
+
})().catch((error) => {
|
|
1635
|
+
rejectStartupMetadata(error)
|
|
1636
|
+
throw this.markTerminalFailure(error, lifecycleGeneration)
|
|
1637
|
+
})
|
|
1638
|
+
void this.startPromise.catch(() => undefined)
|
|
1639
|
+
return this.startPromise
|
|
1640
|
+
}
|
|
911
1641
|
|
|
912
1642
|
ensureResumeBaselineHydrated(): Promise<void> {
|
|
1643
|
+
this.throwIfTerminal()
|
|
913
1644
|
if (this.resumeBaselinePromise) {
|
|
914
1645
|
return this.resumeBaselinePromise
|
|
915
1646
|
}
|
|
@@ -920,100 +1651,172 @@ class PersistedCollectionRuntime<
|
|
|
920
1651
|
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
921
1652
|
if (this.syncMode !== `on-demand`) return
|
|
922
1653
|
|
|
923
|
-
await this.
|
|
924
|
-
|
|
1654
|
+
const appliedCursor = await this.applyMutex.run(async () => {
|
|
1655
|
+
const result = await this.runInHydrationScope((adapter) =>
|
|
1656
|
+
this.hydrateBaseline(lifecycleGeneration, adapter),
|
|
1657
|
+
)
|
|
1658
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
1659
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
1660
|
+
}
|
|
1661
|
+
return result
|
|
1662
|
+
})
|
|
1663
|
+
if (
|
|
1664
|
+
appliedCursor !== undefined &&
|
|
1665
|
+
lifecycleGeneration === this.lifecycleGeneration
|
|
1666
|
+
) {
|
|
1667
|
+
await this.waitForAppliedReceiptsAfter(appliedCursor)
|
|
1668
|
+
}
|
|
1669
|
+
})().catch((error) => {
|
|
1670
|
+
throw this.markTerminalFailure(error, lifecycleGeneration)
|
|
1671
|
+
})
|
|
925
1672
|
return this.resumeBaselinePromise
|
|
926
1673
|
}
|
|
927
1674
|
|
|
928
|
-
|
|
929
|
-
if (
|
|
1675
|
+
ensureResumeBaselineCertified(): Promise<void> {
|
|
1676
|
+
if (this.resumeCertificationPromise) {
|
|
1677
|
+
return this.resumeCertificationPromise
|
|
1678
|
+
}
|
|
930
1679
|
|
|
1680
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1681
|
+
this.resumeCertificationPromise = (async () => {
|
|
1682
|
+
await this.ensureStarted()
|
|
1683
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1684
|
+
|
|
1685
|
+
const snapshot = await this.persistence.adapter.loadResumeSnapshot(
|
|
1686
|
+
this.collectionId,
|
|
1687
|
+
{
|
|
1688
|
+
requiredIndexSignatures: this.getRequiredIndexSignatures(),
|
|
1689
|
+
includeRows: false,
|
|
1690
|
+
},
|
|
1691
|
+
)
|
|
1692
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1693
|
+
this.bindResumeSnapshotEvidence(snapshot)
|
|
1694
|
+
})()
|
|
1695
|
+
return this.resumeCertificationPromise
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
getKeySetEvidence(): PersistedKeySetEvidence | undefined {
|
|
1699
|
+
return this.persistedKeySetEvidence
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
getResumeGenerationOwner(): symbol {
|
|
1703
|
+
return this.resumeGenerationOwner
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
private async hydrateBaseline(
|
|
1707
|
+
lifecycleGeneration: number,
|
|
1708
|
+
adapter: HydrationPersistenceAdapter,
|
|
1709
|
+
): Promise<number | undefined> {
|
|
1710
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return undefined
|
|
1711
|
+
|
|
1712
|
+
// The baseline shares the unconstrained demand key. Its lease is never
|
|
1713
|
+
// released, and every reload rereads it, so that coverage stays valid.
|
|
931
1714
|
const baseline = {}
|
|
932
1715
|
this.activeSubsets.set(this.getSubsetKey(baseline), baseline)
|
|
933
1716
|
const appliedCursor = this.appliedReceiptSequence
|
|
934
|
-
await this.
|
|
935
|
-
|
|
936
|
-
|
|
1717
|
+
await this.hydrateSubsetUnsafe(
|
|
1718
|
+
baseline,
|
|
1719
|
+
{
|
|
937
1720
|
requestRemoteEnsure: false,
|
|
938
1721
|
lifecycleGeneration,
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1722
|
+
bindKeySetEvidence: true,
|
|
1723
|
+
},
|
|
1724
|
+
adapter,
|
|
1725
|
+
)
|
|
1726
|
+
return lifecycleGeneration === this.lifecycleGeneration
|
|
1727
|
+
? appliedCursor
|
|
1728
|
+
: undefined
|
|
943
1729
|
}
|
|
944
1730
|
|
|
945
1731
|
async ensureStartupMetadataLoaded(): Promise<void> {
|
|
1732
|
+
this.throwIfTerminal()
|
|
946
1733
|
if (this.startupMetadataPromise) {
|
|
947
1734
|
return this.startupMetadataPromise
|
|
948
1735
|
}
|
|
949
1736
|
|
|
950
|
-
|
|
951
|
-
this.startupMetadataPromise
|
|
952
|
-
this.loadStartupMetadataInternal(lifecycleGeneration)
|
|
953
|
-
return this.startupMetadataPromise
|
|
1737
|
+
void this.ensureStarted()
|
|
1738
|
+
return this.startupMetadataPromise!
|
|
954
1739
|
}
|
|
955
1740
|
|
|
956
|
-
private async startInternal(
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1741
|
+
private async startInternal(
|
|
1742
|
+
lifecycleGeneration: number,
|
|
1743
|
+
adapter: HydrationPersistenceAdapter,
|
|
1744
|
+
onStartupMetadataLoaded: () => void,
|
|
1745
|
+
): Promise<
|
|
1746
|
+
| {
|
|
1747
|
+
appliedCursor: number | undefined
|
|
1748
|
+
indexBootstrapSnapshot: Array<CollectionIndexMetadata>
|
|
1749
|
+
completedLocalIndexSignatures: Set<string>
|
|
1750
|
+
}
|
|
1751
|
+
| undefined
|
|
1752
|
+
> {
|
|
1753
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return undefined
|
|
965
1754
|
|
|
966
1755
|
const indexBootstrapSnapshot = this.collection?.getIndexMetadata() ?? []
|
|
967
1756
|
this.attachIndexLifecycleListeners()
|
|
968
|
-
await this.bootstrapPersistedIndexes(
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1757
|
+
const completedLocalIndexSignatures = await this.bootstrapPersistedIndexes(
|
|
1758
|
+
indexBootstrapSnapshot,
|
|
1759
|
+
adapter,
|
|
1760
|
+
)
|
|
1761
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return undefined
|
|
1762
|
+
|
|
1763
|
+
// Let the source run only once the startup hydrate is about to begin.
|
|
1764
|
+
// Its first transaction must bind to that hydrate's sequence, not the
|
|
1765
|
+
// sequence before index bootstrap yielded.
|
|
1766
|
+
onStartupMetadataLoaded()
|
|
1767
|
+
const appliedCursor =
|
|
1768
|
+
this.syncMode !== `on-demand`
|
|
1769
|
+
? await this.hydrateBaseline(lifecycleGeneration, adapter)
|
|
1770
|
+
: undefined
|
|
1771
|
+
return lifecycleGeneration === this.lifecycleGeneration
|
|
1772
|
+
? {
|
|
1773
|
+
appliedCursor,
|
|
1774
|
+
indexBootstrapSnapshot,
|
|
1775
|
+
completedLocalIndexSignatures,
|
|
1776
|
+
}
|
|
1777
|
+
: undefined
|
|
974
1778
|
}
|
|
975
1779
|
|
|
976
1780
|
private async loadStartupMetadataInternal(
|
|
977
1781
|
lifecycleGeneration: number,
|
|
1782
|
+
adapter: HydrationPersistenceAdapter,
|
|
978
1783
|
): Promise<void> {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const position = await this.persistence.adapter.getStreamPosition(
|
|
983
|
-
this.collectionId,
|
|
984
|
-
)
|
|
985
|
-
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
986
|
-
this.observeStreamPosition(
|
|
987
|
-
position.latestTerm,
|
|
988
|
-
position.latestSeq,
|
|
989
|
-
position.latestRowVersion,
|
|
990
|
-
)
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
const collectionMetadata = await this.loadCollectionMetadataSnapshot()
|
|
1784
|
+
const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
|
|
1785
|
+
includeRows: false,
|
|
1786
|
+
})
|
|
994
1787
|
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
995
|
-
this.
|
|
1788
|
+
this.persistedResumeGeneration = this.getResumeSnapshotGeneration(snapshot)
|
|
1789
|
+
this.persistedKeySetEvidence = snapshot.keySet
|
|
1790
|
+
this.observeStreamPosition(
|
|
1791
|
+
snapshot.latestTerm,
|
|
1792
|
+
snapshot.latestSeq,
|
|
1793
|
+
snapshot.latestRowVersion,
|
|
1794
|
+
)
|
|
1795
|
+
const applied = this.replaceCollectionMetadataSnapshot(
|
|
1796
|
+
snapshot.collectionMetadata,
|
|
1797
|
+
)
|
|
1798
|
+
if (applied !== true) await applied
|
|
996
1799
|
}
|
|
997
1800
|
|
|
998
|
-
private async loadCollectionMetadataSnapshot(
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
if (!
|
|
1801
|
+
private async loadCollectionMetadataSnapshot(
|
|
1802
|
+
adapter: HydrationPersistenceAdapter,
|
|
1803
|
+
): Promise<Array<{ key: string; value: unknown }>> {
|
|
1804
|
+
if (!adapter.loadCollectionMetadata) {
|
|
1002
1805
|
return []
|
|
1003
1806
|
}
|
|
1004
1807
|
|
|
1005
|
-
return
|
|
1808
|
+
return adapter.loadCollectionMetadata(this.collectionId)
|
|
1006
1809
|
}
|
|
1007
1810
|
|
|
1008
1811
|
private replaceCollectionMetadataSnapshot(
|
|
1009
1812
|
collectionMetadata: Array<{ key: string; value: unknown }>,
|
|
1010
|
-
):
|
|
1813
|
+
): SyncAppliedReceipt {
|
|
1011
1814
|
if (
|
|
1012
1815
|
!this.syncControls.begin ||
|
|
1013
1816
|
!this.syncControls.commit ||
|
|
1014
1817
|
!this.syncControls.metadata
|
|
1015
1818
|
) {
|
|
1016
|
-
return
|
|
1819
|
+
return true
|
|
1017
1820
|
}
|
|
1018
1821
|
|
|
1019
1822
|
const nextMetadata = new Map(
|
|
@@ -1023,7 +1826,7 @@ class PersistedCollectionRuntime<
|
|
|
1023
1826
|
.list()
|
|
1024
1827
|
.map(({ key }) => key)
|
|
1025
1828
|
|
|
1026
|
-
this.withInternalApply(() => {
|
|
1829
|
+
return this.withInternalApply(() => {
|
|
1027
1830
|
this.syncControls.begin?.({ immediate: true })
|
|
1028
1831
|
|
|
1029
1832
|
currentKeys.forEach((key) => {
|
|
@@ -1036,7 +1839,7 @@ class PersistedCollectionRuntime<
|
|
|
1036
1839
|
this.syncControls.metadata?.collection.set(key, value)
|
|
1037
1840
|
})
|
|
1038
1841
|
|
|
1039
|
-
this.syncControls.commit?.()
|
|
1842
|
+
return this.syncControls.commit?.() ?? true
|
|
1040
1843
|
})
|
|
1041
1844
|
}
|
|
1042
1845
|
|
|
@@ -1044,22 +1847,62 @@ class PersistedCollectionRuntime<
|
|
|
1044
1847
|
options: LoadSubsetOptions,
|
|
1045
1848
|
upstreamLoadSubset?: LoadSubsetFn,
|
|
1046
1849
|
): Promise<void> {
|
|
1850
|
+
this.throwIfTerminal()
|
|
1047
1851
|
const lifecycleGeneration = this.lifecycleGeneration
|
|
1048
|
-
this.
|
|
1049
|
-
|
|
1852
|
+
const subsetKey = this.getSubsetKey(options)
|
|
1853
|
+
const truncateGeneration = this.sourceTruncateGeneration
|
|
1854
|
+
const routeRemoteDemandDuringHydration =
|
|
1855
|
+
this.canRouteRemoteDemandThroughCoordinator()
|
|
1856
|
+
this.activeSubsets.set(subsetKey, options)
|
|
1050
1857
|
const appliedCursor = this.appliedReceiptSequence
|
|
1051
|
-
|
|
1052
|
-
this.
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1858
|
+
try {
|
|
1859
|
+
await this.applyMutex.run(async () => {
|
|
1860
|
+
try {
|
|
1861
|
+
await this.runInHydrationScope((adapter) =>
|
|
1862
|
+
this.hydrateSubsetUnsafe(
|
|
1863
|
+
options,
|
|
1864
|
+
{
|
|
1865
|
+
requestRemoteEnsure:
|
|
1866
|
+
this.mode === `sync-present` &&
|
|
1867
|
+
!routeRemoteDemandDuringHydration,
|
|
1868
|
+
lifecycleGeneration,
|
|
1869
|
+
requestLocalLoadFailure: true,
|
|
1870
|
+
rejectBufferedReplayFailure: true,
|
|
1871
|
+
},
|
|
1872
|
+
adapter,
|
|
1873
|
+
),
|
|
1874
|
+
)
|
|
1875
|
+
} finally {
|
|
1876
|
+
if (
|
|
1877
|
+
lifecycleGeneration === this.lifecycleGeneration &&
|
|
1878
|
+
!this.getCurrentTerminalFailure()
|
|
1879
|
+
) {
|
|
1880
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
})
|
|
1884
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1885
|
+
await this.waitForAppliedReceiptsAfter(appliedCursor)
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
if (this.activeSubsets.get(subsetKey) === options) {
|
|
1888
|
+
this.activeSubsets.delete(subsetKey)
|
|
1889
|
+
}
|
|
1890
|
+
throw error
|
|
1891
|
+
}
|
|
1059
1892
|
|
|
1060
|
-
if (
|
|
1893
|
+
if (
|
|
1894
|
+
options.signal?.aborted ||
|
|
1895
|
+
this.activeSubsets.get(this.getSubsetKey(options)) !== options
|
|
1896
|
+
) {
|
|
1897
|
+
return
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
if (this.canRouteRemoteDemandThroughCoordinator()) {
|
|
1061
1901
|
try {
|
|
1062
|
-
await
|
|
1902
|
+
await this.persistence.coordinator.requestEnsureRemoteSubset(
|
|
1903
|
+
this.collectionId,
|
|
1904
|
+
options,
|
|
1905
|
+
)
|
|
1063
1906
|
} catch (error) {
|
|
1064
1907
|
if (
|
|
1065
1908
|
options.signal?.aborted ||
|
|
@@ -1071,46 +1914,177 @@ class PersistedCollectionRuntime<
|
|
|
1071
1914
|
this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options))
|
|
1072
1915
|
throw error
|
|
1073
1916
|
}
|
|
1074
|
-
console.warn(`Failed to trigger remote subset load:`, error)
|
|
1075
1917
|
this.queueRemoteSubsetEnsure(options)
|
|
1076
|
-
// Hydration remains readable, but it does not satisfy remote demand.
|
|
1077
1918
|
throw error
|
|
1078
1919
|
}
|
|
1920
|
+
return
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
if (
|
|
1924
|
+
truncateGeneration === this.sourceTruncateGeneration &&
|
|
1925
|
+
this.activeSubsets.get(subsetKey) === options
|
|
1926
|
+
) {
|
|
1927
|
+
this.hydratedDemands.add(getLoadSubsetDemandKey(options))
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (upstreamLoadSubset) {
|
|
1931
|
+
try {
|
|
1932
|
+
await upstreamLoadSubset(options)
|
|
1933
|
+
} catch (error) {
|
|
1934
|
+
this.noteUpstreamLoadFailure(options, error)
|
|
1935
|
+
throw error
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
loadHydratedSubset(
|
|
1941
|
+
options: LoadSubsetOptions,
|
|
1942
|
+
upstreamLoadSubset?: LoadSubsetFn,
|
|
1943
|
+
): true | Promise<void> | undefined {
|
|
1944
|
+
const failure = this.getCurrentTerminalFailure()
|
|
1945
|
+
if (failure) return Promise.reject(failure.error)
|
|
1946
|
+
if (
|
|
1947
|
+
!this.startupSettled ||
|
|
1948
|
+
this.isHydratingNow() ||
|
|
1949
|
+
this.canRouteRemoteDemandThroughCoordinator() ||
|
|
1950
|
+
options.refetch === true ||
|
|
1951
|
+
!this.hydratedDemands.has(getLoadSubsetDemandKey(options))
|
|
1952
|
+
) {
|
|
1953
|
+
return
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
this.activeSubsets.set(this.getSubsetKey(options), options)
|
|
1957
|
+
let result: true | Promise<void>
|
|
1958
|
+
try {
|
|
1959
|
+
result = upstreamLoadSubset?.(options) ?? true
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
this.noteUpstreamLoadFailure(options, error)
|
|
1962
|
+
return Promise.reject(error)
|
|
1079
1963
|
}
|
|
1964
|
+
if (result === true) {
|
|
1965
|
+
return true
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
return result.then(
|
|
1969
|
+
() => {
|
|
1970
|
+
this.queueRemoteSubsetEnsure(options)
|
|
1971
|
+
},
|
|
1972
|
+
(error: unknown) => {
|
|
1973
|
+
this.noteUpstreamLoadFailure(options, error)
|
|
1974
|
+
throw error
|
|
1975
|
+
},
|
|
1976
|
+
)
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private noteUpstreamLoadFailure(
|
|
1980
|
+
options: LoadSubsetOptions,
|
|
1981
|
+
error: unknown,
|
|
1982
|
+
): void {
|
|
1983
|
+
if (
|
|
1984
|
+
options.signal?.aborted ||
|
|
1985
|
+
(typeof error === `object` &&
|
|
1986
|
+
error !== null &&
|
|
1987
|
+
`name` in error &&
|
|
1988
|
+
error.name === `AbortError`)
|
|
1989
|
+
) {
|
|
1990
|
+
this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options))
|
|
1991
|
+
return
|
|
1992
|
+
}
|
|
1993
|
+
console.warn(`Failed to trigger remote subset load:`, error)
|
|
1994
|
+
this.queueRemoteSubsetEnsure(options)
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
noteSourceTruncate(): void {
|
|
1998
|
+
this.sourceTruncateGeneration++
|
|
1999
|
+
this.hydratedDemands.clear()
|
|
1080
2000
|
}
|
|
1081
2001
|
|
|
1082
2002
|
unloadSubset(
|
|
1083
2003
|
options: LoadSubsetOptions,
|
|
1084
2004
|
upstreamUnloadSubset?: (options: LoadSubsetOptions) => void,
|
|
1085
2005
|
): void {
|
|
1086
|
-
this.
|
|
1087
|
-
this.
|
|
1088
|
-
|
|
2006
|
+
const subsetKey = this.getSubsetKey(options)
|
|
2007
|
+
this.activeSubsets.delete(subsetKey)
|
|
2008
|
+
this.pendingRemoteSubsetEnsures.delete(subsetKey)
|
|
2009
|
+
const demandKey = getLoadSubsetDemandKey(options)
|
|
2010
|
+
const stillActive = Array.from(this.activeSubsets.values()).some(
|
|
2011
|
+
(active) => getLoadSubsetDemandKey(active) === demandKey,
|
|
2012
|
+
)
|
|
2013
|
+
if (!stillActive) this.hydratedDemands.delete(demandKey)
|
|
2014
|
+
if (this.mode === `sync-present`) {
|
|
2015
|
+
void this.persistence.coordinator
|
|
2016
|
+
.requestReleaseRemoteSubset(this.collectionId, options)
|
|
2017
|
+
.catch((error) => {
|
|
2018
|
+
this.reportSyncError(error)
|
|
2019
|
+
})
|
|
2020
|
+
}
|
|
2021
|
+
if (upstreamUnloadSubset) {
|
|
2022
|
+
try {
|
|
2023
|
+
const result = (
|
|
2024
|
+
upstreamUnloadSubset as unknown as (
|
|
2025
|
+
options: LoadSubsetOptions,
|
|
2026
|
+
) => unknown
|
|
2027
|
+
)(options)
|
|
2028
|
+
void Promise.resolve(result).catch((error) => {
|
|
2029
|
+
this.reportSyncError(error)
|
|
2030
|
+
})
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
this.reportSyncError(error)
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1089
2035
|
}
|
|
1090
2036
|
|
|
1091
2037
|
async forceReloadSubset(options: LoadSubsetOptions): Promise<void> {
|
|
2038
|
+
this.throwIfTerminal()
|
|
1092
2039
|
const lifecycleGeneration = this.lifecycleGeneration
|
|
1093
2040
|
// A one-shot refresh does not acquire an enduring subscription lease.
|
|
1094
|
-
await this.applyMutex.run(() =>
|
|
1095
|
-
this.
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
2041
|
+
await this.applyMutex.run(async () => {
|
|
2042
|
+
await this.runInHydrationScope((adapter) =>
|
|
2043
|
+
this.hydrateSubsetUnsafe(
|
|
2044
|
+
options,
|
|
2045
|
+
{
|
|
2046
|
+
requestRemoteEnsure: false,
|
|
2047
|
+
lifecycleGeneration,
|
|
2048
|
+
rejectBufferedReplayFailure: true,
|
|
2049
|
+
},
|
|
2050
|
+
adapter,
|
|
2051
|
+
),
|
|
2052
|
+
)
|
|
2053
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
2054
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
2055
|
+
}
|
|
2056
|
+
})
|
|
1100
2057
|
}
|
|
1101
2058
|
|
|
1102
2059
|
queueHydrationBufferedTransaction(
|
|
1103
2060
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
1104
2061
|
): void {
|
|
2062
|
+
const failure = this.getCurrentTerminalFailure()
|
|
2063
|
+
if (failure) {
|
|
2064
|
+
transaction.rejectApplied?.(failure.error)
|
|
2065
|
+
return
|
|
2066
|
+
}
|
|
1105
2067
|
this.queuedHydrationTransactions.push(transaction)
|
|
1106
2068
|
}
|
|
1107
2069
|
|
|
1108
|
-
|
|
2070
|
+
applyHydrationBufferedTransaction(
|
|
1109
2071
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
1110
2072
|
): Promise<void> {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
2073
|
+
const failure = this.getCurrentTerminalFailure()
|
|
2074
|
+
if (failure) return Promise.reject(failure.error)
|
|
2075
|
+
if (
|
|
2076
|
+
transaction.beginOptions?.immediate &&
|
|
2077
|
+
this.sourcePublicationWaitDepth > 0
|
|
2078
|
+
) {
|
|
2079
|
+
return Promise.reject(
|
|
2080
|
+
new InvalidPersistedCollectionConfigError(
|
|
2081
|
+
`immediate persisted source replay cannot enter while an earlier source publication is waiting`,
|
|
2082
|
+
),
|
|
2083
|
+
)
|
|
2084
|
+
}
|
|
2085
|
+
return this.applyMutex.run(async () => {
|
|
2086
|
+
await this.applyBufferedSyncTransactionUnsafe(transaction)
|
|
2087
|
+
})
|
|
1114
2088
|
}
|
|
1115
2089
|
|
|
1116
2090
|
normalizeSyncWriteMessage(
|
|
@@ -1127,10 +2101,6 @@ class PersistedCollectionRuntime<
|
|
|
1127
2101
|
const previousValue = this.collection.get(key) ?? ({} as T)
|
|
1128
2102
|
|
|
1129
2103
|
return {
|
|
1130
|
-
forwardMessage: {
|
|
1131
|
-
type: `delete`,
|
|
1132
|
-
key,
|
|
1133
|
-
},
|
|
1134
2104
|
operation: {
|
|
1135
2105
|
type: `delete`,
|
|
1136
2106
|
key,
|
|
@@ -1146,10 +2116,6 @@ class PersistedCollectionRuntime<
|
|
|
1146
2116
|
const previousValue = this.collection.get(key) ?? message.value
|
|
1147
2117
|
|
|
1148
2118
|
return {
|
|
1149
|
-
forwardMessage: {
|
|
1150
|
-
type: `delete`,
|
|
1151
|
-
key,
|
|
1152
|
-
},
|
|
1153
2119
|
operation: {
|
|
1154
2120
|
type: `delete`,
|
|
1155
2121
|
key,
|
|
@@ -1160,11 +2126,6 @@ class PersistedCollectionRuntime<
|
|
|
1160
2126
|
|
|
1161
2127
|
const key = this.collection.getKeyFromItem(message.value)
|
|
1162
2128
|
return {
|
|
1163
|
-
forwardMessage: {
|
|
1164
|
-
type: `update`,
|
|
1165
|
-
value: message.value,
|
|
1166
|
-
metadata: message.metadata,
|
|
1167
|
-
},
|
|
1168
2129
|
operation: {
|
|
1169
2130
|
type: `update`,
|
|
1170
2131
|
key,
|
|
@@ -1176,32 +2137,55 @@ class PersistedCollectionRuntime<
|
|
|
1176
2137
|
|
|
1177
2138
|
async persistAndConfirmCollectionMutations(
|
|
1178
2139
|
mutations: Array<PendingMutation<T>>,
|
|
2140
|
+
lifecycleGeneration = this.lifecycleGeneration,
|
|
1179
2141
|
): Promise<void> {
|
|
2142
|
+
this.throwIfTerminal()
|
|
2143
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
1180
2144
|
if (mutations.length === 0) {
|
|
1181
2145
|
return
|
|
1182
2146
|
}
|
|
1183
2147
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
2148
|
+
try {
|
|
2149
|
+
await this.applyMutex.run(async () => {
|
|
2150
|
+
// Startup metadata establishes the durable term/sequence boundary. A
|
|
2151
|
+
// mutation admitted before it resolves must wait rather than allocate a
|
|
2152
|
+
// default position that can collide with an already-applied transaction.
|
|
2153
|
+
await this.ensureStartupMetadataLoaded()
|
|
2154
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
2155
|
+
const acceptedMutationIds = await this.persistCollectionMutationsUnsafe(
|
|
2156
|
+
mutations,
|
|
2157
|
+
lifecycleGeneration,
|
|
2158
|
+
)
|
|
2159
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
2160
|
+
const acceptedMutationIdSet = new Set(acceptedMutationIds)
|
|
2161
|
+
const acceptedMutations = mutations.filter((mutation) =>
|
|
2162
|
+
acceptedMutationIdSet.has(mutation.mutationId),
|
|
1195
2163
|
)
|
|
1196
|
-
}
|
|
1197
2164
|
|
|
1198
|
-
|
|
1199
|
-
|
|
2165
|
+
if (acceptedMutations.length !== mutations.length) {
|
|
2166
|
+
throw new Error(
|
|
2167
|
+
`persistence coordinator accepted ${acceptedMutations.length} of ${mutations.length} mutations; partial acceptance is not supported`,
|
|
2168
|
+
)
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
try {
|
|
2172
|
+
await this.confirmMutationsSyncUnsafe(acceptedMutations)
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
throw this.markTerminalFailure(error, lifecycleGeneration)
|
|
2175
|
+
}
|
|
2176
|
+
})
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
if (error instanceof PersistedCollectionDurabilityError) {
|
|
2179
|
+
throw this.markTerminalFailure(error, lifecycleGeneration)
|
|
2180
|
+
}
|
|
2181
|
+
throw error
|
|
2182
|
+
}
|
|
1200
2183
|
}
|
|
1201
2184
|
|
|
1202
2185
|
async acceptTransactionMutations(transaction: {
|
|
1203
2186
|
mutations: Array<PendingMutation<Record<string, unknown>>>
|
|
1204
2187
|
}): Promise<void> {
|
|
2188
|
+
this.throwIfTerminal()
|
|
1205
2189
|
const collectionMutations = this.filterMutationsForCollection(
|
|
1206
2190
|
transaction.mutations,
|
|
1207
2191
|
)
|
|
@@ -1213,16 +2197,42 @@ class PersistedCollectionRuntime<
|
|
|
1213
2197
|
await this.persistAndConfirmCollectionMutations(collectionMutations)
|
|
1214
2198
|
}
|
|
1215
2199
|
|
|
1216
|
-
cleanup(): void {
|
|
2200
|
+
cleanup(): Promise<void> {
|
|
1217
2201
|
this.advanceLifecycle()
|
|
2202
|
+
const remoteReleases: Array<Promise<void>> = []
|
|
2203
|
+
const failures: Array<unknown> = []
|
|
2204
|
+
const attempt = (callback: () => void): void => {
|
|
2205
|
+
try {
|
|
2206
|
+
callback()
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
failures.push(error)
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
1218
2211
|
|
|
1219
|
-
this.
|
|
2212
|
+
if (this.mode === `sync-present`) {
|
|
2213
|
+
for (const options of this.activeSubsets.values()) {
|
|
2214
|
+
attempt(() => {
|
|
2215
|
+
remoteReleases.push(
|
|
2216
|
+
this.persistence.coordinator
|
|
2217
|
+
.requestReleaseRemoteSubset(this.collectionId, options)
|
|
2218
|
+
.catch((error) => {
|
|
2219
|
+
this.reportSyncError(error)
|
|
2220
|
+
}),
|
|
2221
|
+
)
|
|
2222
|
+
})
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
attempt(() => this.coordinatorUnsubscribe?.())
|
|
1220
2227
|
this.coordinatorUnsubscribe = null
|
|
1221
2228
|
|
|
1222
|
-
this.
|
|
2229
|
+
attempt(() => this.remoteSubsetOwnerUnsubscribe?.())
|
|
2230
|
+
this.remoteSubsetOwnerUnsubscribe = null
|
|
2231
|
+
|
|
2232
|
+
attempt(() => this.indexAddedUnsubscribe?.())
|
|
1223
2233
|
this.indexAddedUnsubscribe = null
|
|
1224
2234
|
|
|
1225
|
-
this.indexRemovedUnsubscribe?.()
|
|
2235
|
+
attempt(() => this.indexRemovedUnsubscribe?.())
|
|
1226
2236
|
this.indexRemovedUnsubscribe = null
|
|
1227
2237
|
|
|
1228
2238
|
if (this.remoteEnsureRetryTimer !== null) {
|
|
@@ -1232,6 +2242,7 @@ class PersistedCollectionRuntime<
|
|
|
1232
2242
|
|
|
1233
2243
|
this.pendingRemoteSubsetEnsures.clear()
|
|
1234
2244
|
this.activeSubsets.clear()
|
|
2245
|
+
this.hydratedDemands.clear()
|
|
1235
2246
|
for (const transaction of this.queuedHydrationTransactions) {
|
|
1236
2247
|
transaction.rejectApplied?.(new SyncTransactionAbortedError())
|
|
1237
2248
|
}
|
|
@@ -1239,14 +2250,29 @@ class PersistedCollectionRuntime<
|
|
|
1239
2250
|
this.queuedTxCommitted.length = 0
|
|
1240
2251
|
this.clearSyncControls()
|
|
1241
2252
|
this.collection = null
|
|
2253
|
+
return Promise.all(remoteReleases).then(() => {
|
|
2254
|
+
if (failures.length === 1) throw failures[0]
|
|
2255
|
+
if (failures.length > 1) {
|
|
2256
|
+
throw new AggregateError(failures, `Persistence cleanup failed`, {
|
|
2257
|
+
cause: failures[0],
|
|
2258
|
+
})
|
|
2259
|
+
}
|
|
2260
|
+
})
|
|
1242
2261
|
}
|
|
1243
2262
|
|
|
1244
2263
|
private advanceLifecycle(): void {
|
|
1245
2264
|
this.lifecycleGeneration++
|
|
1246
|
-
this.
|
|
2265
|
+
this.persistedReadiness?.set({ status: `loading` })
|
|
2266
|
+
this.startupSettled = false
|
|
2267
|
+
this.hydratedDemands.clear()
|
|
1247
2268
|
this.startupMetadataPromise = null
|
|
1248
2269
|
this.startPromise = null
|
|
1249
2270
|
this.resumeBaselinePromise = null
|
|
2271
|
+
this.terminalFailure = undefined
|
|
2272
|
+
this.resumeCertificationPromise = null
|
|
2273
|
+
this.persistedKeySetEvidence = undefined
|
|
2274
|
+
this.persistedResumeGeneration = undefined
|
|
2275
|
+
this.resumeGenerationOwner = Symbol(`persisted resume generation owner`)
|
|
1250
2276
|
}
|
|
1251
2277
|
|
|
1252
2278
|
private withInternalApply<TResult>(task: () => TResult): TResult {
|
|
@@ -1270,8 +2296,9 @@ class PersistedCollectionRuntime<
|
|
|
1270
2296
|
|
|
1271
2297
|
private loadSubsetRowsUnsafe(
|
|
1272
2298
|
options: LoadSubsetOptions,
|
|
2299
|
+
adapter: HydrationPersistenceAdapter,
|
|
1273
2300
|
): Promise<Array<{ key: TKey; value: T; metadata?: unknown }>> {
|
|
1274
|
-
return
|
|
2301
|
+
return adapter.loadSubset(this.collectionId, options, {
|
|
1275
2302
|
requiredIndexSignatures: this.getRequiredIndexSignatures(),
|
|
1276
2303
|
}) as Promise<Array<{ key: TKey; value: T; metadata?: unknown }>>
|
|
1277
2304
|
}
|
|
@@ -1292,6 +2319,7 @@ class PersistedCollectionRuntime<
|
|
|
1292
2319
|
async scanPersistedRows(
|
|
1293
2320
|
options?: PersistedRowScanOptions,
|
|
1294
2321
|
): Promise<Array<PersistedScannedRow<T, TKey>>> {
|
|
2322
|
+
this.throwIfTerminal()
|
|
1295
2323
|
return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options))
|
|
1296
2324
|
}
|
|
1297
2325
|
|
|
@@ -1300,40 +2328,248 @@ class PersistedCollectionRuntime<
|
|
|
1300
2328
|
config: {
|
|
1301
2329
|
requestRemoteEnsure: boolean
|
|
1302
2330
|
lifecycleGeneration: number
|
|
2331
|
+
bindKeySetEvidence?: boolean
|
|
2332
|
+
requestLocalLoadFailure?: boolean
|
|
2333
|
+
rejectBufferedReplayFailure?: boolean
|
|
1303
2334
|
},
|
|
2335
|
+
adapter: HydrationPersistenceAdapter,
|
|
1304
2336
|
): Promise<void> {
|
|
1305
|
-
|
|
2337
|
+
let rowsLoaded = false
|
|
2338
|
+
let replayFailure: { reason: unknown } | undefined
|
|
2339
|
+
const resetSequence = this.resetSequence
|
|
2340
|
+
this.hydrationSequence++
|
|
2341
|
+
const hydrationContext = { suppliedRowKeys: new Set<TKey>() }
|
|
2342
|
+
this.activeHydrationContext = hydrationContext
|
|
1306
2343
|
try {
|
|
1307
|
-
|
|
2344
|
+
this.throwIfTerminal()
|
|
2345
|
+
this.hydratingGeneration = config.lifecycleGeneration
|
|
2346
|
+
try {
|
|
2347
|
+
let rows: Array<{ key: TKey; value: T; metadata?: unknown }>
|
|
2348
|
+
if (config.bindKeySetEvidence) {
|
|
2349
|
+
const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
|
|
2350
|
+
requiredIndexSignatures: this.getRequiredIndexSignatures(),
|
|
2351
|
+
includeRows: true,
|
|
2352
|
+
})
|
|
2353
|
+
rows = snapshot.rows as Array<{
|
|
2354
|
+
key: TKey
|
|
2355
|
+
value: T
|
|
2356
|
+
metadata?: unknown
|
|
2357
|
+
}>
|
|
2358
|
+
if (config.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2359
|
+
if (resetSequence !== this.resetSequence) return
|
|
2360
|
+
this.bindResumeSnapshotEvidence(snapshot)
|
|
2361
|
+
} else {
|
|
2362
|
+
rows = await this.loadSubsetRowsUnsafe(options, adapter)
|
|
2363
|
+
}
|
|
2364
|
+
rowsLoaded = true
|
|
2365
|
+
if (config.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2366
|
+
if (resetSequence !== this.resetSequence) return
|
|
2367
|
+
|
|
2368
|
+
if (
|
|
2369
|
+
!config.bindKeySetEvidence ||
|
|
2370
|
+
this.persistedKeySetEvidence?.status !== `incompatible`
|
|
2371
|
+
) {
|
|
2372
|
+
for (const row of rows) {
|
|
2373
|
+
hydrationContext.suppliedRowKeys.add(row.key)
|
|
2374
|
+
}
|
|
2375
|
+
const applied = this.applyRowsToCollection(rows)
|
|
2376
|
+
if (applied !== true) await applied
|
|
2377
|
+
}
|
|
2378
|
+
} finally {
|
|
2379
|
+
if (
|
|
2380
|
+
this.activeHydrationContext === hydrationContext &&
|
|
2381
|
+
this.hydratingGeneration === config.lifecycleGeneration
|
|
2382
|
+
) {
|
|
2383
|
+
this.hydratingGeneration = null
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
if (config.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2388
|
+
replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter)
|
|
1308
2389
|
if (config.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1309
2390
|
|
|
1310
|
-
|
|
2391
|
+
if (config.requestRemoteEnsure && !replayFailure) {
|
|
2392
|
+
this.queueRemoteSubsetEnsure(options)
|
|
2393
|
+
}
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
if (config.requestLocalLoadFailure && !rowsLoaded) {
|
|
2396
|
+
// Keep admitting source work to the hydration queue while recovery
|
|
2397
|
+
// reconstructs any persisted baseline required by partial updates.
|
|
2398
|
+
// The failed subset itself owns no rows, so recovery must evaluate
|
|
2399
|
+
// each queued transaction against the durable snapshot at its exact
|
|
2400
|
+
// FIFO cut instead of treating the snapshot as a successful load.
|
|
2401
|
+
this.hydratingGeneration = config.lifecycleGeneration
|
|
2402
|
+
try {
|
|
2403
|
+
await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
|
|
2404
|
+
config.lifecycleGeneration,
|
|
2405
|
+
adapter,
|
|
2406
|
+
)
|
|
2407
|
+
} catch (baselineError) {
|
|
2408
|
+
const terminalError = this.markTerminalFailure(
|
|
2409
|
+
baselineError,
|
|
2410
|
+
config.lifecycleGeneration,
|
|
2411
|
+
)
|
|
2412
|
+
throw terminalError
|
|
2413
|
+
} finally {
|
|
2414
|
+
if (this.hydratingGeneration === config.lifecycleGeneration) {
|
|
2415
|
+
this.hydratingGeneration = null
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
throw error
|
|
2419
|
+
}
|
|
2420
|
+
throw this.markTerminalFailure(error, config.lifecycleGeneration)
|
|
1311
2421
|
} finally {
|
|
1312
|
-
if (this.
|
|
1313
|
-
this.
|
|
2422
|
+
if (this.activeHydrationContext === hydrationContext) {
|
|
2423
|
+
this.activeHydrationContext = undefined
|
|
1314
2424
|
}
|
|
1315
2425
|
}
|
|
2426
|
+
if (replayFailure && config.rejectBufferedReplayFailure) {
|
|
2427
|
+
throw replayFailure.reason
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
1316
2430
|
|
|
1317
|
-
|
|
1318
|
-
|
|
2431
|
+
private async recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
|
|
2432
|
+
lifecycleGeneration: number,
|
|
2433
|
+
adapter: HydrationPersistenceAdapter,
|
|
2434
|
+
): Promise<void> {
|
|
2435
|
+
let snapshotRows:
|
|
2436
|
+
| Map<TKey, { key: TKey; value: T; metadata?: unknown }>
|
|
2437
|
+
| undefined
|
|
2438
|
+
type RecoveryPresence = `present` | `absent` | `unknown`
|
|
2439
|
+
const recoveredPresence = new Map<TKey, RecoveryPresence>()
|
|
2440
|
+
let snapshotInvalidatedByTruncate = false
|
|
2441
|
+
|
|
2442
|
+
while (this.queuedHydrationTransactions.length > 0) {
|
|
2443
|
+
const transaction = this.queuedHydrationTransactions.shift()
|
|
2444
|
+
if (!transaction) continue
|
|
2445
|
+
|
|
2446
|
+
try {
|
|
2447
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
2448
|
+
const baselineRows = new Map<
|
|
2449
|
+
TKey,
|
|
2450
|
+
{ key: TKey; value: T; metadata?: unknown }
|
|
2451
|
+
>()
|
|
2452
|
+
const transactionPresence = new Map<TKey, RecoveryPresence>()
|
|
2453
|
+
const transactionStartsWithTruncate = transaction.truncate
|
|
2454
|
+
const getPresence = (key: TKey): RecoveryPresence => {
|
|
2455
|
+
const known = transactionPresence.get(key)
|
|
2456
|
+
if (known !== undefined) return known
|
|
2457
|
+
if (transactionStartsWithTruncate) {
|
|
2458
|
+
transactionPresence.set(key, `absent`)
|
|
2459
|
+
return `absent`
|
|
2460
|
+
}
|
|
2461
|
+
const recovered = recoveredPresence.get(key)
|
|
2462
|
+
if (recovered !== undefined) {
|
|
2463
|
+
transactionPresence.set(key, recovered)
|
|
2464
|
+
return recovered
|
|
2465
|
+
}
|
|
2466
|
+
const present =
|
|
2467
|
+
this.collection?._hasHydratedKey(key) === true
|
|
2468
|
+
? `present`
|
|
2469
|
+
: snapshotInvalidatedByTruncate
|
|
2470
|
+
? `absent`
|
|
2471
|
+
: `unknown`
|
|
2472
|
+
transactionPresence.set(key, present)
|
|
2473
|
+
return present
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
if (!transaction.signal?.aborted) {
|
|
2477
|
+
for (const [index, operation] of transaction.operations.entries()) {
|
|
2478
|
+
const key = operation.key
|
|
2479
|
+
if (
|
|
2480
|
+
operation.type === `update` &&
|
|
2481
|
+
transaction.partialUpdateOperationIndexes.has(index) &&
|
|
2482
|
+
getPresence(key) === `unknown`
|
|
2483
|
+
) {
|
|
2484
|
+
if (snapshotRows === undefined) {
|
|
2485
|
+
const snapshot = await adapter.loadResumeSnapshot(
|
|
2486
|
+
this.collectionId,
|
|
2487
|
+
{
|
|
2488
|
+
requiredIndexSignatures: this.getRequiredIndexSignatures(),
|
|
2489
|
+
includeRows: true,
|
|
2490
|
+
},
|
|
2491
|
+
)
|
|
2492
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
2493
|
+
if (transaction.signal?.aborted) break
|
|
2494
|
+
snapshotRows = new Map(
|
|
2495
|
+
(
|
|
2496
|
+
snapshot.rows as Array<{
|
|
2497
|
+
key: TKey
|
|
2498
|
+
value: T
|
|
2499
|
+
metadata?: unknown
|
|
2500
|
+
}>
|
|
2501
|
+
).map((row) => [row.key, row]),
|
|
2502
|
+
)
|
|
2503
|
+
}
|
|
2504
|
+
const baseline = snapshotRows.get(key)
|
|
2505
|
+
if (baseline === undefined) {
|
|
2506
|
+
transactionPresence.set(key, `absent`)
|
|
2507
|
+
} else {
|
|
2508
|
+
baselineRows.set(key, baseline)
|
|
2509
|
+
transactionPresence.set(key, `present`)
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
transactionPresence.set(
|
|
2514
|
+
key,
|
|
2515
|
+
operation.type === `delete` ? `absent` : `present`,
|
|
2516
|
+
)
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
if (baselineRows.size > 0) {
|
|
2520
|
+
// Fold the durable baseline into the dependent reservation. It
|
|
2521
|
+
// must never become a separately observable publication: the
|
|
2522
|
+
// partial source value and its unseen fields are one atomic row.
|
|
2523
|
+
if (!transaction.signal?.aborted) {
|
|
2524
|
+
transaction.prependHydrationRows([...baselineRows.values()])
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const transactionOutcome =
|
|
2530
|
+
await this.applyBufferedSyncTransactionUnsafe(transaction, adapter)
|
|
2531
|
+
if (transactionOutcome.applied) {
|
|
2532
|
+
if (transaction.truncate) {
|
|
2533
|
+
snapshotInvalidatedByTruncate = true
|
|
2534
|
+
recoveredPresence.clear()
|
|
2535
|
+
}
|
|
2536
|
+
for (const operation of transaction.operations) {
|
|
2537
|
+
recoveredPresence.set(
|
|
2538
|
+
operation.key,
|
|
2539
|
+
operation.type === `delete` ? `absent` : `present`,
|
|
2540
|
+
)
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
} catch (error) {
|
|
2544
|
+
transaction.rejectApplied?.(error)
|
|
2545
|
+
for (const abandoned of this.queuedHydrationTransactions) {
|
|
2546
|
+
abandoned.rejectApplied?.(error)
|
|
2547
|
+
}
|
|
2548
|
+
this.queuedHydrationTransactions.length = 0
|
|
2549
|
+
throw error
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
1319
2553
|
|
|
1320
|
-
|
|
1321
|
-
|
|
2554
|
+
private rejectQueuedHydrationTransactions(error: unknown): void {
|
|
2555
|
+
for (const transaction of this.queuedHydrationTransactions) {
|
|
2556
|
+
transaction.rejectApplied?.(error)
|
|
1322
2557
|
}
|
|
2558
|
+
this.queuedHydrationTransactions.length = 0
|
|
1323
2559
|
}
|
|
1324
2560
|
|
|
1325
2561
|
private applyRowsToCollection(
|
|
1326
2562
|
rows: Array<{ key: TKey; value: T; metadata?: unknown }>,
|
|
1327
|
-
):
|
|
2563
|
+
): SyncAppliedReceipt {
|
|
1328
2564
|
if (
|
|
1329
2565
|
!this.syncControls.begin ||
|
|
1330
2566
|
!this.syncControls.write ||
|
|
1331
2567
|
!this.syncControls.commit
|
|
1332
2568
|
) {
|
|
1333
|
-
return
|
|
2569
|
+
return true
|
|
1334
2570
|
}
|
|
1335
2571
|
|
|
1336
|
-
this.withInternalApply(() => {
|
|
2572
|
+
return this.withInternalApply(() => {
|
|
1337
2573
|
this.syncControls.begin?.({ immediate: true })
|
|
1338
2574
|
|
|
1339
2575
|
for (const row of rows) {
|
|
@@ -1347,21 +2583,81 @@ class PersistedCollectionRuntime<
|
|
|
1347
2583
|
})
|
|
1348
2584
|
}
|
|
1349
2585
|
|
|
1350
|
-
this.syncControls.commit?.()
|
|
2586
|
+
return this.syncControls.commit?.() ?? true
|
|
1351
2587
|
})
|
|
1352
2588
|
}
|
|
1353
2589
|
|
|
2590
|
+
private getResumeSnapshotGeneration(snapshot: {
|
|
2591
|
+
latestTerm: number
|
|
2592
|
+
latestSeq: number
|
|
2593
|
+
latestRowVersion: number
|
|
2594
|
+
resetEpoch: number
|
|
2595
|
+
}): PersistedResumeGeneration {
|
|
2596
|
+
return {
|
|
2597
|
+
latestTerm: snapshot.latestTerm,
|
|
2598
|
+
latestSeq: snapshot.latestSeq,
|
|
2599
|
+
latestRowVersion: snapshot.latestRowVersion,
|
|
2600
|
+
resetEpoch: snapshot.resetEpoch,
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
private isExpectedResumeGeneration(
|
|
2605
|
+
generation: PersistedResumeGeneration,
|
|
2606
|
+
): boolean {
|
|
2607
|
+
const expected = this.persistedResumeGeneration
|
|
2608
|
+
return (
|
|
2609
|
+
expected !== undefined &&
|
|
2610
|
+
expected.latestTerm === generation.latestTerm &&
|
|
2611
|
+
expected.latestSeq === generation.latestSeq &&
|
|
2612
|
+
expected.latestRowVersion === generation.latestRowVersion &&
|
|
2613
|
+
expected.resetEpoch === generation.resetEpoch
|
|
2614
|
+
)
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
private bindResumeSnapshotEvidence(snapshot: {
|
|
2618
|
+
keySet?: PersistedKeySetEvidence
|
|
2619
|
+
latestTerm: number
|
|
2620
|
+
latestSeq: number
|
|
2621
|
+
latestRowVersion: number
|
|
2622
|
+
resetEpoch: number
|
|
2623
|
+
}): void {
|
|
2624
|
+
const generation = this.getResumeSnapshotGeneration(snapshot)
|
|
2625
|
+
const previousEvidenceStatus = this.persistedKeySetEvidence?.status
|
|
2626
|
+
// An uncertified sync baseline never authorized a persisted resume cursor,
|
|
2627
|
+
// so a later atomic snapshot may replace its evidence while the source
|
|
2628
|
+
// performs the already-required fresh snapshot. Local-only collections
|
|
2629
|
+
// have no remote cursor to fence; a managed write may advance a still-
|
|
2630
|
+
// consistent SQLite baseline during startup. Incompatible evidence remains
|
|
2631
|
+
// fail-closed in both modes.
|
|
2632
|
+
const mayAcceptUnownedGeneration =
|
|
2633
|
+
(this.mode === `sync-present` &&
|
|
2634
|
+
previousEvidenceStatus !== `consistent` &&
|
|
2635
|
+
previousEvidenceStatus !== `incompatible`) ||
|
|
2636
|
+
(this.mode === `sync-absent` &&
|
|
2637
|
+
previousEvidenceStatus === `consistent` &&
|
|
2638
|
+
snapshot.keySet?.status === `consistent`)
|
|
2639
|
+
this.observeStreamPosition(
|
|
2640
|
+
snapshot.latestTerm,
|
|
2641
|
+
snapshot.latestSeq,
|
|
2642
|
+
snapshot.latestRowVersion,
|
|
2643
|
+
)
|
|
2644
|
+
this.persistedKeySetEvidence =
|
|
2645
|
+
this.isExpectedResumeGeneration(generation) || mayAcceptUnownedGeneration
|
|
2646
|
+
? snapshot.keySet
|
|
2647
|
+
: { status: `incompatible` }
|
|
2648
|
+
}
|
|
2649
|
+
|
|
1354
2650
|
private replaceCollectionSnapshot(
|
|
1355
2651
|
rows: Array<{ key: TKey; value: T; metadata?: unknown }>,
|
|
1356
2652
|
collectionMetadata: Array<{ key: string; value: unknown }>,
|
|
1357
|
-
):
|
|
2653
|
+
): SyncAppliedReceipt {
|
|
1358
2654
|
if (
|
|
1359
2655
|
!this.syncControls.begin ||
|
|
1360
2656
|
!this.syncControls.write ||
|
|
1361
2657
|
!this.syncControls.commit ||
|
|
1362
2658
|
!this.syncControls.metadata
|
|
1363
2659
|
) {
|
|
1364
|
-
return
|
|
2660
|
+
return true
|
|
1365
2661
|
}
|
|
1366
2662
|
|
|
1367
2663
|
const nextMetadata = new Map(
|
|
@@ -1371,7 +2667,7 @@ class PersistedCollectionRuntime<
|
|
|
1371
2667
|
.list()
|
|
1372
2668
|
.map(({ key }) => key)
|
|
1373
2669
|
|
|
1374
|
-
this.withInternalApply(() => {
|
|
2670
|
+
return this.withInternalApply(() => {
|
|
1375
2671
|
this.syncControls.begin?.({ immediate: true })
|
|
1376
2672
|
this.syncControls.truncate?.()
|
|
1377
2673
|
|
|
@@ -1393,18 +2689,25 @@ class PersistedCollectionRuntime<
|
|
|
1393
2689
|
this.syncControls.metadata?.collection.set(key, value)
|
|
1394
2690
|
})
|
|
1395
2691
|
|
|
1396
|
-
this.syncControls.commit?.()
|
|
2692
|
+
return this.syncControls.commit?.() ?? true
|
|
1397
2693
|
})
|
|
1398
2694
|
}
|
|
1399
2695
|
|
|
1400
|
-
private async flushQueuedHydrationTransactionsUnsafe(
|
|
2696
|
+
private async flushQueuedHydrationTransactionsUnsafe(
|
|
2697
|
+
adapter: HydrationPersistenceAdapter,
|
|
2698
|
+
): Promise<{ reason: unknown } | undefined> {
|
|
2699
|
+
let operationFailure: { reason: unknown } | undefined
|
|
1401
2700
|
while (this.queuedHydrationTransactions.length > 0) {
|
|
1402
2701
|
const transaction = this.queuedHydrationTransactions.shift()
|
|
1403
2702
|
if (!transaction) {
|
|
1404
2703
|
continue
|
|
1405
2704
|
}
|
|
1406
2705
|
try {
|
|
1407
|
-
await this.applyBufferedSyncTransactionUnsafe(
|
|
2706
|
+
const outcome = await this.applyBufferedSyncTransactionUnsafe(
|
|
2707
|
+
transaction,
|
|
2708
|
+
adapter,
|
|
2709
|
+
)
|
|
2710
|
+
operationFailure ??= outcome.hydrationFailure
|
|
1408
2711
|
} catch (error) {
|
|
1409
2712
|
transaction.rejectApplied?.(error)
|
|
1410
2713
|
for (const abandoned of this.queuedHydrationTransactions) {
|
|
@@ -1414,133 +2717,168 @@ class PersistedCollectionRuntime<
|
|
|
1414
2717
|
throw error
|
|
1415
2718
|
}
|
|
1416
2719
|
}
|
|
2720
|
+
return operationFailure
|
|
1417
2721
|
}
|
|
1418
2722
|
|
|
1419
2723
|
private async applyBufferedSyncTransactionUnsafe(
|
|
1420
2724
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
const applyToCollection = (): SyncAppliedReceipt => {
|
|
1434
|
-
begin()
|
|
1435
|
-
|
|
1436
|
-
if (transaction.truncate) {
|
|
1437
|
-
truncate?.()
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
for (const operation of transaction.operations) {
|
|
1441
|
-
if (operation.type === `delete`) {
|
|
1442
|
-
write({
|
|
1443
|
-
type: `delete`,
|
|
1444
|
-
key: operation.key,
|
|
1445
|
-
})
|
|
1446
|
-
} else {
|
|
1447
|
-
write({
|
|
1448
|
-
type: `update`,
|
|
1449
|
-
value: operation.value,
|
|
1450
|
-
metadata: operation.metadata,
|
|
1451
|
-
})
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
for (const [key, metadataWrite] of transaction.rowMetadataWrites) {
|
|
1456
|
-
if (metadataWrite.type === `delete`) {
|
|
1457
|
-
metadata?.row.delete(key)
|
|
1458
|
-
} else {
|
|
1459
|
-
metadata?.row.set(key, metadataWrite.value)
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
for (const [key, metadataWrite] of transaction.collectionMetadataWrites) {
|
|
1464
|
-
if (metadataWrite.type === `delete`) {
|
|
1465
|
-
metadata?.collection.delete(key)
|
|
1466
|
-
} else {
|
|
1467
|
-
metadata?.collection.set(key, metadataWrite.value)
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
return commit(transaction.signal)
|
|
1472
|
-
}
|
|
2725
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
2726
|
+
): Promise<{
|
|
2727
|
+
applied: boolean
|
|
2728
|
+
hydrationFailure?: { reason: unknown }
|
|
2729
|
+
}> {
|
|
2730
|
+
this.throwIfTerminal()
|
|
2731
|
+
this.throwIfLifecycleReplaced(transaction.lifecycleGeneration)
|
|
2732
|
+
const abortedBeforeApplication = transaction.signal?.aborted === true
|
|
2733
|
+
let abortedDuringApplication = false
|
|
2734
|
+
let applicationReturned = false
|
|
1473
2735
|
|
|
1474
2736
|
try {
|
|
1475
2737
|
const applied = transaction.internal
|
|
1476
|
-
? this.withInternalApply(applyToCollection)
|
|
1477
|
-
: applyToCollection()
|
|
2738
|
+
? this.withInternalApply(transaction.applyToCollection)
|
|
2739
|
+
: transaction.applyToCollection()
|
|
2740
|
+
applicationReturned = true
|
|
2741
|
+
abortedDuringApplication = transaction.signal?.aborted === true
|
|
1478
2742
|
if (applied !== true) {
|
|
1479
|
-
|
|
2743
|
+
this.sourcePublicationWaitDepth++
|
|
2744
|
+
try {
|
|
2745
|
+
await applied
|
|
2746
|
+
} finally {
|
|
2747
|
+
this.sourcePublicationWaitDepth--
|
|
2748
|
+
}
|
|
1480
2749
|
}
|
|
2750
|
+
this.throwIfLifecycleReplaced(transaction.lifecycleGeneration)
|
|
1481
2751
|
|
|
1482
2752
|
if (!transaction.internal) {
|
|
1483
|
-
await this.persistAndBroadcastExternalSyncTransactionUnsafe(
|
|
2753
|
+
await this.persistAndBroadcastExternalSyncTransactionUnsafe(
|
|
2754
|
+
transaction,
|
|
2755
|
+
scopedAdapter,
|
|
2756
|
+
)
|
|
1484
2757
|
}
|
|
1485
2758
|
transaction.resolveApplied?.()
|
|
2759
|
+
return { applied: true }
|
|
1486
2760
|
} catch (error) {
|
|
1487
|
-
|
|
1488
|
-
|
|
2761
|
+
if (!applicationReturned) {
|
|
2762
|
+
abortedDuringApplication = transaction.signal?.aborted === true
|
|
2763
|
+
}
|
|
2764
|
+
const aborted =
|
|
2765
|
+
transaction.signal?.aborted ||
|
|
2766
|
+
error instanceof SyncTransactionAbortedError
|
|
2767
|
+
const shouldFailStop =
|
|
2768
|
+
aborted && transaction.shouldFailStopOnAbort?.() === true
|
|
2769
|
+
const terminalError = this.classifyBufferedTransactionFailure(
|
|
2770
|
+
transaction,
|
|
2771
|
+
error,
|
|
2772
|
+
)
|
|
2773
|
+
transaction.rejectApplied?.(terminalError)
|
|
2774
|
+
if (aborted && !shouldFailStop && transaction.rejectApplied) {
|
|
2775
|
+
return {
|
|
2776
|
+
applied: false,
|
|
2777
|
+
hydrationFailure:
|
|
2778
|
+
!abortedBeforeApplication && abortedDuringApplication
|
|
2779
|
+
? { reason: terminalError }
|
|
2780
|
+
: undefined,
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
throw terminalError
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
private classifyBufferedTransactionFailure(
|
|
2788
|
+
transaction: BufferedSyncTransaction<T, TKey>,
|
|
2789
|
+
error: unknown,
|
|
2790
|
+
): unknown {
|
|
2791
|
+
if (error instanceof PersistedCollectionDurabilityError) {
|
|
2792
|
+
return this.markTerminalFailure(error, transaction.lifecycleGeneration)
|
|
1489
2793
|
}
|
|
2794
|
+
const aborted =
|
|
2795
|
+
transaction.signal?.aborted ||
|
|
2796
|
+
error instanceof SyncTransactionAbortedError
|
|
2797
|
+
if (aborted && transaction.shouldFailStopOnAbort?.() !== true) {
|
|
2798
|
+
return error
|
|
2799
|
+
}
|
|
2800
|
+
return this.markTerminalFailure(error, transaction.lifecycleGeneration)
|
|
1490
2801
|
}
|
|
1491
2802
|
|
|
1492
2803
|
private async persistAndBroadcastExternalSyncTransactionUnsafe(
|
|
1493
2804
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
2805
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
1494
2806
|
): Promise<void> {
|
|
1495
2807
|
if (transaction.internal) {
|
|
1496
2808
|
return
|
|
1497
2809
|
}
|
|
1498
2810
|
|
|
1499
|
-
|
|
1500
|
-
|
|
2811
|
+
this.throwIfLifecycleReplaced(transaction.lifecycleGeneration)
|
|
1501
2812
|
if (
|
|
1502
2813
|
!transaction.truncate &&
|
|
1503
2814
|
transaction.operations.length === 0 &&
|
|
1504
2815
|
transaction.rowMetadataWrites.size === 0 &&
|
|
1505
2816
|
transaction.collectionMetadataWrites.size === 0
|
|
1506
2817
|
) {
|
|
1507
|
-
this.publishTxCommittedEvent(
|
|
1508
|
-
this.createTxCommittedPayload({
|
|
1509
|
-
term: streamPosition.term,
|
|
1510
|
-
seq: streamPosition.seq,
|
|
1511
|
-
txId: safeRandomUUID(),
|
|
1512
|
-
latestRowVersion: streamPosition.rowVersion,
|
|
1513
|
-
changedRows: [],
|
|
1514
|
-
deletedKeys: [],
|
|
1515
|
-
requiresFullReload: true,
|
|
1516
|
-
}),
|
|
1517
|
-
)
|
|
1518
2818
|
return
|
|
1519
2819
|
}
|
|
1520
2820
|
|
|
2821
|
+
const streamPosition = this.nextLocalStreamPosition()
|
|
1521
2822
|
const tx = this.createPersistedTxFromOperations(transaction, streamPosition)
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
latestRowVersion: tx.rowVersion,
|
|
1530
|
-
requiresFullReload: transaction.truncate,
|
|
1531
|
-
changedRows: transaction.operations
|
|
1532
|
-
.filter((operation) => operation.type === `update`)
|
|
1533
|
-
.map((operation) => ({
|
|
1534
|
-
key: operation.key,
|
|
1535
|
-
value: operation.value as Record<string, unknown>,
|
|
1536
|
-
})),
|
|
1537
|
-
deletedKeys: transaction.operations
|
|
1538
|
-
.filter((operation) => operation.type === `delete`)
|
|
1539
|
-
.map((operation) => operation.key),
|
|
1540
|
-
rowMetadataMutations: tx.rowMetadataMutations,
|
|
1541
|
-
collectionMetadataMutations: tx.collectionMetadataMutations,
|
|
1542
|
-
}),
|
|
2823
|
+
const response = await this.persistence.coordinator.requestApplyCommittedTx(
|
|
2824
|
+
this.collectionId,
|
|
2825
|
+
tx,
|
|
2826
|
+
// A hydration-scoped adapter is already inside the shared scheduler.
|
|
2827
|
+
// Ordinary source commits pass no scoped adapter so the coordinator
|
|
2828
|
+
// enters that scheduler before taking the database-wide writer lock.
|
|
2829
|
+
scopedAdapter,
|
|
1543
2830
|
)
|
|
2831
|
+
if (!response.ok) {
|
|
2832
|
+
if (response.code === `PERSISTENCE_ERROR`) {
|
|
2833
|
+
const error = new PersistedCollectionDurabilityError(
|
|
2834
|
+
`Failed to durably persist collection "${this.collectionId}": ${response.error}`,
|
|
2835
|
+
{
|
|
2836
|
+
cause: response,
|
|
2837
|
+
code: response.sourceCode ?? response.code,
|
|
2838
|
+
path: response.path,
|
|
2839
|
+
},
|
|
2840
|
+
)
|
|
2841
|
+
throw error
|
|
2842
|
+
}
|
|
2843
|
+
throw new Error(
|
|
2844
|
+
`failed to apply external sync transaction through coordinator: ${response.error}`,
|
|
2845
|
+
)
|
|
2846
|
+
}
|
|
2847
|
+
if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2848
|
+
this.observeStreamPosition(
|
|
2849
|
+
response.term,
|
|
2850
|
+
response.seq,
|
|
2851
|
+
response.latestRowVersion,
|
|
2852
|
+
)
|
|
2853
|
+
if (
|
|
2854
|
+
transaction.expectedResumeGenerationOwner ===
|
|
2855
|
+
this.resumeGenerationOwner &&
|
|
2856
|
+
this.persistedResumeGeneration !== undefined
|
|
2857
|
+
) {
|
|
2858
|
+
this.persistedResumeGeneration = {
|
|
2859
|
+
...this.persistedResumeGeneration,
|
|
2860
|
+
latestTerm: response.term,
|
|
2861
|
+
latestSeq: response.seq,
|
|
2862
|
+
latestRowVersion: response.latestRowVersion,
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
private async applyCommittedTx(
|
|
2868
|
+
tx: PersistedTx,
|
|
2869
|
+
lifecycleGeneration: number,
|
|
2870
|
+
): Promise<void> {
|
|
2871
|
+
this.throwIfTerminal()
|
|
2872
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
2873
|
+
try {
|
|
2874
|
+
await this.persistence.adapter.applyCommittedTx(this.collectionId, tx)
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
throw this.markTerminalFailure(
|
|
2877
|
+
toPersistedCollectionDurabilityError(this.collectionId, error),
|
|
2878
|
+
lifecycleGeneration,
|
|
2879
|
+
)
|
|
2880
|
+
}
|
|
2881
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
1544
2882
|
}
|
|
1545
2883
|
|
|
1546
2884
|
private createPersistedTxFromOperations(
|
|
@@ -1622,9 +2960,9 @@ class PersistedCollectionRuntime<
|
|
|
1622
2960
|
}
|
|
1623
2961
|
}
|
|
1624
2962
|
|
|
1625
|
-
private confirmMutationsSyncUnsafe(
|
|
2963
|
+
private async confirmMutationsSyncUnsafe(
|
|
1626
2964
|
mutations: Array<PendingMutation<T>>,
|
|
1627
|
-
): void {
|
|
2965
|
+
): Promise<void> {
|
|
1628
2966
|
if (
|
|
1629
2967
|
!this.syncControls.begin ||
|
|
1630
2968
|
!this.syncControls.write ||
|
|
@@ -1633,7 +2971,7 @@ class PersistedCollectionRuntime<
|
|
|
1633
2971
|
return
|
|
1634
2972
|
}
|
|
1635
2973
|
|
|
1636
|
-
this.withInternalApply(() => {
|
|
2974
|
+
const applied = this.withInternalApply(() => {
|
|
1637
2975
|
this.syncControls.begin?.({ immediate: true })
|
|
1638
2976
|
|
|
1639
2977
|
for (const mutation of mutations) {
|
|
@@ -1650,8 +2988,12 @@ class PersistedCollectionRuntime<
|
|
|
1650
2988
|
}
|
|
1651
2989
|
}
|
|
1652
2990
|
|
|
1653
|
-
this.syncControls.commit?.()
|
|
2991
|
+
return this.syncControls.commit?.() ?? true
|
|
1654
2992
|
})
|
|
2993
|
+
|
|
2994
|
+
if (applied !== true) {
|
|
2995
|
+
await applied
|
|
2996
|
+
}
|
|
1655
2997
|
}
|
|
1656
2998
|
|
|
1657
2999
|
private filterMutationsForCollection(
|
|
@@ -1668,7 +3010,9 @@ class PersistedCollectionRuntime<
|
|
|
1668
3010
|
|
|
1669
3011
|
private async persistCollectionMutationsUnsafe(
|
|
1670
3012
|
mutations: Array<PendingMutation<T>>,
|
|
3013
|
+
lifecycleGeneration: number,
|
|
1671
3014
|
): Promise<Array<string>> {
|
|
3015
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
1672
3016
|
// When a coordinator with requestApplyLocalMutations is available, always
|
|
1673
3017
|
// route through it — even on the leader tab. This ensures the coordinator's
|
|
1674
3018
|
// seq/rowVersion counters stay in sync with actual writes. Without this,
|
|
@@ -1688,7 +3032,20 @@ class PersistedCollectionRuntime<
|
|
|
1688
3032
|
envelopeMutations,
|
|
1689
3033
|
)
|
|
1690
3034
|
|
|
3035
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
3036
|
+
|
|
1691
3037
|
if (!response.ok) {
|
|
3038
|
+
if (response.code === `PERSISTENCE_ERROR`) {
|
|
3039
|
+
const error = new PersistedCollectionDurabilityError(
|
|
3040
|
+
`Failed to durably persist collection "${this.collectionId}": ${response.error}`,
|
|
3041
|
+
{
|
|
3042
|
+
cause: response,
|
|
3043
|
+
code: response.sourceCode ?? response.code,
|
|
3044
|
+
path: response.path,
|
|
3045
|
+
},
|
|
3046
|
+
)
|
|
3047
|
+
throw error
|
|
3048
|
+
}
|
|
1692
3049
|
throw new Error(
|
|
1693
3050
|
`failed to apply local mutations through coordinator: ${response.error}`,
|
|
1694
3051
|
)
|
|
@@ -1723,7 +3080,9 @@ class PersistedCollectionRuntime<
|
|
|
1723
3080
|
// SingleProcessCoordinator). Apply directly and broadcast.
|
|
1724
3081
|
const streamPosition = this.nextLocalStreamPosition()
|
|
1725
3082
|
const tx = this.createPersistedTxFromMutations(mutations, streamPosition)
|
|
1726
|
-
await this.
|
|
3083
|
+
await this.applyCommittedTx(tx, lifecycleGeneration)
|
|
3084
|
+
|
|
3085
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
1727
3086
|
|
|
1728
3087
|
this.publishTxCommittedEvent(
|
|
1729
3088
|
this.createTxCommittedPayload({
|
|
@@ -1868,23 +3227,44 @@ class PersistedCollectionRuntime<
|
|
|
1868
3227
|
return id
|
|
1869
3228
|
}
|
|
1870
3229
|
|
|
1871
|
-
private
|
|
3230
|
+
private canRouteRemoteDemandThroughCoordinator(): boolean {
|
|
1872
3231
|
if (
|
|
3232
|
+
this.getCurrentTerminalFailure() ||
|
|
1873
3233
|
this.mode !== `sync-present` ||
|
|
1874
|
-
|
|
3234
|
+
this.persistence.coordinator instanceof SingleProcessCoordinator
|
|
3235
|
+
) {
|
|
3236
|
+
return false
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// A follower routes demand to the elected owner even when its own source
|
|
3240
|
+
// cannot own acquisitions. Only an elected node needs a local owner.
|
|
3241
|
+
return (
|
|
3242
|
+
!this.persistence.coordinator.isLeader(this.collectionId) ||
|
|
3243
|
+
this.remoteSubsetOwnerUnsubscribe !== null
|
|
3244
|
+
)
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
private queueRemoteSubsetEnsure(options: LoadSubsetOptions): void {
|
|
3248
|
+
if (
|
|
3249
|
+
options.signal?.aborted ||
|
|
3250
|
+
!this.canRouteRemoteDemandThroughCoordinator() ||
|
|
1875
3251
|
this.activeSubsets.get(this.getSubsetKey(options)) !== options
|
|
1876
3252
|
) {
|
|
1877
3253
|
return
|
|
1878
3254
|
}
|
|
1879
3255
|
|
|
1880
|
-
this.
|
|
3256
|
+
const subsetKey = this.getSubsetKey(options)
|
|
3257
|
+
if (this.activeSubsets.get(subsetKey) !== options) return
|
|
3258
|
+
|
|
3259
|
+
this.pendingRemoteSubsetEnsures.set(subsetKey, options)
|
|
1881
3260
|
void this.flushPendingRemoteSubsetEnsures()
|
|
1882
3261
|
}
|
|
1883
3262
|
|
|
1884
3263
|
private scheduleRemoteEnsureRetry(): void {
|
|
1885
3264
|
if (
|
|
3265
|
+
this.getCurrentTerminalFailure() ||
|
|
1886
3266
|
this.mode !== `sync-present` ||
|
|
1887
|
-
|
|
3267
|
+
this.persistence.coordinator instanceof SingleProcessCoordinator
|
|
1888
3268
|
) {
|
|
1889
3269
|
return
|
|
1890
3270
|
}
|
|
@@ -1904,8 +3284,9 @@ class PersistedCollectionRuntime<
|
|
|
1904
3284
|
|
|
1905
3285
|
private async flushPendingRemoteSubsetEnsures(): Promise<void> {
|
|
1906
3286
|
if (
|
|
3287
|
+
this.getCurrentTerminalFailure() ||
|
|
1907
3288
|
this.mode !== `sync-present` ||
|
|
1908
|
-
|
|
3289
|
+
this.persistence.coordinator instanceof SingleProcessCoordinator
|
|
1909
3290
|
) {
|
|
1910
3291
|
return
|
|
1911
3292
|
}
|
|
@@ -1916,6 +3297,13 @@ class PersistedCollectionRuntime<
|
|
|
1916
3297
|
}
|
|
1917
3298
|
|
|
1918
3299
|
for (const [subsetKey, options] of this.pendingRemoteSubsetEnsures) {
|
|
3300
|
+
if (
|
|
3301
|
+
options.signal?.aborted ||
|
|
3302
|
+
this.activeSubsets.get(subsetKey) !== options
|
|
3303
|
+
) {
|
|
3304
|
+
this.pendingRemoteSubsetEnsures.delete(subsetKey)
|
|
3305
|
+
continue
|
|
3306
|
+
}
|
|
1919
3307
|
try {
|
|
1920
3308
|
await this.persistence.coordinator.requestEnsureRemoteSubset(
|
|
1921
3309
|
this.collectionId,
|
|
@@ -1923,7 +3311,14 @@ class PersistedCollectionRuntime<
|
|
|
1923
3311
|
)
|
|
1924
3312
|
this.pendingRemoteSubsetEnsures.delete(subsetKey)
|
|
1925
3313
|
} catch (error) {
|
|
1926
|
-
|
|
3314
|
+
if (
|
|
3315
|
+
options.signal?.aborted ||
|
|
3316
|
+
this.activeSubsets.get(subsetKey) !== options
|
|
3317
|
+
) {
|
|
3318
|
+
this.pendingRemoteSubsetEnsures.delete(subsetKey)
|
|
3319
|
+
} else {
|
|
3320
|
+
console.warn(`Failed to ensure remote subset:`, error)
|
|
3321
|
+
}
|
|
1927
3322
|
}
|
|
1928
3323
|
}
|
|
1929
3324
|
|
|
@@ -1935,6 +3330,10 @@ class PersistedCollectionRuntime<
|
|
|
1935
3330
|
return
|
|
1936
3331
|
}
|
|
1937
3332
|
|
|
3333
|
+
this.persistence.coordinator.setAdapterForCollection?.(
|
|
3334
|
+
this.collectionId,
|
|
3335
|
+
this.persistence.adapter,
|
|
3336
|
+
)
|
|
1938
3337
|
this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
|
|
1939
3338
|
this.collectionId,
|
|
1940
3339
|
(message) => {
|
|
@@ -1947,8 +3346,12 @@ class PersistedCollectionRuntime<
|
|
|
1947
3346
|
if (message.collectionId !== this.collectionId) {
|
|
1948
3347
|
return
|
|
1949
3348
|
}
|
|
3349
|
+
if (this.getCurrentTerminalFailure()) {
|
|
3350
|
+
return
|
|
3351
|
+
}
|
|
1950
3352
|
|
|
1951
3353
|
const { payload } = message
|
|
3354
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1952
3355
|
const isSelf = message.senderId === this.persistence.coordinator.getNodeId()
|
|
1953
3356
|
|
|
1954
3357
|
// Allow tx:committed from self — the coordinator produces these on behalf
|
|
@@ -1956,14 +3359,17 @@ class PersistedCollectionRuntime<
|
|
|
1956
3359
|
// processCommittedTxUnsafe prevents double-processing of our own writes.
|
|
1957
3360
|
if (isTxCommittedPayload(payload)) {
|
|
1958
3361
|
if (this.isHydratingNow()) {
|
|
1959
|
-
this.queuedTxCommitted.push(
|
|
3362
|
+
this.queuedTxCommitted.push({
|
|
3363
|
+
txCommitted: payload,
|
|
3364
|
+
lifecycleGeneration,
|
|
3365
|
+
})
|
|
1960
3366
|
return
|
|
1961
3367
|
}
|
|
1962
3368
|
|
|
1963
3369
|
void this.applyMutex
|
|
1964
|
-
.run(() => this.processCommittedTxUnsafe(payload))
|
|
3370
|
+
.run(() => this.processCommittedTxUnsafe(payload, lifecycleGeneration))
|
|
1965
3371
|
.catch((error) => {
|
|
1966
|
-
|
|
3372
|
+
this.markTerminalFailure(error, lifecycleGeneration)
|
|
1967
3373
|
})
|
|
1968
3374
|
return
|
|
1969
3375
|
}
|
|
@@ -1975,9 +3381,16 @@ class PersistedCollectionRuntime<
|
|
|
1975
3381
|
|
|
1976
3382
|
if (isCollectionResetPayload(payload)) {
|
|
1977
3383
|
void this.applyMutex
|
|
1978
|
-
.run(() =>
|
|
3384
|
+
.run(async () => {
|
|
3385
|
+
await this.runInHydrationScope((adapter) =>
|
|
3386
|
+
this.truncateAndReloadUnsafe(adapter, lifecycleGeneration),
|
|
3387
|
+
)
|
|
3388
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
3389
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
3390
|
+
}
|
|
3391
|
+
})
|
|
1979
3392
|
.catch((error) => {
|
|
1980
|
-
|
|
3393
|
+
this.markTerminalFailure(error, lifecycleGeneration)
|
|
1981
3394
|
})
|
|
1982
3395
|
}
|
|
1983
3396
|
}
|
|
@@ -1988,13 +3401,18 @@ class PersistedCollectionRuntime<
|
|
|
1988
3401
|
if (!queued) {
|
|
1989
3402
|
continue
|
|
1990
3403
|
}
|
|
1991
|
-
await this.processCommittedTxUnsafe(
|
|
3404
|
+
await this.processCommittedTxUnsafe(
|
|
3405
|
+
queued.txCommitted,
|
|
3406
|
+
queued.lifecycleGeneration,
|
|
3407
|
+
)
|
|
1992
3408
|
}
|
|
1993
3409
|
}
|
|
1994
3410
|
|
|
1995
3411
|
private async processCommittedTxUnsafe(
|
|
1996
3412
|
txCommitted: TxCommitted,
|
|
3413
|
+
lifecycleGeneration = this.lifecycleGeneration,
|
|
1997
3414
|
): Promise<void> {
|
|
3415
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1998
3416
|
if (txCommitted.term < this.latestTerm) {
|
|
1999
3417
|
return
|
|
2000
3418
|
}
|
|
@@ -2014,7 +3432,8 @@ class PersistedCollectionRuntime<
|
|
|
2014
3432
|
const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms
|
|
2015
3433
|
|
|
2016
3434
|
if (hasGap) {
|
|
2017
|
-
await this.recoverFromSeqGapUnsafe()
|
|
3435
|
+
await this.recoverFromSeqGapUnsafe(lifecycleGeneration)
|
|
3436
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2018
3437
|
if (
|
|
2019
3438
|
txCommitted.term < this.latestTerm ||
|
|
2020
3439
|
(txCommitted.term === this.latestTerm &&
|
|
@@ -2030,16 +3449,32 @@ class PersistedCollectionRuntime<
|
|
|
2030
3449
|
txCommitted.latestRowVersion,
|
|
2031
3450
|
)
|
|
2032
3451
|
|
|
2033
|
-
await this.invalidateFromCommittedTxUnsafe(
|
|
3452
|
+
await this.invalidateFromCommittedTxUnsafe(
|
|
3453
|
+
txCommitted,
|
|
3454
|
+
this.persistence.adapter,
|
|
3455
|
+
)
|
|
3456
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
3457
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
3458
|
+
}
|
|
2034
3459
|
}
|
|
2035
3460
|
|
|
2036
|
-
private async recoverFromSeqGapUnsafe(
|
|
3461
|
+
private async recoverFromSeqGapUnsafe(
|
|
3462
|
+
lifecycleGeneration: number,
|
|
3463
|
+
): Promise<void> {
|
|
3464
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2037
3465
|
if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
|
|
3466
|
+
let pullResponse: PullSinceResponse | undefined
|
|
2038
3467
|
try {
|
|
2039
|
-
|
|
3468
|
+
pullResponse = await this.persistence.coordinator.pullSince(
|
|
2040
3469
|
this.collectionId,
|
|
2041
3470
|
this.latestRowVersion,
|
|
2042
3471
|
)
|
|
3472
|
+
} catch (error) {
|
|
3473
|
+
console.warn(`Failed pullSince recovery attempt:`, error)
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
if (pullResponse) {
|
|
3477
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2043
3478
|
|
|
2044
3479
|
if (pullResponse.ok) {
|
|
2045
3480
|
this.observeStreamPosition(
|
|
@@ -2047,33 +3482,51 @@ class PersistedCollectionRuntime<
|
|
|
2047
3482
|
pullResponse.latestSeq,
|
|
2048
3483
|
pullResponse.latestRowVersion,
|
|
2049
3484
|
)
|
|
2050
|
-
if (pullResponse.requiresFullReload
|
|
2051
|
-
await this.
|
|
3485
|
+
if (pullResponse.requiresFullReload) {
|
|
3486
|
+
await this.runInHydrationScope((adapter) =>
|
|
3487
|
+
this.reloadActiveSubsetsUnsafe(adapter),
|
|
3488
|
+
)
|
|
3489
|
+
return
|
|
3490
|
+
}
|
|
3491
|
+
const deltas = pullResponse.deltas
|
|
3492
|
+
if (!deltas) {
|
|
3493
|
+
await this.runInHydrationScope((adapter) =>
|
|
3494
|
+
this.reloadActiveSubsetsUnsafe(adapter),
|
|
3495
|
+
)
|
|
2052
3496
|
return
|
|
2053
3497
|
}
|
|
2054
3498
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
3499
|
+
await this.runInHydrationScope(async (adapter) => {
|
|
3500
|
+
for (const delta of deltas) {
|
|
3501
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
3502
|
+
await this.invalidateFromCommittedTxUnsafe(
|
|
3503
|
+
{
|
|
3504
|
+
type: `tx:committed`,
|
|
3505
|
+
term: pullResponse.latestTerm,
|
|
3506
|
+
seq: pullResponse.latestSeq,
|
|
3507
|
+
txId: delta.txId,
|
|
3508
|
+
latestRowVersion: delta.latestRowVersion,
|
|
3509
|
+
requiresFullReload: false,
|
|
3510
|
+
changedRows: delta.changedRows,
|
|
3511
|
+
deletedKeys: delta.deletedKeys,
|
|
3512
|
+
rowMetadataMutations: delta.rowMetadataMutations,
|
|
3513
|
+
collectionMetadataMutations:
|
|
3514
|
+
delta.collectionMetadataMutations,
|
|
3515
|
+
},
|
|
3516
|
+
adapter,
|
|
3517
|
+
)
|
|
3518
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
3519
|
+
}
|
|
3520
|
+
})
|
|
2069
3521
|
return
|
|
2070
3522
|
}
|
|
2071
|
-
} catch (error) {
|
|
2072
|
-
console.warn(`Failed pullSince recovery attempt:`, error)
|
|
2073
3523
|
}
|
|
2074
3524
|
}
|
|
2075
3525
|
|
|
2076
|
-
|
|
3526
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
3527
|
+
await this.runInHydrationScope((adapter) =>
|
|
3528
|
+
this.truncateAndReloadUnsafe(adapter, lifecycleGeneration),
|
|
3529
|
+
)
|
|
2077
3530
|
|
|
2078
3531
|
if (this.mode === `sync-present`) {
|
|
2079
3532
|
for (const options of this.activeSubsets.values()) {
|
|
@@ -2082,30 +3535,46 @@ class PersistedCollectionRuntime<
|
|
|
2082
3535
|
}
|
|
2083
3536
|
}
|
|
2084
3537
|
|
|
2085
|
-
private async truncateAndReloadUnsafe(
|
|
3538
|
+
private async truncateAndReloadUnsafe(
|
|
3539
|
+
adapter: HydrationPersistenceAdapter,
|
|
3540
|
+
lifecycleGeneration = this.lifecycleGeneration,
|
|
3541
|
+
): Promise<void> {
|
|
3542
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
3543
|
+
// A subscriber may reacquire reentrantly from the truncate commit.
|
|
3544
|
+
this.hydratedDemands.clear()
|
|
3545
|
+
this.resetSequence++
|
|
2086
3546
|
if (this.syncControls.begin && this.syncControls.commit) {
|
|
2087
|
-
this.withInternalApply(() => {
|
|
3547
|
+
const applied = this.withInternalApply(() => {
|
|
2088
3548
|
this.syncControls.begin?.({ immediate: true })
|
|
2089
3549
|
this.syncControls.truncate?.()
|
|
2090
|
-
this.syncControls.commit?.()
|
|
3550
|
+
return this.syncControls.commit?.() ?? true
|
|
2091
3551
|
})
|
|
3552
|
+
if (applied !== true) await applied
|
|
2092
3553
|
}
|
|
2093
3554
|
|
|
2094
|
-
|
|
3555
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
3556
|
+
await this.reloadActiveSubsetsUnsafe(adapter)
|
|
2095
3557
|
}
|
|
2096
3558
|
|
|
2097
3559
|
private async invalidateFromCommittedTxUnsafe(
|
|
2098
3560
|
txCommitted: TxCommitted,
|
|
3561
|
+
adapter: HydrationPersistenceAdapter,
|
|
2099
3562
|
): Promise<void> {
|
|
3563
|
+
const reloadActiveSubsets = () =>
|
|
3564
|
+
this.runInHydrationScope(
|
|
3565
|
+
(scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
|
|
3566
|
+
adapter,
|
|
3567
|
+
)
|
|
3568
|
+
|
|
2100
3569
|
if (txCommitted.requiresFullReload) {
|
|
2101
|
-
await
|
|
3570
|
+
await reloadActiveSubsets()
|
|
2102
3571
|
return
|
|
2103
3572
|
}
|
|
2104
3573
|
|
|
2105
3574
|
const changedKeyCount =
|
|
2106
3575
|
txCommitted.changedRows.length + txCommitted.deletedKeys.length
|
|
2107
3576
|
if (changedKeyCount > TARGETED_INVALIDATION_KEY_LIMIT) {
|
|
2108
|
-
await
|
|
3577
|
+
await reloadActiveSubsets()
|
|
2109
3578
|
return
|
|
2110
3579
|
}
|
|
2111
3580
|
|
|
@@ -2120,7 +3589,7 @@ class PersistedCollectionRuntime<
|
|
|
2120
3589
|
|
|
2121
3590
|
// Has paginated subsets — fall back to full reload.
|
|
2122
3591
|
// Targeted invalidation for paginated subsets is deferred to a future iteration.
|
|
2123
|
-
await
|
|
3592
|
+
await reloadActiveSubsets()
|
|
2124
3593
|
}
|
|
2125
3594
|
|
|
2126
3595
|
private async applyTargetedInvalidationUnsafe(
|
|
@@ -2130,7 +3599,7 @@ class PersistedCollectionRuntime<
|
|
|
2130
3599
|
(opt) => (opt.where ? compileSingleRowExpression(opt.where) : null),
|
|
2131
3600
|
)
|
|
2132
3601
|
|
|
2133
|
-
this.withInternalApply(() => {
|
|
3602
|
+
const applied = this.withInternalApply(() => {
|
|
2134
3603
|
this.syncControls.begin?.({ immediate: true })
|
|
2135
3604
|
|
|
2136
3605
|
for (const {
|
|
@@ -2175,24 +3644,33 @@ class PersistedCollectionRuntime<
|
|
|
2175
3644
|
}
|
|
2176
3645
|
})
|
|
2177
3646
|
|
|
2178
|
-
this.syncControls.commit?.()
|
|
3647
|
+
return this.syncControls.commit?.() ?? true
|
|
2179
3648
|
})
|
|
3649
|
+
if (applied !== true) await applied
|
|
2180
3650
|
}
|
|
2181
3651
|
|
|
2182
|
-
private async reloadActiveSubsetsUnsafe(
|
|
3652
|
+
private async reloadActiveSubsetsUnsafe(
|
|
3653
|
+
adapter: HydrationPersistenceAdapter,
|
|
3654
|
+
): Promise<void> {
|
|
2183
3655
|
const lifecycleGeneration = this.lifecycleGeneration
|
|
3656
|
+
const truncateGeneration = this.sourceTruncateGeneration
|
|
2184
3657
|
const activeSubsetOptions =
|
|
2185
3658
|
this.activeSubsets.size > 0
|
|
2186
3659
|
? Array.from(this.activeSubsets.values())
|
|
2187
3660
|
: [{}]
|
|
2188
3661
|
|
|
3662
|
+
this.hydratedDemands.clear()
|
|
3663
|
+
this.hydrationSequence++
|
|
3664
|
+
const hydrationContext = { suppliedRowKeys: new Set<TKey>() }
|
|
3665
|
+
this.activeHydrationContext = hydrationContext
|
|
2189
3666
|
this.hydratingGeneration = lifecycleGeneration
|
|
2190
3667
|
try {
|
|
2191
3668
|
const mergedRows = new Map<TKey, { value: T; metadata?: unknown }>()
|
|
2192
|
-
const collectionMetadata =
|
|
3669
|
+
const collectionMetadata =
|
|
3670
|
+
await this.loadCollectionMetadataSnapshot(adapter)
|
|
2193
3671
|
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2194
3672
|
for (const options of activeSubsetOptions) {
|
|
2195
|
-
const subsetRows = await this.loadSubsetRowsUnsafe(options)
|
|
3673
|
+
const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter)
|
|
2196
3674
|
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2197
3675
|
for (const row of subsetRows) {
|
|
2198
3676
|
mergedRows.set(row.key, {
|
|
@@ -2202,7 +3680,11 @@ class PersistedCollectionRuntime<
|
|
|
2202
3680
|
}
|
|
2203
3681
|
}
|
|
2204
3682
|
|
|
2205
|
-
|
|
3683
|
+
for (const key of mergedRows.keys()) {
|
|
3684
|
+
hydrationContext.suppliedRowKeys.add(key)
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
const applied = this.replaceCollectionSnapshot(
|
|
2206
3688
|
Array.from(mergedRows.entries()).map(([key, row]) => ({
|
|
2207
3689
|
key,
|
|
2208
3690
|
value: row.value,
|
|
@@ -2210,14 +3692,34 @@ class PersistedCollectionRuntime<
|
|
|
2210
3692
|
})),
|
|
2211
3693
|
collectionMetadata,
|
|
2212
3694
|
)
|
|
3695
|
+
if (applied !== true) await applied
|
|
2213
3696
|
} finally {
|
|
2214
|
-
if (
|
|
3697
|
+
if (
|
|
3698
|
+
this.activeHydrationContext === hydrationContext &&
|
|
3699
|
+
this.hydratingGeneration === lifecycleGeneration
|
|
3700
|
+
) {
|
|
2215
3701
|
this.hydratingGeneration = null
|
|
2216
3702
|
}
|
|
3703
|
+
if (this.activeHydrationContext === hydrationContext) {
|
|
3704
|
+
this.activeHydrationContext = undefined
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
3708
|
+
await this.flushQueuedHydrationTransactionsUnsafe(adapter)
|
|
3709
|
+
// Coordinator commits queued during this hydrate are processed outside
|
|
3710
|
+
// its scope. Leave the synchronous fast path closed until a later load
|
|
3711
|
+
// if any such commit still needs to be applied.
|
|
3712
|
+
if (
|
|
3713
|
+
truncateGeneration === this.sourceTruncateGeneration &&
|
|
3714
|
+
this.queuedTxCommitted.length === 0
|
|
3715
|
+
) {
|
|
3716
|
+
for (const options of activeSubsetOptions) {
|
|
3717
|
+
if (this.activeSubsets.get(this.getSubsetKey(options)) === options) {
|
|
3718
|
+
this.hydratedDemands.add(getLoadSubsetDemandKey(options))
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
2217
3722
|
}
|
|
2218
|
-
|
|
2219
|
-
await this.flushQueuedHydrationTransactionsUnsafe()
|
|
2220
|
-
await this.flushQueuedTxCommittedUnsafe()
|
|
2221
3723
|
}
|
|
2222
3724
|
|
|
2223
3725
|
private attachIndexLifecycleListeners(): void {
|
|
@@ -2242,16 +3744,33 @@ class PersistedCollectionRuntime<
|
|
|
2242
3744
|
|
|
2243
3745
|
private async bootstrapPersistedIndexes(
|
|
2244
3746
|
indexMetadataSnapshot?: Array<CollectionIndexMetadata>,
|
|
2245
|
-
|
|
3747
|
+
adapter: HydrationPersistenceAdapter = this.persistence.adapter,
|
|
3748
|
+
): Promise<Set<string>> {
|
|
2246
3749
|
const collection = this.collection
|
|
2247
3750
|
if (!collection && !indexMetadataSnapshot) {
|
|
2248
|
-
return
|
|
3751
|
+
return new Set()
|
|
2249
3752
|
}
|
|
2250
3753
|
|
|
2251
3754
|
const indexMetadata =
|
|
2252
3755
|
indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? []
|
|
3756
|
+
const completedLocalIndexSignatures = new Set<string>()
|
|
2253
3757
|
for (const metadata of indexMetadata) {
|
|
2254
|
-
await this.
|
|
3758
|
+
if (await this.ensureLocalPersistedIndex(metadata, adapter)) {
|
|
3759
|
+
completedLocalIndexSignatures.add(metadata.signature)
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
return completedLocalIndexSignatures
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
private async requestCoordinatorPersistedIndexes(
|
|
3766
|
+
indexMetadata: Array<CollectionIndexMetadata>,
|
|
3767
|
+
completedLocalIndexSignatures: ReadonlySet<string>,
|
|
3768
|
+
): Promise<void> {
|
|
3769
|
+
for (const metadata of indexMetadata) {
|
|
3770
|
+
await this.requestCoordinatorPersistedIndex(
|
|
3771
|
+
metadata,
|
|
3772
|
+
completedLocalIndexSignatures.has(metadata.signature),
|
|
3773
|
+
)
|
|
2255
3774
|
}
|
|
2256
3775
|
}
|
|
2257
3776
|
|
|
@@ -2270,24 +3789,47 @@ class PersistedCollectionRuntime<
|
|
|
2270
3789
|
|
|
2271
3790
|
private async ensurePersistedIndex(
|
|
2272
3791
|
indexMetadata: CollectionIndexMetadata,
|
|
3792
|
+
adapter: HydrationPersistenceAdapter = this.persistence.adapter,
|
|
2273
3793
|
): Promise<void> {
|
|
3794
|
+
const completedLocally = await this.ensureLocalPersistedIndex(
|
|
3795
|
+
indexMetadata,
|
|
3796
|
+
adapter,
|
|
3797
|
+
)
|
|
3798
|
+
await this.requestCoordinatorPersistedIndex(indexMetadata, completedLocally)
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
private async ensureLocalPersistedIndex(
|
|
3802
|
+
indexMetadata: CollectionIndexMetadata,
|
|
3803
|
+
adapter: HydrationPersistenceAdapter,
|
|
3804
|
+
): Promise<boolean> {
|
|
2274
3805
|
const spec = this.buildPersistedIndexSpec(indexMetadata)
|
|
2275
3806
|
|
|
2276
3807
|
try {
|
|
2277
|
-
await
|
|
3808
|
+
await adapter.ensureIndex(
|
|
2278
3809
|
this.collectionId,
|
|
2279
3810
|
indexMetadata.signature,
|
|
2280
3811
|
spec,
|
|
2281
3812
|
)
|
|
3813
|
+
return true
|
|
2282
3814
|
} catch (error) {
|
|
2283
3815
|
console.warn(`Failed to ensure persisted index in adapter:`, error)
|
|
3816
|
+
return false
|
|
2284
3817
|
}
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
private async requestCoordinatorPersistedIndex(
|
|
3821
|
+
indexMetadata: CollectionIndexMetadata,
|
|
3822
|
+
completedLocally: boolean,
|
|
3823
|
+
): Promise<void> {
|
|
3824
|
+
const spec = this.buildPersistedIndexSpec(indexMetadata)
|
|
2285
3825
|
|
|
2286
3826
|
try {
|
|
2287
3827
|
await this.persistence.coordinator.requestEnsurePersistedIndex(
|
|
2288
3828
|
this.collectionId,
|
|
2289
3829
|
indexMetadata.signature,
|
|
2290
3830
|
spec,
|
|
3831
|
+
completedLocally ? this.persistence.adapter : undefined,
|
|
3832
|
+
completedLocally,
|
|
2291
3833
|
)
|
|
2292
3834
|
} catch (error) {
|
|
2293
3835
|
console.warn(
|
|
@@ -2321,73 +3863,406 @@ function createWrappedSyncConfig<
|
|
|
2321
3863
|
>(
|
|
2322
3864
|
sourceSyncConfig: SyncConfig<T, TKey>,
|
|
2323
3865
|
runtime: PersistedCollectionRuntime<T, TKey>,
|
|
3866
|
+
persistedReadiness?: PersistedReadinessTracker,
|
|
2324
3867
|
): SyncConfig<T, TKey> {
|
|
2325
3868
|
return {
|
|
2326
3869
|
...sourceSyncConfig,
|
|
2327
3870
|
sync: (params) => {
|
|
2328
3871
|
const transactionStack: Array<OpenSyncTransaction<T, TKey>> = []
|
|
3872
|
+
const pendingPublicationTransactions: Array<
|
|
3873
|
+
OpenSyncTransaction<T, TKey>
|
|
3874
|
+
> = []
|
|
2329
3875
|
const getOpenTransaction = () =>
|
|
2330
3876
|
transactionStack[transactionStack.length - 1]
|
|
3877
|
+
const settlePublicationAdmissionWaiters = (
|
|
3878
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
3879
|
+
error?: unknown,
|
|
3880
|
+
) => {
|
|
3881
|
+
const waiters = transaction.publicationAdmissionWaiters
|
|
3882
|
+
transaction.publicationAdmissionWaiters = undefined
|
|
3883
|
+
if (!waiters) return
|
|
3884
|
+
for (const waiter of waiters) {
|
|
3885
|
+
if (error === undefined) waiter.resolve()
|
|
3886
|
+
else waiter.reject(error)
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
const bindToCurrentHydration = (
|
|
3890
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
3891
|
+
) => {
|
|
3892
|
+
if (transaction.internal || !runtime.isHydratingNow()) return
|
|
3893
|
+
const hydrationSequence = runtime.getHydrationSequence()
|
|
3894
|
+
if (
|
|
3895
|
+
transaction.hydrationSequence !== undefined &&
|
|
3896
|
+
transaction.hydrationSequence !== hydrationSequence
|
|
3897
|
+
) {
|
|
3898
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
3899
|
+
`a persisted sync transaction cannot cross a hydration cycle`,
|
|
3900
|
+
)
|
|
3901
|
+
}
|
|
3902
|
+
transaction.queuedBecauseHydrating = true
|
|
3903
|
+
transaction.hydrationSequence = hydrationSequence
|
|
3904
|
+
transaction.hydrationContext = runtime.getActiveHydrationContext()
|
|
3905
|
+
}
|
|
3906
|
+
const assertHydrationSequenceCurrent = (
|
|
3907
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
3908
|
+
) => {
|
|
3909
|
+
if (
|
|
3910
|
+
transaction.hydrationSequence !== undefined &&
|
|
3911
|
+
transaction.hydrationSequence !== runtime.getHydrationSequence()
|
|
3912
|
+
) {
|
|
3913
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
3914
|
+
`a persisted sync transaction cannot cross a hydration cycle`,
|
|
3915
|
+
)
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
const markPendingMetadataDependency = (
|
|
3919
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
3920
|
+
) => {
|
|
3921
|
+
const openTransaction = getOpenTransaction()
|
|
3922
|
+
if (openTransaction && !openTransaction.internal) {
|
|
3923
|
+
transaction.hasDependentSuccessor = true
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
const removePendingPublicationTransaction = (
|
|
3927
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
3928
|
+
) => {
|
|
3929
|
+
const index = pendingPublicationTransactions.indexOf(transaction)
|
|
3930
|
+
if (index !== -1) pendingPublicationTransactions.splice(index, 1)
|
|
3931
|
+
}
|
|
3932
|
+
const settlePendingTransaction = (
|
|
3933
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
3934
|
+
) => {
|
|
3935
|
+
removePendingPublicationTransaction(transaction)
|
|
3936
|
+
}
|
|
3937
|
+
const settleRuntimeTransaction = (
|
|
3938
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
3939
|
+
applied: Promise<void>,
|
|
3940
|
+
) => {
|
|
3941
|
+
void applied.then(
|
|
3942
|
+
() => settlePendingTransaction(transaction),
|
|
3943
|
+
() => {
|
|
3944
|
+
settlePendingTransaction(transaction)
|
|
3945
|
+
},
|
|
3946
|
+
)
|
|
3947
|
+
}
|
|
3948
|
+
const getPendingRowMetadataWrite = (key: TKey) => {
|
|
3949
|
+
for (
|
|
3950
|
+
let index = pendingPublicationTransactions.length - 1;
|
|
3951
|
+
index >= 0;
|
|
3952
|
+
index--
|
|
3953
|
+
) {
|
|
3954
|
+
const transaction = pendingPublicationTransactions[index]!
|
|
3955
|
+
const write = transaction.rowMetadataWrites.get(key)
|
|
3956
|
+
if (write) {
|
|
3957
|
+
markPendingMetadataDependency(transaction)
|
|
3958
|
+
return { found: true, write }
|
|
3959
|
+
}
|
|
3960
|
+
if (transaction.truncate) {
|
|
3961
|
+
markPendingMetadataDependency(transaction)
|
|
3962
|
+
return {
|
|
3963
|
+
found: true,
|
|
3964
|
+
write: { type: `delete` as const },
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return { found: false as const }
|
|
3969
|
+
}
|
|
3970
|
+
const getPendingCollectionMetadataWrite = (key: string) => {
|
|
3971
|
+
for (
|
|
3972
|
+
let index = pendingPublicationTransactions.length - 1;
|
|
3973
|
+
index >= 0;
|
|
3974
|
+
index--
|
|
3975
|
+
) {
|
|
3976
|
+
const transaction = pendingPublicationTransactions[index]!
|
|
3977
|
+
const write = transaction.collectionMetadataWrites.get(key)
|
|
3978
|
+
if (write) {
|
|
3979
|
+
markPendingMetadataDependency(transaction)
|
|
3980
|
+
return { found: true, write }
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
return { found: false as const }
|
|
3984
|
+
}
|
|
2331
3985
|
let fullStartPromise: Promise<void> | null = null
|
|
3986
|
+
let sourceResultPromise: Promise<SyncConfigRes> | null = null
|
|
3987
|
+
let resolveSourceResultAssigned!: () => void
|
|
3988
|
+
const sourceResultAssigned = new Promise<void>((resolve) => {
|
|
3989
|
+
resolveSourceResultAssigned = resolve
|
|
3990
|
+
})
|
|
2332
3991
|
const startupState = { cleanedUp: false }
|
|
2333
|
-
const
|
|
3992
|
+
const isCleanedUp = () => startupState.cleanedUp
|
|
3993
|
+
type SubsetAcquisition = {
|
|
3994
|
+
forwarded: boolean
|
|
3995
|
+
cancelAdmissionWait?: () => void
|
|
3996
|
+
}
|
|
3997
|
+
const acquisitions = new Map<LoadSubsetOptions, SubsetAcquisition>()
|
|
3998
|
+
const waitForPublicationAdmission = (
|
|
3999
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
4000
|
+
options: LoadSubsetOptions,
|
|
4001
|
+
acquisition: SubsetAcquisition,
|
|
4002
|
+
): Promise<void> => {
|
|
4003
|
+
if (options.signal?.aborted) {
|
|
4004
|
+
return Promise.reject(
|
|
4005
|
+
options.signal.reason ?? new SyncTransactionAbortedError(),
|
|
4006
|
+
)
|
|
4007
|
+
}
|
|
4008
|
+
return new Promise<void>((resolve, reject) => {
|
|
4009
|
+
const waiters =
|
|
4010
|
+
transaction.publicationAdmissionWaiters ??
|
|
4011
|
+
(transaction.publicationAdmissionWaiters = new Set())
|
|
4012
|
+
let settled = false
|
|
4013
|
+
const settle = (action: () => void) => {
|
|
4014
|
+
if (settled) return
|
|
4015
|
+
settled = true
|
|
4016
|
+
waiters.delete(waiter)
|
|
4017
|
+
options.signal?.removeEventListener(`abort`, abort)
|
|
4018
|
+
acquisition.cancelAdmissionWait = undefined
|
|
4019
|
+
action()
|
|
4020
|
+
}
|
|
4021
|
+
const waiter = {
|
|
4022
|
+
resolve: () => settle(resolve),
|
|
4023
|
+
reject: (error: unknown) => settle(() => reject(error)),
|
|
4024
|
+
}
|
|
4025
|
+
const abort = () =>
|
|
4026
|
+
waiter.reject(
|
|
4027
|
+
options.signal?.reason ?? new SyncTransactionAbortedError(),
|
|
4028
|
+
)
|
|
4029
|
+
waiters.add(waiter)
|
|
4030
|
+
options.signal?.addEventListener(`abort`, abort, { once: true })
|
|
4031
|
+
acquisition.cancelAdmissionWait = waiter.resolve
|
|
4032
|
+
})
|
|
4033
|
+
}
|
|
4034
|
+
const getTerminalFailure = () => runtime.getCurrentTerminalFailure()
|
|
4035
|
+
const reportUpstreamError = (error: unknown) => {
|
|
4036
|
+
if (persistedReadiness?.getSnapshot().status !== `loading`) {
|
|
4037
|
+
runtime.reportSyncError(error)
|
|
4038
|
+
return
|
|
4039
|
+
}
|
|
4040
|
+
// An early network error must not stop the live-query graph before
|
|
4041
|
+
// the persisted baseline has published its rows.
|
|
4042
|
+
void (fullStartPromise ?? runtime.ensureStarted()).then(
|
|
4043
|
+
() => {
|
|
4044
|
+
if (!isCleanedUp() && !getTerminalFailure()) {
|
|
4045
|
+
runtime.reportSyncError(error)
|
|
4046
|
+
}
|
|
4047
|
+
},
|
|
4048
|
+
() => undefined,
|
|
4049
|
+
)
|
|
4050
|
+
}
|
|
4051
|
+
const createHandledRejection = (error: unknown): Promise<never> => {
|
|
4052
|
+
const rejected = Promise.reject(error)
|
|
4053
|
+
void rejected.catch(() => undefined)
|
|
4054
|
+
return rejected
|
|
4055
|
+
}
|
|
4056
|
+
const applyTransactionToCollection = (
|
|
4057
|
+
transaction: OpenSyncTransaction<T, TKey>,
|
|
4058
|
+
signal?: AbortSignal,
|
|
4059
|
+
): SyncAppliedReceipt => {
|
|
4060
|
+
if (transaction.applicationReceipt !== undefined) {
|
|
4061
|
+
return transaction.applicationReceipt
|
|
4062
|
+
}
|
|
4063
|
+
try {
|
|
4064
|
+
assertHydrationSequenceCurrent(transaction)
|
|
4065
|
+
for (const key of transaction.deferredHydrationMetadataDeleteKeys) {
|
|
4066
|
+
if (
|
|
4067
|
+
!transaction.hydrationContext?.suppliedRowKeys.has(key) &&
|
|
4068
|
+
!transaction.rowMetadataWrites.has(key)
|
|
4069
|
+
) {
|
|
4070
|
+
transaction.rowMetadataWrites.set(key, { type: `delete` })
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
transaction.deferredHydrationMetadataDeleteKeys.clear()
|
|
4074
|
+
|
|
4075
|
+
// A buffered source replay is part of the hydrate that owns it.
|
|
4076
|
+
// It must not wait for a mutation whose persistence is queued
|
|
4077
|
+
// behind that hydrate's apply mutex.
|
|
4078
|
+
params.begin(
|
|
4079
|
+
transaction.queuedBecauseHydrating
|
|
4080
|
+
? { immediate: true }
|
|
4081
|
+
: transaction.beginOptions,
|
|
4082
|
+
)
|
|
4083
|
+
if (transaction.truncate) params.truncate()
|
|
4084
|
+
for (const operation of transaction.operations) {
|
|
4085
|
+
if (operation.type === `delete`) {
|
|
4086
|
+
params.write({ type: `delete`, key: operation.key })
|
|
4087
|
+
} else {
|
|
4088
|
+
params.write({
|
|
4089
|
+
type: `update`,
|
|
4090
|
+
value: operation.value,
|
|
4091
|
+
metadata: operation.metadata,
|
|
4092
|
+
})
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
if (params.metadata) {
|
|
4096
|
+
for (const [key, write] of transaction.rowMetadataWrites) {
|
|
4097
|
+
if (write.type === `delete`) params.metadata.row.delete(key)
|
|
4098
|
+
else params.metadata.row.set(key, write.value)
|
|
4099
|
+
}
|
|
4100
|
+
for (const [key, write] of transaction.collectionMetadataWrites) {
|
|
4101
|
+
if (write.type === `delete`)
|
|
4102
|
+
params.metadata.collection.delete(key)
|
|
4103
|
+
else params.metadata.collection.set(key, write.value)
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
const applied = params.commit(signal)
|
|
4108
|
+
transaction.applicationReceipt = applied
|
|
4109
|
+
if (applied === true) {
|
|
4110
|
+
removePendingPublicationTransaction(transaction)
|
|
4111
|
+
} else {
|
|
4112
|
+
void applied.then(
|
|
4113
|
+
() => removePendingPublicationTransaction(transaction),
|
|
4114
|
+
() => removePendingPublicationTransaction(transaction),
|
|
4115
|
+
)
|
|
4116
|
+
}
|
|
4117
|
+
return applied
|
|
4118
|
+
} catch (error) {
|
|
4119
|
+
removePendingPublicationTransaction(transaction)
|
|
4120
|
+
throw error
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
2334
4123
|
runtime.setSyncControls({
|
|
2335
4124
|
begin: params.begin,
|
|
2336
4125
|
write: params.write as SyncControlFns<T, TKey>[`write`],
|
|
2337
4126
|
commit: params.commit,
|
|
2338
4127
|
truncate: params.truncate,
|
|
2339
4128
|
metadata: params.metadata ?? null,
|
|
4129
|
+
markError: params.markError,
|
|
2340
4130
|
})
|
|
2341
4131
|
runtime.setCollection(
|
|
2342
4132
|
params.collection as Collection<T, TKey, PersistedCollectionUtils>,
|
|
2343
4133
|
)
|
|
2344
4134
|
|
|
4135
|
+
const persistenceCapability: SyncPersistenceCapabilityV1<TKey> = {
|
|
4136
|
+
protocol: SYNC_PERSISTENCE_PROTOCOL,
|
|
4137
|
+
version: SYNC_PERSISTENCE_VERSION,
|
|
4138
|
+
hydrateBaseline: async () => {
|
|
4139
|
+
if (startupState.cleanedUp) return
|
|
4140
|
+
try {
|
|
4141
|
+
await runtime.ensureResumeBaselineHydrated()
|
|
4142
|
+
} catch (error) {
|
|
4143
|
+
throw runtime.reportSyncError(error)
|
|
4144
|
+
}
|
|
4145
|
+
},
|
|
4146
|
+
scanPersistedRows: (options) =>
|
|
4147
|
+
startupState.cleanedUp
|
|
4148
|
+
? Promise.resolve([])
|
|
4149
|
+
: runtime.scanPersistedRows(options),
|
|
4150
|
+
resumeSnapshot: {
|
|
4151
|
+
certify: async () => {
|
|
4152
|
+
if (startupState.cleanedUp) return
|
|
4153
|
+
try {
|
|
4154
|
+
await runtime.ensureResumeBaselineCertified()
|
|
4155
|
+
} catch (error) {
|
|
4156
|
+
throw runtime.reportSyncError(error)
|
|
4157
|
+
}
|
|
4158
|
+
},
|
|
4159
|
+
getKeySetEvidence: () =>
|
|
4160
|
+
startupState.cleanedUp ? undefined : runtime.getKeySetEvidence(),
|
|
4161
|
+
expectCurrentCommit: () => {
|
|
4162
|
+
if (startupState.cleanedUp) return
|
|
4163
|
+
const openTransaction = getOpenTransaction()
|
|
4164
|
+
if (!openTransaction) {
|
|
4165
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
4166
|
+
`resumeSnapshot.expectCurrentCommit must be called within an open sync transaction`,
|
|
4167
|
+
)
|
|
4168
|
+
}
|
|
4169
|
+
openTransaction.expectedResumeGenerationOwner =
|
|
4170
|
+
runtime.getResumeGenerationOwner()
|
|
4171
|
+
},
|
|
4172
|
+
},
|
|
4173
|
+
}
|
|
4174
|
+
|
|
2345
4175
|
const wrappedParams = {
|
|
2346
4176
|
...params,
|
|
4177
|
+
markError: reportUpstreamError,
|
|
2347
4178
|
markReady: () => {
|
|
2348
|
-
if (startupState.cleanedUp) return
|
|
4179
|
+
if (startupState.cleanedUp || getTerminalFailure()) return
|
|
2349
4180
|
void (fullStartPromise ?? runtime.ensureStarted())
|
|
2350
|
-
.then(() => {
|
|
2351
|
-
if (
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
)
|
|
4181
|
+
.then(async () => {
|
|
4182
|
+
if (isCleanedUp() || getTerminalFailure()) return
|
|
4183
|
+
await sourceResultAssigned
|
|
4184
|
+
try {
|
|
4185
|
+
await sourceResultPromise
|
|
4186
|
+
} catch (error) {
|
|
4187
|
+
reportUpstreamError(error)
|
|
4188
|
+
return
|
|
4189
|
+
}
|
|
4190
|
+
if (isCleanedUp() || getTerminalFailure()) return
|
|
2360
4191
|
params.markReady()
|
|
2361
4192
|
})
|
|
4193
|
+
.catch(() => undefined)
|
|
2362
4194
|
},
|
|
2363
4195
|
begin: (options?: { immediate?: boolean }) => {
|
|
2364
|
-
if (startupState.cleanedUp) return
|
|
4196
|
+
if (startupState.cleanedUp) return undefined
|
|
4197
|
+
const terminalFailure = getTerminalFailure()
|
|
4198
|
+
const internal = runtime.isApplyingInternally()
|
|
2365
4199
|
const transaction: OpenSyncTransaction<T, TKey> = {
|
|
2366
4200
|
operations: [],
|
|
4201
|
+
partialUpdateOperationIndexes: new Set(),
|
|
2367
4202
|
rowMetadataWrites: new Map(),
|
|
2368
4203
|
collectionMetadataWrites: new Map(),
|
|
4204
|
+
deferredHydrationMetadataDeleteKeys: new Set(),
|
|
2369
4205
|
truncate: false,
|
|
2370
|
-
internal
|
|
4206
|
+
internal,
|
|
4207
|
+
lifecycleGeneration: runtime.getLifecycleGeneration(),
|
|
4208
|
+
beginOptions: options,
|
|
4209
|
+
operationKeys: new Set(),
|
|
4210
|
+
hasDependentSuccessor: false,
|
|
2371
4211
|
queuedBecauseHydrating:
|
|
2372
|
-
|
|
4212
|
+
terminalFailure === undefined &&
|
|
4213
|
+
!internal &&
|
|
4214
|
+
runtime.isHydratingNow(),
|
|
4215
|
+
hydrationSequence:
|
|
4216
|
+
terminalFailure === undefined && !internal
|
|
4217
|
+
? runtime.getHydrationSequence()
|
|
4218
|
+
: undefined,
|
|
4219
|
+
hydrationContext:
|
|
4220
|
+
terminalFailure === undefined &&
|
|
4221
|
+
!internal &&
|
|
4222
|
+
runtime.isHydratingNow()
|
|
4223
|
+
? runtime.getActiveHydrationContext()
|
|
4224
|
+
: undefined,
|
|
4225
|
+
...(terminalFailure === undefined ? {} : { terminalFailure }),
|
|
2373
4226
|
}
|
|
2374
4227
|
transactionStack.push(transaction)
|
|
2375
|
-
|
|
2376
|
-
if (!transaction.queuedBecauseHydrating) {
|
|
2377
|
-
params.begin(options)
|
|
2378
|
-
}
|
|
2379
4228
|
},
|
|
2380
4229
|
write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => {
|
|
2381
4230
|
if (startupState.cleanedUp) return
|
|
2382
|
-
const normalization = runtime.normalizeSyncWriteMessage(message)
|
|
2383
4231
|
const openTransaction = getOpenTransaction()
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
4232
|
+
const terminalFailure =
|
|
4233
|
+
openTransaction?.terminalFailure ?? getTerminalFailure()
|
|
4234
|
+
if (terminalFailure) {
|
|
4235
|
+
if (openTransaction)
|
|
4236
|
+
openTransaction.terminalFailure = terminalFailure
|
|
2387
4237
|
return
|
|
2388
4238
|
}
|
|
4239
|
+
if (!openTransaction) {
|
|
4240
|
+
throw new NoPendingSyncTransactionWriteError()
|
|
4241
|
+
}
|
|
4242
|
+
bindToCurrentHydration(openTransaction)
|
|
4243
|
+
const normalization = runtime.normalizeSyncWriteMessage(message)
|
|
4244
|
+
|
|
4245
|
+
if (
|
|
4246
|
+
message.type === `update` &&
|
|
4247
|
+
sourceSyncConfig.rowUpdateMode !== `full`
|
|
4248
|
+
) {
|
|
4249
|
+
if (!openTransaction.internal) {
|
|
4250
|
+
openTransaction.partialUpdateOperationIndexes.add(
|
|
4251
|
+
openTransaction.operations.length,
|
|
4252
|
+
)
|
|
4253
|
+
}
|
|
4254
|
+
for (const pending of pendingPublicationTransactions) {
|
|
4255
|
+
if (
|
|
4256
|
+
pending.truncate ||
|
|
4257
|
+
pending.operationKeys.has(normalization.operation.key)
|
|
4258
|
+
) {
|
|
4259
|
+
markPendingMetadataDependency(pending)
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
2389
4263
|
|
|
2390
4264
|
openTransaction.operations.push(normalization.operation)
|
|
4265
|
+
openTransaction.operationKeys.add(normalization.operation.key)
|
|
2391
4266
|
if (normalization.operation.type === `delete`) {
|
|
2392
4267
|
openTransaction.rowMetadataWrites.set(normalization.operation.key, {
|
|
2393
4268
|
type: `delete`,
|
|
@@ -2399,7 +4274,11 @@ function createWrappedSyncConfig<
|
|
|
2399
4274
|
// Reset stale metadata for a fresh insert, but don't clobber an
|
|
2400
4275
|
// explicit metadata write already queued for this key in the same
|
|
2401
4276
|
// transaction (e.g. query reconcile stamps owners, then inserts).
|
|
2402
|
-
if (
|
|
4277
|
+
if (openTransaction.queuedBecauseHydrating) {
|
|
4278
|
+
openTransaction.deferredHydrationMetadataDeleteKeys.add(
|
|
4279
|
+
normalization.operation.key,
|
|
4280
|
+
)
|
|
4281
|
+
} else if (
|
|
2403
4282
|
!openTransaction.rowMetadataWrites.has(
|
|
2404
4283
|
normalization.operation.key,
|
|
2405
4284
|
)
|
|
@@ -2417,17 +4296,11 @@ function createWrappedSyncConfig<
|
|
|
2417
4296
|
value: normalization.operation.metadata,
|
|
2418
4297
|
})
|
|
2419
4298
|
}
|
|
2420
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2421
|
-
params.write(normalization.forwardMessage)
|
|
2422
|
-
}
|
|
2423
4299
|
},
|
|
2424
4300
|
metadata: params.metadata
|
|
2425
4301
|
? {
|
|
4302
|
+
persistence: persistenceCapability,
|
|
2426
4303
|
row: {
|
|
2427
|
-
whenHydrated: () =>
|
|
2428
|
-
startupState.cleanedUp
|
|
2429
|
-
? Promise.resolve()
|
|
2430
|
-
: runtime.ensureResumeBaselineHydrated(),
|
|
2431
4304
|
get: (key: TKey) => {
|
|
2432
4305
|
if (startupState.cleanedUp) return undefined
|
|
2433
4306
|
const openTransaction = getOpenTransaction()
|
|
@@ -2441,15 +4314,19 @@ function createWrappedSyncConfig<
|
|
|
2441
4314
|
if (openTransaction?.truncate) {
|
|
2442
4315
|
return undefined
|
|
2443
4316
|
}
|
|
4317
|
+
const pending = getPendingRowMetadataWrite(key)
|
|
4318
|
+
if (pending.found) {
|
|
4319
|
+
return pending.write.type === `delete`
|
|
4320
|
+
? undefined
|
|
4321
|
+
: pending.write.value
|
|
4322
|
+
}
|
|
2444
4323
|
return params.metadata!.row.get(key)
|
|
2445
4324
|
},
|
|
2446
|
-
scanPersisted: (options?: PersistedRowScanOptions) =>
|
|
2447
|
-
startupState.cleanedUp
|
|
2448
|
-
? Promise.resolve([])
|
|
2449
|
-
: runtime.scanPersistedRows(options),
|
|
2450
4325
|
set: (key: TKey, value: unknown) => {
|
|
2451
4326
|
if (startupState.cleanedUp) return
|
|
2452
4327
|
const openTransaction = getOpenTransaction()
|
|
4328
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4329
|
+
return
|
|
2453
4330
|
if (!openTransaction) {
|
|
2454
4331
|
throw new InvalidPersistedCollectionConfigError(
|
|
2455
4332
|
`metadata.row.set must be called within an open sync transaction`,
|
|
@@ -2459,13 +4336,12 @@ function createWrappedSyncConfig<
|
|
|
2459
4336
|
type: `set`,
|
|
2460
4337
|
value,
|
|
2461
4338
|
})
|
|
2462
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2463
|
-
params.metadata!.row.set(key, value)
|
|
2464
|
-
}
|
|
2465
4339
|
},
|
|
2466
4340
|
delete: (key: TKey) => {
|
|
2467
4341
|
if (startupState.cleanedUp) return
|
|
2468
4342
|
const openTransaction = getOpenTransaction()
|
|
4343
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4344
|
+
return
|
|
2469
4345
|
if (!openTransaction) {
|
|
2470
4346
|
throw new InvalidPersistedCollectionConfigError(
|
|
2471
4347
|
`metadata.row.delete must be called within an open sync transaction`,
|
|
@@ -2474,9 +4350,6 @@ function createWrappedSyncConfig<
|
|
|
2474
4350
|
openTransaction.rowMetadataWrites.set(key, {
|
|
2475
4351
|
type: `delete`,
|
|
2476
4352
|
})
|
|
2477
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2478
|
-
params.metadata!.row.delete(key)
|
|
2479
|
-
}
|
|
2480
4353
|
},
|
|
2481
4354
|
},
|
|
2482
4355
|
collection: {
|
|
@@ -2490,11 +4363,19 @@ function createWrappedSyncConfig<
|
|
|
2490
4363
|
? undefined
|
|
2491
4364
|
: pendingWrite.value
|
|
2492
4365
|
}
|
|
4366
|
+
const pending = getPendingCollectionMetadataWrite(key)
|
|
4367
|
+
if (pending.found) {
|
|
4368
|
+
return pending.write.type === `delete`
|
|
4369
|
+
? undefined
|
|
4370
|
+
: pending.write.value
|
|
4371
|
+
}
|
|
2493
4372
|
return params.metadata!.collection.get(key)
|
|
2494
4373
|
},
|
|
2495
4374
|
set: (key: string, value: unknown) => {
|
|
2496
4375
|
if (startupState.cleanedUp) return
|
|
2497
4376
|
const openTransaction = getOpenTransaction()
|
|
4377
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4378
|
+
return
|
|
2498
4379
|
if (!openTransaction) {
|
|
2499
4380
|
throw new InvalidPersistedCollectionConfigError(
|
|
2500
4381
|
`metadata.collection.set must be called within an open sync transaction`,
|
|
@@ -2504,13 +4385,12 @@ function createWrappedSyncConfig<
|
|
|
2504
4385
|
type: `set`,
|
|
2505
4386
|
value,
|
|
2506
4387
|
})
|
|
2507
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2508
|
-
params.metadata!.collection.set(key, value)
|
|
2509
|
-
}
|
|
2510
4388
|
},
|
|
2511
4389
|
delete: (key: string) => {
|
|
2512
4390
|
if (startupState.cleanedUp) return
|
|
2513
4391
|
const openTransaction = getOpenTransaction()
|
|
4392
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4393
|
+
return
|
|
2514
4394
|
if (!openTransaction) {
|
|
2515
4395
|
throw new InvalidPersistedCollectionConfigError(
|
|
2516
4396
|
`metadata.collection.delete must be called within an open sync transaction`,
|
|
@@ -2519,9 +4399,6 @@ function createWrappedSyncConfig<
|
|
|
2519
4399
|
openTransaction.collectionMetadataWrites.set(key, {
|
|
2520
4400
|
type: `delete`,
|
|
2521
4401
|
})
|
|
2522
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2523
|
-
params.metadata!.collection.delete(key)
|
|
2524
|
-
}
|
|
2525
4402
|
},
|
|
2526
4403
|
list: (prefix?: string) => {
|
|
2527
4404
|
if (startupState.cleanedUp) return []
|
|
@@ -2530,6 +4407,28 @@ function createWrappedSyncConfig<
|
|
|
2530
4407
|
.metadata!.collection.list()
|
|
2531
4408
|
.map(({ key, value }) => [key, value]),
|
|
2532
4409
|
)
|
|
4410
|
+
const pendingOwners = new Map<
|
|
4411
|
+
string,
|
|
4412
|
+
OpenSyncTransaction<T, TKey>
|
|
4413
|
+
>()
|
|
4414
|
+
for (const transaction of pendingPublicationTransactions) {
|
|
4415
|
+
for (const [
|
|
4416
|
+
key,
|
|
4417
|
+
metadataWrite,
|
|
4418
|
+
] of transaction.collectionMetadataWrites) {
|
|
4419
|
+
if (!prefix || key.startsWith(prefix)) {
|
|
4420
|
+
pendingOwners.set(key, transaction)
|
|
4421
|
+
}
|
|
4422
|
+
if (metadataWrite.type === `delete`) {
|
|
4423
|
+
merged.delete(key)
|
|
4424
|
+
} else {
|
|
4425
|
+
merged.set(key, metadataWrite.value)
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
for (const owner of new Set(pendingOwners.values())) {
|
|
4430
|
+
markPendingMetadataDependency(owner)
|
|
4431
|
+
}
|
|
2533
4432
|
const openTransaction = getOpenTransaction()
|
|
2534
4433
|
if (openTransaction) {
|
|
2535
4434
|
for (const [
|
|
@@ -2556,36 +4455,113 @@ function createWrappedSyncConfig<
|
|
|
2556
4455
|
: undefined,
|
|
2557
4456
|
truncate: () => {
|
|
2558
4457
|
if (startupState.cleanedUp) return
|
|
4458
|
+
runtime.noteSourceTruncate()
|
|
2559
4459
|
const openTransaction = getOpenTransaction()
|
|
4460
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure()) return
|
|
2560
4461
|
if (!openTransaction) {
|
|
2561
|
-
|
|
2562
|
-
return
|
|
4462
|
+
throw new NoPendingSyncTransactionWriteError()
|
|
2563
4463
|
}
|
|
2564
4464
|
|
|
2565
4465
|
openTransaction.operations = []
|
|
4466
|
+
openTransaction.operationKeys.clear()
|
|
4467
|
+
openTransaction.partialUpdateOperationIndexes.clear()
|
|
2566
4468
|
openTransaction.rowMetadataWrites.clear()
|
|
4469
|
+
openTransaction.deferredHydrationMetadataDeleteKeys.clear()
|
|
2567
4470
|
// Intentionally preserve collectionMetadataWrites across truncate.
|
|
2568
4471
|
// Callers (for example electric resume/reset handling) may stage
|
|
2569
4472
|
// collection-scoped metadata before truncating row data, and those
|
|
2570
4473
|
// writes must commit atomically with the truncate transaction.
|
|
2571
4474
|
openTransaction.truncate = true
|
|
2572
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2573
|
-
params.truncate()
|
|
2574
|
-
}
|
|
2575
4475
|
},
|
|
2576
4476
|
commit: (signal?: AbortSignal) => {
|
|
2577
4477
|
if (startupState.cleanedUp) return true
|
|
2578
4478
|
const openTransaction = transactionStack.pop()
|
|
4479
|
+
const terminalFailure =
|
|
4480
|
+
openTransaction?.terminalFailure ?? getTerminalFailure()
|
|
4481
|
+
if (terminalFailure) {
|
|
4482
|
+
if (openTransaction) {
|
|
4483
|
+
settlePendingTransaction(openTransaction)
|
|
4484
|
+
settlePublicationAdmissionWaiters(
|
|
4485
|
+
openTransaction,
|
|
4486
|
+
terminalFailure.error,
|
|
4487
|
+
)
|
|
4488
|
+
}
|
|
4489
|
+
return createHandledRejection(terminalFailure.error)
|
|
4490
|
+
}
|
|
2579
4491
|
if (!openTransaction) {
|
|
2580
|
-
|
|
4492
|
+
throw new NoPendingSyncTransactionCommitError()
|
|
2581
4493
|
}
|
|
2582
4494
|
|
|
2583
|
-
if (openTransaction.
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
4495
|
+
if (openTransaction.internal) {
|
|
4496
|
+
return applyTransactionToCollection(openTransaction, signal)
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
if (signal?.aborted) {
|
|
4500
|
+
settlePendingTransaction(openTransaction)
|
|
4501
|
+
const error = new SyncTransactionAbortedError()
|
|
4502
|
+
settlePublicationAdmissionWaiters(openTransaction, error)
|
|
4503
|
+
return createHandledRejection(error)
|
|
4504
|
+
}
|
|
4505
|
+
try {
|
|
4506
|
+
bindToCurrentHydration(openTransaction)
|
|
4507
|
+
assertHydrationSequenceCurrent(openTransaction)
|
|
4508
|
+
} catch (error) {
|
|
4509
|
+
settlePublicationAdmissionWaiters(openTransaction, error)
|
|
4510
|
+
throw error
|
|
4511
|
+
}
|
|
4512
|
+
const transaction = {
|
|
4513
|
+
operations: openTransaction.operations,
|
|
4514
|
+
partialUpdateOperationIndexes:
|
|
4515
|
+
openTransaction.partialUpdateOperationIndexes,
|
|
4516
|
+
rowMetadataWrites: openTransaction.rowMetadataWrites,
|
|
4517
|
+
collectionMetadataWrites: openTransaction.collectionMetadataWrites,
|
|
4518
|
+
deferredHydrationMetadataDeleteKeys:
|
|
4519
|
+
openTransaction.deferredHydrationMetadataDeleteKeys,
|
|
4520
|
+
hydrationContext: openTransaction.hydrationContext,
|
|
4521
|
+
hydrationSequence: openTransaction.hydrationSequence,
|
|
4522
|
+
truncate: openTransaction.truncate,
|
|
4523
|
+
internal: false,
|
|
4524
|
+
lifecycleGeneration: openTransaction.lifecycleGeneration,
|
|
4525
|
+
beginOptions: openTransaction.beginOptions,
|
|
4526
|
+
expectedResumeGenerationOwner:
|
|
4527
|
+
openTransaction.expectedResumeGenerationOwner,
|
|
4528
|
+
signal,
|
|
4529
|
+
prependHydrationRows: (
|
|
4530
|
+
rows: Array<{ key: TKey; value: T; metadata?: unknown }>,
|
|
4531
|
+
) => {
|
|
4532
|
+
const baselineOperations: Array<
|
|
4533
|
+
NormalizedSyncOperation<T, TKey>
|
|
4534
|
+
> = rows.map(({ key, value, metadata }) => ({
|
|
4535
|
+
type: `update`,
|
|
4536
|
+
key,
|
|
4537
|
+
value,
|
|
4538
|
+
metadata: metadata as Record<string, unknown> | undefined,
|
|
4539
|
+
}))
|
|
4540
|
+
openTransaction.operations = baselineOperations.concat(
|
|
4541
|
+
openTransaction.operations,
|
|
4542
|
+
)
|
|
4543
|
+
for (const { key, metadata } of rows) {
|
|
4544
|
+
if (
|
|
4545
|
+
metadata !== undefined &&
|
|
4546
|
+
!openTransaction.rowMetadataWrites.has(key)
|
|
4547
|
+
) {
|
|
4548
|
+
openTransaction.rowMetadataWrites.set(key, {
|
|
4549
|
+
type: `set`,
|
|
4550
|
+
value: metadata,
|
|
4551
|
+
})
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
},
|
|
4555
|
+
applyToCollection: () =>
|
|
4556
|
+
applyTransactionToCollection(openTransaction, signal),
|
|
4557
|
+
shouldFailStopOnAbort: () => openTransaction.hasDependentSuccessor,
|
|
4558
|
+
}
|
|
4559
|
+
openTransaction.signal = signal
|
|
4560
|
+
pendingPublicationTransactions.push(openTransaction)
|
|
4561
|
+
if (
|
|
4562
|
+
openTransaction.queuedBecauseHydrating &&
|
|
4563
|
+
runtime.isHydratingNow()
|
|
4564
|
+
) {
|
|
2589
4565
|
let resolveApplied!: () => void
|
|
2590
4566
|
let rejectApplied!: (error: unknown) => void
|
|
2591
4567
|
const applied = new Promise<void>((resolve, reject) => {
|
|
@@ -2594,75 +4570,142 @@ function createWrappedSyncConfig<
|
|
|
2594
4570
|
})
|
|
2595
4571
|
void applied.catch(() => undefined)
|
|
2596
4572
|
runtime.queueHydrationBufferedTransaction({
|
|
2597
|
-
|
|
2598
|
-
rowMetadataWrites: openTransaction.rowMetadataWrites,
|
|
2599
|
-
collectionMetadataWrites:
|
|
2600
|
-
openTransaction.collectionMetadataWrites,
|
|
2601
|
-
truncate: openTransaction.truncate,
|
|
2602
|
-
internal: openTransaction.internal,
|
|
2603
|
-
signal,
|
|
4573
|
+
...transaction,
|
|
2604
4574
|
resolveApplied,
|
|
2605
4575
|
rejectApplied,
|
|
2606
4576
|
})
|
|
4577
|
+
settlePublicationAdmissionWaiters(openTransaction)
|
|
4578
|
+
settleRuntimeTransaction(openTransaction, applied)
|
|
2607
4579
|
return applied
|
|
2608
4580
|
}
|
|
2609
4581
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
rowMetadataWrites: openTransaction.rowMetadataWrites,
|
|
2617
|
-
collectionMetadataWrites:
|
|
2618
|
-
openTransaction.collectionMetadataWrites,
|
|
2619
|
-
truncate: openTransaction.truncate,
|
|
2620
|
-
internal: false,
|
|
2621
|
-
})
|
|
2622
|
-
}
|
|
2623
|
-
const persisted = persistAfterApplication()
|
|
2624
|
-
void persisted.catch(() => undefined)
|
|
2625
|
-
return persisted
|
|
4582
|
+
let applied: Promise<void>
|
|
4583
|
+
try {
|
|
4584
|
+
applied = runtime.applyHydrationBufferedTransaction(transaction)
|
|
4585
|
+
} catch (error) {
|
|
4586
|
+
settlePublicationAdmissionWaiters(openTransaction, error)
|
|
4587
|
+
throw error
|
|
2626
4588
|
}
|
|
4589
|
+
settlePublicationAdmissionWaiters(openTransaction)
|
|
4590
|
+
settleRuntimeTransaction(openTransaction, applied)
|
|
2627
4591
|
return applied
|
|
2628
4592
|
},
|
|
2629
4593
|
}
|
|
2630
4594
|
|
|
2631
4595
|
let sourceResult: SyncConfigRes = {}
|
|
4596
|
+
let sourceResultSettled = false
|
|
4597
|
+
let activeSourceSyncEntry: Promise<void> | undefined
|
|
2632
4598
|
fullStartPromise = runtime.ensureStarted()
|
|
2633
|
-
|
|
4599
|
+
// Startup can fail before the source reaches markReady or loadSubset,
|
|
4600
|
+
// which are the two eventual consumers of this outer adopting promise.
|
|
4601
|
+
// The runtime-owned inner promise still installs and reports the exact
|
|
4602
|
+
// terminal failure; this observer only prevents the unused outer wrapper
|
|
4603
|
+
// from becoming an unhandled rejection on that fail-stop path.
|
|
4604
|
+
void fullStartPromise.catch(() => undefined)
|
|
4605
|
+
sourceResultPromise = (async () => {
|
|
2634
4606
|
await runtime.ensureStartupMetadataLoaded()
|
|
2635
4607
|
|
|
2636
4608
|
if (startupState.cleanedUp) {
|
|
4609
|
+
sourceResultSettled = true
|
|
2637
4610
|
return sourceResult
|
|
2638
4611
|
}
|
|
2639
4612
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
4613
|
+
let settleSourceSyncEntry!: () => void
|
|
4614
|
+
activeSourceSyncEntry = new Promise<void>((resolve) => {
|
|
4615
|
+
settleSourceSyncEntry = resolve
|
|
4616
|
+
})
|
|
4617
|
+
try {
|
|
4618
|
+
sourceResult = normalizeSyncFnResult(
|
|
4619
|
+
sourceSyncConfig.sync(wrappedParams),
|
|
4620
|
+
)
|
|
4621
|
+
} finally {
|
|
4622
|
+
activeSourceSyncEntry = undefined
|
|
4623
|
+
settleSourceSyncEntry()
|
|
4624
|
+
}
|
|
4625
|
+
// Cleanup can reenter while the source sync function is still active.
|
|
4626
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
4627
|
+
if (!startupState.cleanedUp && sourceResult.loadSubset) {
|
|
4628
|
+
const loadSubset = async (options: TransportedLoadSubsetOptions) => {
|
|
4629
|
+
if (startupState.cleanedUp) {
|
|
4630
|
+
throw new Error(`persisted sync source is no longer active`)
|
|
4631
|
+
}
|
|
4632
|
+
await sourceResult.loadSubset?.(
|
|
4633
|
+
options as unknown as LoadSubsetOptions,
|
|
4634
|
+
)
|
|
4635
|
+
}
|
|
4636
|
+
runtime.registerRemoteSubsetOwner(
|
|
4637
|
+
Object.assign(loadSubset, {
|
|
4638
|
+
unloadSubset: (options: TransportedLoadSubsetOptions) =>
|
|
4639
|
+
sourceResult.unloadSubset?.(
|
|
4640
|
+
options as unknown as LoadSubsetOptions,
|
|
4641
|
+
),
|
|
4642
|
+
onError: reportUpstreamError,
|
|
4643
|
+
}),
|
|
4644
|
+
)
|
|
4645
|
+
}
|
|
4646
|
+
sourceResultSettled = true
|
|
2643
4647
|
return sourceResult
|
|
2644
4648
|
})()
|
|
4649
|
+
resolveSourceResultAssigned()
|
|
4650
|
+
void sourceResultPromise.catch((error) => {
|
|
4651
|
+
reportUpstreamError(error)
|
|
4652
|
+
})
|
|
2645
4653
|
|
|
2646
4654
|
return {
|
|
2647
|
-
cleanup: () => {
|
|
4655
|
+
cleanup: async () => {
|
|
2648
4656
|
startupState.cleanedUp = true
|
|
4657
|
+
// The optional source starts behind persistence metadata. If it
|
|
4658
|
+
// reenters cleanup before returning, wait for that exact synchronous
|
|
4659
|
+
// entry to publish its cleanup callback before sampling the result.
|
|
4660
|
+
const sourceSyncEntry = activeSourceSyncEntry
|
|
4661
|
+
const cleanupError = new SyncTransactionAbortedError()
|
|
4662
|
+
for (const acquisition of acquisitions.values()) {
|
|
4663
|
+
acquisition.cancelAdmissionWait?.()
|
|
4664
|
+
}
|
|
2649
4665
|
acquisitions.clear()
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
4666
|
+
pendingPublicationTransactions.length = 0
|
|
4667
|
+
for (const transaction of transactionStack) {
|
|
4668
|
+
settlePublicationAdmissionWaiters(transaction, cleanupError)
|
|
4669
|
+
}
|
|
4670
|
+
transactionStack.length = 0
|
|
4671
|
+
let sourceCleanup: Promise<void>
|
|
4672
|
+
try {
|
|
4673
|
+
sourceCleanup = sourceSyncEntry
|
|
4674
|
+
? sourceSyncEntry.then(() => sourceResult.cleanup?.())
|
|
4675
|
+
: Promise.resolve(sourceResult.cleanup?.())
|
|
4676
|
+
} catch (error) {
|
|
4677
|
+
sourceCleanup = Promise.reject(error)
|
|
4678
|
+
}
|
|
4679
|
+
let runtimeCleanup: Promise<void>
|
|
4680
|
+
try {
|
|
4681
|
+
runtimeCleanup = runtime.cleanup()
|
|
4682
|
+
} catch (error) {
|
|
4683
|
+
runtimeCleanup = Promise.reject(error)
|
|
4684
|
+
}
|
|
4685
|
+
const [sourceOutcome, runtimeOutcome] = await Promise.allSettled([
|
|
4686
|
+
sourceCleanup,
|
|
4687
|
+
runtimeCleanup,
|
|
4688
|
+
])
|
|
2659
4689
|
if (
|
|
2660
|
-
|
|
2661
|
-
|
|
4690
|
+
sourceOutcome.status === `rejected` &&
|
|
4691
|
+
runtimeOutcome.status === `rejected`
|
|
2662
4692
|
) {
|
|
2663
|
-
|
|
4693
|
+
throw new AggregateError(
|
|
4694
|
+
[sourceOutcome.reason, runtimeOutcome.reason],
|
|
4695
|
+
`Source cleanup and persistence runtime teardown both failed`,
|
|
4696
|
+
{ cause: sourceOutcome.reason },
|
|
4697
|
+
)
|
|
2664
4698
|
}
|
|
2665
|
-
|
|
4699
|
+
if (sourceOutcome.status === `rejected`) throw sourceOutcome.reason
|
|
4700
|
+
if (runtimeOutcome.status === `rejected`) throw runtimeOutcome.reason
|
|
4701
|
+
},
|
|
4702
|
+
loadSubset: (options: LoadSubsetOptions): true | Promise<void> => {
|
|
4703
|
+
const acquisition = { forwarded: false }
|
|
4704
|
+
acquisitions.set(options, acquisition)
|
|
4705
|
+
const forwardUpstream = (
|
|
4706
|
+
resolvedSourceResult: SyncConfigRes,
|
|
4707
|
+
loadOptions: LoadSubsetOptions,
|
|
4708
|
+
): true | Promise<void> => {
|
|
2666
4709
|
// Hydration is another async boundary. A release before this
|
|
2667
4710
|
// point owns no upstream lease and must not start one later.
|
|
2668
4711
|
if (
|
|
@@ -2681,10 +4724,60 @@ function createWrappedSyncConfig<
|
|
|
2681
4724
|
acquisition.forwarded = false
|
|
2682
4725
|
throw error
|
|
2683
4726
|
}
|
|
2684
|
-
}
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
const openTransaction = getOpenTransaction()
|
|
4730
|
+
const needsPublicationAdmission =
|
|
4731
|
+
openTransaction &&
|
|
4732
|
+
!openTransaction.internal &&
|
|
4733
|
+
openTransaction.beginOptions?.immediate
|
|
4734
|
+
const hydrated =
|
|
4735
|
+
sourceResultSettled && !needsPublicationAdmission
|
|
4736
|
+
? runtime.loadHydratedSubset(options, (loadOptions) =>
|
|
4737
|
+
forwardUpstream(sourceResult, loadOptions),
|
|
4738
|
+
)
|
|
4739
|
+
: undefined
|
|
4740
|
+
if (hydrated !== undefined) {
|
|
4741
|
+
return hydrated
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
return (async () => {
|
|
4745
|
+
await fullStartPromise
|
|
4746
|
+
const resolvedSourceResult = await sourceResultPromise
|
|
4747
|
+
if (
|
|
4748
|
+
startupState.cleanedUp ||
|
|
4749
|
+
acquisitions.get(options) !== acquisition
|
|
4750
|
+
) {
|
|
4751
|
+
return
|
|
4752
|
+
}
|
|
4753
|
+
const currentTransaction = getOpenTransaction()
|
|
4754
|
+
// An immediate source transaction reserves its FIFO turn before
|
|
4755
|
+
// subset hydration advances the generation it was built against.
|
|
4756
|
+
if (
|
|
4757
|
+
currentTransaction &&
|
|
4758
|
+
!currentTransaction.internal &&
|
|
4759
|
+
currentTransaction.beginOptions?.immediate
|
|
4760
|
+
) {
|
|
4761
|
+
await waitForPublicationAdmission(
|
|
4762
|
+
currentTransaction,
|
|
4763
|
+
options,
|
|
4764
|
+
acquisition,
|
|
4765
|
+
)
|
|
4766
|
+
}
|
|
4767
|
+
if (
|
|
4768
|
+
options.signal?.aborted ||
|
|
4769
|
+
acquisitions.get(options) !== acquisition
|
|
4770
|
+
) {
|
|
4771
|
+
return
|
|
4772
|
+
}
|
|
4773
|
+
return runtime.loadSubset(options, (loadOptions) =>
|
|
4774
|
+
forwardUpstream(resolvedSourceResult, loadOptions),
|
|
4775
|
+
)
|
|
4776
|
+
})()
|
|
2685
4777
|
},
|
|
2686
4778
|
unloadSubset: (options: LoadSubsetOptions) => {
|
|
2687
4779
|
const acquisition = acquisitions.get(options)
|
|
4780
|
+
acquisition?.cancelAdmissionWait?.()
|
|
2688
4781
|
acquisitions.delete(options)
|
|
2689
4782
|
runtime.unloadSubset(
|
|
2690
4783
|
options,
|
|
@@ -2708,6 +4801,7 @@ function createLoopbackSyncConfig<
|
|
|
2708
4801
|
commit: params.commit,
|
|
2709
4802
|
truncate: params.truncate,
|
|
2710
4803
|
metadata: params.metadata ?? null,
|
|
4804
|
+
markError: params.markError,
|
|
2711
4805
|
})
|
|
2712
4806
|
runtime.setCollection(
|
|
2713
4807
|
params.collection as Collection<T, TKey, PersistedCollectionUtils>,
|
|
@@ -2716,19 +4810,15 @@ function createLoopbackSyncConfig<
|
|
|
2716
4810
|
void runtime
|
|
2717
4811
|
.ensureStarted()
|
|
2718
4812
|
.then(() => {
|
|
4813
|
+
if (runtime.getCurrentTerminalFailure()) return
|
|
2719
4814
|
params.markReady()
|
|
2720
4815
|
})
|
|
2721
|
-
.catch((
|
|
2722
|
-
console.warn(`Failed persisted loopback startup:`, error)
|
|
2723
|
-
params.markReady()
|
|
2724
|
-
})
|
|
4816
|
+
.catch(() => undefined)
|
|
2725
4817
|
|
|
2726
4818
|
return {
|
|
2727
|
-
|
|
2728
|
-
runtime.
|
|
2729
|
-
|
|
2730
|
-
},
|
|
2731
|
-
loadSubset: (options: LoadSubsetOptions) => runtime.loadSubset(options),
|
|
4819
|
+
loadSubset: (options: LoadSubsetOptions): true | Promise<void> =>
|
|
4820
|
+
runtime.loadHydratedSubset(options) ?? runtime.loadSubset(options),
|
|
4821
|
+
cleanup: () => runtime.cleanup(),
|
|
2732
4822
|
unloadSubset: (options: LoadSubsetOptions) =>
|
|
2733
4823
|
runtime.unloadSubset(options),
|
|
2734
4824
|
}
|
|
@@ -2739,13 +4829,59 @@ function createLoopbackSyncConfig<
|
|
|
2739
4829
|
}
|
|
2740
4830
|
}
|
|
2741
4831
|
|
|
4832
|
+
export function persistedCollectionOptions<
|
|
4833
|
+
TSchema extends StandardSchemaV1,
|
|
4834
|
+
TKey extends string | number,
|
|
4835
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
4836
|
+
>(
|
|
4837
|
+
options: PersistedSyncWrappedOptions<
|
|
4838
|
+
InferSchemaOutput<TSchema>,
|
|
4839
|
+
TKey,
|
|
4840
|
+
TSchema,
|
|
4841
|
+
TUtils
|
|
4842
|
+
> & {
|
|
4843
|
+
schema: TSchema
|
|
4844
|
+
},
|
|
4845
|
+
): PersistedSyncOptionsResult<
|
|
4846
|
+
InferSchemaOutput<TSchema>,
|
|
4847
|
+
TKey,
|
|
4848
|
+
TSchema,
|
|
4849
|
+
TUtils
|
|
4850
|
+
> & {
|
|
4851
|
+
schema: TSchema
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4854
|
+
export function persistedCollectionOptions<
|
|
4855
|
+
TSchema extends StandardSchemaV1,
|
|
4856
|
+
TKey extends string | number,
|
|
4857
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
4858
|
+
>(
|
|
4859
|
+
options: PersistedLocalOnlyOptions<
|
|
4860
|
+
InferSchemaOutput<TSchema>,
|
|
4861
|
+
TKey,
|
|
4862
|
+
TSchema,
|
|
4863
|
+
TUtils
|
|
4864
|
+
> & {
|
|
4865
|
+
schema: TSchema
|
|
4866
|
+
},
|
|
4867
|
+
): PersistedLocalOnlyOptionsResult<
|
|
4868
|
+
InferSchemaOutput<TSchema>,
|
|
4869
|
+
TKey,
|
|
4870
|
+
TSchema,
|
|
4871
|
+
TUtils
|
|
4872
|
+
> & {
|
|
4873
|
+
schema: TSchema
|
|
4874
|
+
}
|
|
4875
|
+
|
|
2742
4876
|
export function persistedCollectionOptions<
|
|
2743
4877
|
T extends object,
|
|
2744
4878
|
TKey extends string | number,
|
|
2745
4879
|
TSchema extends StandardSchemaV1 = never,
|
|
2746
4880
|
TUtils extends UtilsRecord = UtilsRecord,
|
|
2747
4881
|
>(
|
|
2748
|
-
options: PersistedSyncWrappedOptions<T, TKey, TSchema, TUtils
|
|
4882
|
+
options: PersistedSyncWrappedOptions<T, TKey, TSchema, TUtils> & {
|
|
4883
|
+
schema?: never
|
|
4884
|
+
},
|
|
2749
4885
|
): PersistedSyncOptionsResult<T, TKey, TSchema, TUtils>
|
|
2750
4886
|
|
|
2751
4887
|
export function persistedCollectionOptions<
|
|
@@ -2754,7 +4890,9 @@ export function persistedCollectionOptions<
|
|
|
2754
4890
|
TSchema extends StandardSchemaV1 = never,
|
|
2755
4891
|
TUtils extends UtilsRecord = UtilsRecord,
|
|
2756
4892
|
>(
|
|
2757
|
-
options: PersistedLocalOnlyOptions<T, TKey, TSchema, TUtils
|
|
4893
|
+
options: PersistedLocalOnlyOptions<T, TKey, TSchema, TUtils> & {
|
|
4894
|
+
schema?: never
|
|
4895
|
+
},
|
|
2758
4896
|
): PersistedLocalOnlyOptionsResult<T, TKey, TSchema, TUtils>
|
|
2759
4897
|
|
|
2760
4898
|
export function persistedCollectionOptions<
|
|
@@ -2775,6 +4913,17 @@ export function persistedCollectionOptions<
|
|
|
2775
4913
|
)
|
|
2776
4914
|
}
|
|
2777
4915
|
|
|
4916
|
+
const networkTimeoutMs = getNetworkTimeoutMs(options.initialRender)
|
|
4917
|
+
if (networkTimeoutMs !== undefined && options.syncMode === `on-demand`) {
|
|
4918
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
4919
|
+
`network-first initial rendering requires eager persistence syncMode`,
|
|
4920
|
+
)
|
|
4921
|
+
}
|
|
4922
|
+
const persistedReadinessTracker =
|
|
4923
|
+
networkTimeoutMs !== undefined
|
|
4924
|
+
? new PersistedReadinessTracker(networkTimeoutMs)
|
|
4925
|
+
: undefined
|
|
4926
|
+
|
|
2778
4927
|
if (hasOwnSyncKey(options)) {
|
|
2779
4928
|
if (!isValidSyncConfig(options.sync)) {
|
|
2780
4929
|
throw new InvalidSyncConfigError(
|
|
@@ -2782,7 +4931,11 @@ export function persistedCollectionOptions<
|
|
|
2782
4931
|
)
|
|
2783
4932
|
}
|
|
2784
4933
|
|
|
2785
|
-
const {
|
|
4934
|
+
const {
|
|
4935
|
+
schemaVersion,
|
|
4936
|
+
initialRender: _initialRender,
|
|
4937
|
+
...syncOptions
|
|
4938
|
+
} = options
|
|
2786
4939
|
const collectionId =
|
|
2787
4940
|
syncOptions.id ?? `persisted-collection:${safeRandomUUID()}`
|
|
2788
4941
|
const persistence = resolvePersistenceForCollection(
|
|
@@ -2800,13 +4953,21 @@ export function persistedCollectionOptions<
|
|
|
2800
4953
|
persistence,
|
|
2801
4954
|
syncOptions.syncMode ?? `eager`,
|
|
2802
4955
|
collectionId,
|
|
4956
|
+
persistedReadinessTracker,
|
|
2803
4957
|
)
|
|
2804
4958
|
|
|
2805
4959
|
const result = {
|
|
2806
4960
|
...syncOptions,
|
|
2807
4961
|
id: collectionId,
|
|
2808
|
-
sync: createWrappedSyncConfig<T, TKey>(
|
|
4962
|
+
sync: createWrappedSyncConfig<T, TKey>(
|
|
4963
|
+
syncOptions.sync,
|
|
4964
|
+
runtime,
|
|
4965
|
+
persistedReadinessTracker,
|
|
4966
|
+
),
|
|
2809
4967
|
persistence,
|
|
4968
|
+
...(persistedReadinessTracker
|
|
4969
|
+
? { [PERSISTED_READINESS]: persistedReadinessTracker }
|
|
4970
|
+
: {}),
|
|
2810
4971
|
}
|
|
2811
4972
|
|
|
2812
4973
|
return withCollectionConfigFactory(
|
|
@@ -2815,11 +4976,15 @@ export function persistedCollectionOptions<
|
|
|
2815
4976
|
persistedCollectionOptions({
|
|
2816
4977
|
...options,
|
|
2817
4978
|
id: collectionId,
|
|
2818
|
-
}) as typeof result,
|
|
4979
|
+
} as never) as typeof result,
|
|
2819
4980
|
)
|
|
2820
4981
|
}
|
|
2821
4982
|
|
|
2822
|
-
const {
|
|
4983
|
+
const {
|
|
4984
|
+
schemaVersion,
|
|
4985
|
+
initialRender: _initialRender,
|
|
4986
|
+
...localOnlyOptions
|
|
4987
|
+
} = options
|
|
2823
4988
|
const collectionId =
|
|
2824
4989
|
localOnlyOptions.id ?? `persisted-collection:${safeRandomUUID()}`
|
|
2825
4990
|
const persistence = resolvePersistenceForCollection(
|
|
@@ -2836,11 +5001,13 @@ export function persistedCollectionOptions<
|
|
|
2836
5001
|
persistence,
|
|
2837
5002
|
localOnlyOptions.syncMode ?? `eager`,
|
|
2838
5003
|
localOnlyOptions.id ?? DEFAULT_DB_NAME,
|
|
5004
|
+
persistedReadinessTracker,
|
|
2839
5005
|
)
|
|
2840
5006
|
|
|
2841
5007
|
const wrappedOnInsert = async (
|
|
2842
5008
|
params: InsertMutationFnParams<T, TKey, TUtils & PersistedCollectionUtils>,
|
|
2843
5009
|
) => {
|
|
5010
|
+
const lifecycleGeneration = runtime.getLifecycleGeneration()
|
|
2844
5011
|
const handlerResult = localOnlyOptions.onInsert
|
|
2845
5012
|
? await localOnlyOptions.onInsert(
|
|
2846
5013
|
params as unknown as InsertMutationFnParams<T, TKey, TUtils>,
|
|
@@ -2849,6 +5016,7 @@ export function persistedCollectionOptions<
|
|
|
2849
5016
|
|
|
2850
5017
|
await runtime.persistAndConfirmCollectionMutations(
|
|
2851
5018
|
params.transaction.mutations as Array<PendingMutation<T>>,
|
|
5019
|
+
lifecycleGeneration,
|
|
2852
5020
|
)
|
|
2853
5021
|
|
|
2854
5022
|
return handlerResult ?? {}
|
|
@@ -2857,6 +5025,7 @@ export function persistedCollectionOptions<
|
|
|
2857
5025
|
const wrappedOnUpdate = async (
|
|
2858
5026
|
params: UpdateMutationFnParams<T, TKey, TUtils & PersistedCollectionUtils>,
|
|
2859
5027
|
) => {
|
|
5028
|
+
const lifecycleGeneration = runtime.getLifecycleGeneration()
|
|
2860
5029
|
const handlerResult = localOnlyOptions.onUpdate
|
|
2861
5030
|
? await localOnlyOptions.onUpdate(
|
|
2862
5031
|
params as unknown as UpdateMutationFnParams<T, TKey, TUtils>,
|
|
@@ -2865,6 +5034,7 @@ export function persistedCollectionOptions<
|
|
|
2865
5034
|
|
|
2866
5035
|
await runtime.persistAndConfirmCollectionMutations(
|
|
2867
5036
|
params.transaction.mutations as Array<PendingMutation<T>>,
|
|
5037
|
+
lifecycleGeneration,
|
|
2868
5038
|
)
|
|
2869
5039
|
|
|
2870
5040
|
return handlerResult ?? {}
|
|
@@ -2873,6 +5043,7 @@ export function persistedCollectionOptions<
|
|
|
2873
5043
|
const wrappedOnDelete = async (
|
|
2874
5044
|
params: DeleteMutationFnParams<T, TKey, TUtils & PersistedCollectionUtils>,
|
|
2875
5045
|
) => {
|
|
5046
|
+
const lifecycleGeneration = runtime.getLifecycleGeneration()
|
|
2876
5047
|
const handlerResult = localOnlyOptions.onDelete
|
|
2877
5048
|
? await localOnlyOptions.onDelete(
|
|
2878
5049
|
params as unknown as DeleteMutationFnParams<T, TKey, TUtils>,
|
|
@@ -2881,6 +5052,7 @@ export function persistedCollectionOptions<
|
|
|
2881
5052
|
|
|
2882
5053
|
await runtime.persistAndConfirmCollectionMutations(
|
|
2883
5054
|
params.transaction.mutations as Array<PendingMutation<T>>,
|
|
5055
|
+
lifecycleGeneration,
|
|
2884
5056
|
)
|
|
2885
5057
|
|
|
2886
5058
|
return handlerResult ?? {}
|
|
@@ -2908,6 +5080,9 @@ export function persistedCollectionOptions<
|
|
|
2908
5080
|
...localOnlyOptions,
|
|
2909
5081
|
id: collectionId,
|
|
2910
5082
|
persistence,
|
|
5083
|
+
...(persistedReadinessTracker
|
|
5084
|
+
? { [PERSISTED_READINESS]: persistedReadinessTracker }
|
|
5085
|
+
: {}),
|
|
2911
5086
|
sync: createLoopbackSyncConfig(runtime),
|
|
2912
5087
|
onInsert: wrappedOnInsert,
|
|
2913
5088
|
onUpdate: wrappedOnUpdate,
|
|
@@ -2923,7 +5098,7 @@ export function persistedCollectionOptions<
|
|
|
2923
5098
|
persistedCollectionOptions({
|
|
2924
5099
|
...options,
|
|
2925
5100
|
id: collectionId,
|
|
2926
|
-
}) as typeof result,
|
|
5101
|
+
} as never) as typeof result,
|
|
2927
5102
|
)
|
|
2928
5103
|
}
|
|
2929
5104
|
|