@syncular/server 0.0.6-159 → 0.0.6-167
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/blobs/adapters/database.d.ts +26 -9
- package/dist/blobs/adapters/database.d.ts.map +1 -1
- package/dist/blobs/adapters/database.js +65 -21
- package/dist/blobs/adapters/database.js.map +1 -1
- package/dist/blobs/manager.d.ts +60 -3
- package/dist/blobs/manager.d.ts.map +1 -1
- package/dist/blobs/manager.js +227 -56
- package/dist/blobs/manager.js.map +1 -1
- package/dist/blobs/migrate.d.ts.map +1 -1
- package/dist/blobs/migrate.js +16 -8
- package/dist/blobs/migrate.js.map +1 -1
- package/dist/blobs/types.d.ts +4 -0
- package/dist/blobs/types.d.ts.map +1 -1
- package/dist/dialect/helpers.d.ts +3 -0
- package/dist/dialect/helpers.d.ts.map +1 -1
- package/dist/dialect/helpers.js +17 -0
- package/dist/dialect/helpers.js.map +1 -1
- package/dist/handlers/collection.d.ts +0 -2
- package/dist/handlers/collection.d.ts.map +1 -1
- package/dist/handlers/collection.js +5 -56
- package/dist/handlers/collection.js.map +1 -1
- package/dist/handlers/create-handler.d.ts +0 -4
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +6 -34
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +13 -37
- package/dist/notify.js.map +1 -1
- package/dist/proxy/collection.d.ts +0 -2
- package/dist/proxy/collection.d.ts.map +1 -1
- package/dist/proxy/collection.js +2 -17
- package/dist/proxy/collection.js.map +1 -1
- package/dist/proxy/handler.d.ts +1 -1
- package/dist/proxy/handler.d.ts.map +1 -1
- package/dist/proxy/handler.js +1 -2
- package/dist/proxy/handler.js.map +1 -1
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +1 -1
- package/dist/proxy/index.js.map +1 -1
- package/dist/proxy/oplog.d.ts.map +1 -1
- package/dist/proxy/oplog.js +1 -7
- package/dist/proxy/oplog.js.map +1 -1
- package/dist/prune.d.ts.map +1 -1
- package/dist/prune.js +1 -13
- package/dist/prune.js.map +1 -1
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +186 -54
- package/dist/pull.js.map +1 -1
- package/dist/push.d.ts +1 -1
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +9 -36
- package/dist/push.js.map +1 -1
- package/dist/snapshot-chunks/db-metadata.d.ts +18 -0
- package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.js +71 -23
- package/dist/snapshot-chunks/db-metadata.js.map +1 -1
- package/dist/snapshot-chunks.d.ts +5 -1
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +14 -1
- package/dist/snapshot-chunks.js.map +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +1 -13
- package/dist/stats.js.map +1 -1
- package/dist/subscriptions/resolve.d.ts +1 -1
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +3 -16
- package/dist/subscriptions/resolve.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +2 -4
- package/dist/sync.js.map +1 -1
- package/package.json +2 -2
- package/src/blobs/adapters/database.test.ts +7 -0
- package/src/blobs/adapters/database.ts +119 -39
- package/src/blobs/manager.ts +339 -53
- package/src/blobs/migrate.ts +16 -8
- package/src/blobs/types.ts +4 -0
- package/src/dialect/helpers.ts +19 -0
- package/src/handlers/collection.ts +17 -86
- package/src/handlers/create-handler.ts +9 -44
- package/src/notify.ts +15 -40
- package/src/proxy/collection.ts +5 -27
- package/src/proxy/handler.ts +2 -2
- package/src/proxy/index.ts +0 -2
- package/src/proxy/oplog.ts +1 -9
- package/src/prune.ts +1 -12
- package/src/pull.ts +280 -105
- package/src/push.ts +14 -43
- package/src/snapshot-chunks/db-metadata.ts +107 -27
- package/src/snapshot-chunks.ts +18 -0
- package/src/stats.ts +1 -12
- package/src/subscriptions/resolve.ts +4 -20
- package/src/sync.ts +6 -6
package/src/pull.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
encodeSnapshotRowFrames,
|
|
6
6
|
encodeSnapshotRows,
|
|
7
7
|
gzipBytes,
|
|
8
|
-
gzipBytesToStream,
|
|
9
8
|
randomId,
|
|
10
9
|
type ScopeValues,
|
|
11
10
|
SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
@@ -24,15 +23,15 @@ import type { Kysely } from 'kysely';
|
|
|
24
23
|
import type { DbExecutor, ServerSyncDialect } from './dialect/types';
|
|
25
24
|
import {
|
|
26
25
|
getServerBootstrapOrderFor,
|
|
27
|
-
getServerHandlerOrThrow,
|
|
28
26
|
type ServerHandlerCollection,
|
|
29
27
|
} from './handlers/collection';
|
|
30
|
-
import type {
|
|
28
|
+
import type { SyncServerAuth } from './handlers/types';
|
|
31
29
|
import { EXTERNAL_CLIENT_ID } from './notify';
|
|
32
30
|
import type { SyncCoreDb } from './schema';
|
|
33
31
|
import {
|
|
34
32
|
insertSnapshotChunk,
|
|
35
33
|
readSnapshotChunkRefByPageKey,
|
|
34
|
+
scopesToSnapshotChunkScopeKey,
|
|
36
35
|
} from './snapshot-chunks';
|
|
37
36
|
import type { SnapshotChunkStorage } from './snapshot-chunks/types';
|
|
38
37
|
import {
|
|
@@ -62,6 +61,152 @@ function concatByteChunks(chunks: readonly Uint8Array[]): Uint8Array {
|
|
|
62
61
|
return merged;
|
|
63
62
|
}
|
|
64
63
|
|
|
64
|
+
function byteChunksToStream(
|
|
65
|
+
chunks: readonly Uint8Array[]
|
|
66
|
+
): ReadableStream<BufferSource> {
|
|
67
|
+
return new ReadableStream<BufferSource>({
|
|
68
|
+
start(controller) {
|
|
69
|
+
for (const chunk of chunks) {
|
|
70
|
+
if (chunk.length === 0) continue;
|
|
71
|
+
controller.enqueue(chunk.slice());
|
|
72
|
+
}
|
|
73
|
+
controller.close();
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bufferSourceToUint8Array(chunk: BufferSource): Uint8Array {
|
|
79
|
+
if (chunk instanceof Uint8Array) {
|
|
80
|
+
return chunk;
|
|
81
|
+
}
|
|
82
|
+
if (chunk instanceof ArrayBuffer) {
|
|
83
|
+
return new Uint8Array(chunk);
|
|
84
|
+
}
|
|
85
|
+
return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function streamToBytes(
|
|
89
|
+
stream: ReadableStream<BufferSource>
|
|
90
|
+
): Promise<Uint8Array> {
|
|
91
|
+
const reader = stream.getReader();
|
|
92
|
+
const chunks: Uint8Array[] = [];
|
|
93
|
+
let total = 0;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
while (true) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done) break;
|
|
99
|
+
if (!value) continue;
|
|
100
|
+
const bytes = bufferSourceToUint8Array(value);
|
|
101
|
+
if (bytes.length === 0) continue;
|
|
102
|
+
chunks.push(bytes);
|
|
103
|
+
total += bytes.length;
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
reader.releaseLock();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (chunks.length === 0) return new Uint8Array();
|
|
110
|
+
if (chunks.length === 1) return chunks[0] ?? new Uint8Array();
|
|
111
|
+
|
|
112
|
+
const merged = new Uint8Array(total);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
merged.set(chunk, offset);
|
|
116
|
+
offset += chunk.length;
|
|
117
|
+
}
|
|
118
|
+
return merged;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function bufferSourceStreamToUint8ArrayStream(
|
|
122
|
+
stream: ReadableStream<BufferSource>
|
|
123
|
+
): ReadableStream<Uint8Array> {
|
|
124
|
+
return new ReadableStream<Uint8Array>({
|
|
125
|
+
async start(controller) {
|
|
126
|
+
const reader = stream.getReader();
|
|
127
|
+
try {
|
|
128
|
+
while (true) {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done) break;
|
|
131
|
+
if (!value) continue;
|
|
132
|
+
const bytes = bufferSourceToUint8Array(value);
|
|
133
|
+
if (bytes.length === 0) continue;
|
|
134
|
+
controller.enqueue(bytes);
|
|
135
|
+
}
|
|
136
|
+
controller.close();
|
|
137
|
+
} catch (err) {
|
|
138
|
+
controller.error(err);
|
|
139
|
+
} finally {
|
|
140
|
+
reader.releaseLock();
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let nodeCryptoModulePromise: Promise<
|
|
147
|
+
typeof import('node:crypto') | null
|
|
148
|
+
> | null = null;
|
|
149
|
+
|
|
150
|
+
async function getNodeCryptoModule(): Promise<
|
|
151
|
+
typeof import('node:crypto') | null
|
|
152
|
+
> {
|
|
153
|
+
if (!nodeCryptoModulePromise) {
|
|
154
|
+
nodeCryptoModulePromise = import('node:crypto').catch(() => null);
|
|
155
|
+
}
|
|
156
|
+
return nodeCryptoModulePromise;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function sha256HexFromByteChunks(
|
|
160
|
+
chunks: readonly Uint8Array[]
|
|
161
|
+
): Promise<string> {
|
|
162
|
+
const nodeCrypto = await getNodeCryptoModule();
|
|
163
|
+
if (nodeCrypto && typeof nodeCrypto.createHash === 'function') {
|
|
164
|
+
const hasher = nodeCrypto.createHash('sha256');
|
|
165
|
+
for (const chunk of chunks) {
|
|
166
|
+
if (chunk.length === 0) continue;
|
|
167
|
+
hasher.update(chunk);
|
|
168
|
+
}
|
|
169
|
+
return hasher.digest('hex');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return sha256Hex(concatByteChunks(chunks));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function gzipByteChunks(
|
|
176
|
+
chunks: readonly Uint8Array[]
|
|
177
|
+
): Promise<Uint8Array> {
|
|
178
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
179
|
+
const stream = byteChunksToStream(chunks).pipeThrough(
|
|
180
|
+
new CompressionStream('gzip')
|
|
181
|
+
);
|
|
182
|
+
return streamToBytes(stream);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return gzipBytes(concatByteChunks(chunks));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function gzipByteChunksToStream(chunks: readonly Uint8Array[]): Promise<{
|
|
189
|
+
stream: ReadableStream<Uint8Array>;
|
|
190
|
+
byteLength?: number;
|
|
191
|
+
}> {
|
|
192
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
193
|
+
const source = byteChunksToStream(chunks).pipeThrough(
|
|
194
|
+
new CompressionStream('gzip')
|
|
195
|
+
);
|
|
196
|
+
return {
|
|
197
|
+
stream: bufferSourceStreamToUint8ArrayStream(source),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const compressed = await gzipBytes(concatByteChunks(chunks));
|
|
202
|
+
return {
|
|
203
|
+
stream: bufferSourceStreamToUint8ArrayStream(
|
|
204
|
+
byteChunksToStream([compressed])
|
|
205
|
+
),
|
|
206
|
+
byteLength: compressed.length,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
65
210
|
export interface PullResult {
|
|
66
211
|
response: SyncPullResponse;
|
|
67
212
|
/**
|
|
@@ -83,22 +228,31 @@ interface PendingExternalChunkWrite {
|
|
|
83
228
|
rowCursor: string | null;
|
|
84
229
|
rowLimit: number;
|
|
85
230
|
};
|
|
86
|
-
|
|
231
|
+
rowFrameParts: Uint8Array[];
|
|
87
232
|
expiresAt: string;
|
|
88
233
|
}
|
|
89
234
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
235
|
+
async function runWithConcurrency<T>(
|
|
236
|
+
items: readonly T[],
|
|
237
|
+
concurrency: number,
|
|
238
|
+
worker: (item: T) => Promise<void>
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
if (items.length === 0) return;
|
|
241
|
+
|
|
242
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
243
|
+
let nextIndex = 0;
|
|
244
|
+
|
|
245
|
+
async function runWorker(): Promise<void> {
|
|
246
|
+
while (nextIndex < items.length) {
|
|
247
|
+
const index = nextIndex;
|
|
248
|
+
nextIndex += 1;
|
|
249
|
+
const item = items[index];
|
|
250
|
+
if (item === undefined) continue;
|
|
251
|
+
await worker(item);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
102
256
|
}
|
|
103
257
|
|
|
104
258
|
/**
|
|
@@ -239,31 +393,43 @@ function recordPullMetrics(args: {
|
|
|
239
393
|
);
|
|
240
394
|
}
|
|
241
395
|
|
|
242
|
-
|
|
243
|
-
* Read synthetic commits created by notifyExternalDataChange() after a given cursor.
|
|
244
|
-
* Returns commit_seq and affected tables for each external change commit.
|
|
245
|
-
*/
|
|
246
|
-
async function readExternalDataChanges<DB extends SyncCoreDb>(
|
|
396
|
+
async function readLatestExternalCommitByTable<DB extends SyncCoreDb>(
|
|
247
397
|
trx: DbExecutor<DB>,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
398
|
+
args: { partitionId: string; afterCursor: number; tables: string[] }
|
|
399
|
+
): Promise<Map<string, number>> {
|
|
400
|
+
const tableNames = Array.from(
|
|
401
|
+
new Set(args.tables.filter((table) => typeof table === 'string'))
|
|
402
|
+
);
|
|
403
|
+
const latestByTable = new Map<string, number>();
|
|
404
|
+
if (tableNames.length === 0) {
|
|
405
|
+
return latestByTable;
|
|
406
|
+
}
|
|
407
|
+
|
|
251
408
|
type SyncExecutor = Pick<Kysely<SyncCoreDb>, 'selectFrom'>;
|
|
252
409
|
const executor = trx as SyncExecutor;
|
|
253
|
-
|
|
254
410
|
const rows = await executor
|
|
255
|
-
.selectFrom('
|
|
256
|
-
.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
411
|
+
.selectFrom('sync_table_commits as tc')
|
|
412
|
+
.innerJoin('sync_commits as cm', (join) =>
|
|
413
|
+
join
|
|
414
|
+
.onRef('cm.commit_seq', '=', 'tc.commit_seq')
|
|
415
|
+
.onRef('cm.partition_id', '=', 'tc.partition_id')
|
|
416
|
+
)
|
|
417
|
+
.select(['tc.table as table'])
|
|
418
|
+
.select((eb) => eb.fn.max('tc.commit_seq').as('latest_commit_seq'))
|
|
419
|
+
.where('tc.partition_id', '=', args.partitionId)
|
|
420
|
+
.where('cm.client_id', '=', EXTERNAL_CLIENT_ID)
|
|
421
|
+
.where('tc.commit_seq', '>', args.afterCursor)
|
|
422
|
+
.where('tc.table', 'in', tableNames)
|
|
423
|
+
.groupBy('tc.table')
|
|
261
424
|
.execute();
|
|
262
425
|
|
|
263
|
-
|
|
264
|
-
commitSeq
|
|
265
|
-
|
|
266
|
-
|
|
426
|
+
for (const row of rows) {
|
|
427
|
+
const commitSeq = Number(row.latest_commit_seq ?? -1);
|
|
428
|
+
if (!Number.isFinite(commitSeq) || commitSeq < 0) continue;
|
|
429
|
+
latestByTable.set(row.table, commitSeq);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return latestByTable;
|
|
267
433
|
}
|
|
268
434
|
|
|
269
435
|
export async function pull<
|
|
@@ -350,39 +516,35 @@ export async function pull<
|
|
|
350
516
|
// Detect external data changes (synthetic commits from notifyExternalDataChange)
|
|
351
517
|
// Compute minimum cursor across all active subscriptions to scope the query.
|
|
352
518
|
let minSubCursor = Number.MAX_SAFE_INTEGER;
|
|
519
|
+
const activeTables = new Set<string>();
|
|
353
520
|
for (const sub of resolved) {
|
|
354
521
|
if (
|
|
355
522
|
sub.status === 'revoked' ||
|
|
356
523
|
Object.keys(sub.scopes).length === 0
|
|
357
524
|
)
|
|
358
525
|
continue;
|
|
526
|
+
activeTables.add(sub.table);
|
|
359
527
|
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
360
528
|
if (cursor >= 0 && cursor < minSubCursor) {
|
|
361
529
|
minSubCursor = cursor;
|
|
362
530
|
}
|
|
363
531
|
}
|
|
364
532
|
|
|
365
|
-
const
|
|
533
|
+
const maxExternalCommitByTable =
|
|
366
534
|
minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
|
|
367
|
-
? await
|
|
535
|
+
? await readLatestExternalCommitByTable(trx, {
|
|
368
536
|
partitionId,
|
|
369
537
|
afterCursor: minSubCursor,
|
|
538
|
+
tables: Array.from(activeTables),
|
|
370
539
|
})
|
|
371
|
-
:
|
|
372
|
-
const maxExternalCommitByTable = new Map<string, number>();
|
|
373
|
-
for (const change of externalDataChanges) {
|
|
374
|
-
for (const table of change.tables) {
|
|
375
|
-
const previous = maxExternalCommitByTable.get(table) ?? -1;
|
|
376
|
-
if (change.commitSeq > previous) {
|
|
377
|
-
maxExternalCommitByTable.set(table, change.commitSeq);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
540
|
+
: new Map<string, number>();
|
|
381
541
|
|
|
382
542
|
for (const sub of resolved) {
|
|
383
543
|
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
384
544
|
// Validate table handler exists (throws if not registered)
|
|
385
|
-
|
|
545
|
+
if (!args.handlers.byTable.has(sub.table)) {
|
|
546
|
+
throw new Error(`Unknown table: ${sub.table}`);
|
|
547
|
+
}
|
|
386
548
|
|
|
387
549
|
if (
|
|
388
550
|
sub.status === 'revoked' ||
|
|
@@ -460,7 +622,9 @@ export async function pull<
|
|
|
460
622
|
|
|
461
623
|
const snapshots: SyncSnapshot[] = [];
|
|
462
624
|
let nextState: SyncBootstrapState | null = effectiveState;
|
|
463
|
-
const cacheKey = `${partitionId}:${await
|
|
625
|
+
const cacheKey = `${partitionId}:${await scopesToSnapshotChunkScopeKey(
|
|
626
|
+
effectiveScopes
|
|
627
|
+
)}`;
|
|
464
628
|
|
|
465
629
|
interface SnapshotBundle {
|
|
466
630
|
table: string;
|
|
@@ -495,9 +659,6 @@ export async function pull<
|
|
|
495
659
|
|
|
496
660
|
let chunkRef = cached;
|
|
497
661
|
if (!chunkRef) {
|
|
498
|
-
const rowFramePayload = concatByteChunks(
|
|
499
|
-
bundle.rowFrameParts
|
|
500
|
-
);
|
|
501
662
|
const expiresAt = new Date(
|
|
502
663
|
Date.now() + Math.max(1000, bundle.ttlMs)
|
|
503
664
|
).toISOString();
|
|
@@ -521,13 +682,17 @@ export async function pull<
|
|
|
521
682
|
rowCursor: bundle.startCursor,
|
|
522
683
|
rowLimit: bundleRowLimit,
|
|
523
684
|
},
|
|
524
|
-
|
|
685
|
+
rowFrameParts: [...bundle.rowFrameParts],
|
|
525
686
|
expiresAt,
|
|
526
687
|
});
|
|
527
688
|
return;
|
|
528
689
|
}
|
|
529
|
-
const sha256 = await
|
|
530
|
-
|
|
690
|
+
const sha256 = await sha256HexFromByteChunks(
|
|
691
|
+
bundle.rowFrameParts
|
|
692
|
+
);
|
|
693
|
+
const compressedBody = await gzipByteChunks(
|
|
694
|
+
bundle.rowFrameParts
|
|
695
|
+
);
|
|
531
696
|
const chunkId = randomId();
|
|
532
697
|
chunkRef = await insertSnapshotChunk(trx, {
|
|
533
698
|
chunkId,
|
|
@@ -575,8 +740,10 @@ export async function pull<
|
|
|
575
740
|
break;
|
|
576
741
|
}
|
|
577
742
|
|
|
578
|
-
const tableHandler
|
|
579
|
-
|
|
743
|
+
const tableHandler = args.handlers.byTable.get(nextTableName);
|
|
744
|
+
if (!tableHandler) {
|
|
745
|
+
throw new Error(`Unknown table: ${nextTableName}`);
|
|
746
|
+
}
|
|
580
747
|
if (!activeBundle || activeBundle.table !== nextTableName) {
|
|
581
748
|
if (activeBundle) {
|
|
582
749
|
await flushSnapshotBundle(activeBundle);
|
|
@@ -846,57 +1013,65 @@ export async function pull<
|
|
|
846
1013
|
|
|
847
1014
|
const chunkStorage = args.chunkStorage;
|
|
848
1015
|
if (chunkStorage && pendingExternalChunkWrites.length > 0) {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1016
|
+
await runWithConcurrency(
|
|
1017
|
+
pendingExternalChunkWrites,
|
|
1018
|
+
4,
|
|
1019
|
+
async (pending) => {
|
|
1020
|
+
let chunkRef = await readSnapshotChunkRefByPageKey(db, {
|
|
1021
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
1022
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
1023
|
+
scope: pending.cacheLookup.scope,
|
|
1024
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
1025
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
1026
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
1027
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1028
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1029
|
+
});
|
|
860
1030
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1031
|
+
if (!chunkRef) {
|
|
1032
|
+
const sha256 = await sha256HexFromByteChunks(
|
|
1033
|
+
pending.rowFrameParts
|
|
1034
|
+
);
|
|
1035
|
+
if (chunkStorage.storeChunkStream) {
|
|
1036
|
+
const { stream: bodyStream, byteLength } =
|
|
1037
|
+
await gzipByteChunksToStream(pending.rowFrameParts);
|
|
1038
|
+
chunkRef = await chunkStorage.storeChunkStream({
|
|
1039
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
1040
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
1041
|
+
scope: pending.cacheLookup.scope,
|
|
1042
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
1043
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
1044
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
1045
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1046
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1047
|
+
sha256,
|
|
1048
|
+
byteLength,
|
|
1049
|
+
bodyStream,
|
|
1050
|
+
expiresAt: pending.expiresAt,
|
|
1051
|
+
});
|
|
1052
|
+
} else {
|
|
1053
|
+
const compressedBody = await gzipByteChunks(
|
|
1054
|
+
pending.rowFrameParts
|
|
1055
|
+
);
|
|
1056
|
+
chunkRef = await chunkStorage.storeChunk({
|
|
1057
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
1058
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
1059
|
+
scope: pending.cacheLookup.scope,
|
|
1060
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
1061
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
1062
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
1063
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
1064
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
1065
|
+
sha256,
|
|
1066
|
+
body: compressedBody,
|
|
1067
|
+
expiresAt: pending.expiresAt,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
895
1070
|
}
|
|
896
|
-
}
|
|
897
1071
|
|
|
898
|
-
|
|
899
|
-
|
|
1072
|
+
pending.snapshot.chunks = [chunkRef];
|
|
1073
|
+
}
|
|
1074
|
+
);
|
|
900
1075
|
}
|
|
901
1076
|
|
|
902
1077
|
const durationMs = Math.max(0, Date.now() - startedAtMs);
|
package/src/push.ts
CHANGED
|
@@ -15,11 +15,13 @@ import type {
|
|
|
15
15
|
Updateable,
|
|
16
16
|
} from 'kysely';
|
|
17
17
|
import { sql } from 'kysely';
|
|
18
|
-
import type { ServerSyncDialect } from './dialect/types';
|
|
19
18
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
coerceNumber,
|
|
20
|
+
parseJsonValue,
|
|
21
|
+
toDialectJsonValue,
|
|
22
|
+
} from './dialect/helpers';
|
|
23
|
+
import type { ServerSyncDialect } from './dialect/types';
|
|
24
|
+
import type { ServerHandlerCollection } from './handlers/collection';
|
|
23
25
|
import type { SyncServerAuth } from './handlers/types';
|
|
24
26
|
import {
|
|
25
27
|
type SyncServerPushPlugin,
|
|
@@ -67,40 +69,6 @@ class RejectCommitError extends Error {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
function toDialectJsonValue(
|
|
71
|
-
dialect: ServerSyncDialect,
|
|
72
|
-
value: unknown
|
|
73
|
-
): unknown {
|
|
74
|
-
if (value === null || value === undefined) return null;
|
|
75
|
-
if (dialect.family === 'sqlite') return JSON.stringify(value);
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseJsonValue(value: unknown): unknown {
|
|
80
|
-
if (typeof value !== 'string') return value;
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(value);
|
|
83
|
-
} catch {
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function coerceNumber(value: unknown): number | null {
|
|
89
|
-
if (value === null || value === undefined) return null;
|
|
90
|
-
if (typeof value === 'number') {
|
|
91
|
-
return Number.isFinite(value) ? value : null;
|
|
92
|
-
}
|
|
93
|
-
if (typeof value === 'bigint') {
|
|
94
|
-
const coerced = Number(value);
|
|
95
|
-
return Number.isFinite(coerced) ? coerced : null;
|
|
96
|
-
}
|
|
97
|
-
if (typeof value === 'string') {
|
|
98
|
-
const coerced = Number(value);
|
|
99
|
-
return Number.isFinite(coerced) ? coerced : null;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
72
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
105
73
|
return typeof value === 'object' && value !== null;
|
|
106
74
|
}
|
|
@@ -537,10 +505,10 @@ export async function pushCommit<
|
|
|
537
505
|
const savepointName = 'sync_apply';
|
|
538
506
|
let useSavepoints = dialect.supportsSavepoints;
|
|
539
507
|
if (useSavepoints && ops.length === 1) {
|
|
540
|
-
const singleOpHandler =
|
|
541
|
-
|
|
542
|
-
ops[0]!.table
|
|
543
|
-
|
|
508
|
+
const singleOpHandler = handlers.byTable.get(ops[0]!.table);
|
|
509
|
+
if (!singleOpHandler) {
|
|
510
|
+
throw new Error(`Unknown table: ${ops[0]!.table}`);
|
|
511
|
+
}
|
|
544
512
|
if (singleOpHandler.canRejectSingleOperationWithoutSavepoint) {
|
|
545
513
|
useSavepoints = false;
|
|
546
514
|
}
|
|
@@ -561,7 +529,10 @@ export async function pushCommit<
|
|
|
561
529
|
|
|
562
530
|
for (let i = 0; i < ops.length; ) {
|
|
563
531
|
const op = ops[i]!;
|
|
564
|
-
const handler =
|
|
532
|
+
const handler = handlers.byTable.get(op.table);
|
|
533
|
+
if (!handler) {
|
|
534
|
+
throw new Error(`Unknown table: ${op.table}`);
|
|
535
|
+
}
|
|
565
536
|
|
|
566
537
|
const operationCtx = {
|
|
567
538
|
db: trx,
|