@syncular/client 0.0.6-219 → 0.0.6-221
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/client.d.ts +12 -20
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +20 -5
- package/dist/client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +4 -3
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +84 -14
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +53 -2
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/pull-engine.d.ts +6 -2
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +564 -220
- package/dist/pull-engine.js.map +1 -1
- package/dist/sync-loop.d.ts +5 -3
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +30 -0
- package/dist/sync-loop.js.map +1 -1
- package/dist/sync.d.ts +4 -3
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +1 -0
- package/dist/sync.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +35 -25
- package/src/engine/SyncEngine.test.ts +64 -0
- package/src/engine/SyncEngine.ts +117 -24
- package/src/engine/types.ts +70 -1
- package/src/pull-engine.test.ts +118 -0
- package/src/pull-engine.ts +679 -261
- package/src/sync-loop.ts +52 -3
- package/src/sync.ts +6 -9
package/dist/pull-engine.js
CHANGED
|
@@ -243,59 +243,119 @@ async function readAllBytesFromStream(stream) {
|
|
|
243
243
|
}
|
|
244
244
|
return bytes;
|
|
245
245
|
}
|
|
246
|
-
async function materializeSnapshotChunkRows(transport, request, expectedHash, sha256Override) {
|
|
246
|
+
async function materializeSnapshotChunkRows(transport, request, expectedHash, sha256Override, trace) {
|
|
247
|
+
emitTrace(trace?.onTrace, {
|
|
248
|
+
stage: 'apply:chunk-materialize:start',
|
|
249
|
+
stateId: trace?.stateId,
|
|
250
|
+
subscriptionId: trace?.subscriptionId,
|
|
251
|
+
table: trace?.table,
|
|
252
|
+
chunkId: request.chunkId,
|
|
253
|
+
chunkIndex: trace?.chunkIndex,
|
|
254
|
+
});
|
|
255
|
+
const startedAt = Date.now();
|
|
247
256
|
if (transport.capabilities?.snapshotChunkReadMode === 'bytes' &&
|
|
248
257
|
transport.fetchSnapshotChunk) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
try {
|
|
259
|
+
let bytes = await transport.fetchSnapshotChunk(request);
|
|
260
|
+
if (isGzipBytes(bytes)) {
|
|
261
|
+
bytes = await gunzipBytes(bytes);
|
|
262
|
+
}
|
|
263
|
+
if (expectedHash) {
|
|
264
|
+
const actualHash = await computeSha256Hex(bytes, sha256Override);
|
|
265
|
+
if (actualHash !== expectedHash) {
|
|
266
|
+
throw new Error(`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}`);
|
|
267
|
+
}
|
|
257
268
|
}
|
|
269
|
+
const rows = decodeSnapshotRows(bytes);
|
|
270
|
+
emitTrace(trace?.onTrace, {
|
|
271
|
+
stage: 'apply:chunk-materialize:complete',
|
|
272
|
+
stateId: trace?.stateId,
|
|
273
|
+
subscriptionId: trace?.subscriptionId,
|
|
274
|
+
table: trace?.table,
|
|
275
|
+
chunkId: request.chunkId,
|
|
276
|
+
chunkIndex: trace?.chunkIndex,
|
|
277
|
+
rowCount: rows.length,
|
|
278
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
279
|
+
});
|
|
280
|
+
return rows;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
emitTrace(trace?.onTrace, {
|
|
284
|
+
stage: 'apply:chunk-materialize:error',
|
|
285
|
+
stateId: trace?.stateId,
|
|
286
|
+
subscriptionId: trace?.subscriptionId,
|
|
287
|
+
table: trace?.table,
|
|
288
|
+
chunkId: request.chunkId,
|
|
289
|
+
chunkIndex: trace?.chunkIndex,
|
|
290
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
291
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
292
|
+
});
|
|
293
|
+
throw error;
|
|
258
294
|
}
|
|
259
|
-
return decodeSnapshotRows(bytes);
|
|
260
|
-
}
|
|
261
|
-
const rawStream = await fetchSnapshotChunkStream(transport, request);
|
|
262
|
-
const decodedStream = await maybeGunzipStream(rawStream);
|
|
263
|
-
let streamForDecode = decodedStream;
|
|
264
|
-
let chunkHashPromise = null;
|
|
265
|
-
if (expectedHash) {
|
|
266
|
-
const [hashStream, decodeStream] = decodedStream.tee();
|
|
267
|
-
streamForDecode = decodeStream;
|
|
268
|
-
chunkHashPromise = readAllBytesFromStream(hashStream).then((bytes) => computeSha256Hex(bytes, sha256Override));
|
|
269
295
|
}
|
|
270
|
-
const rows = [];
|
|
271
|
-
let materializeError = null;
|
|
272
296
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
297
|
+
const rawStream = await fetchSnapshotChunkStream(transport, request);
|
|
298
|
+
const decodedStream = await maybeGunzipStream(rawStream);
|
|
299
|
+
let streamForDecode = decodedStream;
|
|
300
|
+
let chunkHashPromise = null;
|
|
301
|
+
if (expectedHash) {
|
|
302
|
+
const [hashStream, decodeStream] = decodedStream.tee();
|
|
303
|
+
streamForDecode = decodeStream;
|
|
304
|
+
chunkHashPromise = readAllBytesFromStream(hashStream).then((bytes) => computeSha256Hex(bytes, sha256Override));
|
|
275
305
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
materializeError = error;
|
|
279
|
-
}
|
|
280
|
-
if (chunkHashPromise) {
|
|
306
|
+
const rows = [];
|
|
307
|
+
let materializeError = null;
|
|
281
308
|
try {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
materializeError = new Error(`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}`);
|
|
309
|
+
for await (const batch of decodeSnapshotRowStreamBatches(streamForDecode, SNAPSHOT_APPLY_BATCH_ROWS)) {
|
|
310
|
+
rows.push(...batch);
|
|
285
311
|
}
|
|
286
312
|
}
|
|
287
|
-
catch (
|
|
288
|
-
|
|
289
|
-
|
|
313
|
+
catch (error) {
|
|
314
|
+
materializeError = error;
|
|
315
|
+
}
|
|
316
|
+
if (chunkHashPromise) {
|
|
317
|
+
try {
|
|
318
|
+
const actualHash = await chunkHashPromise;
|
|
319
|
+
if (!materializeError && actualHash !== expectedHash) {
|
|
320
|
+
materializeError = new Error(`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}`);
|
|
321
|
+
}
|
|
290
322
|
}
|
|
323
|
+
catch (hashError) {
|
|
324
|
+
if (!materializeError) {
|
|
325
|
+
materializeError = hashError;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (materializeError) {
|
|
330
|
+
throw materializeError;
|
|
291
331
|
}
|
|
332
|
+
emitTrace(trace?.onTrace, {
|
|
333
|
+
stage: 'apply:chunk-materialize:complete',
|
|
334
|
+
stateId: trace?.stateId,
|
|
335
|
+
subscriptionId: trace?.subscriptionId,
|
|
336
|
+
table: trace?.table,
|
|
337
|
+
chunkId: request.chunkId,
|
|
338
|
+
chunkIndex: trace?.chunkIndex,
|
|
339
|
+
rowCount: rows.length,
|
|
340
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
341
|
+
});
|
|
342
|
+
return rows;
|
|
292
343
|
}
|
|
293
|
-
|
|
294
|
-
|
|
344
|
+
catch (error) {
|
|
345
|
+
emitTrace(trace?.onTrace, {
|
|
346
|
+
stage: 'apply:chunk-materialize:error',
|
|
347
|
+
stateId: trace?.stateId,
|
|
348
|
+
subscriptionId: trace?.subscriptionId,
|
|
349
|
+
table: trace?.table,
|
|
350
|
+
chunkId: request.chunkId,
|
|
351
|
+
chunkIndex: trace?.chunkIndex,
|
|
352
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
353
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
354
|
+
});
|
|
355
|
+
throw error;
|
|
295
356
|
}
|
|
296
|
-
return rows;
|
|
297
357
|
}
|
|
298
|
-
async function materializeChunkedSnapshots(transport, response, sha256Override) {
|
|
358
|
+
async function materializeChunkedSnapshots(transport, response, sha256Override, trace) {
|
|
299
359
|
const subscriptions = [];
|
|
300
360
|
for (const sub of response.subscriptions) {
|
|
301
361
|
if (!sub.bootstrap || !sub.snapshots || sub.snapshots.length === 0) {
|
|
@@ -310,11 +370,20 @@ async function materializeChunkedSnapshots(transport, response, sha256Override)
|
|
|
310
370
|
continue;
|
|
311
371
|
}
|
|
312
372
|
const rows = [];
|
|
313
|
-
for (
|
|
373
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
|
374
|
+
const chunk = chunks[chunkIndex];
|
|
375
|
+
if (!chunk)
|
|
376
|
+
continue;
|
|
314
377
|
const chunkRows = await materializeSnapshotChunkRows(transport, {
|
|
315
378
|
chunkId: chunk.id,
|
|
316
379
|
scopeValues: sub.scopes,
|
|
317
|
-
}, chunk.sha256, sha256Override
|
|
380
|
+
}, chunk.sha256, sha256Override, {
|
|
381
|
+
stateId: trace?.stateId ?? 'default',
|
|
382
|
+
subscriptionId: sub.id,
|
|
383
|
+
table: snapshot.table,
|
|
384
|
+
chunkIndex,
|
|
385
|
+
onTrace: trace?.onTrace,
|
|
386
|
+
});
|
|
318
387
|
rows.push(...chunkRows);
|
|
319
388
|
}
|
|
320
389
|
snapshots.push({
|
|
@@ -330,7 +399,7 @@ async function materializeChunkedSnapshots(transport, response, sha256Override)
|
|
|
330
399
|
}
|
|
331
400
|
return { ...response, subscriptions };
|
|
332
401
|
}
|
|
333
|
-
async function applyChunkedSnapshot(transport, handler, trx, snapshot, scopeValues, sha256Override) {
|
|
402
|
+
async function applyChunkedSnapshot(transport, handler, trx, snapshot, scopeValues, sha256Override, trace) {
|
|
334
403
|
const chunks = snapshot.chunks ?? [];
|
|
335
404
|
if (chunks.length === 0) {
|
|
336
405
|
await handler.applySnapshot({ trx }, snapshot);
|
|
@@ -341,69 +410,105 @@ async function applyChunkedSnapshot(transport, handler, trx, snapshot, scopeValu
|
|
|
341
410
|
const chunk = chunks[chunkIndex];
|
|
342
411
|
if (!chunk)
|
|
343
412
|
continue;
|
|
344
|
-
|
|
413
|
+
emitTrace(trace?.onTrace, {
|
|
414
|
+
stage: 'apply:chunk-materialize:start',
|
|
415
|
+
stateId: trace?.stateId,
|
|
416
|
+
subscriptionId: trace?.subscriptionId,
|
|
417
|
+
table: snapshot.table,
|
|
345
418
|
chunkId: chunk.id,
|
|
346
|
-
|
|
419
|
+
chunkIndex,
|
|
347
420
|
});
|
|
348
|
-
const
|
|
349
|
-
let streamForDecode = decodedStream;
|
|
350
|
-
let chunkHashPromise = null;
|
|
351
|
-
if (chunk.sha256) {
|
|
352
|
-
const [hashStream, decodeStream] = decodedStream.tee();
|
|
353
|
-
streamForDecode = decodeStream;
|
|
354
|
-
chunkHashPromise = readAllBytesFromStream(hashStream).then((bytes) => computeSha256Hex(bytes, sha256Override));
|
|
355
|
-
}
|
|
356
|
-
const rowBatchIterator = decodeSnapshotRowStreamBatches(streamForDecode, SNAPSHOT_APPLY_BATCH_ROWS);
|
|
357
|
-
let pendingBatch = null;
|
|
358
|
-
let applyError = null;
|
|
421
|
+
const chunkStartedAt = Date.now();
|
|
359
422
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
423
|
+
const rawStream = await fetchSnapshotChunkStream(transport, {
|
|
424
|
+
chunkId: chunk.id,
|
|
425
|
+
scopeValues,
|
|
426
|
+
});
|
|
427
|
+
const decodedStream = await maybeGunzipStream(rawStream);
|
|
428
|
+
let streamForDecode = decodedStream;
|
|
429
|
+
let chunkHashPromise = null;
|
|
430
|
+
if (chunk.sha256) {
|
|
431
|
+
const [hashStream, decodeStream] = decodedStream.tee();
|
|
432
|
+
streamForDecode = decodeStream;
|
|
433
|
+
chunkHashPromise = readAllBytesFromStream(hashStream).then((bytes) => computeSha256Hex(bytes, sha256Override));
|
|
434
|
+
}
|
|
435
|
+
const rowBatchIterator = decodeSnapshotRowStreamBatches(streamForDecode, SNAPSHOT_APPLY_BATCH_ROWS);
|
|
436
|
+
let pendingBatch = null;
|
|
437
|
+
let applyError = null;
|
|
438
|
+
let chunkRowCount = 0;
|
|
439
|
+
try {
|
|
440
|
+
// eslint-disable-next-line no-await-in-loop
|
|
441
|
+
for await (const batch of rowBatchIterator) {
|
|
442
|
+
chunkRowCount += batch.length;
|
|
443
|
+
if (pendingBatch) {
|
|
444
|
+
// eslint-disable-next-line no-await-in-loop
|
|
445
|
+
await handler.applySnapshot({ trx }, {
|
|
446
|
+
...snapshot,
|
|
447
|
+
rows: pendingBatch,
|
|
448
|
+
chunks: undefined,
|
|
449
|
+
isFirstPage: nextIsFirstPage,
|
|
450
|
+
isLastPage: false,
|
|
451
|
+
});
|
|
452
|
+
nextIsFirstPage = false;
|
|
453
|
+
}
|
|
454
|
+
pendingBatch = batch;
|
|
455
|
+
}
|
|
362
456
|
if (pendingBatch) {
|
|
457
|
+
const isLastChunk = chunkIndex === chunks.length - 1;
|
|
363
458
|
// eslint-disable-next-line no-await-in-loop
|
|
364
459
|
await handler.applySnapshot({ trx }, {
|
|
365
460
|
...snapshot,
|
|
366
461
|
rows: pendingBatch,
|
|
367
462
|
chunks: undefined,
|
|
368
463
|
isFirstPage: nextIsFirstPage,
|
|
369
|
-
isLastPage: false,
|
|
464
|
+
isLastPage: isLastChunk ? snapshot.isLastPage : false,
|
|
370
465
|
});
|
|
371
466
|
nextIsFirstPage = false;
|
|
372
467
|
}
|
|
373
|
-
pendingBatch = batch;
|
|
374
468
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// eslint-disable-next-line no-await-in-loop
|
|
378
|
-
await handler.applySnapshot({ trx }, {
|
|
379
|
-
...snapshot,
|
|
380
|
-
rows: pendingBatch,
|
|
381
|
-
chunks: undefined,
|
|
382
|
-
isFirstPage: nextIsFirstPage,
|
|
383
|
-
isLastPage: isLastChunk ? snapshot.isLastPage : false,
|
|
384
|
-
});
|
|
385
|
-
nextIsFirstPage = false;
|
|
469
|
+
catch (error) {
|
|
470
|
+
applyError = error;
|
|
386
471
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const actualHash = await chunkHashPromise;
|
|
395
|
-
if (!applyError && actualHash !== chunk.sha256) {
|
|
396
|
-
applyError = new Error(`Snapshot chunk integrity check failed: expected sha256 ${chunk.sha256}, got ${actualHash}`);
|
|
472
|
+
if (chunkHashPromise) {
|
|
473
|
+
try {
|
|
474
|
+
// eslint-disable-next-line no-await-in-loop
|
|
475
|
+
const actualHash = await chunkHashPromise;
|
|
476
|
+
if (!applyError && actualHash !== chunk.sha256) {
|
|
477
|
+
applyError = new Error(`Snapshot chunk integrity check failed: expected sha256 ${chunk.sha256}, got ${actualHash}`);
|
|
478
|
+
}
|
|
397
479
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
480
|
+
catch (hashError) {
|
|
481
|
+
if (!applyError) {
|
|
482
|
+
applyError = hashError;
|
|
483
|
+
}
|
|
402
484
|
}
|
|
403
485
|
}
|
|
486
|
+
if (applyError) {
|
|
487
|
+
throw applyError;
|
|
488
|
+
}
|
|
489
|
+
emitTrace(trace?.onTrace, {
|
|
490
|
+
stage: 'apply:chunk-materialize:complete',
|
|
491
|
+
stateId: trace?.stateId,
|
|
492
|
+
subscriptionId: trace?.subscriptionId,
|
|
493
|
+
table: snapshot.table,
|
|
494
|
+
chunkId: chunk.id,
|
|
495
|
+
chunkIndex,
|
|
496
|
+
rowCount: chunkRowCount,
|
|
497
|
+
durationMs: Math.max(0, Date.now() - chunkStartedAt),
|
|
498
|
+
});
|
|
404
499
|
}
|
|
405
|
-
|
|
406
|
-
|
|
500
|
+
catch (error) {
|
|
501
|
+
emitTrace(trace?.onTrace, {
|
|
502
|
+
stage: 'apply:chunk-materialize:error',
|
|
503
|
+
stateId: trace?.stateId,
|
|
504
|
+
subscriptionId: trace?.subscriptionId,
|
|
505
|
+
table: snapshot.table,
|
|
506
|
+
chunkId: chunk.id,
|
|
507
|
+
chunkIndex,
|
|
508
|
+
durationMs: Math.max(0, Date.now() - chunkStartedAt),
|
|
509
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
510
|
+
});
|
|
511
|
+
throw error;
|
|
407
512
|
}
|
|
408
513
|
}
|
|
409
514
|
}
|
|
@@ -431,6 +536,50 @@ function parseBootstrapState(value) {
|
|
|
431
536
|
return null;
|
|
432
537
|
}
|
|
433
538
|
}
|
|
539
|
+
function normalizeBootstrapPhase(value) {
|
|
540
|
+
if (value === undefined)
|
|
541
|
+
return 0;
|
|
542
|
+
return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
543
|
+
}
|
|
544
|
+
function isSubscriptionReady(row) {
|
|
545
|
+
return (row?.status === 'active' &&
|
|
546
|
+
parseBootstrapState(row.bootstrap_state_json) === null &&
|
|
547
|
+
row.cursor >= 0);
|
|
548
|
+
}
|
|
549
|
+
function isSubscriptionBootstrapping(row) {
|
|
550
|
+
return (row?.status === 'active' &&
|
|
551
|
+
parseBootstrapState(row.bootstrap_state_json) !== null);
|
|
552
|
+
}
|
|
553
|
+
function resolveActiveBootstrapPhase(subscriptions, existingById) {
|
|
554
|
+
let lowestPendingPhase = null;
|
|
555
|
+
for (const subscription of subscriptions) {
|
|
556
|
+
const phase = normalizeBootstrapPhase(subscription.bootstrapPhase);
|
|
557
|
+
if (isSubscriptionReady(existingById.get(subscription.id))) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (lowestPendingPhase === null || phase < lowestPendingPhase) {
|
|
561
|
+
lowestPendingPhase = phase;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return lowestPendingPhase;
|
|
565
|
+
}
|
|
566
|
+
function selectPullSubscriptions(subscriptions, existingById) {
|
|
567
|
+
const activePhase = resolveActiveBootstrapPhase(subscriptions, existingById);
|
|
568
|
+
if (activePhase === null) {
|
|
569
|
+
return [...subscriptions];
|
|
570
|
+
}
|
|
571
|
+
return subscriptions.filter((subscription) => {
|
|
572
|
+
const phase = normalizeBootstrapPhase(subscription.bootstrapPhase);
|
|
573
|
+
const existing = existingById.get(subscription.id);
|
|
574
|
+
if (phase <= activePhase)
|
|
575
|
+
return true;
|
|
576
|
+
if (isSubscriptionReady(existing))
|
|
577
|
+
return true;
|
|
578
|
+
if (isSubscriptionBootstrapping(existing))
|
|
579
|
+
return true;
|
|
580
|
+
return false;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
434
583
|
function parseScopeValuesJson(value) {
|
|
435
584
|
if (!value)
|
|
436
585
|
return {};
|
|
@@ -517,6 +666,26 @@ function resolveBootstrapClearScopes(previous, next) {
|
|
|
517
666
|
}
|
|
518
667
|
return narrowed;
|
|
519
668
|
}
|
|
669
|
+
function emitTrace(onTrace, event) {
|
|
670
|
+
onTrace?.({
|
|
671
|
+
timestamp: Date.now(),
|
|
672
|
+
...event,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function countSubscriptionRows(subscription) {
|
|
676
|
+
if (!subscription.bootstrap)
|
|
677
|
+
return undefined;
|
|
678
|
+
const snapshots = subscription.snapshots ?? [];
|
|
679
|
+
if (snapshots.length === 0)
|
|
680
|
+
return 0;
|
|
681
|
+
return snapshots.reduce((sum, snapshot) => sum + (snapshot.rows?.length ?? 0), 0);
|
|
682
|
+
}
|
|
683
|
+
function countSubscriptionChunks(subscription) {
|
|
684
|
+
if (!subscription.bootstrap)
|
|
685
|
+
return undefined;
|
|
686
|
+
const snapshots = subscription.snapshots ?? [];
|
|
687
|
+
return snapshots.reduce((sum, snapshot) => sum + (snapshot.chunks?.length ?? 0), 0);
|
|
688
|
+
}
|
|
520
689
|
/**
|
|
521
690
|
* Build a pull request from subscription state. Exported for use
|
|
522
691
|
* by the combined sync path in sync-loop.ts.
|
|
@@ -542,19 +711,27 @@ export async function buildPullRequest(db, options) {
|
|
|
542
711
|
const existingById = new Map();
|
|
543
712
|
for (const row of existing)
|
|
544
713
|
existingById.set(row.subscription_id, row);
|
|
714
|
+
const configuredSubscriptions = options.subscriptions ?? [];
|
|
715
|
+
const selectedSubscriptions = selectPullSubscriptions(configuredSubscriptions, existingById);
|
|
545
716
|
const request = {
|
|
546
717
|
clientId: options.clientId,
|
|
547
718
|
limitCommits: options.limitCommits ?? 50,
|
|
548
719
|
limitSnapshotRows: options.limitSnapshotRows ?? 1000,
|
|
549
720
|
maxSnapshotPages: options.maxSnapshotPages ?? 4,
|
|
550
721
|
dedupeRows: options.dedupeRows,
|
|
551
|
-
subscriptions:
|
|
722
|
+
subscriptions: selectedSubscriptions.map((sub) => ({
|
|
552
723
|
...sub,
|
|
553
724
|
cursor: Math.max(-1, existingById.get(sub.id)?.cursor ?? -1),
|
|
554
725
|
bootstrapState: parseBootstrapState(existingById.get(sub.id)?.bootstrap_state_json),
|
|
555
726
|
})),
|
|
556
727
|
};
|
|
557
|
-
return {
|
|
728
|
+
return {
|
|
729
|
+
request,
|
|
730
|
+
existing,
|
|
731
|
+
existingById,
|
|
732
|
+
stateId,
|
|
733
|
+
configuredSubscriptions,
|
|
734
|
+
};
|
|
558
735
|
}
|
|
559
736
|
export function createFollowupPullState(pullState, response) {
|
|
560
737
|
const responseById = new Map();
|
|
@@ -593,9 +770,10 @@ export function createFollowupPullState(pullState, response) {
|
|
|
593
770
|
nextExisting.push(nextRow);
|
|
594
771
|
nextExistingById.set(nextRow.subscription_id, nextRow);
|
|
595
772
|
}
|
|
773
|
+
const nextSelectedSubscriptions = selectPullSubscriptions(pullState.configuredSubscriptions, nextExistingById);
|
|
596
774
|
const nextRequest = {
|
|
597
775
|
...pullState.request,
|
|
598
|
-
subscriptions:
|
|
776
|
+
subscriptions: nextSelectedSubscriptions.map((sub) => {
|
|
599
777
|
const row = nextExistingById.get(sub.id);
|
|
600
778
|
return {
|
|
601
779
|
...sub,
|
|
@@ -609,6 +787,7 @@ export function createFollowupPullState(pullState, response) {
|
|
|
609
787
|
existing: nextExisting,
|
|
610
788
|
existingById: nextExistingById,
|
|
611
789
|
stateId: pullState.stateId,
|
|
790
|
+
configuredSubscriptions: pullState.configuredSubscriptions,
|
|
612
791
|
};
|
|
613
792
|
}
|
|
614
793
|
export async function applyIncrementalCommitChanges(handlers, trx, args) {
|
|
@@ -657,7 +836,10 @@ export async function applyPullResponse(db, transport, handlers, options, pullSt
|
|
|
657
836
|
transport.capabilities?.preferMaterializedSnapshots === true;
|
|
658
837
|
const bootstrapApplyMode = resolveBootstrapApplyMode(options, rawResponse, transport.capabilities);
|
|
659
838
|
let responseToApply = requiresMaterializedSnapshots
|
|
660
|
-
? await materializeChunkedSnapshots(transport, rawResponse, options.sha256
|
|
839
|
+
? await materializeChunkedSnapshots(transport, rawResponse, options.sha256, {
|
|
840
|
+
stateId,
|
|
841
|
+
onTrace: options.onTrace,
|
|
842
|
+
})
|
|
661
843
|
: rawResponse;
|
|
662
844
|
for (const plugin of plugins) {
|
|
663
845
|
if (!plugin.afterPull)
|
|
@@ -675,35 +857,95 @@ export async function applyPullResponse(db, transport, handlers, options, pullSt
|
|
|
675
857
|
});
|
|
676
858
|
if (bootstrapApplyMode === 'per-subscription') {
|
|
677
859
|
for (const sub of responseToApply.subscriptions) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
860
|
+
emitTrace(options.onTrace, {
|
|
861
|
+
stage: 'apply:transaction:start',
|
|
862
|
+
stateId,
|
|
863
|
+
transactionMode: bootstrapApplyMode,
|
|
864
|
+
subscriptionIds: [sub.id],
|
|
865
|
+
subscriptionCount: 1,
|
|
866
|
+
});
|
|
867
|
+
const transactionStartedAt = Date.now();
|
|
868
|
+
try {
|
|
869
|
+
await db.transaction().execute(async (trx) => {
|
|
870
|
+
await applySubscriptionResponse({
|
|
871
|
+
trx,
|
|
872
|
+
handlers,
|
|
873
|
+
transport,
|
|
874
|
+
options,
|
|
875
|
+
stateId,
|
|
876
|
+
existingById,
|
|
877
|
+
subsById,
|
|
878
|
+
sub,
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
emitTrace(options.onTrace, {
|
|
882
|
+
stage: 'apply:transaction:complete',
|
|
684
883
|
stateId,
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
884
|
+
transactionMode: bootstrapApplyMode,
|
|
885
|
+
subscriptionIds: [sub.id],
|
|
886
|
+
subscriptionCount: 1,
|
|
887
|
+
durationMs: Math.max(0, Date.now() - transactionStartedAt),
|
|
688
888
|
});
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
await db.transaction().execute(async (trx) => {
|
|
694
|
-
for (const sub of responseToApply.subscriptions) {
|
|
695
|
-
await applySubscriptionResponse({
|
|
696
|
-
trx,
|
|
697
|
-
handlers,
|
|
698
|
-
transport,
|
|
699
|
-
options,
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
emitTrace(options.onTrace, {
|
|
892
|
+
stage: 'apply:transaction:error',
|
|
700
893
|
stateId,
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
894
|
+
transactionMode: bootstrapApplyMode,
|
|
895
|
+
subscriptionIds: [sub.id],
|
|
896
|
+
subscriptionCount: 1,
|
|
897
|
+
durationMs: Math.max(0, Date.now() - transactionStartedAt),
|
|
898
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
704
899
|
});
|
|
900
|
+
throw error;
|
|
705
901
|
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
emitTrace(options.onTrace, {
|
|
906
|
+
stage: 'apply:transaction:start',
|
|
907
|
+
stateId,
|
|
908
|
+
transactionMode: bootstrapApplyMode,
|
|
909
|
+
subscriptionIds: responseToApply.subscriptions.map((sub) => sub.id),
|
|
910
|
+
subscriptionCount: responseToApply.subscriptions.length,
|
|
706
911
|
});
|
|
912
|
+
const transactionStartedAt = Date.now();
|
|
913
|
+
try {
|
|
914
|
+
await db.transaction().execute(async (trx) => {
|
|
915
|
+
for (const sub of responseToApply.subscriptions) {
|
|
916
|
+
await applySubscriptionResponse({
|
|
917
|
+
trx,
|
|
918
|
+
handlers,
|
|
919
|
+
transport,
|
|
920
|
+
options,
|
|
921
|
+
stateId,
|
|
922
|
+
existingById,
|
|
923
|
+
subsById,
|
|
924
|
+
sub,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
emitTrace(options.onTrace, {
|
|
929
|
+
stage: 'apply:transaction:complete',
|
|
930
|
+
stateId,
|
|
931
|
+
transactionMode: bootstrapApplyMode,
|
|
932
|
+
subscriptionIds: responseToApply.subscriptions.map((sub) => sub.id),
|
|
933
|
+
subscriptionCount: responseToApply.subscriptions.length,
|
|
934
|
+
durationMs: Math.max(0, Date.now() - transactionStartedAt),
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
emitTrace(options.onTrace, {
|
|
939
|
+
stage: 'apply:transaction:error',
|
|
940
|
+
stateId,
|
|
941
|
+
transactionMode: bootstrapApplyMode,
|
|
942
|
+
subscriptionIds: responseToApply.subscriptions.map((sub) => sub.id),
|
|
943
|
+
subscriptionCount: responseToApply.subscriptions.length,
|
|
944
|
+
durationMs: Math.max(0, Date.now() - transactionStartedAt),
|
|
945
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
946
|
+
});
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
707
949
|
}
|
|
708
950
|
return responseToApply;
|
|
709
951
|
}
|
|
@@ -791,137 +1033,239 @@ async function applySubscriptionResponse(args) {
|
|
|
791
1033
|
const staleIncrementalResponse = !sub.bootstrap &&
|
|
792
1034
|
effectiveCursor !== null &&
|
|
793
1035
|
sub.nextCursor < effectiveCursor;
|
|
1036
|
+
const applyStartedAt = Date.now();
|
|
1037
|
+
emitTrace(options.onTrace, {
|
|
1038
|
+
stage: 'apply:subscription:start',
|
|
1039
|
+
stateId,
|
|
1040
|
+
subscriptionId: sub.id,
|
|
1041
|
+
table: def?.table ?? prev?.table,
|
|
1042
|
+
bootstrap: sub.bootstrap,
|
|
1043
|
+
snapshotCount: sub.snapshots?.length ?? 0,
|
|
1044
|
+
commitCount: sub.commits?.length ?? 0,
|
|
1045
|
+
chunkCount: countSubscriptionChunks(sub),
|
|
1046
|
+
rowCount: countSubscriptionRows(sub),
|
|
1047
|
+
nextCursor: sub.nextCursor,
|
|
1048
|
+
});
|
|
794
1049
|
if (staleIncrementalResponse) {
|
|
1050
|
+
emitTrace(options.onTrace, {
|
|
1051
|
+
stage: 'apply:subscription:complete',
|
|
1052
|
+
stateId,
|
|
1053
|
+
subscriptionId: sub.id,
|
|
1054
|
+
table: def?.table ?? prev?.table,
|
|
1055
|
+
bootstrap: sub.bootstrap,
|
|
1056
|
+
snapshotCount: sub.snapshots?.length ?? 0,
|
|
1057
|
+
commitCount: sub.commits?.length ?? 0,
|
|
1058
|
+
chunkCount: countSubscriptionChunks(sub),
|
|
1059
|
+
rowCount: countSubscriptionRows(sub),
|
|
1060
|
+
nextCursor: sub.nextCursor,
|
|
1061
|
+
durationMs: Math.max(0, Date.now() - applyStartedAt),
|
|
1062
|
+
});
|
|
795
1063
|
return;
|
|
796
1064
|
}
|
|
797
|
-
|
|
798
|
-
if (
|
|
1065
|
+
try {
|
|
1066
|
+
if (sub.status === 'revoked') {
|
|
1067
|
+
if (prev?.table) {
|
|
1068
|
+
try {
|
|
1069
|
+
const scopes = parseScopeValuesJson(prev.scopes_json);
|
|
1070
|
+
await getClientHandlerOrThrow(handlers, prev.table).clearAll({
|
|
1071
|
+
trx,
|
|
1072
|
+
scopes,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
// ignore missing handler
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
await sql `
|
|
1080
|
+
delete from ${sql.table('sync_subscription_state')}
|
|
1081
|
+
where ${sql.ref('state_id')} = ${sql.val(stateId)}
|
|
1082
|
+
and ${sql.ref('subscription_id')} = ${sql.val(sub.id)}
|
|
1083
|
+
`.execute(trx);
|
|
1084
|
+
emitTrace(options.onTrace, {
|
|
1085
|
+
stage: 'apply:subscription:complete',
|
|
1086
|
+
stateId,
|
|
1087
|
+
subscriptionId: sub.id,
|
|
1088
|
+
table: def?.table ?? prev?.table,
|
|
1089
|
+
bootstrap: sub.bootstrap,
|
|
1090
|
+
snapshotCount: sub.snapshots?.length ?? 0,
|
|
1091
|
+
commitCount: sub.commits?.length ?? 0,
|
|
1092
|
+
chunkCount: countSubscriptionChunks(sub),
|
|
1093
|
+
rowCount: countSubscriptionRows(sub),
|
|
1094
|
+
nextCursor: null,
|
|
1095
|
+
durationMs: Math.max(0, Date.now() - applyStartedAt),
|
|
1096
|
+
});
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const nextScopes = sub.scopes ?? def?.scopes ?? {};
|
|
1100
|
+
const previousScopes = parseScopeValuesJson(prev?.scopes_json);
|
|
1101
|
+
const scopesChanged = !scopeValuesEqual(previousScopes, nextScopes);
|
|
1102
|
+
if (sub.bootstrap && prev?.table && scopesChanged) {
|
|
799
1103
|
try {
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1104
|
+
const clearScopes = resolveBootstrapClearScopes(previousScopes, nextScopes);
|
|
1105
|
+
if (clearScopes !== 'none') {
|
|
1106
|
+
await getClientHandlerOrThrow(handlers, prev.table).clearAll({
|
|
1107
|
+
trx,
|
|
1108
|
+
scopes: clearScopes ?? previousScopes,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
805
1111
|
}
|
|
806
1112
|
catch {
|
|
807
1113
|
// ignore missing handler
|
|
808
1114
|
}
|
|
809
1115
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
1116
|
+
if (sub.bootstrap) {
|
|
1117
|
+
for (const snapshot of sub.snapshots ?? []) {
|
|
1118
|
+
const handler = getClientHandlerOrThrow(handlers, snapshot.table);
|
|
1119
|
+
const hasChunkRefs = Array.isArray(snapshot.chunks) && snapshot.chunks.length > 0;
|
|
1120
|
+
if (snapshot.isFirstPage && handler.onSnapshotStart) {
|
|
1121
|
+
await handler.onSnapshotStart({
|
|
1122
|
+
trx,
|
|
1123
|
+
table: snapshot.table,
|
|
1124
|
+
scopes: sub.scopes,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (hasChunkRefs) {
|
|
1128
|
+
await applyChunkedSnapshot(transport, handler, trx, snapshot, sub.scopes, options.sha256, {
|
|
1129
|
+
stateId,
|
|
1130
|
+
subscriptionId: sub.id,
|
|
1131
|
+
onTrace: options.onTrace,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
await handler.applySnapshot({ trx }, snapshot);
|
|
1136
|
+
}
|
|
1137
|
+
if (snapshot.isLastPage && handler.onSnapshotEnd) {
|
|
1138
|
+
await handler.onSnapshotEnd({
|
|
1139
|
+
trx,
|
|
1140
|
+
table: snapshot.table,
|
|
1141
|
+
scopes: sub.scopes,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
828
1144
|
}
|
|
829
1145
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const hasChunkRefs = Array.isArray(snapshot.chunks) && snapshot.chunks.length > 0;
|
|
838
|
-
if (snapshot.isFirstPage && handler.onSnapshotStart) {
|
|
839
|
-
await handler.onSnapshotStart({
|
|
840
|
-
trx,
|
|
841
|
-
table: snapshot.table,
|
|
842
|
-
scopes: sub.scopes,
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
if (hasChunkRefs) {
|
|
846
|
-
await applyChunkedSnapshot(transport, handler, trx, snapshot, sub.scopes, options.sha256);
|
|
847
|
-
}
|
|
848
|
-
else {
|
|
849
|
-
await handler.applySnapshot({ trx }, snapshot);
|
|
850
|
-
}
|
|
851
|
-
if (snapshot.isLastPage && handler.onSnapshotEnd) {
|
|
852
|
-
await handler.onSnapshotEnd({
|
|
853
|
-
trx,
|
|
854
|
-
table: snapshot.table,
|
|
855
|
-
scopes: sub.scopes,
|
|
1146
|
+
else {
|
|
1147
|
+
for (const commit of sub.commits) {
|
|
1148
|
+
await applyIncrementalCommitChanges(handlers, trx, {
|
|
1149
|
+
changes: commit.changes,
|
|
1150
|
+
commitSeq: commit.commitSeq ?? null,
|
|
1151
|
+
actorId: commit.actorId ?? null,
|
|
1152
|
+
createdAt: commit.createdAt ?? null,
|
|
856
1153
|
});
|
|
857
1154
|
}
|
|
858
1155
|
}
|
|
1156
|
+
const now = Date.now();
|
|
1157
|
+
const paramsJson = serializeJsonCached(def?.params ?? {});
|
|
1158
|
+
const scopesJson = serializeJsonCached(nextScopes);
|
|
1159
|
+
const bootstrapStateJson = sub.bootstrap
|
|
1160
|
+
? sub.bootstrapState
|
|
1161
|
+
? serializeJsonCached(sub.bootstrapState)
|
|
1162
|
+
: null
|
|
1163
|
+
: null;
|
|
1164
|
+
const table = def?.table ?? 'unknown';
|
|
1165
|
+
await sql `
|
|
1166
|
+
insert into ${sql.table('sync_subscription_state')} (
|
|
1167
|
+
${sql.join([
|
|
1168
|
+
sql.ref('state_id'),
|
|
1169
|
+
sql.ref('subscription_id'),
|
|
1170
|
+
sql.ref('table'),
|
|
1171
|
+
sql.ref('scopes_json'),
|
|
1172
|
+
sql.ref('params_json'),
|
|
1173
|
+
sql.ref('cursor'),
|
|
1174
|
+
sql.ref('bootstrap_state_json'),
|
|
1175
|
+
sql.ref('status'),
|
|
1176
|
+
sql.ref('created_at'),
|
|
1177
|
+
sql.ref('updated_at'),
|
|
1178
|
+
])}
|
|
1179
|
+
) values (
|
|
1180
|
+
${sql.join([
|
|
1181
|
+
sql.val(stateId),
|
|
1182
|
+
sql.val(sub.id),
|
|
1183
|
+
sql.val(table),
|
|
1184
|
+
sql.val(scopesJson),
|
|
1185
|
+
sql.val(paramsJson),
|
|
1186
|
+
sql.val(sub.nextCursor),
|
|
1187
|
+
sql.val(bootstrapStateJson),
|
|
1188
|
+
sql.val('active'),
|
|
1189
|
+
sql.val(now),
|
|
1190
|
+
sql.val(now),
|
|
1191
|
+
])}
|
|
1192
|
+
)
|
|
1193
|
+
on conflict (${sql.join([sql.ref('state_id'), sql.ref('subscription_id')])})
|
|
1194
|
+
do update set
|
|
1195
|
+
${sql.ref('table')} = ${sql.val(table)},
|
|
1196
|
+
${sql.ref('scopes_json')} = ${sql.val(scopesJson)},
|
|
1197
|
+
${sql.ref('params_json')} = ${sql.val(paramsJson)},
|
|
1198
|
+
${sql.ref('cursor')} = ${sql.val(sub.nextCursor)},
|
|
1199
|
+
${sql.ref('bootstrap_state_json')} = ${sql.val(bootstrapStateJson)},
|
|
1200
|
+
${sql.ref('status')} = ${sql.val('active')},
|
|
1201
|
+
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
1202
|
+
`.execute(trx);
|
|
1203
|
+
emitTrace(options.onTrace, {
|
|
1204
|
+
stage: 'apply:subscription:complete',
|
|
1205
|
+
stateId,
|
|
1206
|
+
subscriptionId: sub.id,
|
|
1207
|
+
table,
|
|
1208
|
+
bootstrap: sub.bootstrap,
|
|
1209
|
+
snapshotCount: sub.snapshots?.length ?? 0,
|
|
1210
|
+
commitCount: sub.commits?.length ?? 0,
|
|
1211
|
+
chunkCount: countSubscriptionChunks(sub),
|
|
1212
|
+
rowCount: countSubscriptionRows(sub),
|
|
1213
|
+
nextCursor: sub.nextCursor,
|
|
1214
|
+
durationMs: Math.max(0, Date.now() - applyStartedAt),
|
|
1215
|
+
});
|
|
859
1216
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1217
|
+
catch (error) {
|
|
1218
|
+
emitTrace(options.onTrace, {
|
|
1219
|
+
stage: 'apply:subscription:error',
|
|
1220
|
+
stateId,
|
|
1221
|
+
subscriptionId: sub.id,
|
|
1222
|
+
table: def?.table ?? prev?.table,
|
|
1223
|
+
bootstrap: sub.bootstrap,
|
|
1224
|
+
snapshotCount: sub.snapshots?.length ?? 0,
|
|
1225
|
+
commitCount: sub.commits?.length ?? 0,
|
|
1226
|
+
chunkCount: countSubscriptionChunks(sub),
|
|
1227
|
+
rowCount: countSubscriptionRows(sub),
|
|
1228
|
+
nextCursor: sub.nextCursor,
|
|
1229
|
+
durationMs: Math.max(0, Date.now() - applyStartedAt),
|
|
1230
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
1231
|
+
});
|
|
1232
|
+
throw error;
|
|
869
1233
|
}
|
|
870
|
-
const now = Date.now();
|
|
871
|
-
const paramsJson = serializeJsonCached(def?.params ?? {});
|
|
872
|
-
const scopesJson = serializeJsonCached(nextScopes);
|
|
873
|
-
const bootstrapStateJson = sub.bootstrap
|
|
874
|
-
? sub.bootstrapState
|
|
875
|
-
? serializeJsonCached(sub.bootstrapState)
|
|
876
|
-
: null
|
|
877
|
-
: null;
|
|
878
|
-
const table = def?.table ?? 'unknown';
|
|
879
|
-
await sql `
|
|
880
|
-
insert into ${sql.table('sync_subscription_state')} (
|
|
881
|
-
${sql.join([
|
|
882
|
-
sql.ref('state_id'),
|
|
883
|
-
sql.ref('subscription_id'),
|
|
884
|
-
sql.ref('table'),
|
|
885
|
-
sql.ref('scopes_json'),
|
|
886
|
-
sql.ref('params_json'),
|
|
887
|
-
sql.ref('cursor'),
|
|
888
|
-
sql.ref('bootstrap_state_json'),
|
|
889
|
-
sql.ref('status'),
|
|
890
|
-
sql.ref('created_at'),
|
|
891
|
-
sql.ref('updated_at'),
|
|
892
|
-
])}
|
|
893
|
-
) values (
|
|
894
|
-
${sql.join([
|
|
895
|
-
sql.val(stateId),
|
|
896
|
-
sql.val(sub.id),
|
|
897
|
-
sql.val(table),
|
|
898
|
-
sql.val(scopesJson),
|
|
899
|
-
sql.val(paramsJson),
|
|
900
|
-
sql.val(sub.nextCursor),
|
|
901
|
-
sql.val(bootstrapStateJson),
|
|
902
|
-
sql.val('active'),
|
|
903
|
-
sql.val(now),
|
|
904
|
-
sql.val(now),
|
|
905
|
-
])}
|
|
906
|
-
)
|
|
907
|
-
on conflict (${sql.join([sql.ref('state_id'), sql.ref('subscription_id')])})
|
|
908
|
-
do update set
|
|
909
|
-
${sql.ref('table')} = ${sql.val(table)},
|
|
910
|
-
${sql.ref('scopes_json')} = ${sql.val(scopesJson)},
|
|
911
|
-
${sql.ref('params_json')} = ${sql.val(paramsJson)},
|
|
912
|
-
${sql.ref('cursor')} = ${sql.val(sub.nextCursor)},
|
|
913
|
-
${sql.ref('bootstrap_state_json')} = ${sql.val(bootstrapStateJson)},
|
|
914
|
-
${sql.ref('status')} = ${sql.val('active')},
|
|
915
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
916
|
-
`.execute(trx);
|
|
917
1234
|
}
|
|
918
1235
|
export async function syncPullOnce(db, transport, handlers, options, pullStateOverride) {
|
|
919
1236
|
const pullState = pullStateOverride ?? (await buildPullRequest(db, options));
|
|
920
1237
|
const { clientId, ...pullBody } = pullState.request;
|
|
921
|
-
|
|
1238
|
+
emitTrace(options.onTrace, {
|
|
1239
|
+
stage: 'pull:start',
|
|
1240
|
+
stateId: pullState.stateId,
|
|
1241
|
+
subscriptionIds: pullState.request.subscriptions.map((subscription) => subscription.id),
|
|
1242
|
+
subscriptionCount: pullState.request.subscriptions.length,
|
|
1243
|
+
});
|
|
1244
|
+
let combined;
|
|
1245
|
+
try {
|
|
1246
|
+
combined = await transport.sync({ clientId, pull: pullBody });
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
emitTrace(options.onTrace, {
|
|
1250
|
+
stage: 'pull:error',
|
|
1251
|
+
stateId: pullState.stateId,
|
|
1252
|
+
subscriptionIds: pullState.request.subscriptions.map((subscription) => subscription.id),
|
|
1253
|
+
subscriptionCount: pullState.request.subscriptions.length,
|
|
1254
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
1255
|
+
});
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
922
1258
|
if (!combined.pull) {
|
|
923
1259
|
return { ok: true, subscriptions: [] };
|
|
924
1260
|
}
|
|
1261
|
+
emitTrace(options.onTrace, {
|
|
1262
|
+
stage: 'pull:response',
|
|
1263
|
+
stateId: pullState.stateId,
|
|
1264
|
+
subscriptionIds: combined.pull.subscriptions.map((subscription) => subscription.id),
|
|
1265
|
+
subscriptionCount: combined.pull.subscriptions.length,
|
|
1266
|
+
commitCount: combined.pull.subscriptions.reduce((sum, subscription) => sum + (subscription.commits?.length ?? 0), 0),
|
|
1267
|
+
snapshotCount: combined.pull.subscriptions.reduce((sum, subscription) => sum + (subscription.snapshots?.length ?? 0), 0),
|
|
1268
|
+
});
|
|
925
1269
|
return applyPullResponse(db, transport, handlers, options, pullState, combined.pull);
|
|
926
1270
|
}
|
|
927
1271
|
//# sourceMappingURL=pull-engine.js.map
|