@syncular/server 0.0.6-90 → 0.0.6-93
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/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +98 -66
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/pull.d.ts +7 -0
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +93 -47
- package/dist/pull.js.map +1 -1
- package/dist/subscriptions/cache.d.ts +55 -0
- package/dist/subscriptions/cache.d.ts.map +1 -0
- package/dist/subscriptions/cache.js +206 -0
- package/dist/subscriptions/cache.js.map +1 -0
- package/dist/subscriptions/index.d.ts +1 -0
- package/dist/subscriptions/index.d.ts.map +1 -1
- package/dist/subscriptions/index.js +1 -0
- package/dist/subscriptions/index.js.map +1 -1
- package/dist/subscriptions/resolve.d.ts +2 -0
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +72 -11
- package/dist/subscriptions/resolve.js.map +1 -1
- package/package.json +2 -2
- package/src/handlers/create-handler.ts +136 -91
- package/src/pull.ts +120 -50
- package/src/subscriptions/cache.ts +318 -0
- package/src/subscriptions/index.ts +1 -0
- package/src/subscriptions/resolve.test.ts +180 -0
- package/src/subscriptions/resolve.ts +85 -15
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
UpdateQueryBuilder,
|
|
32
32
|
UpdateResult,
|
|
33
33
|
} from 'kysely';
|
|
34
|
+
import { sql } from 'kysely';
|
|
34
35
|
import type { SyncCoreDb } from '../schema';
|
|
35
36
|
import type {
|
|
36
37
|
ApplyOperationResult,
|
|
@@ -67,6 +68,14 @@ function isConstraintViolationError(message: string): boolean {
|
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function isMissingColumnReferenceError(message: string): boolean {
|
|
72
|
+
const normalized = message.toLowerCase();
|
|
73
|
+
return (
|
|
74
|
+
normalized.includes('no such column') ||
|
|
75
|
+
(normalized.includes('column') && normalized.includes('does not exist'))
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
/**
|
|
71
80
|
* Scope definition for a column - maps scope variable to column name.
|
|
72
81
|
*/
|
|
@@ -505,96 +514,19 @@ export function createServerHandler<
|
|
|
505
514
|
: {};
|
|
506
515
|
const payload = applyInboundTransform(payloadRecord, ctx.schemaVersion);
|
|
507
516
|
|
|
508
|
-
// Check whether the row exists and fetch only version metadata for hot path.
|
|
509
|
-
const existingRow = await (
|
|
510
|
-
trx.selectFrom(table) as SelectQueryBuilder<
|
|
511
|
-
ServerDB,
|
|
512
|
-
keyof ServerDB & string,
|
|
513
|
-
Record<string, unknown>
|
|
514
|
-
>
|
|
515
|
-
)
|
|
516
|
-
.select(ref<string>(versionColumn))
|
|
517
|
-
.where(ref<string>(primaryKey), '=', op.row_id)
|
|
518
|
-
.executeTakeFirst();
|
|
519
|
-
|
|
520
|
-
const hasExistingRow = existingRow !== undefined;
|
|
521
|
-
const existingVersion =
|
|
522
|
-
(existingRow?.[versionColumn] as number | undefined) ?? 0;
|
|
523
|
-
|
|
524
|
-
// Check version conflict
|
|
525
|
-
if (
|
|
526
|
-
hasExistingRow &&
|
|
527
|
-
op.base_version != null &&
|
|
528
|
-
existingVersion !== op.base_version
|
|
529
|
-
) {
|
|
530
|
-
const conflictRow = await (
|
|
531
|
-
trx.selectFrom(table).selectAll() as SelectQueryBuilder<
|
|
532
|
-
ServerDB,
|
|
533
|
-
keyof ServerDB & string,
|
|
534
|
-
Record<string, unknown>
|
|
535
|
-
>
|
|
536
|
-
)
|
|
537
|
-
.where(ref<string>(primaryKey), '=', op.row_id)
|
|
538
|
-
.executeTakeFirst();
|
|
539
|
-
|
|
540
|
-
if (!conflictRow) {
|
|
541
|
-
return {
|
|
542
|
-
result: {
|
|
543
|
-
opIndex,
|
|
544
|
-
status: 'error',
|
|
545
|
-
error: 'ROW_NOT_FOUND_FOR_BASE_VERSION',
|
|
546
|
-
code: 'ROW_MISSING',
|
|
547
|
-
retriable: false,
|
|
548
|
-
},
|
|
549
|
-
emittedChanges: [],
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return {
|
|
554
|
-
result: {
|
|
555
|
-
opIndex,
|
|
556
|
-
status: 'conflict',
|
|
557
|
-
message: `Version conflict: server=${existingVersion}, base=${op.base_version}`,
|
|
558
|
-
server_version: existingVersion,
|
|
559
|
-
server_row: applyOutboundTransform(
|
|
560
|
-
conflictRow as Selectable<ServerDB[TableName]>
|
|
561
|
-
),
|
|
562
|
-
},
|
|
563
|
-
emittedChanges: [],
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// If the client provided a base version, they expected this row to exist.
|
|
568
|
-
// A missing row usually indicates stale local state after a server reset.
|
|
569
|
-
if (!hasExistingRow && op.base_version != null) {
|
|
570
|
-
return {
|
|
571
|
-
result: {
|
|
572
|
-
opIndex,
|
|
573
|
-
status: 'error',
|
|
574
|
-
error: 'ROW_NOT_FOUND_FOR_BASE_VERSION',
|
|
575
|
-
code: 'ROW_MISSING',
|
|
576
|
-
retriable: false,
|
|
577
|
-
},
|
|
578
|
-
emittedChanges: [],
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const nextVersion = existingVersion + 1;
|
|
583
|
-
|
|
584
517
|
let updated: Record<string, unknown> | undefined;
|
|
585
518
|
let constraintError: { message: string; code: string } | null = null;
|
|
586
519
|
|
|
587
520
|
try {
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
const
|
|
521
|
+
if (op.base_version != null) {
|
|
522
|
+
const expectedVersion = op.base_version;
|
|
523
|
+
const conditionalUpdateSet: Record<string, unknown> = {
|
|
591
524
|
...payload,
|
|
592
|
-
[versionColumn]:
|
|
525
|
+
[versionColumn]: expectedVersion + 1,
|
|
593
526
|
};
|
|
594
|
-
|
|
595
|
-
delete updateSet[primaryKey];
|
|
527
|
+
delete conditionalUpdateSet[primaryKey];
|
|
596
528
|
for (const col of Object.values(scopeColumns)) {
|
|
597
|
-
delete
|
|
529
|
+
delete conditionalUpdateSet[col];
|
|
598
530
|
}
|
|
599
531
|
|
|
600
532
|
updated = (await (
|
|
@@ -605,31 +537,144 @@ export function createServerHandler<
|
|
|
605
537
|
UpdateResult
|
|
606
538
|
>
|
|
607
539
|
)
|
|
608
|
-
.set(
|
|
540
|
+
.set(conditionalUpdateSet as UpdateSetObject)
|
|
609
541
|
.where(ref<string>(primaryKey), '=', op.row_id)
|
|
542
|
+
.where(ref<string>(versionColumn), '=', expectedVersion)
|
|
610
543
|
.returningAll()
|
|
611
544
|
.executeTakeFirst()) as Record<string, unknown> | undefined;
|
|
545
|
+
|
|
546
|
+
if (!updated) {
|
|
547
|
+
const conflictRow = await (
|
|
548
|
+
trx.selectFrom(table).selectAll() as SelectQueryBuilder<
|
|
549
|
+
ServerDB,
|
|
550
|
+
keyof ServerDB & string,
|
|
551
|
+
Record<string, unknown>
|
|
552
|
+
>
|
|
553
|
+
)
|
|
554
|
+
.where(ref<string>(primaryKey), '=', op.row_id)
|
|
555
|
+
.executeTakeFirst();
|
|
556
|
+
|
|
557
|
+
if (!conflictRow) {
|
|
558
|
+
return {
|
|
559
|
+
result: {
|
|
560
|
+
opIndex,
|
|
561
|
+
status: 'error',
|
|
562
|
+
error: 'ROW_NOT_FOUND_FOR_BASE_VERSION',
|
|
563
|
+
code: 'ROW_MISSING',
|
|
564
|
+
retriable: false,
|
|
565
|
+
},
|
|
566
|
+
emittedChanges: [],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const existingVersion =
|
|
571
|
+
(conflictRow[versionColumn] as number | undefined) ?? 0;
|
|
572
|
+
return {
|
|
573
|
+
result: {
|
|
574
|
+
opIndex,
|
|
575
|
+
status: 'conflict',
|
|
576
|
+
message: `Version conflict: server=${existingVersion}, base=${expectedVersion}`,
|
|
577
|
+
server_version: existingVersion,
|
|
578
|
+
server_row: applyOutboundTransform(
|
|
579
|
+
conflictRow as Selectable<ServerDB[TableName]>
|
|
580
|
+
),
|
|
581
|
+
},
|
|
582
|
+
emittedChanges: [],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
612
585
|
} else {
|
|
613
|
-
|
|
614
|
-
const insertValues: Record<string, unknown> = {
|
|
586
|
+
const updateSet: Record<string, unknown> = {
|
|
615
587
|
...payload,
|
|
616
|
-
[
|
|
617
|
-
[versionColumn]: 1,
|
|
588
|
+
[versionColumn]: sql`${sql.ref(versionColumn)} + 1`,
|
|
618
589
|
};
|
|
590
|
+
delete updateSet[primaryKey];
|
|
591
|
+
for (const col of Object.values(scopeColumns)) {
|
|
592
|
+
delete updateSet[col];
|
|
593
|
+
}
|
|
619
594
|
|
|
620
595
|
updated = (await (
|
|
621
|
-
trx.
|
|
596
|
+
trx.updateTable(table) as UpdateQueryBuilder<
|
|
622
597
|
ServerDB,
|
|
623
598
|
TableName,
|
|
624
|
-
|
|
599
|
+
TableName,
|
|
600
|
+
UpdateResult
|
|
625
601
|
>
|
|
626
602
|
)
|
|
627
|
-
.
|
|
603
|
+
.set(updateSet as UpdateSetObject)
|
|
604
|
+
.where(ref<string>(primaryKey), '=', op.row_id)
|
|
628
605
|
.returningAll()
|
|
629
606
|
.executeTakeFirst()) as Record<string, unknown> | undefined;
|
|
607
|
+
|
|
608
|
+
if (!updated) {
|
|
609
|
+
const insertValues: Record<string, unknown> = {
|
|
610
|
+
...payload,
|
|
611
|
+
[primaryKey]: op.row_id,
|
|
612
|
+
[versionColumn]: 1,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
updated = (await (
|
|
617
|
+
trx.insertInto(table) as InsertQueryBuilder<
|
|
618
|
+
ServerDB,
|
|
619
|
+
TableName,
|
|
620
|
+
InsertResult
|
|
621
|
+
>
|
|
622
|
+
)
|
|
623
|
+
.values(insertValues as Insertable<ServerDB[TableName]>)
|
|
624
|
+
.returningAll()
|
|
625
|
+
.executeTakeFirst()) as Record<string, unknown> | undefined;
|
|
626
|
+
} catch (err) {
|
|
627
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
628
|
+
if (!isConstraintViolationError(message)) {
|
|
629
|
+
throw err;
|
|
630
|
+
}
|
|
631
|
+
updated = (await (
|
|
632
|
+
trx.updateTable(table) as UpdateQueryBuilder<
|
|
633
|
+
ServerDB,
|
|
634
|
+
TableName,
|
|
635
|
+
TableName,
|
|
636
|
+
UpdateResult
|
|
637
|
+
>
|
|
638
|
+
)
|
|
639
|
+
.set(updateSet as UpdateSetObject)
|
|
640
|
+
.where(ref<string>(primaryKey), '=', op.row_id)
|
|
641
|
+
.returningAll()
|
|
642
|
+
.executeTakeFirst()) as Record<string, unknown> | undefined;
|
|
643
|
+
if (!updated) {
|
|
644
|
+
constraintError = {
|
|
645
|
+
message,
|
|
646
|
+
code: classifyConstraintViolationCode(message),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
630
651
|
}
|
|
631
652
|
} catch (err) {
|
|
632
653
|
const message = err instanceof Error ? err.message : String(err);
|
|
654
|
+
if (op.base_version != null && isMissingColumnReferenceError(message)) {
|
|
655
|
+
const row = await (
|
|
656
|
+
trx.selectFrom(table).selectAll() as SelectQueryBuilder<
|
|
657
|
+
ServerDB,
|
|
658
|
+
keyof ServerDB & string,
|
|
659
|
+
Record<string, unknown>
|
|
660
|
+
>
|
|
661
|
+
)
|
|
662
|
+
.where(ref<string>(primaryKey), '=', op.row_id)
|
|
663
|
+
.executeTakeFirst();
|
|
664
|
+
if (!row) {
|
|
665
|
+
return {
|
|
666
|
+
result: {
|
|
667
|
+
opIndex,
|
|
668
|
+
status: 'error',
|
|
669
|
+
error: 'ROW_NOT_FOUND_FOR_BASE_VERSION',
|
|
670
|
+
code: 'ROW_MISSING',
|
|
671
|
+
retriable: false,
|
|
672
|
+
},
|
|
673
|
+
emittedChanges: [],
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
633
678
|
if (!isConstraintViolationError(message)) {
|
|
634
679
|
throw err;
|
|
635
680
|
}
|
package/src/pull.ts
CHANGED
|
@@ -35,8 +35,14 @@ import {
|
|
|
35
35
|
readSnapshotChunkRefByPageKey,
|
|
36
36
|
} from './snapshot-chunks';
|
|
37
37
|
import type { SnapshotChunkStorage } from './snapshot-chunks/types';
|
|
38
|
+
import {
|
|
39
|
+
createMemoryScopeCache,
|
|
40
|
+
type ScopeCacheBackend,
|
|
41
|
+
} from './subscriptions/cache';
|
|
38
42
|
import { resolveEffectiveScopesForSubscriptions } from './subscriptions/resolve';
|
|
39
43
|
|
|
44
|
+
const defaultScopeCache = createMemoryScopeCache();
|
|
45
|
+
|
|
40
46
|
function concatByteChunks(chunks: readonly Uint8Array[]): Uint8Array {
|
|
41
47
|
if (chunks.length === 1) {
|
|
42
48
|
return chunks[0] ?? new Uint8Array();
|
|
@@ -67,6 +73,20 @@ export interface PullResult {
|
|
|
67
73
|
clientCursor: number;
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
interface PendingExternalChunkWrite {
|
|
77
|
+
snapshot: SyncSnapshot;
|
|
78
|
+
cacheLookup: {
|
|
79
|
+
partitionId: string;
|
|
80
|
+
scopeKey: string;
|
|
81
|
+
scope: string;
|
|
82
|
+
asOfCommitSeq: number;
|
|
83
|
+
rowCursor: string | null;
|
|
84
|
+
rowLimit: number;
|
|
85
|
+
};
|
|
86
|
+
rowFramePayload: Uint8Array;
|
|
87
|
+
expiresAt: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
/**
|
|
71
91
|
* Generate a stable cache key for snapshot chunks.
|
|
72
92
|
*/
|
|
@@ -261,6 +281,12 @@ export async function pull<
|
|
|
261
281
|
* instead of inline in the database.
|
|
262
282
|
*/
|
|
263
283
|
chunkStorage?: SnapshotChunkStorage;
|
|
284
|
+
/**
|
|
285
|
+
* Optional shared scope cache backend.
|
|
286
|
+
* Request-local memoization is always applied, even with custom backends.
|
|
287
|
+
* Defaults to process-local memory cache.
|
|
288
|
+
*/
|
|
289
|
+
scopeCache?: ScopeCacheBackend;
|
|
264
290
|
}): Promise<PullResult> {
|
|
265
291
|
const { request, dialect } = args;
|
|
266
292
|
const db = args.db;
|
|
@@ -296,6 +322,7 @@ export async function pull<
|
|
|
296
322
|
50
|
|
297
323
|
);
|
|
298
324
|
const dedupeRows = request.dedupeRows === true;
|
|
325
|
+
const pendingExternalChunkWrites: PendingExternalChunkWrite[] = [];
|
|
299
326
|
|
|
300
327
|
// Resolve effective scopes for each subscription
|
|
301
328
|
const resolved = await resolveEffectiveScopesForSubscriptions({
|
|
@@ -303,6 +330,7 @@ export async function pull<
|
|
|
303
330
|
auth: args.auth,
|
|
304
331
|
subscriptions: request.subscriptions ?? [],
|
|
305
332
|
handlers: args.handlers,
|
|
333
|
+
scopeCache: args.scopeCache ?? defaultScopeCache,
|
|
306
334
|
});
|
|
307
335
|
|
|
308
336
|
const result = await dialect.executeInTransaction(db, async (trx) => {
|
|
@@ -470,63 +498,51 @@ export async function pull<
|
|
|
470
498
|
const rowFramePayload = concatByteChunks(
|
|
471
499
|
bundle.rowFrameParts
|
|
472
500
|
);
|
|
473
|
-
const sha256 = await sha256Hex(rowFramePayload);
|
|
474
501
|
const expiresAt = new Date(
|
|
475
502
|
Date.now() + Math.max(1000, bundle.ttlMs)
|
|
476
503
|
).toISOString();
|
|
477
504
|
|
|
478
505
|
if (args.chunkStorage) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
506
|
+
const snapshot: SyncSnapshot = {
|
|
507
|
+
table: bundle.table,
|
|
508
|
+
rows: [],
|
|
509
|
+
chunks: [],
|
|
510
|
+
isFirstPage: bundle.isFirstPage,
|
|
511
|
+
isLastPage: bundle.isLastPage,
|
|
512
|
+
};
|
|
513
|
+
snapshots.push(snapshot);
|
|
514
|
+
pendingExternalChunkWrites.push({
|
|
515
|
+
snapshot,
|
|
516
|
+
cacheLookup: {
|
|
483
517
|
partitionId,
|
|
484
518
|
scopeKey: cacheKey,
|
|
485
519
|
scope: bundle.table,
|
|
486
520
|
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
487
521
|
rowCursor: bundle.startCursor,
|
|
488
522
|
rowLimit: bundleRowLimit,
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
sha256,
|
|
492
|
-
byteLength,
|
|
493
|
-
bodyStream,
|
|
494
|
-
expiresAt,
|
|
495
|
-
});
|
|
496
|
-
} else {
|
|
497
|
-
const compressedBody = await gzipBytes(rowFramePayload);
|
|
498
|
-
chunkRef = await args.chunkStorage.storeChunk({
|
|
499
|
-
partitionId,
|
|
500
|
-
scopeKey: cacheKey,
|
|
501
|
-
scope: bundle.table,
|
|
502
|
-
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
503
|
-
rowCursor: bundle.startCursor,
|
|
504
|
-
rowLimit: bundleRowLimit,
|
|
505
|
-
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
506
|
-
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
507
|
-
sha256,
|
|
508
|
-
body: compressedBody,
|
|
509
|
-
expiresAt,
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
} else {
|
|
513
|
-
const compressedBody = await gzipBytes(rowFramePayload);
|
|
514
|
-
const chunkId = randomId();
|
|
515
|
-
chunkRef = await insertSnapshotChunk(trx, {
|
|
516
|
-
chunkId,
|
|
517
|
-
partitionId,
|
|
518
|
-
scopeKey: cacheKey,
|
|
519
|
-
scope: bundle.table,
|
|
520
|
-
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
521
|
-
rowCursor: bundle.startCursor,
|
|
522
|
-
rowLimit: bundleRowLimit,
|
|
523
|
-
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
524
|
-
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
525
|
-
sha256,
|
|
526
|
-
body: compressedBody,
|
|
523
|
+
},
|
|
524
|
+
rowFramePayload,
|
|
527
525
|
expiresAt,
|
|
528
526
|
});
|
|
527
|
+
return;
|
|
529
528
|
}
|
|
529
|
+
const sha256 = await sha256Hex(rowFramePayload);
|
|
530
|
+
const compressedBody = await gzipBytes(rowFramePayload);
|
|
531
|
+
const chunkId = randomId();
|
|
532
|
+
chunkRef = await insertSnapshotChunk(trx, {
|
|
533
|
+
chunkId,
|
|
534
|
+
partitionId,
|
|
535
|
+
scopeKey: cacheKey,
|
|
536
|
+
scope: bundle.table,
|
|
537
|
+
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
538
|
+
rowCursor: bundle.startCursor,
|
|
539
|
+
rowLimit: bundleRowLimit,
|
|
540
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
541
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
542
|
+
sha256,
|
|
543
|
+
body: compressedBody,
|
|
544
|
+
expiresAt,
|
|
545
|
+
});
|
|
530
546
|
}
|
|
531
547
|
|
|
532
548
|
snapshots.push({
|
|
@@ -671,7 +687,6 @@ export async function pull<
|
|
|
671
687
|
commitSeq: number;
|
|
672
688
|
createdAt: string;
|
|
673
689
|
actorId: string;
|
|
674
|
-
changeId: number;
|
|
675
690
|
change: SyncChange;
|
|
676
691
|
}
|
|
677
692
|
>();
|
|
@@ -694,11 +709,15 @@ export async function pull<
|
|
|
694
709
|
scopes: r.scopes,
|
|
695
710
|
};
|
|
696
711
|
|
|
712
|
+
// Move row keys to insertion tail so Map iteration yields
|
|
713
|
+
// "latest change wins" order without a full array sort.
|
|
714
|
+
if (latestByRowKey.has(rowKey)) {
|
|
715
|
+
latestByRowKey.delete(rowKey);
|
|
716
|
+
}
|
|
697
717
|
latestByRowKey.set(rowKey, {
|
|
698
718
|
commitSeq: r.commit_seq,
|
|
699
719
|
createdAt: r.created_at,
|
|
700
720
|
actorId: r.actor_id,
|
|
701
|
-
changeId: r.change_id,
|
|
702
721
|
change,
|
|
703
722
|
});
|
|
704
723
|
}
|
|
@@ -718,12 +737,8 @@ export async function pull<
|
|
|
718
737
|
continue;
|
|
719
738
|
}
|
|
720
739
|
|
|
721
|
-
const latest = Array.from(latestByRowKey.values()).sort(
|
|
722
|
-
(a, b) => a.commitSeq - b.commitSeq || a.changeId - b.changeId
|
|
723
|
-
);
|
|
724
|
-
|
|
725
740
|
const commits: SyncCommit[] = [];
|
|
726
|
-
for (const item of
|
|
741
|
+
for (const item of latestByRowKey.values()) {
|
|
727
742
|
const lastCommit = commits[commits.length - 1];
|
|
728
743
|
if (!lastCommit || lastCommit.commitSeq !== item.commitSeq) {
|
|
729
744
|
commits.push({
|
|
@@ -829,6 +844,61 @@ export async function pull<
|
|
|
829
844
|
};
|
|
830
845
|
});
|
|
831
846
|
|
|
847
|
+
const chunkStorage = args.chunkStorage;
|
|
848
|
+
if (chunkStorage && pendingExternalChunkWrites.length > 0) {
|
|
849
|
+
for (const pending of pendingExternalChunkWrites) {
|
|
850
|
+
let chunkRef = await readSnapshotChunkRefByPageKey(db, {
|
|
851
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
852
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
853
|
+
scope: pending.cacheLookup.scope,
|
|
854
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
855
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
856
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
857
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
858
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if (!chunkRef) {
|
|
862
|
+
const sha256 = await sha256Hex(pending.rowFramePayload);
|
|
863
|
+
if (chunkStorage.storeChunkStream) {
|
|
864
|
+
const { stream: bodyStream, byteLength } =
|
|
865
|
+
await gzipBytesToStream(pending.rowFramePayload);
|
|
866
|
+
chunkRef = await chunkStorage.storeChunkStream({
|
|
867
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
868
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
869
|
+
scope: pending.cacheLookup.scope,
|
|
870
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
871
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
872
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
873
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
874
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
875
|
+
sha256,
|
|
876
|
+
byteLength,
|
|
877
|
+
bodyStream,
|
|
878
|
+
expiresAt: pending.expiresAt,
|
|
879
|
+
});
|
|
880
|
+
} else {
|
|
881
|
+
const compressedBody = await gzipBytes(pending.rowFramePayload);
|
|
882
|
+
chunkRef = await chunkStorage.storeChunk({
|
|
883
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
884
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
885
|
+
scope: pending.cacheLookup.scope,
|
|
886
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
887
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
888
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
889
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
890
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
891
|
+
sha256,
|
|
892
|
+
body: compressedBody,
|
|
893
|
+
expiresAt: pending.expiresAt,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
pending.snapshot.chunks = [chunkRef];
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
832
902
|
const durationMs = Math.max(0, Date.now() - startedAtMs);
|
|
833
903
|
const stats = summarizePullResponse(result.response);
|
|
834
904
|
|