@syncular/client 0.0.6-221 → 0.0.6-223
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/client.d.ts +14 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +22 -2
- package/dist/client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +115 -12
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +8 -1
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +31 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +202 -48
- package/dist/pull-engine.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +44 -4
- package/src/engine/SyncEngine.test.ts +174 -0
- package/src/engine/SyncEngine.ts +140 -16
- package/src/engine/types.ts +11 -0
- package/src/errors.ts +59 -0
- package/src/index.ts +1 -0
- package/src/pull-engine.test.ts +304 -7
- package/src/pull-engine.ts +268 -56
package/src/pull-engine.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '@syncular/core';
|
|
22
22
|
import { type Kysely, sql, type Transaction } from 'kysely';
|
|
23
23
|
import type { SyncClientSubscription, SyncTraceEvent } from './engine/types';
|
|
24
|
+
import { SyncClientStageError, wrapSyncClientStageError } from './errors';
|
|
24
25
|
import {
|
|
25
26
|
type ClientHandlerCollection,
|
|
26
27
|
getClientHandlerOrThrow,
|
|
@@ -94,7 +95,13 @@ function toOwnedUint8Array(chunk: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
async function maybeGunzipStream(
|
|
97
|
-
stream: ReadableStream<Uint8Array
|
|
98
|
+
stream: ReadableStream<Uint8Array>,
|
|
99
|
+
context?: {
|
|
100
|
+
stateId?: string;
|
|
101
|
+
subscriptionId?: string;
|
|
102
|
+
table?: string;
|
|
103
|
+
chunkId?: string;
|
|
104
|
+
}
|
|
98
105
|
): Promise<ReadableStream<Uint8Array>> {
|
|
99
106
|
const reader = stream.getReader();
|
|
100
107
|
const prefetched: Uint8Array[] = [];
|
|
@@ -140,7 +147,20 @@ async function maybeGunzipStream(
|
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
const compressedBytes = await readAllBytesFromCoreStream(replayStream);
|
|
143
|
-
|
|
150
|
+
try {
|
|
151
|
+
return bytesToReadableStream(await gunzipBytes(compressedBytes));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw wrapSyncClientStageError(
|
|
154
|
+
error,
|
|
155
|
+
{
|
|
156
|
+
stage: 'snapshot-gzip-decode',
|
|
157
|
+
...context,
|
|
158
|
+
},
|
|
159
|
+
`Failed to gunzip snapshot chunk${
|
|
160
|
+
context?.chunkId ? ` "${context.chunkId}"` : ''
|
|
161
|
+
}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
async function* decodeSnapshotRowStreamBatches(
|
|
@@ -281,13 +301,30 @@ async function fetchSnapshotChunkStream(
|
|
|
281
301
|
request: {
|
|
282
302
|
chunkId: string;
|
|
283
303
|
scopeValues?: ScopeValues;
|
|
304
|
+
},
|
|
305
|
+
context?: {
|
|
306
|
+
stateId?: string;
|
|
307
|
+
subscriptionId?: string;
|
|
308
|
+
table?: string;
|
|
284
309
|
}
|
|
285
310
|
): Promise<ReadableStream<Uint8Array>> {
|
|
286
|
-
|
|
287
|
-
|
|
311
|
+
try {
|
|
312
|
+
if (transport.fetchSnapshotChunkStream) {
|
|
313
|
+
return await transport.fetchSnapshotChunkStream(request);
|
|
314
|
+
}
|
|
315
|
+
const bytes = await transport.fetchSnapshotChunk(request);
|
|
316
|
+
return bytesToReadableStream(bytes);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
throw wrapSyncClientStageError(
|
|
319
|
+
error,
|
|
320
|
+
{
|
|
321
|
+
stage: 'snapshot-chunk-fetch',
|
|
322
|
+
chunkId: request.chunkId,
|
|
323
|
+
...context,
|
|
324
|
+
},
|
|
325
|
+
`Failed to fetch snapshot chunk "${request.chunkId}"`
|
|
326
|
+
);
|
|
288
327
|
}
|
|
289
|
-
const bytes = await transport.fetchSnapshotChunk(request);
|
|
290
|
-
return bytesToReadableStream(bytes);
|
|
291
328
|
}
|
|
292
329
|
|
|
293
330
|
async function readAllBytesFromStream(
|
|
@@ -359,17 +396,53 @@ async function materializeSnapshotChunkRows(
|
|
|
359
396
|
try {
|
|
360
397
|
let bytes = await transport.fetchSnapshotChunk(request);
|
|
361
398
|
if (isGzipBytes(bytes)) {
|
|
362
|
-
|
|
399
|
+
try {
|
|
400
|
+
bytes = await gunzipBytes(bytes);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw wrapSyncClientStageError(
|
|
403
|
+
error,
|
|
404
|
+
{
|
|
405
|
+
stage: 'snapshot-gzip-decode',
|
|
406
|
+
stateId: trace?.stateId,
|
|
407
|
+
subscriptionId: trace?.subscriptionId,
|
|
408
|
+
table: trace?.table,
|
|
409
|
+
chunkId: request.chunkId,
|
|
410
|
+
},
|
|
411
|
+
`Failed to gunzip snapshot chunk "${request.chunkId}"`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
363
414
|
}
|
|
364
415
|
if (expectedHash) {
|
|
365
416
|
const actualHash = await computeSha256Hex(bytes, sha256Override);
|
|
366
417
|
if (actualHash !== expectedHash) {
|
|
367
|
-
throw new
|
|
368
|
-
`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}
|
|
418
|
+
throw new SyncClientStageError(
|
|
419
|
+
`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}`,
|
|
420
|
+
{
|
|
421
|
+
stage: 'snapshot-integrity',
|
|
422
|
+
stateId: trace?.stateId,
|
|
423
|
+
subscriptionId: trace?.subscriptionId,
|
|
424
|
+
table: trace?.table,
|
|
425
|
+
chunkId: request.chunkId,
|
|
426
|
+
}
|
|
369
427
|
);
|
|
370
428
|
}
|
|
371
429
|
}
|
|
372
|
-
|
|
430
|
+
let rows: unknown[];
|
|
431
|
+
try {
|
|
432
|
+
rows = decodeSnapshotRows(bytes);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
throw wrapSyncClientStageError(
|
|
435
|
+
error,
|
|
436
|
+
{
|
|
437
|
+
stage: 'snapshot-chunk-decode',
|
|
438
|
+
stateId: trace?.stateId,
|
|
439
|
+
subscriptionId: trace?.subscriptionId,
|
|
440
|
+
table: trace?.table,
|
|
441
|
+
chunkId: request.chunkId,
|
|
442
|
+
},
|
|
443
|
+
`Failed to decode snapshot chunk "${request.chunkId}"`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
373
446
|
emitTrace(trace?.onTrace, {
|
|
374
447
|
stage: 'apply:chunk-materialize:complete',
|
|
375
448
|
stateId: trace?.stateId,
|
|
@@ -397,8 +470,17 @@ async function materializeSnapshotChunkRows(
|
|
|
397
470
|
}
|
|
398
471
|
|
|
399
472
|
try {
|
|
400
|
-
const rawStream = await fetchSnapshotChunkStream(transport, request
|
|
401
|
-
|
|
473
|
+
const rawStream = await fetchSnapshotChunkStream(transport, request, {
|
|
474
|
+
stateId: trace?.stateId,
|
|
475
|
+
subscriptionId: trace?.subscriptionId,
|
|
476
|
+
table: trace?.table,
|
|
477
|
+
});
|
|
478
|
+
const decodedStream = await maybeGunzipStream(rawStream, {
|
|
479
|
+
stateId: trace?.stateId,
|
|
480
|
+
subscriptionId: trace?.subscriptionId,
|
|
481
|
+
table: trace?.table,
|
|
482
|
+
chunkId: request.chunkId,
|
|
483
|
+
});
|
|
402
484
|
let streamForDecode = decodedStream;
|
|
403
485
|
let chunkHashPromise: Promise<string> | null = null;
|
|
404
486
|
|
|
@@ -421,15 +503,32 @@ async function materializeSnapshotChunkRows(
|
|
|
421
503
|
rows.push(...batch);
|
|
422
504
|
}
|
|
423
505
|
} catch (error) {
|
|
424
|
-
materializeError =
|
|
506
|
+
materializeError = wrapSyncClientStageError(
|
|
507
|
+
error,
|
|
508
|
+
{
|
|
509
|
+
stage: 'snapshot-chunk-decode',
|
|
510
|
+
stateId: trace?.stateId,
|
|
511
|
+
subscriptionId: trace?.subscriptionId,
|
|
512
|
+
table: trace?.table,
|
|
513
|
+
chunkId: request.chunkId,
|
|
514
|
+
},
|
|
515
|
+
`Failed to decode snapshot chunk "${request.chunkId}"`
|
|
516
|
+
);
|
|
425
517
|
}
|
|
426
518
|
|
|
427
519
|
if (chunkHashPromise) {
|
|
428
520
|
try {
|
|
429
521
|
const actualHash = await chunkHashPromise;
|
|
430
522
|
if (!materializeError && actualHash !== expectedHash) {
|
|
431
|
-
materializeError = new
|
|
432
|
-
`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}
|
|
523
|
+
materializeError = new SyncClientStageError(
|
|
524
|
+
`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}`,
|
|
525
|
+
{
|
|
526
|
+
stage: 'snapshot-integrity',
|
|
527
|
+
stateId: trace?.stateId,
|
|
528
|
+
subscriptionId: trace?.subscriptionId,
|
|
529
|
+
table: trace?.table,
|
|
530
|
+
chunkId: request.chunkId,
|
|
531
|
+
}
|
|
433
532
|
);
|
|
434
533
|
}
|
|
435
534
|
} catch (hashError) {
|
|
@@ -568,11 +667,24 @@ async function applyChunkedSnapshot<DB extends SyncClientDb>(
|
|
|
568
667
|
const chunkStartedAt = Date.now();
|
|
569
668
|
|
|
570
669
|
try {
|
|
571
|
-
const rawStream = await fetchSnapshotChunkStream(
|
|
670
|
+
const rawStream = await fetchSnapshotChunkStream(
|
|
671
|
+
transport,
|
|
672
|
+
{
|
|
673
|
+
chunkId: chunk.id,
|
|
674
|
+
scopeValues,
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
stateId: trace?.stateId,
|
|
678
|
+
subscriptionId: trace?.subscriptionId,
|
|
679
|
+
table: snapshot.table,
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
const decodedStream = await maybeGunzipStream(rawStream, {
|
|
683
|
+
stateId: trace?.stateId,
|
|
684
|
+
subscriptionId: trace?.subscriptionId,
|
|
685
|
+
table: snapshot.table,
|
|
572
686
|
chunkId: chunk.id,
|
|
573
|
-
scopeValues,
|
|
574
687
|
});
|
|
575
|
-
const decodedStream = await maybeGunzipStream(rawStream);
|
|
576
688
|
let streamForDecode = decodedStream;
|
|
577
689
|
let chunkHashPromise: Promise<string> | null = null;
|
|
578
690
|
|
|
@@ -599,6 +711,39 @@ async function applyChunkedSnapshot<DB extends SyncClientDb>(
|
|
|
599
711
|
chunkRowCount += batch.length;
|
|
600
712
|
if (pendingBatch) {
|
|
601
713
|
// eslint-disable-next-line no-await-in-loop
|
|
714
|
+
try {
|
|
715
|
+
await handler.applySnapshot(
|
|
716
|
+
{ trx },
|
|
717
|
+
{
|
|
718
|
+
...snapshot,
|
|
719
|
+
rows: pendingBatch,
|
|
720
|
+
chunks: undefined,
|
|
721
|
+
isFirstPage: nextIsFirstPage,
|
|
722
|
+
isLastPage: false,
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
throw wrapSyncClientStageError(
|
|
727
|
+
error,
|
|
728
|
+
{
|
|
729
|
+
stage: 'snapshot-apply',
|
|
730
|
+
stateId: trace?.stateId,
|
|
731
|
+
subscriptionId: trace?.subscriptionId,
|
|
732
|
+
table: snapshot.table,
|
|
733
|
+
chunkId: chunk.id,
|
|
734
|
+
},
|
|
735
|
+
`Failed to apply snapshot chunk "${chunk.id}"`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
nextIsFirstPage = false;
|
|
739
|
+
}
|
|
740
|
+
pendingBatch = batch;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (pendingBatch) {
|
|
744
|
+
const isLastChunk = chunkIndex === chunks.length - 1;
|
|
745
|
+
// eslint-disable-next-line no-await-in-loop
|
|
746
|
+
try {
|
|
602
747
|
await handler.applySnapshot(
|
|
603
748
|
{ trx },
|
|
604
749
|
{
|
|
@@ -606,31 +751,39 @@ async function applyChunkedSnapshot<DB extends SyncClientDb>(
|
|
|
606
751
|
rows: pendingBatch,
|
|
607
752
|
chunks: undefined,
|
|
608
753
|
isFirstPage: nextIsFirstPage,
|
|
609
|
-
isLastPage: false,
|
|
754
|
+
isLastPage: isLastChunk ? snapshot.isLastPage : false,
|
|
610
755
|
}
|
|
611
756
|
);
|
|
612
|
-
|
|
757
|
+
} catch (error) {
|
|
758
|
+
throw wrapSyncClientStageError(
|
|
759
|
+
error,
|
|
760
|
+
{
|
|
761
|
+
stage: 'snapshot-apply',
|
|
762
|
+
stateId: trace?.stateId,
|
|
763
|
+
subscriptionId: trace?.subscriptionId,
|
|
764
|
+
table: snapshot.table,
|
|
765
|
+
chunkId: chunk.id,
|
|
766
|
+
},
|
|
767
|
+
`Failed to apply snapshot chunk "${chunk.id}"`
|
|
768
|
+
);
|
|
613
769
|
}
|
|
614
|
-
pendingBatch = batch;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (pendingBatch) {
|
|
618
|
-
const isLastChunk = chunkIndex === chunks.length - 1;
|
|
619
|
-
// eslint-disable-next-line no-await-in-loop
|
|
620
|
-
await handler.applySnapshot(
|
|
621
|
-
{ trx },
|
|
622
|
-
{
|
|
623
|
-
...snapshot,
|
|
624
|
-
rows: pendingBatch,
|
|
625
|
-
chunks: undefined,
|
|
626
|
-
isFirstPage: nextIsFirstPage,
|
|
627
|
-
isLastPage: isLastChunk ? snapshot.isLastPage : false,
|
|
628
|
-
}
|
|
629
|
-
);
|
|
630
770
|
nextIsFirstPage = false;
|
|
631
771
|
}
|
|
632
772
|
} catch (error) {
|
|
633
|
-
applyError =
|
|
773
|
+
applyError =
|
|
774
|
+
error instanceof SyncClientStageError
|
|
775
|
+
? error
|
|
776
|
+
: wrapSyncClientStageError(
|
|
777
|
+
error,
|
|
778
|
+
{
|
|
779
|
+
stage: 'snapshot-chunk-decode',
|
|
780
|
+
stateId: trace?.stateId,
|
|
781
|
+
subscriptionId: trace?.subscriptionId,
|
|
782
|
+
table: snapshot.table,
|
|
783
|
+
chunkId: chunk.id,
|
|
784
|
+
},
|
|
785
|
+
`Failed to decode snapshot chunk "${chunk.id}"`
|
|
786
|
+
);
|
|
634
787
|
}
|
|
635
788
|
|
|
636
789
|
if (chunkHashPromise) {
|
|
@@ -638,8 +791,15 @@ async function applyChunkedSnapshot<DB extends SyncClientDb>(
|
|
|
638
791
|
// eslint-disable-next-line no-await-in-loop
|
|
639
792
|
const actualHash = await chunkHashPromise;
|
|
640
793
|
if (!applyError && actualHash !== chunk.sha256) {
|
|
641
|
-
applyError = new
|
|
642
|
-
`Snapshot chunk integrity check failed: expected sha256 ${chunk.sha256}, got ${actualHash}
|
|
794
|
+
applyError = new SyncClientStageError(
|
|
795
|
+
`Snapshot chunk integrity check failed: expected sha256 ${chunk.sha256}, got ${actualHash}`,
|
|
796
|
+
{
|
|
797
|
+
stage: 'snapshot-integrity',
|
|
798
|
+
stateId: trace?.stateId,
|
|
799
|
+
subscriptionId: trace?.subscriptionId,
|
|
800
|
+
table: snapshot.table,
|
|
801
|
+
chunkId: chunk.id,
|
|
802
|
+
}
|
|
643
803
|
);
|
|
644
804
|
}
|
|
645
805
|
} catch (hashError) {
|
|
@@ -1499,11 +1659,24 @@ async function applySubscriptionResponse<DB extends SyncClientDb>(args: {
|
|
|
1499
1659
|
Array.isArray(snapshot.chunks) && snapshot.chunks.length > 0;
|
|
1500
1660
|
|
|
1501
1661
|
if (snapshot.isFirstPage && handler.onSnapshotStart) {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1662
|
+
try {
|
|
1663
|
+
await handler.onSnapshotStart({
|
|
1664
|
+
trx,
|
|
1665
|
+
table: snapshot.table,
|
|
1666
|
+
scopes: sub.scopes,
|
|
1667
|
+
});
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
throw wrapSyncClientStageError(
|
|
1670
|
+
error,
|
|
1671
|
+
{
|
|
1672
|
+
stage: 'snapshot-apply',
|
|
1673
|
+
stateId,
|
|
1674
|
+
subscriptionId: sub.id,
|
|
1675
|
+
table: snapshot.table,
|
|
1676
|
+
},
|
|
1677
|
+
`Failed to start snapshot apply for subscription "${sub.id}"`
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1507
1680
|
}
|
|
1508
1681
|
|
|
1509
1682
|
if (hasChunkRefs) {
|
|
@@ -1521,25 +1694,64 @@ async function applySubscriptionResponse<DB extends SyncClientDb>(args: {
|
|
|
1521
1694
|
}
|
|
1522
1695
|
);
|
|
1523
1696
|
} else {
|
|
1524
|
-
|
|
1697
|
+
try {
|
|
1698
|
+
await handler.applySnapshot({ trx }, snapshot);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
throw wrapSyncClientStageError(
|
|
1701
|
+
error,
|
|
1702
|
+
{
|
|
1703
|
+
stage: 'snapshot-apply',
|
|
1704
|
+
stateId,
|
|
1705
|
+
subscriptionId: sub.id,
|
|
1706
|
+
table: snapshot.table,
|
|
1707
|
+
},
|
|
1708
|
+
`Failed to apply snapshot for subscription "${sub.id}"`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1525
1711
|
}
|
|
1526
1712
|
|
|
1527
1713
|
if (snapshot.isLastPage && handler.onSnapshotEnd) {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1714
|
+
try {
|
|
1715
|
+
await handler.onSnapshotEnd({
|
|
1716
|
+
trx,
|
|
1717
|
+
table: snapshot.table,
|
|
1718
|
+
scopes: sub.scopes,
|
|
1719
|
+
});
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
throw wrapSyncClientStageError(
|
|
1722
|
+
error,
|
|
1723
|
+
{
|
|
1724
|
+
stage: 'snapshot-apply',
|
|
1725
|
+
stateId,
|
|
1726
|
+
subscriptionId: sub.id,
|
|
1727
|
+
table: snapshot.table,
|
|
1728
|
+
},
|
|
1729
|
+
`Failed to finalize snapshot apply for subscription "${sub.id}"`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1533
1732
|
}
|
|
1534
1733
|
}
|
|
1535
1734
|
} else {
|
|
1536
1735
|
for (const commit of sub.commits) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1736
|
+
try {
|
|
1737
|
+
await applyIncrementalCommitChanges(handlers, trx, {
|
|
1738
|
+
changes: commit.changes,
|
|
1739
|
+
commitSeq: commit.commitSeq ?? null,
|
|
1740
|
+
actorId: commit.actorId ?? null,
|
|
1741
|
+
createdAt: commit.createdAt ?? null,
|
|
1742
|
+
});
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
throw wrapSyncClientStageError(
|
|
1745
|
+
error,
|
|
1746
|
+
{
|
|
1747
|
+
stage: 'snapshot-apply',
|
|
1748
|
+
stateId,
|
|
1749
|
+
subscriptionId: sub.id,
|
|
1750
|
+
table: def?.table ?? prev?.table,
|
|
1751
|
+
},
|
|
1752
|
+
`Failed to apply incremental changes for subscription "${sub.id}"`
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1543
1755
|
}
|
|
1544
1756
|
}
|
|
1545
1757
|
|