@loro-dev/flock-sqlite 0.1.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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.cjs +27 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.mjs +27 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +76 -0
- package/src/digest.ts +259 -0
- package/src/index.ts +1040 -0
- package/src/key-encoding.ts +106 -0
- package/src/memcomparable.d.ts +5 -0
- package/src/memcomparable.ts +1852 -0
- package/src/moonbit.d.ts +57 -0
- package/src/types.ts +128 -0
package/src/digest.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { EntryClock, MetadataMap, Value } from "./types";
|
|
2
|
+
|
|
3
|
+
const TAG_RAW_VALUE = 0x10;
|
|
4
|
+
const TAG_RAW_CLOCK = 0x11;
|
|
5
|
+
const TAG_RAW_METADATA = 0x12;
|
|
6
|
+
const TAG_JSON_NULL = 0x20;
|
|
7
|
+
const TAG_JSON_BOOL = 0x21;
|
|
8
|
+
const TAG_JSON_NUMBER = 0x22;
|
|
9
|
+
const TAG_JSON_STRING = 0x23;
|
|
10
|
+
const TAG_JSON_ARRAY = 0x24;
|
|
11
|
+
const TAG_JSON_OBJECT = 0x25;
|
|
12
|
+
|
|
13
|
+
type RawValue = {
|
|
14
|
+
data?: Value;
|
|
15
|
+
metadata?: MetadataMap;
|
|
16
|
+
clock: EntryClock;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DigestRow = {
|
|
20
|
+
key: Uint8Array;
|
|
21
|
+
data: string | null;
|
|
22
|
+
metadata: string | null;
|
|
23
|
+
physical: number;
|
|
24
|
+
logical: number;
|
|
25
|
+
peer: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const textEncoder = new TextEncoder();
|
|
29
|
+
|
|
30
|
+
function toHex(bytes: Uint8Array): string {
|
|
31
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function encodeU64(value: number | bigint): Uint8Array {
|
|
35
|
+
let remaining = BigInt(value);
|
|
36
|
+
const out: number[] = [];
|
|
37
|
+
do {
|
|
38
|
+
let byte = Number(remaining & 0x7fn);
|
|
39
|
+
remaining >>= 7n;
|
|
40
|
+
if (remaining !== 0n) {
|
|
41
|
+
byte |= 0x80;
|
|
42
|
+
}
|
|
43
|
+
out.push(byte);
|
|
44
|
+
} while (remaining !== 0n);
|
|
45
|
+
return new Uint8Array(out);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function encodeI64(value: number | bigint): Uint8Array {
|
|
49
|
+
let remaining = BigInt(value);
|
|
50
|
+
const out: number[] = [];
|
|
51
|
+
let more = true;
|
|
52
|
+
while (more) {
|
|
53
|
+
let byte = Number(remaining & 0x7fn);
|
|
54
|
+
remaining >>= 7n;
|
|
55
|
+
const signBitSet = (byte & 0x40) !== 0;
|
|
56
|
+
if (
|
|
57
|
+
(remaining === 0n && !signBitSet) ||
|
|
58
|
+
(remaining === -1n && signBitSet)
|
|
59
|
+
) {
|
|
60
|
+
more = false;
|
|
61
|
+
} else {
|
|
62
|
+
byte |= 0x80;
|
|
63
|
+
}
|
|
64
|
+
out.push(byte);
|
|
65
|
+
}
|
|
66
|
+
return new Uint8Array(out);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function sha256(bytes: Uint8Array): Promise<Uint8Array> {
|
|
70
|
+
const cryptoMaybe = typeof crypto !== "undefined" ? crypto : undefined;
|
|
71
|
+
if (cryptoMaybe?.subtle) {
|
|
72
|
+
const digestInput = new ArrayBuffer(bytes.byteLength);
|
|
73
|
+
new Uint8Array(digestInput).set(bytes);
|
|
74
|
+
const digest = await cryptoMaybe.subtle.digest("SHA-256", digestInput);
|
|
75
|
+
return new Uint8Array(digest);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const { createHash } = await import("crypto");
|
|
79
|
+
const hash = createHash("sha256");
|
|
80
|
+
hash.update(bytes);
|
|
81
|
+
return new Uint8Array(hash.digest());
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error("No crypto implementation available for digest");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class DigestWriter {
|
|
88
|
+
private chunks: number[] = [];
|
|
89
|
+
|
|
90
|
+
writeTag(tag: number): void {
|
|
91
|
+
this.chunks.push(tag & 0xff);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
writeBool(value: boolean): void {
|
|
95
|
+
this.chunks.push(value ? 1 : 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
writeBytes(bytes: Uint8Array): void {
|
|
99
|
+
this.writeLen(bytes.length);
|
|
100
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
101
|
+
this.chunks.push(bytes[i]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
writeRawBytes(bytes: Uint8Array): void {
|
|
106
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
107
|
+
this.chunks.push(bytes[i]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
writeLen(length: number): void {
|
|
112
|
+
this.writeRawBytes(encodeU64(length));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
writeI64(value: number): void {
|
|
116
|
+
this.writeRawBytes(encodeI64(value));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
writeF64(value: number): void {
|
|
120
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
121
|
+
view.setFloat64(0, value, false);
|
|
122
|
+
const bytes = new Uint8Array(view.buffer);
|
|
123
|
+
this.writeRawBytes(bytes);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
writeString(value: string): void {
|
|
127
|
+
this.writeBytes(textEncoder.encode(value));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
writeJson(value: Value): void {
|
|
131
|
+
if (value === null) {
|
|
132
|
+
this.writeTag(TAG_JSON_NULL);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === "boolean") {
|
|
136
|
+
this.writeTag(TAG_JSON_BOOL);
|
|
137
|
+
this.writeBool(value);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === "number") {
|
|
141
|
+
this.writeTag(TAG_JSON_NUMBER);
|
|
142
|
+
this.writeF64(value);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (typeof value === "string") {
|
|
146
|
+
this.writeTag(TAG_JSON_STRING);
|
|
147
|
+
this.writeString(value);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
this.writeTag(TAG_JSON_ARRAY);
|
|
152
|
+
this.writeLen(value.length);
|
|
153
|
+
for (const entry of value) {
|
|
154
|
+
this.writeJson(entry);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (value && typeof value === "object") {
|
|
159
|
+
const entries = Object.entries(value as Record<string, Value>).sort(
|
|
160
|
+
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
|
|
161
|
+
);
|
|
162
|
+
this.writeTag(TAG_JSON_OBJECT);
|
|
163
|
+
this.writeLen(entries.length);
|
|
164
|
+
for (const [name, entry] of entries) {
|
|
165
|
+
this.writeString(name);
|
|
166
|
+
this.writeJson(entry);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
throw new TypeError("Unsupported JSON value in digest");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
writeRawValue(value: RawValue): void {
|
|
174
|
+
this.writeTag(TAG_RAW_VALUE);
|
|
175
|
+
if (value.data === undefined) {
|
|
176
|
+
this.writeBool(false);
|
|
177
|
+
} else {
|
|
178
|
+
this.writeBool(true);
|
|
179
|
+
this.writeJson(value.data);
|
|
180
|
+
}
|
|
181
|
+
this.writeTag(TAG_RAW_METADATA);
|
|
182
|
+
if (!value.metadata || typeof value.metadata !== "object") {
|
|
183
|
+
this.writeBool(false);
|
|
184
|
+
} else {
|
|
185
|
+
const entries = Object.entries(value.metadata).sort(
|
|
186
|
+
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
|
|
187
|
+
);
|
|
188
|
+
this.writeBool(true);
|
|
189
|
+
this.writeLen(entries.length);
|
|
190
|
+
for (const [name, entry] of entries) {
|
|
191
|
+
this.writeString(name);
|
|
192
|
+
this.writeJson(entry as Value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.writeTag(TAG_RAW_CLOCK);
|
|
196
|
+
this.writeF64(value.clock.physicalTime);
|
|
197
|
+
this.writeI64(Math.trunc(value.clock.logicalCounter));
|
|
198
|
+
this.writeBytes(textEncoder.encode(value.clock.peerId));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async finish(): Promise<Uint8Array> {
|
|
202
|
+
const data = new Uint8Array(this.chunks);
|
|
203
|
+
return sha256(data);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function parseMetadata(json: string | null): MetadataMap | undefined {
|
|
208
|
+
if (!json) return undefined;
|
|
209
|
+
try {
|
|
210
|
+
const value = JSON.parse(json);
|
|
211
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
return value as MetadataMap;
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseData(json: string | null): Value | undefined {
|
|
221
|
+
if (json === null || json === undefined) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return JSON.parse(json) as Value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function digestEntry(row: DigestRow): Promise<Uint8Array | null> {
|
|
228
|
+
const data = parseData(row.data);
|
|
229
|
+
if (data === undefined) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const metadata = parseMetadata(row.metadata);
|
|
233
|
+
const writer = new DigestWriter();
|
|
234
|
+
writer.writeBytes(row.key);
|
|
235
|
+
writer.writeRawValue({
|
|
236
|
+
data,
|
|
237
|
+
metadata,
|
|
238
|
+
clock: {
|
|
239
|
+
physicalTime: Number(row.physical),
|
|
240
|
+
logicalCounter: Number(row.logical),
|
|
241
|
+
peerId: String(row.peer),
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return writer.finish();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function computeDigest(rows: DigestRow[]): Promise<string> {
|
|
248
|
+
const acc = new Uint8Array(32);
|
|
249
|
+
for (const row of rows) {
|
|
250
|
+
const digest = await digestEntry(row);
|
|
251
|
+
if (!digest) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
for (let i = 0; i < acc.length; i += 1) {
|
|
255
|
+
acc[i] ^= digest[i];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return toHex(acc);
|
|
259
|
+
}
|