@tanstack/db-sqlite-persistence-core 0.1.2
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 +95 -0
- package/dist/cjs/errors.cjs +72 -0
- package/dist/cjs/errors.cjs.map +1 -0
- package/dist/cjs/errors.d.cts +25 -0
- package/dist/cjs/index.cjs +22 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +3 -0
- package/dist/cjs/persisted.cjs +1705 -0
- package/dist/cjs/persisted.cjs.map +1 -0
- package/dist/cjs/persisted.d.cts +293 -0
- package/dist/cjs/sqlite-core-adapter.cjs +1588 -0
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -0
- package/dist/cjs/sqlite-core-adapter.d.cts +67 -0
- package/dist/esm/errors.d.ts +25 -0
- package/dist/esm/errors.js +72 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +22 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/persisted.d.ts +293 -0
- package/dist/esm/persisted.js +1705 -0
- package/dist/esm/persisted.js.map +1 -0
- package/dist/esm/sqlite-core-adapter.d.ts +67 -0
- package/dist/esm/sqlite-core-adapter.js +1588 -0
- package/dist/esm/sqlite-core-adapter.js.map +1 -0
- package/package.json +57 -0
- package/src/errors.ts +73 -0
- package/src/index.ts +3 -0
- package/src/persisted.ts +2820 -0
- package/src/sqlite-core-adapter.ts +2129 -0
|
@@ -0,0 +1,1705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const db = require("@tanstack/db");
|
|
4
|
+
const errors = require("./errors.cjs");
|
|
5
|
+
const REQUIRED_COORDINATOR_METHODS = [
|
|
6
|
+
`getNodeId`,
|
|
7
|
+
`subscribe`,
|
|
8
|
+
`publish`,
|
|
9
|
+
`isLeader`,
|
|
10
|
+
`ensureLeadership`,
|
|
11
|
+
`requestEnsurePersistedIndex`
|
|
12
|
+
];
|
|
13
|
+
const REQUIRED_ADAPTER_METHODS = [`loadSubset`, `applyCommittedTx`, `ensureIndex`];
|
|
14
|
+
const TARGETED_INVALIDATION_KEY_LIMIT = 128;
|
|
15
|
+
const DEFAULT_DB_NAME = `tanstack-db`;
|
|
16
|
+
const REMOTE_ENSURE_RETRY_DELAY_MS = 50;
|
|
17
|
+
class SingleProcessCoordinator {
|
|
18
|
+
constructor(nodeId = crypto.randomUUID()) {
|
|
19
|
+
this.nodeId = nodeId;
|
|
20
|
+
}
|
|
21
|
+
getNodeId() {
|
|
22
|
+
return this.nodeId;
|
|
23
|
+
}
|
|
24
|
+
subscribe() {
|
|
25
|
+
return () => {
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
publish() {
|
|
29
|
+
}
|
|
30
|
+
isLeader() {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
async ensureLeadership() {
|
|
34
|
+
}
|
|
35
|
+
async requestEnsureRemoteSubset() {
|
|
36
|
+
}
|
|
37
|
+
async requestEnsurePersistedIndex() {
|
|
38
|
+
}
|
|
39
|
+
pullSince() {
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
type: `rpc:pullSince:res`,
|
|
42
|
+
rpcId: crypto.randomUUID(),
|
|
43
|
+
ok: true,
|
|
44
|
+
latestTerm: 1,
|
|
45
|
+
latestSeq: 0,
|
|
46
|
+
latestRowVersion: 0,
|
|
47
|
+
requiresFullReload: false,
|
|
48
|
+
changedKeys: [],
|
|
49
|
+
deletedKeys: [],
|
|
50
|
+
deltas: []
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function validatePersistedCollectionCoordinator(coordinator) {
|
|
55
|
+
for (const method of REQUIRED_COORDINATOR_METHODS) {
|
|
56
|
+
if (typeof coordinator[method] !== `function`) {
|
|
57
|
+
throw new errors.InvalidPersistedCollectionCoordinatorError(method);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function validatePersistenceAdapter(adapter) {
|
|
62
|
+
for (const method of REQUIRED_ADAPTER_METHODS) {
|
|
63
|
+
if (typeof adapter[method] !== `function`) {
|
|
64
|
+
throw new errors.InvalidPersistenceAdapterError(method);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function resolvePersistence(persistence) {
|
|
69
|
+
validatePersistenceAdapter(persistence.adapter);
|
|
70
|
+
const coordinator = persistence.coordinator ?? new SingleProcessCoordinator();
|
|
71
|
+
validatePersistedCollectionCoordinator(coordinator);
|
|
72
|
+
return {
|
|
73
|
+
...persistence,
|
|
74
|
+
coordinator
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function resolvePersistenceForMode(persistence, mode) {
|
|
78
|
+
const modeSpecificPersistence = persistence.resolvePersistenceForMode?.(mode);
|
|
79
|
+
return resolvePersistence(modeSpecificPersistence ?? persistence);
|
|
80
|
+
}
|
|
81
|
+
function resolvePersistenceForCollection(persistence, options) {
|
|
82
|
+
const collectionSpecificPersistence = persistence.resolvePersistenceForCollection?.(options);
|
|
83
|
+
if (collectionSpecificPersistence) {
|
|
84
|
+
return resolvePersistence(collectionSpecificPersistence);
|
|
85
|
+
}
|
|
86
|
+
return resolvePersistenceForMode(persistence, options.mode);
|
|
87
|
+
}
|
|
88
|
+
function hasOwnSyncKey(options) {
|
|
89
|
+
return Object.prototype.hasOwnProperty.call(options, `sync`);
|
|
90
|
+
}
|
|
91
|
+
function isRecord(value) {
|
|
92
|
+
return typeof value === `object` && value !== null;
|
|
93
|
+
}
|
|
94
|
+
function isValidSyncConfig(value) {
|
|
95
|
+
if (!isRecord(value)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return typeof value.sync === `function`;
|
|
99
|
+
}
|
|
100
|
+
class ApplyMutex {
|
|
101
|
+
constructor() {
|
|
102
|
+
this.queue = Promise.resolve();
|
|
103
|
+
}
|
|
104
|
+
async run(task) {
|
|
105
|
+
const taskPromise = this.queue.then(() => task());
|
|
106
|
+
this.queue = taskPromise.then(
|
|
107
|
+
() => void 0,
|
|
108
|
+
() => void 0
|
|
109
|
+
);
|
|
110
|
+
return taskPromise;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function toStableSerializable(value) {
|
|
114
|
+
if (value == null) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
switch (typeof value) {
|
|
118
|
+
case `string`:
|
|
119
|
+
case `number`:
|
|
120
|
+
case `boolean`:
|
|
121
|
+
return value;
|
|
122
|
+
case `bigint`:
|
|
123
|
+
return value.toString();
|
|
124
|
+
case `function`:
|
|
125
|
+
case `symbol`:
|
|
126
|
+
case `undefined`:
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
if (value instanceof Date) {
|
|
130
|
+
return value.toISOString();
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.map((entry) => toStableSerializable(entry)).filter((entry) => entry !== void 0);
|
|
134
|
+
}
|
|
135
|
+
if (value instanceof Set) {
|
|
136
|
+
return Array.from(value).map((entry) => toStableSerializable(entry)).filter((entry) => entry !== void 0).sort((left, right) => {
|
|
137
|
+
const leftSerialized = JSON.stringify(left);
|
|
138
|
+
const rightSerialized = JSON.stringify(right);
|
|
139
|
+
return leftSerialized < rightSerialized ? -1 : leftSerialized > rightSerialized ? 1 : 0;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (value instanceof Map) {
|
|
143
|
+
return Array.from(value.entries()).map(([key, mapValue]) => ({
|
|
144
|
+
key: toStableSerializable(key),
|
|
145
|
+
value: toStableSerializable(mapValue)
|
|
146
|
+
})).filter((entry) => entry.key !== void 0 && entry.value !== void 0).sort((left, right) => {
|
|
147
|
+
const leftSerialized = JSON.stringify(left.key);
|
|
148
|
+
const rightSerialized = JSON.stringify(right.key);
|
|
149
|
+
return leftSerialized < rightSerialized ? -1 : leftSerialized > rightSerialized ? 1 : 0;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const record = value;
|
|
153
|
+
const orderedKeys = Object.keys(record).sort(
|
|
154
|
+
(left, right) => left < right ? -1 : left > right ? 1 : 0
|
|
155
|
+
);
|
|
156
|
+
const serializableRecord = {};
|
|
157
|
+
for (const key of orderedKeys) {
|
|
158
|
+
const serializableValue = toStableSerializable(record[key]);
|
|
159
|
+
if (serializableValue !== void 0) {
|
|
160
|
+
serializableRecord[key] = serializableValue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return serializableRecord;
|
|
164
|
+
}
|
|
165
|
+
function stableSerialize(value) {
|
|
166
|
+
return JSON.stringify(toStableSerializable(value) ?? null);
|
|
167
|
+
}
|
|
168
|
+
function normalizeSubsetOptionsForKey(options) {
|
|
169
|
+
return {
|
|
170
|
+
where: toStableSerializable(options.where),
|
|
171
|
+
orderBy: toStableSerializable(options.orderBy),
|
|
172
|
+
limit: options.limit,
|
|
173
|
+
cursor: toStableSerializable(options.cursor),
|
|
174
|
+
offset: options.offset
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function normalizeSyncFnResult(result) {
|
|
178
|
+
if (typeof result === `function`) {
|
|
179
|
+
return { cleanup: result };
|
|
180
|
+
}
|
|
181
|
+
if (result === void 0) {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
function isTxCommittedPayload(payload) {
|
|
187
|
+
if (!isRecord(payload) || payload.type !== `tx:committed`) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (typeof payload.term !== `number` || typeof payload.seq !== `number` || typeof payload.txId !== `string` || typeof payload.latestRowVersion !== `number` || typeof payload.requiresFullReload !== `boolean`) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (payload.requiresFullReload) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return Array.isArray(payload.changedRows) && Array.isArray(payload.deletedKeys) && (payload.rowMetadataMutations === void 0 || Array.isArray(payload.rowMetadataMutations)) && (payload.collectionMetadataMutations === void 0 || Array.isArray(payload.collectionMetadataMutations));
|
|
197
|
+
}
|
|
198
|
+
function isCollectionResetPayload(payload) {
|
|
199
|
+
return isRecord(payload) && payload.type === `collection:reset` && typeof payload.schemaVersion === `number` && typeof payload.resetEpoch === `number`;
|
|
200
|
+
}
|
|
201
|
+
function toPersistedMutationEnvelope(mutation) {
|
|
202
|
+
const key = mutation.key;
|
|
203
|
+
const value = mutation.type === `delete` ? mutation.original : mutation.modified;
|
|
204
|
+
return {
|
|
205
|
+
mutationId: mutation.mutationId,
|
|
206
|
+
type: mutation.type,
|
|
207
|
+
key,
|
|
208
|
+
value
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
class PersistedCollectionRuntime {
|
|
212
|
+
constructor(mode, collectionId, persistence, syncMode, dbName) {
|
|
213
|
+
this.mode = mode;
|
|
214
|
+
this.collectionId = collectionId;
|
|
215
|
+
this.persistence = persistence;
|
|
216
|
+
this.syncMode = syncMode;
|
|
217
|
+
this.dbName = dbName;
|
|
218
|
+
this.applyMutex = new ApplyMutex();
|
|
219
|
+
this.activeSubsets = /* @__PURE__ */ new Map();
|
|
220
|
+
this.pendingRemoteSubsetEnsures = /* @__PURE__ */ new Map();
|
|
221
|
+
this.queuedHydrationTransactions = [];
|
|
222
|
+
this.queuedTxCommitted = [];
|
|
223
|
+
this.subscriptionIds = /* @__PURE__ */ new WeakMap();
|
|
224
|
+
this.collection = null;
|
|
225
|
+
this.syncControls = {
|
|
226
|
+
begin: null,
|
|
227
|
+
write: null,
|
|
228
|
+
commit: null,
|
|
229
|
+
truncate: null,
|
|
230
|
+
metadata: null
|
|
231
|
+
};
|
|
232
|
+
this.started = false;
|
|
233
|
+
this.startupMetadataPromise = null;
|
|
234
|
+
this.startPromise = null;
|
|
235
|
+
this.internalApplyDepth = 0;
|
|
236
|
+
this.isHydrating = false;
|
|
237
|
+
this.coordinatorUnsubscribe = null;
|
|
238
|
+
this.indexAddedUnsubscribe = null;
|
|
239
|
+
this.indexRemovedUnsubscribe = null;
|
|
240
|
+
this.remoteEnsureRetryTimer = null;
|
|
241
|
+
this.nextSubscriptionId = 0;
|
|
242
|
+
this.latestTerm = 0;
|
|
243
|
+
this.latestSeq = 0;
|
|
244
|
+
this.latestRowVersion = 0;
|
|
245
|
+
this.localTerm = 1;
|
|
246
|
+
this.localSeq = 0;
|
|
247
|
+
this.localRowVersion = 0;
|
|
248
|
+
}
|
|
249
|
+
setSyncControls(syncControls) {
|
|
250
|
+
this.syncControls = syncControls;
|
|
251
|
+
}
|
|
252
|
+
clearSyncControls() {
|
|
253
|
+
this.syncControls = {
|
|
254
|
+
begin: null,
|
|
255
|
+
write: null,
|
|
256
|
+
commit: null,
|
|
257
|
+
truncate: null,
|
|
258
|
+
metadata: null
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
isHydratingNow() {
|
|
262
|
+
return this.isHydrating;
|
|
263
|
+
}
|
|
264
|
+
isApplyingInternally() {
|
|
265
|
+
return this.internalApplyDepth > 0;
|
|
266
|
+
}
|
|
267
|
+
setCollection(collection) {
|
|
268
|
+
if (this.collection === collection) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.collection = collection;
|
|
272
|
+
this.attachCoordinatorSubscription();
|
|
273
|
+
}
|
|
274
|
+
getLeadershipState() {
|
|
275
|
+
return {
|
|
276
|
+
nodeId: this.persistence.coordinator.getNodeId(),
|
|
277
|
+
isLeader: this.persistence.coordinator.isLeader(this.collectionId)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async ensureStarted() {
|
|
281
|
+
if (this.startPromise) {
|
|
282
|
+
return this.startPromise;
|
|
283
|
+
}
|
|
284
|
+
this.startPromise = this.startInternal();
|
|
285
|
+
return this.startPromise;
|
|
286
|
+
}
|
|
287
|
+
async ensureStartupMetadataLoaded() {
|
|
288
|
+
if (this.startupMetadataPromise) {
|
|
289
|
+
return this.startupMetadataPromise;
|
|
290
|
+
}
|
|
291
|
+
this.startupMetadataPromise = this.loadStartupMetadataInternal();
|
|
292
|
+
return this.startupMetadataPromise;
|
|
293
|
+
}
|
|
294
|
+
async startInternal() {
|
|
295
|
+
if (this.started) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.started = true;
|
|
299
|
+
await this.ensureStartupMetadataLoaded();
|
|
300
|
+
const indexBootstrapSnapshot = this.collection?.getIndexMetadata() ?? [];
|
|
301
|
+
this.attachIndexLifecycleListeners();
|
|
302
|
+
await this.bootstrapPersistedIndexes(indexBootstrapSnapshot);
|
|
303
|
+
if (this.syncMode !== `on-demand`) {
|
|
304
|
+
this.activeSubsets.set(this.getSubsetKey({}), {});
|
|
305
|
+
await this.applyMutex.run(
|
|
306
|
+
() => this.hydrateSubsetUnsafe({}, { requestRemoteEnsure: false })
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async loadStartupMetadataInternal() {
|
|
311
|
+
if (this.persistence.adapter.getStreamPosition) {
|
|
312
|
+
const position = await this.persistence.adapter.getStreamPosition(
|
|
313
|
+
this.collectionId
|
|
314
|
+
);
|
|
315
|
+
this.observeStreamPosition(
|
|
316
|
+
position.latestTerm,
|
|
317
|
+
position.latestSeq,
|
|
318
|
+
position.latestRowVersion
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
await this.loadCollectionMetadataIntoCollection();
|
|
322
|
+
}
|
|
323
|
+
async loadCollectionMetadataIntoCollection() {
|
|
324
|
+
const collectionMetadata = await this.loadCollectionMetadataSnapshot();
|
|
325
|
+
this.replaceCollectionMetadataSnapshot(collectionMetadata);
|
|
326
|
+
}
|
|
327
|
+
async loadCollectionMetadataSnapshot() {
|
|
328
|
+
if (!this.persistence.adapter.loadCollectionMetadata) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
return this.persistence.adapter.loadCollectionMetadata(this.collectionId);
|
|
332
|
+
}
|
|
333
|
+
replaceCollectionMetadataSnapshot(collectionMetadata) {
|
|
334
|
+
if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const nextMetadata = new Map(
|
|
338
|
+
collectionMetadata.map(({ key, value }) => [key, value])
|
|
339
|
+
);
|
|
340
|
+
const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
|
|
341
|
+
this.withInternalApply(() => {
|
|
342
|
+
this.syncControls.begin?.({ immediate: true });
|
|
343
|
+
currentKeys.forEach((key) => {
|
|
344
|
+
if (!nextMetadata.has(key)) {
|
|
345
|
+
this.syncControls.metadata?.collection.delete(key);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
nextMetadata.forEach((value, key) => {
|
|
349
|
+
this.syncControls.metadata?.collection.set(key, value);
|
|
350
|
+
});
|
|
351
|
+
this.syncControls.commit?.();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async loadSubset(options, upstreamLoadSubset) {
|
|
355
|
+
this.activeSubsets.set(this.getSubsetKey(options), options);
|
|
356
|
+
await this.applyMutex.run(
|
|
357
|
+
() => this.hydrateSubsetUnsafe(options, {
|
|
358
|
+
requestRemoteEnsure: this.mode === `sync-present`
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
if (upstreamLoadSubset) {
|
|
362
|
+
try {
|
|
363
|
+
const maybePromise = upstreamLoadSubset(options);
|
|
364
|
+
if (maybePromise instanceof Promise) {
|
|
365
|
+
maybePromise.catch((error) => {
|
|
366
|
+
console.warn(
|
|
367
|
+
`Failed to load remote subset in persisted wrapper:`,
|
|
368
|
+
error
|
|
369
|
+
);
|
|
370
|
+
this.queueRemoteSubsetEnsure(options);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.warn(`Failed to trigger remote subset load:`, error);
|
|
375
|
+
this.queueRemoteSubsetEnsure(options);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
unloadSubset(options, upstreamUnloadSubset) {
|
|
380
|
+
this.activeSubsets.delete(this.getSubsetKey(options));
|
|
381
|
+
upstreamUnloadSubset?.(options);
|
|
382
|
+
}
|
|
383
|
+
async forceReloadSubset(options) {
|
|
384
|
+
this.activeSubsets.set(this.getSubsetKey(options), options);
|
|
385
|
+
await this.applyMutex.run(
|
|
386
|
+
() => this.hydrateSubsetUnsafe(options, { requestRemoteEnsure: false })
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
queueHydrationBufferedTransaction(transaction) {
|
|
390
|
+
this.queuedHydrationTransactions.push(transaction);
|
|
391
|
+
}
|
|
392
|
+
async persistAndBroadcastExternalSyncTransaction(transaction) {
|
|
393
|
+
await this.applyMutex.run(
|
|
394
|
+
() => this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
normalizeSyncWriteMessage(message) {
|
|
398
|
+
if (!this.collection) {
|
|
399
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
400
|
+
`collection must be attached before sync writes are processed`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (`key` in message) {
|
|
404
|
+
const key2 = message.key;
|
|
405
|
+
const previousValue = this.collection.get(key2) ?? {};
|
|
406
|
+
return {
|
|
407
|
+
forwardMessage: {
|
|
408
|
+
type: `delete`,
|
|
409
|
+
key: key2
|
|
410
|
+
},
|
|
411
|
+
operation: {
|
|
412
|
+
type: `delete`,
|
|
413
|
+
key: key2,
|
|
414
|
+
value: previousValue
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (message.type === `delete`) {
|
|
419
|
+
const key2 = this.collection.getKeyFromItem(message.value);
|
|
420
|
+
const previousValue = this.collection.get(key2) ?? message.value;
|
|
421
|
+
return {
|
|
422
|
+
forwardMessage: {
|
|
423
|
+
type: `delete`,
|
|
424
|
+
key: key2
|
|
425
|
+
},
|
|
426
|
+
operation: {
|
|
427
|
+
type: `delete`,
|
|
428
|
+
key: key2,
|
|
429
|
+
value: previousValue
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const key = this.collection.getKeyFromItem(message.value);
|
|
434
|
+
return {
|
|
435
|
+
forwardMessage: {
|
|
436
|
+
type: `update`,
|
|
437
|
+
value: message.value,
|
|
438
|
+
metadata: message.metadata
|
|
439
|
+
},
|
|
440
|
+
operation: {
|
|
441
|
+
type: `update`,
|
|
442
|
+
key,
|
|
443
|
+
value: message.value,
|
|
444
|
+
metadata: message.metadata
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async persistAndConfirmCollectionMutations(mutations) {
|
|
449
|
+
if (mutations.length === 0) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
await this.applyMutex.run(async () => {
|
|
453
|
+
const acceptedMutationIds = await this.persistCollectionMutationsUnsafe(mutations);
|
|
454
|
+
const acceptedMutationIdSet = new Set(acceptedMutationIds);
|
|
455
|
+
const acceptedMutations = mutations.filter(
|
|
456
|
+
(mutation) => acceptedMutationIdSet.has(mutation.mutationId)
|
|
457
|
+
);
|
|
458
|
+
if (acceptedMutations.length !== mutations.length) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`persistence coordinator accepted ${acceptedMutations.length} of ${mutations.length} mutations; partial acceptance is not supported`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
this.confirmMutationsSyncUnsafe(acceptedMutations);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
async acceptTransactionMutations(transaction) {
|
|
467
|
+
const collectionMutations = this.filterMutationsForCollection(
|
|
468
|
+
transaction.mutations
|
|
469
|
+
);
|
|
470
|
+
if (collectionMutations.length === 0) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
await this.persistAndConfirmCollectionMutations(collectionMutations);
|
|
474
|
+
}
|
|
475
|
+
cleanup() {
|
|
476
|
+
this.coordinatorUnsubscribe?.();
|
|
477
|
+
this.coordinatorUnsubscribe = null;
|
|
478
|
+
this.indexAddedUnsubscribe?.();
|
|
479
|
+
this.indexAddedUnsubscribe = null;
|
|
480
|
+
this.indexRemovedUnsubscribe?.();
|
|
481
|
+
this.indexRemovedUnsubscribe = null;
|
|
482
|
+
if (this.remoteEnsureRetryTimer !== null) {
|
|
483
|
+
clearTimeout(this.remoteEnsureRetryTimer);
|
|
484
|
+
this.remoteEnsureRetryTimer = null;
|
|
485
|
+
}
|
|
486
|
+
this.pendingRemoteSubsetEnsures.clear();
|
|
487
|
+
this.activeSubsets.clear();
|
|
488
|
+
this.queuedHydrationTransactions.length = 0;
|
|
489
|
+
this.queuedTxCommitted.length = 0;
|
|
490
|
+
this.clearSyncControls();
|
|
491
|
+
}
|
|
492
|
+
withInternalApply(task) {
|
|
493
|
+
this.internalApplyDepth++;
|
|
494
|
+
try {
|
|
495
|
+
task();
|
|
496
|
+
} finally {
|
|
497
|
+
this.internalApplyDepth--;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
getRequiredIndexSignatures() {
|
|
501
|
+
if (!this.collection) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
|
|
505
|
+
}
|
|
506
|
+
loadSubsetRowsUnsafe(options) {
|
|
507
|
+
return this.persistence.adapter.loadSubset(this.collectionId, options, {
|
|
508
|
+
requiredIndexSignatures: this.getRequiredIndexSignatures()
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async scanPersistedRowsUnsafe(options) {
|
|
512
|
+
if (!this.persistence.adapter.scanRows) {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
return this.persistence.adapter.scanRows(this.collectionId, options);
|
|
516
|
+
}
|
|
517
|
+
async scanPersistedRows(options) {
|
|
518
|
+
return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options));
|
|
519
|
+
}
|
|
520
|
+
async hydrateSubsetUnsafe(options, config) {
|
|
521
|
+
this.isHydrating = true;
|
|
522
|
+
try {
|
|
523
|
+
const rows = await this.loadSubsetRowsUnsafe(options);
|
|
524
|
+
this.applyRowsToCollection(rows);
|
|
525
|
+
} finally {
|
|
526
|
+
this.isHydrating = false;
|
|
527
|
+
}
|
|
528
|
+
await this.flushQueuedHydrationTransactionsUnsafe();
|
|
529
|
+
await this.flushQueuedTxCommittedUnsafe();
|
|
530
|
+
if (config.requestRemoteEnsure) {
|
|
531
|
+
this.queueRemoteSubsetEnsure(options);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
applyRowsToCollection(rows) {
|
|
535
|
+
if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
this.withInternalApply(() => {
|
|
539
|
+
this.syncControls.begin?.({ immediate: true });
|
|
540
|
+
for (const row of rows) {
|
|
541
|
+
this.syncControls.write?.({
|
|
542
|
+
type: `update`,
|
|
543
|
+
value: row.value,
|
|
544
|
+
metadata: row.metadata
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
this.syncControls.commit?.();
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
replaceCollectionSnapshot(rows, collectionMetadata) {
|
|
551
|
+
if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const nextMetadata = new Map(
|
|
555
|
+
collectionMetadata.map(({ key, value }) => [key, value])
|
|
556
|
+
);
|
|
557
|
+
const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
|
|
558
|
+
this.withInternalApply(() => {
|
|
559
|
+
this.syncControls.begin?.({ immediate: true });
|
|
560
|
+
this.syncControls.truncate?.();
|
|
561
|
+
for (const row of rows) {
|
|
562
|
+
this.syncControls.write?.({
|
|
563
|
+
type: `update`,
|
|
564
|
+
value: row.value,
|
|
565
|
+
metadata: row.metadata
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
currentKeys.forEach((key) => {
|
|
569
|
+
if (!nextMetadata.has(key)) {
|
|
570
|
+
this.syncControls.metadata?.collection.delete(key);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
nextMetadata.forEach((value, key) => {
|
|
574
|
+
this.syncControls.metadata?.collection.set(key, value);
|
|
575
|
+
});
|
|
576
|
+
this.syncControls.commit?.();
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async flushQueuedHydrationTransactionsUnsafe() {
|
|
580
|
+
while (this.queuedHydrationTransactions.length > 0) {
|
|
581
|
+
const transaction = this.queuedHydrationTransactions.shift();
|
|
582
|
+
if (!transaction) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
await this.applyBufferedSyncTransactionUnsafe(transaction);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async applyBufferedSyncTransactionUnsafe(transaction) {
|
|
589
|
+
if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const applyToCollection = () => {
|
|
593
|
+
this.syncControls.begin?.();
|
|
594
|
+
if (transaction.truncate) {
|
|
595
|
+
this.syncControls.truncate?.();
|
|
596
|
+
}
|
|
597
|
+
for (const operation of transaction.operations) {
|
|
598
|
+
if (operation.type === `delete`) {
|
|
599
|
+
this.syncControls.write?.({
|
|
600
|
+
type: `delete`,
|
|
601
|
+
key: operation.key
|
|
602
|
+
});
|
|
603
|
+
} else {
|
|
604
|
+
this.syncControls.write?.({
|
|
605
|
+
type: `update`,
|
|
606
|
+
value: operation.value,
|
|
607
|
+
metadata: operation.metadata
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
for (const [key, metadataWrite] of transaction.rowMetadataWrites) {
|
|
612
|
+
if (metadataWrite.type === `delete`) {
|
|
613
|
+
this.syncControls.metadata?.row.delete(key);
|
|
614
|
+
} else {
|
|
615
|
+
this.syncControls.metadata?.row.set(key, metadataWrite.value);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const [key, metadataWrite] of transaction.collectionMetadataWrites) {
|
|
619
|
+
if (metadataWrite.type === `delete`) {
|
|
620
|
+
this.syncControls.metadata?.collection.delete(key);
|
|
621
|
+
} else {
|
|
622
|
+
this.syncControls.metadata?.collection.set(key, metadataWrite.value);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
this.syncControls.commit?.();
|
|
626
|
+
};
|
|
627
|
+
if (transaction.internal) {
|
|
628
|
+
this.withInternalApply(applyToCollection);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
applyToCollection();
|
|
632
|
+
await this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction);
|
|
633
|
+
}
|
|
634
|
+
async persistAndBroadcastExternalSyncTransactionUnsafe(transaction) {
|
|
635
|
+
if (transaction.internal) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const streamPosition = this.nextLocalStreamPosition();
|
|
639
|
+
if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
|
|
640
|
+
this.publishTxCommittedEvent(
|
|
641
|
+
this.createTxCommittedPayload({
|
|
642
|
+
term: streamPosition.term,
|
|
643
|
+
seq: streamPosition.seq,
|
|
644
|
+
txId: crypto.randomUUID(),
|
|
645
|
+
latestRowVersion: streamPosition.rowVersion,
|
|
646
|
+
changedRows: [],
|
|
647
|
+
deletedKeys: [],
|
|
648
|
+
requiresFullReload: true
|
|
649
|
+
})
|
|
650
|
+
);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
|
|
654
|
+
await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
|
|
655
|
+
this.publishTxCommittedEvent(
|
|
656
|
+
this.createTxCommittedPayload({
|
|
657
|
+
term: tx.term,
|
|
658
|
+
seq: tx.seq,
|
|
659
|
+
txId: tx.txId,
|
|
660
|
+
latestRowVersion: tx.rowVersion,
|
|
661
|
+
requiresFullReload: transaction.truncate,
|
|
662
|
+
changedRows: transaction.operations.filter((operation) => operation.type === `update`).map((operation) => ({ key: operation.key, value: operation.value })),
|
|
663
|
+
deletedKeys: transaction.operations.filter((operation) => operation.type === `delete`).map((operation) => operation.key),
|
|
664
|
+
rowMetadataMutations: tx.rowMetadataMutations,
|
|
665
|
+
collectionMetadataMutations: tx.collectionMetadataMutations
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
createPersistedTxFromOperations(transaction, streamPosition) {
|
|
670
|
+
return {
|
|
671
|
+
txId: crypto.randomUUID(),
|
|
672
|
+
term: streamPosition.term,
|
|
673
|
+
seq: streamPosition.seq,
|
|
674
|
+
rowVersion: streamPosition.rowVersion,
|
|
675
|
+
truncate: transaction.truncate,
|
|
676
|
+
mutations: transaction.operations.map(
|
|
677
|
+
(operation) => operation.type === `update` ? {
|
|
678
|
+
type: `update`,
|
|
679
|
+
key: operation.key,
|
|
680
|
+
value: operation.value
|
|
681
|
+
} : {
|
|
682
|
+
type: `delete`,
|
|
683
|
+
key: operation.key,
|
|
684
|
+
value: operation.value
|
|
685
|
+
}
|
|
686
|
+
),
|
|
687
|
+
rowMetadataMutations: Array.from(
|
|
688
|
+
transaction.rowMetadataWrites.entries()
|
|
689
|
+
).map(
|
|
690
|
+
([key, metadataWrite]) => metadataWrite.type === `delete` ? { type: `delete`, key } : { type: `set`, key, value: metadataWrite.value }
|
|
691
|
+
),
|
|
692
|
+
collectionMetadataMutations: Array.from(
|
|
693
|
+
transaction.collectionMetadataWrites.entries()
|
|
694
|
+
).map(
|
|
695
|
+
([key, metadataWrite]) => metadataWrite.type === `delete` ? { type: `delete`, key } : { type: `set`, key, value: metadataWrite.value }
|
|
696
|
+
)
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
createPersistedTxFromMutations(mutations, streamPosition) {
|
|
700
|
+
return {
|
|
701
|
+
txId: crypto.randomUUID(),
|
|
702
|
+
term: streamPosition.term,
|
|
703
|
+
seq: streamPosition.seq,
|
|
704
|
+
rowVersion: streamPosition.rowVersion,
|
|
705
|
+
mutations: mutations.map((mutation) => {
|
|
706
|
+
if (mutation.type === `delete`) {
|
|
707
|
+
return {
|
|
708
|
+
type: `delete`,
|
|
709
|
+
key: mutation.key,
|
|
710
|
+
value: mutation.original
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (mutation.type === `insert`) {
|
|
714
|
+
return {
|
|
715
|
+
type: `insert`,
|
|
716
|
+
key: mutation.key,
|
|
717
|
+
value: mutation.modified
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
type: `update`,
|
|
722
|
+
key: mutation.key,
|
|
723
|
+
value: mutation.modified
|
|
724
|
+
};
|
|
725
|
+
})
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
confirmMutationsSyncUnsafe(mutations) {
|
|
729
|
+
if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
this.withInternalApply(() => {
|
|
733
|
+
this.syncControls.begin?.({ immediate: true });
|
|
734
|
+
for (const mutation of mutations) {
|
|
735
|
+
if (mutation.type === `delete`) {
|
|
736
|
+
this.syncControls.write?.({
|
|
737
|
+
type: `delete`,
|
|
738
|
+
key: mutation.key
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
this.syncControls.write?.({
|
|
742
|
+
type: `update`,
|
|
743
|
+
value: mutation.modified
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
this.syncControls.commit?.();
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
filterMutationsForCollection(mutations) {
|
|
751
|
+
const collection = this.collection;
|
|
752
|
+
return mutations.filter((mutation) => {
|
|
753
|
+
if (collection) {
|
|
754
|
+
return mutation.collection === collection;
|
|
755
|
+
}
|
|
756
|
+
return mutation.collection.id === this.collectionId;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
async persistCollectionMutationsUnsafe(mutations) {
|
|
760
|
+
if (this.persistence.coordinator.requestApplyLocalMutations) {
|
|
761
|
+
const envelopeMutations = mutations.map(
|
|
762
|
+
(mutation) => toPersistedMutationEnvelope(
|
|
763
|
+
mutation
|
|
764
|
+
)
|
|
765
|
+
);
|
|
766
|
+
const response = await this.persistence.coordinator.requestApplyLocalMutations(
|
|
767
|
+
this.collectionId,
|
|
768
|
+
envelopeMutations
|
|
769
|
+
);
|
|
770
|
+
if (!response.ok) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`failed to apply local mutations through coordinator: ${response.error}`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
this.observeStreamPosition(
|
|
776
|
+
response.term,
|
|
777
|
+
response.seq,
|
|
778
|
+
response.latestRowVersion
|
|
779
|
+
);
|
|
780
|
+
const uniqueAcceptedMutationIds = Array.from(
|
|
781
|
+
new Set(response.acceptedMutationIds)
|
|
782
|
+
);
|
|
783
|
+
const submittedMutationIds = new Set(
|
|
784
|
+
mutations.map((mutation) => mutation.mutationId)
|
|
785
|
+
);
|
|
786
|
+
const hasUnknownAcceptedMutationId = uniqueAcceptedMutationIds.some(
|
|
787
|
+
(mutationId) => !submittedMutationIds.has(mutationId)
|
|
788
|
+
);
|
|
789
|
+
if (hasUnknownAcceptedMutationId) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`persistence coordinator returned unknown mutation ids in applyLocalMutations response`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
return uniqueAcceptedMutationIds;
|
|
795
|
+
}
|
|
796
|
+
const streamPosition = this.nextLocalStreamPosition();
|
|
797
|
+
const tx = this.createPersistedTxFromMutations(mutations, streamPosition);
|
|
798
|
+
await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
|
|
799
|
+
this.publishTxCommittedEvent(
|
|
800
|
+
this.createTxCommittedPayload({
|
|
801
|
+
term: tx.term,
|
|
802
|
+
seq: tx.seq,
|
|
803
|
+
txId: tx.txId,
|
|
804
|
+
latestRowVersion: tx.rowVersion,
|
|
805
|
+
changedRows: mutations.filter((mutation) => mutation.type !== `delete`).map((mutation) => ({
|
|
806
|
+
key: mutation.key,
|
|
807
|
+
value: mutation.modified
|
|
808
|
+
})),
|
|
809
|
+
deletedKeys: mutations.filter((mutation) => mutation.type === `delete`).map((mutation) => mutation.key),
|
|
810
|
+
rowMetadataMutations: tx.rowMetadataMutations,
|
|
811
|
+
collectionMetadataMutations: tx.collectionMetadataMutations
|
|
812
|
+
})
|
|
813
|
+
);
|
|
814
|
+
return mutations.map((mutation) => mutation.mutationId);
|
|
815
|
+
}
|
|
816
|
+
createTxCommittedPayload(args) {
|
|
817
|
+
const rowMetadataMutations = args.rowMetadataMutations ?? [];
|
|
818
|
+
const collectionMetadataMutations = args.collectionMetadataMutations ?? [];
|
|
819
|
+
const requiresFullReload = args.requiresFullReload === true || args.changedRows.length + args.deletedKeys.length + rowMetadataMutations.length + collectionMetadataMutations.length > TARGETED_INVALIDATION_KEY_LIMIT;
|
|
820
|
+
if (requiresFullReload) {
|
|
821
|
+
return {
|
|
822
|
+
type: `tx:committed`,
|
|
823
|
+
term: args.term,
|
|
824
|
+
seq: args.seq,
|
|
825
|
+
txId: args.txId,
|
|
826
|
+
latestRowVersion: args.latestRowVersion,
|
|
827
|
+
requiresFullReload: true
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
type: `tx:committed`,
|
|
832
|
+
term: args.term,
|
|
833
|
+
seq: args.seq,
|
|
834
|
+
txId: args.txId,
|
|
835
|
+
latestRowVersion: args.latestRowVersion,
|
|
836
|
+
requiresFullReload: false,
|
|
837
|
+
changedRows: args.changedRows,
|
|
838
|
+
deletedKeys: args.deletedKeys,
|
|
839
|
+
rowMetadataMutations,
|
|
840
|
+
collectionMetadataMutations
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
publishTxCommittedEvent(txCommitted) {
|
|
844
|
+
this.observeStreamPosition(
|
|
845
|
+
txCommitted.term,
|
|
846
|
+
txCommitted.seq,
|
|
847
|
+
txCommitted.latestRowVersion
|
|
848
|
+
);
|
|
849
|
+
const envelope = {
|
|
850
|
+
v: 1,
|
|
851
|
+
dbName: this.dbName,
|
|
852
|
+
collectionId: this.collectionId,
|
|
853
|
+
senderId: this.persistence.coordinator.getNodeId(),
|
|
854
|
+
ts: Date.now(),
|
|
855
|
+
payload: txCommitted
|
|
856
|
+
};
|
|
857
|
+
this.persistence.coordinator.publish(this.collectionId, envelope);
|
|
858
|
+
}
|
|
859
|
+
observeStreamPosition(term, seq, rowVersion) {
|
|
860
|
+
if (term > this.latestTerm || term === this.latestTerm && seq > this.latestSeq) {
|
|
861
|
+
this.latestTerm = term;
|
|
862
|
+
this.latestSeq = seq;
|
|
863
|
+
}
|
|
864
|
+
if (rowVersion > this.latestRowVersion) {
|
|
865
|
+
this.latestRowVersion = rowVersion;
|
|
866
|
+
}
|
|
867
|
+
if (term > this.localTerm) {
|
|
868
|
+
this.localTerm = term;
|
|
869
|
+
this.localSeq = seq;
|
|
870
|
+
} else if (term === this.localTerm && seq > this.localSeq) {
|
|
871
|
+
this.localSeq = seq;
|
|
872
|
+
}
|
|
873
|
+
if (rowVersion > this.localRowVersion) {
|
|
874
|
+
this.localRowVersion = rowVersion;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
nextLocalStreamPosition() {
|
|
878
|
+
this.localTerm = Math.max(this.localTerm, this.latestTerm || 1);
|
|
879
|
+
this.localSeq = Math.max(this.localSeq, this.latestSeq) + 1;
|
|
880
|
+
this.localRowVersion = Math.max(this.localRowVersion, this.latestRowVersion) + 1;
|
|
881
|
+
return {
|
|
882
|
+
term: this.localTerm,
|
|
883
|
+
seq: this.localSeq,
|
|
884
|
+
rowVersion: this.localRowVersion
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
getSubsetKey(options) {
|
|
888
|
+
const subscription = options.subscription;
|
|
889
|
+
if (subscription && typeof subscription === `object`) {
|
|
890
|
+
const existingId = this.subscriptionIds.get(subscription);
|
|
891
|
+
if (existingId) {
|
|
892
|
+
return existingId;
|
|
893
|
+
}
|
|
894
|
+
this.nextSubscriptionId++;
|
|
895
|
+
const id = `sub:${this.nextSubscriptionId}`;
|
|
896
|
+
this.subscriptionIds.set(subscription, id);
|
|
897
|
+
return id;
|
|
898
|
+
}
|
|
899
|
+
return `opts:${stableSerialize(normalizeSubsetOptionsForKey(options))}`;
|
|
900
|
+
}
|
|
901
|
+
queueRemoteSubsetEnsure(options) {
|
|
902
|
+
if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.pendingRemoteSubsetEnsures.set(this.getSubsetKey(options), options);
|
|
906
|
+
void this.flushPendingRemoteSubsetEnsures();
|
|
907
|
+
}
|
|
908
|
+
scheduleRemoteEnsureRetry() {
|
|
909
|
+
if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (this.pendingRemoteSubsetEnsures.size === 0 || this.remoteEnsureRetryTimer !== null) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
this.remoteEnsureRetryTimer = setTimeout(() => {
|
|
916
|
+
this.remoteEnsureRetryTimer = null;
|
|
917
|
+
void this.flushPendingRemoteSubsetEnsures();
|
|
918
|
+
}, REMOTE_ENSURE_RETRY_DELAY_MS);
|
|
919
|
+
}
|
|
920
|
+
async flushPendingRemoteSubsetEnsures() {
|
|
921
|
+
if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (this.remoteEnsureRetryTimer !== null) {
|
|
925
|
+
clearTimeout(this.remoteEnsureRetryTimer);
|
|
926
|
+
this.remoteEnsureRetryTimer = null;
|
|
927
|
+
}
|
|
928
|
+
for (const [subsetKey, options] of this.pendingRemoteSubsetEnsures) {
|
|
929
|
+
try {
|
|
930
|
+
await this.persistence.coordinator.requestEnsureRemoteSubset(
|
|
931
|
+
this.collectionId,
|
|
932
|
+
options
|
|
933
|
+
);
|
|
934
|
+
this.pendingRemoteSubsetEnsures.delete(subsetKey);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
console.warn(`Failed to ensure remote subset:`, error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
this.scheduleRemoteEnsureRetry();
|
|
940
|
+
}
|
|
941
|
+
attachCoordinatorSubscription() {
|
|
942
|
+
if (this.coordinatorUnsubscribe) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
|
|
946
|
+
this.collectionId,
|
|
947
|
+
(message) => {
|
|
948
|
+
this.onCoordinatorMessage(message);
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
onCoordinatorMessage(message) {
|
|
953
|
+
if (message.collectionId !== this.collectionId) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const { payload } = message;
|
|
957
|
+
const isSelf = message.senderId === this.persistence.coordinator.getNodeId();
|
|
958
|
+
if (isTxCommittedPayload(payload)) {
|
|
959
|
+
if (this.isHydrating) {
|
|
960
|
+
this.queuedTxCommitted.push(payload);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload)).catch((error) => {
|
|
964
|
+
console.warn(`Failed to process tx:committed message:`, error);
|
|
965
|
+
});
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (isSelf) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (isCollectionResetPayload(payload)) {
|
|
972
|
+
void this.applyMutex.run(() => this.truncateAndReloadUnsafe()).catch((error) => {
|
|
973
|
+
console.warn(`Failed to process collection reset message:`, error);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async flushQueuedTxCommittedUnsafe() {
|
|
978
|
+
while (this.queuedTxCommitted.length > 0) {
|
|
979
|
+
const queued = this.queuedTxCommitted.shift();
|
|
980
|
+
if (!queued) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
await this.processCommittedTxUnsafe(queued);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async processCommittedTxUnsafe(txCommitted) {
|
|
987
|
+
if (txCommitted.term < this.latestTerm) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const hasGapInCurrentTerm = txCommitted.term === this.latestTerm && txCommitted.seq > this.latestSeq + 1;
|
|
994
|
+
const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
|
|
995
|
+
const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
|
|
996
|
+
if (hasGap) {
|
|
997
|
+
await this.recoverFromSeqGapUnsafe();
|
|
998
|
+
if (txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
this.observeStreamPosition(
|
|
1003
|
+
txCommitted.term,
|
|
1004
|
+
txCommitted.seq,
|
|
1005
|
+
txCommitted.latestRowVersion
|
|
1006
|
+
);
|
|
1007
|
+
await this.invalidateFromCommittedTxUnsafe(txCommitted);
|
|
1008
|
+
}
|
|
1009
|
+
async recoverFromSeqGapUnsafe() {
|
|
1010
|
+
if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
|
|
1011
|
+
try {
|
|
1012
|
+
const pullResponse = await this.persistence.coordinator.pullSince(
|
|
1013
|
+
this.collectionId,
|
|
1014
|
+
this.latestRowVersion
|
|
1015
|
+
);
|
|
1016
|
+
if (pullResponse.ok) {
|
|
1017
|
+
this.observeStreamPosition(
|
|
1018
|
+
pullResponse.latestTerm,
|
|
1019
|
+
pullResponse.latestSeq,
|
|
1020
|
+
pullResponse.latestRowVersion
|
|
1021
|
+
);
|
|
1022
|
+
if (pullResponse.requiresFullReload || !pullResponse.deltas) {
|
|
1023
|
+
await this.reloadActiveSubsetsUnsafe();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
for (const delta of pullResponse.deltas) {
|
|
1027
|
+
await this.invalidateFromCommittedTxUnsafe({
|
|
1028
|
+
type: `tx:committed`,
|
|
1029
|
+
term: pullResponse.latestTerm,
|
|
1030
|
+
seq: pullResponse.latestSeq,
|
|
1031
|
+
txId: delta.txId,
|
|
1032
|
+
latestRowVersion: delta.latestRowVersion,
|
|
1033
|
+
requiresFullReload: false,
|
|
1034
|
+
changedRows: delta.changedRows,
|
|
1035
|
+
deletedKeys: delta.deletedKeys,
|
|
1036
|
+
rowMetadataMutations: delta.rowMetadataMutations,
|
|
1037
|
+
collectionMetadataMutations: delta.collectionMetadataMutations
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
console.warn(`Failed pullSince recovery attempt:`, error);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
await this.truncateAndReloadUnsafe();
|
|
1047
|
+
if (this.mode === `sync-present`) {
|
|
1048
|
+
for (const options of this.activeSubsets.values()) {
|
|
1049
|
+
this.queueRemoteSubsetEnsure(options);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async truncateAndReloadUnsafe() {
|
|
1054
|
+
if (this.syncControls.begin && this.syncControls.commit) {
|
|
1055
|
+
this.withInternalApply(() => {
|
|
1056
|
+
this.syncControls.begin?.({ immediate: true });
|
|
1057
|
+
this.syncControls.truncate?.();
|
|
1058
|
+
this.syncControls.commit?.();
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
await this.reloadActiveSubsetsUnsafe();
|
|
1062
|
+
}
|
|
1063
|
+
async invalidateFromCommittedTxUnsafe(txCommitted) {
|
|
1064
|
+
if (txCommitted.requiresFullReload) {
|
|
1065
|
+
await this.reloadActiveSubsetsUnsafe();
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
|
|
1069
|
+
if (changedKeyCount > TARGETED_INVALIDATION_KEY_LIMIT) {
|
|
1070
|
+
await this.reloadActiveSubsetsUnsafe();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
|
|
1074
|
+
(opt) => opt.limit != null || opt.offset != null || opt.cursor != null
|
|
1075
|
+
);
|
|
1076
|
+
if (!hasPaginatedSubset || changedKeyCount === 0) {
|
|
1077
|
+
await this.applyTargetedInvalidationUnsafe(txCommitted);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
await this.reloadActiveSubsetsUnsafe();
|
|
1081
|
+
}
|
|
1082
|
+
async applyTargetedInvalidationUnsafe(txCommitted) {
|
|
1083
|
+
const subsetEvaluators = Array.from(this.activeSubsets.values()).map(
|
|
1084
|
+
(opt) => opt.where ? db.compileSingleRowExpression(opt.where) : null
|
|
1085
|
+
);
|
|
1086
|
+
this.withInternalApply(() => {
|
|
1087
|
+
this.syncControls.begin?.({ immediate: true });
|
|
1088
|
+
for (const {
|
|
1089
|
+
key: changedKey,
|
|
1090
|
+
value: newValue
|
|
1091
|
+
} of txCommitted.changedRows) {
|
|
1092
|
+
const matchesAnySubset = subsetEvaluators.some((evaluator) => {
|
|
1093
|
+
if (!evaluator) return true;
|
|
1094
|
+
return db.toBooleanPredicate(evaluator(newValue));
|
|
1095
|
+
});
|
|
1096
|
+
if (matchesAnySubset) {
|
|
1097
|
+
this.syncControls.write?.({ type: `update`, value: newValue });
|
|
1098
|
+
} else if (this.collection?.get(changedKey) !== void 0) {
|
|
1099
|
+
this.syncControls.write?.({ type: `delete`, key: changedKey });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
for (const deletedKey of txCommitted.deletedKeys) {
|
|
1103
|
+
this.syncControls.write?.({ type: `delete`, key: deletedKey });
|
|
1104
|
+
}
|
|
1105
|
+
txCommitted.rowMetadataMutations?.forEach((mutation) => {
|
|
1106
|
+
if (mutation.type === `delete`) {
|
|
1107
|
+
this.syncControls.metadata?.row.delete(mutation.key);
|
|
1108
|
+
} else {
|
|
1109
|
+
this.syncControls.metadata?.row.set(
|
|
1110
|
+
mutation.key,
|
|
1111
|
+
mutation.value
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
txCommitted.collectionMetadataMutations?.forEach((mutation) => {
|
|
1116
|
+
if (mutation.type === `delete`) {
|
|
1117
|
+
this.syncControls.metadata?.collection.delete(mutation.key);
|
|
1118
|
+
} else {
|
|
1119
|
+
this.syncControls.metadata?.collection.set(
|
|
1120
|
+
mutation.key,
|
|
1121
|
+
mutation.value
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
this.syncControls.commit?.();
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
async reloadActiveSubsetsUnsafe() {
|
|
1129
|
+
const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
|
|
1130
|
+
this.isHydrating = true;
|
|
1131
|
+
try {
|
|
1132
|
+
const mergedRows = /* @__PURE__ */ new Map();
|
|
1133
|
+
const collectionMetadata = await this.loadCollectionMetadataSnapshot();
|
|
1134
|
+
for (const options of activeSubsetOptions) {
|
|
1135
|
+
const subsetRows = await this.loadSubsetRowsUnsafe(options);
|
|
1136
|
+
for (const row of subsetRows) {
|
|
1137
|
+
mergedRows.set(row.key, {
|
|
1138
|
+
value: row.value,
|
|
1139
|
+
metadata: row.metadata
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
this.replaceCollectionSnapshot(
|
|
1144
|
+
Array.from(mergedRows.entries()).map(([key, row]) => ({
|
|
1145
|
+
key,
|
|
1146
|
+
value: row.value,
|
|
1147
|
+
metadata: row.metadata
|
|
1148
|
+
})),
|
|
1149
|
+
collectionMetadata
|
|
1150
|
+
);
|
|
1151
|
+
} finally {
|
|
1152
|
+
this.isHydrating = false;
|
|
1153
|
+
}
|
|
1154
|
+
await this.flushQueuedHydrationTransactionsUnsafe();
|
|
1155
|
+
await this.flushQueuedTxCommittedUnsafe();
|
|
1156
|
+
}
|
|
1157
|
+
attachIndexLifecycleListeners() {
|
|
1158
|
+
if (!this.collection || this.indexAddedUnsubscribe || this.indexRemovedUnsubscribe) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
this.indexAddedUnsubscribe = this.collection.on(`index:added`, (event) => {
|
|
1162
|
+
void this.ensurePersistedIndex(event.index);
|
|
1163
|
+
});
|
|
1164
|
+
this.indexRemovedUnsubscribe = this.collection.on(
|
|
1165
|
+
`index:removed`,
|
|
1166
|
+
(event) => {
|
|
1167
|
+
void this.markIndexRemoved(event.index);
|
|
1168
|
+
}
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
async bootstrapPersistedIndexes(indexMetadataSnapshot) {
|
|
1172
|
+
const collection = this.collection;
|
|
1173
|
+
if (!collection && !indexMetadataSnapshot) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
|
|
1177
|
+
for (const metadata of indexMetadata) {
|
|
1178
|
+
await this.ensurePersistedIndex(metadata);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
buildPersistedIndexSpec(index) {
|
|
1182
|
+
return {
|
|
1183
|
+
expressionSql: [stableSerialize(index.expression)],
|
|
1184
|
+
metadata: {
|
|
1185
|
+
name: index.name ?? null,
|
|
1186
|
+
resolver: toStableSerializable(index.resolver),
|
|
1187
|
+
options: toStableSerializable(index.options)
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
async ensurePersistedIndex(indexMetadata) {
|
|
1192
|
+
const spec = this.buildPersistedIndexSpec(indexMetadata);
|
|
1193
|
+
try {
|
|
1194
|
+
await this.persistence.adapter.ensureIndex(
|
|
1195
|
+
this.collectionId,
|
|
1196
|
+
indexMetadata.signature,
|
|
1197
|
+
spec
|
|
1198
|
+
);
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
console.warn(`Failed to ensure persisted index in adapter:`, error);
|
|
1201
|
+
}
|
|
1202
|
+
try {
|
|
1203
|
+
await this.persistence.coordinator.requestEnsurePersistedIndex(
|
|
1204
|
+
this.collectionId,
|
|
1205
|
+
indexMetadata.signature,
|
|
1206
|
+
spec
|
|
1207
|
+
);
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
console.warn(
|
|
1210
|
+
`Failed to ensure persisted index through coordinator:`,
|
|
1211
|
+
error
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async markIndexRemoved(indexMetadata) {
|
|
1216
|
+
if (!this.persistence.adapter.markIndexRemoved) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
await this.persistence.adapter.markIndexRemoved(
|
|
1221
|
+
this.collectionId,
|
|
1222
|
+
indexMetadata.signature
|
|
1223
|
+
);
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
console.warn(`Failed to mark persisted index removed:`, error);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function createWrappedSyncConfig(sourceSyncConfig, runtime) {
|
|
1230
|
+
return {
|
|
1231
|
+
...sourceSyncConfig,
|
|
1232
|
+
sync: (params) => {
|
|
1233
|
+
const transactionStack = [];
|
|
1234
|
+
const getOpenTransaction = () => transactionStack[transactionStack.length - 1];
|
|
1235
|
+
let fullStartPromise = null;
|
|
1236
|
+
const cancelledLoadKeys = /* @__PURE__ */ new Set();
|
|
1237
|
+
const loadSubscriptionIds = /* @__PURE__ */ new WeakMap();
|
|
1238
|
+
let nextLoadSubscriptionId = 0;
|
|
1239
|
+
const getLoadKey = (options) => {
|
|
1240
|
+
const subscription = options.subscription;
|
|
1241
|
+
if (subscription && typeof subscription === `object`) {
|
|
1242
|
+
const existingId = loadSubscriptionIds.get(subscription);
|
|
1243
|
+
if (existingId) {
|
|
1244
|
+
return `sub:${existingId}`;
|
|
1245
|
+
}
|
|
1246
|
+
nextLoadSubscriptionId++;
|
|
1247
|
+
const nextId = String(nextLoadSubscriptionId);
|
|
1248
|
+
loadSubscriptionIds.set(subscription, nextId);
|
|
1249
|
+
return `sub:${nextId}`;
|
|
1250
|
+
}
|
|
1251
|
+
return `opts:${stableSerialize(normalizeSubsetOptionsForKey(options))}`;
|
|
1252
|
+
};
|
|
1253
|
+
runtime.setSyncControls({
|
|
1254
|
+
begin: params.begin,
|
|
1255
|
+
write: params.write,
|
|
1256
|
+
commit: params.commit,
|
|
1257
|
+
truncate: params.truncate,
|
|
1258
|
+
metadata: params.metadata ?? null
|
|
1259
|
+
});
|
|
1260
|
+
runtime.setCollection(
|
|
1261
|
+
params.collection
|
|
1262
|
+
);
|
|
1263
|
+
const wrappedParams = {
|
|
1264
|
+
...params,
|
|
1265
|
+
markReady: () => {
|
|
1266
|
+
void (fullStartPromise ?? runtime.ensureStarted()).then(() => {
|
|
1267
|
+
params.markReady();
|
|
1268
|
+
}).catch((error) => {
|
|
1269
|
+
console.warn(
|
|
1270
|
+
`Failed persisted sync startup before markReady:`,
|
|
1271
|
+
error
|
|
1272
|
+
);
|
|
1273
|
+
params.markReady();
|
|
1274
|
+
});
|
|
1275
|
+
},
|
|
1276
|
+
begin: (options) => {
|
|
1277
|
+
const transaction = {
|
|
1278
|
+
operations: [],
|
|
1279
|
+
rowMetadataWrites: /* @__PURE__ */ new Map(),
|
|
1280
|
+
collectionMetadataWrites: /* @__PURE__ */ new Map(),
|
|
1281
|
+
truncate: false,
|
|
1282
|
+
internal: runtime.isApplyingInternally(),
|
|
1283
|
+
queuedBecauseHydrating: !runtime.isApplyingInternally() && runtime.isHydratingNow()
|
|
1284
|
+
};
|
|
1285
|
+
transactionStack.push(transaction);
|
|
1286
|
+
if (!transaction.queuedBecauseHydrating) {
|
|
1287
|
+
params.begin(options);
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
write: (message) => {
|
|
1291
|
+
const normalization = runtime.normalizeSyncWriteMessage(message);
|
|
1292
|
+
const openTransaction = getOpenTransaction();
|
|
1293
|
+
if (!openTransaction) {
|
|
1294
|
+
params.write(normalization.forwardMessage);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
openTransaction.operations.push(normalization.operation);
|
|
1298
|
+
if (normalization.operation.type === `delete`) {
|
|
1299
|
+
openTransaction.rowMetadataWrites.set(normalization.operation.key, {
|
|
1300
|
+
type: `delete`
|
|
1301
|
+
});
|
|
1302
|
+
} else if (message.type === `insert` && normalization.operation.metadata === void 0) {
|
|
1303
|
+
openTransaction.rowMetadataWrites.set(normalization.operation.key, {
|
|
1304
|
+
type: `delete`
|
|
1305
|
+
});
|
|
1306
|
+
} else if (normalization.operation.metadata !== void 0) {
|
|
1307
|
+
openTransaction.rowMetadataWrites.set(normalization.operation.key, {
|
|
1308
|
+
type: `set`,
|
|
1309
|
+
value: normalization.operation.metadata
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
if (!openTransaction.queuedBecauseHydrating) {
|
|
1313
|
+
params.write(normalization.forwardMessage);
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
metadata: params.metadata ? {
|
|
1317
|
+
row: {
|
|
1318
|
+
get: (key) => {
|
|
1319
|
+
const openTransaction = getOpenTransaction();
|
|
1320
|
+
const pendingWrite = openTransaction?.rowMetadataWrites.get(key);
|
|
1321
|
+
if (pendingWrite) {
|
|
1322
|
+
return pendingWrite.type === `delete` ? void 0 : pendingWrite.value;
|
|
1323
|
+
}
|
|
1324
|
+
if (openTransaction?.truncate) {
|
|
1325
|
+
return void 0;
|
|
1326
|
+
}
|
|
1327
|
+
return params.metadata.row.get(key);
|
|
1328
|
+
},
|
|
1329
|
+
scanPersisted: (options) => runtime.scanPersistedRows(options),
|
|
1330
|
+
set: (key, value) => {
|
|
1331
|
+
const openTransaction = getOpenTransaction();
|
|
1332
|
+
if (!openTransaction) {
|
|
1333
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
1334
|
+
`metadata.row.set must be called within an open sync transaction`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
openTransaction.rowMetadataWrites.set(key, {
|
|
1338
|
+
type: `set`,
|
|
1339
|
+
value
|
|
1340
|
+
});
|
|
1341
|
+
if (!openTransaction.queuedBecauseHydrating) {
|
|
1342
|
+
params.metadata.row.set(key, value);
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
delete: (key) => {
|
|
1346
|
+
const openTransaction = getOpenTransaction();
|
|
1347
|
+
if (!openTransaction) {
|
|
1348
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
1349
|
+
`metadata.row.delete must be called within an open sync transaction`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
openTransaction.rowMetadataWrites.set(key, {
|
|
1353
|
+
type: `delete`
|
|
1354
|
+
});
|
|
1355
|
+
if (!openTransaction.queuedBecauseHydrating) {
|
|
1356
|
+
params.metadata.row.delete(key);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
collection: {
|
|
1361
|
+
get: (key) => {
|
|
1362
|
+
const openTransaction = getOpenTransaction();
|
|
1363
|
+
const pendingWrite = openTransaction?.collectionMetadataWrites.get(key);
|
|
1364
|
+
if (pendingWrite) {
|
|
1365
|
+
return pendingWrite.type === `delete` ? void 0 : pendingWrite.value;
|
|
1366
|
+
}
|
|
1367
|
+
return params.metadata.collection.get(key);
|
|
1368
|
+
},
|
|
1369
|
+
set: (key, value) => {
|
|
1370
|
+
const openTransaction = getOpenTransaction();
|
|
1371
|
+
if (!openTransaction) {
|
|
1372
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
1373
|
+
`metadata.collection.set must be called within an open sync transaction`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
openTransaction.collectionMetadataWrites.set(key, {
|
|
1377
|
+
type: `set`,
|
|
1378
|
+
value
|
|
1379
|
+
});
|
|
1380
|
+
if (!openTransaction.queuedBecauseHydrating) {
|
|
1381
|
+
params.metadata.collection.set(key, value);
|
|
1382
|
+
}
|
|
1383
|
+
},
|
|
1384
|
+
delete: (key) => {
|
|
1385
|
+
const openTransaction = getOpenTransaction();
|
|
1386
|
+
if (!openTransaction) {
|
|
1387
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
1388
|
+
`metadata.collection.delete must be called within an open sync transaction`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
openTransaction.collectionMetadataWrites.set(key, {
|
|
1392
|
+
type: `delete`
|
|
1393
|
+
});
|
|
1394
|
+
if (!openTransaction.queuedBecauseHydrating) {
|
|
1395
|
+
params.metadata.collection.delete(key);
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
list: (prefix) => {
|
|
1399
|
+
const merged = new Map(
|
|
1400
|
+
params.metadata.collection.list().map(({ key, value }) => [key, value])
|
|
1401
|
+
);
|
|
1402
|
+
const openTransaction = getOpenTransaction();
|
|
1403
|
+
if (openTransaction) {
|
|
1404
|
+
for (const [
|
|
1405
|
+
key,
|
|
1406
|
+
metadataWrite
|
|
1407
|
+
] of openTransaction.collectionMetadataWrites) {
|
|
1408
|
+
if (metadataWrite.type === `delete`) {
|
|
1409
|
+
merged.delete(key);
|
|
1410
|
+
} else {
|
|
1411
|
+
merged.set(key, metadataWrite.value);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return Array.from(merged.entries()).filter(([key]) => prefix ? key.startsWith(prefix) : true).map(([key, value]) => ({
|
|
1416
|
+
key,
|
|
1417
|
+
value
|
|
1418
|
+
}));
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
} : void 0,
|
|
1422
|
+
truncate: () => {
|
|
1423
|
+
const openTransaction = getOpenTransaction();
|
|
1424
|
+
if (!openTransaction) {
|
|
1425
|
+
params.truncate();
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
openTransaction.operations = [];
|
|
1429
|
+
openTransaction.rowMetadataWrites.clear();
|
|
1430
|
+
openTransaction.truncate = true;
|
|
1431
|
+
if (!openTransaction.queuedBecauseHydrating) {
|
|
1432
|
+
params.truncate();
|
|
1433
|
+
}
|
|
1434
|
+
},
|
|
1435
|
+
commit: () => {
|
|
1436
|
+
const openTransaction = transactionStack.pop();
|
|
1437
|
+
if (!openTransaction) {
|
|
1438
|
+
params.commit();
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (openTransaction.queuedBecauseHydrating) {
|
|
1442
|
+
runtime.queueHydrationBufferedTransaction({
|
|
1443
|
+
operations: openTransaction.operations,
|
|
1444
|
+
rowMetadataWrites: openTransaction.rowMetadataWrites,
|
|
1445
|
+
collectionMetadataWrites: openTransaction.collectionMetadataWrites,
|
|
1446
|
+
truncate: openTransaction.truncate,
|
|
1447
|
+
internal: openTransaction.internal
|
|
1448
|
+
});
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
params.commit();
|
|
1452
|
+
if (!openTransaction.internal) {
|
|
1453
|
+
void runtime.persistAndBroadcastExternalSyncTransaction({
|
|
1454
|
+
operations: openTransaction.operations,
|
|
1455
|
+
rowMetadataWrites: openTransaction.rowMetadataWrites,
|
|
1456
|
+
collectionMetadataWrites: openTransaction.collectionMetadataWrites,
|
|
1457
|
+
truncate: openTransaction.truncate,
|
|
1458
|
+
internal: false
|
|
1459
|
+
}).catch((error) => {
|
|
1460
|
+
console.warn(
|
|
1461
|
+
`Failed to persist wrapped sync transaction:`,
|
|
1462
|
+
error
|
|
1463
|
+
);
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
let sourceResult = {};
|
|
1469
|
+
const startupState = { cleanedUp: false };
|
|
1470
|
+
fullStartPromise = runtime.ensureStarted();
|
|
1471
|
+
const sourceResultPromise = (async () => {
|
|
1472
|
+
await runtime.ensureStartupMetadataLoaded();
|
|
1473
|
+
if (startupState.cleanedUp) {
|
|
1474
|
+
return sourceResult;
|
|
1475
|
+
}
|
|
1476
|
+
sourceResult = normalizeSyncFnResult(
|
|
1477
|
+
sourceSyncConfig.sync(wrappedParams)
|
|
1478
|
+
);
|
|
1479
|
+
return sourceResult;
|
|
1480
|
+
})();
|
|
1481
|
+
return {
|
|
1482
|
+
cleanup: () => {
|
|
1483
|
+
startupState.cleanedUp = true;
|
|
1484
|
+
sourceResult.cleanup?.();
|
|
1485
|
+
runtime.cleanup();
|
|
1486
|
+
runtime.clearSyncControls();
|
|
1487
|
+
},
|
|
1488
|
+
loadSubset: async (options) => {
|
|
1489
|
+
const loadKey = getLoadKey(options);
|
|
1490
|
+
cancelledLoadKeys.delete(loadKey);
|
|
1491
|
+
await fullStartPromise;
|
|
1492
|
+
const resolvedSourceResult = await sourceResultPromise;
|
|
1493
|
+
if (startupState.cleanedUp || cancelledLoadKeys.has(loadKey)) {
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
await runtime.loadSubset(options, resolvedSourceResult.loadSubset);
|
|
1497
|
+
},
|
|
1498
|
+
unloadSubset: (options) => {
|
|
1499
|
+
cancelledLoadKeys.add(getLoadKey(options));
|
|
1500
|
+
runtime.unloadSubset(options, sourceResult.unloadSubset);
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function createLoopbackSyncConfig(runtime) {
|
|
1507
|
+
return {
|
|
1508
|
+
sync: (params) => {
|
|
1509
|
+
runtime.setSyncControls({
|
|
1510
|
+
begin: params.begin,
|
|
1511
|
+
write: params.write,
|
|
1512
|
+
commit: params.commit,
|
|
1513
|
+
truncate: params.truncate,
|
|
1514
|
+
metadata: params.metadata ?? null
|
|
1515
|
+
});
|
|
1516
|
+
runtime.setCollection(
|
|
1517
|
+
params.collection
|
|
1518
|
+
);
|
|
1519
|
+
void runtime.ensureStarted().then(() => {
|
|
1520
|
+
params.markReady();
|
|
1521
|
+
}).catch((error) => {
|
|
1522
|
+
console.warn(`Failed persisted loopback startup:`, error);
|
|
1523
|
+
params.markReady();
|
|
1524
|
+
});
|
|
1525
|
+
return {
|
|
1526
|
+
cleanup: () => {
|
|
1527
|
+
runtime.cleanup();
|
|
1528
|
+
runtime.clearSyncControls();
|
|
1529
|
+
},
|
|
1530
|
+
loadSubset: (options) => runtime.loadSubset(options),
|
|
1531
|
+
unloadSubset: (options) => runtime.unloadSubset(options)
|
|
1532
|
+
};
|
|
1533
|
+
},
|
|
1534
|
+
getSyncMetadata: () => ({
|
|
1535
|
+
source: `persisted-phase-2-loopback`
|
|
1536
|
+
})
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
function persistedCollectionOptions(options) {
|
|
1540
|
+
if (!isRecord(options.persistence)) {
|
|
1541
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
1542
|
+
`persistedCollectionOptions requires a persistence adapter`
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
if (hasOwnSyncKey(options)) {
|
|
1546
|
+
if (!isValidSyncConfig(options.sync)) {
|
|
1547
|
+
throw new errors.InvalidSyncConfigError(
|
|
1548
|
+
`when the "sync" key is present it must provide a callable sync function`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
const { schemaVersion: schemaVersion2, ...syncOptions } = options;
|
|
1552
|
+
const collectionId2 = syncOptions.id ?? `persisted-collection:${crypto.randomUUID()}`;
|
|
1553
|
+
const persistence2 = resolvePersistenceForCollection(
|
|
1554
|
+
syncOptions.persistence,
|
|
1555
|
+
{
|
|
1556
|
+
collectionId: collectionId2,
|
|
1557
|
+
mode: `sync-present`,
|
|
1558
|
+
schemaVersion: schemaVersion2
|
|
1559
|
+
}
|
|
1560
|
+
);
|
|
1561
|
+
const runtime2 = new PersistedCollectionRuntime(
|
|
1562
|
+
`sync-present`,
|
|
1563
|
+
collectionId2,
|
|
1564
|
+
persistence2,
|
|
1565
|
+
syncOptions.syncMode ?? `eager`,
|
|
1566
|
+
collectionId2
|
|
1567
|
+
);
|
|
1568
|
+
return {
|
|
1569
|
+
...syncOptions,
|
|
1570
|
+
id: collectionId2,
|
|
1571
|
+
sync: createWrappedSyncConfig(syncOptions.sync, runtime2),
|
|
1572
|
+
persistence: persistence2
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const { schemaVersion, ...localOnlyOptions } = options;
|
|
1576
|
+
const collectionId = localOnlyOptions.id ?? `persisted-collection:${crypto.randomUUID()}`;
|
|
1577
|
+
const persistence = resolvePersistenceForCollection(
|
|
1578
|
+
localOnlyOptions.persistence,
|
|
1579
|
+
{
|
|
1580
|
+
collectionId,
|
|
1581
|
+
mode: `sync-absent`,
|
|
1582
|
+
schemaVersion
|
|
1583
|
+
}
|
|
1584
|
+
);
|
|
1585
|
+
const runtime = new PersistedCollectionRuntime(
|
|
1586
|
+
`sync-absent`,
|
|
1587
|
+
collectionId,
|
|
1588
|
+
persistence,
|
|
1589
|
+
localOnlyOptions.syncMode ?? `eager`,
|
|
1590
|
+
localOnlyOptions.id ?? DEFAULT_DB_NAME
|
|
1591
|
+
);
|
|
1592
|
+
const wrappedOnInsert = async (params) => {
|
|
1593
|
+
const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
|
|
1594
|
+
params
|
|
1595
|
+
) : void 0;
|
|
1596
|
+
await runtime.persistAndConfirmCollectionMutations(
|
|
1597
|
+
params.transaction.mutations
|
|
1598
|
+
);
|
|
1599
|
+
return handlerResult ?? {};
|
|
1600
|
+
};
|
|
1601
|
+
const wrappedOnUpdate = async (params) => {
|
|
1602
|
+
const handlerResult = localOnlyOptions.onUpdate ? await localOnlyOptions.onUpdate(
|
|
1603
|
+
params
|
|
1604
|
+
) : void 0;
|
|
1605
|
+
await runtime.persistAndConfirmCollectionMutations(
|
|
1606
|
+
params.transaction.mutations
|
|
1607
|
+
);
|
|
1608
|
+
return handlerResult ?? {};
|
|
1609
|
+
};
|
|
1610
|
+
const wrappedOnDelete = async (params) => {
|
|
1611
|
+
const handlerResult = localOnlyOptions.onDelete ? await localOnlyOptions.onDelete(
|
|
1612
|
+
params
|
|
1613
|
+
) : void 0;
|
|
1614
|
+
await runtime.persistAndConfirmCollectionMutations(
|
|
1615
|
+
params.transaction.mutations
|
|
1616
|
+
);
|
|
1617
|
+
return handlerResult ?? {};
|
|
1618
|
+
};
|
|
1619
|
+
const acceptMutations = async (transaction) => {
|
|
1620
|
+
await runtime.acceptTransactionMutations(transaction);
|
|
1621
|
+
};
|
|
1622
|
+
const persistedUtils = {
|
|
1623
|
+
acceptMutations,
|
|
1624
|
+
getLeadershipState: () => runtime.getLeadershipState(),
|
|
1625
|
+
forceReloadSubset: (subsetOptions) => runtime.forceReloadSubset(subsetOptions)
|
|
1626
|
+
};
|
|
1627
|
+
const mergedUtils = {
|
|
1628
|
+
...localOnlyOptions.utils ?? {},
|
|
1629
|
+
...persistedUtils
|
|
1630
|
+
};
|
|
1631
|
+
return {
|
|
1632
|
+
...localOnlyOptions,
|
|
1633
|
+
id: collectionId,
|
|
1634
|
+
persistence,
|
|
1635
|
+
sync: createLoopbackSyncConfig(runtime),
|
|
1636
|
+
onInsert: wrappedOnInsert,
|
|
1637
|
+
onUpdate: wrappedOnUpdate,
|
|
1638
|
+
onDelete: wrappedOnDelete,
|
|
1639
|
+
utils: mergedUtils,
|
|
1640
|
+
startSync: true,
|
|
1641
|
+
gcTime: localOnlyOptions.gcTime ?? 0
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
function encodePersistedStorageKey(key) {
|
|
1645
|
+
if (typeof key === `number`) {
|
|
1646
|
+
if (!Number.isFinite(key)) {
|
|
1647
|
+
throw new errors.InvalidPersistedStorageKeyError(key);
|
|
1648
|
+
}
|
|
1649
|
+
if (Object.is(key, -0)) {
|
|
1650
|
+
return `n:-0`;
|
|
1651
|
+
}
|
|
1652
|
+
return `n:${key}`;
|
|
1653
|
+
}
|
|
1654
|
+
return `s:${key}`;
|
|
1655
|
+
}
|
|
1656
|
+
function decodePersistedStorageKey(encoded) {
|
|
1657
|
+
if (encoded === `n:-0`) {
|
|
1658
|
+
return -0;
|
|
1659
|
+
}
|
|
1660
|
+
if (encoded.startsWith(`n:`)) {
|
|
1661
|
+
return Number(encoded.slice(2));
|
|
1662
|
+
}
|
|
1663
|
+
if (encoded.startsWith(`s:`)) {
|
|
1664
|
+
return encoded.slice(2);
|
|
1665
|
+
}
|
|
1666
|
+
throw new errors.InvalidPersistedStorageKeyEncodingError(encoded);
|
|
1667
|
+
}
|
|
1668
|
+
const PERSISTED_TABLE_NAME_ALPHABET = `abcdefghijklmnopqrstuvwxyz234567`;
|
|
1669
|
+
function hashCollectionId(collectionId) {
|
|
1670
|
+
let hash = 2166136261;
|
|
1671
|
+
for (let index = 0; index < collectionId.length; index++) {
|
|
1672
|
+
hash ^= collectionId.charCodeAt(index);
|
|
1673
|
+
hash = Math.imul(hash, 16777619);
|
|
1674
|
+
}
|
|
1675
|
+
return hash >>> 0;
|
|
1676
|
+
}
|
|
1677
|
+
function toBase32(input) {
|
|
1678
|
+
if (input === 0) {
|
|
1679
|
+
return PERSISTED_TABLE_NAME_ALPHABET.charAt(0);
|
|
1680
|
+
}
|
|
1681
|
+
let value = input >>> 0;
|
|
1682
|
+
let output = ``;
|
|
1683
|
+
while (value > 0) {
|
|
1684
|
+
output = `${PERSISTED_TABLE_NAME_ALPHABET.charAt(value % 32)}${output}`;
|
|
1685
|
+
value = Math.floor(value / 32);
|
|
1686
|
+
}
|
|
1687
|
+
return output;
|
|
1688
|
+
}
|
|
1689
|
+
function createPersistedTableName(collectionId, prefix = `c`) {
|
|
1690
|
+
if (!collectionId) {
|
|
1691
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
1692
|
+
`collectionId is required to derive a persisted table name`
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
const hashPart = toBase32(hashCollectionId(collectionId));
|
|
1696
|
+
const lengthPart = collectionId.length.toString(36);
|
|
1697
|
+
return `${prefix}_${hashPart}_${lengthPart}`;
|
|
1698
|
+
}
|
|
1699
|
+
exports.SingleProcessCoordinator = SingleProcessCoordinator;
|
|
1700
|
+
exports.createPersistedTableName = createPersistedTableName;
|
|
1701
|
+
exports.decodePersistedStorageKey = decodePersistedStorageKey;
|
|
1702
|
+
exports.encodePersistedStorageKey = encodePersistedStorageKey;
|
|
1703
|
+
exports.persistedCollectionOptions = persistedCollectionOptions;
|
|
1704
|
+
exports.validatePersistedCollectionCoordinator = validatePersistedCollectionCoordinator;
|
|
1705
|
+
//# sourceMappingURL=persisted.cjs.map
|