@tanstack/db-sqlite-persistence-core 0.2.21 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1840 -399
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +177 -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 +177 -11
- package/dist/esm/persisted.js +1842 -401
- 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 +2690 -444
- 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
|
|
|
@@ -352,9 +582,78 @@ export interface PersistedCollectionUtils extends UtilsRecord {
|
|
|
352
582
|
mutations: Array<PendingMutation<Record<string, unknown>>>
|
|
353
583
|
}) => Promise<void> | void
|
|
354
584
|
getLeadershipState?: () => PersistedCollectionLeadershipState
|
|
585
|
+
/** Hydrate once without acquiring a new ongoing subset lease. */
|
|
355
586
|
forceReloadSubset?: (options: LoadSubsetOptions) => Promise<void> | void
|
|
356
587
|
}
|
|
357
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
|
+
|
|
358
657
|
export type PersistedSyncWrappedOptions<
|
|
359
658
|
T extends object,
|
|
360
659
|
TKey extends string | number,
|
|
@@ -364,6 +663,8 @@ export type PersistedSyncWrappedOptions<
|
|
|
364
663
|
sync: SyncConfig<T, TKey>
|
|
365
664
|
persistence: PersistedCollectionPersistence
|
|
366
665
|
schemaVersion?: number
|
|
666
|
+
/** Opt into network-first rendering with a persisted fallback. */
|
|
667
|
+
initialRender?: PersistedInitialRenderOptions
|
|
367
668
|
}
|
|
368
669
|
|
|
369
670
|
export type PersistedLocalOnlyOptions<
|
|
@@ -374,6 +675,8 @@ export type PersistedLocalOnlyOptions<
|
|
|
374
675
|
> = Omit<CollectionConfig<T, TKey, TSchema, TUtils>, `sync`> & {
|
|
375
676
|
persistence: PersistedCollectionPersistence
|
|
376
677
|
schemaVersion?: number
|
|
678
|
+
/** Opt into network-first rendering with a persisted fallback. */
|
|
679
|
+
initialRender?: PersistedInitialRenderOptions
|
|
377
680
|
}
|
|
378
681
|
|
|
379
682
|
type PersistedSyncOptionsResult<
|
|
@@ -404,7 +707,11 @@ const REQUIRED_COORDINATOR_METHODS: ReadonlyArray<
|
|
|
404
707
|
| `publish`
|
|
405
708
|
| `isLeader`
|
|
406
709
|
| `ensureLeadership`
|
|
710
|
+
| `requestEnsureRemoteSubset`
|
|
711
|
+
| `requestReleaseRemoteSubset`
|
|
712
|
+
| `registerRemoteSubsetOwner`
|
|
407
713
|
| `requestEnsurePersistedIndex`
|
|
714
|
+
| `requestApplyCommittedTx`
|
|
408
715
|
>
|
|
409
716
|
> = [
|
|
410
717
|
`getNodeId`,
|
|
@@ -412,15 +719,19 @@ const REQUIRED_COORDINATOR_METHODS: ReadonlyArray<
|
|
|
412
719
|
`publish`,
|
|
413
720
|
`isLeader`,
|
|
414
721
|
`ensureLeadership`,
|
|
722
|
+
`requestEnsureRemoteSubset`,
|
|
723
|
+
`requestReleaseRemoteSubset`,
|
|
724
|
+
`registerRemoteSubsetOwner`,
|
|
415
725
|
`requestEnsurePersistedIndex`,
|
|
726
|
+
`requestApplyCommittedTx`,
|
|
416
727
|
]
|
|
417
728
|
|
|
418
729
|
const REQUIRED_ADAPTER_METHODS: ReadonlyArray<
|
|
419
730
|
keyof Pick<
|
|
420
731
|
PersistenceAdapter,
|
|
421
|
-
`loadSubset` | `applyCommittedTx` | `ensureIndex`
|
|
732
|
+
`loadSubset` | `loadResumeSnapshot` | `applyCommittedTx` | `ensureIndex`
|
|
422
733
|
>
|
|
423
|
-
> = [`loadSubset`, `applyCommittedTx`, `ensureIndex`]
|
|
734
|
+
> = [`loadSubset`, `loadResumeSnapshot`, `applyCommittedTx`, `ensureIndex`]
|
|
424
735
|
|
|
425
736
|
const TARGETED_INVALIDATION_KEY_LIMIT = 128
|
|
426
737
|
const DEFAULT_DB_NAME = `tanstack-db`
|
|
@@ -439,6 +750,7 @@ type SyncControlFns<T extends object, TKey extends string | number> = {
|
|
|
439
750
|
commit: ((signal?: AbortSignal) => SyncAppliedReceipt) | null
|
|
440
751
|
truncate: (() => void) | null
|
|
441
752
|
metadata: SyncMetadataApi<TKey> | null
|
|
753
|
+
markError: ((error: unknown) => void) | null
|
|
442
754
|
}
|
|
443
755
|
|
|
444
756
|
/**
|
|
@@ -447,6 +759,12 @@ type SyncControlFns<T extends object, TKey extends string | number> = {
|
|
|
447
759
|
*/
|
|
448
760
|
export class SingleProcessCoordinator implements PersistedCollectionCoordinator {
|
|
449
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
|
+
>()
|
|
450
768
|
|
|
451
769
|
constructor(nodeId: string = safeRandomUUID()) {
|
|
452
770
|
this.nodeId = nodeId
|
|
@@ -468,10 +786,129 @@ export class SingleProcessCoordinator implements PersistedCollectionCoordinator
|
|
|
468
786
|
|
|
469
787
|
public async ensureLeadership(): Promise<void> {}
|
|
470
788
|
|
|
471
|
-
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
|
+
}
|
|
472
876
|
|
|
473
877
|
public async requestEnsurePersistedIndex(): Promise<void> {}
|
|
474
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
|
+
|
|
475
912
|
public pullSince(): Promise<PullSinceResponse> {
|
|
476
913
|
return Promise.resolve({
|
|
477
914
|
type: `rpc:pullSince:res`,
|
|
@@ -508,10 +945,14 @@ function validatePersistenceAdapter(adapter: PersistenceAdapter): void {
|
|
|
508
945
|
|
|
509
946
|
function resolvePersistence(
|
|
510
947
|
persistence: PersistedCollectionPersistence,
|
|
948
|
+
collectionId: string,
|
|
511
949
|
): PersistedResolvedPersistence {
|
|
512
950
|
validatePersistenceAdapter(persistence.adapter)
|
|
513
951
|
|
|
514
952
|
const coordinator = persistence.coordinator ?? new SingleProcessCoordinator()
|
|
953
|
+
if (coordinator instanceof SingleProcessCoordinator) {
|
|
954
|
+
coordinator.setAdapterForCollection(collectionId, persistence.adapter)
|
|
955
|
+
}
|
|
515
956
|
validatePersistedCollectionCoordinator(coordinator)
|
|
516
957
|
|
|
517
958
|
return {
|
|
@@ -523,9 +964,13 @@ function resolvePersistence(
|
|
|
523
964
|
function resolvePersistenceForMode(
|
|
524
965
|
persistence: PersistedCollectionPersistence,
|
|
525
966
|
mode: PersistedCollectionMode,
|
|
967
|
+
collectionId: string,
|
|
526
968
|
): PersistedResolvedPersistence {
|
|
527
969
|
const modeSpecificPersistence = persistence.resolvePersistenceForMode?.(mode)
|
|
528
|
-
return resolvePersistence(
|
|
970
|
+
return resolvePersistence(
|
|
971
|
+
modeSpecificPersistence ?? persistence,
|
|
972
|
+
collectionId,
|
|
973
|
+
)
|
|
529
974
|
}
|
|
530
975
|
|
|
531
976
|
function resolvePersistenceForCollection(
|
|
@@ -539,10 +984,17 @@ function resolvePersistenceForCollection(
|
|
|
539
984
|
const collectionSpecificPersistence =
|
|
540
985
|
persistence.resolvePersistenceForCollection?.(options)
|
|
541
986
|
if (collectionSpecificPersistence) {
|
|
542
|
-
return resolvePersistence(
|
|
987
|
+
return resolvePersistence(
|
|
988
|
+
collectionSpecificPersistence,
|
|
989
|
+
options.collectionId,
|
|
990
|
+
)
|
|
543
991
|
}
|
|
544
992
|
|
|
545
|
-
return resolvePersistenceForMode(
|
|
993
|
+
return resolvePersistenceForMode(
|
|
994
|
+
persistence,
|
|
995
|
+
options.mode,
|
|
996
|
+
options.collectionId,
|
|
997
|
+
)
|
|
546
998
|
}
|
|
547
999
|
|
|
548
1000
|
function hasOwnSyncKey(options: object): options is { sync: unknown } {
|
|
@@ -579,6 +1031,7 @@ type NormalizedSyncOperation<T extends object, TKey extends string | number> =
|
|
|
579
1031
|
|
|
580
1032
|
type BufferedSyncTransaction<T extends object, TKey extends string | number> = {
|
|
581
1033
|
operations: Array<NormalizedSyncOperation<T, TKey>>
|
|
1034
|
+
partialUpdateOperationIndexes: Set<number>
|
|
582
1035
|
rowMetadataWrites: Map<
|
|
583
1036
|
TKey,
|
|
584
1037
|
{ type: `set`; value: unknown } | { type: `delete` }
|
|
@@ -587,44 +1040,95 @@ type BufferedSyncTransaction<T extends object, TKey extends string | number> = {
|
|
|
587
1040
|
string,
|
|
588
1041
|
{ type: `set`; value: unknown } | { type: `delete` }
|
|
589
1042
|
>
|
|
1043
|
+
deferredHydrationMetadataDeleteKeys: Set<TKey>
|
|
1044
|
+
hydrationContext?: { suppliedRowKeys: Set<TKey> }
|
|
1045
|
+
hydrationSequence?: number
|
|
590
1046
|
truncate: boolean
|
|
591
1047
|
internal: boolean
|
|
1048
|
+
lifecycleGeneration: number
|
|
1049
|
+
beginOptions?: { immediate?: boolean }
|
|
1050
|
+
expectedResumeGenerationOwner?: symbol
|
|
592
1051
|
signal?: AbortSignal
|
|
1052
|
+
prependHydrationRows: (
|
|
1053
|
+
rows: Array<{ key: TKey; value: T; metadata?: unknown }>,
|
|
1054
|
+
) => void
|
|
1055
|
+
applyToCollection: () => SyncAppliedReceipt
|
|
1056
|
+
shouldFailStopOnAbort?: () => boolean
|
|
593
1057
|
resolveApplied?: () => void
|
|
594
1058
|
rejectApplied?: (error: unknown) => void
|
|
595
1059
|
}
|
|
596
1060
|
|
|
597
|
-
type OpenSyncTransaction<
|
|
598
|
-
T
|
|
599
|
-
|
|
600
|
-
>
|
|
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>
|
|
601
1068
|
queuedBecauseHydrating: boolean
|
|
1069
|
+
hasDependentSuccessor: boolean
|
|
1070
|
+
publicationAdmissionWaiters?: Set<{
|
|
1071
|
+
resolve: () => void
|
|
1072
|
+
reject: (error: unknown) => void
|
|
1073
|
+
}>
|
|
1074
|
+
terminalFailure?: { error: unknown }
|
|
602
1075
|
}
|
|
603
1076
|
|
|
604
1077
|
type SyncWriteNormalization<T extends object, TKey extends string | number> = {
|
|
605
|
-
forwardMessage:
|
|
606
|
-
| {
|
|
607
|
-
type: `update`
|
|
608
|
-
value: T
|
|
609
|
-
metadata?: Record<string, unknown>
|
|
610
|
-
}
|
|
611
|
-
| {
|
|
612
|
-
type: `delete`
|
|
613
|
-
key: TKey
|
|
614
|
-
}
|
|
615
1078
|
operation: NormalizedSyncOperation<T, TKey>
|
|
616
1079
|
}
|
|
617
1080
|
|
|
618
1081
|
class ApplyMutex {
|
|
619
1082
|
private queue: Promise<void> = Promise.resolve()
|
|
1083
|
+
private pending = 0
|
|
620
1084
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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)
|
|
628
1132
|
}
|
|
629
1133
|
}
|
|
630
1134
|
|
|
@@ -639,7 +1143,7 @@ function toStableSerializable(value: unknown): unknown {
|
|
|
639
1143
|
case `boolean`:
|
|
640
1144
|
return value
|
|
641
1145
|
case `bigint`:
|
|
642
|
-
return value
|
|
1146
|
+
return serializeSQLiteBigInt(value)
|
|
643
1147
|
case `function`:
|
|
644
1148
|
case `symbol`:
|
|
645
1149
|
case `undefined`:
|
|
@@ -707,18 +1211,6 @@ function stableSerialize(value: unknown): string {
|
|
|
707
1211
|
return JSON.stringify(toStableSerializable(value) ?? null)
|
|
708
1212
|
}
|
|
709
1213
|
|
|
710
|
-
function normalizeSubsetOptionsForKey(
|
|
711
|
-
options: LoadSubsetOptions,
|
|
712
|
-
): Record<string, unknown> {
|
|
713
|
-
return {
|
|
714
|
-
where: toStableSerializable(options.where),
|
|
715
|
-
orderBy: toStableSerializable(options.orderBy),
|
|
716
|
-
limit: options.limit,
|
|
717
|
-
cursor: toStableSerializable(options.cursor),
|
|
718
|
-
offset: options.offset,
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
1214
|
function normalizeSyncFnResult(result: void | (() => void) | SyncConfigRes) {
|
|
723
1215
|
if (typeof result === `function`) {
|
|
724
1216
|
return { cleanup: result } satisfies SyncConfigRes
|
|
@@ -780,6 +1272,8 @@ function toPersistedMutationEnvelope(
|
|
|
780
1272
|
? (mutation.original as Record<string, unknown>)
|
|
781
1273
|
: mutation.modified
|
|
782
1274
|
|
|
1275
|
+
// PendingMutation.metadata belongs to the optimistic transaction and is
|
|
1276
|
+
// consumed by mutation handlers. It must not overwrite persisted row metadata.
|
|
783
1277
|
return {
|
|
784
1278
|
mutationId: mutation.mutationId,
|
|
785
1279
|
type: mutation.type,
|
|
@@ -794,6 +1288,10 @@ class PersistedCollectionRuntime<
|
|
|
794
1288
|
> {
|
|
795
1289
|
private readonly applyMutex = new ApplyMutex()
|
|
796
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
|
|
797
1295
|
private readonly pendingRemoteSubsetEnsures = new Map<
|
|
798
1296
|
string,
|
|
799
1297
|
LoadSubsetOptions
|
|
@@ -801,8 +1299,12 @@ class PersistedCollectionRuntime<
|
|
|
801
1299
|
private readonly queuedHydrationTransactions: Array<
|
|
802
1300
|
BufferedSyncTransaction<T, TKey>
|
|
803
1301
|
> = []
|
|
804
|
-
private readonly queuedTxCommitted: Array<
|
|
805
|
-
|
|
1302
|
+
private readonly queuedTxCommitted: Array<{
|
|
1303
|
+
txCommitted: TxCommitted
|
|
1304
|
+
lifecycleGeneration: number
|
|
1305
|
+
}> = []
|
|
1306
|
+
private readonly requestIds = new WeakMap<LoadSubsetOptions, string>()
|
|
1307
|
+
private readonly hydratedDemands = new Set<string | undefined>()
|
|
806
1308
|
|
|
807
1309
|
private collection: Collection<T, TKey, PersistedCollectionUtils> | null =
|
|
808
1310
|
null
|
|
@@ -812,19 +1314,35 @@ class PersistedCollectionRuntime<
|
|
|
812
1314
|
commit: null,
|
|
813
1315
|
truncate: null,
|
|
814
1316
|
metadata: null,
|
|
1317
|
+
markError: null,
|
|
815
1318
|
}
|
|
816
|
-
private started = false
|
|
817
1319
|
private startupMetadataPromise: Promise<void> | null = null
|
|
818
1320
|
private startPromise: Promise<void> | null = null
|
|
1321
|
+
private hasAttemptedStartup = false
|
|
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`)
|
|
1327
|
+
private lifecycleGeneration = 0
|
|
819
1328
|
private internalApplyDepth = 0
|
|
1329
|
+
private sourcePublicationWaitDepth = 0
|
|
820
1330
|
private appliedReceiptSequence = 0
|
|
1331
|
+
private syncErrorReported = false
|
|
1332
|
+
private reportedSyncError: unknown
|
|
821
1333
|
private readonly pendingAppliedReceipts = new Map<number, Promise<void>>()
|
|
822
|
-
private
|
|
1334
|
+
private hydratingGeneration: number | null = null
|
|
1335
|
+
private terminalFailure:
|
|
1336
|
+
| { lifecycleGeneration: number; error: unknown }
|
|
1337
|
+
| undefined
|
|
823
1338
|
private coordinatorUnsubscribe: (() => void) | null = null
|
|
1339
|
+
private remoteSubsetOwnerUnsubscribe: (() => void) | null = null
|
|
824
1340
|
private indexAddedUnsubscribe: (() => void) | null = null
|
|
825
1341
|
private indexRemovedUnsubscribe: (() => void) | null = null
|
|
826
1342
|
private remoteEnsureRetryTimer: ReturnType<typeof setTimeout> | null = null
|
|
827
|
-
private
|
|
1343
|
+
private nextRequestId = 0
|
|
1344
|
+
private startupSettled = false
|
|
1345
|
+
private sourceTruncateGeneration = 0
|
|
828
1346
|
|
|
829
1347
|
private latestTerm = 0
|
|
830
1348
|
private latestSeq = 0
|
|
@@ -839,9 +1357,14 @@ class PersistedCollectionRuntime<
|
|
|
839
1357
|
private readonly persistence: PersistedResolvedPersistence,
|
|
840
1358
|
private readonly syncMode: `eager` | `on-demand`,
|
|
841
1359
|
private readonly dbName: string,
|
|
1360
|
+
private readonly persistedReadiness?: PersistedReadinessTracker,
|
|
842
1361
|
) {}
|
|
843
1362
|
|
|
844
1363
|
setSyncControls(syncControls: SyncControlFns<T, TKey>): void {
|
|
1364
|
+
this.advanceLifecycle()
|
|
1365
|
+
this.syncErrorReported = false
|
|
1366
|
+
this.reportedSyncError = undefined
|
|
1367
|
+
|
|
845
1368
|
const commit = syncControls.commit
|
|
846
1369
|
this.syncControls = {
|
|
847
1370
|
...syncControls,
|
|
@@ -851,6 +1374,30 @@ class PersistedCollectionRuntime<
|
|
|
851
1374
|
}
|
|
852
1375
|
}
|
|
853
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
|
+
|
|
854
1401
|
private trackAppliedReceipt(receipt: SyncAppliedReceipt): SyncAppliedReceipt {
|
|
855
1402
|
const sequence = ++this.appliedReceiptSequence
|
|
856
1403
|
if (receipt === true) {
|
|
@@ -870,18 +1417,75 @@ class PersistedCollectionRuntime<
|
|
|
870
1417
|
)
|
|
871
1418
|
}
|
|
872
1419
|
|
|
873
|
-
clearSyncControls(): void {
|
|
1420
|
+
private clearSyncControls(): void {
|
|
874
1421
|
this.syncControls = {
|
|
875
1422
|
begin: null,
|
|
876
1423
|
write: null,
|
|
877
1424
|
commit: null,
|
|
878
1425
|
truncate: null,
|
|
879
1426
|
metadata: null,
|
|
1427
|
+
markError: null,
|
|
880
1428
|
}
|
|
881
1429
|
}
|
|
882
1430
|
|
|
883
1431
|
isHydratingNow(): boolean {
|
|
884
|
-
return this.
|
|
1432
|
+
return this.hydratingGeneration === this.lifecycleGeneration
|
|
1433
|
+
}
|
|
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
|
|
885
1489
|
}
|
|
886
1490
|
|
|
887
1491
|
isApplyingInternally(): boolean {
|
|
@@ -906,88 +1510,313 @@ class PersistedCollectionRuntime<
|
|
|
906
1510
|
}
|
|
907
1511
|
}
|
|
908
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
|
+
|
|
909
1523
|
async ensureStarted(): Promise<void> {
|
|
1524
|
+
this.throwIfTerminal()
|
|
910
1525
|
if (this.startPromise) {
|
|
911
1526
|
return this.startPromise
|
|
912
1527
|
}
|
|
913
1528
|
|
|
914
|
-
|
|
1529
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
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)
|
|
915
1639
|
return this.startPromise
|
|
916
1640
|
}
|
|
917
1641
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1642
|
+
ensureResumeBaselineHydrated(): Promise<void> {
|
|
1643
|
+
this.throwIfTerminal()
|
|
1644
|
+
if (this.resumeBaselinePromise) {
|
|
1645
|
+
return this.resumeBaselinePromise
|
|
921
1646
|
}
|
|
922
1647
|
|
|
923
|
-
|
|
924
|
-
|
|
1648
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1649
|
+
this.resumeBaselinePromise = (async () => {
|
|
1650
|
+
await this.ensureStarted()
|
|
1651
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1652
|
+
if (this.syncMode !== `on-demand`) return
|
|
1653
|
+
|
|
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
|
+
})
|
|
1672
|
+
return this.resumeBaselinePromise
|
|
925
1673
|
}
|
|
926
1674
|
|
|
927
|
-
|
|
928
|
-
if (this.
|
|
929
|
-
return
|
|
1675
|
+
ensureResumeBaselineCertified(): Promise<void> {
|
|
1676
|
+
if (this.resumeCertificationPromise) {
|
|
1677
|
+
return this.resumeCertificationPromise
|
|
930
1678
|
}
|
|
931
1679
|
|
|
932
|
-
|
|
1680
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1681
|
+
this.resumeCertificationPromise = (async () => {
|
|
1682
|
+
await this.ensureStarted()
|
|
1683
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
933
1684
|
|
|
934
|
-
|
|
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
|
+
}
|
|
935
1697
|
|
|
936
|
-
|
|
937
|
-
this.
|
|
938
|
-
|
|
1698
|
+
getKeySetEvidence(): PersistedKeySetEvidence | undefined {
|
|
1699
|
+
return this.persistedKeySetEvidence
|
|
1700
|
+
}
|
|
939
1701
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const appliedCursor = this.appliedReceiptSequence
|
|
943
|
-
await this.applyMutex.run(() =>
|
|
944
|
-
this.hydrateSubsetUnsafe({}, { requestRemoteEnsure: false }),
|
|
945
|
-
)
|
|
946
|
-
await this.waitForAppliedReceiptsAfter(appliedCursor)
|
|
947
|
-
}
|
|
1702
|
+
getResumeGenerationOwner(): symbol {
|
|
1703
|
+
return this.resumeGenerationOwner
|
|
948
1704
|
}
|
|
949
1705
|
|
|
950
|
-
private async
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
this.collectionId,
|
|
956
|
-
)
|
|
957
|
-
this.observeStreamPosition(
|
|
958
|
-
position.latestTerm,
|
|
959
|
-
position.latestSeq,
|
|
960
|
-
position.latestRowVersion,
|
|
961
|
-
)
|
|
962
|
-
}
|
|
1706
|
+
private async hydrateBaseline(
|
|
1707
|
+
lifecycleGeneration: number,
|
|
1708
|
+
adapter: HydrationPersistenceAdapter,
|
|
1709
|
+
): Promise<number | undefined> {
|
|
1710
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return undefined
|
|
963
1711
|
|
|
964
|
-
|
|
1712
|
+
// The baseline shares the unconstrained demand key. Its lease is never
|
|
1713
|
+
// released, and every reload rereads it, so that coverage stays valid.
|
|
1714
|
+
const baseline = {}
|
|
1715
|
+
this.activeSubsets.set(this.getSubsetKey(baseline), baseline)
|
|
1716
|
+
const appliedCursor = this.appliedReceiptSequence
|
|
1717
|
+
await this.hydrateSubsetUnsafe(
|
|
1718
|
+
baseline,
|
|
1719
|
+
{
|
|
1720
|
+
requestRemoteEnsure: false,
|
|
1721
|
+
lifecycleGeneration,
|
|
1722
|
+
bindKeySetEvidence: true,
|
|
1723
|
+
},
|
|
1724
|
+
adapter,
|
|
1725
|
+
)
|
|
1726
|
+
return lifecycleGeneration === this.lifecycleGeneration
|
|
1727
|
+
? appliedCursor
|
|
1728
|
+
: undefined
|
|
965
1729
|
}
|
|
966
1730
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
this.
|
|
1731
|
+
async ensureStartupMetadataLoaded(): Promise<void> {
|
|
1732
|
+
this.throwIfTerminal()
|
|
1733
|
+
if (this.startupMetadataPromise) {
|
|
1734
|
+
return this.startupMetadataPromise
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
void this.ensureStarted()
|
|
1738
|
+
return this.startupMetadataPromise!
|
|
970
1739
|
}
|
|
971
1740
|
|
|
972
|
-
private async
|
|
973
|
-
|
|
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
|
|
974
1752
|
> {
|
|
975
|
-
if (
|
|
1753
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return undefined
|
|
1754
|
+
|
|
1755
|
+
const indexBootstrapSnapshot = this.collection?.getIndexMetadata() ?? []
|
|
1756
|
+
this.attachIndexLifecycleListeners()
|
|
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
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
private async loadStartupMetadataInternal(
|
|
1781
|
+
lifecycleGeneration: number,
|
|
1782
|
+
adapter: HydrationPersistenceAdapter,
|
|
1783
|
+
): Promise<void> {
|
|
1784
|
+
const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
|
|
1785
|
+
includeRows: false,
|
|
1786
|
+
})
|
|
1787
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
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
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
private async loadCollectionMetadataSnapshot(
|
|
1802
|
+
adapter: HydrationPersistenceAdapter,
|
|
1803
|
+
): Promise<Array<{ key: string; value: unknown }>> {
|
|
1804
|
+
if (!adapter.loadCollectionMetadata) {
|
|
976
1805
|
return []
|
|
977
1806
|
}
|
|
978
1807
|
|
|
979
|
-
return
|
|
1808
|
+
return adapter.loadCollectionMetadata(this.collectionId)
|
|
980
1809
|
}
|
|
981
1810
|
|
|
982
1811
|
private replaceCollectionMetadataSnapshot(
|
|
983
1812
|
collectionMetadata: Array<{ key: string; value: unknown }>,
|
|
984
|
-
):
|
|
1813
|
+
): SyncAppliedReceipt {
|
|
985
1814
|
if (
|
|
986
1815
|
!this.syncControls.begin ||
|
|
987
1816
|
!this.syncControls.commit ||
|
|
988
1817
|
!this.syncControls.metadata
|
|
989
1818
|
) {
|
|
990
|
-
return
|
|
1819
|
+
return true
|
|
991
1820
|
}
|
|
992
1821
|
|
|
993
1822
|
const nextMetadata = new Map(
|
|
@@ -997,7 +1826,7 @@ class PersistedCollectionRuntime<
|
|
|
997
1826
|
.list()
|
|
998
1827
|
.map(({ key }) => key)
|
|
999
1828
|
|
|
1000
|
-
this.withInternalApply(() => {
|
|
1829
|
+
return this.withInternalApply(() => {
|
|
1001
1830
|
this.syncControls.begin?.({ immediate: true })
|
|
1002
1831
|
|
|
1003
1832
|
currentKeys.forEach((key) => {
|
|
@@ -1010,7 +1839,7 @@ class PersistedCollectionRuntime<
|
|
|
1010
1839
|
this.syncControls.metadata?.collection.set(key, value)
|
|
1011
1840
|
})
|
|
1012
1841
|
|
|
1013
|
-
this.syncControls.commit?.()
|
|
1842
|
+
return this.syncControls.commit?.() ?? true
|
|
1014
1843
|
})
|
|
1015
1844
|
}
|
|
1016
1845
|
|
|
@@ -1018,19 +1847,62 @@ class PersistedCollectionRuntime<
|
|
|
1018
1847
|
options: LoadSubsetOptions,
|
|
1019
1848
|
upstreamLoadSubset?: LoadSubsetFn,
|
|
1020
1849
|
): Promise<void> {
|
|
1021
|
-
this.
|
|
1022
|
-
|
|
1850
|
+
this.throwIfTerminal()
|
|
1851
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1852
|
+
const subsetKey = this.getSubsetKey(options)
|
|
1853
|
+
const truncateGeneration = this.sourceTruncateGeneration
|
|
1854
|
+
const routeRemoteDemandDuringHydration =
|
|
1855
|
+
this.canRouteRemoteDemandThroughCoordinator()
|
|
1856
|
+
this.activeSubsets.set(subsetKey, options)
|
|
1023
1857
|
const appliedCursor = this.appliedReceiptSequence
|
|
1024
|
-
|
|
1025
|
-
this.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
+
}
|
|
1030
1892
|
|
|
1031
|
-
if (
|
|
1893
|
+
if (
|
|
1894
|
+
options.signal?.aborted ||
|
|
1895
|
+
this.activeSubsets.get(this.getSubsetKey(options)) !== options
|
|
1896
|
+
) {
|
|
1897
|
+
return
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
if (this.canRouteRemoteDemandThroughCoordinator()) {
|
|
1032
1901
|
try {
|
|
1033
|
-
await
|
|
1902
|
+
await this.persistence.coordinator.requestEnsureRemoteSubset(
|
|
1903
|
+
this.collectionId,
|
|
1904
|
+
options,
|
|
1905
|
+
)
|
|
1034
1906
|
} catch (error) {
|
|
1035
1907
|
if (
|
|
1036
1908
|
options.signal?.aborted ||
|
|
@@ -1042,40 +1914,177 @@ class PersistedCollectionRuntime<
|
|
|
1042
1914
|
this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options))
|
|
1043
1915
|
throw error
|
|
1044
1916
|
}
|
|
1045
|
-
console.warn(`Failed to trigger remote subset load:`, error)
|
|
1046
1917
|
this.queueRemoteSubsetEnsure(options)
|
|
1918
|
+
throw error
|
|
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
|
|
1047
1936
|
}
|
|
1048
1937
|
}
|
|
1049
1938
|
}
|
|
1050
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)
|
|
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()
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1051
2002
|
unloadSubset(
|
|
1052
2003
|
options: LoadSubsetOptions,
|
|
1053
2004
|
upstreamUnloadSubset?: (options: LoadSubsetOptions) => void,
|
|
1054
2005
|
): void {
|
|
1055
|
-
this.
|
|
1056
|
-
this.
|
|
1057
|
-
|
|
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
|
+
}
|
|
1058
2035
|
}
|
|
1059
2036
|
|
|
1060
2037
|
async forceReloadSubset(options: LoadSubsetOptions): Promise<void> {
|
|
1061
|
-
this.
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
)
|
|
2038
|
+
this.throwIfTerminal()
|
|
2039
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
2040
|
+
// A one-shot refresh does not acquire an enduring subscription lease.
|
|
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
|
+
})
|
|
1065
2057
|
}
|
|
1066
2058
|
|
|
1067
2059
|
queueHydrationBufferedTransaction(
|
|
1068
2060
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
1069
2061
|
): void {
|
|
2062
|
+
const failure = this.getCurrentTerminalFailure()
|
|
2063
|
+
if (failure) {
|
|
2064
|
+
transaction.rejectApplied?.(failure.error)
|
|
2065
|
+
return
|
|
2066
|
+
}
|
|
1070
2067
|
this.queuedHydrationTransactions.push(transaction)
|
|
1071
2068
|
}
|
|
1072
2069
|
|
|
1073
|
-
|
|
2070
|
+
applyHydrationBufferedTransaction(
|
|
1074
2071
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
1075
2072
|
): Promise<void> {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
+
})
|
|
1079
2088
|
}
|
|
1080
2089
|
|
|
1081
2090
|
normalizeSyncWriteMessage(
|
|
@@ -1092,10 +2101,6 @@ class PersistedCollectionRuntime<
|
|
|
1092
2101
|
const previousValue = this.collection.get(key) ?? ({} as T)
|
|
1093
2102
|
|
|
1094
2103
|
return {
|
|
1095
|
-
forwardMessage: {
|
|
1096
|
-
type: `delete`,
|
|
1097
|
-
key,
|
|
1098
|
-
},
|
|
1099
2104
|
operation: {
|
|
1100
2105
|
type: `delete`,
|
|
1101
2106
|
key,
|
|
@@ -1111,10 +2116,6 @@ class PersistedCollectionRuntime<
|
|
|
1111
2116
|
const previousValue = this.collection.get(key) ?? message.value
|
|
1112
2117
|
|
|
1113
2118
|
return {
|
|
1114
|
-
forwardMessage: {
|
|
1115
|
-
type: `delete`,
|
|
1116
|
-
key,
|
|
1117
|
-
},
|
|
1118
2119
|
operation: {
|
|
1119
2120
|
type: `delete`,
|
|
1120
2121
|
key,
|
|
@@ -1125,11 +2126,6 @@ class PersistedCollectionRuntime<
|
|
|
1125
2126
|
|
|
1126
2127
|
const key = this.collection.getKeyFromItem(message.value)
|
|
1127
2128
|
return {
|
|
1128
|
-
forwardMessage: {
|
|
1129
|
-
type: `update`,
|
|
1130
|
-
value: message.value,
|
|
1131
|
-
metadata: message.metadata,
|
|
1132
|
-
},
|
|
1133
2129
|
operation: {
|
|
1134
2130
|
type: `update`,
|
|
1135
2131
|
key,
|
|
@@ -1141,32 +2137,55 @@ class PersistedCollectionRuntime<
|
|
|
1141
2137
|
|
|
1142
2138
|
async persistAndConfirmCollectionMutations(
|
|
1143
2139
|
mutations: Array<PendingMutation<T>>,
|
|
2140
|
+
lifecycleGeneration = this.lifecycleGeneration,
|
|
1144
2141
|
): Promise<void> {
|
|
2142
|
+
this.throwIfTerminal()
|
|
2143
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
1145
2144
|
if (mutations.length === 0) {
|
|
1146
2145
|
return
|
|
1147
2146
|
}
|
|
1148
2147
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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),
|
|
1160
2163
|
)
|
|
1161
|
-
}
|
|
1162
2164
|
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
2183
|
}
|
|
1166
2184
|
|
|
1167
2185
|
async acceptTransactionMutations(transaction: {
|
|
1168
2186
|
mutations: Array<PendingMutation<Record<string, unknown>>>
|
|
1169
2187
|
}): Promise<void> {
|
|
2188
|
+
this.throwIfTerminal()
|
|
1170
2189
|
const collectionMutations = this.filterMutationsForCollection(
|
|
1171
2190
|
transaction.mutations,
|
|
1172
2191
|
)
|
|
@@ -1178,14 +2197,42 @@ class PersistedCollectionRuntime<
|
|
|
1178
2197
|
await this.persistAndConfirmCollectionMutations(collectionMutations)
|
|
1179
2198
|
}
|
|
1180
2199
|
|
|
1181
|
-
cleanup(): void {
|
|
1182
|
-
this.
|
|
2200
|
+
cleanup(): Promise<void> {
|
|
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
|
+
}
|
|
2211
|
+
|
|
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?.())
|
|
1183
2227
|
this.coordinatorUnsubscribe = null
|
|
1184
2228
|
|
|
1185
|
-
this.
|
|
2229
|
+
attempt(() => this.remoteSubsetOwnerUnsubscribe?.())
|
|
2230
|
+
this.remoteSubsetOwnerUnsubscribe = null
|
|
2231
|
+
|
|
2232
|
+
attempt(() => this.indexAddedUnsubscribe?.())
|
|
1186
2233
|
this.indexAddedUnsubscribe = null
|
|
1187
2234
|
|
|
1188
|
-
this.indexRemovedUnsubscribe?.()
|
|
2235
|
+
attempt(() => this.indexRemovedUnsubscribe?.())
|
|
1189
2236
|
this.indexRemovedUnsubscribe = null
|
|
1190
2237
|
|
|
1191
2238
|
if (this.remoteEnsureRetryTimer !== null) {
|
|
@@ -1195,12 +2242,37 @@ class PersistedCollectionRuntime<
|
|
|
1195
2242
|
|
|
1196
2243
|
this.pendingRemoteSubsetEnsures.clear()
|
|
1197
2244
|
this.activeSubsets.clear()
|
|
2245
|
+
this.hydratedDemands.clear()
|
|
1198
2246
|
for (const transaction of this.queuedHydrationTransactions) {
|
|
1199
2247
|
transaction.rejectApplied?.(new SyncTransactionAbortedError())
|
|
1200
2248
|
}
|
|
1201
2249
|
this.queuedHydrationTransactions.length = 0
|
|
1202
2250
|
this.queuedTxCommitted.length = 0
|
|
1203
2251
|
this.clearSyncControls()
|
|
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
|
+
})
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
private advanceLifecycle(): void {
|
|
2264
|
+
this.lifecycleGeneration++
|
|
2265
|
+
this.persistedReadiness?.set({ status: `loading` })
|
|
2266
|
+
this.startupSettled = false
|
|
2267
|
+
this.hydratedDemands.clear()
|
|
2268
|
+
this.startupMetadataPromise = null
|
|
2269
|
+
this.startPromise = null
|
|
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`)
|
|
1204
2276
|
}
|
|
1205
2277
|
|
|
1206
2278
|
private withInternalApply<TResult>(task: () => TResult): TResult {
|
|
@@ -1224,8 +2296,9 @@ class PersistedCollectionRuntime<
|
|
|
1224
2296
|
|
|
1225
2297
|
private loadSubsetRowsUnsafe(
|
|
1226
2298
|
options: LoadSubsetOptions,
|
|
2299
|
+
adapter: HydrationPersistenceAdapter,
|
|
1227
2300
|
): Promise<Array<{ key: TKey; value: T; metadata?: unknown }>> {
|
|
1228
|
-
return
|
|
2301
|
+
return adapter.loadSubset(this.collectionId, options, {
|
|
1229
2302
|
requiredIndexSignatures: this.getRequiredIndexSignatures(),
|
|
1230
2303
|
}) as Promise<Array<{ key: TKey; value: T; metadata?: unknown }>>
|
|
1231
2304
|
}
|
|
@@ -1246,6 +2319,7 @@ class PersistedCollectionRuntime<
|
|
|
1246
2319
|
async scanPersistedRows(
|
|
1247
2320
|
options?: PersistedRowScanOptions,
|
|
1248
2321
|
): Promise<Array<PersistedScannedRow<T, TKey>>> {
|
|
2322
|
+
this.throwIfTerminal()
|
|
1249
2323
|
return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options))
|
|
1250
2324
|
}
|
|
1251
2325
|
|
|
@@ -1253,37 +2327,249 @@ class PersistedCollectionRuntime<
|
|
|
1253
2327
|
options: LoadSubsetOptions,
|
|
1254
2328
|
config: {
|
|
1255
2329
|
requestRemoteEnsure: boolean
|
|
2330
|
+
lifecycleGeneration: number
|
|
2331
|
+
bindKeySetEvidence?: boolean
|
|
2332
|
+
requestLocalLoadFailure?: boolean
|
|
2333
|
+
rejectBufferedReplayFailure?: boolean
|
|
1256
2334
|
},
|
|
2335
|
+
adapter: HydrationPersistenceAdapter,
|
|
1257
2336
|
): Promise<void> {
|
|
1258
|
-
|
|
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
|
|
1259
2343
|
try {
|
|
1260
|
-
|
|
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
|
+
}
|
|
1261
2386
|
|
|
1262
|
-
this.
|
|
2387
|
+
if (config.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2388
|
+
replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter)
|
|
2389
|
+
if (config.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2390
|
+
|
|
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)
|
|
1263
2421
|
} finally {
|
|
1264
|
-
this.
|
|
2422
|
+
if (this.activeHydrationContext === hydrationContext) {
|
|
2423
|
+
this.activeHydrationContext = undefined
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (replayFailure && config.rejectBufferedReplayFailure) {
|
|
2427
|
+
throw replayFailure.reason
|
|
1265
2428
|
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
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
|
+
}
|
|
1266
2528
|
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
+
}
|
|
1269
2553
|
|
|
1270
|
-
|
|
1271
|
-
|
|
2554
|
+
private rejectQueuedHydrationTransactions(error: unknown): void {
|
|
2555
|
+
for (const transaction of this.queuedHydrationTransactions) {
|
|
2556
|
+
transaction.rejectApplied?.(error)
|
|
1272
2557
|
}
|
|
2558
|
+
this.queuedHydrationTransactions.length = 0
|
|
1273
2559
|
}
|
|
1274
2560
|
|
|
1275
2561
|
private applyRowsToCollection(
|
|
1276
2562
|
rows: Array<{ key: TKey; value: T; metadata?: unknown }>,
|
|
1277
|
-
):
|
|
2563
|
+
): SyncAppliedReceipt {
|
|
1278
2564
|
if (
|
|
1279
2565
|
!this.syncControls.begin ||
|
|
1280
2566
|
!this.syncControls.write ||
|
|
1281
2567
|
!this.syncControls.commit
|
|
1282
2568
|
) {
|
|
1283
|
-
return
|
|
2569
|
+
return true
|
|
1284
2570
|
}
|
|
1285
2571
|
|
|
1286
|
-
this.withInternalApply(() => {
|
|
2572
|
+
return this.withInternalApply(() => {
|
|
1287
2573
|
this.syncControls.begin?.({ immediate: true })
|
|
1288
2574
|
|
|
1289
2575
|
for (const row of rows) {
|
|
@@ -1297,21 +2583,81 @@ class PersistedCollectionRuntime<
|
|
|
1297
2583
|
})
|
|
1298
2584
|
}
|
|
1299
2585
|
|
|
1300
|
-
this.syncControls.commit?.()
|
|
2586
|
+
return this.syncControls.commit?.() ?? true
|
|
1301
2587
|
})
|
|
1302
2588
|
}
|
|
1303
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
|
+
|
|
1304
2650
|
private replaceCollectionSnapshot(
|
|
1305
2651
|
rows: Array<{ key: TKey; value: T; metadata?: unknown }>,
|
|
1306
2652
|
collectionMetadata: Array<{ key: string; value: unknown }>,
|
|
1307
|
-
):
|
|
2653
|
+
): SyncAppliedReceipt {
|
|
1308
2654
|
if (
|
|
1309
2655
|
!this.syncControls.begin ||
|
|
1310
2656
|
!this.syncControls.write ||
|
|
1311
2657
|
!this.syncControls.commit ||
|
|
1312
2658
|
!this.syncControls.metadata
|
|
1313
2659
|
) {
|
|
1314
|
-
return
|
|
2660
|
+
return true
|
|
1315
2661
|
}
|
|
1316
2662
|
|
|
1317
2663
|
const nextMetadata = new Map(
|
|
@@ -1321,7 +2667,7 @@ class PersistedCollectionRuntime<
|
|
|
1321
2667
|
.list()
|
|
1322
2668
|
.map(({ key }) => key)
|
|
1323
2669
|
|
|
1324
|
-
this.withInternalApply(() => {
|
|
2670
|
+
return this.withInternalApply(() => {
|
|
1325
2671
|
this.syncControls.begin?.({ immediate: true })
|
|
1326
2672
|
this.syncControls.truncate?.()
|
|
1327
2673
|
|
|
@@ -1343,18 +2689,25 @@ class PersistedCollectionRuntime<
|
|
|
1343
2689
|
this.syncControls.metadata?.collection.set(key, value)
|
|
1344
2690
|
})
|
|
1345
2691
|
|
|
1346
|
-
this.syncControls.commit?.()
|
|
2692
|
+
return this.syncControls.commit?.() ?? true
|
|
1347
2693
|
})
|
|
1348
2694
|
}
|
|
1349
2695
|
|
|
1350
|
-
private async flushQueuedHydrationTransactionsUnsafe(
|
|
2696
|
+
private async flushQueuedHydrationTransactionsUnsafe(
|
|
2697
|
+
adapter: HydrationPersistenceAdapter,
|
|
2698
|
+
): Promise<{ reason: unknown } | undefined> {
|
|
2699
|
+
let operationFailure: { reason: unknown } | undefined
|
|
1351
2700
|
while (this.queuedHydrationTransactions.length > 0) {
|
|
1352
2701
|
const transaction = this.queuedHydrationTransactions.shift()
|
|
1353
2702
|
if (!transaction) {
|
|
1354
2703
|
continue
|
|
1355
2704
|
}
|
|
1356
2705
|
try {
|
|
1357
|
-
await this.applyBufferedSyncTransactionUnsafe(
|
|
2706
|
+
const outcome = await this.applyBufferedSyncTransactionUnsafe(
|
|
2707
|
+
transaction,
|
|
2708
|
+
adapter,
|
|
2709
|
+
)
|
|
2710
|
+
operationFailure ??= outcome.hydrationFailure
|
|
1358
2711
|
} catch (error) {
|
|
1359
2712
|
transaction.rejectApplied?.(error)
|
|
1360
2713
|
for (const abandoned of this.queuedHydrationTransactions) {
|
|
@@ -1364,133 +2717,168 @@ class PersistedCollectionRuntime<
|
|
|
1364
2717
|
throw error
|
|
1365
2718
|
}
|
|
1366
2719
|
}
|
|
2720
|
+
return operationFailure
|
|
1367
2721
|
}
|
|
1368
2722
|
|
|
1369
2723
|
private async applyBufferedSyncTransactionUnsafe(
|
|
1370
2724
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
const applyToCollection = (): SyncAppliedReceipt => {
|
|
1384
|
-
begin()
|
|
1385
|
-
|
|
1386
|
-
if (transaction.truncate) {
|
|
1387
|
-
truncate?.()
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
for (const operation of transaction.operations) {
|
|
1391
|
-
if (operation.type === `delete`) {
|
|
1392
|
-
write({
|
|
1393
|
-
type: `delete`,
|
|
1394
|
-
key: operation.key,
|
|
1395
|
-
})
|
|
1396
|
-
} else {
|
|
1397
|
-
write({
|
|
1398
|
-
type: `update`,
|
|
1399
|
-
value: operation.value,
|
|
1400
|
-
metadata: operation.metadata,
|
|
1401
|
-
})
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
for (const [key, metadataWrite] of transaction.rowMetadataWrites) {
|
|
1406
|
-
if (metadataWrite.type === `delete`) {
|
|
1407
|
-
metadata?.row.delete(key)
|
|
1408
|
-
} else {
|
|
1409
|
-
metadata?.row.set(key, metadataWrite.value)
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
for (const [key, metadataWrite] of transaction.collectionMetadataWrites) {
|
|
1414
|
-
if (metadataWrite.type === `delete`) {
|
|
1415
|
-
metadata?.collection.delete(key)
|
|
1416
|
-
} else {
|
|
1417
|
-
metadata?.collection.set(key, metadataWrite.value)
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
return commit(transaction.signal)
|
|
1422
|
-
}
|
|
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
|
|
1423
2735
|
|
|
1424
2736
|
try {
|
|
1425
2737
|
const applied = transaction.internal
|
|
1426
|
-
? this.withInternalApply(applyToCollection)
|
|
1427
|
-
: applyToCollection()
|
|
2738
|
+
? this.withInternalApply(transaction.applyToCollection)
|
|
2739
|
+
: transaction.applyToCollection()
|
|
2740
|
+
applicationReturned = true
|
|
2741
|
+
abortedDuringApplication = transaction.signal?.aborted === true
|
|
1428
2742
|
if (applied !== true) {
|
|
1429
|
-
|
|
2743
|
+
this.sourcePublicationWaitDepth++
|
|
2744
|
+
try {
|
|
2745
|
+
await applied
|
|
2746
|
+
} finally {
|
|
2747
|
+
this.sourcePublicationWaitDepth--
|
|
2748
|
+
}
|
|
1430
2749
|
}
|
|
2750
|
+
this.throwIfLifecycleReplaced(transaction.lifecycleGeneration)
|
|
1431
2751
|
|
|
1432
2752
|
if (!transaction.internal) {
|
|
1433
|
-
await this.persistAndBroadcastExternalSyncTransactionUnsafe(
|
|
2753
|
+
await this.persistAndBroadcastExternalSyncTransactionUnsafe(
|
|
2754
|
+
transaction,
|
|
2755
|
+
scopedAdapter,
|
|
2756
|
+
)
|
|
1434
2757
|
}
|
|
1435
2758
|
transaction.resolveApplied?.()
|
|
2759
|
+
return { applied: true }
|
|
1436
2760
|
} catch (error) {
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
|
1439
2784
|
}
|
|
1440
2785
|
}
|
|
1441
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)
|
|
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)
|
|
2801
|
+
}
|
|
2802
|
+
|
|
1442
2803
|
private async persistAndBroadcastExternalSyncTransactionUnsafe(
|
|
1443
2804
|
transaction: BufferedSyncTransaction<T, TKey>,
|
|
2805
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
1444
2806
|
): Promise<void> {
|
|
1445
2807
|
if (transaction.internal) {
|
|
1446
2808
|
return
|
|
1447
2809
|
}
|
|
1448
2810
|
|
|
1449
|
-
|
|
1450
|
-
|
|
2811
|
+
this.throwIfLifecycleReplaced(transaction.lifecycleGeneration)
|
|
1451
2812
|
if (
|
|
1452
2813
|
!transaction.truncate &&
|
|
1453
2814
|
transaction.operations.length === 0 &&
|
|
1454
2815
|
transaction.rowMetadataWrites.size === 0 &&
|
|
1455
2816
|
transaction.collectionMetadataWrites.size === 0
|
|
1456
2817
|
) {
|
|
1457
|
-
this.publishTxCommittedEvent(
|
|
1458
|
-
this.createTxCommittedPayload({
|
|
1459
|
-
term: streamPosition.term,
|
|
1460
|
-
seq: streamPosition.seq,
|
|
1461
|
-
txId: safeRandomUUID(),
|
|
1462
|
-
latestRowVersion: streamPosition.rowVersion,
|
|
1463
|
-
changedRows: [],
|
|
1464
|
-
deletedKeys: [],
|
|
1465
|
-
requiresFullReload: true,
|
|
1466
|
-
}),
|
|
1467
|
-
)
|
|
1468
2818
|
return
|
|
1469
2819
|
}
|
|
1470
2820
|
|
|
2821
|
+
const streamPosition = this.nextLocalStreamPosition()
|
|
1471
2822
|
const tx = this.createPersistedTxFromOperations(transaction, streamPosition)
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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,
|
|
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,
|
|
1493
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)
|
|
1494
2882
|
}
|
|
1495
2883
|
|
|
1496
2884
|
private createPersistedTxFromOperations(
|
|
@@ -1572,9 +2960,9 @@ class PersistedCollectionRuntime<
|
|
|
1572
2960
|
}
|
|
1573
2961
|
}
|
|
1574
2962
|
|
|
1575
|
-
private confirmMutationsSyncUnsafe(
|
|
2963
|
+
private async confirmMutationsSyncUnsafe(
|
|
1576
2964
|
mutations: Array<PendingMutation<T>>,
|
|
1577
|
-
): void {
|
|
2965
|
+
): Promise<void> {
|
|
1578
2966
|
if (
|
|
1579
2967
|
!this.syncControls.begin ||
|
|
1580
2968
|
!this.syncControls.write ||
|
|
@@ -1583,7 +2971,7 @@ class PersistedCollectionRuntime<
|
|
|
1583
2971
|
return
|
|
1584
2972
|
}
|
|
1585
2973
|
|
|
1586
|
-
this.withInternalApply(() => {
|
|
2974
|
+
const applied = this.withInternalApply(() => {
|
|
1587
2975
|
this.syncControls.begin?.({ immediate: true })
|
|
1588
2976
|
|
|
1589
2977
|
for (const mutation of mutations) {
|
|
@@ -1600,8 +2988,12 @@ class PersistedCollectionRuntime<
|
|
|
1600
2988
|
}
|
|
1601
2989
|
}
|
|
1602
2990
|
|
|
1603
|
-
this.syncControls.commit?.()
|
|
2991
|
+
return this.syncControls.commit?.() ?? true
|
|
1604
2992
|
})
|
|
2993
|
+
|
|
2994
|
+
if (applied !== true) {
|
|
2995
|
+
await applied
|
|
2996
|
+
}
|
|
1605
2997
|
}
|
|
1606
2998
|
|
|
1607
2999
|
private filterMutationsForCollection(
|
|
@@ -1618,7 +3010,9 @@ class PersistedCollectionRuntime<
|
|
|
1618
3010
|
|
|
1619
3011
|
private async persistCollectionMutationsUnsafe(
|
|
1620
3012
|
mutations: Array<PendingMutation<T>>,
|
|
3013
|
+
lifecycleGeneration: number,
|
|
1621
3014
|
): Promise<Array<string>> {
|
|
3015
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
1622
3016
|
// When a coordinator with requestApplyLocalMutations is available, always
|
|
1623
3017
|
// route through it — even on the leader tab. This ensures the coordinator's
|
|
1624
3018
|
// seq/rowVersion counters stay in sync with actual writes. Without this,
|
|
@@ -1638,7 +3032,20 @@ class PersistedCollectionRuntime<
|
|
|
1638
3032
|
envelopeMutations,
|
|
1639
3033
|
)
|
|
1640
3034
|
|
|
3035
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
3036
|
+
|
|
1641
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
|
+
}
|
|
1642
3049
|
throw new Error(
|
|
1643
3050
|
`failed to apply local mutations through coordinator: ${response.error}`,
|
|
1644
3051
|
)
|
|
@@ -1673,7 +3080,9 @@ class PersistedCollectionRuntime<
|
|
|
1673
3080
|
// SingleProcessCoordinator). Apply directly and broadcast.
|
|
1674
3081
|
const streamPosition = this.nextLocalStreamPosition()
|
|
1675
3082
|
const tx = this.createPersistedTxFromMutations(mutations, streamPosition)
|
|
1676
|
-
await this.
|
|
3083
|
+
await this.applyCommittedTx(tx, lifecycleGeneration)
|
|
3084
|
+
|
|
3085
|
+
this.throwIfLifecycleReplaced(lifecycleGeneration)
|
|
1677
3086
|
|
|
1678
3087
|
this.publishTxCommittedEvent(
|
|
1679
3088
|
this.createTxCommittedPayload({
|
|
@@ -1808,39 +3217,54 @@ class PersistedCollectionRuntime<
|
|
|
1808
3217
|
}
|
|
1809
3218
|
|
|
1810
3219
|
private getSubsetKey(options: LoadSubsetOptions): string {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
3220
|
+
// A subscription can own several independent acquisitions, including
|
|
3221
|
+
// identical requests. Only releasing this options object ends its lease.
|
|
3222
|
+
let id = this.requestIds.get(options)
|
|
3223
|
+
if (id === undefined) {
|
|
3224
|
+
id = `request:${++this.nextRequestId}`
|
|
3225
|
+
this.requestIds.set(options, id)
|
|
3226
|
+
}
|
|
3227
|
+
return id
|
|
3228
|
+
}
|
|
1817
3229
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
this.
|
|
1821
|
-
|
|
3230
|
+
private canRouteRemoteDemandThroughCoordinator(): boolean {
|
|
3231
|
+
if (
|
|
3232
|
+
this.getCurrentTerminalFailure() ||
|
|
3233
|
+
this.mode !== `sync-present` ||
|
|
3234
|
+
this.persistence.coordinator instanceof SingleProcessCoordinator
|
|
3235
|
+
) {
|
|
3236
|
+
return false
|
|
1822
3237
|
}
|
|
1823
3238
|
|
|
1824
|
-
|
|
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
|
+
)
|
|
1825
3245
|
}
|
|
1826
3246
|
|
|
1827
3247
|
private queueRemoteSubsetEnsure(options: LoadSubsetOptions): void {
|
|
1828
3248
|
if (
|
|
1829
|
-
|
|
1830
|
-
!this.
|
|
3249
|
+
options.signal?.aborted ||
|
|
3250
|
+
!this.canRouteRemoteDemandThroughCoordinator() ||
|
|
1831
3251
|
this.activeSubsets.get(this.getSubsetKey(options)) !== options
|
|
1832
3252
|
) {
|
|
1833
3253
|
return
|
|
1834
3254
|
}
|
|
1835
3255
|
|
|
1836
|
-
this.
|
|
3256
|
+
const subsetKey = this.getSubsetKey(options)
|
|
3257
|
+
if (this.activeSubsets.get(subsetKey) !== options) return
|
|
3258
|
+
|
|
3259
|
+
this.pendingRemoteSubsetEnsures.set(subsetKey, options)
|
|
1837
3260
|
void this.flushPendingRemoteSubsetEnsures()
|
|
1838
3261
|
}
|
|
1839
3262
|
|
|
1840
3263
|
private scheduleRemoteEnsureRetry(): void {
|
|
1841
3264
|
if (
|
|
3265
|
+
this.getCurrentTerminalFailure() ||
|
|
1842
3266
|
this.mode !== `sync-present` ||
|
|
1843
|
-
|
|
3267
|
+
this.persistence.coordinator instanceof SingleProcessCoordinator
|
|
1844
3268
|
) {
|
|
1845
3269
|
return
|
|
1846
3270
|
}
|
|
@@ -1860,8 +3284,9 @@ class PersistedCollectionRuntime<
|
|
|
1860
3284
|
|
|
1861
3285
|
private async flushPendingRemoteSubsetEnsures(): Promise<void> {
|
|
1862
3286
|
if (
|
|
3287
|
+
this.getCurrentTerminalFailure() ||
|
|
1863
3288
|
this.mode !== `sync-present` ||
|
|
1864
|
-
|
|
3289
|
+
this.persistence.coordinator instanceof SingleProcessCoordinator
|
|
1865
3290
|
) {
|
|
1866
3291
|
return
|
|
1867
3292
|
}
|
|
@@ -1872,6 +3297,13 @@ class PersistedCollectionRuntime<
|
|
|
1872
3297
|
}
|
|
1873
3298
|
|
|
1874
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
|
+
}
|
|
1875
3307
|
try {
|
|
1876
3308
|
await this.persistence.coordinator.requestEnsureRemoteSubset(
|
|
1877
3309
|
this.collectionId,
|
|
@@ -1879,7 +3311,14 @@ class PersistedCollectionRuntime<
|
|
|
1879
3311
|
)
|
|
1880
3312
|
this.pendingRemoteSubsetEnsures.delete(subsetKey)
|
|
1881
3313
|
} catch (error) {
|
|
1882
|
-
|
|
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
|
+
}
|
|
1883
3322
|
}
|
|
1884
3323
|
}
|
|
1885
3324
|
|
|
@@ -1891,6 +3330,10 @@ class PersistedCollectionRuntime<
|
|
|
1891
3330
|
return
|
|
1892
3331
|
}
|
|
1893
3332
|
|
|
3333
|
+
this.persistence.coordinator.setAdapterForCollection?.(
|
|
3334
|
+
this.collectionId,
|
|
3335
|
+
this.persistence.adapter,
|
|
3336
|
+
)
|
|
1894
3337
|
this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
|
|
1895
3338
|
this.collectionId,
|
|
1896
3339
|
(message) => {
|
|
@@ -1903,23 +3346,30 @@ class PersistedCollectionRuntime<
|
|
|
1903
3346
|
if (message.collectionId !== this.collectionId) {
|
|
1904
3347
|
return
|
|
1905
3348
|
}
|
|
3349
|
+
if (this.getCurrentTerminalFailure()) {
|
|
3350
|
+
return
|
|
3351
|
+
}
|
|
1906
3352
|
|
|
1907
3353
|
const { payload } = message
|
|
3354
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1908
3355
|
const isSelf = message.senderId === this.persistence.coordinator.getNodeId()
|
|
1909
3356
|
|
|
1910
3357
|
// Allow tx:committed from self — the coordinator produces these on behalf
|
|
1911
3358
|
// of both local and remote mutations. The seq dedup in
|
|
1912
3359
|
// processCommittedTxUnsafe prevents double-processing of our own writes.
|
|
1913
3360
|
if (isTxCommittedPayload(payload)) {
|
|
1914
|
-
if (this.
|
|
1915
|
-
this.queuedTxCommitted.push(
|
|
3361
|
+
if (this.isHydratingNow()) {
|
|
3362
|
+
this.queuedTxCommitted.push({
|
|
3363
|
+
txCommitted: payload,
|
|
3364
|
+
lifecycleGeneration,
|
|
3365
|
+
})
|
|
1916
3366
|
return
|
|
1917
3367
|
}
|
|
1918
3368
|
|
|
1919
3369
|
void this.applyMutex
|
|
1920
|
-
.run(() => this.processCommittedTxUnsafe(payload))
|
|
3370
|
+
.run(() => this.processCommittedTxUnsafe(payload, lifecycleGeneration))
|
|
1921
3371
|
.catch((error) => {
|
|
1922
|
-
|
|
3372
|
+
this.markTerminalFailure(error, lifecycleGeneration)
|
|
1923
3373
|
})
|
|
1924
3374
|
return
|
|
1925
3375
|
}
|
|
@@ -1931,9 +3381,16 @@ class PersistedCollectionRuntime<
|
|
|
1931
3381
|
|
|
1932
3382
|
if (isCollectionResetPayload(payload)) {
|
|
1933
3383
|
void this.applyMutex
|
|
1934
|
-
.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
|
+
})
|
|
1935
3392
|
.catch((error) => {
|
|
1936
|
-
|
|
3393
|
+
this.markTerminalFailure(error, lifecycleGeneration)
|
|
1937
3394
|
})
|
|
1938
3395
|
}
|
|
1939
3396
|
}
|
|
@@ -1944,13 +3401,18 @@ class PersistedCollectionRuntime<
|
|
|
1944
3401
|
if (!queued) {
|
|
1945
3402
|
continue
|
|
1946
3403
|
}
|
|
1947
|
-
await this.processCommittedTxUnsafe(
|
|
3404
|
+
await this.processCommittedTxUnsafe(
|
|
3405
|
+
queued.txCommitted,
|
|
3406
|
+
queued.lifecycleGeneration,
|
|
3407
|
+
)
|
|
1948
3408
|
}
|
|
1949
3409
|
}
|
|
1950
3410
|
|
|
1951
3411
|
private async processCommittedTxUnsafe(
|
|
1952
3412
|
txCommitted: TxCommitted,
|
|
3413
|
+
lifecycleGeneration = this.lifecycleGeneration,
|
|
1953
3414
|
): Promise<void> {
|
|
3415
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1954
3416
|
if (txCommitted.term < this.latestTerm) {
|
|
1955
3417
|
return
|
|
1956
3418
|
}
|
|
@@ -1970,7 +3432,8 @@ class PersistedCollectionRuntime<
|
|
|
1970
3432
|
const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms
|
|
1971
3433
|
|
|
1972
3434
|
if (hasGap) {
|
|
1973
|
-
await this.recoverFromSeqGapUnsafe()
|
|
3435
|
+
await this.recoverFromSeqGapUnsafe(lifecycleGeneration)
|
|
3436
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1974
3437
|
if (
|
|
1975
3438
|
txCommitted.term < this.latestTerm ||
|
|
1976
3439
|
(txCommitted.term === this.latestTerm &&
|
|
@@ -1986,16 +3449,32 @@ class PersistedCollectionRuntime<
|
|
|
1986
3449
|
txCommitted.latestRowVersion,
|
|
1987
3450
|
)
|
|
1988
3451
|
|
|
1989
|
-
await this.invalidateFromCommittedTxUnsafe(
|
|
3452
|
+
await this.invalidateFromCommittedTxUnsafe(
|
|
3453
|
+
txCommitted,
|
|
3454
|
+
this.persistence.adapter,
|
|
3455
|
+
)
|
|
3456
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
3457
|
+
await this.flushQueuedTxCommittedUnsafe()
|
|
3458
|
+
}
|
|
1990
3459
|
}
|
|
1991
3460
|
|
|
1992
|
-
private async recoverFromSeqGapUnsafe(
|
|
3461
|
+
private async recoverFromSeqGapUnsafe(
|
|
3462
|
+
lifecycleGeneration: number,
|
|
3463
|
+
): Promise<void> {
|
|
3464
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1993
3465
|
if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
|
|
3466
|
+
let pullResponse: PullSinceResponse | undefined
|
|
1994
3467
|
try {
|
|
1995
|
-
|
|
3468
|
+
pullResponse = await this.persistence.coordinator.pullSince(
|
|
1996
3469
|
this.collectionId,
|
|
1997
3470
|
this.latestRowVersion,
|
|
1998
3471
|
)
|
|
3472
|
+
} catch (error) {
|
|
3473
|
+
console.warn(`Failed pullSince recovery attempt:`, error)
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
if (pullResponse) {
|
|
3477
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1999
3478
|
|
|
2000
3479
|
if (pullResponse.ok) {
|
|
2001
3480
|
this.observeStreamPosition(
|
|
@@ -2003,33 +3482,51 @@ class PersistedCollectionRuntime<
|
|
|
2003
3482
|
pullResponse.latestSeq,
|
|
2004
3483
|
pullResponse.latestRowVersion,
|
|
2005
3484
|
)
|
|
2006
|
-
if (pullResponse.requiresFullReload
|
|
2007
|
-
await this.
|
|
3485
|
+
if (pullResponse.requiresFullReload) {
|
|
3486
|
+
await this.runInHydrationScope((adapter) =>
|
|
3487
|
+
this.reloadActiveSubsetsUnsafe(adapter),
|
|
3488
|
+
)
|
|
2008
3489
|
return
|
|
2009
3490
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
await this.
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
txId: delta.txId,
|
|
2017
|
-
latestRowVersion: delta.latestRowVersion,
|
|
2018
|
-
requiresFullReload: false,
|
|
2019
|
-
changedRows: delta.changedRows,
|
|
2020
|
-
deletedKeys: delta.deletedKeys,
|
|
2021
|
-
rowMetadataMutations: delta.rowMetadataMutations,
|
|
2022
|
-
collectionMetadataMutations: delta.collectionMetadataMutations,
|
|
2023
|
-
})
|
|
3491
|
+
const deltas = pullResponse.deltas
|
|
3492
|
+
if (!deltas) {
|
|
3493
|
+
await this.runInHydrationScope((adapter) =>
|
|
3494
|
+
this.reloadActiveSubsetsUnsafe(adapter),
|
|
3495
|
+
)
|
|
3496
|
+
return
|
|
2024
3497
|
}
|
|
3498
|
+
|
|
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
|
+
})
|
|
2025
3521
|
return
|
|
2026
3522
|
}
|
|
2027
|
-
} catch (error) {
|
|
2028
|
-
console.warn(`Failed pullSince recovery attempt:`, error)
|
|
2029
3523
|
}
|
|
2030
3524
|
}
|
|
2031
3525
|
|
|
2032
|
-
|
|
3526
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
3527
|
+
await this.runInHydrationScope((adapter) =>
|
|
3528
|
+
this.truncateAndReloadUnsafe(adapter, lifecycleGeneration),
|
|
3529
|
+
)
|
|
2033
3530
|
|
|
2034
3531
|
if (this.mode === `sync-present`) {
|
|
2035
3532
|
for (const options of this.activeSubsets.values()) {
|
|
@@ -2038,30 +3535,46 @@ class PersistedCollectionRuntime<
|
|
|
2038
3535
|
}
|
|
2039
3536
|
}
|
|
2040
3537
|
|
|
2041
|
-
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++
|
|
2042
3546
|
if (this.syncControls.begin && this.syncControls.commit) {
|
|
2043
|
-
this.withInternalApply(() => {
|
|
3547
|
+
const applied = this.withInternalApply(() => {
|
|
2044
3548
|
this.syncControls.begin?.({ immediate: true })
|
|
2045
3549
|
this.syncControls.truncate?.()
|
|
2046
|
-
this.syncControls.commit?.()
|
|
3550
|
+
return this.syncControls.commit?.() ?? true
|
|
2047
3551
|
})
|
|
3552
|
+
if (applied !== true) await applied
|
|
2048
3553
|
}
|
|
2049
3554
|
|
|
2050
|
-
|
|
3555
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
3556
|
+
await this.reloadActiveSubsetsUnsafe(adapter)
|
|
2051
3557
|
}
|
|
2052
3558
|
|
|
2053
3559
|
private async invalidateFromCommittedTxUnsafe(
|
|
2054
3560
|
txCommitted: TxCommitted,
|
|
3561
|
+
adapter: HydrationPersistenceAdapter,
|
|
2055
3562
|
): Promise<void> {
|
|
3563
|
+
const reloadActiveSubsets = () =>
|
|
3564
|
+
this.runInHydrationScope(
|
|
3565
|
+
(scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
|
|
3566
|
+
adapter,
|
|
3567
|
+
)
|
|
3568
|
+
|
|
2056
3569
|
if (txCommitted.requiresFullReload) {
|
|
2057
|
-
await
|
|
3570
|
+
await reloadActiveSubsets()
|
|
2058
3571
|
return
|
|
2059
3572
|
}
|
|
2060
3573
|
|
|
2061
3574
|
const changedKeyCount =
|
|
2062
3575
|
txCommitted.changedRows.length + txCommitted.deletedKeys.length
|
|
2063
3576
|
if (changedKeyCount > TARGETED_INVALIDATION_KEY_LIMIT) {
|
|
2064
|
-
await
|
|
3577
|
+
await reloadActiveSubsets()
|
|
2065
3578
|
return
|
|
2066
3579
|
}
|
|
2067
3580
|
|
|
@@ -2076,7 +3589,7 @@ class PersistedCollectionRuntime<
|
|
|
2076
3589
|
|
|
2077
3590
|
// Has paginated subsets — fall back to full reload.
|
|
2078
3591
|
// Targeted invalidation for paginated subsets is deferred to a future iteration.
|
|
2079
|
-
await
|
|
3592
|
+
await reloadActiveSubsets()
|
|
2080
3593
|
}
|
|
2081
3594
|
|
|
2082
3595
|
private async applyTargetedInvalidationUnsafe(
|
|
@@ -2086,7 +3599,7 @@ class PersistedCollectionRuntime<
|
|
|
2086
3599
|
(opt) => (opt.where ? compileSingleRowExpression(opt.where) : null),
|
|
2087
3600
|
)
|
|
2088
3601
|
|
|
2089
|
-
this.withInternalApply(() => {
|
|
3602
|
+
const applied = this.withInternalApply(() => {
|
|
2090
3603
|
this.syncControls.begin?.({ immediate: true })
|
|
2091
3604
|
|
|
2092
3605
|
for (const {
|
|
@@ -2131,22 +3644,34 @@ class PersistedCollectionRuntime<
|
|
|
2131
3644
|
}
|
|
2132
3645
|
})
|
|
2133
3646
|
|
|
2134
|
-
this.syncControls.commit?.()
|
|
3647
|
+
return this.syncControls.commit?.() ?? true
|
|
2135
3648
|
})
|
|
3649
|
+
if (applied !== true) await applied
|
|
2136
3650
|
}
|
|
2137
3651
|
|
|
2138
|
-
private async reloadActiveSubsetsUnsafe(
|
|
3652
|
+
private async reloadActiveSubsetsUnsafe(
|
|
3653
|
+
adapter: HydrationPersistenceAdapter,
|
|
3654
|
+
): Promise<void> {
|
|
3655
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
3656
|
+
const truncateGeneration = this.sourceTruncateGeneration
|
|
2139
3657
|
const activeSubsetOptions =
|
|
2140
3658
|
this.activeSubsets.size > 0
|
|
2141
3659
|
? Array.from(this.activeSubsets.values())
|
|
2142
3660
|
: [{}]
|
|
2143
3661
|
|
|
2144
|
-
this.
|
|
3662
|
+
this.hydratedDemands.clear()
|
|
3663
|
+
this.hydrationSequence++
|
|
3664
|
+
const hydrationContext = { suppliedRowKeys: new Set<TKey>() }
|
|
3665
|
+
this.activeHydrationContext = hydrationContext
|
|
3666
|
+
this.hydratingGeneration = lifecycleGeneration
|
|
2145
3667
|
try {
|
|
2146
3668
|
const mergedRows = new Map<TKey, { value: T; metadata?: unknown }>()
|
|
2147
|
-
const collectionMetadata =
|
|
3669
|
+
const collectionMetadata =
|
|
3670
|
+
await this.loadCollectionMetadataSnapshot(adapter)
|
|
3671
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2148
3672
|
for (const options of activeSubsetOptions) {
|
|
2149
|
-
const subsetRows = await this.loadSubsetRowsUnsafe(options)
|
|
3673
|
+
const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter)
|
|
3674
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2150
3675
|
for (const row of subsetRows) {
|
|
2151
3676
|
mergedRows.set(row.key, {
|
|
2152
3677
|
value: row.value,
|
|
@@ -2155,7 +3680,11 @@ class PersistedCollectionRuntime<
|
|
|
2155
3680
|
}
|
|
2156
3681
|
}
|
|
2157
3682
|
|
|
2158
|
-
|
|
3683
|
+
for (const key of mergedRows.keys()) {
|
|
3684
|
+
hydrationContext.suppliedRowKeys.add(key)
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
const applied = this.replaceCollectionSnapshot(
|
|
2159
3688
|
Array.from(mergedRows.entries()).map(([key, row]) => ({
|
|
2160
3689
|
key,
|
|
2161
3690
|
value: row.value,
|
|
@@ -2163,12 +3692,34 @@ class PersistedCollectionRuntime<
|
|
|
2163
3692
|
})),
|
|
2164
3693
|
collectionMetadata,
|
|
2165
3694
|
)
|
|
3695
|
+
if (applied !== true) await applied
|
|
2166
3696
|
} finally {
|
|
2167
|
-
|
|
3697
|
+
if (
|
|
3698
|
+
this.activeHydrationContext === hydrationContext &&
|
|
3699
|
+
this.hydratingGeneration === lifecycleGeneration
|
|
3700
|
+
) {
|
|
3701
|
+
this.hydratingGeneration = null
|
|
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
|
+
}
|
|
2168
3722
|
}
|
|
2169
|
-
|
|
2170
|
-
await this.flushQueuedHydrationTransactionsUnsafe()
|
|
2171
|
-
await this.flushQueuedTxCommittedUnsafe()
|
|
2172
3723
|
}
|
|
2173
3724
|
|
|
2174
3725
|
private attachIndexLifecycleListeners(): void {
|
|
@@ -2193,16 +3744,33 @@ class PersistedCollectionRuntime<
|
|
|
2193
3744
|
|
|
2194
3745
|
private async bootstrapPersistedIndexes(
|
|
2195
3746
|
indexMetadataSnapshot?: Array<CollectionIndexMetadata>,
|
|
2196
|
-
|
|
3747
|
+
adapter: HydrationPersistenceAdapter = this.persistence.adapter,
|
|
3748
|
+
): Promise<Set<string>> {
|
|
2197
3749
|
const collection = this.collection
|
|
2198
3750
|
if (!collection && !indexMetadataSnapshot) {
|
|
2199
|
-
return
|
|
3751
|
+
return new Set()
|
|
2200
3752
|
}
|
|
2201
3753
|
|
|
2202
3754
|
const indexMetadata =
|
|
2203
3755
|
indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? []
|
|
3756
|
+
const completedLocalIndexSignatures = new Set<string>()
|
|
3757
|
+
for (const metadata of indexMetadata) {
|
|
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> {
|
|
2204
3769
|
for (const metadata of indexMetadata) {
|
|
2205
|
-
await this.
|
|
3770
|
+
await this.requestCoordinatorPersistedIndex(
|
|
3771
|
+
metadata,
|
|
3772
|
+
completedLocalIndexSignatures.has(metadata.signature),
|
|
3773
|
+
)
|
|
2206
3774
|
}
|
|
2207
3775
|
}
|
|
2208
3776
|
|
|
@@ -2221,24 +3789,47 @@ class PersistedCollectionRuntime<
|
|
|
2221
3789
|
|
|
2222
3790
|
private async ensurePersistedIndex(
|
|
2223
3791
|
indexMetadata: CollectionIndexMetadata,
|
|
3792
|
+
adapter: HydrationPersistenceAdapter = this.persistence.adapter,
|
|
2224
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> {
|
|
2225
3805
|
const spec = this.buildPersistedIndexSpec(indexMetadata)
|
|
2226
3806
|
|
|
2227
3807
|
try {
|
|
2228
|
-
await
|
|
3808
|
+
await adapter.ensureIndex(
|
|
2229
3809
|
this.collectionId,
|
|
2230
3810
|
indexMetadata.signature,
|
|
2231
3811
|
spec,
|
|
2232
3812
|
)
|
|
3813
|
+
return true
|
|
2233
3814
|
} catch (error) {
|
|
2234
3815
|
console.warn(`Failed to ensure persisted index in adapter:`, error)
|
|
3816
|
+
return false
|
|
2235
3817
|
}
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
private async requestCoordinatorPersistedIndex(
|
|
3821
|
+
indexMetadata: CollectionIndexMetadata,
|
|
3822
|
+
completedLocally: boolean,
|
|
3823
|
+
): Promise<void> {
|
|
3824
|
+
const spec = this.buildPersistedIndexSpec(indexMetadata)
|
|
2236
3825
|
|
|
2237
3826
|
try {
|
|
2238
3827
|
await this.persistence.coordinator.requestEnsurePersistedIndex(
|
|
2239
3828
|
this.collectionId,
|
|
2240
3829
|
indexMetadata.signature,
|
|
2241
3830
|
spec,
|
|
3831
|
+
completedLocally ? this.persistence.adapter : undefined,
|
|
3832
|
+
completedLocally,
|
|
2242
3833
|
)
|
|
2243
3834
|
} catch (error) {
|
|
2244
3835
|
console.warn(
|
|
@@ -2272,67 +3863,406 @@ function createWrappedSyncConfig<
|
|
|
2272
3863
|
>(
|
|
2273
3864
|
sourceSyncConfig: SyncConfig<T, TKey>,
|
|
2274
3865
|
runtime: PersistedCollectionRuntime<T, TKey>,
|
|
3866
|
+
persistedReadiness?: PersistedReadinessTracker,
|
|
2275
3867
|
): SyncConfig<T, TKey> {
|
|
2276
3868
|
return {
|
|
2277
3869
|
...sourceSyncConfig,
|
|
2278
3870
|
sync: (params) => {
|
|
2279
3871
|
const transactionStack: Array<OpenSyncTransaction<T, TKey>> = []
|
|
3872
|
+
const pendingPublicationTransactions: Array<
|
|
3873
|
+
OpenSyncTransaction<T, TKey>
|
|
3874
|
+
> = []
|
|
2280
3875
|
const getOpenTransaction = () =>
|
|
2281
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
|
+
}
|
|
2282
3985
|
let fullStartPromise: Promise<void> | null = null
|
|
2283
|
-
|
|
3986
|
+
let sourceResultPromise: Promise<SyncConfigRes> | null = null
|
|
3987
|
+
let resolveSourceResultAssigned!: () => void
|
|
3988
|
+
const sourceResultAssigned = new Promise<void>((resolve) => {
|
|
3989
|
+
resolveSourceResultAssigned = resolve
|
|
3990
|
+
})
|
|
3991
|
+
const startupState = { cleanedUp: false }
|
|
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
|
+
}
|
|
2284
4123
|
runtime.setSyncControls({
|
|
2285
4124
|
begin: params.begin,
|
|
2286
4125
|
write: params.write as SyncControlFns<T, TKey>[`write`],
|
|
2287
4126
|
commit: params.commit,
|
|
2288
4127
|
truncate: params.truncate,
|
|
2289
4128
|
metadata: params.metadata ?? null,
|
|
4129
|
+
markError: params.markError,
|
|
2290
4130
|
})
|
|
2291
4131
|
runtime.setCollection(
|
|
2292
4132
|
params.collection as Collection<T, TKey, PersistedCollectionUtils>,
|
|
2293
4133
|
)
|
|
2294
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
|
+
|
|
2295
4175
|
const wrappedParams = {
|
|
2296
4176
|
...params,
|
|
4177
|
+
markError: reportUpstreamError,
|
|
2297
4178
|
markReady: () => {
|
|
4179
|
+
if (startupState.cleanedUp || getTerminalFailure()) return
|
|
2298
4180
|
void (fullStartPromise ?? runtime.ensureStarted())
|
|
2299
|
-
.then(() => {
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
error
|
|
2306
|
-
|
|
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
|
|
2307
4191
|
params.markReady()
|
|
2308
4192
|
})
|
|
4193
|
+
.catch(() => undefined)
|
|
2309
4194
|
},
|
|
2310
4195
|
begin: (options?: { immediate?: boolean }) => {
|
|
4196
|
+
if (startupState.cleanedUp) return undefined
|
|
4197
|
+
const terminalFailure = getTerminalFailure()
|
|
4198
|
+
const internal = runtime.isApplyingInternally()
|
|
2311
4199
|
const transaction: OpenSyncTransaction<T, TKey> = {
|
|
2312
4200
|
operations: [],
|
|
4201
|
+
partialUpdateOperationIndexes: new Set(),
|
|
2313
4202
|
rowMetadataWrites: new Map(),
|
|
2314
4203
|
collectionMetadataWrites: new Map(),
|
|
4204
|
+
deferredHydrationMetadataDeleteKeys: new Set(),
|
|
2315
4205
|
truncate: false,
|
|
2316
|
-
internal
|
|
4206
|
+
internal,
|
|
4207
|
+
lifecycleGeneration: runtime.getLifecycleGeneration(),
|
|
4208
|
+
beginOptions: options,
|
|
4209
|
+
operationKeys: new Set(),
|
|
4210
|
+
hasDependentSuccessor: false,
|
|
2317
4211
|
queuedBecauseHydrating:
|
|
2318
|
-
|
|
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 }),
|
|
2319
4226
|
}
|
|
2320
4227
|
transactionStack.push(transaction)
|
|
2321
|
-
|
|
2322
|
-
if (!transaction.queuedBecauseHydrating) {
|
|
2323
|
-
params.begin(options)
|
|
2324
|
-
}
|
|
2325
4228
|
},
|
|
2326
4229
|
write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => {
|
|
2327
|
-
|
|
4230
|
+
if (startupState.cleanedUp) return
|
|
2328
4231
|
const openTransaction = getOpenTransaction()
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
4232
|
+
const terminalFailure =
|
|
4233
|
+
openTransaction?.terminalFailure ?? getTerminalFailure()
|
|
4234
|
+
if (terminalFailure) {
|
|
4235
|
+
if (openTransaction)
|
|
4236
|
+
openTransaction.terminalFailure = terminalFailure
|
|
2332
4237
|
return
|
|
2333
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
|
+
}
|
|
2334
4263
|
|
|
2335
4264
|
openTransaction.operations.push(normalization.operation)
|
|
4265
|
+
openTransaction.operationKeys.add(normalization.operation.key)
|
|
2336
4266
|
if (normalization.operation.type === `delete`) {
|
|
2337
4267
|
openTransaction.rowMetadataWrites.set(normalization.operation.key, {
|
|
2338
4268
|
type: `delete`,
|
|
@@ -2344,7 +4274,11 @@ function createWrappedSyncConfig<
|
|
|
2344
4274
|
// Reset stale metadata for a fresh insert, but don't clobber an
|
|
2345
4275
|
// explicit metadata write already queued for this key in the same
|
|
2346
4276
|
// transaction (e.g. query reconcile stamps owners, then inserts).
|
|
2347
|
-
if (
|
|
4277
|
+
if (openTransaction.queuedBecauseHydrating) {
|
|
4278
|
+
openTransaction.deferredHydrationMetadataDeleteKeys.add(
|
|
4279
|
+
normalization.operation.key,
|
|
4280
|
+
)
|
|
4281
|
+
} else if (
|
|
2348
4282
|
!openTransaction.rowMetadataWrites.has(
|
|
2349
4283
|
normalization.operation.key,
|
|
2350
4284
|
)
|
|
@@ -2362,14 +4296,13 @@ function createWrappedSyncConfig<
|
|
|
2362
4296
|
value: normalization.operation.metadata,
|
|
2363
4297
|
})
|
|
2364
4298
|
}
|
|
2365
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2366
|
-
params.write(normalization.forwardMessage)
|
|
2367
|
-
}
|
|
2368
4299
|
},
|
|
2369
4300
|
metadata: params.metadata
|
|
2370
4301
|
? {
|
|
4302
|
+
persistence: persistenceCapability,
|
|
2371
4303
|
row: {
|
|
2372
4304
|
get: (key: TKey) => {
|
|
4305
|
+
if (startupState.cleanedUp) return undefined
|
|
2373
4306
|
const openTransaction = getOpenTransaction()
|
|
2374
4307
|
const pendingWrite =
|
|
2375
4308
|
openTransaction?.rowMetadataWrites.get(key)
|
|
@@ -2381,12 +4314,19 @@ function createWrappedSyncConfig<
|
|
|
2381
4314
|
if (openTransaction?.truncate) {
|
|
2382
4315
|
return undefined
|
|
2383
4316
|
}
|
|
4317
|
+
const pending = getPendingRowMetadataWrite(key)
|
|
4318
|
+
if (pending.found) {
|
|
4319
|
+
return pending.write.type === `delete`
|
|
4320
|
+
? undefined
|
|
4321
|
+
: pending.write.value
|
|
4322
|
+
}
|
|
2384
4323
|
return params.metadata!.row.get(key)
|
|
2385
4324
|
},
|
|
2386
|
-
scanPersisted: (options?: PersistedRowScanOptions) =>
|
|
2387
|
-
runtime.scanPersistedRows(options),
|
|
2388
4325
|
set: (key: TKey, value: unknown) => {
|
|
4326
|
+
if (startupState.cleanedUp) return
|
|
2389
4327
|
const openTransaction = getOpenTransaction()
|
|
4328
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4329
|
+
return
|
|
2390
4330
|
if (!openTransaction) {
|
|
2391
4331
|
throw new InvalidPersistedCollectionConfigError(
|
|
2392
4332
|
`metadata.row.set must be called within an open sync transaction`,
|
|
@@ -2396,12 +4336,12 @@ function createWrappedSyncConfig<
|
|
|
2396
4336
|
type: `set`,
|
|
2397
4337
|
value,
|
|
2398
4338
|
})
|
|
2399
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2400
|
-
params.metadata!.row.set(key, value)
|
|
2401
|
-
}
|
|
2402
4339
|
},
|
|
2403
4340
|
delete: (key: TKey) => {
|
|
4341
|
+
if (startupState.cleanedUp) return
|
|
2404
4342
|
const openTransaction = getOpenTransaction()
|
|
4343
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4344
|
+
return
|
|
2405
4345
|
if (!openTransaction) {
|
|
2406
4346
|
throw new InvalidPersistedCollectionConfigError(
|
|
2407
4347
|
`metadata.row.delete must be called within an open sync transaction`,
|
|
@@ -2410,13 +4350,11 @@ function createWrappedSyncConfig<
|
|
|
2410
4350
|
openTransaction.rowMetadataWrites.set(key, {
|
|
2411
4351
|
type: `delete`,
|
|
2412
4352
|
})
|
|
2413
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2414
|
-
params.metadata!.row.delete(key)
|
|
2415
|
-
}
|
|
2416
4353
|
},
|
|
2417
4354
|
},
|
|
2418
4355
|
collection: {
|
|
2419
4356
|
get: (key: string) => {
|
|
4357
|
+
if (startupState.cleanedUp) return undefined
|
|
2420
4358
|
const openTransaction = getOpenTransaction()
|
|
2421
4359
|
const pendingWrite =
|
|
2422
4360
|
openTransaction?.collectionMetadataWrites.get(key)
|
|
@@ -2425,10 +4363,19 @@ function createWrappedSyncConfig<
|
|
|
2425
4363
|
? undefined
|
|
2426
4364
|
: pendingWrite.value
|
|
2427
4365
|
}
|
|
4366
|
+
const pending = getPendingCollectionMetadataWrite(key)
|
|
4367
|
+
if (pending.found) {
|
|
4368
|
+
return pending.write.type === `delete`
|
|
4369
|
+
? undefined
|
|
4370
|
+
: pending.write.value
|
|
4371
|
+
}
|
|
2428
4372
|
return params.metadata!.collection.get(key)
|
|
2429
4373
|
},
|
|
2430
4374
|
set: (key: string, value: unknown) => {
|
|
4375
|
+
if (startupState.cleanedUp) return
|
|
2431
4376
|
const openTransaction = getOpenTransaction()
|
|
4377
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4378
|
+
return
|
|
2432
4379
|
if (!openTransaction) {
|
|
2433
4380
|
throw new InvalidPersistedCollectionConfigError(
|
|
2434
4381
|
`metadata.collection.set must be called within an open sync transaction`,
|
|
@@ -2438,12 +4385,12 @@ function createWrappedSyncConfig<
|
|
|
2438
4385
|
type: `set`,
|
|
2439
4386
|
value,
|
|
2440
4387
|
})
|
|
2441
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2442
|
-
params.metadata!.collection.set(key, value)
|
|
2443
|
-
}
|
|
2444
4388
|
},
|
|
2445
4389
|
delete: (key: string) => {
|
|
4390
|
+
if (startupState.cleanedUp) return
|
|
2446
4391
|
const openTransaction = getOpenTransaction()
|
|
4392
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure())
|
|
4393
|
+
return
|
|
2447
4394
|
if (!openTransaction) {
|
|
2448
4395
|
throw new InvalidPersistedCollectionConfigError(
|
|
2449
4396
|
`metadata.collection.delete must be called within an open sync transaction`,
|
|
@@ -2452,16 +4399,36 @@ function createWrappedSyncConfig<
|
|
|
2452
4399
|
openTransaction.collectionMetadataWrites.set(key, {
|
|
2453
4400
|
type: `delete`,
|
|
2454
4401
|
})
|
|
2455
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2456
|
-
params.metadata!.collection.delete(key)
|
|
2457
|
-
}
|
|
2458
4402
|
},
|
|
2459
4403
|
list: (prefix?: string) => {
|
|
4404
|
+
if (startupState.cleanedUp) return []
|
|
2460
4405
|
const merged = new Map(
|
|
2461
4406
|
params
|
|
2462
4407
|
.metadata!.collection.list()
|
|
2463
4408
|
.map(({ key, value }) => [key, value]),
|
|
2464
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
|
+
}
|
|
2465
4432
|
const openTransaction = getOpenTransaction()
|
|
2466
4433
|
if (openTransaction) {
|
|
2467
4434
|
for (const [
|
|
@@ -2487,35 +4454,114 @@ function createWrappedSyncConfig<
|
|
|
2487
4454
|
}
|
|
2488
4455
|
: undefined,
|
|
2489
4456
|
truncate: () => {
|
|
4457
|
+
if (startupState.cleanedUp) return
|
|
4458
|
+
runtime.noteSourceTruncate()
|
|
2490
4459
|
const openTransaction = getOpenTransaction()
|
|
4460
|
+
if (openTransaction?.terminalFailure ?? getTerminalFailure()) return
|
|
2491
4461
|
if (!openTransaction) {
|
|
2492
|
-
|
|
2493
|
-
return
|
|
4462
|
+
throw new NoPendingSyncTransactionWriteError()
|
|
2494
4463
|
}
|
|
2495
4464
|
|
|
2496
4465
|
openTransaction.operations = []
|
|
4466
|
+
openTransaction.operationKeys.clear()
|
|
4467
|
+
openTransaction.partialUpdateOperationIndexes.clear()
|
|
2497
4468
|
openTransaction.rowMetadataWrites.clear()
|
|
4469
|
+
openTransaction.deferredHydrationMetadataDeleteKeys.clear()
|
|
2498
4470
|
// Intentionally preserve collectionMetadataWrites across truncate.
|
|
2499
4471
|
// Callers (for example electric resume/reset handling) may stage
|
|
2500
4472
|
// collection-scoped metadata before truncating row data, and those
|
|
2501
4473
|
// writes must commit atomically with the truncate transaction.
|
|
2502
4474
|
openTransaction.truncate = true
|
|
2503
|
-
if (!openTransaction.queuedBecauseHydrating) {
|
|
2504
|
-
params.truncate()
|
|
2505
|
-
}
|
|
2506
4475
|
},
|
|
2507
4476
|
commit: (signal?: AbortSignal) => {
|
|
4477
|
+
if (startupState.cleanedUp) return true
|
|
2508
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
|
+
}
|
|
2509
4491
|
if (!openTransaction) {
|
|
2510
|
-
|
|
4492
|
+
throw new NoPendingSyncTransactionCommitError()
|
|
2511
4493
|
}
|
|
2512
4494
|
|
|
2513
|
-
if (openTransaction.
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
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
|
+
) {
|
|
2519
4565
|
let resolveApplied!: () => void
|
|
2520
4566
|
let rejectApplied!: (error: unknown) => void
|
|
2521
4567
|
const applied = new Promise<void>((resolve, reject) => {
|
|
@@ -2524,76 +4570,142 @@ function createWrappedSyncConfig<
|
|
|
2524
4570
|
})
|
|
2525
4571
|
void applied.catch(() => undefined)
|
|
2526
4572
|
runtime.queueHydrationBufferedTransaction({
|
|
2527
|
-
|
|
2528
|
-
rowMetadataWrites: openTransaction.rowMetadataWrites,
|
|
2529
|
-
collectionMetadataWrites:
|
|
2530
|
-
openTransaction.collectionMetadataWrites,
|
|
2531
|
-
truncate: openTransaction.truncate,
|
|
2532
|
-
internal: openTransaction.internal,
|
|
2533
|
-
signal,
|
|
4573
|
+
...transaction,
|
|
2534
4574
|
resolveApplied,
|
|
2535
4575
|
rejectApplied,
|
|
2536
4576
|
})
|
|
4577
|
+
settlePublicationAdmissionWaiters(openTransaction)
|
|
4578
|
+
settleRuntimeTransaction(openTransaction, applied)
|
|
2537
4579
|
return applied
|
|
2538
4580
|
}
|
|
2539
4581
|
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
rowMetadataWrites: openTransaction.rowMetadataWrites,
|
|
2547
|
-
collectionMetadataWrites:
|
|
2548
|
-
openTransaction.collectionMetadataWrites,
|
|
2549
|
-
truncate: openTransaction.truncate,
|
|
2550
|
-
internal: false,
|
|
2551
|
-
})
|
|
2552
|
-
}
|
|
2553
|
-
const persisted = persistAfterApplication()
|
|
2554
|
-
void persisted.catch(() => undefined)
|
|
2555
|
-
return persisted
|
|
4582
|
+
let applied: Promise<void>
|
|
4583
|
+
try {
|
|
4584
|
+
applied = runtime.applyHydrationBufferedTransaction(transaction)
|
|
4585
|
+
} catch (error) {
|
|
4586
|
+
settlePublicationAdmissionWaiters(openTransaction, error)
|
|
4587
|
+
throw error
|
|
2556
4588
|
}
|
|
4589
|
+
settlePublicationAdmissionWaiters(openTransaction)
|
|
4590
|
+
settleRuntimeTransaction(openTransaction, applied)
|
|
2557
4591
|
return applied
|
|
2558
4592
|
},
|
|
2559
4593
|
}
|
|
2560
4594
|
|
|
2561
4595
|
let sourceResult: SyncConfigRes = {}
|
|
2562
|
-
|
|
4596
|
+
let sourceResultSettled = false
|
|
4597
|
+
let activeSourceSyncEntry: Promise<void> | undefined
|
|
2563
4598
|
fullStartPromise = runtime.ensureStarted()
|
|
2564
|
-
|
|
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 () => {
|
|
2565
4606
|
await runtime.ensureStartupMetadataLoaded()
|
|
2566
4607
|
|
|
2567
4608
|
if (startupState.cleanedUp) {
|
|
4609
|
+
sourceResultSettled = true
|
|
2568
4610
|
return sourceResult
|
|
2569
4611
|
}
|
|
2570
4612
|
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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
|
|
2574
4647
|
return sourceResult
|
|
2575
4648
|
})()
|
|
4649
|
+
resolveSourceResultAssigned()
|
|
4650
|
+
void sourceResultPromise.catch((error) => {
|
|
4651
|
+
reportUpstreamError(error)
|
|
4652
|
+
})
|
|
2576
4653
|
|
|
2577
4654
|
return {
|
|
2578
|
-
cleanup: () => {
|
|
4655
|
+
cleanup: async () => {
|
|
2579
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
|
+
}
|
|
2580
4665
|
acquisitions.clear()
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
+
])
|
|
2590
4689
|
if (
|
|
2591
|
-
|
|
2592
|
-
|
|
4690
|
+
sourceOutcome.status === `rejected` &&
|
|
4691
|
+
runtimeOutcome.status === `rejected`
|
|
2593
4692
|
) {
|
|
2594
|
-
|
|
4693
|
+
throw new AggregateError(
|
|
4694
|
+
[sourceOutcome.reason, runtimeOutcome.reason],
|
|
4695
|
+
`Source cleanup and persistence runtime teardown both failed`,
|
|
4696
|
+
{ cause: sourceOutcome.reason },
|
|
4697
|
+
)
|
|
2595
4698
|
}
|
|
2596
|
-
|
|
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> => {
|
|
2597
4709
|
// Hydration is another async boundary. A release before this
|
|
2598
4710
|
// point owns no upstream lease and must not start one later.
|
|
2599
4711
|
if (
|
|
@@ -2605,15 +4717,67 @@ function createWrappedSyncConfig<
|
|
|
2605
4717
|
if (!resolvedSourceResult.loadSubset) return true
|
|
2606
4718
|
acquisition.forwarded = true
|
|
2607
4719
|
try {
|
|
4720
|
+
// Returning a promise transfers its lease even if it rejects.
|
|
4721
|
+
// Only a synchronous throw leaves no upstream lease to release.
|
|
2608
4722
|
return resolvedSourceResult.loadSubset(loadOptions)
|
|
2609
4723
|
} catch (error) {
|
|
2610
4724
|
acquisition.forwarded = false
|
|
2611
4725
|
throw error
|
|
2612
4726
|
}
|
|
2613
|
-
}
|
|
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
|
+
})()
|
|
2614
4777
|
},
|
|
2615
4778
|
unloadSubset: (options: LoadSubsetOptions) => {
|
|
2616
4779
|
const acquisition = acquisitions.get(options)
|
|
4780
|
+
acquisition?.cancelAdmissionWait?.()
|
|
2617
4781
|
acquisitions.delete(options)
|
|
2618
4782
|
runtime.unloadSubset(
|
|
2619
4783
|
options,
|
|
@@ -2637,6 +4801,7 @@ function createLoopbackSyncConfig<
|
|
|
2637
4801
|
commit: params.commit,
|
|
2638
4802
|
truncate: params.truncate,
|
|
2639
4803
|
metadata: params.metadata ?? null,
|
|
4804
|
+
markError: params.markError,
|
|
2640
4805
|
})
|
|
2641
4806
|
runtime.setCollection(
|
|
2642
4807
|
params.collection as Collection<T, TKey, PersistedCollectionUtils>,
|
|
@@ -2645,19 +4810,15 @@ function createLoopbackSyncConfig<
|
|
|
2645
4810
|
void runtime
|
|
2646
4811
|
.ensureStarted()
|
|
2647
4812
|
.then(() => {
|
|
4813
|
+
if (runtime.getCurrentTerminalFailure()) return
|
|
2648
4814
|
params.markReady()
|
|
2649
4815
|
})
|
|
2650
|
-
.catch((
|
|
2651
|
-
console.warn(`Failed persisted loopback startup:`, error)
|
|
2652
|
-
params.markReady()
|
|
2653
|
-
})
|
|
4816
|
+
.catch(() => undefined)
|
|
2654
4817
|
|
|
2655
4818
|
return {
|
|
2656
|
-
|
|
2657
|
-
runtime.
|
|
2658
|
-
|
|
2659
|
-
},
|
|
2660
|
-
loadSubset: (options: LoadSubsetOptions) => runtime.loadSubset(options),
|
|
4819
|
+
loadSubset: (options: LoadSubsetOptions): true | Promise<void> =>
|
|
4820
|
+
runtime.loadHydratedSubset(options) ?? runtime.loadSubset(options),
|
|
4821
|
+
cleanup: () => runtime.cleanup(),
|
|
2661
4822
|
unloadSubset: (options: LoadSubsetOptions) =>
|
|
2662
4823
|
runtime.unloadSubset(options),
|
|
2663
4824
|
}
|
|
@@ -2668,13 +4829,59 @@ function createLoopbackSyncConfig<
|
|
|
2668
4829
|
}
|
|
2669
4830
|
}
|
|
2670
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
|
+
|
|
2671
4876
|
export function persistedCollectionOptions<
|
|
2672
4877
|
T extends object,
|
|
2673
4878
|
TKey extends string | number,
|
|
2674
4879
|
TSchema extends StandardSchemaV1 = never,
|
|
2675
4880
|
TUtils extends UtilsRecord = UtilsRecord,
|
|
2676
4881
|
>(
|
|
2677
|
-
options: PersistedSyncWrappedOptions<T, TKey, TSchema, TUtils
|
|
4882
|
+
options: PersistedSyncWrappedOptions<T, TKey, TSchema, TUtils> & {
|
|
4883
|
+
schema?: never
|
|
4884
|
+
},
|
|
2678
4885
|
): PersistedSyncOptionsResult<T, TKey, TSchema, TUtils>
|
|
2679
4886
|
|
|
2680
4887
|
export function persistedCollectionOptions<
|
|
@@ -2683,7 +4890,9 @@ export function persistedCollectionOptions<
|
|
|
2683
4890
|
TSchema extends StandardSchemaV1 = never,
|
|
2684
4891
|
TUtils extends UtilsRecord = UtilsRecord,
|
|
2685
4892
|
>(
|
|
2686
|
-
options: PersistedLocalOnlyOptions<T, TKey, TSchema, TUtils
|
|
4893
|
+
options: PersistedLocalOnlyOptions<T, TKey, TSchema, TUtils> & {
|
|
4894
|
+
schema?: never
|
|
4895
|
+
},
|
|
2687
4896
|
): PersistedLocalOnlyOptionsResult<T, TKey, TSchema, TUtils>
|
|
2688
4897
|
|
|
2689
4898
|
export function persistedCollectionOptions<
|
|
@@ -2704,6 +4913,17 @@ export function persistedCollectionOptions<
|
|
|
2704
4913
|
)
|
|
2705
4914
|
}
|
|
2706
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
|
+
|
|
2707
4927
|
if (hasOwnSyncKey(options)) {
|
|
2708
4928
|
if (!isValidSyncConfig(options.sync)) {
|
|
2709
4929
|
throw new InvalidSyncConfigError(
|
|
@@ -2711,7 +4931,11 @@ export function persistedCollectionOptions<
|
|
|
2711
4931
|
)
|
|
2712
4932
|
}
|
|
2713
4933
|
|
|
2714
|
-
const {
|
|
4934
|
+
const {
|
|
4935
|
+
schemaVersion,
|
|
4936
|
+
initialRender: _initialRender,
|
|
4937
|
+
...syncOptions
|
|
4938
|
+
} = options
|
|
2715
4939
|
const collectionId =
|
|
2716
4940
|
syncOptions.id ?? `persisted-collection:${safeRandomUUID()}`
|
|
2717
4941
|
const persistence = resolvePersistenceForCollection(
|
|
@@ -2729,13 +4953,21 @@ export function persistedCollectionOptions<
|
|
|
2729
4953
|
persistence,
|
|
2730
4954
|
syncOptions.syncMode ?? `eager`,
|
|
2731
4955
|
collectionId,
|
|
4956
|
+
persistedReadinessTracker,
|
|
2732
4957
|
)
|
|
2733
4958
|
|
|
2734
4959
|
const result = {
|
|
2735
4960
|
...syncOptions,
|
|
2736
4961
|
id: collectionId,
|
|
2737
|
-
sync: createWrappedSyncConfig<T, TKey>(
|
|
4962
|
+
sync: createWrappedSyncConfig<T, TKey>(
|
|
4963
|
+
syncOptions.sync,
|
|
4964
|
+
runtime,
|
|
4965
|
+
persistedReadinessTracker,
|
|
4966
|
+
),
|
|
2738
4967
|
persistence,
|
|
4968
|
+
...(persistedReadinessTracker
|
|
4969
|
+
? { [PERSISTED_READINESS]: persistedReadinessTracker }
|
|
4970
|
+
: {}),
|
|
2739
4971
|
}
|
|
2740
4972
|
|
|
2741
4973
|
return withCollectionConfigFactory(
|
|
@@ -2744,11 +4976,15 @@ export function persistedCollectionOptions<
|
|
|
2744
4976
|
persistedCollectionOptions({
|
|
2745
4977
|
...options,
|
|
2746
4978
|
id: collectionId,
|
|
2747
|
-
}) as typeof result,
|
|
4979
|
+
} as never) as typeof result,
|
|
2748
4980
|
)
|
|
2749
4981
|
}
|
|
2750
4982
|
|
|
2751
|
-
const {
|
|
4983
|
+
const {
|
|
4984
|
+
schemaVersion,
|
|
4985
|
+
initialRender: _initialRender,
|
|
4986
|
+
...localOnlyOptions
|
|
4987
|
+
} = options
|
|
2752
4988
|
const collectionId =
|
|
2753
4989
|
localOnlyOptions.id ?? `persisted-collection:${safeRandomUUID()}`
|
|
2754
4990
|
const persistence = resolvePersistenceForCollection(
|
|
@@ -2765,11 +5001,13 @@ export function persistedCollectionOptions<
|
|
|
2765
5001
|
persistence,
|
|
2766
5002
|
localOnlyOptions.syncMode ?? `eager`,
|
|
2767
5003
|
localOnlyOptions.id ?? DEFAULT_DB_NAME,
|
|
5004
|
+
persistedReadinessTracker,
|
|
2768
5005
|
)
|
|
2769
5006
|
|
|
2770
5007
|
const wrappedOnInsert = async (
|
|
2771
5008
|
params: InsertMutationFnParams<T, TKey, TUtils & PersistedCollectionUtils>,
|
|
2772
5009
|
) => {
|
|
5010
|
+
const lifecycleGeneration = runtime.getLifecycleGeneration()
|
|
2773
5011
|
const handlerResult = localOnlyOptions.onInsert
|
|
2774
5012
|
? await localOnlyOptions.onInsert(
|
|
2775
5013
|
params as unknown as InsertMutationFnParams<T, TKey, TUtils>,
|
|
@@ -2778,6 +5016,7 @@ export function persistedCollectionOptions<
|
|
|
2778
5016
|
|
|
2779
5017
|
await runtime.persistAndConfirmCollectionMutations(
|
|
2780
5018
|
params.transaction.mutations as Array<PendingMutation<T>>,
|
|
5019
|
+
lifecycleGeneration,
|
|
2781
5020
|
)
|
|
2782
5021
|
|
|
2783
5022
|
return handlerResult ?? {}
|
|
@@ -2786,6 +5025,7 @@ export function persistedCollectionOptions<
|
|
|
2786
5025
|
const wrappedOnUpdate = async (
|
|
2787
5026
|
params: UpdateMutationFnParams<T, TKey, TUtils & PersistedCollectionUtils>,
|
|
2788
5027
|
) => {
|
|
5028
|
+
const lifecycleGeneration = runtime.getLifecycleGeneration()
|
|
2789
5029
|
const handlerResult = localOnlyOptions.onUpdate
|
|
2790
5030
|
? await localOnlyOptions.onUpdate(
|
|
2791
5031
|
params as unknown as UpdateMutationFnParams<T, TKey, TUtils>,
|
|
@@ -2794,6 +5034,7 @@ export function persistedCollectionOptions<
|
|
|
2794
5034
|
|
|
2795
5035
|
await runtime.persistAndConfirmCollectionMutations(
|
|
2796
5036
|
params.transaction.mutations as Array<PendingMutation<T>>,
|
|
5037
|
+
lifecycleGeneration,
|
|
2797
5038
|
)
|
|
2798
5039
|
|
|
2799
5040
|
return handlerResult ?? {}
|
|
@@ -2802,6 +5043,7 @@ export function persistedCollectionOptions<
|
|
|
2802
5043
|
const wrappedOnDelete = async (
|
|
2803
5044
|
params: DeleteMutationFnParams<T, TKey, TUtils & PersistedCollectionUtils>,
|
|
2804
5045
|
) => {
|
|
5046
|
+
const lifecycleGeneration = runtime.getLifecycleGeneration()
|
|
2805
5047
|
const handlerResult = localOnlyOptions.onDelete
|
|
2806
5048
|
? await localOnlyOptions.onDelete(
|
|
2807
5049
|
params as unknown as DeleteMutationFnParams<T, TKey, TUtils>,
|
|
@@ -2810,6 +5052,7 @@ export function persistedCollectionOptions<
|
|
|
2810
5052
|
|
|
2811
5053
|
await runtime.persistAndConfirmCollectionMutations(
|
|
2812
5054
|
params.transaction.mutations as Array<PendingMutation<T>>,
|
|
5055
|
+
lifecycleGeneration,
|
|
2813
5056
|
)
|
|
2814
5057
|
|
|
2815
5058
|
return handlerResult ?? {}
|
|
@@ -2837,6 +5080,9 @@ export function persistedCollectionOptions<
|
|
|
2837
5080
|
...localOnlyOptions,
|
|
2838
5081
|
id: collectionId,
|
|
2839
5082
|
persistence,
|
|
5083
|
+
...(persistedReadinessTracker
|
|
5084
|
+
? { [PERSISTED_READINESS]: persistedReadinessTracker }
|
|
5085
|
+
: {}),
|
|
2840
5086
|
sync: createLoopbackSyncConfig(runtime),
|
|
2841
5087
|
onInsert: wrappedOnInsert,
|
|
2842
5088
|
onUpdate: wrappedOnUpdate,
|
|
@@ -2852,7 +5098,7 @@ export function persistedCollectionOptions<
|
|
|
2852
5098
|
persistedCollectionOptions({
|
|
2853
5099
|
...options,
|
|
2854
5100
|
id: collectionId,
|
|
2855
|
-
}) as typeof result,
|
|
5101
|
+
} as never) as typeof result,
|
|
2856
5102
|
)
|
|
2857
5103
|
}
|
|
2858
5104
|
|