@tanstack/db-sqlite-persistence-core 0.4.8 → 0.4.9
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 +10 -1
- package/dist/cjs/broadcast-coordinator.cjs +117 -29
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
- package/dist/cjs/broadcast-coordinator.d.cts +16 -8
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +1408 -278
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +62 -15
- package/dist/cjs/remote-subset-wire.cjs +11 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.d.cts +1 -0
- package/dist/cjs/sqlite-core-adapter.cjs +623 -73
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +46 -12
- package/dist/esm/broadcast-coordinator.d.ts +16 -8
- package/dist/esm/broadcast-coordinator.js +118 -30
- package/dist/esm/broadcast-coordinator.js.map +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/persisted.d.ts +62 -15
- package/dist/esm/persisted.js +1409 -279
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.d.ts +1 -0
- package/dist/esm/remote-subset-wire.js +11 -0
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.d.ts +46 -12
- package/dist/esm/sqlite-core-adapter.js +626 -76
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/broadcast-coordinator.ts +153 -23
- package/src/persisted.ts +1973 -322
- package/src/remote-subset-wire.ts +12 -0
- package/src/sqlite-core-adapter.ts +760 -77
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LoadSubsetOptions } from '@tanstack/db';
|
|
2
|
-
import { CommittedTxAnchor, HydrationPersistenceAdapter, PersistedIndexSpec, PersistedKeySetEvidence, PersistedRowScanOptions, PersistedScannedRow, PersistedTx, PersistenceAdapter, ReconciledCommittedTx, ReplayableTxDelta, SQLiteDriver } from './persisted.cjs';
|
|
2
|
+
import { CommittedTxAnchor, HydrationPersistenceAdapter, PersistedCacheGenerationClaim, PersistedIndexSpec, PersistedKeySetEvidence, PersistedRowScanOptions, PersistedScannedRow, PersistedStorageTarget, PersistedTx, PersistenceAdapter, ReconciledCommittedTx, ReplayableTxDelta, SQLiteDriver } from './persisted.cjs';
|
|
3
3
|
type SQLiteCoreAdapterSchemaMismatchPolicy = `sync-present-reset` | `sync-absent-error` | `reset`;
|
|
4
4
|
export type SQLiteCoreAdapterOptions = {
|
|
5
5
|
driver: SQLiteDriver;
|
|
@@ -8,6 +8,10 @@ export type SQLiteCoreAdapterOptions = {
|
|
|
8
8
|
appliedTxPruneMaxRows?: number;
|
|
9
9
|
appliedTxPruneMaxAgeSeconds?: number;
|
|
10
10
|
pullSinceReloadThreshold?: number;
|
|
11
|
+
/** Milliseconds without renewal before a sync run loses cache access. */
|
|
12
|
+
cacheGenerationClaimTtlMs?: number;
|
|
13
|
+
/** Host clock, injectable for expiry histories. */
|
|
14
|
+
now?: () => number;
|
|
11
15
|
};
|
|
12
16
|
export type SQLitePullSinceResult<TKey extends string | number> = {
|
|
13
17
|
latestRowVersion: number;
|
|
@@ -19,6 +23,7 @@ export type SQLitePullSinceResult<TKey extends string | number> = {
|
|
|
19
23
|
deletedKeys: Array<TKey>;
|
|
20
24
|
deltas: Array<ReplayableTxDelta<Record<string, unknown>, TKey>>;
|
|
21
25
|
};
|
|
26
|
+
export declare const DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS: number;
|
|
22
27
|
/**
|
|
23
28
|
* Default cap on retained `applied_tx` rows per collection. The log is a
|
|
24
29
|
* replayable cache, so a bounded row count keeps SQLite files from growing
|
|
@@ -43,9 +48,21 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
|
|
|
43
48
|
private readonly appliedTxPruneMaxAgeSeconds;
|
|
44
49
|
private readonly pullSinceReloadThreshold;
|
|
45
50
|
private readonly replacementBatchSize;
|
|
51
|
+
private readonly cacheGenerationClaimTtlMs;
|
|
52
|
+
private readonly now;
|
|
46
53
|
private initialized;
|
|
47
54
|
private readonly collectionTableCache;
|
|
48
55
|
private readonly collectionTableLoads;
|
|
56
|
+
claimCacheGeneration(collectionId: string): Promise<PersistedCacheGenerationClaim>;
|
|
57
|
+
rotateCacheGeneration(collectionId: string, claimId: string, resetMetadata?: {
|
|
58
|
+
key: string;
|
|
59
|
+
value: unknown;
|
|
60
|
+
}, expectedStorageCollectionId?: string): Promise<PersistedCacheGenerationClaim>;
|
|
61
|
+
releaseCacheGenerationClaim(claimId: string): Promise<void>;
|
|
62
|
+
renewCacheGenerationClaim(storageCollectionId: string, claimId: string): Promise<number | undefined>;
|
|
63
|
+
private renewCacheGenerationClaimUnscheduled;
|
|
64
|
+
private collectRetiredCacheGenerations;
|
|
65
|
+
getCacheGenerationNow(): number;
|
|
49
66
|
constructor(options: SQLiteCoreAdapterOptions);
|
|
50
67
|
runInHydrationScope<T>(task: (adapter: HydrationPersistenceAdapter) => Promise<T>): Promise<T>;
|
|
51
68
|
runInRegularScope<T>(task: (adapter: HydrationPersistenceAdapter) => Promise<T>): Promise<T>;
|
|
@@ -54,17 +71,20 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
|
|
|
54
71
|
private resolveScheduler;
|
|
55
72
|
private runInTransaction;
|
|
56
73
|
private assertCurrentSchemaVersion;
|
|
57
|
-
|
|
74
|
+
private assertCacheGenerationReadClaim;
|
|
75
|
+
loadSubset(target: PersistedStorageTarget, options: LoadSubsetOptions, ctx?: {
|
|
58
76
|
requiredIndexSignatures?: ReadonlyArray<string>;
|
|
77
|
+
cacheGenerationClaimId?: string;
|
|
59
78
|
}): Promise<Array<{
|
|
60
79
|
key: string | number;
|
|
61
80
|
value: Record<string, unknown>;
|
|
62
81
|
metadata?: unknown;
|
|
63
82
|
}>>;
|
|
64
83
|
private loadSubsetUnscheduled;
|
|
65
|
-
loadResumeSnapshot(
|
|
84
|
+
loadResumeSnapshot(target: PersistedStorageTarget, ctx?: {
|
|
66
85
|
requiredIndexSignatures?: ReadonlyArray<string>;
|
|
67
86
|
includeRows?: boolean;
|
|
87
|
+
cacheGenerationClaimId?: string;
|
|
68
88
|
}): Promise<{
|
|
69
89
|
rows: Array<{
|
|
70
90
|
key: string | number;
|
|
@@ -82,29 +102,41 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
|
|
|
82
102
|
resetEpoch: number;
|
|
83
103
|
}>;
|
|
84
104
|
private loadResumeSnapshotUnscheduled;
|
|
85
|
-
applyCommittedTx(
|
|
86
|
-
reconcileCommittedTx(
|
|
105
|
+
applyCommittedTx(target: PersistedStorageTarget, tx: PersistedTx): Promise<void>;
|
|
106
|
+
reconcileCommittedTx(target: PersistedStorageTarget, tx: PersistedTx, anchor: CommittedTxAnchor): Promise<ReconciledCommittedTx>;
|
|
87
107
|
private applyCommittedTxUnscheduled;
|
|
88
108
|
private reconcileCommittedTxUnscheduled;
|
|
89
109
|
private applyCommittedTxInternal;
|
|
90
|
-
loadCollectionMetadata(
|
|
110
|
+
loadCollectionMetadata(target: PersistedStorageTarget, ctx?: {
|
|
111
|
+
cacheGenerationClaimId?: string;
|
|
112
|
+
}): Promise<Array<{
|
|
91
113
|
key: string;
|
|
92
114
|
value: unknown;
|
|
93
115
|
}>>;
|
|
94
116
|
private loadCollectionMetadataUnscheduled;
|
|
95
|
-
scanRows(
|
|
117
|
+
scanRows(target: PersistedStorageTarget, options?: PersistedRowScanOptions, ctx?: {
|
|
118
|
+
cacheGenerationClaimId?: string;
|
|
119
|
+
}): Promise<Array<PersistedScannedRow>>;
|
|
96
120
|
private scanRowsUnscheduled;
|
|
97
|
-
ensureIndex(
|
|
121
|
+
ensureIndex(target: PersistedStorageTarget, signature: string, spec: PersistedIndexSpec, ctx?: {
|
|
122
|
+
cacheGenerationClaimId?: string;
|
|
123
|
+
}): Promise<void>;
|
|
98
124
|
private ensureIndexUnscheduled;
|
|
99
|
-
markIndexRemoved(
|
|
125
|
+
markIndexRemoved(target: PersistedStorageTarget, signature: string, ctx?: {
|
|
126
|
+
cacheGenerationClaimId?: string;
|
|
127
|
+
}): Promise<void>;
|
|
100
128
|
private markIndexRemovedUnscheduled;
|
|
101
|
-
reserveLeadershipTerm(
|
|
129
|
+
reserveLeadershipTerm(target: PersistedStorageTarget, observedTerm: number, ctx?: {
|
|
130
|
+
cacheGenerationClaimId?: string;
|
|
131
|
+
}): Promise<{
|
|
102
132
|
latestTerm: number;
|
|
103
133
|
latestSeq: number;
|
|
104
134
|
latestRowVersion: number;
|
|
105
135
|
}>;
|
|
106
136
|
private reserveLeadershipTermUnscheduled;
|
|
107
|
-
getStreamPosition(
|
|
137
|
+
getStreamPosition(target: PersistedStorageTarget, ctx?: {
|
|
138
|
+
cacheGenerationClaimId?: string;
|
|
139
|
+
}): Promise<{
|
|
108
140
|
latestTerm: number;
|
|
109
141
|
latestSeq: number;
|
|
110
142
|
latestRowVersion: number;
|
|
@@ -113,7 +145,9 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
|
|
|
113
145
|
private readLatestRowVersion;
|
|
114
146
|
private readStreamPosition;
|
|
115
147
|
private readKeySetEvidence;
|
|
116
|
-
pullSince(
|
|
148
|
+
pullSince(target: PersistedStorageTarget, fromRowVersion: number, ctx?: {
|
|
149
|
+
cacheGenerationClaimId?: string;
|
|
150
|
+
}): Promise<SQLitePullSinceResult<string | number>>;
|
|
117
151
|
private pullSinceUnscheduled;
|
|
118
152
|
private loadSubsetInternal;
|
|
119
153
|
private applyInMemoryWhere;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { LoadSubsetOptions } from '@tanstack/db';
|
|
2
2
|
import { RemoteSubsetOwner } from './remote-subset-owner.js';
|
|
3
|
-
import { ApplyCommittedTxResponse, ApplyLocalMutationsResponse, CommittedTxAnchor, HydrationPersistenceAdapter, PersistedCollectionCoordinator, PersistedIndexSpec, PersistedMutationEnvelope, PersistedTx, PersistenceAdapter, ProtocolEnvelope, PullSinceResponse, ReconcileCommittedTxResponse } from './persisted.js';
|
|
3
|
+
import { ApplyCommittedTxResponse, ApplyLocalMutationsResponse, CommittedTxAnchor, HydrationPersistenceAdapter, PersistedCollectionCoordinator, PersistedIndexSpec, PersistedMutationEnvelope, PersistedStorageTarget, PersistedTx, PersistenceAdapter, ProtocolEnvelope, PullSinceResponse, ReconcileCommittedTxResponse } from './persisted.js';
|
|
4
4
|
type CoordinatorAdapter = Pick<PersistenceAdapter, `loadSubset` | `applyCommittedTx` | `reconcileCommittedTx` | `ensureIndex` | `runInRegularScope`> & {
|
|
5
|
-
pullSince?: (
|
|
5
|
+
pullSince?: (target: PersistedStorageTarget, fromRowVersion: number, ctx?: {
|
|
6
|
+
cacheGenerationClaimId?: string;
|
|
7
|
+
}) => Promise<{
|
|
6
8
|
latestRowVersion: number;
|
|
7
9
|
requiresFullReload: true;
|
|
8
10
|
} | {
|
|
@@ -11,12 +13,16 @@ type CoordinatorAdapter = Pick<PersistenceAdapter, `loadSubset` | `applyCommitte
|
|
|
11
13
|
changedKeys: Array<string | number>;
|
|
12
14
|
deletedKeys: Array<string | number>;
|
|
13
15
|
}>;
|
|
14
|
-
getStreamPosition?: (
|
|
16
|
+
getStreamPosition?: (target: PersistedStorageTarget, ctx?: {
|
|
17
|
+
cacheGenerationClaimId?: string;
|
|
18
|
+
}) => Promise<{
|
|
15
19
|
latestTerm: number;
|
|
16
20
|
latestSeq: number;
|
|
17
21
|
latestRowVersion: number;
|
|
18
22
|
}>;
|
|
19
|
-
reserveLeadershipTerm?: (
|
|
23
|
+
reserveLeadershipTerm?: (target: PersistedStorageTarget, observedTerm: number, ctx?: {
|
|
24
|
+
cacheGenerationClaimId?: string;
|
|
25
|
+
}) => Promise<{
|
|
20
26
|
latestTerm: number;
|
|
21
27
|
latestSeq: number;
|
|
22
28
|
latestRowVersion: number;
|
|
@@ -35,6 +41,7 @@ export declare class BroadcastCollectionCoordinator implements PersistedCollecti
|
|
|
35
41
|
private readonly coordinatorName;
|
|
36
42
|
private defaultAdapter;
|
|
37
43
|
private readonly collectionAdapters;
|
|
44
|
+
private readonly collectionClaimIds;
|
|
38
45
|
private readonly remoteSubsetOwners;
|
|
39
46
|
private readonly remoteSubsetIds;
|
|
40
47
|
private readonly outboundRemoteSubsetAcquisitions;
|
|
@@ -61,7 +68,7 @@ export declare class BroadcastCollectionCoordinator implements PersistedCollecti
|
|
|
61
68
|
*/
|
|
62
69
|
setAdapter(adapter: CoordinatorAdapter): void;
|
|
63
70
|
/** Register the persistence adapter that owns one collection. */
|
|
64
|
-
setAdapterForCollection(collectionId: string, adapter: CoordinatorAdapter): void;
|
|
71
|
+
setAdapterForCollection(collectionId: string, adapter: CoordinatorAdapter, cacheGenerationClaimId?: string): void;
|
|
65
72
|
registerRemoteSubsetOwner(collectionId: string, owner: RemoteSubsetOwner): () => void;
|
|
66
73
|
getNodeId(): string;
|
|
67
74
|
subscribe(collectionId: string, onMessage: (message: ProtocolEnvelope<unknown>) => void): () => void;
|
|
@@ -70,12 +77,13 @@ export declare class BroadcastCollectionCoordinator implements PersistedCollecti
|
|
|
70
77
|
ensureLeadership(collectionId: string): Promise<void>;
|
|
71
78
|
requestEnsureRemoteSubset(collectionId: string, options: LoadSubsetOptions): Promise<void>;
|
|
72
79
|
requestReleaseRemoteSubset(collectionId: string, options: LoadSubsetOptions): Promise<void>;
|
|
80
|
+
private scheduleRemoteSubsetReleaseRetry;
|
|
73
81
|
private acquireRemoteSubset;
|
|
74
|
-
requestEnsurePersistedIndex(collectionId: string, signature: string, spec: PersistedIndexSpec, scopedAdapter?: HydrationPersistenceAdapter, localEnsureCompleted?: boolean): Promise<void>;
|
|
75
|
-
requestApplyLocalMutations(collectionId: string, mutations: Array<PersistedMutationEnvelope
|
|
82
|
+
requestEnsurePersistedIndex(collectionId: string, signature: string, spec: PersistedIndexSpec, scopedAdapter?: HydrationPersistenceAdapter, localEnsureCompleted?: boolean, cacheGenerationClaimId?: string): Promise<void>;
|
|
83
|
+
requestApplyLocalMutations(collectionId: string, mutations: Array<PersistedMutationEnvelope>, cacheGenerationClaimId?: string): Promise<ApplyLocalMutationsResponse>;
|
|
76
84
|
requestApplyCommittedTx(collectionId: string, tx: PersistedTx, scopedAdapter?: HydrationPersistenceAdapter): Promise<ApplyCommittedTxResponse>;
|
|
77
85
|
reconcileCommittedTx(collectionId: string, tx: PersistedTx, anchor: CommittedTxAnchor, scopedAdapter?: HydrationPersistenceAdapter): Promise<ReconcileCommittedTxResponse>;
|
|
78
|
-
pullSince(collectionId: string, fromRowVersion: number, scopedAdapter?: HydrationPersistenceAdapter): Promise<PullSinceResponse>;
|
|
86
|
+
pullSince(collectionId: string, fromRowVersion: number, scopedAdapter?: HydrationPersistenceAdapter, cacheGenerationClaimId?: string): Promise<PullSinceResponse>;
|
|
79
87
|
dispose(): void;
|
|
80
88
|
private getOrCreateCollectionState;
|
|
81
89
|
private ensureCollectionState;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { safeRandomUUID } from "@tanstack/db";
|
|
2
2
|
import { isValidCommittedTxAnchor } from "./committed-tx-anchor.js";
|
|
3
|
-
import { DuplicateRemoteSubsetOwnerError, InvalidPersistenceAdapterError, RetryableRemoteSubsetAcquisitionError, IndeterminateCommitError, toPersistedCollectionDurabilityError, PersistedCollectionDurabilityError } from "./errors.js";
|
|
3
|
+
import { DuplicateRemoteSubsetOwnerError, InvalidPersistenceAdapterError, RetryableRemoteSubsetAcquisitionError, IndeterminateCommitError, InvalidPersistedCollectionConfigError, toPersistedCollectionDurabilityError, PersistedCollectionDurabilityError } from "./errors.js";
|
|
4
4
|
import { toTransportedLoadSubsetOptions, toProcessLocalLoadSubsetOptions } from "./remote-subset-wire.js";
|
|
5
5
|
import { unloadRemoteSubsetOwner } from "./remote-subset-owner.js";
|
|
6
|
+
import { managedStorageTarget, eagerStorageTarget } from "./persisted.js";
|
|
6
7
|
const HEARTBEAT_INTERVAL_MS = 3e3;
|
|
7
8
|
const LEADERSHIP_RETRY_DELAY_MS = 200;
|
|
8
9
|
const RPC_TIMEOUT_MS = 1e4;
|
|
@@ -21,6 +22,7 @@ class BroadcastCollectionCoordinator {
|
|
|
21
22
|
constructor(options) {
|
|
22
23
|
this.nodeId = safeRandomUUID();
|
|
23
24
|
this.collectionAdapters = /* @__PURE__ */ new Map();
|
|
25
|
+
this.collectionClaimIds = /* @__PURE__ */ new Map();
|
|
24
26
|
this.remoteSubsetOwners = /* @__PURE__ */ new Map();
|
|
25
27
|
this.remoteSubsetIds = /* @__PURE__ */ new Map();
|
|
26
28
|
this.outboundRemoteSubsetAcquisitions = /* @__PURE__ */ new Map();
|
|
@@ -76,9 +78,14 @@ class BroadcastCollectionCoordinator {
|
|
|
76
78
|
this.defaultAdapter = adapter;
|
|
77
79
|
}
|
|
78
80
|
/** Register the persistence adapter that owns one collection. */
|
|
79
|
-
setAdapterForCollection(collectionId, adapter) {
|
|
81
|
+
setAdapterForCollection(collectionId, adapter, cacheGenerationClaimId) {
|
|
80
82
|
requireDurableElection(adapter);
|
|
81
83
|
this.collectionAdapters.set(collectionId, adapter);
|
|
84
|
+
if (cacheGenerationClaimId) {
|
|
85
|
+
this.collectionClaimIds.set(collectionId, cacheGenerationClaimId);
|
|
86
|
+
} else {
|
|
87
|
+
this.collectionClaimIds.delete(collectionId);
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
90
|
registerRemoteSubsetOwner(collectionId, owner) {
|
|
84
91
|
if (this.remoteSubsetOwners.has(collectionId)) {
|
|
@@ -222,10 +229,29 @@ class BroadcastCollectionCoordinator {
|
|
|
222
229
|
}
|
|
223
230
|
try {
|
|
224
231
|
await acquisition.release;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.scheduleRemoteSubsetReleaseRetry(acquisition, options);
|
|
234
|
+
throw error;
|
|
225
235
|
} finally {
|
|
226
236
|
acquisition.release = null;
|
|
227
237
|
}
|
|
228
238
|
}
|
|
239
|
+
scheduleRemoteSubsetReleaseRetry(acquisition, options) {
|
|
240
|
+
const key = remoteSubsetAcquisitionKey(
|
|
241
|
+
acquisition.collectionId,
|
|
242
|
+
acquisition.acquisitionId
|
|
243
|
+
);
|
|
244
|
+
if (this.isDisposed() || this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition || acquisition.retryTimer !== null) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
acquisition.retryTimer = setTimeout(() => {
|
|
248
|
+
acquisition.retryTimer = null;
|
|
249
|
+
void this.requestReleaseRemoteSubset(
|
|
250
|
+
acquisition.collectionId,
|
|
251
|
+
options
|
|
252
|
+
).catch(() => void 0);
|
|
253
|
+
}, RPC_RETRY_DELAY_MS);
|
|
254
|
+
}
|
|
229
255
|
async acquireRemoteSubset(acquisition) {
|
|
230
256
|
if (acquisition.inFlight) return acquisition.inFlight;
|
|
231
257
|
const route = { localOwner: false };
|
|
@@ -285,7 +311,9 @@ class BroadcastCollectionCoordinator {
|
|
|
285
311
|
try {
|
|
286
312
|
await work;
|
|
287
313
|
acquired = true;
|
|
288
|
-
|
|
314
|
+
if (!acquisition.releaseRequested) {
|
|
315
|
+
this.cancelRemoteSubsetReplayRetry(acquisition);
|
|
316
|
+
}
|
|
289
317
|
} finally {
|
|
290
318
|
if (acquisition.inFlight === work) acquisition.inFlight = null;
|
|
291
319
|
const current = this.collections.get(acquisition.collectionId);
|
|
@@ -302,21 +330,34 @@ class BroadcastCollectionCoordinator {
|
|
|
302
330
|
}
|
|
303
331
|
}
|
|
304
332
|
}
|
|
305
|
-
async requestEnsurePersistedIndex(collectionId, signature, spec, scopedAdapter, localEnsureCompleted = false) {
|
|
333
|
+
async requestEnsurePersistedIndex(collectionId, signature, spec, scopedAdapter, localEnsureCompleted = false, cacheGenerationClaimId) {
|
|
306
334
|
if (this.isLeader(collectionId)) {
|
|
307
335
|
if (localEnsureCompleted) return;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
336
|
+
const adapter = scopedAdapter ?? this.requireAdapter(collectionId);
|
|
337
|
+
if (cacheGenerationClaimId) {
|
|
338
|
+
await adapter.ensureIndex(
|
|
339
|
+
managedStorageTarget(collectionId, cacheGenerationClaimId),
|
|
340
|
+
signature,
|
|
341
|
+
spec,
|
|
342
|
+
{
|
|
343
|
+
cacheGenerationClaimId
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
await adapter.ensureIndex(
|
|
348
|
+
eagerStorageTarget(collectionId),
|
|
349
|
+
signature,
|
|
350
|
+
spec
|
|
351
|
+
);
|
|
352
|
+
}
|
|
313
353
|
return;
|
|
314
354
|
}
|
|
315
355
|
const response = await this.sendRPC(collectionId, {
|
|
316
356
|
type: `rpc:ensurePersistedIndex:req`,
|
|
317
357
|
rpcId: safeRandomUUID(),
|
|
318
358
|
signature,
|
|
319
|
-
spec
|
|
359
|
+
spec,
|
|
360
|
+
cacheGenerationClaimId
|
|
320
361
|
});
|
|
321
362
|
if (!response.ok) {
|
|
322
363
|
throw new Error(
|
|
@@ -324,7 +365,7 @@ class BroadcastCollectionCoordinator {
|
|
|
324
365
|
);
|
|
325
366
|
}
|
|
326
367
|
}
|
|
327
|
-
async requestApplyLocalMutations(collectionId, mutations) {
|
|
368
|
+
async requestApplyLocalMutations(collectionId, mutations, cacheGenerationClaimId) {
|
|
328
369
|
const initialRoute = this.waitForInitialRoute(collectionId);
|
|
329
370
|
if (initialRoute) await initialRoute;
|
|
330
371
|
if (this.isLeader(collectionId)) {
|
|
@@ -332,14 +373,16 @@ class BroadcastCollectionCoordinator {
|
|
|
332
373
|
type: `rpc:applyLocalMutations:req`,
|
|
333
374
|
rpcId: safeRandomUUID(),
|
|
334
375
|
envelopeId: safeRandomUUID(),
|
|
335
|
-
mutations
|
|
376
|
+
mutations,
|
|
377
|
+
cacheGenerationClaimId
|
|
336
378
|
});
|
|
337
379
|
}
|
|
338
380
|
return this.sendRPC(collectionId, {
|
|
339
381
|
type: `rpc:applyLocalMutations:req`,
|
|
340
382
|
rpcId: safeRandomUUID(),
|
|
341
383
|
envelopeId: safeRandomUUID(),
|
|
342
|
-
mutations
|
|
384
|
+
mutations,
|
|
385
|
+
cacheGenerationClaimId
|
|
343
386
|
});
|
|
344
387
|
}
|
|
345
388
|
async requestApplyCommittedTx(collectionId, tx, scopedAdapter) {
|
|
@@ -380,14 +423,15 @@ class BroadcastCollectionCoordinator {
|
|
|
380
423
|
}
|
|
381
424
|
return response;
|
|
382
425
|
}
|
|
383
|
-
async pullSince(collectionId, fromRowVersion, scopedAdapter) {
|
|
426
|
+
async pullSince(collectionId, fromRowVersion, scopedAdapter, cacheGenerationClaimId) {
|
|
384
427
|
if (this.isLeader(collectionId)) {
|
|
385
428
|
return this.handlePullSince(
|
|
386
429
|
collectionId,
|
|
387
430
|
{
|
|
388
431
|
type: `rpc:pullSince:req`,
|
|
389
432
|
rpcId: safeRandomUUID(),
|
|
390
|
-
fromRowVersion
|
|
433
|
+
fromRowVersion,
|
|
434
|
+
cacheGenerationClaimId
|
|
391
435
|
},
|
|
392
436
|
scopedAdapter
|
|
393
437
|
);
|
|
@@ -395,7 +439,8 @@ class BroadcastCollectionCoordinator {
|
|
|
395
439
|
return this.sendRPC(collectionId, {
|
|
396
440
|
type: `rpc:pullSince:req`,
|
|
397
441
|
rpcId: safeRandomUUID(),
|
|
398
|
-
fromRowVersion
|
|
442
|
+
fromRowVersion,
|
|
443
|
+
cacheGenerationClaimId
|
|
399
444
|
});
|
|
400
445
|
}
|
|
401
446
|
// -----------------------------------------------------------------------
|
|
@@ -428,6 +473,7 @@ class BroadcastCollectionCoordinator {
|
|
|
428
473
|
this.channel.close();
|
|
429
474
|
this.collections.clear();
|
|
430
475
|
this.collectionAdapters.clear();
|
|
476
|
+
this.collectionClaimIds.clear();
|
|
431
477
|
for (const collectionId of this.remoteSubsetOwners.keys()) {
|
|
432
478
|
this.releaseInboundRemoteSubsetAcquisitions(collectionId);
|
|
433
479
|
}
|
|
@@ -488,9 +534,15 @@ class BroadcastCollectionCoordinator {
|
|
|
488
534
|
const reserveTerm = adapter.reserveLeadershipTerm;
|
|
489
535
|
if (!reserveTerm)
|
|
490
536
|
throw new InvalidPersistenceAdapterError(`reserveLeadershipTerm`);
|
|
491
|
-
const pos = await this.withWriterLock(
|
|
492
|
-
|
|
493
|
-
|
|
537
|
+
const pos = await this.withWriterLock(() => {
|
|
538
|
+
const claimId = this.collectionClaimIds.get(collectionId);
|
|
539
|
+
return reserveTerm.call(
|
|
540
|
+
adapter,
|
|
541
|
+
claimId ? managedStorageTarget(collectionId, claimId) : eagerStorageTarget(collectionId),
|
|
542
|
+
state.latestTerm,
|
|
543
|
+
{ cacheGenerationClaimId: claimId }
|
|
544
|
+
);
|
|
545
|
+
});
|
|
494
546
|
state.latestTerm = pos.latestTerm;
|
|
495
547
|
state.latestSeq = pos.latestSeq;
|
|
496
548
|
state.latestRowVersion = pos.latestRowVersion;
|
|
@@ -637,6 +689,7 @@ class BroadcastCollectionCoordinator {
|
|
|
637
689
|
this.releaseLeadership(collectionId, state);
|
|
638
690
|
this.collections.delete(collectionId);
|
|
639
691
|
this.collectionAdapters.delete(collectionId);
|
|
692
|
+
this.collectionClaimIds.delete(collectionId);
|
|
640
693
|
const prefix = `${JSON.stringify([collectionId]).slice(0, -1)},`;
|
|
641
694
|
for (const key of this.appliedEnvelopes.keys()) {
|
|
642
695
|
if (key.startsWith(prefix)) this.appliedEnvelopes.delete(key);
|
|
@@ -1050,10 +1103,7 @@ class BroadcastCollectionCoordinator {
|
|
|
1050
1103
|
acquisition.released = true;
|
|
1051
1104
|
acquisition.release = (async () => {
|
|
1052
1105
|
try {
|
|
1053
|
-
await acquisition.
|
|
1054
|
-
} catch {
|
|
1055
|
-
}
|
|
1056
|
-
try {
|
|
1106
|
+
await acquisition.admitted;
|
|
1057
1107
|
if (acquisition.transferred) {
|
|
1058
1108
|
await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options);
|
|
1059
1109
|
}
|
|
@@ -1169,6 +1219,7 @@ class BroadcastCollectionCoordinator {
|
|
|
1169
1219
|
owner: options.owner,
|
|
1170
1220
|
options: options.options,
|
|
1171
1221
|
load: Promise.resolve(),
|
|
1222
|
+
admitted: Promise.resolve(),
|
|
1172
1223
|
transferred: false,
|
|
1173
1224
|
released: false,
|
|
1174
1225
|
terminalRelease: false,
|
|
@@ -1176,6 +1227,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1176
1227
|
};
|
|
1177
1228
|
this.releasedRemoteSubsetAcquisitionTimes.delete(key);
|
|
1178
1229
|
this.inboundRemoteSubsetAcquisitions.set(key, acquisition);
|
|
1230
|
+
let resolveAdmission;
|
|
1231
|
+
acquisition.admitted = new Promise((resolve) => {
|
|
1232
|
+
resolveAdmission = resolve;
|
|
1233
|
+
});
|
|
1179
1234
|
let resolveLoad;
|
|
1180
1235
|
let rejectLoad;
|
|
1181
1236
|
acquisition.load = new Promise((resolve, reject) => {
|
|
@@ -1188,6 +1243,8 @@ class BroadcastCollectionCoordinator {
|
|
|
1188
1243
|
void Promise.resolve(load).then(resolveLoad, rejectLoad);
|
|
1189
1244
|
} catch (error) {
|
|
1190
1245
|
rejectLoad(error);
|
|
1246
|
+
} finally {
|
|
1247
|
+
resolveAdmission();
|
|
1191
1248
|
}
|
|
1192
1249
|
try {
|
|
1193
1250
|
await acquisition.load;
|
|
@@ -1212,10 +1269,23 @@ class BroadcastCollectionCoordinator {
|
|
|
1212
1269
|
}
|
|
1213
1270
|
}
|
|
1214
1271
|
async handleEnsurePersistedIndex(collectionId, request) {
|
|
1215
|
-
await this.withScheduledWriterLock(
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1272
|
+
await this.withScheduledWriterLock(collectionId, (adapter) => {
|
|
1273
|
+
if (request.cacheGenerationClaimId) {
|
|
1274
|
+
return adapter.ensureIndex(
|
|
1275
|
+
managedStorageTarget(collectionId, request.cacheGenerationClaimId),
|
|
1276
|
+
request.signature,
|
|
1277
|
+
request.spec,
|
|
1278
|
+
{
|
|
1279
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId
|
|
1280
|
+
}
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
return adapter.ensureIndex(
|
|
1284
|
+
eagerStorageTarget(collectionId),
|
|
1285
|
+
request.signature,
|
|
1286
|
+
request.spec
|
|
1287
|
+
);
|
|
1288
|
+
});
|
|
1219
1289
|
return {
|
|
1220
1290
|
type: `rpc:ensurePersistedIndex:res`,
|
|
1221
1291
|
rpcId: request.rpcId,
|
|
@@ -1244,6 +1314,11 @@ class BroadcastCollectionCoordinator {
|
|
|
1244
1314
|
});
|
|
1245
1315
|
}
|
|
1246
1316
|
async applyLocalMutationsOnce(collectionId, request) {
|
|
1317
|
+
if (this.collectionClaimIds.has(collectionId) && !request.cacheGenerationClaimId) {
|
|
1318
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1319
|
+
`A managed local mutation requires a persisted cache claim`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1247
1322
|
const state = this.collections.get(collectionId);
|
|
1248
1323
|
if (!state || !state.isLeader) {
|
|
1249
1324
|
return {
|
|
@@ -1269,6 +1344,7 @@ class BroadcastCollectionCoordinator {
|
|
|
1269
1344
|
}
|
|
1270
1345
|
const pendingTx = {
|
|
1271
1346
|
txId: safeRandomUUID(),
|
|
1347
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId,
|
|
1272
1348
|
mutations: request.mutations.map((m) => ({
|
|
1273
1349
|
type: m.type,
|
|
1274
1350
|
key: m.key,
|
|
@@ -1501,7 +1577,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1501
1577
|
if (!adapter.reconcileCommittedTx) return { kind: `unknown` };
|
|
1502
1578
|
try {
|
|
1503
1579
|
const reconciled = await adapter.reconcileCommittedTx(
|
|
1504
|
-
|
|
1580
|
+
candidate.cacheGenerationClaimId ? managedStorageTarget(
|
|
1581
|
+
collectionId,
|
|
1582
|
+
candidate.cacheGenerationClaimId
|
|
1583
|
+
) : eagerStorageTarget(collectionId),
|
|
1505
1584
|
candidate,
|
|
1506
1585
|
request.anchor
|
|
1507
1586
|
);
|
|
@@ -1577,7 +1656,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1577
1656
|
);
|
|
1578
1657
|
if (tx === null) return null;
|
|
1579
1658
|
try {
|
|
1580
|
-
await adapter.applyCommittedTx(
|
|
1659
|
+
await adapter.applyCommittedTx(
|
|
1660
|
+
tx.cacheGenerationClaimId ? managedStorageTarget(collectionId, tx.cacheGenerationClaimId) : eagerStorageTarget(collectionId),
|
|
1661
|
+
tx
|
|
1662
|
+
);
|
|
1581
1663
|
} catch (error) {
|
|
1582
1664
|
throw toPersistedCollectionDurabilityError(collectionId, error);
|
|
1583
1665
|
}
|
|
@@ -1602,7 +1684,13 @@ class BroadcastCollectionCoordinator {
|
|
|
1602
1684
|
requiresFullReload: true
|
|
1603
1685
|
};
|
|
1604
1686
|
}
|
|
1605
|
-
const result = await adapter.pullSince(
|
|
1687
|
+
const result = await adapter.pullSince(
|
|
1688
|
+
request.cacheGenerationClaimId ? managedStorageTarget(collectionId, request.cacheGenerationClaimId) : eagerStorageTarget(collectionId),
|
|
1689
|
+
request.fromRowVersion,
|
|
1690
|
+
{
|
|
1691
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId
|
|
1692
|
+
}
|
|
1693
|
+
);
|
|
1606
1694
|
if (result.requiresFullReload) {
|
|
1607
1695
|
return {
|
|
1608
1696
|
type: `rpc:pullSince:res`,
|