@loro-dev/flock-sqlite 0.1.0 → 0.4.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/dist/index.cjs +7 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.mjs +7 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +657 -71
- package/src/types.ts +22 -4
package/src/index.ts
CHANGED
|
@@ -29,8 +29,9 @@ import type {
|
|
|
29
29
|
ScanOptions,
|
|
30
30
|
ScanRow,
|
|
31
31
|
Value,
|
|
32
|
-
VersionVector,
|
|
32
|
+
VersionVector as VersionVectorType,
|
|
33
33
|
VersionVectorEntry,
|
|
34
|
+
EntryInfo,
|
|
34
35
|
} from "./types";
|
|
35
36
|
|
|
36
37
|
type ClockRow = {
|
|
@@ -60,9 +61,20 @@ type PutOperation = {
|
|
|
60
61
|
eventSink?: Array<{ key: KeyPart[]; payload: ExportPayload; source: string }>;
|
|
61
62
|
};
|
|
62
63
|
|
|
64
|
+
type EncodableVersionVectorEntry = {
|
|
65
|
+
peer: string;
|
|
66
|
+
peerBytes: Uint8Array;
|
|
67
|
+
timestamp: number;
|
|
68
|
+
counter: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export interface VersionVector extends VersionVectorType {}
|
|
72
|
+
|
|
63
73
|
const textEncoder = new TextEncoder();
|
|
64
|
-
const
|
|
65
|
-
|
|
74
|
+
const textDecoder = new TextDecoder();
|
|
75
|
+
const structuredCloneFn: (<T>(value: T) => T) | undefined = (
|
|
76
|
+
globalThis as typeof globalThis & { structuredClone?: <T>(value: T) => T }
|
|
77
|
+
).structuredClone;
|
|
66
78
|
|
|
67
79
|
function utf8ByteLength(value: string): number {
|
|
68
80
|
return textEncoder.encode(value).length;
|
|
@@ -74,11 +86,18 @@ function isValidPeerId(peerId: unknown): peerId is string {
|
|
|
74
86
|
|
|
75
87
|
function createRandomPeerId(): string {
|
|
76
88
|
const id = new Uint8Array(32);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
89
|
+
type CryptoLike = {
|
|
90
|
+
getRandomValues?: (buffer: Uint8Array) => Uint8Array;
|
|
91
|
+
randomBytes?: (len: number) => Uint8Array;
|
|
92
|
+
};
|
|
93
|
+
const cryptoLike: CryptoLike | undefined =
|
|
94
|
+
typeof crypto !== "undefined"
|
|
95
|
+
? (crypto as unknown as CryptoLike)
|
|
96
|
+
: undefined;
|
|
97
|
+
if (cryptoLike?.getRandomValues) {
|
|
98
|
+
cryptoLike.getRandomValues(id);
|
|
99
|
+
} else if (cryptoLike?.randomBytes) {
|
|
100
|
+
const buf: Uint8Array = cryptoLike.randomBytes(32);
|
|
82
101
|
id.set(buf);
|
|
83
102
|
} else {
|
|
84
103
|
for (let i = 0; i < 32; i += 1) {
|
|
@@ -115,7 +134,15 @@ function cloneMetadata(metadata: unknown): MetadataMap | undefined {
|
|
|
115
134
|
return cloneJson(metadata as MetadataMap);
|
|
116
135
|
}
|
|
117
136
|
|
|
118
|
-
function
|
|
137
|
+
function normalizeMetadataMap(metadata: unknown): MetadataMap {
|
|
138
|
+
const cloned = cloneMetadata(metadata);
|
|
139
|
+
return cloned ?? {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function assignPayload(
|
|
143
|
+
target: ExportPayload,
|
|
144
|
+
source?: ExportPayload | void,
|
|
145
|
+
): void {
|
|
119
146
|
if (!source || typeof source !== "object") {
|
|
120
147
|
return;
|
|
121
148
|
}
|
|
@@ -134,12 +161,253 @@ function clonePayload(payload: ExportPayload | undefined): ExportPayload {
|
|
|
134
161
|
return result;
|
|
135
162
|
}
|
|
136
163
|
|
|
137
|
-
function mergePayload(
|
|
164
|
+
function mergePayload(
|
|
165
|
+
base: ExportPayload,
|
|
166
|
+
update?: ExportPayload | void,
|
|
167
|
+
): ExportPayload {
|
|
138
168
|
const result = clonePayload(base);
|
|
139
169
|
assignPayload(result, update);
|
|
140
170
|
return result;
|
|
141
171
|
}
|
|
142
172
|
|
|
173
|
+
function comparePeerBytes(a: Uint8Array, b: Uint8Array): number {
|
|
174
|
+
if (a === b) {
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
const limit = Math.min(a.length, b.length);
|
|
178
|
+
for (let i = 0; i < limit; i += 1) {
|
|
179
|
+
const diff = a[i] - b[i];
|
|
180
|
+
if (diff !== 0) {
|
|
181
|
+
return diff;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return a.length - b.length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function collectEncodableVersionVectorEntries(
|
|
188
|
+
vv?: VersionVector,
|
|
189
|
+
): EncodableVersionVectorEntry[] {
|
|
190
|
+
if (!vv || typeof vv !== "object") {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const entries: EncodableVersionVectorEntry[] = [];
|
|
194
|
+
for (const [peer, entry] of Object.entries(vv)) {
|
|
195
|
+
if (!entry || !isValidPeerId(peer)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const { physicalTime, logicalCounter } = entry;
|
|
199
|
+
if (
|
|
200
|
+
typeof physicalTime !== "number" ||
|
|
201
|
+
!Number.isFinite(physicalTime) ||
|
|
202
|
+
typeof logicalCounter !== "number" ||
|
|
203
|
+
!Number.isFinite(logicalCounter)
|
|
204
|
+
) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const peerBytes = textEncoder.encode(peer);
|
|
208
|
+
entries.push({
|
|
209
|
+
peer,
|
|
210
|
+
peerBytes,
|
|
211
|
+
timestamp: Math.trunc(physicalTime),
|
|
212
|
+
counter: Math.max(0, Math.trunc(logicalCounter)),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
entries.sort((a, b) => {
|
|
216
|
+
if (a.timestamp !== b.timestamp) {
|
|
217
|
+
return a.timestamp - b.timestamp;
|
|
218
|
+
}
|
|
219
|
+
const peerCmp = comparePeerBytes(a.peerBytes, b.peerBytes);
|
|
220
|
+
if (peerCmp !== 0) {
|
|
221
|
+
return peerCmp;
|
|
222
|
+
}
|
|
223
|
+
return a.counter - b.counter;
|
|
224
|
+
});
|
|
225
|
+
return entries;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function writeUnsignedLeb128(value: number, target: number[]): void {
|
|
229
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
230
|
+
throw new TypeError("leb128 values must be finite and non-negative");
|
|
231
|
+
}
|
|
232
|
+
let remaining = Math.trunc(value);
|
|
233
|
+
if (remaining === 0) {
|
|
234
|
+
target.push(0);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
while (remaining > 0) {
|
|
238
|
+
const byte = remaining % 0x80;
|
|
239
|
+
remaining = Math.floor(remaining / 0x80);
|
|
240
|
+
target.push(remaining > 0 ? byte | 0x80 : byte);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function writeVarStringBytes(bytes: Uint8Array, target: number[]): void {
|
|
245
|
+
writeUnsignedLeb128(bytes.length, target);
|
|
246
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
247
|
+
target.push(bytes[i]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const VERSION_VECTOR_MAGIC = new Uint8Array([86, 69, 86, 69]); // "VEVE"
|
|
252
|
+
|
|
253
|
+
function encodeVersionVectorBinary(vv?: VersionVector): Uint8Array {
|
|
254
|
+
const entries = collectEncodableVersionVectorEntries(vv);
|
|
255
|
+
const buffer: number[] = Array.from(VERSION_VECTOR_MAGIC);
|
|
256
|
+
if (entries.length === 0) {
|
|
257
|
+
return Uint8Array.from(buffer);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let lastTimestamp = 0;
|
|
261
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
262
|
+
const entry = entries[i];
|
|
263
|
+
if (entry.timestamp < 0) {
|
|
264
|
+
throw new TypeError("timestamp must be non-negative");
|
|
265
|
+
}
|
|
266
|
+
if (i === 0) {
|
|
267
|
+
writeUnsignedLeb128(entry.timestamp, buffer);
|
|
268
|
+
lastTimestamp = entry.timestamp;
|
|
269
|
+
} else {
|
|
270
|
+
const delta = entry.timestamp - lastTimestamp;
|
|
271
|
+
if (delta < 0) {
|
|
272
|
+
throw new TypeError("version vector timestamps must be non-decreasing");
|
|
273
|
+
}
|
|
274
|
+
writeUnsignedLeb128(delta, buffer);
|
|
275
|
+
lastTimestamp = entry.timestamp;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
writeUnsignedLeb128(entry.counter, buffer);
|
|
279
|
+
writeVarStringBytes(entry.peerBytes, buffer);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return Uint8Array.from(buffer);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function decodeUnsignedLeb128(
|
|
286
|
+
bytes: Uint8Array,
|
|
287
|
+
offset: number,
|
|
288
|
+
): [number, number] {
|
|
289
|
+
let result = 0;
|
|
290
|
+
let multiplier = 1;
|
|
291
|
+
let consumed = 0;
|
|
292
|
+
while (offset + consumed < bytes.length) {
|
|
293
|
+
const byte = bytes[offset + consumed];
|
|
294
|
+
consumed += 1;
|
|
295
|
+
// Use arithmetic instead of bitwise operations to avoid 32-bit overflow.
|
|
296
|
+
// JavaScript bitwise operators convert to 32-bit signed integers,
|
|
297
|
+
// which breaks for values >= 2^31.
|
|
298
|
+
result += (byte & 0x7f) * multiplier;
|
|
299
|
+
if ((byte & 0x80) === 0) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
multiplier *= 128;
|
|
303
|
+
}
|
|
304
|
+
return [result, consumed];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function decodeVarString(bytes: Uint8Array, offset: number): [string, number] {
|
|
308
|
+
const [length, used] = decodeUnsignedLeb128(bytes, offset);
|
|
309
|
+
const start = offset + used;
|
|
310
|
+
const end = start + length;
|
|
311
|
+
if (end > bytes.length) {
|
|
312
|
+
throw new TypeError("varString length exceeds buffer");
|
|
313
|
+
}
|
|
314
|
+
const slice = bytes.subarray(start, end);
|
|
315
|
+
return [textDecoder.decode(slice), used + length];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function hasMagic(bytes: Uint8Array): boolean {
|
|
319
|
+
return (
|
|
320
|
+
bytes.length >= 4 &&
|
|
321
|
+
bytes[0] === VERSION_VECTOR_MAGIC[0] &&
|
|
322
|
+
bytes[1] === VERSION_VECTOR_MAGIC[1] &&
|
|
323
|
+
bytes[2] === VERSION_VECTOR_MAGIC[2] &&
|
|
324
|
+
bytes[3] === VERSION_VECTOR_MAGIC[3]
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function decodeLegacyVersionVector(bytes: Uint8Array): VersionVector {
|
|
329
|
+
let offset = 0;
|
|
330
|
+
const [count, usedCount] = decodeUnsignedLeb128(bytes, offset);
|
|
331
|
+
offset += usedCount;
|
|
332
|
+
const [baseTimestamp, usedBase] = decodeUnsignedLeb128(bytes, offset);
|
|
333
|
+
offset += usedBase;
|
|
334
|
+
const vv: VersionVector = {};
|
|
335
|
+
for (let i = 0; i < count; i += 1) {
|
|
336
|
+
const [peer, usedPeer] = decodeVarString(bytes, offset);
|
|
337
|
+
offset += usedPeer;
|
|
338
|
+
if (!isValidPeerId(peer)) {
|
|
339
|
+
throw new TypeError("invalid peer id in encoded version vector");
|
|
340
|
+
}
|
|
341
|
+
const [delta, usedDelta] = decodeUnsignedLeb128(bytes, offset);
|
|
342
|
+
offset += usedDelta;
|
|
343
|
+
const [counter, usedCounter] = decodeUnsignedLeb128(bytes, offset);
|
|
344
|
+
offset += usedCounter;
|
|
345
|
+
vv[peer] = {
|
|
346
|
+
physicalTime: baseTimestamp + delta,
|
|
347
|
+
logicalCounter: counter,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return vv;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function decodeNewVersionVector(bytes: Uint8Array): VersionVector {
|
|
354
|
+
let offset = 4;
|
|
355
|
+
const vv: VersionVector = {};
|
|
356
|
+
if (offset === bytes.length) {
|
|
357
|
+
return vv;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const [firstTimestamp, usedTs] = decodeUnsignedLeb128(bytes, offset);
|
|
361
|
+
offset += usedTs;
|
|
362
|
+
const [firstCounter, usedCounter] = decodeUnsignedLeb128(bytes, offset);
|
|
363
|
+
offset += usedCounter;
|
|
364
|
+
const [firstPeer, usedPeer] = decodeVarString(bytes, offset);
|
|
365
|
+
offset += usedPeer;
|
|
366
|
+
if (!isValidPeerId(firstPeer)) {
|
|
367
|
+
throw new TypeError("invalid peer id in encoded version vector");
|
|
368
|
+
}
|
|
369
|
+
vv[firstPeer] = {
|
|
370
|
+
physicalTime: firstTimestamp,
|
|
371
|
+
logicalCounter: firstCounter,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
let lastTimestamp = firstTimestamp;
|
|
375
|
+
while (offset < bytes.length) {
|
|
376
|
+
const [delta, usedDelta] = decodeUnsignedLeb128(bytes, offset);
|
|
377
|
+
offset += usedDelta;
|
|
378
|
+
const [counter, usedCtr] = decodeUnsignedLeb128(bytes, offset);
|
|
379
|
+
offset += usedCtr;
|
|
380
|
+
const [peer, usedPeerLen] = decodeVarString(bytes, offset);
|
|
381
|
+
offset += usedPeerLen;
|
|
382
|
+
if (!isValidPeerId(peer)) {
|
|
383
|
+
throw new TypeError("invalid peer id in encoded version vector");
|
|
384
|
+
}
|
|
385
|
+
const timestamp = lastTimestamp + delta;
|
|
386
|
+
if (timestamp < lastTimestamp) {
|
|
387
|
+
throw new TypeError("version vector timestamps must be non-decreasing");
|
|
388
|
+
}
|
|
389
|
+
vv[peer] = { physicalTime: timestamp, logicalCounter: counter };
|
|
390
|
+
lastTimestamp = timestamp;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return vv;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function decodeVersionVectorBinary(bytes: Uint8Array): VersionVector {
|
|
397
|
+
if (hasMagic(bytes)) {
|
|
398
|
+
return decodeNewVersionVector(bytes);
|
|
399
|
+
}
|
|
400
|
+
return decodeLegacyVersionVector(bytes);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function encodeVersionVector(vector: VersionVector): Uint8Array {
|
|
404
|
+
return encodeVersionVectorBinary(vector);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function decodeVersionVector(bytes: Uint8Array): VersionVector {
|
|
408
|
+
return decodeVersionVectorBinary(bytes);
|
|
409
|
+
}
|
|
410
|
+
|
|
143
411
|
function buildRecord(clock: EntryClock, payload: ExportPayload): ExportRecord {
|
|
144
412
|
const record: ExportRecord = {
|
|
145
413
|
c: formatClock(clock),
|
|
@@ -154,7 +422,10 @@ function buildRecord(clock: EntryClock, payload: ExportPayload): ExportRecord {
|
|
|
154
422
|
return record;
|
|
155
423
|
}
|
|
156
424
|
|
|
157
|
-
function normalizeImportDecision(decision: ImportDecision): {
|
|
425
|
+
function normalizeImportDecision(decision: ImportDecision): {
|
|
426
|
+
accept: boolean;
|
|
427
|
+
reason?: string;
|
|
428
|
+
} {
|
|
158
429
|
if (!decision || typeof decision !== "object") {
|
|
159
430
|
return { accept: true };
|
|
160
431
|
}
|
|
@@ -167,7 +438,9 @@ function normalizeImportDecision(decision: ImportDecision): { accept: boolean; r
|
|
|
167
438
|
return { accept: true };
|
|
168
439
|
}
|
|
169
440
|
|
|
170
|
-
function isExportOptions(
|
|
441
|
+
function isExportOptions(
|
|
442
|
+
arg: VersionVector | ExportOptions | undefined,
|
|
443
|
+
): arg is ExportOptions {
|
|
171
444
|
return (
|
|
172
445
|
typeof arg === "object" &&
|
|
173
446
|
arg !== null &&
|
|
@@ -178,8 +451,14 @@ function isExportOptions(arg: VersionVector | ExportOptions | undefined): arg is
|
|
|
178
451
|
);
|
|
179
452
|
}
|
|
180
453
|
|
|
181
|
-
function isImportOptions(
|
|
182
|
-
|
|
454
|
+
function isImportOptions(
|
|
455
|
+
arg: ExportBundle | ImportOptions,
|
|
456
|
+
): arg is ImportOptions {
|
|
457
|
+
return (
|
|
458
|
+
typeof arg === "object" &&
|
|
459
|
+
arg !== null &&
|
|
460
|
+
Object.prototype.hasOwnProperty.call(arg, "bundle")
|
|
461
|
+
);
|
|
183
462
|
}
|
|
184
463
|
|
|
185
464
|
function parseMetadata(json: string | null): MetadataMap | undefined {
|
|
@@ -219,6 +498,31 @@ function parseClockString(raw: string): EntryClock {
|
|
|
219
498
|
};
|
|
220
499
|
}
|
|
221
500
|
|
|
501
|
+
function normalizeRowClock(
|
|
502
|
+
physical: unknown,
|
|
503
|
+
logical: unknown,
|
|
504
|
+
peer: unknown,
|
|
505
|
+
): EntryClock | undefined {
|
|
506
|
+
if (
|
|
507
|
+
typeof physical !== "number" ||
|
|
508
|
+
typeof logical !== "number" ||
|
|
509
|
+
typeof peer !== "string"
|
|
510
|
+
) {
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
if (!isValidPeerId(peer)) {
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
if (!Number.isFinite(physical) || !Number.isFinite(logical)) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
physicalTime: physical,
|
|
521
|
+
logicalCounter: Math.trunc(logical),
|
|
522
|
+
peerId: peer,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
222
526
|
function formatClock(clock: EntryClock): string {
|
|
223
527
|
return `${clock.physicalTime},${clock.logicalCounter},${clock.peerId}`;
|
|
224
528
|
}
|
|
@@ -236,7 +540,9 @@ function compareClock(a: EntryClock, b: EntryClock): number {
|
|
|
236
540
|
return a.peerId > b.peerId ? 1 : -1;
|
|
237
541
|
}
|
|
238
542
|
|
|
239
|
-
function normalizeVersionEntry(
|
|
543
|
+
function normalizeVersionEntry(
|
|
544
|
+
entry?: VersionVectorEntry,
|
|
545
|
+
): VersionVectorEntry | undefined {
|
|
240
546
|
if (!entry) return undefined;
|
|
241
547
|
const { physicalTime, logicalCounter } = entry;
|
|
242
548
|
if (!Number.isFinite(physicalTime) || !Number.isFinite(logicalCounter)) {
|
|
@@ -296,7 +602,9 @@ function normalizeTablePrefix(prefix?: string): string {
|
|
|
296
602
|
throw new TypeError("tablePrefix must be a string");
|
|
297
603
|
}
|
|
298
604
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {
|
|
299
|
-
throw new TypeError(
|
|
605
|
+
throw new TypeError(
|
|
606
|
+
"tablePrefix must start with a letter/underscore and use only letters, digits, or underscores",
|
|
607
|
+
);
|
|
300
608
|
}
|
|
301
609
|
return prefix;
|
|
302
610
|
}
|
|
@@ -320,7 +628,13 @@ export class FlockSQLite {
|
|
|
320
628
|
private listeners: Set<EventListener>;
|
|
321
629
|
private tables: TableNames;
|
|
322
630
|
|
|
323
|
-
private constructor(
|
|
631
|
+
private constructor(
|
|
632
|
+
db: UniStoreConnection,
|
|
633
|
+
peerId: string,
|
|
634
|
+
vv: Map<string, VersionVectorEntry>,
|
|
635
|
+
maxHlc: { physicalTime: number; logicalCounter: number },
|
|
636
|
+
tables: TableNames,
|
|
637
|
+
) {
|
|
324
638
|
this.db = db;
|
|
325
639
|
this.peerIdValue = peerId;
|
|
326
640
|
this.vv = vv;
|
|
@@ -339,7 +653,9 @@ export class FlockSQLite {
|
|
|
339
653
|
return new FlockSQLite(db, peerId, vv, maxHlc, tables);
|
|
340
654
|
}
|
|
341
655
|
|
|
342
|
-
static async fromJson(
|
|
656
|
+
static async fromJson(
|
|
657
|
+
options: FlockSQLiteOptions & { bundle: ExportBundle },
|
|
658
|
+
): Promise<FlockSQLite> {
|
|
343
659
|
const flock = await FlockSQLite.open(options);
|
|
344
660
|
await flock.importJson(options.bundle);
|
|
345
661
|
return flock;
|
|
@@ -349,7 +665,10 @@ export class FlockSQLite {
|
|
|
349
665
|
await this.db.close();
|
|
350
666
|
}
|
|
351
667
|
|
|
352
|
-
private static async ensureSchema(
|
|
668
|
+
private static async ensureSchema(
|
|
669
|
+
db: UniStoreConnection,
|
|
670
|
+
tables: TableNames,
|
|
671
|
+
): Promise<void> {
|
|
353
672
|
await db.exec(`
|
|
354
673
|
CREATE TABLE IF NOT EXISTS ${tables.kv} (
|
|
355
674
|
key BLOB PRIMARY KEY,
|
|
@@ -382,9 +701,15 @@ export class FlockSQLite {
|
|
|
382
701
|
);
|
|
383
702
|
}
|
|
384
703
|
|
|
385
|
-
private static async resolvePeerId(
|
|
704
|
+
private static async resolvePeerId(
|
|
705
|
+
db: UniStoreConnection,
|
|
706
|
+
tables: TableNames,
|
|
707
|
+
provided?: string,
|
|
708
|
+
): Promise<string> {
|
|
386
709
|
const normalized = normalizePeerId(provided);
|
|
387
|
-
const rows = await db.query<{ peer_id: string }>(
|
|
710
|
+
const rows = await db.query<{ peer_id: string }>(
|
|
711
|
+
`SELECT peer_id FROM ${tables.meta} LIMIT 1`,
|
|
712
|
+
);
|
|
388
713
|
if (rows.length > 0 && typeof rows[0]?.peer_id === "string") {
|
|
389
714
|
const existing = rows[0].peer_id;
|
|
390
715
|
if (provided && existing !== normalized) {
|
|
@@ -394,11 +719,19 @@ export class FlockSQLite {
|
|
|
394
719
|
return normalizePeerId(existing);
|
|
395
720
|
}
|
|
396
721
|
await db.exec(`DELETE FROM ${tables.meta}`);
|
|
397
|
-
await db.run(`INSERT INTO ${tables.meta}(peer_id) VALUES (?)`, [
|
|
722
|
+
await db.run(`INSERT INTO ${tables.meta}(peer_id) VALUES (?)`, [
|
|
723
|
+
normalized,
|
|
724
|
+
]);
|
|
398
725
|
return normalized;
|
|
399
726
|
}
|
|
400
727
|
|
|
401
|
-
private static async loadVersionState(
|
|
728
|
+
private static async loadVersionState(
|
|
729
|
+
db: UniStoreConnection,
|
|
730
|
+
tables: TableNames,
|
|
731
|
+
): Promise<{
|
|
732
|
+
vv: Map<string, VersionVectorEntry>;
|
|
733
|
+
maxHlc: { physicalTime: number; logicalCounter: number };
|
|
734
|
+
}> {
|
|
402
735
|
const vv = new Map<string, VersionVectorEntry>();
|
|
403
736
|
const rows = await db.query<ClockRow>(
|
|
404
737
|
`SELECT peer, MAX(physical) AS physical, MAX(logical) AS logical FROM ${tables.kv} GROUP BY peer`,
|
|
@@ -419,14 +752,20 @@ export class FlockSQLite {
|
|
|
419
752
|
const first = maxRow[0];
|
|
420
753
|
const maxHlc =
|
|
421
754
|
first && Number.isFinite(first.physical) && Number.isFinite(first.logical)
|
|
422
|
-
? {
|
|
755
|
+
? {
|
|
756
|
+
physicalTime: Number(first.physical),
|
|
757
|
+
logicalCounter: Number(first.logical),
|
|
758
|
+
}
|
|
423
759
|
: { physicalTime: 0, logicalCounter: 0 };
|
|
424
760
|
return { vv, maxHlc };
|
|
425
761
|
}
|
|
426
762
|
|
|
427
763
|
private bumpVersion(clock: EntryClock): void {
|
|
428
764
|
const current = this.vv.get(clock.peerId);
|
|
429
|
-
if (
|
|
765
|
+
if (
|
|
766
|
+
!current ||
|
|
767
|
+
compareClock(clock, { ...current, peerId: clock.peerId }) > 0
|
|
768
|
+
) {
|
|
430
769
|
this.vv.set(clock.peerId, {
|
|
431
770
|
physicalTime: clock.physicalTime,
|
|
432
771
|
logicalCounter: clock.logicalCounter,
|
|
@@ -437,7 +776,10 @@ export class FlockSQLite {
|
|
|
437
776
|
(this.maxHlc.physicalTime === clock.physicalTime &&
|
|
438
777
|
this.maxHlc.logicalCounter < clock.logicalCounter)
|
|
439
778
|
) {
|
|
440
|
-
this.maxHlc = {
|
|
779
|
+
this.maxHlc = {
|
|
780
|
+
physicalTime: clock.physicalTime,
|
|
781
|
+
logicalCounter: clock.logicalCounter,
|
|
782
|
+
};
|
|
441
783
|
}
|
|
442
784
|
}
|
|
443
785
|
|
|
@@ -451,16 +793,22 @@ export class FlockSQLite {
|
|
|
451
793
|
} else {
|
|
452
794
|
logical = logical + 1;
|
|
453
795
|
}
|
|
454
|
-
return {
|
|
796
|
+
return {
|
|
797
|
+
physicalTime: physical,
|
|
798
|
+
logicalCounter: logical,
|
|
799
|
+
peerId: this.peerIdValue,
|
|
800
|
+
};
|
|
455
801
|
}
|
|
456
802
|
|
|
457
803
|
private async applyOperation(operation: PutOperation): Promise<boolean> {
|
|
458
804
|
const keyBytes = encodeKeyParts(operation.key);
|
|
459
|
-
const clock = operation.clock ?? this.allocateClock(operation.now);
|
|
460
805
|
const payload = mergePayload(operation.payload, {});
|
|
461
|
-
const dataJson =
|
|
462
|
-
|
|
806
|
+
const dataJson =
|
|
807
|
+
payload.data === undefined ? null : JSON.stringify(payload.data);
|
|
808
|
+
const metadataJson =
|
|
809
|
+
payload.metadata === undefined ? null : JSON.stringify(payload.metadata);
|
|
463
810
|
let applied = false;
|
|
811
|
+
let usedClock: EntryClock | undefined;
|
|
464
812
|
|
|
465
813
|
await this.db.asyncTransaction(async (tx) => {
|
|
466
814
|
const existingRows = await tx.query<KvRow>(
|
|
@@ -469,12 +817,6 @@ export class FlockSQLite {
|
|
|
469
817
|
);
|
|
470
818
|
if (existingRows.length > 0) {
|
|
471
819
|
const existing = existingRows[0];
|
|
472
|
-
const existingClock: EntryClock = {
|
|
473
|
-
physicalTime: Number(existing.physical ?? 0),
|
|
474
|
-
logicalCounter: Number(existing.logical ?? 0),
|
|
475
|
-
peerId: String(existing.peer ?? ""),
|
|
476
|
-
};
|
|
477
|
-
const cmp = compareClock(clock, existingClock);
|
|
478
820
|
const existingData = existing.data ?? null;
|
|
479
821
|
const existingMeta = existing.metadata ?? null;
|
|
480
822
|
const samePayload =
|
|
@@ -484,10 +826,38 @@ export class FlockSQLite {
|
|
|
484
826
|
if (samePayload) {
|
|
485
827
|
return;
|
|
486
828
|
}
|
|
829
|
+
} else if (
|
|
830
|
+
operation.skipSameValue &&
|
|
831
|
+
dataJson === null &&
|
|
832
|
+
metadataJson === null
|
|
833
|
+
) {
|
|
834
|
+
// Key doesn't exist and we're trying to delete - skip
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Now allocate clock only if we're going to apply the operation
|
|
839
|
+
const clock = operation.clock ?? this.allocateClock(operation.now);
|
|
840
|
+
usedClock = clock;
|
|
841
|
+
|
|
842
|
+
if (existingRows.length > 0) {
|
|
843
|
+
const existing = existingRows[0];
|
|
844
|
+
const existingClock: EntryClock = {
|
|
845
|
+
physicalTime: Number(existing.physical ?? 0),
|
|
846
|
+
logicalCounter: Number(existing.logical ?? 0),
|
|
847
|
+
peerId: String(existing.peer ?? ""),
|
|
848
|
+
};
|
|
849
|
+
const cmp = compareClock(clock, existingClock);
|
|
487
850
|
if (cmp < 0) {
|
|
488
851
|
await tx.run(
|
|
489
852
|
`INSERT INTO ${this.tables.overridden}(key, data, metadata, physical, logical, peer) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
490
|
-
[
|
|
853
|
+
[
|
|
854
|
+
keyBytes,
|
|
855
|
+
dataJson,
|
|
856
|
+
metadataJson,
|
|
857
|
+
clock.physicalTime,
|
|
858
|
+
clock.logicalCounter,
|
|
859
|
+
clock.peerId,
|
|
860
|
+
],
|
|
491
861
|
);
|
|
492
862
|
return;
|
|
493
863
|
}
|
|
@@ -516,12 +886,21 @@ export class FlockSQLite {
|
|
|
516
886
|
physical=excluded.physical,
|
|
517
887
|
logical=excluded.logical,
|
|
518
888
|
peer=excluded.peer`,
|
|
519
|
-
[
|
|
889
|
+
[
|
|
890
|
+
keyBytes,
|
|
891
|
+
dataJson,
|
|
892
|
+
metadataJson,
|
|
893
|
+
clock.physicalTime,
|
|
894
|
+
clock.logicalCounter,
|
|
895
|
+
clock.peerId,
|
|
896
|
+
],
|
|
520
897
|
);
|
|
521
898
|
applied = true;
|
|
522
899
|
});
|
|
523
900
|
|
|
524
|
-
|
|
901
|
+
if (usedClock) {
|
|
902
|
+
this.bumpVersion(usedClock);
|
|
903
|
+
}
|
|
525
904
|
if (applied) {
|
|
526
905
|
const eventPayload = {
|
|
527
906
|
key: operation.key.slice(),
|
|
@@ -546,12 +925,17 @@ export class FlockSQLite {
|
|
|
546
925
|
}
|
|
547
926
|
const batch: EventBatch = {
|
|
548
927
|
source,
|
|
549
|
-
events: events.map(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
928
|
+
events: events.map(
|
|
929
|
+
(event): Event => ({
|
|
930
|
+
key: cloneJson(event.key),
|
|
931
|
+
value:
|
|
932
|
+
event.payload.data !== undefined
|
|
933
|
+
? cloneJson(event.payload.data)
|
|
934
|
+
: undefined,
|
|
935
|
+
metadata: cloneMetadata(event.payload.metadata),
|
|
936
|
+
payload: clonePayload(event.payload),
|
|
937
|
+
}),
|
|
938
|
+
),
|
|
555
939
|
};
|
|
556
940
|
this.listeners.forEach((listener) => {
|
|
557
941
|
listener(batch);
|
|
@@ -568,7 +952,11 @@ export class FlockSQLite {
|
|
|
568
952
|
});
|
|
569
953
|
}
|
|
570
954
|
|
|
571
|
-
async putWithMeta(
|
|
955
|
+
async putWithMeta(
|
|
956
|
+
key: KeyPart[],
|
|
957
|
+
value: Value,
|
|
958
|
+
options: PutWithMetaOptions = {},
|
|
959
|
+
): Promise<void> {
|
|
572
960
|
const basePayload: ExportPayload = { data: cloneJson(value) };
|
|
573
961
|
if (options.metadata) {
|
|
574
962
|
basePayload.metadata = cloneMetadata(options.metadata);
|
|
@@ -576,7 +964,10 @@ export class FlockSQLite {
|
|
|
576
964
|
const hooks = options.hooks?.transform;
|
|
577
965
|
if (hooks) {
|
|
578
966
|
const working = clonePayload(basePayload);
|
|
579
|
-
const transformed = await hooks(
|
|
967
|
+
const transformed = await hooks(
|
|
968
|
+
{ key: key.slice(), now: options.now },
|
|
969
|
+
working,
|
|
970
|
+
);
|
|
580
971
|
const finalPayload = mergePayload(basePayload, transformed ?? working);
|
|
581
972
|
if (finalPayload.data === undefined) {
|
|
582
973
|
throw new TypeError("putWithMeta requires a data value");
|
|
@@ -609,6 +1000,76 @@ export class FlockSQLite {
|
|
|
609
1000
|
});
|
|
610
1001
|
}
|
|
611
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* Force put a value even if it's the same as the current value.
|
|
1005
|
+
* This will refresh the timestamp.
|
|
1006
|
+
*/
|
|
1007
|
+
async forcePut(key: KeyPart[], value: Value, now?: number): Promise<void> {
|
|
1008
|
+
await this.applyOperation({
|
|
1009
|
+
key,
|
|
1010
|
+
payload: { data: cloneJson(value) },
|
|
1011
|
+
now,
|
|
1012
|
+
skipSameValue: false,
|
|
1013
|
+
source: "local",
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Force put a value with metadata even if it's the same as the current value.
|
|
1019
|
+
* This will refresh the timestamp.
|
|
1020
|
+
*/
|
|
1021
|
+
async forcePutWithMeta(
|
|
1022
|
+
key: KeyPart[],
|
|
1023
|
+
value: Value,
|
|
1024
|
+
options: PutWithMetaOptions = {},
|
|
1025
|
+
): Promise<void> {
|
|
1026
|
+
const basePayload: ExportPayload = { data: cloneJson(value) };
|
|
1027
|
+
if (options.metadata) {
|
|
1028
|
+
basePayload.metadata = cloneMetadata(options.metadata);
|
|
1029
|
+
}
|
|
1030
|
+
const hooks = options.hooks?.transform;
|
|
1031
|
+
if (hooks) {
|
|
1032
|
+
const working = clonePayload(basePayload);
|
|
1033
|
+
const transformed = await hooks(
|
|
1034
|
+
{ key: key.slice(), now: options.now },
|
|
1035
|
+
working,
|
|
1036
|
+
);
|
|
1037
|
+
const finalPayload = mergePayload(basePayload, transformed ?? working);
|
|
1038
|
+
if (finalPayload.data === undefined) {
|
|
1039
|
+
throw new TypeError("forcePutWithMeta requires a data value");
|
|
1040
|
+
}
|
|
1041
|
+
await this.applyOperation({
|
|
1042
|
+
key,
|
|
1043
|
+
payload: finalPayload,
|
|
1044
|
+
now: options.now,
|
|
1045
|
+
skipSameValue: false,
|
|
1046
|
+
source: "local",
|
|
1047
|
+
});
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
await this.applyOperation({
|
|
1051
|
+
key,
|
|
1052
|
+
payload: basePayload,
|
|
1053
|
+
now: options.now,
|
|
1054
|
+
skipSameValue: false,
|
|
1055
|
+
source: "local",
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Force delete a key even if it's already deleted.
|
|
1061
|
+
* This will refresh the timestamp.
|
|
1062
|
+
*/
|
|
1063
|
+
async forceDelete(key: KeyPart[], now?: number): Promise<void> {
|
|
1064
|
+
await this.applyOperation({
|
|
1065
|
+
key,
|
|
1066
|
+
payload: {},
|
|
1067
|
+
now,
|
|
1068
|
+
skipSameValue: false,
|
|
1069
|
+
source: "local",
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
612
1073
|
async set(key: KeyPart[], value: Value, now?: number): Promise<void> {
|
|
613
1074
|
await this.put(key, value, now);
|
|
614
1075
|
}
|
|
@@ -616,7 +1077,9 @@ export class FlockSQLite {
|
|
|
616
1077
|
async setPeerId(peerId: string): Promise<void> {
|
|
617
1078
|
const normalized = normalizePeerId(peerId);
|
|
618
1079
|
await this.db.exec(`DELETE FROM ${this.tables.meta}`);
|
|
619
|
-
await this.db.run(`INSERT INTO ${this.tables.meta}(peer_id) VALUES (?)`, [
|
|
1080
|
+
await this.db.run(`INSERT INTO ${this.tables.meta}(peer_id) VALUES (?)`, [
|
|
1081
|
+
normalized,
|
|
1082
|
+
]);
|
|
620
1083
|
this.peerIdValue = normalized;
|
|
621
1084
|
}
|
|
622
1085
|
|
|
@@ -631,6 +1094,38 @@ export class FlockSQLite {
|
|
|
631
1094
|
return parseData(row.data);
|
|
632
1095
|
}
|
|
633
1096
|
|
|
1097
|
+
/**
|
|
1098
|
+
* Returns the full entry payload (data, metadata, and clock) for a key.
|
|
1099
|
+
*
|
|
1100
|
+
* Compared to `get`, this preserves tombstone information: a deleted entry
|
|
1101
|
+
* still returns its clock and an empty metadata object with `data` omitted.
|
|
1102
|
+
* Missing or invalid keys return `undefined`. Metadata is cloned and
|
|
1103
|
+
* normalized to `{}` when absent.
|
|
1104
|
+
*/
|
|
1105
|
+
async getEntry(key: KeyPart[]): Promise<EntryInfo | undefined> {
|
|
1106
|
+
let keyBytes: Uint8Array;
|
|
1107
|
+
try {
|
|
1108
|
+
keyBytes = encodeKeyParts(key);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
const rows = await this.db.query<KvRow>(
|
|
1113
|
+
`SELECT data, metadata, physical, logical, peer FROM ${this.tables.kv} WHERE key = ? LIMIT 1`,
|
|
1114
|
+
[keyBytes],
|
|
1115
|
+
);
|
|
1116
|
+
const row = rows[0];
|
|
1117
|
+
if (!row) return undefined;
|
|
1118
|
+
const clock = normalizeRowClock(row.physical, row.logical, row.peer);
|
|
1119
|
+
if (!clock) return undefined;
|
|
1120
|
+
const metadata = normalizeMetadataMap(parseMetadata(row.metadata));
|
|
1121
|
+
const data = parseData(row.data);
|
|
1122
|
+
const info: EntryInfo = { metadata, clock };
|
|
1123
|
+
if (data !== undefined) {
|
|
1124
|
+
info.data = data;
|
|
1125
|
+
}
|
|
1126
|
+
return info;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
634
1129
|
async getMvr(key: KeyPart[]): Promise<Value[]> {
|
|
635
1130
|
const rows = await this.scan({ prefix: key });
|
|
636
1131
|
const values: Value[] = [];
|
|
@@ -657,14 +1152,20 @@ export class FlockSQLite {
|
|
|
657
1152
|
await this.put(composite, true, now);
|
|
658
1153
|
}
|
|
659
1154
|
|
|
660
|
-
private buildScanBounds(
|
|
661
|
-
|
|
662
|
-
|
|
1155
|
+
private buildScanBounds(options: ScanOptions): {
|
|
1156
|
+
where: string[];
|
|
1157
|
+
params: unknown[];
|
|
1158
|
+
empty?: boolean;
|
|
1159
|
+
postFilter?: (bytes: Uint8Array) => boolean;
|
|
1160
|
+
} {
|
|
663
1161
|
let lower: { value: Uint8Array; inclusive: boolean } | undefined;
|
|
664
1162
|
let upper: { value: Uint8Array; inclusive: boolean } | undefined;
|
|
665
1163
|
let prefixFilter: Uint8Array | undefined;
|
|
666
1164
|
|
|
667
|
-
const applyLower = (candidate: {
|
|
1165
|
+
const applyLower = (candidate: {
|
|
1166
|
+
value: Uint8Array;
|
|
1167
|
+
inclusive: boolean;
|
|
1168
|
+
}) => {
|
|
668
1169
|
if (!lower) {
|
|
669
1170
|
lower = candidate;
|
|
670
1171
|
return;
|
|
@@ -673,11 +1174,17 @@ export class FlockSQLite {
|
|
|
673
1174
|
if (cmp > 0) {
|
|
674
1175
|
lower = candidate;
|
|
675
1176
|
} else if (cmp === 0) {
|
|
676
|
-
lower = {
|
|
1177
|
+
lower = {
|
|
1178
|
+
value: lower.value,
|
|
1179
|
+
inclusive: lower.inclusive && candidate.inclusive,
|
|
1180
|
+
};
|
|
677
1181
|
}
|
|
678
1182
|
};
|
|
679
1183
|
|
|
680
|
-
const applyUpper = (candidate: {
|
|
1184
|
+
const applyUpper = (candidate: {
|
|
1185
|
+
value: Uint8Array;
|
|
1186
|
+
inclusive: boolean;
|
|
1187
|
+
}) => {
|
|
681
1188
|
if (!upper) {
|
|
682
1189
|
upper = candidate;
|
|
683
1190
|
return;
|
|
@@ -686,7 +1193,10 @@ export class FlockSQLite {
|
|
|
686
1193
|
if (cmp < 0) {
|
|
687
1194
|
upper = candidate;
|
|
688
1195
|
} else if (cmp === 0) {
|
|
689
|
-
upper = {
|
|
1196
|
+
upper = {
|
|
1197
|
+
value: upper.value,
|
|
1198
|
+
inclusive: upper.inclusive && candidate.inclusive,
|
|
1199
|
+
};
|
|
690
1200
|
}
|
|
691
1201
|
};
|
|
692
1202
|
|
|
@@ -727,7 +1237,10 @@ export class FlockSQLite {
|
|
|
727
1237
|
params.push(upper.value);
|
|
728
1238
|
}
|
|
729
1239
|
const postFilter = prefixFilter
|
|
730
|
-
? (
|
|
1240
|
+
? (
|
|
1241
|
+
(pf: Uint8Array) => (bytes: Uint8Array) =>
|
|
1242
|
+
keyMatchesPrefix(bytes, pf)
|
|
1243
|
+
)(prefixFilter)
|
|
731
1244
|
: undefined;
|
|
732
1245
|
return { where, params, postFilter };
|
|
733
1246
|
}
|
|
@@ -737,7 +1250,8 @@ export class FlockSQLite {
|
|
|
737
1250
|
if (bounds.empty) {
|
|
738
1251
|
return [];
|
|
739
1252
|
}
|
|
740
|
-
const clauses =
|
|
1253
|
+
const clauses =
|
|
1254
|
+
bounds.where.length > 0 ? `WHERE ${bounds.where.join(" AND ")}` : "";
|
|
741
1255
|
const rows = await this.db.query<KvRow>(
|
|
742
1256
|
`SELECT key, data, metadata, physical, logical, peer FROM ${this.tables.kv} ${clauses} ORDER BY key ASC`,
|
|
743
1257
|
bounds.params as [],
|
|
@@ -765,7 +1279,42 @@ export class FlockSQLite {
|
|
|
765
1279
|
return result;
|
|
766
1280
|
}
|
|
767
1281
|
|
|
768
|
-
|
|
1282
|
+
/**
|
|
1283
|
+
* Returns the exclusive version vector, which only includes peers that have
|
|
1284
|
+
* at least one entry in the current state. This is consistent with the state
|
|
1285
|
+
* after export and re-import.
|
|
1286
|
+
*
|
|
1287
|
+
* Use this version when sending to other peers for incremental sync.
|
|
1288
|
+
*/
|
|
1289
|
+
async version(): Promise<VersionVector> {
|
|
1290
|
+
// Find the maximum clock per peer. The clock ordering is (physical, logical).
|
|
1291
|
+
// We need the row with the highest (physical, logical) pair for each peer,
|
|
1292
|
+
// not MAX(physical) and MAX(logical) independently which could mix values from different rows.
|
|
1293
|
+
// Use a window function with lexicographic ordering to avoid floating-point precision issues.
|
|
1294
|
+
const rows = await this.db.query<{ peer: string; physical: number; logical: number }>(
|
|
1295
|
+
`SELECT peer, physical, logical FROM (
|
|
1296
|
+
SELECT peer, physical, logical,
|
|
1297
|
+
ROW_NUMBER() OVER (PARTITION BY peer ORDER BY physical DESC, logical DESC) as rn
|
|
1298
|
+
FROM ${this.tables.kv}
|
|
1299
|
+
) WHERE rn = 1`,
|
|
1300
|
+
);
|
|
1301
|
+
const vv: VersionVector = {};
|
|
1302
|
+
for (const row of rows) {
|
|
1303
|
+
vv[row.peer] = {
|
|
1304
|
+
physicalTime: row.physical,
|
|
1305
|
+
logicalCounter: row.logical,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
return vv;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Returns the inclusive version vector, which includes all peers ever seen,
|
|
1313
|
+
* even if their entries have been overridden by other peers.
|
|
1314
|
+
*
|
|
1315
|
+
* Use this version when checking if you have received all data from another peer.
|
|
1316
|
+
*/
|
|
1317
|
+
inclusiveVersion(): VersionVector {
|
|
769
1318
|
const vv: VersionVector = {};
|
|
770
1319
|
for (const [peer, clock] of this.vv.entries()) {
|
|
771
1320
|
vv[peer] = { ...clock };
|
|
@@ -781,7 +1330,11 @@ export class FlockSQLite {
|
|
|
781
1330
|
return this.maxHlc.physicalTime;
|
|
782
1331
|
}
|
|
783
1332
|
|
|
784
|
-
private async exportInternal(
|
|
1333
|
+
private async exportInternal(
|
|
1334
|
+
from?: VersionVector,
|
|
1335
|
+
pruneTombstonesBefore?: number,
|
|
1336
|
+
peerId?: string,
|
|
1337
|
+
): Promise<ExportBundle> {
|
|
785
1338
|
const normalizedFrom = new Map<string, VersionVectorEntry>();
|
|
786
1339
|
if (from) {
|
|
787
1340
|
for (const [peer, entry] of Object.entries(from)) {
|
|
@@ -795,7 +1348,10 @@ export class FlockSQLite {
|
|
|
795
1348
|
const entries: Record<string, ExportRecord> = {};
|
|
796
1349
|
|
|
797
1350
|
const peers = peerId ? [peerId] : Array.from(this.vv.keys());
|
|
798
|
-
const peersToExport: Array<{
|
|
1351
|
+
const peersToExport: Array<{
|
|
1352
|
+
peer: string;
|
|
1353
|
+
fromEntry?: VersionVectorEntry;
|
|
1354
|
+
}> = [];
|
|
799
1355
|
for (const peer of peers) {
|
|
800
1356
|
const localEntry = this.vv.get(peer);
|
|
801
1357
|
const fromEntry = normalizedFrom.get(peer);
|
|
@@ -813,7 +1369,10 @@ export class FlockSQLite {
|
|
|
813
1369
|
}
|
|
814
1370
|
|
|
815
1371
|
if (peerId && peersToExport.every((p) => p.peer !== peerId)) {
|
|
816
|
-
peersToExport.push({
|
|
1372
|
+
peersToExport.push({
|
|
1373
|
+
peer: peerId,
|
|
1374
|
+
fromEntry: normalizedFrom.get(peerId),
|
|
1375
|
+
});
|
|
817
1376
|
}
|
|
818
1377
|
|
|
819
1378
|
if (peersToExport.length === 0) {
|
|
@@ -867,7 +1426,11 @@ export class FlockSQLite {
|
|
|
867
1426
|
}
|
|
868
1427
|
|
|
869
1428
|
private async exportWithHooks(options: ExportOptions): Promise<ExportBundle> {
|
|
870
|
-
const base = await this.exportInternal(
|
|
1429
|
+
const base = await this.exportInternal(
|
|
1430
|
+
options.from,
|
|
1431
|
+
options.pruneTombstonesBefore,
|
|
1432
|
+
options.peerId,
|
|
1433
|
+
);
|
|
871
1434
|
const transform = options.hooks?.transform;
|
|
872
1435
|
if (!transform) {
|
|
873
1436
|
return base;
|
|
@@ -894,9 +1457,15 @@ export class FlockSQLite {
|
|
|
894
1457
|
|
|
895
1458
|
exportJson(): Promise<ExportBundle>;
|
|
896
1459
|
exportJson(from: VersionVector): Promise<ExportBundle>;
|
|
897
|
-
exportJson(
|
|
1460
|
+
exportJson(
|
|
1461
|
+
from: VersionVector,
|
|
1462
|
+
pruneTombstonesBefore: number,
|
|
1463
|
+
): Promise<ExportBundle>;
|
|
898
1464
|
exportJson(options: ExportOptions): Promise<ExportBundle>;
|
|
899
|
-
exportJson(
|
|
1465
|
+
exportJson(
|
|
1466
|
+
arg?: VersionVector | ExportOptions,
|
|
1467
|
+
pruneTombstonesBefore?: number,
|
|
1468
|
+
): Promise<ExportBundle> {
|
|
900
1469
|
if (isExportOptions(arg)) {
|
|
901
1470
|
return this.exportWithHooks(arg);
|
|
902
1471
|
}
|
|
@@ -909,7 +1478,11 @@ export class FlockSQLite {
|
|
|
909
1478
|
}
|
|
910
1479
|
let accepted = 0;
|
|
911
1480
|
const skipped: Array<{ key: KeyPart[]; reason: string }> = [];
|
|
912
|
-
const appliedEvents: Array<{
|
|
1481
|
+
const appliedEvents: Array<{
|
|
1482
|
+
key: KeyPart[];
|
|
1483
|
+
payload: ExportPayload;
|
|
1484
|
+
source: string;
|
|
1485
|
+
}> = [];
|
|
913
1486
|
for (const [keyString, record] of Object.entries(bundle.entries)) {
|
|
914
1487
|
let keyParts: KeyPart[];
|
|
915
1488
|
try {
|
|
@@ -945,14 +1518,18 @@ export class FlockSQLite {
|
|
|
945
1518
|
async importJson(arg: ExportBundle | ImportOptions): Promise<ImportReport> {
|
|
946
1519
|
if (isImportOptions(arg)) {
|
|
947
1520
|
const preprocess = arg.hooks?.preprocess;
|
|
948
|
-
const working = preprocess
|
|
1521
|
+
const working = preprocess
|
|
1522
|
+
? { version: arg.bundle.version, entries: { ...arg.bundle.entries } }
|
|
1523
|
+
: arg.bundle;
|
|
949
1524
|
const skipped: Array<{ key: KeyPart[]; reason: string }> = [];
|
|
950
1525
|
if (preprocess) {
|
|
951
1526
|
for (const [key, record] of Object.entries(working.entries)) {
|
|
952
1527
|
const contextKey = JSON.parse(key) as KeyPart[];
|
|
953
1528
|
const clock = parseClockString(record.c);
|
|
954
1529
|
const payload: ExportPayload = {};
|
|
955
|
-
if (record.d !== undefined)
|
|
1530
|
+
if (record.d !== undefined) {
|
|
1531
|
+
payload.data = cloneJson(record.d);
|
|
1532
|
+
}
|
|
956
1533
|
const metadata = cloneMetadata(record.m);
|
|
957
1534
|
if (metadata !== undefined) payload.metadata = metadata;
|
|
958
1535
|
const decision = await preprocess(
|
|
@@ -961,7 +1538,10 @@ export class FlockSQLite {
|
|
|
961
1538
|
);
|
|
962
1539
|
const normalized = normalizeImportDecision(decision);
|
|
963
1540
|
if (!normalized.accept) {
|
|
964
|
-
skipped.push({
|
|
1541
|
+
skipped.push({
|
|
1542
|
+
key: contextKey,
|
|
1543
|
+
reason: normalized.reason ?? "rejected",
|
|
1544
|
+
});
|
|
965
1545
|
delete working.entries[key];
|
|
966
1546
|
} else {
|
|
967
1547
|
working.entries[key] = buildRecord(clock, payload);
|
|
@@ -969,7 +1549,10 @@ export class FlockSQLite {
|
|
|
969
1549
|
}
|
|
970
1550
|
}
|
|
971
1551
|
const baseReport = await this.importInternal(working);
|
|
972
|
-
return {
|
|
1552
|
+
return {
|
|
1553
|
+
accepted: baseReport.accepted,
|
|
1554
|
+
skipped: skipped.concat(baseReport.skipped),
|
|
1555
|
+
};
|
|
973
1556
|
}
|
|
974
1557
|
return this.importInternal(arg);
|
|
975
1558
|
}
|
|
@@ -996,7 +1579,10 @@ export class FlockSQLite {
|
|
|
996
1579
|
await this.importJson(bundle);
|
|
997
1580
|
}
|
|
998
1581
|
|
|
999
|
-
static async checkConsistency(
|
|
1582
|
+
static async checkConsistency(
|
|
1583
|
+
a: FlockSQLite,
|
|
1584
|
+
b: FlockSQLite,
|
|
1585
|
+
): Promise<boolean> {
|
|
1000
1586
|
const [digestA, digestB] = await Promise.all([a.digest(), b.digest()]);
|
|
1001
1587
|
return digestA === digestB;
|
|
1002
1588
|
}
|
|
@@ -1032,8 +1618,8 @@ export type {
|
|
|
1032
1618
|
ScanOptions,
|
|
1033
1619
|
ScanRow,
|
|
1034
1620
|
Value,
|
|
1035
|
-
VersionVector,
|
|
1036
1621
|
VersionVectorEntry,
|
|
1622
|
+
EntryInfo,
|
|
1037
1623
|
};
|
|
1038
1624
|
|
|
1039
1625
|
export { FlockSQLite as Flock };
|