@syncular/server 0.0.1-92 → 0.0.1-96
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/base.d.ts +5 -21
- package/dist/dialect/base.d.ts.map +1 -1
- package/dist/dialect/base.js +33 -1
- package/dist/dialect/base.js.map +1 -1
- package/dist/dialect/types.d.ts +23 -44
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +116 -87
- package/dist/pull.js.map +1 -1
- package/dist/snapshot-chunks/adapters/s3.d.ts +11 -0
- package/dist/snapshot-chunks/adapters/s3.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.d.ts +5 -0
- package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.js +168 -49
- package/dist/snapshot-chunks/db-metadata.js.map +1 -1
- package/dist/snapshot-chunks/types.d.ts +13 -0
- package/dist/snapshot-chunks/types.d.ts.map +1 -1
- package/dist/snapshot-chunks.d.ts +2 -1
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +27 -7
- package/dist/snapshot-chunks.js.map +1 -1
- package/package.json +1 -1
- package/src/dialect/base.ts +55 -25
- package/src/dialect/types.ts +27 -52
- package/src/pull.ts +146 -101
- package/src/snapshot-chunks/db-metadata.ts +218 -56
- package/src/snapshot-chunks/types.ts +20 -0
- package/src/snapshot-chunks.ts +31 -9
package/src/dialect/types.ts
CHANGED
|
@@ -22,6 +22,28 @@ export type DbExecutor<DB extends SyncCoreDb = SyncCoreDb> =
|
|
|
22
22
|
*/
|
|
23
23
|
export type ServerSyncDialectName = string;
|
|
24
24
|
|
|
25
|
+
export interface IncrementalPullRowsArgs {
|
|
26
|
+
table: string;
|
|
27
|
+
scopes: ScopeValues;
|
|
28
|
+
cursor: number;
|
|
29
|
+
limitCommits: number;
|
|
30
|
+
partitionId?: string;
|
|
31
|
+
batchSize?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IncrementalPullRow {
|
|
35
|
+
commit_seq: number;
|
|
36
|
+
actor_id: string;
|
|
37
|
+
created_at: string;
|
|
38
|
+
change_id: number;
|
|
39
|
+
table: string;
|
|
40
|
+
row_id: string;
|
|
41
|
+
op: SyncOp;
|
|
42
|
+
row_json: unknown | null;
|
|
43
|
+
row_version: number | null;
|
|
44
|
+
scopes: StoredScopes;
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
export interface ServerSyncDialect {
|
|
26
48
|
readonly name: ServerSyncDialectName;
|
|
27
49
|
|
|
@@ -88,62 +110,15 @@ export interface ServerSyncDialect {
|
|
|
88
110
|
): Promise<SyncChangeRow[]>;
|
|
89
111
|
|
|
90
112
|
/**
|
|
91
|
-
*
|
|
113
|
+
* Incremental pull iterator for a subscription.
|
|
92
114
|
*
|
|
93
|
-
*
|
|
115
|
+
* Yields change rows joined with commit metadata and filtered by
|
|
94
116
|
* the subscription's table and scope values.
|
|
95
117
|
*/
|
|
96
|
-
|
|
97
|
-
db: DbExecutor<DB>,
|
|
98
|
-
args: {
|
|
99
|
-
table: string;
|
|
100
|
-
scopes: ScopeValues;
|
|
101
|
-
cursor: number;
|
|
102
|
-
limitCommits: number;
|
|
103
|
-
partitionId?: string;
|
|
104
|
-
}
|
|
105
|
-
): Promise<
|
|
106
|
-
Array<{
|
|
107
|
-
commit_seq: number;
|
|
108
|
-
actor_id: string;
|
|
109
|
-
created_at: string;
|
|
110
|
-
change_id: number;
|
|
111
|
-
table: string;
|
|
112
|
-
row_id: string;
|
|
113
|
-
op: SyncOp;
|
|
114
|
-
row_json: unknown | null;
|
|
115
|
-
row_version: number | null;
|
|
116
|
-
scopes: StoredScopes;
|
|
117
|
-
}>
|
|
118
|
-
>;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Streaming incremental pull for large result sets.
|
|
122
|
-
*
|
|
123
|
-
* Yields changes one at a time instead of loading all into memory.
|
|
124
|
-
* Use this when expecting large numbers of changes.
|
|
125
|
-
*/
|
|
126
|
-
streamIncrementalPullRows?<DB extends SyncCoreDb>(
|
|
118
|
+
iterateIncrementalPullRows<DB extends SyncCoreDb>(
|
|
127
119
|
db: DbExecutor<DB>,
|
|
128
|
-
args:
|
|
129
|
-
|
|
130
|
-
scopes: ScopeValues;
|
|
131
|
-
cursor: number;
|
|
132
|
-
limitCommits: number;
|
|
133
|
-
partitionId?: string;
|
|
134
|
-
}
|
|
135
|
-
): AsyncGenerator<{
|
|
136
|
-
commit_seq: number;
|
|
137
|
-
actor_id: string;
|
|
138
|
-
created_at: string;
|
|
139
|
-
change_id: number;
|
|
140
|
-
table: string;
|
|
141
|
-
row_id: string;
|
|
142
|
-
op: SyncOp;
|
|
143
|
-
row_json: unknown | null;
|
|
144
|
-
row_version: number | null;
|
|
145
|
-
scopes: StoredScopes;
|
|
146
|
-
}>;
|
|
120
|
+
args: IncrementalPullRowsArgs
|
|
121
|
+
): AsyncGenerator<IncrementalPullRow>;
|
|
147
122
|
|
|
148
123
|
/**
|
|
149
124
|
* Optional compaction of the change log to reduce storage.
|
package/src/pull.ts
CHANGED
|
@@ -346,120 +346,182 @@ export async function pull<DB extends SyncCoreDb>(args: {
|
|
|
346
346
|
|
|
347
347
|
const snapshots: SyncSnapshot[] = [];
|
|
348
348
|
let nextState: SyncBootstrapState | null = effectiveState;
|
|
349
|
+
const cacheKey = `${partitionId}:${scopesToCacheKey(effectiveScopes)}`;
|
|
350
|
+
|
|
351
|
+
interface SnapshotBundle {
|
|
352
|
+
table: string;
|
|
353
|
+
startCursor: string | null;
|
|
354
|
+
isFirstPage: boolean;
|
|
355
|
+
isLastPage: boolean;
|
|
356
|
+
pageCount: number;
|
|
357
|
+
ttlMs: number;
|
|
358
|
+
hash: ReturnType<typeof createHash>;
|
|
359
|
+
ndjsonParts: string[];
|
|
360
|
+
}
|
|
349
361
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
pageIndex++
|
|
354
|
-
) {
|
|
355
|
-
if (!nextState) break;
|
|
356
|
-
|
|
357
|
-
const nextTableName = nextState.tables[nextState.tableIndex];
|
|
358
|
-
if (!nextTableName) {
|
|
359
|
-
nextState = null;
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const tableHandler = args.shapes.getOrThrow(nextTableName);
|
|
364
|
-
const isFirstPage = nextState.rowCursor == null;
|
|
365
|
-
|
|
366
|
-
const page = await tableHandler.snapshot(
|
|
367
|
-
{
|
|
368
|
-
db: trx,
|
|
369
|
-
actorId: args.actorId,
|
|
370
|
-
scopeValues: effectiveScopes,
|
|
371
|
-
cursor: nextState.rowCursor,
|
|
372
|
-
limit: limitSnapshotRows,
|
|
373
|
-
},
|
|
374
|
-
sub.params
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const isLastPage = page.nextCursor == null;
|
|
378
|
-
|
|
379
|
-
// Always use NDJSON+gzip for bootstrap snapshots
|
|
380
|
-
const ttlMs =
|
|
381
|
-
tableHandler.snapshotChunkTtlMs ?? 24 * 60 * 60 * 1000; // 24h
|
|
362
|
+
const flushSnapshotBundle = async (
|
|
363
|
+
bundle: SnapshotBundle
|
|
364
|
+
): Promise<void> => {
|
|
382
365
|
const nowIso = new Date().toISOString();
|
|
366
|
+
const bundleRowLimit = Math.max(
|
|
367
|
+
1,
|
|
368
|
+
limitSnapshotRows * bundle.pageCount
|
|
369
|
+
);
|
|
383
370
|
|
|
384
|
-
// Use scope hash for caching
|
|
385
|
-
const cacheKey = `${partitionId}:${scopesToCacheKey(effectiveScopes)}`;
|
|
386
371
|
const cached = await readSnapshotChunkRefByPageKey(trx, {
|
|
387
372
|
partitionId,
|
|
388
373
|
scopeKey: cacheKey,
|
|
389
|
-
scope:
|
|
374
|
+
scope: bundle.table,
|
|
390
375
|
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
391
|
-
rowCursor:
|
|
392
|
-
rowLimit:
|
|
376
|
+
rowCursor: bundle.startCursor,
|
|
377
|
+
rowLimit: bundleRowLimit,
|
|
393
378
|
encoding: 'ndjson',
|
|
394
379
|
compression: 'gzip',
|
|
395
380
|
nowIso,
|
|
396
381
|
});
|
|
397
382
|
|
|
398
383
|
let chunkRef = cached;
|
|
399
|
-
|
|
400
384
|
if (!chunkRef) {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
lines.push(s === undefined ? 'null' : s);
|
|
405
|
-
}
|
|
406
|
-
const ndjson =
|
|
407
|
-
lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
408
|
-
const gz = await compressSnapshotNdjson(ndjson);
|
|
409
|
-
const sha256 = createHash('sha256')
|
|
410
|
-
.update(ndjson)
|
|
411
|
-
.digest('hex');
|
|
385
|
+
const ndjson = bundle.ndjsonParts.join('');
|
|
386
|
+
const compressedBody = await compressSnapshotNdjson(ndjson);
|
|
387
|
+
const sha256 = bundle.hash.digest('hex');
|
|
412
388
|
const expiresAt = new Date(
|
|
413
|
-
Date.now() + Math.max(1000, ttlMs)
|
|
389
|
+
Date.now() + Math.max(1000, bundle.ttlMs)
|
|
414
390
|
).toISOString();
|
|
391
|
+
const byteLength = compressedBody.length;
|
|
415
392
|
|
|
416
|
-
// Use external chunk storage if available, otherwise fall back to inline
|
|
417
393
|
if (args.chunkStorage) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
394
|
+
if (args.chunkStorage.storeChunkStream) {
|
|
395
|
+
chunkRef = await args.chunkStorage.storeChunkStream({
|
|
396
|
+
partitionId,
|
|
397
|
+
scopeKey: cacheKey,
|
|
398
|
+
scope: bundle.table,
|
|
399
|
+
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
400
|
+
rowCursor: bundle.startCursor,
|
|
401
|
+
rowLimit: bundleRowLimit,
|
|
402
|
+
encoding: 'ndjson',
|
|
403
|
+
compression: 'gzip',
|
|
404
|
+
sha256,
|
|
405
|
+
byteLength,
|
|
406
|
+
bodyStream: new ReadableStream<Uint8Array>({
|
|
407
|
+
start(controller) {
|
|
408
|
+
controller.enqueue(compressedBody);
|
|
409
|
+
controller.close();
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
expiresAt,
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
chunkRef = await args.chunkStorage.storeChunk({
|
|
416
|
+
partitionId,
|
|
417
|
+
scopeKey: cacheKey,
|
|
418
|
+
scope: bundle.table,
|
|
419
|
+
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
420
|
+
rowCursor: bundle.startCursor,
|
|
421
|
+
rowLimit: bundleRowLimit,
|
|
422
|
+
encoding: 'ndjson',
|
|
423
|
+
compression: 'gzip',
|
|
424
|
+
sha256,
|
|
425
|
+
body: compressedBody,
|
|
426
|
+
expiresAt,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
431
429
|
} else {
|
|
432
430
|
const chunkId = randomUUID();
|
|
433
431
|
chunkRef = await insertSnapshotChunk(trx, {
|
|
434
432
|
chunkId,
|
|
435
433
|
partitionId,
|
|
436
434
|
scopeKey: cacheKey,
|
|
437
|
-
scope:
|
|
435
|
+
scope: bundle.table,
|
|
438
436
|
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
439
|
-
rowCursor:
|
|
440
|
-
rowLimit:
|
|
437
|
+
rowCursor: bundle.startCursor,
|
|
438
|
+
rowLimit: bundleRowLimit,
|
|
441
439
|
encoding: 'ndjson',
|
|
442
440
|
compression: 'gzip',
|
|
443
441
|
sha256,
|
|
444
|
-
body:
|
|
442
|
+
body: compressedBody,
|
|
445
443
|
expiresAt,
|
|
446
444
|
});
|
|
447
445
|
}
|
|
448
446
|
}
|
|
449
447
|
|
|
450
448
|
snapshots.push({
|
|
451
|
-
table:
|
|
449
|
+
table: bundle.table,
|
|
452
450
|
rows: [],
|
|
453
451
|
chunks: [chunkRef],
|
|
454
|
-
isFirstPage,
|
|
455
|
-
isLastPage,
|
|
452
|
+
isFirstPage: bundle.isFirstPage,
|
|
453
|
+
isLastPage: bundle.isLastPage,
|
|
456
454
|
});
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
let activeBundle: SnapshotBundle | null = null;
|
|
458
|
+
|
|
459
|
+
for (
|
|
460
|
+
let pageIndex = 0;
|
|
461
|
+
pageIndex < maxSnapshotPages;
|
|
462
|
+
pageIndex++
|
|
463
|
+
) {
|
|
464
|
+
if (!nextState) break;
|
|
465
|
+
|
|
466
|
+
const nextTableName = nextState.tables[nextState.tableIndex];
|
|
467
|
+
if (!nextTableName) {
|
|
468
|
+
if (activeBundle) {
|
|
469
|
+
activeBundle.isLastPage = true;
|
|
470
|
+
await flushSnapshotBundle(activeBundle);
|
|
471
|
+
activeBundle = null;
|
|
472
|
+
}
|
|
473
|
+
nextState = null;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const tableHandler = args.shapes.getOrThrow(nextTableName);
|
|
478
|
+
if (!activeBundle || activeBundle.table !== nextTableName) {
|
|
479
|
+
if (activeBundle) {
|
|
480
|
+
await flushSnapshotBundle(activeBundle);
|
|
481
|
+
}
|
|
482
|
+
activeBundle = {
|
|
483
|
+
table: nextTableName,
|
|
484
|
+
startCursor: nextState.rowCursor,
|
|
485
|
+
isFirstPage: nextState.rowCursor == null,
|
|
486
|
+
isLastPage: false,
|
|
487
|
+
pageCount: 0,
|
|
488
|
+
ttlMs:
|
|
489
|
+
tableHandler.snapshotChunkTtlMs ?? 24 * 60 * 60 * 1000,
|
|
490
|
+
hash: createHash('sha256'),
|
|
491
|
+
ndjsonParts: [],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const page = await tableHandler.snapshot(
|
|
496
|
+
{
|
|
497
|
+
db: trx,
|
|
498
|
+
actorId: args.actorId,
|
|
499
|
+
scopeValues: effectiveScopes,
|
|
500
|
+
cursor: nextState.rowCursor,
|
|
501
|
+
limit: limitSnapshotRows,
|
|
502
|
+
},
|
|
503
|
+
sub.params
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const lines: string[] = [];
|
|
507
|
+
for (const r of page.rows ?? []) {
|
|
508
|
+
const s = JSON.stringify(r);
|
|
509
|
+
lines.push(s === undefined ? 'null' : s);
|
|
510
|
+
}
|
|
511
|
+
const ndjson = lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
512
|
+
activeBundle.hash.update(ndjson);
|
|
513
|
+
activeBundle.ndjsonParts.push(ndjson);
|
|
514
|
+
activeBundle.pageCount += 1;
|
|
457
515
|
|
|
458
516
|
if (page.nextCursor != null) {
|
|
459
517
|
nextState = { ...nextState, rowCursor: page.nextCursor };
|
|
460
518
|
continue;
|
|
461
519
|
}
|
|
462
520
|
|
|
521
|
+
activeBundle.isLastPage = true;
|
|
522
|
+
await flushSnapshotBundle(activeBundle);
|
|
523
|
+
activeBundle = null;
|
|
524
|
+
|
|
463
525
|
if (nextState.tableIndex + 1 < nextState.tables.length) {
|
|
464
526
|
nextState = {
|
|
465
527
|
...nextState,
|
|
@@ -473,6 +535,10 @@ export async function pull<DB extends SyncCoreDb>(args: {
|
|
|
473
535
|
break;
|
|
474
536
|
}
|
|
475
537
|
|
|
538
|
+
if (activeBundle) {
|
|
539
|
+
await flushSnapshotBundle(activeBundle);
|
|
540
|
+
}
|
|
541
|
+
|
|
476
542
|
subResponses.push({
|
|
477
543
|
id: sub.id,
|
|
478
544
|
status: 'active',
|
|
@@ -501,17 +567,6 @@ export async function pull<DB extends SyncCoreDb>(args: {
|
|
|
501
567
|
? scannedCommitSeqs[scannedCommitSeqs.length - 1]!
|
|
502
568
|
: cursor;
|
|
503
569
|
|
|
504
|
-
// Use streaming when available to reduce memory pressure for large pulls
|
|
505
|
-
const pullRowStream = dialect.streamIncrementalPullRows
|
|
506
|
-
? dialect.streamIncrementalPullRows(trx, {
|
|
507
|
-
partitionId,
|
|
508
|
-
table: sub.shape,
|
|
509
|
-
scopes: effectiveScopes,
|
|
510
|
-
cursor,
|
|
511
|
-
limitCommits,
|
|
512
|
-
})
|
|
513
|
-
: null;
|
|
514
|
-
|
|
515
570
|
// Collect rows and compute nextCursor in a single pass
|
|
516
571
|
const incrementalRows: Array<{
|
|
517
572
|
commit_seq: number;
|
|
@@ -528,25 +583,15 @@ export async function pull<DB extends SyncCoreDb>(args: {
|
|
|
528
583
|
|
|
529
584
|
let nextCursor = cursor;
|
|
530
585
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
partitionId,
|
|
541
|
-
table: sub.shape,
|
|
542
|
-
scopes: effectiveScopes,
|
|
543
|
-
cursor,
|
|
544
|
-
limitCommits,
|
|
545
|
-
});
|
|
546
|
-
incrementalRows.push(...rows);
|
|
547
|
-
for (const r of incrementalRows) {
|
|
548
|
-
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
549
|
-
}
|
|
586
|
+
for await (const row of dialect.iterateIncrementalPullRows(trx, {
|
|
587
|
+
partitionId,
|
|
588
|
+
table: sub.shape,
|
|
589
|
+
scopes: effectiveScopes,
|
|
590
|
+
cursor,
|
|
591
|
+
limitCommits,
|
|
592
|
+
})) {
|
|
593
|
+
incrementalRows.push(row);
|
|
594
|
+
nextCursor = Math.max(nextCursor, row.commit_seq);
|
|
550
595
|
}
|
|
551
596
|
|
|
552
597
|
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|