@quereus/sync 0.6.3 → 0.7.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.
Files changed (33) hide show
  1. package/README.md +187 -155
  2. package/dist/src/index.d.ts +3 -5
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/index.js +3 -5
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/src/metadata/keys.d.ts +8 -0
  7. package/dist/src/metadata/keys.d.ts.map +1 -1
  8. package/dist/src/metadata/keys.js +11 -0
  9. package/dist/src/metadata/keys.js.map +1 -1
  10. package/dist/src/sync/change-applicator.d.ts +44 -0
  11. package/dist/src/sync/change-applicator.d.ts.map +1 -0
  12. package/dist/src/sync/change-applicator.js +243 -0
  13. package/dist/src/sync/change-applicator.js.map +1 -0
  14. package/dist/src/sync/snapshot-stream.d.ts +26 -0
  15. package/dist/src/sync/snapshot-stream.d.ts.map +1 -0
  16. package/dist/src/sync/snapshot-stream.js +375 -0
  17. package/dist/src/sync/snapshot-stream.js.map +1 -0
  18. package/dist/src/sync/snapshot.d.ts +17 -0
  19. package/dist/src/sync/snapshot.d.ts.map +1 -0
  20. package/dist/src/sync/snapshot.js +166 -0
  21. package/dist/src/sync/snapshot.js.map +1 -0
  22. package/dist/src/sync/store-adapter.d.ts.map +1 -1
  23. package/dist/src/sync/store-adapter.js +3 -2
  24. package/dist/src/sync/store-adapter.js.map +1 -1
  25. package/dist/src/sync/sync-context.d.ts +47 -0
  26. package/dist/src/sync/sync-context.d.ts.map +1 -0
  27. package/dist/src/sync/sync-context.js +36 -0
  28. package/dist/src/sync/sync-context.js.map +1 -0
  29. package/dist/src/sync/sync-manager-impl.d.ts +21 -48
  30. package/dist/src/sync/sync-manager-impl.d.ts.map +1 -1
  31. package/dist/src/sync/sync-manager-impl.js +140 -988
  32. package/dist/src/sync/sync-manager-impl.js.map +1 -1
  33. 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