@syncular/client 0.0.1 → 0.0.2-126
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/README.md +23 -0
- package/dist/blobs/index.js +3 -3
- package/dist/client.d.ts +10 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +70 -21
- package/dist/client.js.map +1 -1
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/conflicts.js +1 -7
- package/dist/conflicts.js.map +1 -1
- package/dist/create-client.d.ts +5 -1
- package/dist/create-client.d.ts.map +1 -1
- package/dist/create-client.js +22 -10
- package/dist/create-client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +24 -2
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +290 -43
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/index.js +2 -2
- package/dist/engine/types.d.ts +16 -4
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/handlers/create-handler.d.ts +15 -5
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +35 -24
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/handlers/types.d.ts +5 -5
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/index.js +19 -19
- package/dist/migrate.d.ts +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/migrate.js +148 -28
- package/dist/migrate.js.map +1 -1
- package/dist/mutations.d.ts +3 -1
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +93 -18
- package/dist/mutations.js.map +1 -1
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +1 -11
- package/dist/outbox.js.map +1 -1
- package/dist/plugins/incrementing-version.d.ts +1 -1
- package/dist/plugins/incrementing-version.js +2 -2
- package/dist/plugins/index.js +2 -2
- package/dist/proxy/dialect.js +1 -1
- package/dist/proxy/driver.js +1 -1
- package/dist/proxy/index.js +4 -4
- package/dist/proxy/mutations.js +1 -1
- package/dist/pull-engine.d.ts +29 -3
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +314 -78
- package/dist/pull-engine.js.map +1 -1
- package/dist/push-engine.d.ts.map +1 -1
- package/dist/push-engine.js +28 -3
- package/dist/push-engine.js.map +1 -1
- package/dist/query/QueryContext.js +1 -1
- package/dist/query/index.js +3 -3
- package/dist/query/tracked-select.d.ts +2 -1
- package/dist/query/tracked-select.d.ts.map +1 -1
- package/dist/query/tracked-select.js +1 -1
- package/dist/schema.d.ts +2 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/sync-loop.d.ts +5 -1
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +167 -18
- package/dist/sync-loop.js.map +1 -1
- package/package.json +30 -6
- package/src/client.test.ts +369 -0
- package/src/client.ts +101 -22
- package/src/conflicts.ts +1 -10
- package/src/create-client.ts +33 -5
- package/src/engine/SyncEngine.test.ts +157 -0
- package/src/engine/SyncEngine.ts +359 -40
- package/src/engine/types.ts +22 -4
- package/src/handlers/create-handler.ts +86 -37
- package/src/handlers/types.ts +10 -4
- package/src/migrate.ts +215 -33
- package/src/mutations.ts +143 -21
- package/src/outbox.ts +1 -15
- package/src/plugins/incrementing-version.ts +2 -2
- package/src/pull-engine.test.ts +147 -0
- package/src/pull-engine.ts +392 -77
- package/src/push-engine.ts +33 -1
- package/src/query/tracked-select.ts +1 -1
- package/src/schema.ts +2 -2
- package/src/sync-loop.ts +215 -19
package/src/mutations.ts
CHANGED
|
@@ -14,11 +14,19 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type {
|
|
17
|
+
ColumnCodecDialect,
|
|
18
|
+
ColumnCodecSource,
|
|
17
19
|
SyncOperation,
|
|
18
20
|
SyncPushRequest,
|
|
19
21
|
SyncPushResponse,
|
|
20
22
|
SyncTransport,
|
|
21
23
|
} from '@syncular/core';
|
|
24
|
+
import {
|
|
25
|
+
applyCodecsToDbRow,
|
|
26
|
+
isRecord,
|
|
27
|
+
randomId,
|
|
28
|
+
toTableColumnCodecs,
|
|
29
|
+
} from '@syncular/core';
|
|
22
30
|
import type { Insertable, Kysely, Transaction, Updateable } from 'kysely';
|
|
23
31
|
import { sql } from 'kysely';
|
|
24
32
|
import { enqueueOutboxCommit } from './outbox';
|
|
@@ -138,21 +146,6 @@ export type MutationsApi<DB, CommitOptions = unknown> = {
|
|
|
138
146
|
[T in KnownTableKey<DB>]: TableMutations<DB, T>;
|
|
139
147
|
};
|
|
140
148
|
|
|
141
|
-
function randomId(): string {
|
|
142
|
-
if (
|
|
143
|
-
typeof crypto !== 'undefined' &&
|
|
144
|
-
typeof crypto.randomUUID === 'function'
|
|
145
|
-
) {
|
|
146
|
-
return crypto.randomUUID();
|
|
147
|
-
}
|
|
148
|
-
// Very small fallback; good enough for tests.
|
|
149
|
-
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
153
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
149
|
function sanitizePayload(
|
|
157
150
|
payload: Record<string, unknown>,
|
|
158
151
|
args: { omit: string[] }
|
|
@@ -268,6 +261,32 @@ async function dynamicInsert<T>(
|
|
|
268
261
|
`.execute(trx);
|
|
269
262
|
}
|
|
270
263
|
|
|
264
|
+
async function dynamicUpsert<T>(
|
|
265
|
+
trx: Transaction<T>,
|
|
266
|
+
table: string,
|
|
267
|
+
idColumn: string,
|
|
268
|
+
id: string,
|
|
269
|
+
values: Record<string, unknown>
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
validateTableName(table);
|
|
272
|
+
validateColumnName(idColumn);
|
|
273
|
+
|
|
274
|
+
// Check if the row already exists
|
|
275
|
+
const existing = await sql`
|
|
276
|
+
select 1 from ${sql.table(table)}
|
|
277
|
+
where ${sql.ref(idColumn)} = ${sql.val(id)}
|
|
278
|
+
limit 1
|
|
279
|
+
`.execute(trx);
|
|
280
|
+
|
|
281
|
+
if (existing.rows.length > 0) {
|
|
282
|
+
// Row exists: just update the provided columns
|
|
283
|
+
await dynamicUpdate(trx, table, idColumn, id, values);
|
|
284
|
+
} else {
|
|
285
|
+
// Row doesn't exist: insert with all provided columns + id
|
|
286
|
+
await dynamicInsert(trx, table, { ...values, [idColumn]: id });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
271
290
|
async function dynamicUpdate<T>(
|
|
272
291
|
trx: Transaction<T>,
|
|
273
292
|
table: string,
|
|
@@ -381,6 +400,8 @@ export interface OutboxCommitConfig<DB extends SyncClientDb> {
|
|
|
381
400
|
idColumn?: string;
|
|
382
401
|
versionColumn?: string | null;
|
|
383
402
|
omitColumns?: string[];
|
|
403
|
+
columnCodecs?: ColumnCodecSource;
|
|
404
|
+
codecDialect?: ColumnCodecDialect;
|
|
384
405
|
}
|
|
385
406
|
|
|
386
407
|
export function createOutboxCommit<DB extends SyncClientDb>(
|
|
@@ -389,6 +410,7 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
389
410
|
const idColumn = config.idColumn ?? 'id';
|
|
390
411
|
const versionColumn = config.versionColumn ?? 'server_version';
|
|
391
412
|
const omitColumns = config.omitColumns ?? [];
|
|
413
|
+
const codecDialect = config.codecDialect ?? 'sqlite';
|
|
392
414
|
|
|
393
415
|
return async (fn) => {
|
|
394
416
|
const operations: SyncOperation[] = [];
|
|
@@ -402,6 +424,38 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
402
424
|
.transaction()
|
|
403
425
|
.execute(async (trx) => {
|
|
404
426
|
const txTableCache = new Map<string, any>();
|
|
427
|
+
const tableCodecCache = new Map<
|
|
428
|
+
string,
|
|
429
|
+
Map<string, ReturnType<typeof toTableColumnCodecs>>
|
|
430
|
+
>();
|
|
431
|
+
const resolveTableCodecs = (
|
|
432
|
+
table: string,
|
|
433
|
+
row: Record<string, unknown>
|
|
434
|
+
) => {
|
|
435
|
+
const columnCodecs = config.columnCodecs;
|
|
436
|
+
if (!columnCodecs) return {};
|
|
437
|
+
const columns = Object.keys(row);
|
|
438
|
+
if (columns.length === 0) return {};
|
|
439
|
+
|
|
440
|
+
let tableCache = tableCodecCache.get(table);
|
|
441
|
+
if (!tableCache) {
|
|
442
|
+
tableCache = new Map<
|
|
443
|
+
string,
|
|
444
|
+
ReturnType<typeof toTableColumnCodecs>
|
|
445
|
+
>();
|
|
446
|
+
tableCodecCache.set(table, tableCache);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const cacheKey = columns.slice().sort().join('\u0000');
|
|
450
|
+
const cached = tableCache.get(cacheKey);
|
|
451
|
+
if (cached) return cached;
|
|
452
|
+
|
|
453
|
+
const resolved = toTableColumnCodecs(table, columnCodecs, columns, {
|
|
454
|
+
dialect: codecDialect,
|
|
455
|
+
});
|
|
456
|
+
tableCache.set(cacheKey, resolved);
|
|
457
|
+
return resolved;
|
|
458
|
+
};
|
|
405
459
|
|
|
406
460
|
const makeTxTable = (table: string) => {
|
|
407
461
|
const cached = txTableCache.get(table);
|
|
@@ -415,8 +469,13 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
415
469
|
typeof rawId === 'string' && rawId ? rawId : randomId();
|
|
416
470
|
|
|
417
471
|
const row = { ...raw, [idColumn]: id };
|
|
472
|
+
const dbRow = applyCodecsToDbRow(
|
|
473
|
+
row,
|
|
474
|
+
resolveTableCodecs(table, row),
|
|
475
|
+
codecDialect
|
|
476
|
+
);
|
|
418
477
|
|
|
419
|
-
await dynamicInsert(trx, table,
|
|
478
|
+
await dynamicInsert(trx, table, dbRow);
|
|
420
479
|
|
|
421
480
|
const payload = sanitizePayload(row, {
|
|
422
481
|
omit: [
|
|
@@ -451,8 +510,16 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
451
510
|
toInsert.push({ ...raw, [idColumn]: id });
|
|
452
511
|
}
|
|
453
512
|
|
|
454
|
-
|
|
455
|
-
|
|
513
|
+
const dbRows = toInsert.map((row) =>
|
|
514
|
+
applyCodecsToDbRow(
|
|
515
|
+
row,
|
|
516
|
+
resolveTableCodecs(table, row),
|
|
517
|
+
codecDialect
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
if (dbRows.length > 0) {
|
|
522
|
+
await dynamicInsert(trx, table, dbRows);
|
|
456
523
|
}
|
|
457
524
|
|
|
458
525
|
for (let i = 0; i < toInsert.length; i++) {
|
|
@@ -492,8 +559,13 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
492
559
|
|
|
493
560
|
const hasExplicitBaseVersion =
|
|
494
561
|
!!opts && hasOwn(opts, 'baseVersion');
|
|
562
|
+
const dbPatch = applyCodecsToDbRow(
|
|
563
|
+
sanitized,
|
|
564
|
+
resolveTableCodecs(table, sanitized),
|
|
565
|
+
codecDialect
|
|
566
|
+
);
|
|
495
567
|
|
|
496
|
-
await dynamicUpdate(trx, table, idColumn, id,
|
|
568
|
+
await dynamicUpdate(trx, table, idColumn, id, dbPatch);
|
|
497
569
|
|
|
498
570
|
const baseVersion = hasExplicitBaseVersion
|
|
499
571
|
? (opts!.baseVersion ?? null)
|
|
@@ -547,7 +619,46 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
547
619
|
},
|
|
548
620
|
|
|
549
621
|
async upsert(id, patch, opts) {
|
|
550
|
-
|
|
622
|
+
const rawPatch = isRecord(patch) ? patch : {};
|
|
623
|
+
const sanitized = sanitizePayload(rawPatch, {
|
|
624
|
+
omit: [
|
|
625
|
+
idColumn,
|
|
626
|
+
...(versionColumn ? [versionColumn] : []),
|
|
627
|
+
...omitColumns,
|
|
628
|
+
],
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const hasExplicitBaseVersion =
|
|
632
|
+
!!opts && hasOwn(opts, 'baseVersion');
|
|
633
|
+
const dbPatch = applyCodecsToDbRow(
|
|
634
|
+
sanitized,
|
|
635
|
+
resolveTableCodecs(table, sanitized),
|
|
636
|
+
codecDialect
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
await dynamicUpsert(trx, table, idColumn, id, dbPatch);
|
|
640
|
+
|
|
641
|
+
const baseVersion = hasExplicitBaseVersion
|
|
642
|
+
? (opts!.baseVersion ?? null)
|
|
643
|
+
: versionColumn
|
|
644
|
+
? await readBaseVersion({
|
|
645
|
+
trx: trx,
|
|
646
|
+
table,
|
|
647
|
+
rowId: id,
|
|
648
|
+
idColumn,
|
|
649
|
+
versionColumn,
|
|
650
|
+
})
|
|
651
|
+
: null;
|
|
652
|
+
|
|
653
|
+
operations.push({
|
|
654
|
+
table: table,
|
|
655
|
+
row_id: id,
|
|
656
|
+
op: 'upsert',
|
|
657
|
+
payload: sanitized,
|
|
658
|
+
base_version: coerceBaseVersion(baseVersion),
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
551
662
|
},
|
|
552
663
|
};
|
|
553
664
|
|
|
@@ -814,7 +925,18 @@ export function createPushCommit<DB = AnyDb>(
|
|
|
814
925
|
}
|
|
815
926
|
}
|
|
816
927
|
|
|
817
|
-
const
|
|
928
|
+
const combined = await config.transport.sync({
|
|
929
|
+
clientId: requestToSend.clientId,
|
|
930
|
+
push: {
|
|
931
|
+
clientCommitId: requestToSend.clientCommitId,
|
|
932
|
+
operations: requestToSend.operations,
|
|
933
|
+
schemaVersion: requestToSend.schemaVersion,
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
if (!combined.push) {
|
|
937
|
+
throw new Error('Server returned no push response');
|
|
938
|
+
}
|
|
939
|
+
const rawResponse = combined.push;
|
|
818
940
|
|
|
819
941
|
let response = rawResponse;
|
|
820
942
|
if (sortedPlugins.length > 0) {
|
package/src/outbox.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { SyncOperation } from '@syncular/core';
|
|
6
|
+
import { isRecord, randomId } from '@syncular/core';
|
|
6
7
|
import type { Kysely } from 'kysely';
|
|
7
8
|
import { sql } from 'kysely';
|
|
8
9
|
import type { OutboxCommitStatus, SyncClientDb } from './schema';
|
|
@@ -22,21 +23,6 @@ export interface OutboxCommit {
|
|
|
22
23
|
schema_version: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function randomId(): string {
|
|
26
|
-
if (
|
|
27
|
-
typeof crypto !== 'undefined' &&
|
|
28
|
-
typeof crypto.randomUUID === 'function'
|
|
29
|
-
) {
|
|
30
|
-
return crypto.randomUUID();
|
|
31
|
-
}
|
|
32
|
-
// Very small fallback; good enough for tests.
|
|
33
|
-
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
37
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
26
|
function isSyncOperation(value: unknown): value is SyncOperation {
|
|
41
27
|
if (!isRecord(value)) return false;
|
|
42
28
|
if (typeof value.table !== 'string') return false;
|
|
@@ -50,7 +50,7 @@ function touchLru(
|
|
|
50
50
|
* - Operations are pushed in commit order for a given client (outbox ordering).
|
|
51
51
|
*
|
|
52
52
|
* This plugin:
|
|
53
|
-
* - Tracks the "next expected server version" per (
|
|
53
|
+
* - Tracks the "next expected server version" per (table, row_id) based on
|
|
54
54
|
* successfully applied pushes.
|
|
55
55
|
* - Rewrites outgoing `base_version` to that expected version when it is higher
|
|
56
56
|
* than the caller-provided value, preventing "self-conflicts" on hot rows.
|
|
@@ -122,7 +122,7 @@ export function createIncrementingVersionPlugin(
|
|
|
122
122
|
maxTrackedRows
|
|
123
123
|
);
|
|
124
124
|
} else {
|
|
125
|
-
// Insert case: most
|
|
125
|
+
// Insert case: most tables start at version 1.
|
|
126
126
|
touchLru(nextExpectedBaseVersionByRow, key, 1, maxTrackedRows);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { gzipSync } from 'node:zlib';
|
|
3
|
+
import {
|
|
4
|
+
encodeSnapshotRows,
|
|
5
|
+
type SyncPullResponse,
|
|
6
|
+
type SyncTransport,
|
|
7
|
+
} from '@syncular/core';
|
|
8
|
+
import { type Kysely, sql } from 'kysely';
|
|
9
|
+
import { createBunSqliteDb } from '../../dialect-bun-sqlite/src';
|
|
10
|
+
import { createClientHandler } from './handlers/create-handler';
|
|
11
|
+
import { ClientTableRegistry } from './handlers/registry';
|
|
12
|
+
import { ensureClientSyncSchema } from './migrate';
|
|
13
|
+
import { applyPullResponse, buildPullRequest } from './pull-engine';
|
|
14
|
+
import type { SyncClientDb } from './schema';
|
|
15
|
+
|
|
16
|
+
interface ItemsTable {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface TestDb extends SyncClientDb {
|
|
22
|
+
items: ItemsTable;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createStreamFromBytes(
|
|
26
|
+
bytes: Uint8Array,
|
|
27
|
+
chunkSize = 1024
|
|
28
|
+
): ReadableStream<Uint8Array> {
|
|
29
|
+
return new ReadableStream<Uint8Array>({
|
|
30
|
+
start(controller) {
|
|
31
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
32
|
+
controller.enqueue(bytes.subarray(index, index + chunkSize));
|
|
33
|
+
}
|
|
34
|
+
controller.close();
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('applyPullResponse chunk streaming', () => {
|
|
40
|
+
let db: Kysely<TestDb>;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
db = createBunSqliteDb<TestDb>({ path: ':memory:' });
|
|
44
|
+
await ensureClientSyncSchema(db);
|
|
45
|
+
await db.schema
|
|
46
|
+
.createTable('items')
|
|
47
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
48
|
+
.addColumn('name', 'text', (col) => col.notNull())
|
|
49
|
+
.execute();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await db.destroy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('applies chunked bootstrap snapshots using streaming transport', async () => {
|
|
57
|
+
const rows = Array.from({ length: 5000 }, (_, index) => ({
|
|
58
|
+
id: `${index + 1}`,
|
|
59
|
+
name: `Item ${index + 1}`,
|
|
60
|
+
}));
|
|
61
|
+
const encoded = encodeSnapshotRows(rows);
|
|
62
|
+
const compressed = new Uint8Array(gzipSync(encoded));
|
|
63
|
+
|
|
64
|
+
let streamFetchCount = 0;
|
|
65
|
+
const transport: SyncTransport = {
|
|
66
|
+
async sync() {
|
|
67
|
+
return {};
|
|
68
|
+
},
|
|
69
|
+
async fetchSnapshotChunk() {
|
|
70
|
+
throw new Error('fetchSnapshotChunk should not be used');
|
|
71
|
+
},
|
|
72
|
+
async fetchSnapshotChunkStream() {
|
|
73
|
+
streamFetchCount += 1;
|
|
74
|
+
return createStreamFromBytes(compressed, 257);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handlers = new ClientTableRegistry<TestDb>().register(
|
|
79
|
+
createClientHandler({
|
|
80
|
+
table: 'items',
|
|
81
|
+
scopes: ['items:{id}'],
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const options = {
|
|
86
|
+
clientId: 'client-1',
|
|
87
|
+
subscriptions: [
|
|
88
|
+
{
|
|
89
|
+
id: 'items-sub',
|
|
90
|
+
table: 'items',
|
|
91
|
+
scopes: {},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
stateId: 'default',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const pullState = await buildPullRequest(db, options);
|
|
98
|
+
|
|
99
|
+
const response: SyncPullResponse = {
|
|
100
|
+
ok: true,
|
|
101
|
+
subscriptions: [
|
|
102
|
+
{
|
|
103
|
+
id: 'items-sub',
|
|
104
|
+
status: 'active',
|
|
105
|
+
scopes: {},
|
|
106
|
+
bootstrap: true,
|
|
107
|
+
bootstrapState: null,
|
|
108
|
+
nextCursor: 1,
|
|
109
|
+
commits: [],
|
|
110
|
+
snapshots: [
|
|
111
|
+
{
|
|
112
|
+
table: 'items',
|
|
113
|
+
rows: [],
|
|
114
|
+
chunks: [
|
|
115
|
+
{
|
|
116
|
+
id: 'chunk-1',
|
|
117
|
+
byteLength: compressed.length,
|
|
118
|
+
sha256: '',
|
|
119
|
+
encoding: 'json-row-frame-v1',
|
|
120
|
+
compression: 'gzip',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
isFirstPage: true,
|
|
124
|
+
isLastPage: true,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await applyPullResponse(
|
|
132
|
+
db,
|
|
133
|
+
transport,
|
|
134
|
+
handlers,
|
|
135
|
+
options,
|
|
136
|
+
pullState,
|
|
137
|
+
response
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const countResult = await sql<{ count: number }>`
|
|
141
|
+
select count(*) as count
|
|
142
|
+
from ${sql.table('items')}
|
|
143
|
+
`.execute(db);
|
|
144
|
+
expect(Number(countResult.rows[0]?.count ?? 0)).toBe(rows.length);
|
|
145
|
+
expect(streamFetchCount).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
});
|