@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.
@@ -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
- return bytesToReadableStream(await gunzipBytes(compressedBytes));
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
- if (transport.fetchSnapshotChunkStream) {
287
- return transport.fetchSnapshotChunkStream(request);
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
- bytes = await gunzipBytes(bytes);
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 Error(
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
- const rows = decodeSnapshotRows(bytes);
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
- const decodedStream = await maybeGunzipStream(rawStream);
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 = error;
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 Error(
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(transport, {
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
- nextIsFirstPage = false;
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 = error;
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 Error(
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
- await handler.onSnapshotStart({
1503
- trx,
1504
- table: snapshot.table,
1505
- scopes: sub.scopes,
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
- await handler.applySnapshot({ trx }, snapshot);
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
- await handler.onSnapshotEnd({
1529
- trx,
1530
- table: snapshot.table,
1531
- scopes: sub.scopes,
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
- await applyIncrementalCommitChanges(handlers, trx, {
1538
- changes: commit.changes,
1539
- commitSeq: commit.commitSeq ?? null,
1540
- actorId: commit.actorId ?? null,
1541
- createdAt: commit.createdAt ?? null,
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