@quereus/sync 0.6.3 → 0.8.0
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 +187 -155
- package/dist/src/index.d.ts +3 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/metadata/keys.d.ts +8 -0
- package/dist/src/metadata/keys.d.ts.map +1 -1
- package/dist/src/metadata/keys.js +11 -0
- package/dist/src/metadata/keys.js.map +1 -1
- package/dist/src/sync/change-applicator.d.ts +44 -0
- package/dist/src/sync/change-applicator.d.ts.map +1 -0
- package/dist/src/sync/change-applicator.js +243 -0
- package/dist/src/sync/change-applicator.js.map +1 -0
- package/dist/src/sync/snapshot-stream.d.ts +26 -0
- package/dist/src/sync/snapshot-stream.d.ts.map +1 -0
- package/dist/src/sync/snapshot-stream.js +375 -0
- package/dist/src/sync/snapshot-stream.js.map +1 -0
- package/dist/src/sync/snapshot.d.ts +17 -0
- package/dist/src/sync/snapshot.d.ts.map +1 -0
- package/dist/src/sync/snapshot.js +166 -0
- package/dist/src/sync/snapshot.js.map +1 -0
- package/dist/src/sync/store-adapter.d.ts.map +1 -1
- package/dist/src/sync/store-adapter.js +3 -2
- package/dist/src/sync/store-adapter.js.map +1 -1
- package/dist/src/sync/sync-context.d.ts +47 -0
- package/dist/src/sync/sync-context.d.ts.map +1 -0
- package/dist/src/sync/sync-context.js +36 -0
- package/dist/src/sync/sync-context.js.map +1 -0
- package/dist/src/sync/sync-manager-impl.d.ts +21 -48
- package/dist/src/sync/sync-manager-impl.d.ts.map +1 -1
- package/dist/src/sync/sync-manager-impl.js +140 -988
- package/dist/src/sync/sync-manager-impl.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change application logic.
|
|
3
|
+
*
|
|
4
|
+
* Handles the 3-phase change application pattern:
|
|
5
|
+
* 1. resolveChange — CRDT conflict resolution (no writes)
|
|
6
|
+
* 2. applyToStore callback — write data to store
|
|
7
|
+
* 3. commitChangeMetadata — persist CRDT metadata
|
|
8
|
+
*/
|
|
9
|
+
import { compareHLC } from '../clock/hlc.js';
|
|
10
|
+
import { siteIdEquals } from '../clock/site.js';
|
|
11
|
+
import { persistHLCState, toError } from './sync-context.js';
|
|
12
|
+
/**
|
|
13
|
+
* Apply change sets from a remote peer.
|
|
14
|
+
*
|
|
15
|
+
* Three-phase process:
|
|
16
|
+
* 1. Resolve all changes (no writes)
|
|
17
|
+
* 2. Apply data to store via callback
|
|
18
|
+
* 3. Commit CRDT metadata
|
|
19
|
+
*/
|
|
20
|
+
export async function applyChanges(ctx, changes) {
|
|
21
|
+
let applied = 0;
|
|
22
|
+
let skipped = 0;
|
|
23
|
+
let conflicts = 0;
|
|
24
|
+
const dataChangesToApply = [];
|
|
25
|
+
const schemaChangesToApply = [];
|
|
26
|
+
const appliedChanges = [];
|
|
27
|
+
const resolvedDataChanges = [];
|
|
28
|
+
const pendingSchemaMigrations = [];
|
|
29
|
+
// PHASE 1: Resolve all changes (no writes yet)
|
|
30
|
+
for (const changeSet of changes) {
|
|
31
|
+
ctx.hlcManager.receive(changeSet.hlc);
|
|
32
|
+
// Process schema migrations first (DDL before DML)
|
|
33
|
+
for (const migration of changeSet.schemaMigrations) {
|
|
34
|
+
const schemaVersion = migration.schemaVersion ??
|
|
35
|
+
(await ctx.schemaMigrations.getCurrentVersion(migration.schema, migration.table)) + 1;
|
|
36
|
+
const existingMigration = await ctx.schemaMigrations.getMigration(migration.schema, migration.table, schemaVersion);
|
|
37
|
+
if (existingMigration) {
|
|
38
|
+
if (compareHLC(migration.hlc, existingMigration.hlc) <= 0) {
|
|
39
|
+
skipped++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
schemaChangesToApply.push({
|
|
44
|
+
type: migration.type,
|
|
45
|
+
schema: migration.schema,
|
|
46
|
+
table: migration.table,
|
|
47
|
+
ddl: migration.ddl,
|
|
48
|
+
});
|
|
49
|
+
pendingSchemaMigrations.push({ migration, schemaVersion });
|
|
50
|
+
applied++;
|
|
51
|
+
}
|
|
52
|
+
// Resolve data changes
|
|
53
|
+
for (const change of changeSet.changes) {
|
|
54
|
+
const resolved = await resolveChange(ctx, change);
|
|
55
|
+
if (resolved.outcome === 'applied') {
|
|
56
|
+
applied++;
|
|
57
|
+
appliedChanges.push({ change, siteId: changeSet.siteId });
|
|
58
|
+
resolvedDataChanges.push(resolved);
|
|
59
|
+
if (resolved.dataChange) {
|
|
60
|
+
dataChangesToApply.push(resolved.dataChange);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (resolved.outcome === 'skipped') {
|
|
64
|
+
skipped++;
|
|
65
|
+
}
|
|
66
|
+
else if (resolved.outcome === 'conflict') {
|
|
67
|
+
conflicts++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// PHASE 2: Apply data and schema changes to the store via callback
|
|
72
|
+
if (ctx.applyToStore && (dataChangesToApply.length > 0 || schemaChangesToApply.length > 0)) {
|
|
73
|
+
try {
|
|
74
|
+
await ctx.applyToStore(dataChangesToApply, schemaChangesToApply, { remote: true });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Emit error state so UI can react. CRDT metadata is NOT committed,
|
|
78
|
+
// allowing the same changes to be re-resolved on the next sync attempt.
|
|
79
|
+
ctx.syncEvents.emitSyncStateChange({
|
|
80
|
+
status: 'error',
|
|
81
|
+
error: toError(error),
|
|
82
|
+
});
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// PHASE 3: Commit CRDT metadata
|
|
87
|
+
await commitChangeMetadata(ctx, resolvedDataChanges);
|
|
88
|
+
// Commit schema migration metadata
|
|
89
|
+
for (const { migration, schemaVersion } of pendingSchemaMigrations) {
|
|
90
|
+
await ctx.schemaMigrations.recordMigration(migration.schema, migration.table, {
|
|
91
|
+
type: migration.type,
|
|
92
|
+
ddl: migration.ddl,
|
|
93
|
+
hlc: migration.hlc,
|
|
94
|
+
schemaVersion,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Emit remote change events
|
|
98
|
+
if (appliedChanges.length > 0) {
|
|
99
|
+
const changesBySite = new Map();
|
|
100
|
+
for (const { change, siteId } of appliedChanges) {
|
|
101
|
+
const siteKey = Array.from(siteId).join(',');
|
|
102
|
+
const siteChanges = changesBySite.get(siteKey);
|
|
103
|
+
if (siteChanges) {
|
|
104
|
+
siteChanges.push(change);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
changesBySite.set(siteKey, [change]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const appliedAt = ctx.hlcManager.now();
|
|
111
|
+
for (const [siteKey, siteChanges] of changesBySite) {
|
|
112
|
+
const siteIdBytes = new Uint8Array(siteKey.split(',').map(Number));
|
|
113
|
+
ctx.syncEvents.emitRemoteChange({
|
|
114
|
+
siteId: siteIdBytes,
|
|
115
|
+
transactionId: crypto.randomUUID(),
|
|
116
|
+
changes: siteChanges,
|
|
117
|
+
appliedAt,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await persistHLCState(ctx);
|
|
122
|
+
return {
|
|
123
|
+
applied,
|
|
124
|
+
skipped,
|
|
125
|
+
conflicts,
|
|
126
|
+
transactions: changes.length,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve CRDT conflicts for a single change WITHOUT writing metadata.
|
|
131
|
+
*
|
|
132
|
+
* Phase 1 of the 3-phase apply pattern.
|
|
133
|
+
*/
|
|
134
|
+
export async function resolveChange(ctx, change) {
|
|
135
|
+
// Skip changes that originated from ourselves (echo prevention)
|
|
136
|
+
if (siteIdEquals(change.hlc.siteId, ctx.getSiteId())) {
|
|
137
|
+
return { outcome: 'skipped', change };
|
|
138
|
+
}
|
|
139
|
+
if (change.type === 'delete') {
|
|
140
|
+
const existingTombstone = await ctx.tombstones.getTombstone(change.schema, change.table, change.pk);
|
|
141
|
+
if (existingTombstone && compareHLC(change.hlc, existingTombstone.hlc) <= 0) {
|
|
142
|
+
return { outcome: 'skipped', change };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
outcome: 'applied',
|
|
146
|
+
change,
|
|
147
|
+
dataChange: {
|
|
148
|
+
type: 'delete',
|
|
149
|
+
schema: change.schema,
|
|
150
|
+
table: change.table,
|
|
151
|
+
pk: change.pk,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Column change
|
|
157
|
+
const shouldApply = await ctx.columnVersions.shouldApplyWrite(change.schema, change.table, change.pk, change.column, change.hlc);
|
|
158
|
+
if (!shouldApply) {
|
|
159
|
+
const localVersion = await ctx.columnVersions.getColumnVersion(change.schema, change.table, change.pk, change.column);
|
|
160
|
+
if (localVersion) {
|
|
161
|
+
const conflictEvent = {
|
|
162
|
+
table: change.table,
|
|
163
|
+
pk: change.pk,
|
|
164
|
+
column: change.column,
|
|
165
|
+
localValue: localVersion.value,
|
|
166
|
+
remoteValue: change.value,
|
|
167
|
+
winner: 'local',
|
|
168
|
+
winningHLC: localVersion.hlc,
|
|
169
|
+
};
|
|
170
|
+
ctx.syncEvents.emitConflictResolved(conflictEvent);
|
|
171
|
+
}
|
|
172
|
+
return { outcome: 'conflict', change };
|
|
173
|
+
}
|
|
174
|
+
// Check for tombstone blocking
|
|
175
|
+
const isBlocked = await ctx.tombstones.isDeletedAndBlocking(change.schema, change.table, change.pk, change.hlc, ctx.config.allowResurrection);
|
|
176
|
+
if (isBlocked) {
|
|
177
|
+
return { outcome: 'skipped', change };
|
|
178
|
+
}
|
|
179
|
+
const oldColumnVersion = await ctx.columnVersions.getColumnVersion(change.schema, change.table, change.pk, change.column) ?? undefined;
|
|
180
|
+
// Emit conflict event when remote overwrites an existing local version
|
|
181
|
+
if (oldColumnVersion) {
|
|
182
|
+
const conflictEvent = {
|
|
183
|
+
table: change.table,
|
|
184
|
+
pk: change.pk,
|
|
185
|
+
column: change.column,
|
|
186
|
+
localValue: oldColumnVersion.value,
|
|
187
|
+
remoteValue: change.value,
|
|
188
|
+
winner: 'remote',
|
|
189
|
+
winningHLC: change.hlc,
|
|
190
|
+
};
|
|
191
|
+
ctx.syncEvents.emitConflictResolved(conflictEvent);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
outcome: 'applied',
|
|
195
|
+
change,
|
|
196
|
+
oldColumnVersion,
|
|
197
|
+
dataChange: {
|
|
198
|
+
type: 'update',
|
|
199
|
+
schema: change.schema,
|
|
200
|
+
table: change.table,
|
|
201
|
+
pk: change.pk,
|
|
202
|
+
columns: { [change.column]: change.value },
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Commit CRDT metadata for resolved changes.
|
|
209
|
+
*
|
|
210
|
+
* Phase 3 of the 3-phase apply pattern: called AFTER data is written to store.
|
|
211
|
+
*/
|
|
212
|
+
export async function commitChangeMetadata(ctx, resolvedChanges) {
|
|
213
|
+
if (resolvedChanges.length === 0)
|
|
214
|
+
return;
|
|
215
|
+
const batch = ctx.kv.batch();
|
|
216
|
+
for (const resolved of resolvedChanges) {
|
|
217
|
+
if (resolved.outcome !== 'applied')
|
|
218
|
+
continue;
|
|
219
|
+
const change = resolved.change;
|
|
220
|
+
if (change.type === 'delete') {
|
|
221
|
+
ctx.tombstones.setTombstoneBatch(batch, change.schema, change.table, change.pk, change.hlc);
|
|
222
|
+
ctx.changeLog.recordDeletionBatch(batch, change.hlc, change.schema, change.table, change.pk);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Delete old change log entry if exists
|
|
226
|
+
if (resolved.oldColumnVersion) {
|
|
227
|
+
ctx.changeLog.deleteEntryBatch(batch, resolved.oldColumnVersion.hlc, 'column', change.schema, change.table, change.pk, change.column);
|
|
228
|
+
}
|
|
229
|
+
ctx.columnVersions.setColumnVersionBatch(batch, change.schema, change.table, change.pk, change.column, { hlc: change.hlc, value: change.value });
|
|
230
|
+
ctx.changeLog.recordColumnChangeBatch(batch, change.hlc, change.schema, change.table, change.pk, change.column);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
await batch.write();
|
|
234
|
+
// Handle column version deletions for delete operations (requires async iteration)
|
|
235
|
+
for (const resolved of resolvedChanges) {
|
|
236
|
+
if (resolved.outcome !== 'applied')
|
|
237
|
+
continue;
|
|
238
|
+
if (resolved.change.type === 'delete') {
|
|
239
|
+
await ctx.columnVersions.deleteRowVersions(resolved.change.schema, resolved.change.table, resolved.change.pk);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=change-applicator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change-applicator.js","sourceRoot":"","sources":["../../../src/sync/change-applicator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming snapshot operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles chunked snapshot generation, application, and checkpoint
|
|
5
|
+
* management for memory-efficient sync of large databases.
|
|
6
|
+
*/
|
|
7
|
+
import type { SnapshotCheckpoint } from './manager.js';
|
|
8
|
+
import type { SnapshotChunk, SnapshotProgress } from './protocol.js';
|
|
9
|
+
import type { SyncContext } from './sync-context.js';
|
|
10
|
+
/**
|
|
11
|
+
* Stream a snapshot as chunks for memory-efficient transfer.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getSnapshotStream(ctx: SyncContext, chunkSize?: number): AsyncIterable<SnapshotChunk>;
|
|
14
|
+
/**
|
|
15
|
+
* Resume a snapshot transfer from a checkpoint.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resumeSnapshotStream(ctx: SyncContext, checkpoint: SnapshotCheckpoint): AsyncIterable<SnapshotChunk>;
|
|
18
|
+
/**
|
|
19
|
+
* Apply a streamed snapshot, processing chunks as they arrive.
|
|
20
|
+
*/
|
|
21
|
+
export declare function applySnapshotStream(ctx: SyncContext, chunks: AsyncIterable<SnapshotChunk>, onProgress?: (progress: SnapshotProgress) => void): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Retrieve a saved checkpoint for an in-progress snapshot.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getSnapshotCheckpoint(ctx: SyncContext, snapshotId: string): Promise<SnapshotCheckpoint | undefined>;
|
|
26
|
+
//# sourceMappingURL=snapshot-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-stream.d.ts","sourceRoot":"","sources":["../../../src/sync/snapshot-stream.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EACX,aAAa,EACb,gBAAgB,EAShB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiKrD;;GAEG;AACH,wBAAuB,iBAAiB,CACvC,GAAG,EAAE,WAAW,EAChB,SAAS,GAAE,MAAoC,GAC7C,aAAa,CAAC,aAAa,CAAC,CAO9B;AAED;;GAEG;AACH,wBAAuB,oBAAoB,CAC1C,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,kBAAkB,GAC5B,aAAa,CAAC,aAAa,CAAC,CAS9B;AAMD;;GAEG;AACH,wBAAsB,mBAAmB,CACxC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,EACpC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,IAAI,GAC/C,OAAO,CAAC,IAAI,CAAC,CA4Mf;AAMD;;GAEG;AACH,wBAAsB,qBAAqB,CAC1C,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAiBzC"}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming snapshot operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles chunked snapshot generation, application, and checkpoint
|
|
5
|
+
* management for memory-efficient sync of large databases.
|
|
6
|
+
*/
|
|
7
|
+
import { deserializeColumnVersion } from '../metadata/column-version.js';
|
|
8
|
+
import { deserializeMigration } from '../metadata/schema-migration.js';
|
|
9
|
+
import { buildAllColumnVersionsScanBounds, buildAllTombstonesScanBounds, buildAllSchemaMigrationsScanBounds, buildAllChangeLogScanBounds, buildTableColumnVersionScanBounds, parseColumnVersionKey, parseSchemaMigrationKey, encodePK, } from '../metadata/keys.js';
|
|
10
|
+
import { persistHLCState } from './sync-context.js';
|
|
11
|
+
/** Default chunk size for streaming snapshots. */
|
|
12
|
+
const DEFAULT_SNAPSHOT_CHUNK_SIZE = 1000;
|
|
13
|
+
/** Key prefix for snapshot checkpoints. */
|
|
14
|
+
const CHECKPOINT_PREFIX = 'sc:';
|
|
15
|
+
/**
|
|
16
|
+
* Shared generator that streams snapshot chunks.
|
|
17
|
+
*
|
|
18
|
+
* Both `getSnapshotStream` and `resumeSnapshotStream` delegate here,
|
|
19
|
+
* differing only in initial parameters (skip set, identity source, entry count).
|
|
20
|
+
*/
|
|
21
|
+
async function* streamSnapshotChunks(ctx, opts) {
|
|
22
|
+
const { snapshotId, siteId, hlc, chunkSize, completedTables, initialEntryCount } = opts;
|
|
23
|
+
const completedSet = completedTables ?? new Set();
|
|
24
|
+
// Count tables and migrations for header
|
|
25
|
+
const tableKeys = new Set();
|
|
26
|
+
const cvBounds = buildAllColumnVersionsScanBounds();
|
|
27
|
+
for await (const entry of ctx.kv.iterate(cvBounds)) {
|
|
28
|
+
const parsed = parseColumnVersionKey(entry.key);
|
|
29
|
+
if (parsed)
|
|
30
|
+
tableKeys.add(`${parsed.schema}.${parsed.table}`);
|
|
31
|
+
}
|
|
32
|
+
let migrationCount = 0;
|
|
33
|
+
const smBounds = buildAllSchemaMigrationsScanBounds();
|
|
34
|
+
for await (const _entry of ctx.kv.iterate(smBounds)) {
|
|
35
|
+
migrationCount++;
|
|
36
|
+
}
|
|
37
|
+
// Yield header
|
|
38
|
+
const header = {
|
|
39
|
+
type: 'header',
|
|
40
|
+
siteId,
|
|
41
|
+
hlc,
|
|
42
|
+
tableCount: tableKeys.size,
|
|
43
|
+
migrationCount,
|
|
44
|
+
snapshotId,
|
|
45
|
+
};
|
|
46
|
+
yield header;
|
|
47
|
+
// Stream each table, skipping completed ones
|
|
48
|
+
let totalEntries = initialEntryCount ?? 0;
|
|
49
|
+
for (const tableKey of tableKeys) {
|
|
50
|
+
if (completedSet.has(tableKey))
|
|
51
|
+
continue;
|
|
52
|
+
const [schema, table] = tableKey.split('.');
|
|
53
|
+
const tableCvBounds = buildTableColumnVersionScanBounds(schema, table);
|
|
54
|
+
// Yield table start (entry count filled in at table-end)
|
|
55
|
+
const tableStart = {
|
|
56
|
+
type: 'table-start',
|
|
57
|
+
schema,
|
|
58
|
+
table,
|
|
59
|
+
estimatedEntries: 0,
|
|
60
|
+
};
|
|
61
|
+
yield tableStart;
|
|
62
|
+
// Stream column versions in chunks (single pass per table)
|
|
63
|
+
let entries = [];
|
|
64
|
+
let entriesWritten = 0;
|
|
65
|
+
for await (const entry of ctx.kv.iterate(tableCvBounds)) {
|
|
66
|
+
const parsed = parseColumnVersionKey(entry.key);
|
|
67
|
+
if (!parsed)
|
|
68
|
+
continue;
|
|
69
|
+
const cv = deserializeColumnVersion(entry.value);
|
|
70
|
+
const versionKey = `${encodePK(parsed.pk)}:${parsed.column}`;
|
|
71
|
+
entries.push([versionKey, cv.hlc, cv.value]);
|
|
72
|
+
entriesWritten++;
|
|
73
|
+
if (entries.length >= chunkSize) {
|
|
74
|
+
const chunk = {
|
|
75
|
+
type: 'column-versions',
|
|
76
|
+
schema,
|
|
77
|
+
table,
|
|
78
|
+
entries,
|
|
79
|
+
};
|
|
80
|
+
yield chunk;
|
|
81
|
+
entries = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Yield remaining entries
|
|
85
|
+
if (entries.length > 0) {
|
|
86
|
+
const chunk = {
|
|
87
|
+
type: 'column-versions',
|
|
88
|
+
schema,
|
|
89
|
+
table,
|
|
90
|
+
entries,
|
|
91
|
+
};
|
|
92
|
+
yield chunk;
|
|
93
|
+
}
|
|
94
|
+
// Yield table end
|
|
95
|
+
const tableEnd = {
|
|
96
|
+
type: 'table-end',
|
|
97
|
+
schema,
|
|
98
|
+
table,
|
|
99
|
+
entriesWritten,
|
|
100
|
+
};
|
|
101
|
+
yield tableEnd;
|
|
102
|
+
totalEntries += entriesWritten;
|
|
103
|
+
}
|
|
104
|
+
// Stream schema migrations
|
|
105
|
+
for await (const entry of ctx.kv.iterate(smBounds)) {
|
|
106
|
+
const parsed = parseSchemaMigrationKey(entry.key);
|
|
107
|
+
if (!parsed)
|
|
108
|
+
continue;
|
|
109
|
+
const migration = deserializeMigration(entry.value);
|
|
110
|
+
const migrationChunk = {
|
|
111
|
+
type: 'schema-migration',
|
|
112
|
+
migration: {
|
|
113
|
+
type: migration.type,
|
|
114
|
+
schema: parsed.schema,
|
|
115
|
+
table: parsed.table,
|
|
116
|
+
ddl: migration.ddl,
|
|
117
|
+
hlc: migration.hlc,
|
|
118
|
+
schemaVersion: migration.schemaVersion,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
yield migrationChunk;
|
|
122
|
+
}
|
|
123
|
+
// Yield footer
|
|
124
|
+
const footer = {
|
|
125
|
+
type: 'footer',
|
|
126
|
+
snapshotId,
|
|
127
|
+
totalTables: tableKeys.size,
|
|
128
|
+
totalEntries,
|
|
129
|
+
totalMigrations: migrationCount,
|
|
130
|
+
};
|
|
131
|
+
yield footer;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Stream a snapshot as chunks for memory-efficient transfer.
|
|
135
|
+
*/
|
|
136
|
+
export async function* getSnapshotStream(ctx, chunkSize = DEFAULT_SNAPSHOT_CHUNK_SIZE) {
|
|
137
|
+
yield* streamSnapshotChunks(ctx, {
|
|
138
|
+
snapshotId: crypto.randomUUID(),
|
|
139
|
+
siteId: ctx.getSiteId(),
|
|
140
|
+
hlc: ctx.getCurrentHLC(),
|
|
141
|
+
chunkSize,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resume a snapshot transfer from a checkpoint.
|
|
146
|
+
*/
|
|
147
|
+
export async function* resumeSnapshotStream(ctx, checkpoint) {
|
|
148
|
+
yield* streamSnapshotChunks(ctx, {
|
|
149
|
+
snapshotId: checkpoint.snapshotId,
|
|
150
|
+
siteId: checkpoint.siteId,
|
|
151
|
+
hlc: checkpoint.hlc,
|
|
152
|
+
chunkSize: DEFAULT_SNAPSHOT_CHUNK_SIZE,
|
|
153
|
+
completedTables: new Set(checkpoint.completedTables),
|
|
154
|
+
initialEntryCount: checkpoint.entriesProcessed,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Snapshot Application
|
|
159
|
+
// ============================================================================
|
|
160
|
+
/**
|
|
161
|
+
* Apply a streamed snapshot, processing chunks as they arrive.
|
|
162
|
+
*/
|
|
163
|
+
export async function applySnapshotStream(ctx, chunks, onProgress) {
|
|
164
|
+
let snapshotId;
|
|
165
|
+
let snapshotHLC;
|
|
166
|
+
let totalTables = 0;
|
|
167
|
+
let totalEntries = 0;
|
|
168
|
+
let tablesProcessed = 0;
|
|
169
|
+
let entriesProcessed = 0;
|
|
170
|
+
let currentTable;
|
|
171
|
+
const completedTables = [];
|
|
172
|
+
// Pending data to apply to store (batched for efficiency)
|
|
173
|
+
let pendingDataChanges = [];
|
|
174
|
+
let pendingSchemaChanges = [];
|
|
175
|
+
const DATA_FLUSH_SIZE = 100;
|
|
176
|
+
const flushDataToStore = async () => {
|
|
177
|
+
if (ctx.applyToStore && (pendingDataChanges.length > 0 || pendingSchemaChanges.length > 0)) {
|
|
178
|
+
await ctx.applyToStore(pendingDataChanges, pendingSchemaChanges, { remote: true });
|
|
179
|
+
pendingDataChanges = [];
|
|
180
|
+
pendingSchemaChanges = [];
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
// Clear existing metadata before applying
|
|
184
|
+
const clearBatch = ctx.kv.batch();
|
|
185
|
+
for await (const entry of ctx.kv.iterate(buildAllColumnVersionsScanBounds())) {
|
|
186
|
+
clearBatch.delete(entry.key);
|
|
187
|
+
}
|
|
188
|
+
for await (const entry of ctx.kv.iterate(buildAllTombstonesScanBounds())) {
|
|
189
|
+
clearBatch.delete(entry.key);
|
|
190
|
+
}
|
|
191
|
+
for await (const entry of ctx.kv.iterate(buildAllChangeLogScanBounds())) {
|
|
192
|
+
clearBatch.delete(entry.key);
|
|
193
|
+
}
|
|
194
|
+
await clearBatch.write();
|
|
195
|
+
// Process chunks
|
|
196
|
+
let batch = ctx.kv.batch();
|
|
197
|
+
let batchSize = 0;
|
|
198
|
+
const BATCH_FLUSH_SIZE = 1000;
|
|
199
|
+
let currentTableSchema;
|
|
200
|
+
let currentTableName;
|
|
201
|
+
const rowColumns = new Map();
|
|
202
|
+
for await (const chunk of chunks) {
|
|
203
|
+
switch (chunk.type) {
|
|
204
|
+
case 'header':
|
|
205
|
+
snapshotId = chunk.snapshotId;
|
|
206
|
+
snapshotHLC = chunk.hlc;
|
|
207
|
+
totalTables = chunk.tableCount;
|
|
208
|
+
break;
|
|
209
|
+
case 'table-start':
|
|
210
|
+
currentTable = `${chunk.schema}.${chunk.table}`;
|
|
211
|
+
currentTableSchema = chunk.schema;
|
|
212
|
+
currentTableName = chunk.table;
|
|
213
|
+
totalEntries += chunk.estimatedEntries;
|
|
214
|
+
rowColumns.clear();
|
|
215
|
+
break;
|
|
216
|
+
case 'column-versions':
|
|
217
|
+
for (const [versionKey, hlc, value] of chunk.entries) {
|
|
218
|
+
const lastColon = versionKey.lastIndexOf(':');
|
|
219
|
+
if (lastColon === -1)
|
|
220
|
+
continue;
|
|
221
|
+
const rowKey = versionKey.slice(0, lastColon);
|
|
222
|
+
const column = versionKey.slice(lastColon + 1);
|
|
223
|
+
const pk = JSON.parse(rowKey);
|
|
224
|
+
// Track column for data application
|
|
225
|
+
if (!rowColumns.has(rowKey)) {
|
|
226
|
+
rowColumns.set(rowKey, {});
|
|
227
|
+
}
|
|
228
|
+
rowColumns.get(rowKey)[column] = value;
|
|
229
|
+
// Write CRDT metadata
|
|
230
|
+
ctx.columnVersions.setColumnVersionBatch(batch, chunk.schema, chunk.table, pk, column, { hlc, value });
|
|
231
|
+
ctx.changeLog.recordColumnChangeBatch(batch, hlc, chunk.schema, chunk.table, pk, column);
|
|
232
|
+
batchSize++;
|
|
233
|
+
entriesProcessed++;
|
|
234
|
+
if (batchSize >= BATCH_FLUSH_SIZE) {
|
|
235
|
+
await batch.write();
|
|
236
|
+
batch = ctx.kv.batch();
|
|
237
|
+
batchSize = 0;
|
|
238
|
+
// Save checkpoint
|
|
239
|
+
if (snapshotId && snapshotHLC) {
|
|
240
|
+
await saveSnapshotCheckpoint(ctx, {
|
|
241
|
+
snapshotId,
|
|
242
|
+
siteId: ctx.getSiteId(),
|
|
243
|
+
hlc: snapshotHLC,
|
|
244
|
+
lastTableIndex: tablesProcessed,
|
|
245
|
+
lastEntryIndex: entriesProcessed,
|
|
246
|
+
completedTables: [...completedTables],
|
|
247
|
+
entriesProcessed,
|
|
248
|
+
createdAt: Date.now(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (onProgress && snapshotId) {
|
|
254
|
+
onProgress({
|
|
255
|
+
snapshotId,
|
|
256
|
+
tablesProcessed,
|
|
257
|
+
totalTables,
|
|
258
|
+
entriesProcessed,
|
|
259
|
+
totalEntries,
|
|
260
|
+
currentTable,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
case 'table-end':
|
|
265
|
+
// Flush accumulated rows to store
|
|
266
|
+
if (currentTableSchema && currentTableName) {
|
|
267
|
+
for (const [rowKey, columns] of rowColumns) {
|
|
268
|
+
const pk = JSON.parse(rowKey);
|
|
269
|
+
pendingDataChanges.push({
|
|
270
|
+
type: 'update',
|
|
271
|
+
schema: currentTableSchema,
|
|
272
|
+
table: currentTableName,
|
|
273
|
+
pk,
|
|
274
|
+
columns,
|
|
275
|
+
});
|
|
276
|
+
if (pendingDataChanges.length >= DATA_FLUSH_SIZE) {
|
|
277
|
+
await flushDataToStore();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
rowColumns.clear();
|
|
281
|
+
}
|
|
282
|
+
tablesProcessed++;
|
|
283
|
+
if (currentTable) {
|
|
284
|
+
completedTables.push(currentTable);
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case 'schema-migration': {
|
|
288
|
+
const migration = chunk.migration;
|
|
289
|
+
pendingSchemaChanges.push({
|
|
290
|
+
type: migration.type,
|
|
291
|
+
schema: migration.schema,
|
|
292
|
+
table: migration.table,
|
|
293
|
+
ddl: migration.ddl,
|
|
294
|
+
});
|
|
295
|
+
const schemaVersion = migration.schemaVersion ??
|
|
296
|
+
(await ctx.schemaMigrations.getCurrentVersion(migration.schema, migration.table)) + 1;
|
|
297
|
+
await ctx.schemaMigrations.recordMigration(migration.schema, migration.table, {
|
|
298
|
+
type: migration.type,
|
|
299
|
+
ddl: migration.ddl,
|
|
300
|
+
hlc: migration.hlc,
|
|
301
|
+
schemaVersion,
|
|
302
|
+
});
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case 'footer':
|
|
306
|
+
// Flush remaining data to store
|
|
307
|
+
await flushDataToStore();
|
|
308
|
+
// Flush remaining metadata batch
|
|
309
|
+
if (batchSize > 0) {
|
|
310
|
+
await batch.write();
|
|
311
|
+
}
|
|
312
|
+
// Update HLC
|
|
313
|
+
if (snapshotHLC) {
|
|
314
|
+
ctx.hlcManager.receive(snapshotHLC);
|
|
315
|
+
await persistHLCState(ctx);
|
|
316
|
+
}
|
|
317
|
+
// Clear checkpoint
|
|
318
|
+
if (snapshotId) {
|
|
319
|
+
await clearSnapshotCheckpoint(ctx, snapshotId);
|
|
320
|
+
}
|
|
321
|
+
// Emit sync state change
|
|
322
|
+
if (snapshotHLC) {
|
|
323
|
+
ctx.syncEvents.emitSyncStateChange({ status: 'synced', lastSyncHLC: snapshotHLC });
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// ============================================================================
|
|
330
|
+
// Checkpoint Management
|
|
331
|
+
// ============================================================================
|
|
332
|
+
/**
|
|
333
|
+
* Retrieve a saved checkpoint for an in-progress snapshot.
|
|
334
|
+
*/
|
|
335
|
+
export async function getSnapshotCheckpoint(ctx, snapshotId) {
|
|
336
|
+
const key = new TextEncoder().encode(`${CHECKPOINT_PREFIX}${snapshotId}`);
|
|
337
|
+
const data = await ctx.kv.get(key);
|
|
338
|
+
if (!data)
|
|
339
|
+
return undefined;
|
|
340
|
+
const json = new TextDecoder().decode(data);
|
|
341
|
+
const obj = JSON.parse(json);
|
|
342
|
+
return {
|
|
343
|
+
...obj,
|
|
344
|
+
hlc: {
|
|
345
|
+
wallTime: BigInt(obj.hlc.wallTime),
|
|
346
|
+
counter: obj.hlc.counter,
|
|
347
|
+
siteId: new Uint8Array(obj.hlc.siteId),
|
|
348
|
+
},
|
|
349
|
+
siteId: new Uint8Array(obj.siteId),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Save a checkpoint during a streaming snapshot apply.
|
|
354
|
+
*/
|
|
355
|
+
async function saveSnapshotCheckpoint(ctx, checkpoint) {
|
|
356
|
+
const key = new TextEncoder().encode(`${CHECKPOINT_PREFIX}${checkpoint.snapshotId}`);
|
|
357
|
+
const json = JSON.stringify({
|
|
358
|
+
...checkpoint,
|
|
359
|
+
hlc: {
|
|
360
|
+
wallTime: checkpoint.hlc.wallTime.toString(),
|
|
361
|
+
counter: checkpoint.hlc.counter,
|
|
362
|
+
siteId: Array.from(checkpoint.hlc.siteId),
|
|
363
|
+
},
|
|
364
|
+
siteId: Array.from(checkpoint.siteId),
|
|
365
|
+
});
|
|
366
|
+
await ctx.kv.put(key, new TextEncoder().encode(json));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Clear checkpoint after a snapshot completes successfully.
|
|
370
|
+
*/
|
|
371
|
+
async function clearSnapshotCheckpoint(ctx, snapshotId) {
|
|
372
|
+
const key = new TextEncoder().encode(`${CHECKPOINT_PREFIX}${snapshotId}`);
|
|
373
|
+
await ctx.kv.delete(key);
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=snapshot-stream.js.map
|