@tanstack/electric-db-collection 0.4.8 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/electric.cjs +751 -495
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +94 -25
- package/dist/cjs/sql-compiler.cjs +47 -95
- package/dist/cjs/sql-compiler.cjs.map +1 -1
- package/dist/esm/electric.d.ts +94 -25
- package/dist/esm/electric.js +753 -497
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/sql-compiler.js +47 -95
- package/dist/esm/sql-compiler.js.map +1 -1
- package/package.json +4 -3
- package/src/electric.ts +1176 -752
- package/src/sql-compiler.ts +74 -137
package/dist/esm/electric.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isVisibleInSnapshot, isChangeMessage, ShapeStream, isControlMessage } from "@electric-sql/client";
|
|
2
2
|
import { isChangeMessage as isChangeMessage2, isControlMessage as isControlMessage2 } from "@electric-sql/client";
|
|
3
|
-
import { Store } from "@tanstack/store";
|
|
4
3
|
import DebugModule from "debug";
|
|
5
|
-
import { withCollectionConfigFactory, DeduplicatedLoadSubset, and, LoadSubsetOperationAbortedError } from "@tanstack/db";
|
|
4
|
+
import { withCollectionSyncConfigFactory, withCollectionConfigFactory, validateSyncPersistenceCapability, warnOnce, withCollectionSyncConfigCleanup, DeduplicatedLoadSubset, and, LoadSubsetOperationAbortedError } from "@tanstack/db";
|
|
6
5
|
import { StreamAbortedError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError, TimeoutWaitingForMatchError } from "./errors.js";
|
|
7
6
|
import { compileSQL } from "./sql-compiler.js";
|
|
8
7
|
import { isMoveOutMessage, isMoveInMessage, deriveDisjunctPositions, getTagLength, addTagToIndex, parseTag, removeTagFromIndex, findRowsMatchingPattern, rowVisible, tagMatchesPattern } from "./tag-index.js";
|
|
9
8
|
const debug = DebugModule.debug(`ts/db:electric`);
|
|
10
|
-
const FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS = 250;
|
|
11
9
|
const ELECTRIC_TEST_HOOKS = /* @__PURE__ */ Symbol(`electricTestHooks`);
|
|
10
|
+
function exportElectricSyncMeta(evidence) {
|
|
11
|
+
const resume = evidence.hydratedResumeState;
|
|
12
|
+
return {
|
|
13
|
+
version: 1,
|
|
14
|
+
...resume ? { resume } : {},
|
|
15
|
+
seenTxids: Array.from(evidence.seenTxids).sort((a, b) => a - b)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function importElectricSyncMeta(evidence, meta) {
|
|
19
|
+
const parsed = parseElectricSyncMeta(meta);
|
|
20
|
+
if (!parsed) return;
|
|
21
|
+
evidence.hydratedResumeState = parsed.resume;
|
|
22
|
+
evidence.seenTxids = new Set(parsed.seenTxids);
|
|
23
|
+
}
|
|
12
24
|
function parseElectricResumeState(value) {
|
|
13
25
|
if (!value || typeof value !== `object`) {
|
|
14
26
|
return void 0;
|
|
@@ -20,7 +32,10 @@ function parseElectricResumeState(value) {
|
|
|
20
32
|
offset: record.offset,
|
|
21
33
|
handle: record.handle,
|
|
22
34
|
shapeId: record.shapeId,
|
|
23
|
-
updatedAt: record.updatedAt
|
|
35
|
+
updatedAt: record.updatedAt,
|
|
36
|
+
...typeof record.requiresTagState === `boolean` && {
|
|
37
|
+
requiresTagState: record.requiresTagState
|
|
38
|
+
}
|
|
24
39
|
};
|
|
25
40
|
}
|
|
26
41
|
if (record.kind === `reset` && typeof record.updatedAt === `number` && Number.isFinite(record.updatedAt)) {
|
|
@@ -75,7 +90,10 @@ function mergeElectricSyncMeta(current, incoming) {
|
|
|
75
90
|
function getNewestElectricResumeState(current, incoming) {
|
|
76
91
|
if (!current) return incoming;
|
|
77
92
|
if (!incoming) return current;
|
|
78
|
-
|
|
93
|
+
if (incoming.updatedAt > current.updatedAt) return incoming;
|
|
94
|
+
if (incoming.updatedAt < current.updatedAt) return current;
|
|
95
|
+
const isSameState = current.kind === incoming.kind && (current.kind === `reset` || incoming.kind === `resume` && current.offset === incoming.offset && current.handle === incoming.handle && current.shapeId === incoming.shapeId);
|
|
96
|
+
return isSameState ? current : { kind: `reset`, updatedAt: current.updatedAt };
|
|
79
97
|
}
|
|
80
98
|
function isUpToDateMessage(message) {
|
|
81
99
|
return isControlMessage(message) && message.headers.control === `up-to-date`;
|
|
@@ -139,6 +157,7 @@ function createLoadSubsetDedupe({
|
|
|
139
157
|
commit,
|
|
140
158
|
getCommitCursor,
|
|
141
159
|
waitForCommitsAfter,
|
|
160
|
+
recordSnapshotRow,
|
|
142
161
|
collectionId,
|
|
143
162
|
encodeColumnName,
|
|
144
163
|
signal
|
|
@@ -181,7 +200,9 @@ function createLoadSubsetDedupe({
|
|
|
181
200
|
metadata: { ...row.headers }
|
|
182
201
|
});
|
|
183
202
|
}
|
|
184
|
-
|
|
203
|
+
const applied = commit(opts.signal);
|
|
204
|
+
for (const row of rows) recordSnapshotRow?.(row.value, row);
|
|
205
|
+
if (applied !== true) await applied;
|
|
185
206
|
debug(`${logPrefix}Applied snapshot with ${rows.length} rows`);
|
|
186
207
|
}
|
|
187
208
|
} catch (error) {
|
|
@@ -197,47 +218,6 @@ function createLoadSubsetDedupe({
|
|
|
197
218
|
return;
|
|
198
219
|
}
|
|
199
220
|
const { cursor, where, orderBy, limit } = opts;
|
|
200
|
-
if (stream.isUpToDate) {
|
|
201
|
-
let timeoutId;
|
|
202
|
-
const abortSignals = [signal, opts.signal].filter(
|
|
203
|
-
(candidate) => candidate !== void 0
|
|
204
|
-
);
|
|
205
|
-
let rejectAbort = () => {
|
|
206
|
-
};
|
|
207
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
208
|
-
rejectAbort = reject;
|
|
209
|
-
});
|
|
210
|
-
const abort = (event) => rejectAbort(abortReason(event.currentTarget));
|
|
211
|
-
for (const abortSignal of abortSignals) {
|
|
212
|
-
abortSignal.addEventListener(`abort`, abort, { once: true });
|
|
213
|
-
}
|
|
214
|
-
try {
|
|
215
|
-
await Promise.race([
|
|
216
|
-
stream.forceDisconnectAndRefresh(),
|
|
217
|
-
new Promise((resolve) => {
|
|
218
|
-
timeoutId = setTimeout(
|
|
219
|
-
resolve,
|
|
220
|
-
FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS
|
|
221
|
-
);
|
|
222
|
-
}),
|
|
223
|
-
aborted
|
|
224
|
-
]);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
if (signal.aborted || opts.signal?.aborted) throw error;
|
|
227
|
-
if (handleSnapshotError(error, `forceDisconnectAndRefresh`)) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
debug(
|
|
231
|
-
`${logPrefix}forceDisconnectAndRefresh failed, proceeding to requestSnapshot: %o`,
|
|
232
|
-
error
|
|
233
|
-
);
|
|
234
|
-
} finally {
|
|
235
|
-
clearTimeout(timeoutId);
|
|
236
|
-
for (const abortSignal of abortSignals) {
|
|
237
|
-
abortSignal.removeEventListener(`abort`, abort);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
221
|
throwIfAborted();
|
|
242
222
|
try {
|
|
243
223
|
if (cursor) {
|
|
@@ -278,204 +258,245 @@ function createLoadSubsetDedupe({
|
|
|
278
258
|
};
|
|
279
259
|
return new DeduplicatedLoadSubset({ loadSubset });
|
|
280
260
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
261
|
+
class ElectricLifecycle {
|
|
262
|
+
constructor(collectionId) {
|
|
263
|
+
this.collectionId = collectionId;
|
|
264
|
+
this.evidence = {
|
|
265
|
+
seenTxids: /* @__PURE__ */ new Set(),
|
|
266
|
+
seenSnapshots: []
|
|
267
|
+
};
|
|
268
|
+
this.pendingMatches = /* @__PURE__ */ new Map();
|
|
269
|
+
this.pendingTxidWaits = /* @__PURE__ */ new Map();
|
|
270
|
+
this.matchBuffer = {
|
|
271
|
+
committedMessages: [],
|
|
272
|
+
pendingMessages: []
|
|
273
|
+
};
|
|
274
|
+
this.nextWaiterId = 0;
|
|
275
|
+
this.epoch = 0;
|
|
276
|
+
this.active = false;
|
|
277
|
+
this.utils = {
|
|
278
|
+
awaitTxId: (txId, timeout) => this.awaitTxId(txId, timeout),
|
|
279
|
+
awaitMatch: (matchFn, timeout) => this.awaitMatch(matchFn, timeout)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
start() {
|
|
283
|
+
if (this.active) this.retire();
|
|
284
|
+
this.active = true;
|
|
285
|
+
this.epoch++;
|
|
286
|
+
this.matchBuffer = { committedMessages: [], pendingMessages: [] };
|
|
287
|
+
return this.epoch;
|
|
288
|
+
}
|
|
289
|
+
isActive(epoch) {
|
|
290
|
+
return this.active && this.epoch === epoch;
|
|
291
|
+
}
|
|
292
|
+
retire(epoch) {
|
|
293
|
+
if (epoch !== void 0 && !this.isActive(epoch)) return;
|
|
294
|
+
this.active = false;
|
|
295
|
+
this.epoch++;
|
|
296
|
+
for (const match of this.pendingMatches.values()) {
|
|
297
|
+
clearTimeout(match.timeoutId);
|
|
298
|
+
match.reject(new StreamAbortedError(this.collectionId));
|
|
299
299
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
300
|
+
this.pendingMatches.clear();
|
|
301
|
+
for (const waiter of this.pendingTxidWaits.values()) {
|
|
302
|
+
clearTimeout(waiter.timeoutId);
|
|
303
|
+
waiter.reject(new StreamAbortedError(this.collectionId));
|
|
304
|
+
}
|
|
305
|
+
this.pendingTxidWaits.clear();
|
|
306
|
+
this.matchBuffer = { committedMessages: [], pendingMessages: [] };
|
|
307
|
+
this.evidence.hydratedResumeState = void 0;
|
|
308
|
+
}
|
|
309
|
+
exportMeta() {
|
|
310
|
+
return exportElectricSyncMeta(this.evidence);
|
|
311
|
+
}
|
|
312
|
+
importMeta(meta) {
|
|
313
|
+
importElectricSyncMeta(this.evidence, meta);
|
|
314
|
+
this.resolveTxidWaiters();
|
|
315
|
+
}
|
|
316
|
+
get resumeState() {
|
|
317
|
+
return this.evidence.hydratedResumeState;
|
|
318
|
+
}
|
|
319
|
+
set resumeState(value) {
|
|
320
|
+
this.evidence.hydratedResumeState = value;
|
|
321
|
+
}
|
|
322
|
+
publishEvidence(txids, snapshots) {
|
|
323
|
+
txids.forEach((txid) => this.evidence.seenTxids.add(txid));
|
|
324
|
+
this.evidence.seenSnapshots.push(...snapshots);
|
|
325
|
+
this.resolveTxidWaiters();
|
|
326
|
+
}
|
|
327
|
+
hasTxid(txId) {
|
|
328
|
+
return this.evidence.seenTxids.has(txId) || this.evidence.seenSnapshots.some(
|
|
329
|
+
(snapshot) => isVisibleInSnapshot(txId, snapshot)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
resolveTxidWaiters() {
|
|
333
|
+
for (const [waitId, waiter] of this.pendingTxidWaits) {
|
|
334
|
+
if (!this.hasTxid(waiter.txId)) continue;
|
|
335
|
+
clearTimeout(waiter.timeoutId);
|
|
336
|
+
this.pendingTxidWaits.delete(waitId);
|
|
337
|
+
waiter.resolve(true);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async awaitTxId(txId, timeout = 15e3) {
|
|
330
341
|
debug(
|
|
331
|
-
`${
|
|
342
|
+
`${this.collectionId ? `[${this.collectionId}] ` : ``}awaitTxId called with txid %d`,
|
|
332
343
|
txId
|
|
333
344
|
);
|
|
334
345
|
if (typeof txId !== `number`) {
|
|
335
|
-
throw new ExpectedNumberInAwaitTxIdError(typeof txId,
|
|
346
|
+
throw new ExpectedNumberInAwaitTxIdError(typeof txId, this.collectionId);
|
|
336
347
|
}
|
|
337
|
-
|
|
338
|
-
if (hasTxid) return true;
|
|
339
|
-
const hasSnapshot = seenSnapshots.state.some(
|
|
340
|
-
(snapshot) => isVisibleInSnapshot(txId, snapshot)
|
|
341
|
-
);
|
|
342
|
-
if (hasSnapshot) return true;
|
|
348
|
+
if (this.hasTxid(txId)) return true;
|
|
343
349
|
return new Promise((resolve, reject) => {
|
|
344
|
-
const
|
|
345
|
-
clearTimeout(timeoutId);
|
|
346
|
-
subSeenTxids.unsubscribe();
|
|
347
|
-
subSeenSnapshots.unsubscribe();
|
|
348
|
-
};
|
|
350
|
+
const waitId = this.nextWaiterId++;
|
|
349
351
|
const timeoutId = setTimeout(() => {
|
|
350
|
-
|
|
351
|
-
reject(new TimeoutWaitingForTxIdError(txId,
|
|
352
|
+
this.pendingTxidWaits.delete(waitId);
|
|
353
|
+
reject(new TimeoutWaitingForTxIdError(txId, this.collectionId));
|
|
352
354
|
}, timeout);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
359
|
-
cleanup();
|
|
360
|
-
resolve(true);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
const subSeenSnapshots = seenSnapshots.subscribe(() => {
|
|
364
|
-
const visibleSnapshot = seenSnapshots.state.find(
|
|
365
|
-
(snapshot) => isVisibleInSnapshot(txId, snapshot)
|
|
366
|
-
);
|
|
367
|
-
if (visibleSnapshot) {
|
|
368
|
-
debug(
|
|
369
|
-
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
|
|
370
|
-
txId,
|
|
371
|
-
visibleSnapshot
|
|
372
|
-
);
|
|
373
|
-
cleanup();
|
|
374
|
-
resolve(true);
|
|
375
|
-
}
|
|
355
|
+
this.pendingTxidWaits.set(waitId, {
|
|
356
|
+
txId,
|
|
357
|
+
resolve,
|
|
358
|
+
reject,
|
|
359
|
+
timeoutId
|
|
376
360
|
});
|
|
377
361
|
});
|
|
378
|
-
}
|
|
379
|
-
|
|
362
|
+
}
|
|
363
|
+
async awaitMatch(matchFn, timeout = 15e3) {
|
|
380
364
|
debug(
|
|
381
|
-
`${
|
|
365
|
+
`${this.collectionId ? `[${this.collectionId}] ` : ``}awaitMatch called with custom function`
|
|
382
366
|
);
|
|
367
|
+
for (const message of this.matchBuffer.committedMessages) {
|
|
368
|
+
if (!matchFn(message)) continue;
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
for (const message of this.matchBuffer.pendingMessages) {
|
|
372
|
+
if (matchFn(message)) return this.registerMatch(matchFn, timeout, true);
|
|
373
|
+
}
|
|
374
|
+
return this.registerMatch(matchFn, timeout, false);
|
|
375
|
+
}
|
|
376
|
+
registerMatch(matchFn, timeout, matched) {
|
|
383
377
|
return new Promise((resolve, reject) => {
|
|
384
|
-
const matchId =
|
|
385
|
-
const
|
|
386
|
-
pendingMatches.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
};
|
|
396
|
-
const timeoutId = setTimeout(onTimeout, timeout);
|
|
397
|
-
const checkMatch = (message) => {
|
|
398
|
-
if (matchFn(message)) {
|
|
399
|
-
debug(
|
|
400
|
-
`${config.id ? `[${config.id}] ` : ``}awaitMatch found matching message, waiting for up-to-date`
|
|
401
|
-
);
|
|
402
|
-
pendingMatches.setState((current) => {
|
|
403
|
-
const newMatches = new Map(current);
|
|
404
|
-
const existing = newMatches.get(matchId);
|
|
405
|
-
if (existing) {
|
|
406
|
-
newMatches.set(matchId, { ...existing, matched: true });
|
|
407
|
-
}
|
|
408
|
-
return newMatches;
|
|
409
|
-
});
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
return false;
|
|
413
|
-
};
|
|
414
|
-
for (const message of currentBatchMessages.state) {
|
|
415
|
-
if (matchFn(message)) {
|
|
416
|
-
if (batchCommitted.state) {
|
|
417
|
-
debug(
|
|
418
|
-
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in committed batch, resolving immediately`
|
|
419
|
-
);
|
|
420
|
-
clearTimeout(timeoutId);
|
|
421
|
-
resolve(true);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
debug(
|
|
425
|
-
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`
|
|
426
|
-
);
|
|
427
|
-
pendingMatches.setState((current) => {
|
|
428
|
-
const newMatches = new Map(current);
|
|
429
|
-
newMatches.set(matchId, {
|
|
430
|
-
matchFn: checkMatch,
|
|
431
|
-
resolve,
|
|
432
|
-
reject,
|
|
433
|
-
timeoutId,
|
|
434
|
-
matched: true
|
|
435
|
-
// Already matched, will resolve on up-to-date
|
|
436
|
-
});
|
|
437
|
-
return newMatches;
|
|
438
|
-
});
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
pendingMatches.setState((current) => {
|
|
443
|
-
const newMatches = new Map(current);
|
|
444
|
-
newMatches.set(matchId, {
|
|
445
|
-
matchFn: checkMatch,
|
|
446
|
-
resolve,
|
|
447
|
-
reject,
|
|
448
|
-
timeoutId,
|
|
449
|
-
matched: false
|
|
450
|
-
});
|
|
451
|
-
return newMatches;
|
|
378
|
+
const matchId = this.nextWaiterId++;
|
|
379
|
+
const timeoutId = setTimeout(() => {
|
|
380
|
+
this.pendingMatches.delete(matchId);
|
|
381
|
+
reject(new TimeoutWaitingForMatchError(this.collectionId));
|
|
382
|
+
}, timeout);
|
|
383
|
+
this.pendingMatches.set(matchId, {
|
|
384
|
+
matchFn,
|
|
385
|
+
resolve,
|
|
386
|
+
reject,
|
|
387
|
+
timeoutId,
|
|
388
|
+
matched
|
|
452
389
|
});
|
|
453
390
|
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
391
|
+
}
|
|
392
|
+
beginMatchGeneration(messages) {
|
|
393
|
+
if (!messages.some(isMustRefetchMessage)) return;
|
|
394
|
+
this.matchBuffer = { committedMessages: [], pendingMessages: [] };
|
|
395
|
+
for (const match of this.pendingMatches.values()) match.matched = false;
|
|
396
|
+
}
|
|
397
|
+
observeMatchMessage(message) {
|
|
398
|
+
if (isChangeMessage(message) || isMoveOutMessage(message) || isMoveInMessage(message)) {
|
|
399
|
+
this.matchBuffer.pendingMessages.push(message);
|
|
400
|
+
let overflow = this.matchBuffer.committedMessages.length + this.matchBuffer.pendingMessages.length - 1e3;
|
|
401
|
+
if (overflow > 0) {
|
|
402
|
+
const committedOverflow = Math.min(
|
|
403
|
+
overflow,
|
|
404
|
+
this.matchBuffer.committedMessages.length
|
|
405
|
+
);
|
|
406
|
+
this.matchBuffer.committedMessages.splice(0, committedOverflow);
|
|
407
|
+
overflow -= committedOverflow;
|
|
408
|
+
if (overflow > 0) {
|
|
409
|
+
this.matchBuffer.pendingMessages.splice(0, overflow);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const epoch = this.epoch;
|
|
414
|
+
for (const [matchId, match] of this.pendingMatches) {
|
|
415
|
+
if (match.matched) continue;
|
|
416
|
+
try {
|
|
417
|
+
match.matched = match.matchFn(message);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
clearTimeout(match.timeoutId);
|
|
420
|
+
this.pendingMatches.delete(matchId);
|
|
421
|
+
match.reject(error instanceof Error ? error : new Error(String(error)));
|
|
422
|
+
}
|
|
423
|
+
if (!this.isActive(epoch)) return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
commitMatches() {
|
|
427
|
+
this.matchBuffer.committedMessages.push(...this.matchBuffer.pendingMessages);
|
|
428
|
+
this.matchBuffer.pendingMessages = [];
|
|
429
|
+
if (this.matchBuffer.committedMessages.length > 1e3) {
|
|
430
|
+
this.matchBuffer.committedMessages.splice(
|
|
431
|
+
0,
|
|
432
|
+
this.matchBuffer.committedMessages.length - 1e3
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
for (const [matchId, match] of this.pendingMatches) {
|
|
436
|
+
if (!match.matched) continue;
|
|
437
|
+
clearTimeout(match.timeoutId);
|
|
438
|
+
this.pendingMatches.delete(matchId);
|
|
439
|
+
match.resolve(true);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function electricCollectionOptions(config) {
|
|
444
|
+
let descriptorLifecycle = new ElectricLifecycle(config.id);
|
|
445
|
+
let utilityLifecycle = descriptorLifecycle;
|
|
446
|
+
const internalSyncMode = config.syncMode ?? `eager`;
|
|
447
|
+
const finalSyncMode = internalSyncMode === `progressive` ? `on-demand` : internalSyncMode;
|
|
448
|
+
const boundLifecycles = /* @__PURE__ */ new WeakMap();
|
|
449
|
+
const awaitTxId = (txId, timeout) => utilityLifecycle.utils.awaitTxId(txId, timeout);
|
|
450
|
+
const awaitMatch = (matchFn, timeout) => utilityLifecycle.utils.awaitMatch(matchFn, timeout);
|
|
451
|
+
const createSync = () => createElectricSync(config.shapeOptions, {
|
|
452
|
+
getLifecycle: (collection) => boundLifecycles.get(collection) ?? descriptorLifecycle,
|
|
453
|
+
syncMode: internalSyncMode,
|
|
454
|
+
collectionId: config.id,
|
|
455
|
+
testHooks: config[ELECTRIC_TEST_HOOKS]
|
|
456
|
+
});
|
|
457
|
+
const sync = createSync();
|
|
458
|
+
const processMatchingStrategy = async (result, waitForTxId) => {
|
|
459
|
+
const canHaveProperties = typeof result === `object` && result !== null || typeof result === `function`;
|
|
460
|
+
if (canHaveProperties && `txid` in result) {
|
|
461
|
+
const strategy = result;
|
|
462
|
+
warnOnce(
|
|
463
|
+
"electric-collection-txid-return",
|
|
464
|
+
"[TanStack DB] DEPRECATED: Returning { txid } from mutation handlers is deprecated and will be removed in v1.0. Use `await collection.utils.awaitTxId(txid)` instead of returning { txid }. See migration guide: https://tanstack.com/db/latest/docs/collections/electric-collection#persistence-handlers--synchronization"
|
|
465
|
+
);
|
|
466
|
+
const timeout = strategy.timeout;
|
|
467
|
+
if (Array.isArray(strategy.txid)) {
|
|
468
|
+
await Promise.all(
|
|
469
|
+
strategy.txid.map((txid) => waitForTxId(txid, timeout))
|
|
470
|
+
);
|
|
460
471
|
} else {
|
|
461
|
-
await
|
|
472
|
+
await waitForTxId(strategy.txid, timeout);
|
|
462
473
|
}
|
|
463
474
|
}
|
|
464
475
|
};
|
|
476
|
+
const getMutationAwaitTxId = (params) => {
|
|
477
|
+
const collection = params.collection;
|
|
478
|
+
return collection?.utils?.awaitTxId ?? awaitTxId;
|
|
479
|
+
};
|
|
465
480
|
const wrappedOnInsert = config.onInsert ? async (params) => {
|
|
466
481
|
const handlerResult = await config.onInsert(params);
|
|
467
|
-
await processMatchingStrategy(
|
|
468
|
-
|
|
482
|
+
await processMatchingStrategy(
|
|
483
|
+
handlerResult,
|
|
484
|
+
getMutationAwaitTxId(params)
|
|
485
|
+
);
|
|
469
486
|
} : void 0;
|
|
470
487
|
const wrappedOnUpdate = config.onUpdate ? async (params) => {
|
|
471
488
|
const handlerResult = await config.onUpdate(params);
|
|
472
|
-
await processMatchingStrategy(
|
|
473
|
-
|
|
489
|
+
await processMatchingStrategy(
|
|
490
|
+
handlerResult,
|
|
491
|
+
getMutationAwaitTxId(params)
|
|
492
|
+
);
|
|
474
493
|
} : void 0;
|
|
475
494
|
const wrappedOnDelete = config.onDelete ? async (params) => {
|
|
476
495
|
const handlerResult = await config.onDelete(params);
|
|
477
|
-
await processMatchingStrategy(
|
|
478
|
-
|
|
496
|
+
await processMatchingStrategy(
|
|
497
|
+
handlerResult,
|
|
498
|
+
getMutationAwaitTxId(params)
|
|
499
|
+
);
|
|
479
500
|
} : void 0;
|
|
480
501
|
const {
|
|
481
502
|
shapeOptions: _shapeOptions,
|
|
@@ -484,236 +505,274 @@ function electricCollectionOptions(config) {
|
|
|
484
505
|
onDelete: _onDelete,
|
|
485
506
|
...restConfig
|
|
486
507
|
} = config;
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
508
|
+
const utilityTemplate = {
|
|
509
|
+
awaitTxId,
|
|
510
|
+
awaitMatch
|
|
511
|
+
};
|
|
512
|
+
const consumeDescriptorLifecycle = () => {
|
|
513
|
+
const lifecycle = descriptorLifecycle;
|
|
514
|
+
descriptorLifecycle = new ElectricLifecycle(config.id);
|
|
515
|
+
utilityLifecycle = lifecycle;
|
|
516
|
+
return lifecycle;
|
|
517
|
+
};
|
|
518
|
+
const createBoundSync = (source, utilities) => {
|
|
519
|
+
const lifecycle = consumeDescriptorLifecycle();
|
|
520
|
+
let collectionKey;
|
|
521
|
+
Object.assign(utilities, lifecycle.utils);
|
|
522
|
+
const boundSync = {
|
|
523
|
+
...source,
|
|
524
|
+
sync: (params) => {
|
|
525
|
+
if (!boundLifecycles.has(params.collection)) {
|
|
526
|
+
collectionKey = params.collection;
|
|
527
|
+
boundLifecycles.set(params.collection, lifecycle);
|
|
501
528
|
}
|
|
502
|
-
|
|
503
|
-
seenTxids.setState(() => new Set(parsed.seenTxids));
|
|
529
|
+
return source.sync(params);
|
|
504
530
|
},
|
|
531
|
+
exportSyncMeta: () => lifecycle.exportMeta(),
|
|
532
|
+
importSyncMeta: (meta) => lifecycle.importMeta(meta),
|
|
533
|
+
mergeSyncMeta: mergeElectricSyncMeta
|
|
534
|
+
};
|
|
535
|
+
return withCollectionSyncConfigCleanup(boundSync, () => {
|
|
536
|
+
lifecycle.retire();
|
|
537
|
+
if (collectionKey) boundLifecycles.delete(collectionKey);
|
|
538
|
+
});
|
|
539
|
+
};
|
|
540
|
+
const syncTemplate = withCollectionSyncConfigFactory(
|
|
541
|
+
{
|
|
542
|
+
...sync,
|
|
543
|
+
exportSyncMeta: () => descriptorLifecycle.exportMeta(),
|
|
544
|
+
importSyncMeta: (meta) => descriptorLifecycle.importMeta(meta),
|
|
505
545
|
mergeSyncMeta: mergeElectricSyncMeta
|
|
506
546
|
},
|
|
547
|
+
createBoundSync
|
|
548
|
+
);
|
|
549
|
+
const options = {
|
|
550
|
+
...restConfig,
|
|
551
|
+
syncMode: finalSyncMode,
|
|
552
|
+
sync: syncTemplate,
|
|
507
553
|
onInsert: wrappedOnInsert,
|
|
508
554
|
onUpdate: wrappedOnUpdate,
|
|
509
555
|
onDelete: wrappedOnDelete,
|
|
510
|
-
utils:
|
|
511
|
-
awaitTxId,
|
|
512
|
-
awaitMatch
|
|
513
|
-
}
|
|
556
|
+
utils: utilityTemplate
|
|
514
557
|
};
|
|
558
|
+
Object.defineProperty(options, `utils`, {
|
|
559
|
+
enumerable: true,
|
|
560
|
+
get: () => ({ ...utilityTemplate })
|
|
561
|
+
});
|
|
515
562
|
return withCollectionConfigFactory(
|
|
516
563
|
options,
|
|
517
564
|
() => electricCollectionOptions(config)
|
|
518
565
|
);
|
|
519
566
|
}
|
|
520
567
|
function createElectricSync(shapeOptions, options) {
|
|
521
|
-
const {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
removePendingMatches,
|
|
530
|
-
resolveMatchedPendingMatches,
|
|
531
|
-
collectionId,
|
|
532
|
-
testHooks
|
|
533
|
-
} = options;
|
|
534
|
-
const MAX_BATCH_MESSAGES = 1e3;
|
|
535
|
-
const relationSchema = new Store(void 0);
|
|
536
|
-
const tagCache = /* @__PURE__ */ new Map();
|
|
537
|
-
const parseTag$1 = (tag) => {
|
|
538
|
-
const cachedTag = tagCache.get(tag);
|
|
539
|
-
if (cachedTag) {
|
|
540
|
-
return cachedTag;
|
|
541
|
-
}
|
|
542
|
-
const parsedTag = parseTag(tag);
|
|
543
|
-
tagCache.set(tag, parsedTag);
|
|
544
|
-
return parsedTag;
|
|
545
|
-
};
|
|
546
|
-
const rowTagSets = /* @__PURE__ */ new Map();
|
|
547
|
-
const tagIndex = [];
|
|
548
|
-
let tagLength = void 0;
|
|
549
|
-
const rowActiveConditions = /* @__PURE__ */ new Map();
|
|
550
|
-
let disjunctPositions = void 0;
|
|
551
|
-
const initializeTagIndex = (length) => {
|
|
552
|
-
if (tagIndex.length < length) {
|
|
553
|
-
for (let i = tagIndex.length; i < length; i++) {
|
|
554
|
-
tagIndex[i] = /* @__PURE__ */ new Map();
|
|
568
|
+
const { getLifecycle, syncMode, collectionId, testHooks } = options;
|
|
569
|
+
let relationSchema;
|
|
570
|
+
const createTagState = () => {
|
|
571
|
+
const tagCache = /* @__PURE__ */ new Map();
|
|
572
|
+
const parseTag$1 = (tag) => {
|
|
573
|
+
const cachedTag = tagCache.get(tag);
|
|
574
|
+
if (cachedTag) {
|
|
575
|
+
return cachedTag;
|
|
555
576
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
577
|
+
const parsedTag = parseTag(tag);
|
|
578
|
+
tagCache.set(tag, parsedTag);
|
|
579
|
+
return parsedTag;
|
|
580
|
+
};
|
|
581
|
+
const rowTagSets = /* @__PURE__ */ new Map();
|
|
582
|
+
const tagIndex = [];
|
|
583
|
+
let tagLength = void 0;
|
|
584
|
+
const rowActiveConditions = /* @__PURE__ */ new Map();
|
|
585
|
+
let disjunctPositions = void 0;
|
|
586
|
+
const initializeTagIndex = (length) => {
|
|
587
|
+
if (tagIndex.length < length) {
|
|
588
|
+
for (let i = tagIndex.length; i < length; i++) {
|
|
589
|
+
tagIndex[i] = /* @__PURE__ */ new Map();
|
|
590
|
+
}
|
|
564
591
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
)
|
|
570
|
-
|
|
592
|
+
};
|
|
593
|
+
const addTagsToRow = (tags, rowId, rowTagSet) => {
|
|
594
|
+
for (const tag of tags) {
|
|
595
|
+
const parsedTag = parseTag$1(tag);
|
|
596
|
+
if (tagLength === void 0) {
|
|
597
|
+
tagLength = getTagLength(parsedTag);
|
|
598
|
+
initializeTagIndex(tagLength);
|
|
599
|
+
}
|
|
600
|
+
const currentTagLength = getTagLength(parsedTag);
|
|
601
|
+
if (currentTagLength !== tagLength) {
|
|
602
|
+
debug(
|
|
603
|
+
`${collectionId ? `[${collectionId}] ` : ``}Tag length mismatch: expected ${tagLength}, got ${currentTagLength}`
|
|
604
|
+
);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
rowTagSet.add(tag);
|
|
608
|
+
addTagToIndex(parsedTag, rowId, tagIndex, tagLength);
|
|
571
609
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const removeTagsFromRow = (removedTags, rowId, rowTagSet) => {
|
|
577
|
-
if (tagLength === void 0) {
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
for (const tag of removedTags) {
|
|
581
|
-
const parsedTag = parseTag$1(tag);
|
|
582
|
-
rowTagSet.delete(tag);
|
|
583
|
-
removeTagFromIndex(parsedTag, rowId, tagIndex, tagLength);
|
|
584
|
-
tagCache.delete(tag);
|
|
585
|
-
}
|
|
586
|
-
};
|
|
587
|
-
const processTagsForChangeMessage = (tags, removedTags, rowId, activeConditions) => {
|
|
588
|
-
if (!rowTagSets.has(rowId)) {
|
|
589
|
-
rowTagSets.set(rowId, /* @__PURE__ */ new Set());
|
|
590
|
-
}
|
|
591
|
-
const rowTagSet = rowTagSets.get(rowId);
|
|
592
|
-
if (tags) {
|
|
593
|
-
addTagsToRow(tags, rowId, rowTagSet);
|
|
594
|
-
if (disjunctPositions === void 0) {
|
|
595
|
-
const parsedTags = tags.map(parseTag$1);
|
|
596
|
-
disjunctPositions = deriveDisjunctPositions(parsedTags);
|
|
610
|
+
};
|
|
611
|
+
const removeTagsFromRow = (removedTags, rowId, rowTagSet) => {
|
|
612
|
+
if (tagLength === void 0) {
|
|
613
|
+
return;
|
|
597
614
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
if (activeConditions && activeConditions.length > 0) {
|
|
603
|
-
rowActiveConditions.set(rowId, [...activeConditions]);
|
|
604
|
-
}
|
|
605
|
-
return rowTagSet;
|
|
606
|
-
};
|
|
607
|
-
const clearTagTrackingState = () => {
|
|
608
|
-
rowTagSets.clear();
|
|
609
|
-
tagIndex.length = 0;
|
|
610
|
-
tagLength = void 0;
|
|
611
|
-
rowActiveConditions.clear();
|
|
612
|
-
disjunctPositions = void 0;
|
|
613
|
-
};
|
|
614
|
-
const clearTagsForRow = (rowId) => {
|
|
615
|
-
if (tagLength === void 0) {
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
const rowTagSet = rowTagSets.get(rowId);
|
|
619
|
-
if (!rowTagSet) {
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
for (const tag of rowTagSet) {
|
|
623
|
-
const parsedTag = parseTag$1(tag);
|
|
624
|
-
const currentTagLength = getTagLength(parsedTag);
|
|
625
|
-
if (currentTagLength === tagLength) {
|
|
615
|
+
for (const tag of removedTags) {
|
|
616
|
+
const parsedTag = parseTag$1(tag);
|
|
617
|
+
rowTagSet.delete(tag);
|
|
626
618
|
removeTagFromIndex(parsedTag, rowId, tagIndex, tagLength);
|
|
619
|
+
tagCache.delete(tag);
|
|
627
620
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (
|
|
642
|
-
|
|
643
|
-
|
|
621
|
+
};
|
|
622
|
+
const processTagsForChangeMessage = (tags, removedTags, rowId, activeConditions) => {
|
|
623
|
+
if (!rowTagSets.has(rowId)) {
|
|
624
|
+
rowTagSets.set(rowId, /* @__PURE__ */ new Set());
|
|
625
|
+
}
|
|
626
|
+
const rowTagSet = rowTagSets.get(rowId);
|
|
627
|
+
if (tags) {
|
|
628
|
+
addTagsToRow(tags, rowId, rowTagSet);
|
|
629
|
+
if (disjunctPositions === void 0) {
|
|
630
|
+
const parsedTags = tags.map(parseTag$1);
|
|
631
|
+
disjunctPositions = deriveDisjunctPositions(parsedTags);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (removedTags) {
|
|
635
|
+
removeTagsFromRow(removedTags, rowId, rowTagSet);
|
|
636
|
+
}
|
|
637
|
+
if (activeConditions && activeConditions.length > 0) {
|
|
638
|
+
rowActiveConditions.set(rowId, [...activeConditions]);
|
|
639
|
+
}
|
|
640
|
+
return rowTagSet;
|
|
641
|
+
};
|
|
642
|
+
const clearTagTrackingState = () => {
|
|
643
|
+
rowTagSets.clear();
|
|
644
|
+
tagIndex.length = 0;
|
|
645
|
+
tagLength = void 0;
|
|
646
|
+
rowActiveConditions.clear();
|
|
647
|
+
disjunctPositions = void 0;
|
|
648
|
+
};
|
|
649
|
+
const clearTagsForRow = (rowId) => {
|
|
650
|
+
if (tagLength === void 0) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const rowTagSet = rowTagSets.get(rowId);
|
|
654
|
+
if (!rowTagSet) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
for (const tag of rowTagSet) {
|
|
658
|
+
const parsedTag = parseTag$1(tag);
|
|
659
|
+
const currentTagLength = getTagLength(parsedTag);
|
|
660
|
+
if (currentTagLength === tagLength) {
|
|
644
661
|
removeTagFromIndex(parsedTag, rowId, tagIndex, tagLength);
|
|
645
|
-
tagCache.delete(tag);
|
|
646
662
|
}
|
|
663
|
+
tagCache.delete(tag);
|
|
664
|
+
}
|
|
665
|
+
rowTagSets.delete(rowId);
|
|
666
|
+
rowActiveConditions.delete(rowId);
|
|
667
|
+
};
|
|
668
|
+
const removeMatchingTagsFromRow = (rowId, pattern) => {
|
|
669
|
+
const rowTagSet = rowTagSets.get(rowId);
|
|
670
|
+
if (!rowTagSet) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
const activeConditions = rowActiveConditions.get(rowId);
|
|
674
|
+
if (activeConditions && disjunctPositions) {
|
|
675
|
+
activeConditions[pattern.pos] = false;
|
|
676
|
+
if (!rowVisible(activeConditions, disjunctPositions)) {
|
|
677
|
+
for (const tag of rowTagSet) {
|
|
678
|
+
const parsedTag = parseTag$1(tag);
|
|
679
|
+
removeTagFromIndex(parsedTag, rowId, tagIndex, tagLength);
|
|
680
|
+
tagCache.delete(tag);
|
|
681
|
+
}
|
|
682
|
+
rowTagSets.delete(rowId);
|
|
683
|
+
rowActiveConditions.delete(rowId);
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
for (const tag of rowTagSet) {
|
|
689
|
+
const parsedTag = parseTag$1(tag);
|
|
690
|
+
if (tagMatchesPattern(parsedTag, pattern)) {
|
|
691
|
+
rowTagSet.delete(tag);
|
|
692
|
+
removeTagFromIndex(parsedTag, rowId, tagIndex, tagLength);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (rowTagSet.size === 0) {
|
|
647
696
|
rowTagSets.delete(rowId);
|
|
648
|
-
rowActiveConditions.delete(rowId);
|
|
649
697
|
return true;
|
|
650
698
|
}
|
|
651
699
|
return false;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
700
|
+
};
|
|
701
|
+
const processMoveOutEvent = (patterns, begin, write, transactionStarted, onDelete) => {
|
|
702
|
+
if (tagLength === void 0) {
|
|
703
|
+
debug(
|
|
704
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received move-out message but no tag length set yet, ignoring`
|
|
705
|
+
);
|
|
706
|
+
return transactionStarted;
|
|
658
707
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
let txStarted = transactionStarted;
|
|
674
|
-
for (const pattern of patterns) {
|
|
675
|
-
const affectedRowIds = findRowsMatchingPattern(pattern, tagIndex);
|
|
676
|
-
for (const rowId of affectedRowIds) {
|
|
677
|
-
if (removeMatchingTagsFromRow(rowId, pattern)) {
|
|
678
|
-
if (!txStarted) {
|
|
679
|
-
begin();
|
|
680
|
-
txStarted = true;
|
|
708
|
+
let txStarted = transactionStarted;
|
|
709
|
+
for (const pattern of patterns) {
|
|
710
|
+
const affectedRowIds = findRowsMatchingPattern(pattern, tagIndex);
|
|
711
|
+
for (const rowId of affectedRowIds) {
|
|
712
|
+
if (removeMatchingTagsFromRow(rowId, pattern)) {
|
|
713
|
+
if (!txStarted) {
|
|
714
|
+
begin();
|
|
715
|
+
txStarted = true;
|
|
716
|
+
}
|
|
717
|
+
write({
|
|
718
|
+
type: `delete`,
|
|
719
|
+
key: rowId
|
|
720
|
+
});
|
|
721
|
+
onDelete(rowId);
|
|
681
722
|
}
|
|
682
|
-
write({
|
|
683
|
-
type: `delete`,
|
|
684
|
-
key: rowId
|
|
685
|
-
});
|
|
686
723
|
}
|
|
687
724
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
725
|
+
return txStarted;
|
|
726
|
+
};
|
|
727
|
+
const processMoveInEvent = (patterns) => {
|
|
728
|
+
if (tagLength === void 0) {
|
|
729
|
+
debug(
|
|
730
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received move-in message but no tag length set yet, ignoring`
|
|
731
|
+
);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (const pattern of patterns) {
|
|
735
|
+
const affectedRowIds = findRowsMatchingPattern(pattern, tagIndex);
|
|
736
|
+
for (const rowId of affectedRowIds) {
|
|
737
|
+
const activeConditions = rowActiveConditions.get(rowId);
|
|
738
|
+
if (activeConditions) {
|
|
739
|
+
activeConditions[pattern.pos] = true;
|
|
740
|
+
}
|
|
704
741
|
}
|
|
705
742
|
}
|
|
706
|
-
}
|
|
707
|
-
};
|
|
708
|
-
const getSyncMetadata = () => {
|
|
709
|
-
const schema = relationSchema.state || `public`;
|
|
743
|
+
};
|
|
710
744
|
return {
|
|
711
|
-
|
|
745
|
+
hasTags: () => tagLength !== void 0,
|
|
746
|
+
processTagsForChangeMessage,
|
|
747
|
+
clearTagTrackingState,
|
|
748
|
+
clearTagsForRow,
|
|
749
|
+
processMoveOutEvent,
|
|
750
|
+
processMoveInEvent
|
|
712
751
|
};
|
|
713
752
|
};
|
|
714
|
-
|
|
753
|
+
const collectionTags = /* @__PURE__ */ new WeakMap();
|
|
715
754
|
return {
|
|
755
|
+
getSyncMetadata: () => ({
|
|
756
|
+
relation: shapeOptions.params?.table ? [relationSchema || `public`, shapeOptions.params.table] : void 0
|
|
757
|
+
}),
|
|
716
758
|
sync: (params) => {
|
|
759
|
+
const retainsTagState = collectionTags.has(params.collection);
|
|
760
|
+
let tagState = collectionTags.get(params.collection);
|
|
761
|
+
if (!tagState) {
|
|
762
|
+
tagState = createTagState();
|
|
763
|
+
collectionTags.set(params.collection, tagState);
|
|
764
|
+
}
|
|
765
|
+
const {
|
|
766
|
+
processTagsForChangeMessage,
|
|
767
|
+
clearTagTrackingState,
|
|
768
|
+
clearTagsForRow,
|
|
769
|
+
processMoveOutEvent,
|
|
770
|
+
processMoveInEvent
|
|
771
|
+
} = tagState;
|
|
772
|
+
const lifecycle = getLifecycle(params.collection);
|
|
773
|
+
const lifecycleEpoch = lifecycle.start();
|
|
774
|
+
const isActiveLifecycle = () => lifecycle.isActive(lifecycleEpoch);
|
|
775
|
+
Object.assign(params.collection.utils, lifecycle.utils);
|
|
717
776
|
const {
|
|
718
777
|
begin,
|
|
719
778
|
write,
|
|
@@ -749,16 +808,35 @@ function createElectricSync(shapeOptions, options) {
|
|
|
749
808
|
const persistedResumeState2 = metadata?.collection.get(`electric:resume`);
|
|
750
809
|
return parseElectricResumeState(persistedResumeState2);
|
|
751
810
|
};
|
|
811
|
+
const persistence = metadata === void 0 ? null : validateSyncPersistenceCapability(metadata.persistence);
|
|
812
|
+
const hydrateBaseline = persistence?.hydrateBaseline;
|
|
813
|
+
const resumeSnapshot = persistence?.resumeSnapshot;
|
|
814
|
+
const certifyResumeSnapshot = resumeSnapshot?.certify;
|
|
815
|
+
const getKeySetEvidence = resumeSnapshot?.getKeySetEvidence;
|
|
816
|
+
const expectCurrentCommit = resumeSnapshot?.expectCurrentCommit;
|
|
817
|
+
const persistedKeySetEvidence = getKeySetEvidence?.();
|
|
752
818
|
const persistedResumeState = getNewestElectricResumeState(
|
|
753
819
|
readPersistedResumeState(),
|
|
754
|
-
|
|
820
|
+
lifecycle.resumeState
|
|
755
821
|
);
|
|
756
822
|
const shapeIdentity = getStableShapeIdentity({
|
|
757
823
|
url: shapeOptions.url,
|
|
758
824
|
params: shapeOptions.params
|
|
759
825
|
});
|
|
760
826
|
const hasIncompatiblePersistedResume = persistedResumeState?.kind === `resume` && persistedResumeState.shapeId !== shapeIdentity;
|
|
761
|
-
const
|
|
827
|
+
const lacksCompletePersistedKeySet = persistedResumeState?.kind === `resume` && getKeySetEvidence !== void 0 && persistedKeySetEvidence?.status !== `consistent`;
|
|
828
|
+
const needsFullSnapshot = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState !== void 0 && (persistedResumeState.kind === `reset` || lacksCompletePersistedKeySet || !retainsTagState && persistedResumeState.requiresTagState !== false);
|
|
829
|
+
const canUsePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && !hasIncompatiblePersistedResume && // Cached rows do not contain authoritative tag/active-condition state.
|
|
830
|
+
// Only a complete adapter ledger can justify a persisted cursor.
|
|
831
|
+
!needsFullSnapshot;
|
|
832
|
+
const hasExplicitResumeOffset = shapeOptions.offset !== void 0 && shapeOptions.offset !== `-1`;
|
|
833
|
+
if (!canUsePersistedResume && !hasExplicitResumeOffset) {
|
|
834
|
+
clearTagTrackingState();
|
|
835
|
+
}
|
|
836
|
+
const receivesCompleteRows = shapeOptions.params?.replica === `full`;
|
|
837
|
+
const requiresKeySetCertification = canUsePersistedResume && certifyResumeSnapshot !== void 0;
|
|
838
|
+
const requiresCompleteResume = syncMode !== `on-demand` && (canUsePersistedResume || hasExplicitResumeOffset && !receivesCompleteRows);
|
|
839
|
+
let freshSnapshotPending = (syncMode === `eager` || needsFullSnapshot) && !canUsePersistedResume && !hasExplicitResumeOffset && hydrateBaseline !== void 0;
|
|
762
840
|
let progressiveReadyGate = null;
|
|
763
841
|
let streamErrorVersion = 0;
|
|
764
842
|
const wrappedMarkReady = (isBuffering, expectedErrorVersion = streamErrorVersion) => {
|
|
@@ -785,21 +863,16 @@ function createElectricSync(shapeOptions, options) {
|
|
|
785
863
|
}
|
|
786
864
|
}
|
|
787
865
|
abortController.signal.addEventListener(`abort`, () => {
|
|
788
|
-
|
|
789
|
-
current.forEach((match) => {
|
|
790
|
-
clearTimeout(match.timeoutId);
|
|
791
|
-
match.reject(new StreamAbortedError());
|
|
792
|
-
});
|
|
793
|
-
return /* @__PURE__ */ new Map();
|
|
794
|
-
});
|
|
866
|
+
lifecycle.retire(lifecycleEpoch);
|
|
795
867
|
});
|
|
796
868
|
const stream = new ShapeStream({
|
|
797
869
|
...shapeOptions,
|
|
798
|
-
//
|
|
799
|
-
|
|
870
|
+
// Recovery needs a complete snapshot even for on-demand shapes.
|
|
871
|
+
// Normal on-demand startup still subscribes to changes only.
|
|
872
|
+
log: syncMode === `on-demand` && !needsFullSnapshot ? `changes_only` : void 0,
|
|
800
873
|
// In on-demand mode, we only need the changes from the point of time the collection was created
|
|
801
874
|
// so we default to `now` when there is no saved offset.
|
|
802
|
-
offset: shapeOptions.offset ?? (canUsePersistedResume ? persistedResumeState.offset : syncMode === `on-demand` ? `now` : void 0),
|
|
875
|
+
offset: shapeOptions.offset ?? (canUsePersistedResume ? persistedResumeState.offset : syncMode === `on-demand` && !needsFullSnapshot ? `now` : void 0),
|
|
803
876
|
handle: shapeOptions.handle ?? (canUsePersistedResume ? persistedResumeState.handle : void 0),
|
|
804
877
|
signal: abortController.signal,
|
|
805
878
|
onError: (errorParams) => {
|
|
@@ -825,11 +898,50 @@ function createElectricSync(shapeOptions, options) {
|
|
|
825
898
|
let transactionStarted = false;
|
|
826
899
|
const newTxids = /* @__PURE__ */ new Set();
|
|
827
900
|
const newSnapshots = [];
|
|
828
|
-
let hasReceivedUpToDate = syncMode === `progressive` &&
|
|
829
|
-
|
|
901
|
+
let hasReceivedUpToDate = syncMode === `progressive` && requiresCompleteResume;
|
|
902
|
+
let isResettingSnapshot = false;
|
|
903
|
+
let resetGeneration = 0;
|
|
904
|
+
const isBufferingInitialSync = () => syncMode === `progressive` && !hasReceivedUpToDate && !isResettingSnapshot;
|
|
830
905
|
const bufferedMessages = [];
|
|
906
|
+
const progressiveSnapshotRows = /* @__PURE__ */ new Map();
|
|
907
|
+
const pendingPresence = /* @__PURE__ */ new Map();
|
|
908
|
+
let usesBaseline = true;
|
|
909
|
+
let pendingPresenceRevision = 0;
|
|
910
|
+
const recordPendingPresence = (rowId, present) => {
|
|
911
|
+
pendingPresenceRevision++;
|
|
912
|
+
pendingPresence.set(rowId, present);
|
|
913
|
+
};
|
|
914
|
+
const recordPendingTruncate = () => {
|
|
915
|
+
pendingPresenceRevision++;
|
|
916
|
+
pendingPresence.clear();
|
|
917
|
+
progressiveSnapshotRows.clear();
|
|
918
|
+
usesBaseline = false;
|
|
919
|
+
};
|
|
920
|
+
const commitSourceTransaction = () => {
|
|
921
|
+
const committedPresenceRevision = pendingPresenceRevision;
|
|
922
|
+
const applied = commit();
|
|
923
|
+
if (metadata?.persistence) {
|
|
924
|
+
const retireAppliedPresence = () => {
|
|
925
|
+
if (pendingPresenceRevision === committedPresenceRevision) {
|
|
926
|
+
pendingPresence.clear();
|
|
927
|
+
usesBaseline = true;
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
if (applied === true) retireAppliedPresence();
|
|
931
|
+
else void applied.then(retireAppliedPresence, () => void 0);
|
|
932
|
+
}
|
|
933
|
+
return applied;
|
|
934
|
+
};
|
|
935
|
+
const beginSourceTransaction = () => {
|
|
936
|
+
if (metadata?.persistence) begin({ immediate: true });
|
|
937
|
+
else begin();
|
|
938
|
+
};
|
|
831
939
|
const syncedKeys = /* @__PURE__ */ new Set();
|
|
940
|
+
let resumeInvalid = false;
|
|
832
941
|
const stageResumeMetadata = () => {
|
|
942
|
+
if (!isActiveLifecycle() || resumeInvalid) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
833
945
|
const shapeHandle = stream.shapeHandle;
|
|
834
946
|
const lastOffset = stream.lastOffset;
|
|
835
947
|
if (!shapeHandle || lastOffset === `-1`) {
|
|
@@ -840,25 +952,29 @@ function createElectricSync(shapeOptions, options) {
|
|
|
840
952
|
offset: lastOffset,
|
|
841
953
|
handle: shapeHandle,
|
|
842
954
|
shapeId: shapeIdentity,
|
|
843
|
-
updatedAt: Date.now()
|
|
955
|
+
updatedAt: Date.now(),
|
|
956
|
+
requiresTagState: tagState.hasTags()
|
|
844
957
|
};
|
|
845
|
-
|
|
958
|
+
lifecycle.resumeState = resumeState;
|
|
846
959
|
metadata?.collection.set(`electric:resume`, resumeState);
|
|
847
960
|
};
|
|
848
|
-
const commitResetResumeMetadataImmediately = () => {
|
|
961
|
+
const commitResetResumeMetadataImmediately = (expectInResumeSnapshot = false) => {
|
|
849
962
|
const resetState = {
|
|
850
963
|
kind: `reset`,
|
|
851
964
|
updatedAt: Date.now()
|
|
852
965
|
};
|
|
853
|
-
|
|
966
|
+
lifecycle.resumeState = resetState;
|
|
854
967
|
if (metadata) {
|
|
855
968
|
begin({ immediate: true });
|
|
856
969
|
metadata.collection.set(`electric:resume`, resetState);
|
|
970
|
+
if (expectInResumeSnapshot) {
|
|
971
|
+
expectCurrentCommit?.();
|
|
972
|
+
}
|
|
857
973
|
commit();
|
|
858
974
|
}
|
|
859
975
|
};
|
|
860
|
-
if (hasIncompatiblePersistedResume) {
|
|
861
|
-
commitResetResumeMetadataImmediately();
|
|
976
|
+
if (hasIncompatiblePersistedResume || needsFullSnapshot && persistedResumeState.kind === `resume`) {
|
|
977
|
+
commitResetResumeMetadataImmediately(true);
|
|
862
978
|
}
|
|
863
979
|
const processChangeMessage = (changeMessage) => {
|
|
864
980
|
if (!isChangeMessage(changeMessage)) {
|
|
@@ -905,6 +1021,10 @@ function createElectricSync(shapeOptions, options) {
|
|
|
905
1021
|
commit,
|
|
906
1022
|
getCommitCursor: () => commitSequence,
|
|
907
1023
|
waitForCommitsAfter,
|
|
1024
|
+
recordSnapshotRow: (row, message) => {
|
|
1025
|
+
recordPendingPresence(collection.getKeyFromItem(row), true);
|
|
1026
|
+
progressiveSnapshotRows.set(collection.getKeyFromItem(row), message);
|
|
1027
|
+
},
|
|
908
1028
|
collectionId,
|
|
909
1029
|
// Pass the columnMapper's encode function to transform column names
|
|
910
1030
|
// (e.g., camelCase to snake_case) when compiling SQL for subset queries
|
|
@@ -912,48 +1032,126 @@ function createElectricSync(shapeOptions, options) {
|
|
|
912
1032
|
// Pass abort signal so requestSnapshot errors can be ignored during cleanup
|
|
913
1033
|
signal: abortController.signal
|
|
914
1034
|
});
|
|
915
|
-
|
|
1035
|
+
const resumeKeysPromise = requiresCompleteResume || freshSnapshotPending ? hydrateBaseline ? (async () => {
|
|
1036
|
+
await hydrateBaseline();
|
|
1037
|
+
const currentKeySetEvidence = getKeySetEvidence?.();
|
|
1038
|
+
if (canUsePersistedResume && currentKeySetEvidence?.status !== `consistent`) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
`Electric persisted resume baseline could not be certified during hydration`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
})() : void 0 : requiresKeySetCertification ? (async () => {
|
|
1044
|
+
await certifyResumeSnapshot();
|
|
1045
|
+
const currentKeySetEvidence = getKeySetEvidence?.();
|
|
1046
|
+
if (currentKeySetEvidence?.status !== `consistent`) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`Electric persisted resume baseline could not be certified`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
})() : void 0;
|
|
1052
|
+
let areResumeKeysReady = !resumeKeysPromise;
|
|
1053
|
+
const pendingResumeBatches = [];
|
|
1054
|
+
let unsubscribeStream = () => {
|
|
1055
|
+
};
|
|
1056
|
+
const invalidateResume = () => {
|
|
1057
|
+
resumeInvalid = true;
|
|
1058
|
+
if (transactionStarted) {
|
|
1059
|
+
const cancellation = new AbortController();
|
|
1060
|
+
cancellation.abort();
|
|
1061
|
+
commit(cancellation.signal);
|
|
1062
|
+
transactionStarted = false;
|
|
1063
|
+
}
|
|
1064
|
+
syncedKeys.clear();
|
|
1065
|
+
newTxids.clear();
|
|
1066
|
+
newSnapshots.length = 0;
|
|
1067
|
+
commitResetResumeMetadataImmediately();
|
|
1068
|
+
streamErrorVersion++;
|
|
1069
|
+
unsubscribeStream();
|
|
1070
|
+
abortController.abort();
|
|
1071
|
+
markError(
|
|
1072
|
+
new Error(
|
|
1073
|
+
`Electric resume state referenced an unseen row; a full snapshot is required`
|
|
1074
|
+
)
|
|
1075
|
+
);
|
|
1076
|
+
};
|
|
1077
|
+
const processMessages = (messages) => {
|
|
1078
|
+
if (!isActiveLifecycle() || resumeInvalid) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (freshSnapshotPending) {
|
|
1082
|
+
freshSnapshotPending = false;
|
|
1083
|
+
beginSourceTransaction();
|
|
1084
|
+
transactionStarted = true;
|
|
1085
|
+
truncate();
|
|
1086
|
+
recordPendingTruncate();
|
|
1087
|
+
syncedKeys.clear();
|
|
1088
|
+
clearTagTrackingState();
|
|
1089
|
+
isResettingSnapshot = true;
|
|
1090
|
+
resetGeneration++;
|
|
1091
|
+
}
|
|
1092
|
+
if (!metadata?.persistence && !transactionStarted) {
|
|
1093
|
+
pendingPresence.clear();
|
|
1094
|
+
usesBaseline = true;
|
|
1095
|
+
for (const pending of collection._state.pendingSyncedTransactions) {
|
|
1096
|
+
if (pending.truncate) {
|
|
1097
|
+
pendingPresence.clear();
|
|
1098
|
+
usesBaseline = false;
|
|
1099
|
+
}
|
|
1100
|
+
for (const operation of pending.operations) {
|
|
1101
|
+
pendingPresence.set(operation.key, operation.type !== `delete`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
for (const message of bufferedMessages) {
|
|
1105
|
+
if (isChangeMessage(message)) {
|
|
1106
|
+
pendingPresence.set(
|
|
1107
|
+
collection.getKeyFromItem(message.value),
|
|
1108
|
+
message.headers.operation !== `delete`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
916
1113
|
let commitPoint = null;
|
|
917
|
-
|
|
1114
|
+
lifecycle.beginMatchGeneration(messages);
|
|
918
1115
|
for (const message of messages) {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const newBuffer = [...currentBuffer, message];
|
|
922
|
-
if (newBuffer.length > MAX_BATCH_MESSAGES) {
|
|
923
|
-
newBuffer.splice(0, newBuffer.length - MAX_BATCH_MESSAGES);
|
|
924
|
-
}
|
|
925
|
-
return newBuffer;
|
|
926
|
-
});
|
|
927
|
-
}
|
|
1116
|
+
lifecycle.observeMatchMessage(message);
|
|
1117
|
+
if (!isActiveLifecycle()) return;
|
|
928
1118
|
if (hasTxids(message) && (!isBufferingInitialSync() || transactionStarted)) {
|
|
929
1119
|
message.headers.txids?.forEach((txid) => newTxids.add(txid));
|
|
930
1120
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
err instanceof Error ? err : new Error(String(err))
|
|
940
|
-
);
|
|
941
|
-
matchesToRemove.push(matchId);
|
|
942
|
-
debug(`matchFn error: %o`, err);
|
|
1121
|
+
if (isChangeMessage(message)) {
|
|
1122
|
+
const rowId = collection.getKeyFromItem(message.value);
|
|
1123
|
+
const operation = message.headers.operation;
|
|
1124
|
+
const hasKnownRow = pendingPresence.get(rowId) ?? (usesBaseline && collection._state.syncedData.has(rowId));
|
|
1125
|
+
if (operation === `update` && !hasKnownRow) {
|
|
1126
|
+
if (requiresCompleteResume && !isResettingSnapshot) {
|
|
1127
|
+
invalidateResume();
|
|
1128
|
+
return;
|
|
943
1129
|
}
|
|
1130
|
+
if (!receivesCompleteRows) continue;
|
|
944
1131
|
}
|
|
945
|
-
|
|
946
|
-
|
|
1132
|
+
recordPendingPresence(rowId, operation !== `delete`);
|
|
1133
|
+
if (operation !== `update`) {
|
|
1134
|
+
progressiveSnapshotRows.delete(rowId);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
947
1137
|
if (isChangeMessage(message)) {
|
|
948
1138
|
const schema = message.headers.schema;
|
|
949
1139
|
if (schema && typeof schema === `string`) {
|
|
950
|
-
relationSchema
|
|
1140
|
+
relationSchema = schema;
|
|
951
1141
|
}
|
|
952
1142
|
if (isBufferingInitialSync() && !transactionStarted) {
|
|
1143
|
+
if (message.headers.operation === `update`) {
|
|
1144
|
+
const rowId = collection.getKeyFromItem(message.value);
|
|
1145
|
+
const snapshotRow = progressiveSnapshotRows.get(rowId);
|
|
1146
|
+
if (snapshotRow) {
|
|
1147
|
+
bufferedMessages.push(snapshotRow);
|
|
1148
|
+
progressiveSnapshotRows.delete(rowId);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
953
1151
|
bufferedMessages.push(message);
|
|
954
1152
|
} else {
|
|
955
1153
|
if (!transactionStarted) {
|
|
956
|
-
|
|
1154
|
+
beginSourceTransaction();
|
|
957
1155
|
transactionStarted = true;
|
|
958
1156
|
}
|
|
959
1157
|
processChangeMessage(message);
|
|
@@ -974,9 +1172,13 @@ function createElectricSync(shapeOptions, options) {
|
|
|
974
1172
|
} else {
|
|
975
1173
|
transactionStarted = processMoveOutEvent(
|
|
976
1174
|
message.headers.patterns,
|
|
977
|
-
|
|
1175
|
+
beginSourceTransaction,
|
|
978
1176
|
write,
|
|
979
|
-
transactionStarted
|
|
1177
|
+
transactionStarted,
|
|
1178
|
+
(rowId) => {
|
|
1179
|
+
recordPendingPresence(rowId, false);
|
|
1180
|
+
syncedKeys.delete(rowId);
|
|
1181
|
+
}
|
|
980
1182
|
);
|
|
981
1183
|
}
|
|
982
1184
|
} else if (isMoveInMessage(message)) {
|
|
@@ -991,26 +1193,34 @@ function createElectricSync(shapeOptions, options) {
|
|
|
991
1193
|
);
|
|
992
1194
|
commitResetResumeMetadataImmediately();
|
|
993
1195
|
if (!transactionStarted) {
|
|
994
|
-
|
|
1196
|
+
beginSourceTransaction();
|
|
995
1197
|
transactionStarted = true;
|
|
996
1198
|
}
|
|
997
1199
|
truncate();
|
|
998
1200
|
clearTagTrackingState();
|
|
999
1201
|
syncedKeys.clear();
|
|
1202
|
+
recordPendingTruncate();
|
|
1203
|
+
isResettingSnapshot = true;
|
|
1204
|
+
resetGeneration++;
|
|
1000
1205
|
loadSubsetDedupe?.reset();
|
|
1001
1206
|
commitPoint = null;
|
|
1002
1207
|
hasReceivedUpToDate = false;
|
|
1003
1208
|
bufferedMessages.length = 0;
|
|
1209
|
+
progressiveSnapshotRows.clear();
|
|
1004
1210
|
}
|
|
1005
1211
|
}
|
|
1212
|
+
if (needsFullSnapshot && isResettingSnapshot && commitPoint === `subset-end`)
|
|
1213
|
+
return;
|
|
1006
1214
|
if (commitPoint !== null) {
|
|
1007
1215
|
let applied = true;
|
|
1008
1216
|
const wasBufferingInitialSync = isBufferingInitialSync();
|
|
1217
|
+
const finishesReset = isResettingSnapshot && commitPoint === `up-to-date`;
|
|
1218
|
+
const finishingResetGeneration = resetGeneration;
|
|
1009
1219
|
if (isBufferingInitialSync() && commitPoint === `up-to-date` && !transactionStarted) {
|
|
1010
1220
|
debug(
|
|
1011
1221
|
`${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`
|
|
1012
1222
|
);
|
|
1013
|
-
|
|
1223
|
+
beginSourceTransaction();
|
|
1014
1224
|
truncate();
|
|
1015
1225
|
clearTagTrackingState();
|
|
1016
1226
|
syncedKeys.clear();
|
|
@@ -1029,27 +1239,36 @@ function createElectricSync(shapeOptions, options) {
|
|
|
1029
1239
|
bufferedMsg.headers.patterns,
|
|
1030
1240
|
begin,
|
|
1031
1241
|
write,
|
|
1032
|
-
|
|
1242
|
+
// The swap already opened a transaction, even though the
|
|
1243
|
+
// normal-stream transactionStarted flag is still false.
|
|
1244
|
+
true,
|
|
1245
|
+
(rowId) => {
|
|
1246
|
+
recordPendingPresence(rowId, false);
|
|
1247
|
+
syncedKeys.delete(rowId);
|
|
1248
|
+
}
|
|
1033
1249
|
);
|
|
1034
1250
|
} else if (isMoveInMessage(bufferedMsg)) {
|
|
1035
1251
|
processMoveInEvent(bufferedMsg.headers.patterns);
|
|
1036
1252
|
}
|
|
1037
1253
|
}
|
|
1038
1254
|
stageResumeMetadata();
|
|
1039
|
-
applied =
|
|
1255
|
+
applied = commitSourceTransaction();
|
|
1040
1256
|
bufferedMessages.length = 0;
|
|
1257
|
+
progressiveSnapshotRows.clear();
|
|
1041
1258
|
debug(
|
|
1042
1259
|
`${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`
|
|
1043
1260
|
);
|
|
1044
1261
|
} else {
|
|
1045
1262
|
if (transactionStarted) {
|
|
1046
|
-
|
|
1047
|
-
|
|
1263
|
+
if (!isResettingSnapshot || finishesReset) {
|
|
1264
|
+
stageResumeMetadata();
|
|
1265
|
+
}
|
|
1266
|
+
applied = commitSourceTransaction();
|
|
1048
1267
|
transactionStarted = false;
|
|
1049
1268
|
} else if (commitPoint === `up-to-date` && metadata) {
|
|
1050
|
-
|
|
1269
|
+
beginSourceTransaction();
|
|
1051
1270
|
stageResumeMetadata();
|
|
1052
|
-
applied =
|
|
1271
|
+
applied = commitSourceTransaction();
|
|
1053
1272
|
}
|
|
1054
1273
|
}
|
|
1055
1274
|
const readyErrorVersion = streamErrorVersion;
|
|
@@ -1058,39 +1277,77 @@ function createElectricSync(shapeOptions, options) {
|
|
|
1058
1277
|
} else {
|
|
1059
1278
|
void applied.then(
|
|
1060
1279
|
() => wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion),
|
|
1061
|
-
() =>
|
|
1280
|
+
(error) => {
|
|
1281
|
+
if (!isActiveLifecycle() || abortController.signal.aborted) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
streamErrorVersion++;
|
|
1285
|
+
unsubscribeStream();
|
|
1286
|
+
abortController.abort();
|
|
1287
|
+
if (collection.status !== `error`) markError(error);
|
|
1288
|
+
}
|
|
1062
1289
|
);
|
|
1063
1290
|
}
|
|
1291
|
+
if (finishesReset) {
|
|
1292
|
+
const finishReset = () => {
|
|
1293
|
+
if (resetGeneration === finishingResetGeneration) {
|
|
1294
|
+
isResettingSnapshot = false;
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
if (applied === true) {
|
|
1298
|
+
finishReset();
|
|
1299
|
+
} else {
|
|
1300
|
+
void applied.then(finishReset, () => void 0);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1064
1303
|
if (commitPoint === `up-to-date`) {
|
|
1065
1304
|
hasReceivedUpToDate = true;
|
|
1066
1305
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
`${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
|
|
1072
|
-
Array.from(newTxids)
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
newTxids.forEach((txid) => clonedSeen.add(txid));
|
|
1076
|
-
newTxids.clear();
|
|
1077
|
-
return clonedSeen;
|
|
1078
|
-
});
|
|
1079
|
-
seenSnapshots.setState((currentSnapshots) => {
|
|
1080
|
-
const seen = [...currentSnapshots, ...newSnapshots];
|
|
1081
|
-
newSnapshots.forEach(
|
|
1082
|
-
(snapshot) => debug(
|
|
1083
|
-
`${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
|
|
1084
|
-
snapshot
|
|
1085
|
-
)
|
|
1306
|
+
if (newTxids.size > 0) {
|
|
1307
|
+
debug(
|
|
1308
|
+
`${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
|
|
1309
|
+
Array.from(newTxids)
|
|
1086
1310
|
);
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1311
|
+
}
|
|
1312
|
+
newSnapshots.forEach(
|
|
1313
|
+
(snapshot) => debug(
|
|
1314
|
+
`${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
|
|
1315
|
+
snapshot
|
|
1316
|
+
)
|
|
1317
|
+
);
|
|
1318
|
+
lifecycle.publishEvidence(newTxids, newSnapshots);
|
|
1319
|
+
newTxids.clear();
|
|
1320
|
+
newSnapshots.length = 0;
|
|
1321
|
+
lifecycle.commitMatches();
|
|
1092
1322
|
}
|
|
1323
|
+
};
|
|
1324
|
+
unsubscribeStream = stream.subscribe((messages) => {
|
|
1325
|
+
if (!areResumeKeysReady) {
|
|
1326
|
+
pendingResumeBatches.push([...messages]);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
processMessages(messages);
|
|
1093
1330
|
});
|
|
1331
|
+
if (!areResumeKeysReady && resumeKeysPromise) {
|
|
1332
|
+
void resumeKeysPromise.then(
|
|
1333
|
+
() => {
|
|
1334
|
+
if (abortController.signal.aborted) return;
|
|
1335
|
+
areResumeKeysReady = true;
|
|
1336
|
+
const queuedBatches = pendingResumeBatches.splice(0);
|
|
1337
|
+
queuedBatches.forEach(processMessages);
|
|
1338
|
+
},
|
|
1339
|
+
(error) => {
|
|
1340
|
+
if (abortController.signal.aborted) return;
|
|
1341
|
+
pendingResumeBatches.length = 0;
|
|
1342
|
+
resumeInvalid = true;
|
|
1343
|
+
commitResetResumeMetadataImmediately();
|
|
1344
|
+
streamErrorVersion++;
|
|
1345
|
+
unsubscribeStream();
|
|
1346
|
+
abortController.abort();
|
|
1347
|
+
markError(error);
|
|
1348
|
+
}
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1094
1351
|
return {
|
|
1095
1352
|
loadSubset: loadSubsetDedupe?.loadSubset,
|
|
1096
1353
|
cleanup: () => {
|
|
@@ -1100,13 +1357,12 @@ function createElectricSync(shapeOptions, options) {
|
|
|
1100
1357
|
);
|
|
1101
1358
|
unsubscribeStream();
|
|
1102
1359
|
abortController.abort();
|
|
1360
|
+
pendingResumeBatches.length = 0;
|
|
1103
1361
|
loadSubsetDedupe?.reset();
|
|
1104
|
-
|
|
1362
|
+
lifecycle.retire(lifecycleEpoch);
|
|
1105
1363
|
}
|
|
1106
1364
|
};
|
|
1107
|
-
}
|
|
1108
|
-
// Expose the getSyncMetadata function
|
|
1109
|
-
getSyncMetadata
|
|
1365
|
+
}
|
|
1110
1366
|
};
|
|
1111
1367
|
}
|
|
1112
1368
|
export {
|