@syncular/server 0.0.6-202 → 0.0.6-205
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/dialect/types.d.ts +1 -0
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +7 -11
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +544 -502
- package/dist/pull.js.map +1 -1
- package/dist/subscriptions/cache.d.ts +3 -0
- package/dist/subscriptions/cache.d.ts.map +1 -1
- package/dist/subscriptions/cache.js +44 -0
- package/dist/subscriptions/cache.js.map +1 -1
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +62 -35
- package/dist/subscriptions/resolve.js.map +1 -1
- package/package.json +2 -2
- package/src/dialect/types.ts +1 -0
- package/src/handlers/create-handler.ts +15 -15
- package/src/pull.ts +737 -660
- package/src/subscriptions/cache.ts +58 -0
- package/src/subscriptions/resolve.test.ts +71 -1
- package/src/subscriptions/resolve.ts +65 -38
package/src/pull.ts
CHANGED
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
startSyncSpan,
|
|
21
21
|
} from '@syncular/core';
|
|
22
22
|
import type { Kysely } from 'kysely';
|
|
23
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
DbExecutor,
|
|
25
|
+
IncrementalPullRow,
|
|
26
|
+
ServerSyncDialect,
|
|
27
|
+
} from './dialect/types';
|
|
24
28
|
import {
|
|
25
29
|
getServerBootstrapOrderFor,
|
|
26
30
|
type ServerHandlerCollection,
|
|
@@ -45,6 +49,8 @@ const DEFAULT_MAX_SNAPSHOT_BUNDLE_ROW_FRAME_BYTES = 512 * 1024;
|
|
|
45
49
|
const MAX_ADAPTIVE_SNAPSHOT_BUNDLE_ROW_FRAME_BYTES = 4 * 1024 * 1024;
|
|
46
50
|
const DEFAULT_INLINE_SNAPSHOT_ROW_FRAME_BYTES = 256 * 1024;
|
|
47
51
|
const EMPTY_SNAPSHOT_ROW_FRAMES = encodeSnapshotRows([]);
|
|
52
|
+
const MAX_PULL_TRANSACTION_RETRIES = 2;
|
|
53
|
+
const PULL_TRANSACTION_RETRY_DELAY_MS = 15;
|
|
48
54
|
|
|
49
55
|
interface PullBootstrapTimings {
|
|
50
56
|
snapshotQueryMs: number;
|
|
@@ -348,6 +354,20 @@ function sanitizeLimit(
|
|
|
348
354
|
return Math.max(min, Math.min(max, value));
|
|
349
355
|
}
|
|
350
356
|
|
|
357
|
+
function isSerializablePullError(error: Error): boolean {
|
|
358
|
+
const withCode = error as Error & { code?: string };
|
|
359
|
+
return (
|
|
360
|
+
withCode.code === '40001' ||
|
|
361
|
+
error.message.toLowerCase().includes('could not serialize access')
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function delay(ms: number): Promise<void> {
|
|
366
|
+
await new Promise((resolve) => {
|
|
367
|
+
setTimeout(resolve, ms);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
351
371
|
/**
|
|
352
372
|
* Merge all scope values into a flat ScopeValues for cursor tracking.
|
|
353
373
|
*/
|
|
@@ -567,9 +587,6 @@ export async function pull<
|
|
|
567
587
|
50
|
|
568
588
|
);
|
|
569
589
|
const dedupeRows = request.dedupeRows === true;
|
|
570
|
-
const pendingExternalChunkWrites: PendingExternalChunkWrite[] = [];
|
|
571
|
-
const bootstrapTimings = createPullBootstrapTimings();
|
|
572
|
-
|
|
573
590
|
// Resolve effective scopes for each subscription
|
|
574
591
|
const resolved = await resolveEffectiveScopesForSubscriptions({
|
|
575
592
|
db,
|
|
@@ -579,630 +596,646 @@ export async function pull<
|
|
|
579
596
|
scopeCache: args.scopeCache ?? defaultScopeCache,
|
|
580
597
|
});
|
|
581
598
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
for (const sub of resolved) {
|
|
601
|
-
if (
|
|
602
|
-
sub.status === 'revoked' ||
|
|
603
|
-
Object.keys(sub.scopes).length === 0
|
|
604
|
-
)
|
|
605
|
-
continue;
|
|
606
|
-
activeTables.add(sub.table);
|
|
607
|
-
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
608
|
-
if (cursor >= 0 && cursor < minSubCursor) {
|
|
609
|
-
minSubCursor = cursor;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const maxExternalCommitByTable =
|
|
614
|
-
minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
|
|
615
|
-
? await readLatestExternalCommitByTable(trx, {
|
|
599
|
+
for (
|
|
600
|
+
let attemptIndex = 0;
|
|
601
|
+
attemptIndex < MAX_PULL_TRANSACTION_RETRIES;
|
|
602
|
+
attemptIndex += 1
|
|
603
|
+
) {
|
|
604
|
+
const pendingExternalChunkWrites: PendingExternalChunkWrite[] = [];
|
|
605
|
+
const bootstrapTimings = createPullBootstrapTimings();
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const result = await dialect.executeInTransaction(
|
|
609
|
+
db,
|
|
610
|
+
async (trx) => {
|
|
611
|
+
await dialect.setRepeatableRead(trx);
|
|
612
|
+
|
|
613
|
+
const maxCommitSeq = await dialect.readMaxCommitSeq(trx, {
|
|
614
|
+
partitionId,
|
|
615
|
+
});
|
|
616
|
+
const minCommitSeq = await dialect.readMinCommitSeq(trx, {
|
|
616
617
|
partitionId,
|
|
617
|
-
afterCursor: minSubCursor,
|
|
618
|
-
tables: Array.from(activeTables),
|
|
619
|
-
})
|
|
620
|
-
: new Map<string, number>();
|
|
621
|
-
|
|
622
|
-
for (const sub of resolved) {
|
|
623
|
-
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
624
|
-
// Validate table handler exists (throws if not registered)
|
|
625
|
-
if (!args.handlers.byTable.has(sub.table)) {
|
|
626
|
-
throw new Error(`Unknown table: ${sub.table}`);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
sub.status === 'revoked' ||
|
|
631
|
-
Object.keys(sub.scopes).length === 0
|
|
632
|
-
) {
|
|
633
|
-
subResponses.push({
|
|
634
|
-
id: sub.id,
|
|
635
|
-
status: 'revoked',
|
|
636
|
-
scopes: {},
|
|
637
|
-
bootstrap: false,
|
|
638
|
-
nextCursor: cursor,
|
|
639
|
-
commits: [],
|
|
640
|
-
});
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const effectiveScopes = sub.scopes;
|
|
645
|
-
activeSubscriptions.push({ scopes: effectiveScopes });
|
|
646
|
-
const latestExternalCommitForTable = maxExternalCommitByTable.get(
|
|
647
|
-
sub.table
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
const needsBootstrap =
|
|
651
|
-
sub.bootstrapState != null ||
|
|
652
|
-
cursor < 0 ||
|
|
653
|
-
cursor > maxCommitSeq ||
|
|
654
|
-
(minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
|
|
655
|
-
(latestExternalCommitForTable !== undefined &&
|
|
656
|
-
latestExternalCommitForTable > cursor);
|
|
657
|
-
|
|
658
|
-
if (needsBootstrap) {
|
|
659
|
-
const tables = getServerBootstrapOrderFor(
|
|
660
|
-
args.handlers,
|
|
661
|
-
sub.table
|
|
662
|
-
).map((handler) => handler.table);
|
|
663
|
-
const preferInlineBootstrapSnapshot =
|
|
664
|
-
cursor >= 0 ||
|
|
665
|
-
sub.bootstrapState != null ||
|
|
666
|
-
(latestExternalCommitForTable !== undefined &&
|
|
667
|
-
latestExternalCommitForTable > cursor);
|
|
668
|
-
|
|
669
|
-
const initState: SyncBootstrapState = {
|
|
670
|
-
asOfCommitSeq: maxCommitSeq,
|
|
671
|
-
tables,
|
|
672
|
-
tableIndex: 0,
|
|
673
|
-
rowCursor: null,
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
const requestedState = sub.bootstrapState ?? null;
|
|
677
|
-
const state =
|
|
678
|
-
requestedState &&
|
|
679
|
-
typeof requestedState.asOfCommitSeq === 'number' &&
|
|
680
|
-
Array.isArray(requestedState.tables) &&
|
|
681
|
-
typeof requestedState.tableIndex === 'number'
|
|
682
|
-
? (requestedState as SyncBootstrapState)
|
|
683
|
-
: initState;
|
|
684
|
-
|
|
685
|
-
// If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
|
|
686
|
-
const effectiveState =
|
|
687
|
-
state.asOfCommitSeq < minCommitSeq - 1 ? initState : state;
|
|
688
|
-
|
|
689
|
-
const tableName =
|
|
690
|
-
effectiveState.tables[effectiveState.tableIndex];
|
|
691
|
-
|
|
692
|
-
// No tables (or ran past the end): treat bootstrap as complete.
|
|
693
|
-
if (!tableName) {
|
|
694
|
-
subResponses.push({
|
|
695
|
-
id: sub.id,
|
|
696
|
-
status: 'active',
|
|
697
|
-
scopes: effectiveScopes,
|
|
698
|
-
bootstrap: true,
|
|
699
|
-
bootstrapState: null,
|
|
700
|
-
nextCursor: effectiveState.asOfCommitSeq,
|
|
701
|
-
commits: [],
|
|
702
|
-
snapshots: [],
|
|
703
618
|
});
|
|
704
|
-
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const snapshots: SyncSnapshot[] = [];
|
|
709
|
-
let nextState: SyncBootstrapState | null = effectiveState;
|
|
710
|
-
const cacheKey = `${partitionId}:${await scopesToSnapshotChunkScopeKey(
|
|
711
|
-
effectiveScopes
|
|
712
|
-
)}`;
|
|
713
|
-
|
|
714
|
-
interface SnapshotBundle {
|
|
715
|
-
table: string;
|
|
716
|
-
startCursor: string | null;
|
|
717
|
-
isFirstPage: boolean;
|
|
718
|
-
isLastPage: boolean;
|
|
719
|
-
pageCount: number;
|
|
720
|
-
ttlMs: number;
|
|
721
|
-
rowFrameByteLength: number;
|
|
722
|
-
rowFrameParts: Uint8Array[];
|
|
723
|
-
inlineRows: unknown[] | null;
|
|
724
|
-
}
|
|
725
619
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
620
|
+
const subResponses: SyncPullSubscriptionResponse[] = [];
|
|
621
|
+
const activeSubscriptions: { scopes: ScopeValues }[] = [];
|
|
622
|
+
const nextCursors: number[] = [];
|
|
623
|
+
|
|
624
|
+
// Detect external data changes (synthetic commits from notifyExternalDataChange)
|
|
625
|
+
// Compute minimum cursor across all active subscriptions to scope the query.
|
|
626
|
+
let minSubCursor = Number.MAX_SAFE_INTEGER;
|
|
627
|
+
const activeTables = new Set<string>();
|
|
628
|
+
for (const sub of resolved) {
|
|
629
|
+
if (
|
|
630
|
+
sub.status === 'revoked' ||
|
|
631
|
+
Object.keys(sub.scopes).length === 0
|
|
632
|
+
)
|
|
633
|
+
continue;
|
|
634
|
+
activeTables.add(sub.table);
|
|
635
|
+
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
636
|
+
if (cursor >= 0 && cursor < minSubCursor) {
|
|
637
|
+
minSubCursor = cursor;
|
|
638
|
+
}
|
|
737
639
|
}
|
|
738
640
|
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
641
|
+
const maxExternalCommitByTable =
|
|
642
|
+
minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
|
|
643
|
+
? await readLatestExternalCommitByTable(trx, {
|
|
644
|
+
partitionId,
|
|
645
|
+
afterCursor: minSubCursor,
|
|
646
|
+
tables: Array.from(activeTables),
|
|
647
|
+
})
|
|
648
|
+
: new Map<string, number>();
|
|
649
|
+
|
|
650
|
+
for (const sub of resolved) {
|
|
651
|
+
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
652
|
+
// Validate table handler exists (throws if not registered)
|
|
653
|
+
if (!args.handlers.byTable.has(sub.table)) {
|
|
654
|
+
throw new Error(`Unknown table: ${sub.table}`);
|
|
655
|
+
}
|
|
744
656
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
657
|
+
if (
|
|
658
|
+
sub.status === 'revoked' ||
|
|
659
|
+
Object.keys(sub.scopes).length === 0
|
|
660
|
+
) {
|
|
661
|
+
subResponses.push({
|
|
662
|
+
id: sub.id,
|
|
663
|
+
status: 'revoked',
|
|
664
|
+
scopes: {},
|
|
665
|
+
bootstrap: false,
|
|
666
|
+
nextCursor: cursor,
|
|
667
|
+
commits: [],
|
|
668
|
+
});
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const effectiveScopes = sub.scopes;
|
|
673
|
+
activeSubscriptions.push({ scopes: effectiveScopes });
|
|
674
|
+
const latestExternalCommitForTable =
|
|
675
|
+
maxExternalCommitByTable.get(sub.table);
|
|
676
|
+
|
|
677
|
+
const needsBootstrap =
|
|
678
|
+
sub.bootstrapState != null ||
|
|
679
|
+
cursor < 0 ||
|
|
680
|
+
cursor > maxCommitSeq ||
|
|
681
|
+
(minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
|
|
682
|
+
(latestExternalCommitForTable !== undefined &&
|
|
683
|
+
latestExternalCommitForTable > cursor);
|
|
684
|
+
|
|
685
|
+
if (needsBootstrap) {
|
|
686
|
+
const tables = getServerBootstrapOrderFor(
|
|
687
|
+
args.handlers,
|
|
688
|
+
sub.table
|
|
689
|
+
).map((handler) => handler.table);
|
|
690
|
+
const preferInlineBootstrapSnapshot =
|
|
691
|
+
cursor >= 0 ||
|
|
692
|
+
sub.bootstrapState != null ||
|
|
693
|
+
(latestExternalCommitForTable !== undefined &&
|
|
694
|
+
latestExternalCommitForTable > cursor);
|
|
695
|
+
|
|
696
|
+
const initState: SyncBootstrapState = {
|
|
697
|
+
asOfCommitSeq: maxCommitSeq,
|
|
698
|
+
tables,
|
|
699
|
+
tableIndex: 0,
|
|
700
|
+
rowCursor: null,
|
|
775
701
|
};
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
702
|
+
|
|
703
|
+
const requestedState = sub.bootstrapState ?? null;
|
|
704
|
+
const state =
|
|
705
|
+
requestedState &&
|
|
706
|
+
typeof requestedState.asOfCommitSeq === 'number' &&
|
|
707
|
+
Array.isArray(requestedState.tables) &&
|
|
708
|
+
typeof requestedState.tableIndex === 'number'
|
|
709
|
+
? (requestedState as SyncBootstrapState)
|
|
710
|
+
: initState;
|
|
711
|
+
|
|
712
|
+
// If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
|
|
713
|
+
const effectiveState =
|
|
714
|
+
state.asOfCommitSeq < minCommitSeq - 1
|
|
715
|
+
? initState
|
|
716
|
+
: state;
|
|
717
|
+
|
|
718
|
+
const tableName =
|
|
719
|
+
effectiveState.tables[effectiveState.tableIndex];
|
|
720
|
+
|
|
721
|
+
// No tables (or ran past the end): treat bootstrap as complete.
|
|
722
|
+
if (!tableName) {
|
|
723
|
+
subResponses.push({
|
|
724
|
+
id: sub.id,
|
|
725
|
+
status: 'active',
|
|
726
|
+
scopes: effectiveScopes,
|
|
727
|
+
bootstrap: true,
|
|
728
|
+
bootstrapState: null,
|
|
729
|
+
nextCursor: effectiveState.asOfCommitSeq,
|
|
730
|
+
commits: [],
|
|
731
|
+
snapshots: [],
|
|
732
|
+
});
|
|
733
|
+
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const snapshots: SyncSnapshot[] = [];
|
|
738
|
+
let nextState: SyncBootstrapState | null = effectiveState;
|
|
739
|
+
const cacheKey = `${partitionId}:${await scopesToSnapshotChunkScopeKey(
|
|
740
|
+
effectiveScopes
|
|
741
|
+
)}`;
|
|
742
|
+
|
|
743
|
+
interface SnapshotBundle {
|
|
744
|
+
table: string;
|
|
745
|
+
startCursor: string | null;
|
|
746
|
+
isFirstPage: boolean;
|
|
747
|
+
isLastPage: boolean;
|
|
748
|
+
pageCount: number;
|
|
749
|
+
ttlMs: number;
|
|
750
|
+
rowFrameByteLength: number;
|
|
751
|
+
rowFrameParts: Uint8Array[];
|
|
752
|
+
inlineRows: unknown[] | null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const flushSnapshotBundle = async (
|
|
756
|
+
bundle: SnapshotBundle
|
|
757
|
+
): Promise<void> => {
|
|
758
|
+
if (bundle.inlineRows) {
|
|
759
|
+
snapshots.push({
|
|
760
|
+
table: bundle.table,
|
|
761
|
+
rows: bundle.inlineRows,
|
|
762
|
+
isFirstPage: bundle.isFirstPage,
|
|
763
|
+
isLastPage: bundle.isLastPage,
|
|
764
|
+
});
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const nowIso = new Date().toISOString();
|
|
769
|
+
const bundleRowLimit = Math.max(
|
|
770
|
+
1,
|
|
771
|
+
limitSnapshotRows * bundle.pageCount
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
const cacheLookupStartedAt = Date.now();
|
|
775
|
+
const cached = await readSnapshotChunkRefByPageKey(trx, {
|
|
780
776
|
partitionId,
|
|
781
777
|
scopeKey: cacheKey,
|
|
782
778
|
scope: bundle.table,
|
|
783
779
|
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
784
780
|
rowCursor: bundle.startCursor,
|
|
785
781
|
rowLimit: bundleRowLimit,
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
782
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
783
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
784
|
+
nowIso,
|
|
785
|
+
});
|
|
786
|
+
bootstrapTimings.chunkCacheLookupMs += Math.max(
|
|
787
|
+
0,
|
|
788
|
+
Date.now() - cacheLookupStartedAt
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
let chunkRef = cached;
|
|
792
|
+
if (!chunkRef) {
|
|
793
|
+
const expiresAt = new Date(
|
|
794
|
+
Date.now() + Math.max(1000, bundle.ttlMs)
|
|
795
|
+
).toISOString();
|
|
796
|
+
|
|
797
|
+
if (args.chunkStorage) {
|
|
798
|
+
const snapshot: SyncSnapshot = {
|
|
799
|
+
table: bundle.table,
|
|
800
|
+
rows: [],
|
|
801
|
+
chunks: [],
|
|
802
|
+
isFirstPage: bundle.isFirstPage,
|
|
803
|
+
isLastPage: bundle.isLastPage,
|
|
804
|
+
};
|
|
805
|
+
snapshots.push(snapshot);
|
|
806
|
+
pendingExternalChunkWrites.push({
|
|
807
|
+
snapshot,
|
|
808
|
+
cacheLookup: {
|
|
809
|
+
partitionId,
|
|
810
|
+
scopeKey: cacheKey,
|
|
811
|
+
scope: bundle.table,
|
|
812
|
+
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
813
|
+
rowCursor: bundle.startCursor,
|
|
814
|
+
rowLimit: bundleRowLimit,
|
|
815
|
+
},
|
|
816
|
+
rowFrameParts: [...bundle.rowFrameParts],
|
|
817
|
+
expiresAt,
|
|
818
|
+
});
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const encodedChunk =
|
|
822
|
+
await encodeCompressedSnapshotChunk(
|
|
823
|
+
bundle.rowFrameParts
|
|
824
|
+
);
|
|
825
|
+
bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
|
|
826
|
+
bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
|
|
827
|
+
const chunkId = randomId();
|
|
828
|
+
const chunkPersistStartedAt = Date.now();
|
|
829
|
+
chunkRef = await insertSnapshotChunk(trx, {
|
|
830
|
+
chunkId,
|
|
831
|
+
partitionId,
|
|
832
|
+
scopeKey: cacheKey,
|
|
833
|
+
scope: bundle.table,
|
|
834
|
+
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
835
|
+
rowCursor: bundle.startCursor,
|
|
836
|
+
rowLimit: bundleRowLimit,
|
|
837
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
838
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
839
|
+
sha256: encodedChunk.sha256,
|
|
840
|
+
body: encodedChunk.body,
|
|
841
|
+
expiresAt,
|
|
842
|
+
});
|
|
843
|
+
bootstrapTimings.chunkPersistMs += Math.max(
|
|
844
|
+
0,
|
|
845
|
+
Date.now() - chunkPersistStartedAt
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
snapshots.push({
|
|
850
|
+
table: bundle.table,
|
|
851
|
+
rows: [],
|
|
852
|
+
chunks: [chunkRef],
|
|
853
|
+
isFirstPage: bundle.isFirstPage,
|
|
854
|
+
isLastPage: bundle.isLastPage,
|
|
855
|
+
});
|
|
856
|
+
};
|
|
818
857
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
858
|
+
let activeBundle: SnapshotBundle | null = null;
|
|
859
|
+
|
|
860
|
+
for (
|
|
861
|
+
let pageIndex = 0;
|
|
862
|
+
pageIndex < maxSnapshotPages;
|
|
863
|
+
pageIndex++
|
|
864
|
+
) {
|
|
865
|
+
if (!nextState) break;
|
|
866
|
+
|
|
867
|
+
const nextTableName: string | undefined =
|
|
868
|
+
nextState.tables[nextState.tableIndex];
|
|
869
|
+
if (!nextTableName) {
|
|
870
|
+
if (activeBundle) {
|
|
871
|
+
activeBundle.isLastPage = true;
|
|
872
|
+
await flushSnapshotBundle(activeBundle);
|
|
873
|
+
activeBundle = null;
|
|
874
|
+
}
|
|
875
|
+
nextState = null;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const tableHandler =
|
|
880
|
+
args.handlers.byTable.get(nextTableName);
|
|
881
|
+
if (!tableHandler) {
|
|
882
|
+
throw new Error(`Unknown table: ${nextTableName}`);
|
|
883
|
+
}
|
|
884
|
+
if (
|
|
885
|
+
!activeBundle ||
|
|
886
|
+
activeBundle.table !== nextTableName
|
|
887
|
+
) {
|
|
888
|
+
if (activeBundle) {
|
|
889
|
+
await flushSnapshotBundle(activeBundle);
|
|
890
|
+
}
|
|
891
|
+
const bundleHeader = EMPTY_SNAPSHOT_ROW_FRAMES;
|
|
892
|
+
activeBundle = {
|
|
893
|
+
table: nextTableName,
|
|
894
|
+
startCursor: nextState.rowCursor,
|
|
895
|
+
isFirstPage: nextState.rowCursor == null,
|
|
896
|
+
isLastPage: false,
|
|
897
|
+
pageCount: 0,
|
|
898
|
+
ttlMs:
|
|
899
|
+
tableHandler.snapshotChunkTtlMs ??
|
|
900
|
+
24 * 60 * 60 * 1000,
|
|
901
|
+
rowFrameByteLength: bundleHeader.length,
|
|
902
|
+
rowFrameParts: [bundleHeader],
|
|
903
|
+
inlineRows: null,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const snapshotQueryStartedAt = Date.now();
|
|
908
|
+
const page: {
|
|
909
|
+
rows: unknown[];
|
|
910
|
+
nextCursor: string | null;
|
|
911
|
+
} = await tableHandler.snapshot(
|
|
912
|
+
{
|
|
913
|
+
db: trx,
|
|
914
|
+
actorId: args.auth.actorId,
|
|
915
|
+
auth: args.auth,
|
|
916
|
+
scopeValues: effectiveScopes,
|
|
917
|
+
cursor: nextState.rowCursor,
|
|
918
|
+
limit: limitSnapshotRows,
|
|
919
|
+
},
|
|
920
|
+
sub.params
|
|
921
|
+
);
|
|
922
|
+
bootstrapTimings.snapshotQueryMs += Math.max(
|
|
923
|
+
0,
|
|
924
|
+
Date.now() - snapshotQueryStartedAt
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
const rowFrameEncodeStartedAt = Date.now();
|
|
928
|
+
const rowFrames = encodeSnapshotRowFrames(
|
|
929
|
+
page.rows ?? []
|
|
930
|
+
);
|
|
931
|
+
bootstrapTimings.rowFrameEncodeMs += Math.max(
|
|
932
|
+
0,
|
|
933
|
+
Date.now() - rowFrameEncodeStartedAt
|
|
934
|
+
);
|
|
935
|
+
const bundleMaxBytes = resolveSnapshotBundleMaxBytes({
|
|
936
|
+
configuredMaxBytes: tableHandler.snapshotBundleMaxBytes,
|
|
937
|
+
pageRowCount: page.rows?.length ?? 0,
|
|
938
|
+
pageRowFrameBytes: rowFrames.length,
|
|
939
|
+
});
|
|
940
|
+
if (
|
|
941
|
+
activeBundle.pageCount > 0 &&
|
|
942
|
+
activeBundle.rowFrameByteLength + rowFrames.length >
|
|
943
|
+
bundleMaxBytes
|
|
944
|
+
) {
|
|
945
|
+
await flushSnapshotBundle(activeBundle);
|
|
946
|
+
const bundleHeader = EMPTY_SNAPSHOT_ROW_FRAMES;
|
|
947
|
+
activeBundle = {
|
|
948
|
+
table: nextTableName,
|
|
949
|
+
startCursor: nextState.rowCursor,
|
|
950
|
+
isFirstPage: nextState.rowCursor == null,
|
|
951
|
+
isLastPage: false,
|
|
952
|
+
pageCount: 0,
|
|
953
|
+
ttlMs:
|
|
954
|
+
tableHandler.snapshotChunkTtlMs ??
|
|
955
|
+
24 * 60 * 60 * 1000,
|
|
956
|
+
rowFrameByteLength: bundleHeader.length,
|
|
957
|
+
rowFrameParts: [bundleHeader],
|
|
958
|
+
inlineRows: null,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (
|
|
963
|
+
preferInlineBootstrapSnapshot &&
|
|
964
|
+
activeBundle.pageCount === 0 &&
|
|
965
|
+
page.nextCursor == null &&
|
|
966
|
+
rowFrames.length <=
|
|
967
|
+
DEFAULT_INLINE_SNAPSHOT_ROW_FRAME_BYTES
|
|
968
|
+
) {
|
|
969
|
+
activeBundle.inlineRows = page.rows ?? [];
|
|
970
|
+
} else {
|
|
971
|
+
activeBundle.inlineRows = null;
|
|
972
|
+
}
|
|
973
|
+
activeBundle.rowFrameParts.push(rowFrames);
|
|
974
|
+
activeBundle.rowFrameByteLength += rowFrames.length;
|
|
975
|
+
activeBundle.pageCount += 1;
|
|
976
|
+
|
|
977
|
+
if (page.nextCursor != null) {
|
|
978
|
+
nextState = {
|
|
979
|
+
...nextState,
|
|
980
|
+
rowCursor: page.nextCursor,
|
|
981
|
+
};
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
activeBundle.isLastPage = true;
|
|
986
|
+
await flushSnapshotBundle(activeBundle);
|
|
987
|
+
activeBundle = null;
|
|
988
|
+
|
|
989
|
+
if (nextState.tableIndex + 1 < nextState.tables.length) {
|
|
990
|
+
nextState = {
|
|
991
|
+
...nextState,
|
|
992
|
+
tableIndex: nextState.tableIndex + 1,
|
|
993
|
+
rowCursor: null,
|
|
994
|
+
};
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
nextState = null;
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (activeBundle) {
|
|
1003
|
+
await flushSnapshotBundle(activeBundle);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
subResponses.push({
|
|
1007
|
+
id: sub.id,
|
|
1008
|
+
status: 'active',
|
|
1009
|
+
scopes: effectiveScopes,
|
|
1010
|
+
bootstrap: true,
|
|
1011
|
+
bootstrapState: nextState,
|
|
1012
|
+
nextCursor: effectiveState.asOfCommitSeq,
|
|
1013
|
+
commits: [],
|
|
1014
|
+
snapshots,
|
|
1015
|
+
});
|
|
1016
|
+
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
1017
|
+
continue;
|
|
844
1018
|
}
|
|
845
|
-
nextState = null;
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
1019
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
await flushSnapshotBundle(activeBundle);
|
|
856
|
-
}
|
|
857
|
-
const bundleHeader = EMPTY_SNAPSHOT_ROW_FRAMES;
|
|
858
|
-
activeBundle = {
|
|
859
|
-
table: nextTableName,
|
|
860
|
-
startCursor: nextState.rowCursor,
|
|
861
|
-
isFirstPage: nextState.rowCursor == null,
|
|
862
|
-
isLastPage: false,
|
|
863
|
-
pageCount: 0,
|
|
864
|
-
ttlMs:
|
|
865
|
-
tableHandler.snapshotChunkTtlMs ?? 24 * 60 * 60 * 1000,
|
|
866
|
-
rowFrameByteLength: bundleHeader.length,
|
|
867
|
-
rowFrameParts: [bundleHeader],
|
|
868
|
-
inlineRows: null,
|
|
869
|
-
};
|
|
870
|
-
}
|
|
1020
|
+
// Incremental pull for this subscription. The dialect row query
|
|
1021
|
+
// carries the scanned commit-window max when matching rows exist,
|
|
1022
|
+
// so we only need a separate commit-window scan when the row query
|
|
1023
|
+
// returns no matches at all.
|
|
1024
|
+
const incrementalRows: IncrementalPullRow[] = [];
|
|
1025
|
+
let maxScannedCommitSeq = cursor;
|
|
871
1026
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
await tableHandler.snapshot(
|
|
1027
|
+
for await (const row of dialect.iterateIncrementalPullRows(
|
|
1028
|
+
trx,
|
|
875
1029
|
{
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const rowFrameEncodeStartedAt = Date.now();
|
|
891
|
-
const rowFrames = encodeSnapshotRowFrames(page.rows ?? []);
|
|
892
|
-
bootstrapTimings.rowFrameEncodeMs += Math.max(
|
|
893
|
-
0,
|
|
894
|
-
Date.now() - rowFrameEncodeStartedAt
|
|
895
|
-
);
|
|
896
|
-
const bundleMaxBytes = resolveSnapshotBundleMaxBytes({
|
|
897
|
-
configuredMaxBytes: tableHandler.snapshotBundleMaxBytes,
|
|
898
|
-
pageRowCount: page.rows?.length ?? 0,
|
|
899
|
-
pageRowFrameBytes: rowFrames.length,
|
|
900
|
-
});
|
|
901
|
-
if (
|
|
902
|
-
activeBundle.pageCount > 0 &&
|
|
903
|
-
activeBundle.rowFrameByteLength + rowFrames.length >
|
|
904
|
-
bundleMaxBytes
|
|
905
|
-
) {
|
|
906
|
-
await flushSnapshotBundle(activeBundle);
|
|
907
|
-
const bundleHeader = EMPTY_SNAPSHOT_ROW_FRAMES;
|
|
908
|
-
activeBundle = {
|
|
909
|
-
table: nextTableName,
|
|
910
|
-
startCursor: nextState.rowCursor,
|
|
911
|
-
isFirstPage: nextState.rowCursor == null,
|
|
912
|
-
isLastPage: false,
|
|
913
|
-
pageCount: 0,
|
|
914
|
-
ttlMs:
|
|
915
|
-
tableHandler.snapshotChunkTtlMs ?? 24 * 60 * 60 * 1000,
|
|
916
|
-
rowFrameByteLength: bundleHeader.length,
|
|
917
|
-
rowFrameParts: [bundleHeader],
|
|
918
|
-
inlineRows: null,
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
if (
|
|
923
|
-
preferInlineBootstrapSnapshot &&
|
|
924
|
-
activeBundle.pageCount === 0 &&
|
|
925
|
-
page.nextCursor == null &&
|
|
926
|
-
rowFrames.length <= DEFAULT_INLINE_SNAPSHOT_ROW_FRAME_BYTES
|
|
927
|
-
) {
|
|
928
|
-
activeBundle.inlineRows = page.rows ?? [];
|
|
929
|
-
} else {
|
|
930
|
-
activeBundle.inlineRows = null;
|
|
931
|
-
}
|
|
932
|
-
activeBundle.rowFrameParts.push(rowFrames);
|
|
933
|
-
activeBundle.rowFrameByteLength += rowFrames.length;
|
|
934
|
-
activeBundle.pageCount += 1;
|
|
935
|
-
|
|
936
|
-
if (page.nextCursor != null) {
|
|
937
|
-
nextState = { ...nextState, rowCursor: page.nextCursor };
|
|
938
|
-
continue;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
activeBundle.isLastPage = true;
|
|
942
|
-
await flushSnapshotBundle(activeBundle);
|
|
943
|
-
activeBundle = null;
|
|
944
|
-
|
|
945
|
-
if (nextState.tableIndex + 1 < nextState.tables.length) {
|
|
946
|
-
nextState = {
|
|
947
|
-
...nextState,
|
|
948
|
-
tableIndex: nextState.tableIndex + 1,
|
|
949
|
-
rowCursor: null,
|
|
950
|
-
};
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
nextState = null;
|
|
955
|
-
break;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (activeBundle) {
|
|
959
|
-
await flushSnapshotBundle(activeBundle);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
subResponses.push({
|
|
963
|
-
id: sub.id,
|
|
964
|
-
status: 'active',
|
|
965
|
-
scopes: effectiveScopes,
|
|
966
|
-
bootstrap: true,
|
|
967
|
-
bootstrapState: nextState,
|
|
968
|
-
nextCursor: effectiveState.asOfCommitSeq,
|
|
969
|
-
commits: [],
|
|
970
|
-
snapshots,
|
|
971
|
-
});
|
|
972
|
-
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// Incremental pull for this subscription
|
|
977
|
-
// Read the commit window for this table up-front so the subscription cursor
|
|
978
|
-
// can advance past commits that don't match the requested scopes.
|
|
979
|
-
const scannedCommitSeqs = await dialect.readCommitSeqsForPull(trx, {
|
|
980
|
-
partitionId,
|
|
981
|
-
cursor,
|
|
982
|
-
limitCommits,
|
|
983
|
-
tables: [sub.table],
|
|
984
|
-
});
|
|
985
|
-
const maxScannedCommitSeq =
|
|
986
|
-
scannedCommitSeqs.length > 0
|
|
987
|
-
? scannedCommitSeqs[scannedCommitSeqs.length - 1]!
|
|
988
|
-
: cursor;
|
|
989
|
-
|
|
990
|
-
if (scannedCommitSeqs.length === 0) {
|
|
991
|
-
subResponses.push({
|
|
992
|
-
id: sub.id,
|
|
993
|
-
status: 'active',
|
|
994
|
-
scopes: effectiveScopes,
|
|
995
|
-
bootstrap: false,
|
|
996
|
-
nextCursor: cursor,
|
|
997
|
-
commits: [],
|
|
998
|
-
});
|
|
999
|
-
nextCursors.push(cursor);
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1030
|
+
partitionId,
|
|
1031
|
+
table: sub.table,
|
|
1032
|
+
scopes: effectiveScopes,
|
|
1033
|
+
cursor,
|
|
1034
|
+
limitCommits,
|
|
1035
|
+
}
|
|
1036
|
+
)) {
|
|
1037
|
+
incrementalRows.push(row);
|
|
1038
|
+
maxScannedCommitSeq = Math.max(
|
|
1039
|
+
maxScannedCommitSeq,
|
|
1040
|
+
row.scanned_max_commit_seq ?? row.commit_seq
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1002
1043
|
|
|
1003
|
-
|
|
1044
|
+
if (incrementalRows.length === 0) {
|
|
1045
|
+
const scannedCommitSeqs =
|
|
1046
|
+
await dialect.readCommitSeqsForPull(trx, {
|
|
1047
|
+
partitionId,
|
|
1048
|
+
cursor,
|
|
1049
|
+
limitCommits,
|
|
1050
|
+
tables: [sub.table],
|
|
1051
|
+
});
|
|
1052
|
+
maxScannedCommitSeq =
|
|
1053
|
+
scannedCommitSeqs.length > 0
|
|
1054
|
+
? scannedCommitSeqs[scannedCommitSeqs.length - 1]!
|
|
1055
|
+
: cursor;
|
|
1056
|
+
|
|
1057
|
+
if (scannedCommitSeqs.length === 0) {
|
|
1058
|
+
subResponses.push({
|
|
1059
|
+
id: sub.id,
|
|
1060
|
+
status: 'active',
|
|
1061
|
+
scopes: effectiveScopes,
|
|
1062
|
+
bootstrap: false,
|
|
1063
|
+
nextCursor: cursor,
|
|
1064
|
+
commits: [],
|
|
1065
|
+
});
|
|
1066
|
+
nextCursors.push(cursor);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1004
1070
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1071
|
+
let nextCursor = cursor;
|
|
1072
|
+
|
|
1073
|
+
if (dedupeRows) {
|
|
1074
|
+
const latestByRowKey = new Map<
|
|
1075
|
+
string,
|
|
1076
|
+
{
|
|
1077
|
+
commitSeq: number;
|
|
1078
|
+
createdAt: string;
|
|
1079
|
+
actorId: string;
|
|
1080
|
+
change: SyncChange;
|
|
1081
|
+
}
|
|
1082
|
+
>();
|
|
1083
|
+
|
|
1084
|
+
for (const r of incrementalRows) {
|
|
1085
|
+
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
1086
|
+
const rowKey = `${r.table}\u0000${r.row_id}`;
|
|
1087
|
+
const change: SyncChange = {
|
|
1088
|
+
table: r.table,
|
|
1089
|
+
row_id: r.row_id,
|
|
1090
|
+
op: r.op,
|
|
1091
|
+
row_json: r.row_json,
|
|
1092
|
+
row_version: r.row_version,
|
|
1093
|
+
scopes: r.scopes,
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// Move row keys to insertion tail so Map iteration yields
|
|
1097
|
+
// "latest change wins" order without a full array sort.
|
|
1098
|
+
if (latestByRowKey.has(rowKey)) {
|
|
1099
|
+
latestByRowKey.delete(rowKey);
|
|
1100
|
+
}
|
|
1101
|
+
latestByRowKey.set(rowKey, {
|
|
1102
|
+
commitSeq: r.commit_seq,
|
|
1103
|
+
createdAt: r.created_at,
|
|
1104
|
+
actorId: r.actor_id,
|
|
1105
|
+
change,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|
|
1110
|
+
|
|
1111
|
+
if (latestByRowKey.size === 0) {
|
|
1112
|
+
subResponses.push({
|
|
1113
|
+
id: sub.id,
|
|
1114
|
+
status: 'active',
|
|
1115
|
+
scopes: effectiveScopes,
|
|
1116
|
+
bootstrap: false,
|
|
1117
|
+
nextCursor,
|
|
1118
|
+
commits: [],
|
|
1119
|
+
});
|
|
1120
|
+
nextCursors.push(nextCursor);
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const commits: SyncCommit[] = [];
|
|
1125
|
+
for (const item of latestByRowKey.values()) {
|
|
1126
|
+
const lastCommit = commits[commits.length - 1];
|
|
1127
|
+
if (
|
|
1128
|
+
!lastCommit ||
|
|
1129
|
+
lastCommit.commitSeq !== item.commitSeq
|
|
1130
|
+
) {
|
|
1131
|
+
commits.push({
|
|
1132
|
+
commitSeq: item.commitSeq,
|
|
1133
|
+
createdAt: item.createdAt,
|
|
1134
|
+
actorId: item.actorId,
|
|
1135
|
+
changes: [item.change],
|
|
1136
|
+
});
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
lastCommit.changes.push(item.change);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
subResponses.push({
|
|
1143
|
+
id: sub.id,
|
|
1144
|
+
status: 'active',
|
|
1145
|
+
scopes: effectiveScopes,
|
|
1146
|
+
bootstrap: false,
|
|
1147
|
+
nextCursor,
|
|
1148
|
+
commits,
|
|
1149
|
+
});
|
|
1150
|
+
nextCursors.push(nextCursor);
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1033
1153
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1154
|
+
const commitsBySeq = new Map<number, SyncCommit>();
|
|
1155
|
+
const commitSeqs: number[] = [];
|
|
1156
|
+
|
|
1157
|
+
for (const r of incrementalRows) {
|
|
1158
|
+
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
1159
|
+
const seq = r.commit_seq;
|
|
1160
|
+
let commit = commitsBySeq.get(seq);
|
|
1161
|
+
if (!commit) {
|
|
1162
|
+
commit = {
|
|
1163
|
+
commitSeq: seq,
|
|
1164
|
+
createdAt: r.created_at,
|
|
1165
|
+
actorId: r.actor_id,
|
|
1166
|
+
changes: [],
|
|
1167
|
+
};
|
|
1168
|
+
commitsBySeq.set(seq, commit);
|
|
1169
|
+
commitSeqs.push(seq);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const change: SyncChange = {
|
|
1173
|
+
table: r.table,
|
|
1174
|
+
row_id: r.row_id,
|
|
1175
|
+
op: r.op,
|
|
1176
|
+
row_json: r.row_json,
|
|
1177
|
+
row_version: r.row_version,
|
|
1178
|
+
scopes: r.scopes,
|
|
1179
|
+
};
|
|
1180
|
+
commit.changes.push(change);
|
|
1181
|
+
}
|
|
1046
1182
|
|
|
1047
|
-
|
|
1183
|
+
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|
|
1048
1184
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1185
|
+
if (commitSeqs.length === 0) {
|
|
1186
|
+
subResponses.push({
|
|
1187
|
+
id: sub.id,
|
|
1188
|
+
status: 'active',
|
|
1189
|
+
scopes: effectiveScopes,
|
|
1190
|
+
bootstrap: false,
|
|
1191
|
+
nextCursor,
|
|
1192
|
+
commits: [],
|
|
1193
|
+
});
|
|
1194
|
+
nextCursors.push(nextCursor);
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1061
1197
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1198
|
+
const commits: SyncCommit[] = commitSeqs
|
|
1199
|
+
.map((seq) => commitsBySeq.get(seq))
|
|
1200
|
+
.filter((c): c is SyncCommit => !!c)
|
|
1201
|
+
.filter((c) => c.changes.length > 0);
|
|
1202
|
+
|
|
1203
|
+
subResponses.push({
|
|
1204
|
+
id: sub.id,
|
|
1205
|
+
status: 'active',
|
|
1206
|
+
scopes: effectiveScopes,
|
|
1207
|
+
bootstrap: false,
|
|
1208
|
+
nextCursor,
|
|
1209
|
+
commits,
|
|
1071
1210
|
});
|
|
1072
|
-
|
|
1211
|
+
nextCursors.push(nextCursor);
|
|
1073
1212
|
}
|
|
1074
|
-
lastCommit.changes.push(item.change);
|
|
1075
|
-
}
|
|
1076
1213
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const commitSeqs: number[] = [];
|
|
1091
|
-
|
|
1092
|
-
for await (const r of dialect.iterateIncrementalPullRows(trx, {
|
|
1093
|
-
partitionId,
|
|
1094
|
-
table: sub.table,
|
|
1095
|
-
scopes: effectiveScopes,
|
|
1096
|
-
cursor,
|
|
1097
|
-
limitCommits,
|
|
1098
|
-
})) {
|
|
1099
|
-
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
1100
|
-
const seq = r.commit_seq;
|
|
1101
|
-
let commit = commitsBySeq.get(seq);
|
|
1102
|
-
if (!commit) {
|
|
1103
|
-
commit = {
|
|
1104
|
-
commitSeq: seq,
|
|
1105
|
-
createdAt: r.created_at,
|
|
1106
|
-
actorId: r.actor_id,
|
|
1107
|
-
changes: [],
|
|
1214
|
+
const effectiveScopes = mergeScopes(activeSubscriptions);
|
|
1215
|
+
const clientCursor =
|
|
1216
|
+
nextCursors.length > 0
|
|
1217
|
+
? Math.min(...nextCursors)
|
|
1218
|
+
: maxCommitSeq;
|
|
1219
|
+
|
|
1220
|
+
return {
|
|
1221
|
+
response: {
|
|
1222
|
+
ok: true as const,
|
|
1223
|
+
subscriptions: subResponses,
|
|
1224
|
+
},
|
|
1225
|
+
effectiveScopes,
|
|
1226
|
+
clientCursor,
|
|
1108
1227
|
};
|
|
1109
|
-
commitsBySeq.set(seq, commit);
|
|
1110
|
-
commitSeqs.push(seq);
|
|
1111
1228
|
}
|
|
1229
|
+
);
|
|
1112
1230
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
commit.changes.push(change);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|
|
1125
|
-
|
|
1126
|
-
if (commitSeqs.length === 0) {
|
|
1127
|
-
subResponses.push({
|
|
1128
|
-
id: sub.id,
|
|
1129
|
-
status: 'active',
|
|
1130
|
-
scopes: effectiveScopes,
|
|
1131
|
-
bootstrap: false,
|
|
1132
|
-
nextCursor,
|
|
1133
|
-
commits: [],
|
|
1134
|
-
});
|
|
1135
|
-
nextCursors.push(nextCursor);
|
|
1136
|
-
continue;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
const commits: SyncCommit[] = commitSeqs
|
|
1140
|
-
.map((seq) => commitsBySeq.get(seq))
|
|
1141
|
-
.filter((c): c is SyncCommit => !!c)
|
|
1142
|
-
.filter((c) => c.changes.length > 0);
|
|
1143
|
-
|
|
1144
|
-
subResponses.push({
|
|
1145
|
-
id: sub.id,
|
|
1146
|
-
status: 'active',
|
|
1147
|
-
scopes: effectiveScopes,
|
|
1148
|
-
bootstrap: false,
|
|
1149
|
-
nextCursor,
|
|
1150
|
-
commits,
|
|
1151
|
-
});
|
|
1152
|
-
nextCursors.push(nextCursor);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
const effectiveScopes = mergeScopes(activeSubscriptions);
|
|
1156
|
-
const clientCursor =
|
|
1157
|
-
nextCursors.length > 0 ? Math.min(...nextCursors) : maxCommitSeq;
|
|
1158
|
-
|
|
1159
|
-
return {
|
|
1160
|
-
response: {
|
|
1161
|
-
ok: true as const,
|
|
1162
|
-
subscriptions: subResponses,
|
|
1163
|
-
},
|
|
1164
|
-
effectiveScopes,
|
|
1165
|
-
clientCursor,
|
|
1166
|
-
};
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
const chunkStorage = args.chunkStorage;
|
|
1170
|
-
if (chunkStorage && pendingExternalChunkWrites.length > 0) {
|
|
1171
|
-
await runWithConcurrency(
|
|
1172
|
-
pendingExternalChunkWrites,
|
|
1173
|
-
4,
|
|
1174
|
-
async (pending) => {
|
|
1175
|
-
const cacheLookupStartedAt = Date.now();
|
|
1176
|
-
let chunkRef = await readSnapshotChunkRefByPageKey(db, {
|
|
1177
|
-
partitionId: pending.cacheLookup.partitionId,
|
|
1178
|
-
scopeKey: pending.cacheLookup.scopeKey,
|
|
1179
|
-
scope: pending.cacheLookup.scope,
|
|
1180
|
-
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
1181
|
-
rowCursor: pending.cacheLookup.rowCursor,
|
|
1182
|
-
rowLimit: pending.cacheLookup.rowLimit,
|
|
1183
|
-
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1184
|
-
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1185
|
-
});
|
|
1186
|
-
bootstrapTimings.chunkCacheLookupMs += Math.max(
|
|
1187
|
-
0,
|
|
1188
|
-
Date.now() - cacheLookupStartedAt
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
if (!chunkRef) {
|
|
1192
|
-
if (chunkStorage.storeChunkStream) {
|
|
1193
|
-
const {
|
|
1194
|
-
stream: bodyStream,
|
|
1195
|
-
byteLength,
|
|
1196
|
-
sha256,
|
|
1197
|
-
gzipMs,
|
|
1198
|
-
hashMs,
|
|
1199
|
-
} = await encodeCompressedSnapshotChunkToStream(
|
|
1200
|
-
pending.rowFrameParts
|
|
1201
|
-
);
|
|
1202
|
-
bootstrapTimings.chunkGzipMs += gzipMs;
|
|
1203
|
-
bootstrapTimings.chunkHashMs += hashMs;
|
|
1204
|
-
const chunkPersistStartedAt = Date.now();
|
|
1205
|
-
chunkRef = await chunkStorage.storeChunkStream({
|
|
1231
|
+
const chunkStorage = args.chunkStorage;
|
|
1232
|
+
if (chunkStorage && pendingExternalChunkWrites.length > 0) {
|
|
1233
|
+
await runWithConcurrency(
|
|
1234
|
+
pendingExternalChunkWrites,
|
|
1235
|
+
4,
|
|
1236
|
+
async (pending) => {
|
|
1237
|
+
const cacheLookupStartedAt = Date.now();
|
|
1238
|
+
let chunkRef = await readSnapshotChunkRefByPageKey(db, {
|
|
1206
1239
|
partitionId: pending.cacheLookup.partitionId,
|
|
1207
1240
|
scopeKey: pending.cacheLookup.scopeKey,
|
|
1208
1241
|
scope: pending.cacheLookup.scope,
|
|
@@ -1211,77 +1244,121 @@ export async function pull<
|
|
|
1211
1244
|
rowLimit: pending.cacheLookup.rowLimit,
|
|
1212
1245
|
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1213
1246
|
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1214
|
-
sha256,
|
|
1215
|
-
byteLength,
|
|
1216
|
-
bodyStream,
|
|
1217
|
-
expiresAt: pending.expiresAt,
|
|
1218
1247
|
});
|
|
1219
|
-
bootstrapTimings.
|
|
1248
|
+
bootstrapTimings.chunkCacheLookupMs += Math.max(
|
|
1220
1249
|
0,
|
|
1221
|
-
Date.now() -
|
|
1222
|
-
);
|
|
1223
|
-
} else {
|
|
1224
|
-
const encodedChunk = await encodeCompressedSnapshotChunk(
|
|
1225
|
-
pending.rowFrameParts
|
|
1250
|
+
Date.now() - cacheLookupStartedAt
|
|
1226
1251
|
);
|
|
1227
|
-
bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
|
|
1228
|
-
bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
|
|
1229
|
-
const chunkPersistStartedAt = Date.now();
|
|
1230
|
-
chunkRef = await chunkStorage.storeChunk({
|
|
1231
|
-
partitionId: pending.cacheLookup.partitionId,
|
|
1232
|
-
scopeKey: pending.cacheLookup.scopeKey,
|
|
1233
|
-
scope: pending.cacheLookup.scope,
|
|
1234
|
-
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
1235
|
-
rowCursor: pending.cacheLookup.rowCursor,
|
|
1236
|
-
rowLimit: pending.cacheLookup.rowLimit,
|
|
1237
|
-
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1238
|
-
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1239
|
-
sha256: encodedChunk.sha256,
|
|
1240
|
-
body: encodedChunk.body,
|
|
1241
|
-
expiresAt: pending.expiresAt,
|
|
1242
|
-
});
|
|
1243
|
-
bootstrapTimings.chunkPersistMs += Math.max(
|
|
1244
|
-
0,
|
|
1245
|
-
Date.now() - chunkPersistStartedAt
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
1252
|
|
|
1250
|
-
|
|
1253
|
+
if (!chunkRef) {
|
|
1254
|
+
if (chunkStorage.storeChunkStream) {
|
|
1255
|
+
const {
|
|
1256
|
+
stream: bodyStream,
|
|
1257
|
+
byteLength,
|
|
1258
|
+
sha256,
|
|
1259
|
+
gzipMs,
|
|
1260
|
+
hashMs,
|
|
1261
|
+
} = await encodeCompressedSnapshotChunkToStream(
|
|
1262
|
+
pending.rowFrameParts
|
|
1263
|
+
);
|
|
1264
|
+
bootstrapTimings.chunkGzipMs += gzipMs;
|
|
1265
|
+
bootstrapTimings.chunkHashMs += hashMs;
|
|
1266
|
+
const chunkPersistStartedAt = Date.now();
|
|
1267
|
+
chunkRef = await chunkStorage.storeChunkStream({
|
|
1268
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
1269
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
1270
|
+
scope: pending.cacheLookup.scope,
|
|
1271
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
1272
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
1273
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
1274
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1275
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1276
|
+
sha256,
|
|
1277
|
+
byteLength,
|
|
1278
|
+
bodyStream,
|
|
1279
|
+
expiresAt: pending.expiresAt,
|
|
1280
|
+
});
|
|
1281
|
+
bootstrapTimings.chunkPersistMs += Math.max(
|
|
1282
|
+
0,
|
|
1283
|
+
Date.now() - chunkPersistStartedAt
|
|
1284
|
+
);
|
|
1285
|
+
} else {
|
|
1286
|
+
const encodedChunk = await encodeCompressedSnapshotChunk(
|
|
1287
|
+
pending.rowFrameParts
|
|
1288
|
+
);
|
|
1289
|
+
bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
|
|
1290
|
+
bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
|
|
1291
|
+
const chunkPersistStartedAt = Date.now();
|
|
1292
|
+
chunkRef = await chunkStorage.storeChunk({
|
|
1293
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
1294
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
1295
|
+
scope: pending.cacheLookup.scope,
|
|
1296
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
1297
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
1298
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
1299
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1300
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1301
|
+
sha256: encodedChunk.sha256,
|
|
1302
|
+
body: encodedChunk.body,
|
|
1303
|
+
expiresAt: pending.expiresAt,
|
|
1304
|
+
});
|
|
1305
|
+
bootstrapTimings.chunkPersistMs += Math.max(
|
|
1306
|
+
0,
|
|
1307
|
+
Date.now() - chunkPersistStartedAt
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
pending.snapshot.chunks = [chunkRef];
|
|
1313
|
+
}
|
|
1314
|
+
);
|
|
1251
1315
|
}
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
1316
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1317
|
+
const durationMs = Math.max(0, Date.now() - startedAtMs);
|
|
1318
|
+
const stats = summarizePullResponse(result.response);
|
|
1319
|
+
|
|
1320
|
+
span.setAttribute('status', 'ok');
|
|
1321
|
+
span.setAttribute('duration_ms', durationMs);
|
|
1322
|
+
span.setAttribute('subscription_count', stats.subscriptionCount);
|
|
1323
|
+
span.setAttribute('commit_count', stats.commitCount);
|
|
1324
|
+
span.setAttribute('change_count', stats.changeCount);
|
|
1325
|
+
span.setAttribute('snapshot_page_count', stats.snapshotPageCount);
|
|
1326
|
+
span.setAttributes({
|
|
1327
|
+
bootstrap_snapshot_query_ms: bootstrapTimings.snapshotQueryMs,
|
|
1328
|
+
bootstrap_row_frame_encode_ms: bootstrapTimings.rowFrameEncodeMs,
|
|
1329
|
+
bootstrap_chunk_cache_lookup_ms:
|
|
1330
|
+
bootstrapTimings.chunkCacheLookupMs,
|
|
1331
|
+
bootstrap_chunk_gzip_ms: bootstrapTimings.chunkGzipMs,
|
|
1332
|
+
bootstrap_chunk_hash_ms: bootstrapTimings.chunkHashMs,
|
|
1333
|
+
bootstrap_chunk_persist_ms: bootstrapTimings.chunkPersistMs,
|
|
1334
|
+
});
|
|
1335
|
+
span.setStatus('ok');
|
|
1257
1336
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
span.setAttributes({
|
|
1265
|
-
bootstrap_snapshot_query_ms: bootstrapTimings.snapshotQueryMs,
|
|
1266
|
-
bootstrap_row_frame_encode_ms: bootstrapTimings.rowFrameEncodeMs,
|
|
1267
|
-
bootstrap_chunk_cache_lookup_ms: bootstrapTimings.chunkCacheLookupMs,
|
|
1268
|
-
bootstrap_chunk_gzip_ms: bootstrapTimings.chunkGzipMs,
|
|
1269
|
-
bootstrap_chunk_hash_ms: bootstrapTimings.chunkHashMs,
|
|
1270
|
-
bootstrap_chunk_persist_ms: bootstrapTimings.chunkPersistMs,
|
|
1271
|
-
});
|
|
1272
|
-
span.setStatus('ok');
|
|
1337
|
+
recordPullMetrics({
|
|
1338
|
+
status: 'ok',
|
|
1339
|
+
dedupeRows,
|
|
1340
|
+
durationMs,
|
|
1341
|
+
stats,
|
|
1342
|
+
});
|
|
1273
1343
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1344
|
+
return {
|
|
1345
|
+
...result,
|
|
1346
|
+
bootstrapTimings,
|
|
1347
|
+
};
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
if (
|
|
1350
|
+
error instanceof Error &&
|
|
1351
|
+
attemptIndex < MAX_PULL_TRANSACTION_RETRIES - 1 &&
|
|
1352
|
+
isSerializablePullError(error)
|
|
1353
|
+
) {
|
|
1354
|
+
await delay(PULL_TRANSACTION_RETRY_DELAY_MS * (attemptIndex + 1));
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1280
1360
|
|
|
1281
|
-
|
|
1282
|
-
...result,
|
|
1283
|
-
bootstrapTimings,
|
|
1284
|
-
};
|
|
1361
|
+
throw new Error('Pull transaction retry loop exhausted unexpectedly');
|
|
1285
1362
|
} catch (error) {
|
|
1286
1363
|
const durationMs = Math.max(0, Date.now() - startedAtMs);
|
|
1287
1364
|
|