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