@tanstack/db-sqlite-persistence-core 0.2.20 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/persisted.cjs +131 -81
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +1 -0
- package/dist/esm/persisted.d.ts +1 -0
- package/dist/esm/persisted.js +131 -81
- package/dist/esm/persisted.js.map +1 -1
- package/package.json +2 -2
- package/src/persisted.ts +171 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db-sqlite-persistence-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "SQLite persisted collection core for TanStack DB",
|
|
5
5
|
"author": "TanStack Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@standard-schema/spec": "^1.1.0",
|
|
43
|
-
"@tanstack/db": "0.
|
|
43
|
+
"@tanstack/db": "0.9.2"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"typescript": ">=4.7"
|
package/src/persisted.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
CollectionIndexMetadata,
|
|
22
22
|
DeleteMutationFnParams,
|
|
23
23
|
InsertMutationFnParams,
|
|
24
|
+
LoadSubsetFn,
|
|
24
25
|
LoadSubsetOptions,
|
|
25
26
|
PendingMutation,
|
|
26
27
|
SyncAppliedReceipt,
|
|
@@ -351,6 +352,7 @@ export interface PersistedCollectionUtils extends UtilsRecord {
|
|
|
351
352
|
mutations: Array<PendingMutation<Record<string, unknown>>>
|
|
352
353
|
}) => Promise<void> | void
|
|
353
354
|
getLeadershipState?: () => PersistedCollectionLeadershipState
|
|
355
|
+
/** Hydrate once without acquiring a new ongoing subset lease. */
|
|
354
356
|
forceReloadSubset?: (options: LoadSubsetOptions) => Promise<void> | void
|
|
355
357
|
}
|
|
356
358
|
|
|
@@ -706,18 +708,6 @@ function stableSerialize(value: unknown): string {
|
|
|
706
708
|
return JSON.stringify(toStableSerializable(value) ?? null)
|
|
707
709
|
}
|
|
708
710
|
|
|
709
|
-
function normalizeSubsetOptionsForKey(
|
|
710
|
-
options: LoadSubsetOptions,
|
|
711
|
-
): Record<string, unknown> {
|
|
712
|
-
return {
|
|
713
|
-
where: toStableSerializable(options.where),
|
|
714
|
-
orderBy: toStableSerializable(options.orderBy),
|
|
715
|
-
limit: options.limit,
|
|
716
|
-
cursor: toStableSerializable(options.cursor),
|
|
717
|
-
offset: options.offset,
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
711
|
function normalizeSyncFnResult(result: void | (() => void) | SyncConfigRes) {
|
|
722
712
|
if (typeof result === `function`) {
|
|
723
713
|
return { cleanup: result } satisfies SyncConfigRes
|
|
@@ -801,7 +791,7 @@ class PersistedCollectionRuntime<
|
|
|
801
791
|
BufferedSyncTransaction<T, TKey>
|
|
802
792
|
> = []
|
|
803
793
|
private readonly queuedTxCommitted: Array<TxCommitted> = []
|
|
804
|
-
private readonly
|
|
794
|
+
private readonly requestIds = new WeakMap<LoadSubsetOptions, string>()
|
|
805
795
|
|
|
806
796
|
private collection: Collection<T, TKey, PersistedCollectionUtils> | null =
|
|
807
797
|
null
|
|
@@ -815,15 +805,17 @@ class PersistedCollectionRuntime<
|
|
|
815
805
|
private started = false
|
|
816
806
|
private startupMetadataPromise: Promise<void> | null = null
|
|
817
807
|
private startPromise: Promise<void> | null = null
|
|
808
|
+
private resumeBaselinePromise: Promise<void> | null = null
|
|
809
|
+
private lifecycleGeneration = 0
|
|
818
810
|
private internalApplyDepth = 0
|
|
819
811
|
private appliedReceiptSequence = 0
|
|
820
812
|
private readonly pendingAppliedReceipts = new Map<number, Promise<void>>()
|
|
821
|
-
private
|
|
813
|
+
private hydratingGeneration: number | null = null
|
|
822
814
|
private coordinatorUnsubscribe: (() => void) | null = null
|
|
823
815
|
private indexAddedUnsubscribe: (() => void) | null = null
|
|
824
816
|
private indexRemovedUnsubscribe: (() => void) | null = null
|
|
825
817
|
private remoteEnsureRetryTimer: ReturnType<typeof setTimeout> | null = null
|
|
826
|
-
private
|
|
818
|
+
private nextRequestId = 0
|
|
827
819
|
|
|
828
820
|
private latestTerm = 0
|
|
829
821
|
private latestSeq = 0
|
|
@@ -841,6 +833,8 @@ class PersistedCollectionRuntime<
|
|
|
841
833
|
) {}
|
|
842
834
|
|
|
843
835
|
setSyncControls(syncControls: SyncControlFns<T, TKey>): void {
|
|
836
|
+
this.advanceLifecycle()
|
|
837
|
+
|
|
844
838
|
const commit = syncControls.commit
|
|
845
839
|
this.syncControls = {
|
|
846
840
|
...syncControls,
|
|
@@ -880,7 +874,7 @@ class PersistedCollectionRuntime<
|
|
|
880
874
|
}
|
|
881
875
|
|
|
882
876
|
isHydratingNow(): boolean {
|
|
883
|
-
return this.
|
|
877
|
+
return this.hydratingGeneration === this.lifecycleGeneration
|
|
884
878
|
}
|
|
885
879
|
|
|
886
880
|
isApplyingInternally(): boolean {
|
|
@@ -910,20 +904,56 @@ class PersistedCollectionRuntime<
|
|
|
910
904
|
return this.startPromise
|
|
911
905
|
}
|
|
912
906
|
|
|
913
|
-
|
|
907
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
908
|
+
this.startPromise = this.startInternal(lifecycleGeneration)
|
|
914
909
|
return this.startPromise
|
|
915
910
|
}
|
|
916
911
|
|
|
912
|
+
ensureResumeBaselineHydrated(): Promise<void> {
|
|
913
|
+
if (this.resumeBaselinePromise) {
|
|
914
|
+
return this.resumeBaselinePromise
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
918
|
+
this.resumeBaselinePromise = (async () => {
|
|
919
|
+
await this.ensureStarted()
|
|
920
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
921
|
+
if (this.syncMode !== `on-demand`) return
|
|
922
|
+
|
|
923
|
+
await this.hydrateBaseline(lifecycleGeneration)
|
|
924
|
+
})()
|
|
925
|
+
return this.resumeBaselinePromise
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private async hydrateBaseline(lifecycleGeneration: number): Promise<void> {
|
|
929
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
930
|
+
|
|
931
|
+
const baseline = {}
|
|
932
|
+
this.activeSubsets.set(this.getSubsetKey(baseline), baseline)
|
|
933
|
+
const appliedCursor = this.appliedReceiptSequence
|
|
934
|
+
await this.applyMutex.run(async () => {
|
|
935
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
936
|
+
await this.hydrateSubsetUnsafe(baseline, {
|
|
937
|
+
requestRemoteEnsure: false,
|
|
938
|
+
lifecycleGeneration,
|
|
939
|
+
})
|
|
940
|
+
})
|
|
941
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
942
|
+
await this.waitForAppliedReceiptsAfter(appliedCursor)
|
|
943
|
+
}
|
|
944
|
+
|
|
917
945
|
async ensureStartupMetadataLoaded(): Promise<void> {
|
|
918
946
|
if (this.startupMetadataPromise) {
|
|
919
947
|
return this.startupMetadataPromise
|
|
920
948
|
}
|
|
921
949
|
|
|
922
|
-
|
|
950
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
951
|
+
this.startupMetadataPromise =
|
|
952
|
+
this.loadStartupMetadataInternal(lifecycleGeneration)
|
|
923
953
|
return this.startupMetadataPromise
|
|
924
954
|
}
|
|
925
955
|
|
|
926
|
-
private async startInternal(): Promise<void> {
|
|
956
|
+
private async startInternal(lifecycleGeneration: number): Promise<void> {
|
|
927
957
|
if (this.started) {
|
|
928
958
|
return
|
|
929
959
|
}
|
|
@@ -931,28 +961,28 @@ class PersistedCollectionRuntime<
|
|
|
931
961
|
this.started = true
|
|
932
962
|
|
|
933
963
|
await this.ensureStartupMetadataLoaded()
|
|
964
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
934
965
|
|
|
935
966
|
const indexBootstrapSnapshot = this.collection?.getIndexMetadata() ?? []
|
|
936
967
|
this.attachIndexLifecycleListeners()
|
|
937
968
|
await this.bootstrapPersistedIndexes(indexBootstrapSnapshot)
|
|
969
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
938
970
|
|
|
939
971
|
if (this.syncMode !== `on-demand`) {
|
|
940
|
-
this.
|
|
941
|
-
const appliedCursor = this.appliedReceiptSequence
|
|
942
|
-
await this.applyMutex.run(() =>
|
|
943
|
-
this.hydrateSubsetUnsafe({}, { requestRemoteEnsure: false }),
|
|
944
|
-
)
|
|
945
|
-
await this.waitForAppliedReceiptsAfter(appliedCursor)
|
|
972
|
+
await this.hydrateBaseline(lifecycleGeneration)
|
|
946
973
|
}
|
|
947
974
|
}
|
|
948
975
|
|
|
949
|
-
private async loadStartupMetadataInternal(
|
|
976
|
+
private async loadStartupMetadataInternal(
|
|
977
|
+
lifecycleGeneration: number,
|
|
978
|
+
): Promise<void> {
|
|
950
979
|
// Restore stream position from the database so that new mutations
|
|
951
980
|
// don't collide with previously applied transactions.
|
|
952
981
|
if (this.persistence.adapter.getStreamPosition) {
|
|
953
982
|
const position = await this.persistence.adapter.getStreamPosition(
|
|
954
983
|
this.collectionId,
|
|
955
984
|
)
|
|
985
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
956
986
|
this.observeStreamPosition(
|
|
957
987
|
position.latestTerm,
|
|
958
988
|
position.latestSeq,
|
|
@@ -960,11 +990,8 @@ class PersistedCollectionRuntime<
|
|
|
960
990
|
)
|
|
961
991
|
}
|
|
962
992
|
|
|
963
|
-
await this.loadCollectionMetadataIntoCollection()
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
private async loadCollectionMetadataIntoCollection(): Promise<void> {
|
|
967
993
|
const collectionMetadata = await this.loadCollectionMetadataSnapshot()
|
|
994
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
968
995
|
this.replaceCollectionMetadataSnapshot(collectionMetadata)
|
|
969
996
|
}
|
|
970
997
|
|
|
@@ -1015,33 +1042,39 @@ class PersistedCollectionRuntime<
|
|
|
1015
1042
|
|
|
1016
1043
|
async loadSubset(
|
|
1017
1044
|
options: LoadSubsetOptions,
|
|
1018
|
-
upstreamLoadSubset?:
|
|
1045
|
+
upstreamLoadSubset?: LoadSubsetFn,
|
|
1019
1046
|
): Promise<void> {
|
|
1047
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1020
1048
|
this.activeSubsets.set(this.getSubsetKey(options), options)
|
|
1021
1049
|
|
|
1022
1050
|
const appliedCursor = this.appliedReceiptSequence
|
|
1023
1051
|
await this.applyMutex.run(() =>
|
|
1024
1052
|
this.hydrateSubsetUnsafe(options, {
|
|
1025
1053
|
requestRemoteEnsure: this.mode === `sync-present`,
|
|
1054
|
+
lifecycleGeneration,
|
|
1026
1055
|
}),
|
|
1027
1056
|
)
|
|
1057
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1028
1058
|
await this.waitForAppliedReceiptsAfter(appliedCursor)
|
|
1029
1059
|
|
|
1030
1060
|
if (upstreamLoadSubset) {
|
|
1031
1061
|
try {
|
|
1032
|
-
|
|
1033
|
-
if (maybePromise instanceof Promise) {
|
|
1034
|
-
await maybePromise.catch((error) => {
|
|
1035
|
-
console.warn(
|
|
1036
|
-
`Failed to load remote subset in persisted wrapper:`,
|
|
1037
|
-
error,
|
|
1038
|
-
)
|
|
1039
|
-
this.queueRemoteSubsetEnsure(options)
|
|
1040
|
-
})
|
|
1041
|
-
}
|
|
1062
|
+
await upstreamLoadSubset(options)
|
|
1042
1063
|
} catch (error) {
|
|
1064
|
+
if (
|
|
1065
|
+
options.signal?.aborted ||
|
|
1066
|
+
(typeof error === `object` &&
|
|
1067
|
+
error !== null &&
|
|
1068
|
+
`name` in error &&
|
|
1069
|
+
error.name === `AbortError`)
|
|
1070
|
+
) {
|
|
1071
|
+
this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options))
|
|
1072
|
+
throw error
|
|
1073
|
+
}
|
|
1043
1074
|
console.warn(`Failed to trigger remote subset load:`, error)
|
|
1044
1075
|
this.queueRemoteSubsetEnsure(options)
|
|
1076
|
+
// Hydration remains readable, but it does not satisfy remote demand.
|
|
1077
|
+
throw error
|
|
1045
1078
|
}
|
|
1046
1079
|
}
|
|
1047
1080
|
}
|
|
@@ -1051,13 +1084,18 @@ class PersistedCollectionRuntime<
|
|
|
1051
1084
|
upstreamUnloadSubset?: (options: LoadSubsetOptions) => void,
|
|
1052
1085
|
): void {
|
|
1053
1086
|
this.activeSubsets.delete(this.getSubsetKey(options))
|
|
1087
|
+
this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options))
|
|
1054
1088
|
upstreamUnloadSubset?.(options)
|
|
1055
1089
|
}
|
|
1056
1090
|
|
|
1057
1091
|
async forceReloadSubset(options: LoadSubsetOptions): Promise<void> {
|
|
1058
|
-
this.
|
|
1092
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
1093
|
+
// A one-shot refresh does not acquire an enduring subscription lease.
|
|
1059
1094
|
await this.applyMutex.run(() =>
|
|
1060
|
-
this.hydrateSubsetUnsafe(options, {
|
|
1095
|
+
this.hydrateSubsetUnsafe(options, {
|
|
1096
|
+
requestRemoteEnsure: false,
|
|
1097
|
+
lifecycleGeneration,
|
|
1098
|
+
}),
|
|
1061
1099
|
)
|
|
1062
1100
|
}
|
|
1063
1101
|
|
|
@@ -1176,6 +1214,8 @@ class PersistedCollectionRuntime<
|
|
|
1176
1214
|
}
|
|
1177
1215
|
|
|
1178
1216
|
cleanup(): void {
|
|
1217
|
+
this.advanceLifecycle()
|
|
1218
|
+
|
|
1179
1219
|
this.coordinatorUnsubscribe?.()
|
|
1180
1220
|
this.coordinatorUnsubscribe = null
|
|
1181
1221
|
|
|
@@ -1198,6 +1238,15 @@ class PersistedCollectionRuntime<
|
|
|
1198
1238
|
this.queuedHydrationTransactions.length = 0
|
|
1199
1239
|
this.queuedTxCommitted.length = 0
|
|
1200
1240
|
this.clearSyncControls()
|
|
1241
|
+
this.collection = null
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private advanceLifecycle(): void {
|
|
1245
|
+
this.lifecycleGeneration++
|
|
1246
|
+
this.started = false
|
|
1247
|
+
this.startupMetadataPromise = null
|
|
1248
|
+
this.startPromise = null
|
|
1249
|
+
this.resumeBaselinePromise = null
|
|
1201
1250
|
}
|
|
1202
1251
|
|
|
1203
1252
|
private withInternalApply<TResult>(task: () => TResult): TResult {
|
|
@@ -1250,15 +1299,19 @@ class PersistedCollectionRuntime<
|
|
|
1250
1299
|
options: LoadSubsetOptions,
|
|
1251
1300
|
config: {
|
|
1252
1301
|
requestRemoteEnsure: boolean
|
|
1302
|
+
lifecycleGeneration: number
|
|
1253
1303
|
},
|
|
1254
1304
|
): Promise<void> {
|
|
1255
|
-
this.
|
|
1305
|
+
this.hydratingGeneration = config.lifecycleGeneration
|
|
1256
1306
|
try {
|
|
1257
1307
|
const rows = await this.loadSubsetRowsUnsafe(options)
|
|
1308
|
+
if (config.lifecycleGeneration !== this.lifecycleGeneration) return
|
|
1258
1309
|
|
|
1259
1310
|
this.applyRowsToCollection(rows)
|
|
1260
1311
|
} finally {
|
|
1261
|
-
this.
|
|
1312
|
+
if (this.hydratingGeneration === config.lifecycleGeneration) {
|
|
1313
|
+
this.hydratingGeneration = null
|
|
1314
|
+
}
|
|
1262
1315
|
}
|
|
1263
1316
|
|
|
1264
1317
|
await this.flushQueuedHydrationTransactionsUnsafe()
|
|
@@ -1805,26 +1858,21 @@ class PersistedCollectionRuntime<
|
|
|
1805
1858
|
}
|
|
1806
1859
|
|
|
1807
1860
|
private getSubsetKey(options: LoadSubsetOptions): string {
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
this.nextSubscriptionId++
|
|
1816
|
-
const id = `sub:${this.nextSubscriptionId}`
|
|
1817
|
-
this.subscriptionIds.set(subscription, id)
|
|
1818
|
-
return id
|
|
1861
|
+
// A subscription can own several independent acquisitions, including
|
|
1862
|
+
// identical requests. Only releasing this options object ends its lease.
|
|
1863
|
+
let id = this.requestIds.get(options)
|
|
1864
|
+
if (id === undefined) {
|
|
1865
|
+
id = `request:${++this.nextRequestId}`
|
|
1866
|
+
this.requestIds.set(options, id)
|
|
1819
1867
|
}
|
|
1820
|
-
|
|
1821
|
-
return `opts:${stableSerialize(normalizeSubsetOptionsForKey(options))}`
|
|
1868
|
+
return id
|
|
1822
1869
|
}
|
|
1823
1870
|
|
|
1824
1871
|
private queueRemoteSubsetEnsure(options: LoadSubsetOptions): void {
|
|
1825
1872
|
if (
|
|
1826
1873
|
this.mode !== `sync-present` ||
|
|
1827
|
-
!this.persistence.coordinator.requestEnsureRemoteSubset
|
|
1874
|
+
!this.persistence.coordinator.requestEnsureRemoteSubset ||
|
|
1875
|
+
this.activeSubsets.get(this.getSubsetKey(options)) !== options
|
|
1828
1876
|
) {
|
|
1829
1877
|
return
|
|
1830
1878
|
}
|
|
@@ -1907,7 +1955,7 @@ class PersistedCollectionRuntime<
|
|
|
1907
1955
|
// of both local and remote mutations. The seq dedup in
|
|
1908
1956
|
// processCommittedTxUnsafe prevents double-processing of our own writes.
|
|
1909
1957
|
if (isTxCommittedPayload(payload)) {
|
|
1910
|
-
if (this.
|
|
1958
|
+
if (this.isHydratingNow()) {
|
|
1911
1959
|
this.queuedTxCommitted.push(payload)
|
|
1912
1960
|
return
|
|
1913
1961
|
}
|
|
@@ -2132,17 +2180,20 @@ class PersistedCollectionRuntime<
|
|
|
2132
2180
|
}
|
|
2133
2181
|
|
|
2134
2182
|
private async reloadActiveSubsetsUnsafe(): Promise<void> {
|
|
2183
|
+
const lifecycleGeneration = this.lifecycleGeneration
|
|
2135
2184
|
const activeSubsetOptions =
|
|
2136
2185
|
this.activeSubsets.size > 0
|
|
2137
2186
|
? Array.from(this.activeSubsets.values())
|
|
2138
2187
|
: [{}]
|
|
2139
2188
|
|
|
2140
|
-
this.
|
|
2189
|
+
this.hydratingGeneration = lifecycleGeneration
|
|
2141
2190
|
try {
|
|
2142
2191
|
const mergedRows = new Map<TKey, { value: T; metadata?: unknown }>()
|
|
2143
2192
|
const collectionMetadata = await this.loadCollectionMetadataSnapshot()
|
|
2193
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2144
2194
|
for (const options of activeSubsetOptions) {
|
|
2145
2195
|
const subsetRows = await this.loadSubsetRowsUnsafe(options)
|
|
2196
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return
|
|
2146
2197
|
for (const row of subsetRows) {
|
|
2147
2198
|
mergedRows.set(row.key, {
|
|
2148
2199
|
value: row.value,
|
|
@@ -2160,7 +2211,9 @@ class PersistedCollectionRuntime<
|
|
|
2160
2211
|
collectionMetadata,
|
|
2161
2212
|
)
|
|
2162
2213
|
} finally {
|
|
2163
|
-
this.
|
|
2214
|
+
if (this.hydratingGeneration === lifecycleGeneration) {
|
|
2215
|
+
this.hydratingGeneration = null
|
|
2216
|
+
}
|
|
2164
2217
|
}
|
|
2165
2218
|
|
|
2166
2219
|
await this.flushQueuedHydrationTransactionsUnsafe()
|
|
@@ -2276,24 +2329,8 @@ function createWrappedSyncConfig<
|
|
|
2276
2329
|
const getOpenTransaction = () =>
|
|
2277
2330
|
transactionStack[transactionStack.length - 1]
|
|
2278
2331
|
let fullStartPromise: Promise<void> | null = null
|
|
2279
|
-
const
|
|
2280
|
-
const
|
|
2281
|
-
let nextLoadSubscriptionId = 0
|
|
2282
|
-
const getLoadKey = (options: LoadSubsetOptions) => {
|
|
2283
|
-
const subscription = options.subscription as object | undefined
|
|
2284
|
-
if (subscription && typeof subscription === `object`) {
|
|
2285
|
-
const existingId = loadSubscriptionIds.get(subscription)
|
|
2286
|
-
if (existingId) {
|
|
2287
|
-
return `sub:${existingId}`
|
|
2288
|
-
}
|
|
2289
|
-
nextLoadSubscriptionId++
|
|
2290
|
-
const nextId = String(nextLoadSubscriptionId)
|
|
2291
|
-
loadSubscriptionIds.set(subscription, nextId)
|
|
2292
|
-
return `sub:${nextId}`
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
return `opts:${stableSerialize(normalizeSubsetOptionsForKey(options))}`
|
|
2296
|
-
}
|
|
2332
|
+
const startupState = { cleanedUp: false }
|
|
2333
|
+
const acquisitions = new Map<LoadSubsetOptions, { forwarded: boolean }>()
|
|
2297
2334
|
runtime.setSyncControls({
|
|
2298
2335
|
begin: params.begin,
|
|
2299
2336
|
write: params.write as SyncControlFns<T, TKey>[`write`],
|
|
@@ -2308,11 +2345,14 @@ function createWrappedSyncConfig<
|
|
|
2308
2345
|
const wrappedParams = {
|
|
2309
2346
|
...params,
|
|
2310
2347
|
markReady: () => {
|
|
2348
|
+
if (startupState.cleanedUp) return
|
|
2311
2349
|
void (fullStartPromise ?? runtime.ensureStarted())
|
|
2312
2350
|
.then(() => {
|
|
2351
|
+
if (startupState.cleanedUp) return
|
|
2313
2352
|
params.markReady()
|
|
2314
2353
|
})
|
|
2315
2354
|
.catch((error) => {
|
|
2355
|
+
if (startupState.cleanedUp) return
|
|
2316
2356
|
console.warn(
|
|
2317
2357
|
`Failed persisted sync startup before markReady:`,
|
|
2318
2358
|
error,
|
|
@@ -2321,6 +2361,7 @@ function createWrappedSyncConfig<
|
|
|
2321
2361
|
})
|
|
2322
2362
|
},
|
|
2323
2363
|
begin: (options?: { immediate?: boolean }) => {
|
|
2364
|
+
if (startupState.cleanedUp) return
|
|
2324
2365
|
const transaction: OpenSyncTransaction<T, TKey> = {
|
|
2325
2366
|
operations: [],
|
|
2326
2367
|
rowMetadataWrites: new Map(),
|
|
@@ -2337,6 +2378,7 @@ function createWrappedSyncConfig<
|
|
|
2337
2378
|
}
|
|
2338
2379
|
},
|
|
2339
2380
|
write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => {
|
|
2381
|
+
if (startupState.cleanedUp) return
|
|
2340
2382
|
const normalization = runtime.normalizeSyncWriteMessage(message)
|
|
2341
2383
|
const openTransaction = getOpenTransaction()
|
|
2342
2384
|
|
|
@@ -2382,7 +2424,12 @@ function createWrappedSyncConfig<
|
|
|
2382
2424
|
metadata: params.metadata
|
|
2383
2425
|
? {
|
|
2384
2426
|
row: {
|
|
2427
|
+
whenHydrated: () =>
|
|
2428
|
+
startupState.cleanedUp
|
|
2429
|
+
? Promise.resolve()
|
|
2430
|
+
: runtime.ensureResumeBaselineHydrated(),
|
|
2385
2431
|
get: (key: TKey) => {
|
|
2432
|
+
if (startupState.cleanedUp) return undefined
|
|
2386
2433
|
const openTransaction = getOpenTransaction()
|
|
2387
2434
|
const pendingWrite =
|
|
2388
2435
|
openTransaction?.rowMetadataWrites.get(key)
|
|
@@ -2397,8 +2444,11 @@ function createWrappedSyncConfig<
|
|
|
2397
2444
|
return params.metadata!.row.get(key)
|
|
2398
2445
|
},
|
|
2399
2446
|
scanPersisted: (options?: PersistedRowScanOptions) =>
|
|
2400
|
-
|
|
2447
|
+
startupState.cleanedUp
|
|
2448
|
+
? Promise.resolve([])
|
|
2449
|
+
: runtime.scanPersistedRows(options),
|
|
2401
2450
|
set: (key: TKey, value: unknown) => {
|
|
2451
|
+
if (startupState.cleanedUp) return
|
|
2402
2452
|
const openTransaction = getOpenTransaction()
|
|
2403
2453
|
if (!openTransaction) {
|
|
2404
2454
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -2414,6 +2464,7 @@ function createWrappedSyncConfig<
|
|
|
2414
2464
|
}
|
|
2415
2465
|
},
|
|
2416
2466
|
delete: (key: TKey) => {
|
|
2467
|
+
if (startupState.cleanedUp) return
|
|
2417
2468
|
const openTransaction = getOpenTransaction()
|
|
2418
2469
|
if (!openTransaction) {
|
|
2419
2470
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -2430,6 +2481,7 @@ function createWrappedSyncConfig<
|
|
|
2430
2481
|
},
|
|
2431
2482
|
collection: {
|
|
2432
2483
|
get: (key: string) => {
|
|
2484
|
+
if (startupState.cleanedUp) return undefined
|
|
2433
2485
|
const openTransaction = getOpenTransaction()
|
|
2434
2486
|
const pendingWrite =
|
|
2435
2487
|
openTransaction?.collectionMetadataWrites.get(key)
|
|
@@ -2441,6 +2493,7 @@ function createWrappedSyncConfig<
|
|
|
2441
2493
|
return params.metadata!.collection.get(key)
|
|
2442
2494
|
},
|
|
2443
2495
|
set: (key: string, value: unknown) => {
|
|
2496
|
+
if (startupState.cleanedUp) return
|
|
2444
2497
|
const openTransaction = getOpenTransaction()
|
|
2445
2498
|
if (!openTransaction) {
|
|
2446
2499
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -2456,6 +2509,7 @@ function createWrappedSyncConfig<
|
|
|
2456
2509
|
}
|
|
2457
2510
|
},
|
|
2458
2511
|
delete: (key: string) => {
|
|
2512
|
+
if (startupState.cleanedUp) return
|
|
2459
2513
|
const openTransaction = getOpenTransaction()
|
|
2460
2514
|
if (!openTransaction) {
|
|
2461
2515
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -2470,6 +2524,7 @@ function createWrappedSyncConfig<
|
|
|
2470
2524
|
}
|
|
2471
2525
|
},
|
|
2472
2526
|
list: (prefix?: string) => {
|
|
2527
|
+
if (startupState.cleanedUp) return []
|
|
2473
2528
|
const merged = new Map(
|
|
2474
2529
|
params
|
|
2475
2530
|
.metadata!.collection.list()
|
|
@@ -2500,6 +2555,7 @@ function createWrappedSyncConfig<
|
|
|
2500
2555
|
}
|
|
2501
2556
|
: undefined,
|
|
2502
2557
|
truncate: () => {
|
|
2558
|
+
if (startupState.cleanedUp) return
|
|
2503
2559
|
const openTransaction = getOpenTransaction()
|
|
2504
2560
|
if (!openTransaction) {
|
|
2505
2561
|
params.truncate()
|
|
@@ -2518,6 +2574,7 @@ function createWrappedSyncConfig<
|
|
|
2518
2574
|
}
|
|
2519
2575
|
},
|
|
2520
2576
|
commit: (signal?: AbortSignal) => {
|
|
2577
|
+
if (startupState.cleanedUp) return true
|
|
2521
2578
|
const openTransaction = transactionStack.pop()
|
|
2522
2579
|
if (!openTransaction) {
|
|
2523
2580
|
return params.commit(signal)
|
|
@@ -2572,7 +2629,6 @@ function createWrappedSyncConfig<
|
|
|
2572
2629
|
}
|
|
2573
2630
|
|
|
2574
2631
|
let sourceResult: SyncConfigRes = {}
|
|
2575
|
-
const startupState = { cleanedUp: false }
|
|
2576
2632
|
fullStartPromise = runtime.ensureStarted()
|
|
2577
2633
|
const sourceResultPromise = (async () => {
|
|
2578
2634
|
await runtime.ensureStartupMetadataLoaded()
|
|
@@ -2590,23 +2646,50 @@ function createWrappedSyncConfig<
|
|
|
2590
2646
|
return {
|
|
2591
2647
|
cleanup: () => {
|
|
2592
2648
|
startupState.cleanedUp = true
|
|
2649
|
+
acquisitions.clear()
|
|
2593
2650
|
sourceResult.cleanup?.()
|
|
2594
2651
|
runtime.cleanup()
|
|
2595
2652
|
runtime.clearSyncControls()
|
|
2596
2653
|
},
|
|
2597
2654
|
loadSubset: async (options: LoadSubsetOptions) => {
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2655
|
+
const acquisition = { forwarded: false }
|
|
2656
|
+
acquisitions.set(options, acquisition)
|
|
2600
2657
|
await fullStartPromise
|
|
2601
2658
|
const resolvedSourceResult = await sourceResultPromise
|
|
2602
|
-
if (
|
|
2659
|
+
if (
|
|
2660
|
+
startupState.cleanedUp ||
|
|
2661
|
+
acquisitions.get(options) !== acquisition
|
|
2662
|
+
) {
|
|
2603
2663
|
return
|
|
2604
2664
|
}
|
|
2605
|
-
|
|
2665
|
+
return runtime.loadSubset(options, (loadOptions) => {
|
|
2666
|
+
// Hydration is another async boundary. A release before this
|
|
2667
|
+
// point owns no upstream lease and must not start one later.
|
|
2668
|
+
if (
|
|
2669
|
+
startupState.cleanedUp ||
|
|
2670
|
+
acquisitions.get(options) !== acquisition
|
|
2671
|
+
) {
|
|
2672
|
+
return true
|
|
2673
|
+
}
|
|
2674
|
+
if (!resolvedSourceResult.loadSubset) return true
|
|
2675
|
+
acquisition.forwarded = true
|
|
2676
|
+
try {
|
|
2677
|
+
// Returning a promise transfers its lease even if it rejects.
|
|
2678
|
+
// Only a synchronous throw leaves no upstream lease to release.
|
|
2679
|
+
return resolvedSourceResult.loadSubset(loadOptions)
|
|
2680
|
+
} catch (error) {
|
|
2681
|
+
acquisition.forwarded = false
|
|
2682
|
+
throw error
|
|
2683
|
+
}
|
|
2684
|
+
})
|
|
2606
2685
|
},
|
|
2607
2686
|
unloadSubset: (options: LoadSubsetOptions) => {
|
|
2608
|
-
|
|
2609
|
-
|
|
2687
|
+
const acquisition = acquisitions.get(options)
|
|
2688
|
+
acquisitions.delete(options)
|
|
2689
|
+
runtime.unloadSubset(
|
|
2690
|
+
options,
|
|
2691
|
+
acquisition?.forwarded ? sourceResult.unloadSubset : undefined,
|
|
2692
|
+
)
|
|
2610
2693
|
},
|
|
2611
2694
|
}
|
|
2612
2695
|
},
|