@hraness/oh 0.4.1 → 0.4.2

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 (92) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +4 -1
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +15590 -1705
  6. package/dist/index.js +15209 -261
  7. package/dist/libsql-model.d.ts +75 -0
  8. package/dist/libsql-model.d.ts.map +1 -0
  9. package/dist/libsql-platform.d.ts +24 -0
  10. package/dist/libsql-platform.d.ts.map +1 -0
  11. package/dist/libsql-program.d.ts +64 -0
  12. package/dist/libsql-program.d.ts.map +1 -0
  13. package/dist/libsql-runtime.d.ts +13 -0
  14. package/dist/libsql-runtime.d.ts.map +1 -0
  15. package/dist/libsql.d.ts +4 -36
  16. package/dist/libsql.d.ts.map +1 -1
  17. package/dist/libsql.js +16583 -1426
  18. package/dist/memory-authority-platform.d.ts +210 -0
  19. package/dist/memory-authority-platform.d.ts.map +1 -0
  20. package/dist/memory-authority-program.d.ts +15 -0
  21. package/dist/memory-authority-program.d.ts.map +1 -0
  22. package/dist/memory-authority-runtime.d.ts +4 -0
  23. package/dist/memory-authority-runtime.d.ts.map +1 -0
  24. package/dist/memory-core.d.ts +522 -0
  25. package/dist/memory-core.d.ts.map +1 -0
  26. package/dist/memory.d.ts +5 -445
  27. package/dist/memory.d.ts.map +1 -1
  28. package/dist/memory.js +18232 -4143
  29. package/dist/sdk.js +15266 -318
  30. package/dist/semantic-cloud.js +1 -260
  31. package/dist/semantic-model.d.ts +75 -0
  32. package/dist/semantic-model.d.ts.map +1 -0
  33. package/dist/semantic-platform.d.ts +32 -0
  34. package/dist/semantic-platform.d.ts.map +1 -0
  35. package/dist/semantic-program.d.ts +18 -0
  36. package/dist/semantic-program.d.ts.map +1 -0
  37. package/dist/semantic-runtime.d.ts +6 -0
  38. package/dist/semantic-runtime.d.ts.map +1 -0
  39. package/dist/semantic.d.ts +6 -57
  40. package/dist/semantic.d.ts.map +1 -1
  41. package/dist/semantic.js +15541 -514
  42. package/dist/sqlite/index.js +1148 -1378
  43. package/dist/sqlite/port.d.ts +1 -1
  44. package/dist/sqlite/port.d.ts.map +1 -1
  45. package/dist/sync-libsql-program.d.ts +5 -0
  46. package/dist/sync-libsql-program.d.ts.map +1 -0
  47. package/dist/sync-model.d.ts +60 -0
  48. package/dist/sync-model.d.ts.map +1 -0
  49. package/dist/sync-platform.d.ts +41 -0
  50. package/dist/sync-platform.d.ts.map +1 -0
  51. package/dist/sync-program.d.ts +11 -0
  52. package/dist/sync-program.d.ts.map +1 -0
  53. package/dist/sync-runtime.d.ts +6 -0
  54. package/dist/sync-runtime.d.ts.map +1 -0
  55. package/dist/sync.d.ts +5 -55
  56. package/dist/sync.d.ts.map +1 -1
  57. package/dist/sync.js +15174 -226
  58. package/package.json +7 -5
  59. package/skills/oh/SKILL.md +2 -1
  60. package/spec/v1/store.md +14 -0
  61. package/src/cli.test.ts +35 -0
  62. package/src/cli.ts +4 -3
  63. package/src/cloudflare-embedding.ts +1 -1
  64. package/src/libsql-lifecycle.test.ts +76 -0
  65. package/src/libsql-model.ts +350 -0
  66. package/src/libsql-platform.ts +51 -0
  67. package/src/libsql-program.ts +1583 -0
  68. package/src/libsql-runtime.ts +89 -0
  69. package/src/libsql-semantic-v2.ts +1 -1
  70. package/src/libsql-semantic.ts +1 -1
  71. package/src/libsql.test.ts +245 -0
  72. package/src/libsql.ts +26 -1698
  73. package/src/memory-authority-platform.ts +133 -0
  74. package/src/memory-authority-program.ts +309 -0
  75. package/src/memory-authority-runtime.ts +40 -0
  76. package/src/memory-core.ts +2401 -0
  77. package/src/memory.test.ts +55 -0
  78. package/src/memory.ts +68 -2759
  79. package/src/semantic-model.ts +142 -0
  80. package/src/semantic-platform.ts +130 -0
  81. package/src/semantic-program.ts +113 -0
  82. package/src/semantic-runtime.ts +53 -0
  83. package/src/semantic.test.ts +115 -1
  84. package/src/semantic.ts +14 -324
  85. package/src/sqlite/port.ts +1 -1
  86. package/src/sync-libsql-program.ts +170 -0
  87. package/src/sync-lifecycle.test.ts +110 -0
  88. package/src/sync-model.ts +530 -0
  89. package/src/sync-platform.ts +87 -0
  90. package/src/sync-program.ts +117 -0
  91. package/src/sync-runtime.ts +33 -0
  92. package/src/sync.ts +10 -765
package/src/sync.ts CHANGED
@@ -1,777 +1,22 @@
1
- import { isProxy } from "node:util/types";
2
-
3
- import {
4
- canonicalJson,
5
- canonicalSha256,
6
- parseCanonicalInstantV1,
7
- parseSha256Hex,
8
- safeCode,
9
- utf8ByteLength,
10
- type Sha256Hex,
11
- } from "./canonical";
12
- import { OH_CONTRACT_MANIFEST_V1, parseOhContractManifestV1, type OhContractManifestV1 } from "./contract";
13
- import { OH_GRAPH_LIMITS_V1 } from "./graph";
14
- import { OH_CONTRACT_ID_V1 } from "./ontology";
15
- import { OH_OPERATION_MAX_BYTES_V1, parseOhOperationV1, type OhOperationV1 } from "./operation";
16
1
  import type { OhSqliteStore } from "./sqlite/store";
17
- import type { OhHeadRefV1 } from "./store";
18
-
19
- export const OH_SYNC_PROTOCOL_V1 = "oh.sync.v1" as const;
20
-
21
- export type OhSyncHeadV1 = Readonly<{
22
- operationSha256: Sha256Hex | null;
23
- sequence: number;
24
- v: 1;
25
- }>;
26
-
27
- const OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 = 1000;
28
- const OH_SYNC_BUNDLE_KEYS_V1 = [
29
- "bundleSha256", "contractSha256", "operations", "protocol", "spaceId", "v",
30
- ] as const;
31
- const OH_SYNC_HEAD_KEYS_V1 = ["operationSha256", "sequence", "v"] as const;
32
- const OH_SYNC_HEAD_REF_KEYS_V1 = ["operationSha256", "sequence"] as const;
33
- const OH_OPERATION_KEYS_V1 = [
34
- "actorId", "changes", "contractId", "graphRevisionSha256", "instant", "operationId",
35
- "operationSha256", "parentOperationSha256", "recordsSha256", "sequence", "spaceId", "v",
36
- ] as const;
37
- const OH_PUT_CHANGE_KEYS_V1 = ["kind", "record", "v"] as const;
38
- const OH_TOMBSTONE_CHANGE_KEYS_V1 = ["key", "kind", "priorSha256", "v"] as const;
39
- const OH_RECORD_KEYS_V1 = ["dependencies", "key", "kind", "recordSha256", "v", "value"] as const;
40
- const OH_SYNC_INGRESS_VALUE_DEPTH_V1 = 128;
41
- const OH_SYNC_INGRESS_VALUE_NODES_V1 = OH_GRAPH_LIMITS_V1.recordBytes;
42
- const OH_SYNC_INGRESS_OPERATION_DEPTH_V1 = OH_SYNC_INGRESS_VALUE_DEPTH_V1 + 4;
43
- const OH_SYNC_INGRESS_OPERATION_NODES_V1 = OH_OPERATION_MAX_BYTES_V1;
44
- export const OH_SYNC_BUNDLE_MAX_BYTES_V1 = OH_OPERATION_MAX_BYTES_V1 + 4 * 1024;
45
- const OH_SYNC_INGRESS_BUNDLE_NODES_V1 = OH_SYNC_INGRESS_OPERATION_NODES_V1 + 4 * 1024;
46
-
47
- type SyncIngressBudgetV1 = {
48
- bytes: number;
49
- maximumBytes: number;
50
- maximumNodes: number;
51
- nodes: number;
52
- };
53
-
54
- type SyncIngressTraversalLimitsV1 = Readonly<{
55
- maximumBytes: number;
56
- maximumDepth: number;
57
- maximumNodes: number;
58
- }>;
59
-
60
- function exactDataRecordV1(value: unknown, expectedKeys: readonly string[]): Record<string, unknown> | null {
61
- try {
62
- if (typeof value !== "object" || value === null || Array.isArray(value) || isProxy(value)) return null;
63
- const prototype = Object.getPrototypeOf(value);
64
- const keys = Reflect.ownKeys(value);
65
- if ((prototype !== Object.prototype && prototype !== null)
66
- || keys.length !== expectedKeys.length || keys.some((key) => typeof key !== "string")
67
- || expectedKeys.some((key) => !keys.includes(key))) return null;
68
- const detached = Object.create(null) as Record<string, unknown>;
69
- for (const key of expectedKeys) {
70
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
71
- if (descriptor === undefined || !descriptor.enumerable
72
- || descriptor.get !== undefined || descriptor.set !== undefined) return null;
73
- Object.defineProperty(detached, key, { configurable: false, enumerable: true,
74
- value: descriptor.value, writable: false });
75
- }
76
- return detached;
77
- } catch {
78
- return null;
79
- }
80
- }
81
-
82
- function exactDataArrayV1(
83
- value: unknown,
84
- maximumLength: number,
85
- clone = true,
86
- ): readonly unknown[] | null {
87
- try {
88
- if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value)) return null;
89
- const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
90
- const length = lengthDescriptor?.value;
91
- if (typeof length !== "number" || !Number.isSafeInteger(length)
92
- || length < 0 || length > maximumLength) return null;
93
- const keys = Reflect.ownKeys(value);
94
- if (keys.length !== length + 1 || !keys.includes("length")
95
- || keys.some((key) => key !== "length" && (typeof key !== "string"
96
- || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) return null;
97
- const detached: unknown[] | null = clone ? [] : null;
98
- for (let index = 0; index < length; index += 1) {
99
- const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
100
- if (descriptor === undefined || !descriptor.enumerable
101
- || descriptor.get !== undefined || descriptor.set !== undefined) return null;
102
- detached?.push(descriptor.value);
103
- }
104
- return detached ?? value as readonly unknown[];
105
- } catch {
106
- return null;
107
- }
108
- }
109
-
110
- function stagedSyncOperationV1(
111
- value: unknown,
112
- bundleBudget: SyncIngressBudgetV1,
113
- ): Record<string, unknown> | null {
114
- const operation = exactDataRecordV1(value, OH_OPERATION_KEYS_V1);
115
- if (operation === null || operation.v !== 1 || operation.contractId !== OH_CONTRACT_ID_V1
116
- || safeCode(operation.actorId) === null || safeCode(operation.operationId) === null
117
- || safeCode(operation.spaceId) === null || parseCanonicalInstantV1(operation.instant) === null
118
- || parseSha256Hex(operation.operationSha256) === null
119
- || parseSha256Hex(operation.graphRevisionSha256) === null
120
- || parseSha256Hex(operation.recordsSha256) === null) return null;
121
- const parentOperationSha256 = operation.parentOperationSha256 === null
122
- ? null : parseSha256Hex(operation.parentOperationSha256);
123
- const sequence = Number.isSafeInteger(operation.sequence) && (operation.sequence as number) > 0
124
- ? operation.sequence as number : null;
125
- if (sequence === null || (operation.parentOperationSha256 !== null && parentOperationSha256 === null)
126
- || ((sequence === 1) !== (parentOperationSha256 === null))) return null;
127
- const changes = exactDataArrayV1(
128
- operation.changes, OH_GRAPH_LIMITS_V1.changesPerOperation, false);
129
- if (changes === null || changes.length === 0) return null;
130
- const aggregateDataBudget: SyncIngressBudgetV1 = {
131
- bytes: 0,
132
- maximumBytes: OH_OPERATION_MAX_BYTES_V1,
133
- maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
134
- nodes: 0,
135
- };
136
- for (const change of changes) {
137
- const put = exactDataRecordV1(change, OH_PUT_CHANGE_KEYS_V1);
138
- if (put !== null && put.kind === "put" && put.v === 1) {
139
- const record = exactDataRecordV1(put.record, OH_RECORD_KEYS_V1);
140
- if (record === null) return null;
141
- const valuePreflight = preflightSyncIngressValueV1(record.value, aggregateDataBudget);
142
- const dependenciesPreflight = preflightSyncIngressDependenciesV1(
143
- record.dependencies, aggregateDataBudget);
144
- if (valuePreflight === null || dependenciesPreflight === null) return null;
145
- continue;
146
- }
147
- const tombstone = exactDataRecordV1(change, OH_TOMBSTONE_CHANGE_KEYS_V1);
148
- if (tombstone === null || tombstone.kind !== "tombstone" || tombstone.v !== 1) return null;
149
- }
150
- const detached = boundedSyncIngressV1(operation, {
151
- maximumBytes: OH_OPERATION_MAX_BYTES_V1,
152
- maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
153
- maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
154
- }, bundleBudget, true);
155
- return detached === null ? null : detached.value as Record<string, unknown>;
156
- }
2
+ import type { LibSqlClientV1, OhOperationSyncTransportV1, OhSyncResultV1 } from "./sync-model";
3
+ import { makeLibSqlSyncBoundary, runSyncBoundary } from "./sync-runtime";
157
4
 
158
- function canonicalStringBytesV1(value: string, maximumBytes: number): number {
159
- if (value.length + 2 > maximumBytes) throw new RangeError("String exceeds its canonical byte budget.");
160
- let bytes = 2;
161
- for (let index = 0; index < value.length; index += 1) {
162
- const code = value.charCodeAt(index);
163
- if (code >= 0xd800 && code <= 0xdbff) {
164
- const next = value.charCodeAt(index + 1);
165
- if (!(next >= 0xdc00 && next <= 0xdfff)) throw new TypeError("Invalid Unicode string.");
166
- bytes += 4;
167
- index += 1;
168
- } else if (code >= 0xdc00 && code <= 0xdfff) {
169
- throw new TypeError("Invalid Unicode string.");
170
- } else if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09
171
- || code === 0x0a || code === 0x0c || code === 0x0d) {
172
- bytes += 2;
173
- } else if (code <= 0x1f) {
174
- bytes += 6;
175
- } else if (code <= 0x7f) {
176
- bytes += 1;
177
- } else if (code <= 0x7ff) {
178
- bytes += 2;
179
- } else {
180
- bytes += 3;
181
- }
182
- if (bytes > maximumBytes) throw new RangeError("String exceeds its canonical byte budget.");
183
- }
184
- return bytes;
185
- }
186
-
187
- function boundedSyncIngressV1(
188
- value: unknown,
189
- limits: SyncIngressTraversalLimitsV1,
190
- aggregate: SyncIngressBudgetV1 | null,
191
- clone: boolean,
192
- ): Readonly<{ value: unknown }> | null {
193
- const ancestors = new Set<object>();
194
- const budget: SyncIngressBudgetV1 = {
195
- bytes: 0,
196
- maximumBytes: limits.maximumBytes,
197
- maximumNodes: limits.maximumNodes,
198
- nodes: 0,
199
- };
200
- const canSpendBytes = (count: number): boolean => budget.bytes + count <= budget.maximumBytes
201
- && (aggregate === null || aggregate.bytes + count <= aggregate.maximumBytes);
202
- const spendBytes = (count: number): void => {
203
- if (!canSpendBytes(count)) throw new RangeError("Sync ingress exceeds its canonical byte budget.");
204
- budget.bytes += count;
205
- if (aggregate !== null) aggregate.bytes += count;
206
- };
207
- const canSpendNodes = (count: number): boolean => budget.nodes + count <= budget.maximumNodes
208
- && (aggregate === null || aggregate.nodes + count <= aggregate.maximumNodes);
209
- const spendNode = (): void => {
210
- if (!canSpendNodes(1)) throw new RangeError("Sync ingress exceeds its node budget.");
211
- budget.nodes += 1;
212
- if (aggregate !== null) aggregate.nodes += 1;
213
- };
214
- const detach = (candidate: unknown, depth: number): unknown => {
215
- if (depth > limits.maximumDepth) throw new RangeError("Sync ingress exceeds its depth budget.");
216
- spendNode();
217
- if (candidate === null) {
218
- spendBytes(4);
219
- return candidate;
220
- }
221
- if (typeof candidate === "boolean") {
222
- spendBytes(candidate ? 4 : 5);
223
- return candidate;
224
- }
225
- if (typeof candidate === "string") {
226
- spendBytes(canonicalStringBytesV1(candidate, Math.min(
227
- budget.maximumBytes - budget.bytes,
228
- aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes,
229
- )));
230
- return candidate;
231
- }
232
- if (typeof candidate === "number") {
233
- if (!Number.isFinite(candidate) || Object.is(candidate, -0)) throw new TypeError("Invalid number.");
234
- spendBytes(utf8ByteLength(canonicalJson(candidate)));
235
- return candidate;
236
- }
237
- if (typeof candidate !== "object" || isProxy(candidate)) throw new TypeError("Invalid data value.");
238
- if (ancestors.has(candidate)) throw new TypeError("Cyclic data value.");
239
- ancestors.add(candidate);
240
- try {
241
- if (Array.isArray(candidate)) {
242
- const lengthDescriptor = Object.getOwnPropertyDescriptor(candidate, "length");
243
- const length = lengthDescriptor?.value;
244
- if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0
245
- || !canSpendNodes(length)
246
- || !canSpendBytes(2 + Math.max(0, length - 1) + length)) {
247
- throw new TypeError("Invalid or over-budget data array.");
248
- }
249
- const keys = Reflect.ownKeys(candidate);
250
- if (keys.length !== length + 1 || !keys.includes("length")
251
- || keys.some((key) => key !== "length" && (typeof key !== "string"
252
- || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
253
- throw new TypeError("Invalid data array.");
254
- }
255
- spendBytes(2 + Math.max(0, length - 1));
256
- const detached: unknown[] | null = clone ? [] : null;
257
- for (let index = 0; index < length; index += 1) {
258
- const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
259
- if (descriptor === undefined || !descriptor.enumerable
260
- || descriptor.get !== undefined || descriptor.set !== undefined) {
261
- throw new TypeError("Invalid data array entry.");
262
- }
263
- const item = detach(descriptor.value, depth + 1);
264
- detached?.push(item);
265
- }
266
- return detached === null ? candidate : Object.freeze(detached);
267
- }
268
- const prototype = Object.getPrototypeOf(candidate);
269
- if (prototype !== Object.prototype && prototype !== null) {
270
- throw new TypeError("Invalid data object.");
271
- }
272
- let containerBytes = 2;
273
- let properties = 0;
274
- for (const key in candidate) {
275
- if (!Object.hasOwn(candidate, key)) continue;
276
- properties += 1;
277
- if (!canSpendNodes(properties)) throw new RangeError("Sync ingress exceeds its node budget.");
278
- const remainingBytes = Math.min(
279
- budget.maximumBytes - budget.bytes - containerBytes - properties,
280
- aggregate === null ? Number.MAX_SAFE_INTEGER
281
- : aggregate.maximumBytes - aggregate.bytes - containerBytes - properties,
282
- );
283
- containerBytes += (properties === 1 ? 0 : 1)
284
- + canonicalStringBytesV1(key, remainingBytes) + 1;
285
- if (!canSpendBytes(containerBytes + properties)) {
286
- throw new RangeError("Sync ingress exceeds its canonical byte budget.");
287
- }
288
- }
289
- const keys = Reflect.ownKeys(candidate);
290
- if (keys.length !== properties || keys.some((key) => typeof key !== "string")) {
291
- throw new TypeError("Invalid data object.");
292
- }
293
- spendBytes(containerBytes);
294
- const detached: Record<string, unknown> | null = clone
295
- ? Object.create(null) as Record<string, unknown>
296
- : null;
297
- for (const key of keys as string[]) {
298
- const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
299
- if (descriptor === undefined || !descriptor.enumerable
300
- || descriptor.get !== undefined || descriptor.set !== undefined) {
301
- throw new TypeError("Invalid data property.");
302
- }
303
- const item = detach(descriptor.value, depth + 1);
304
- if (detached !== null) {
305
- Object.defineProperty(detached, key, { configurable: false, enumerable: true,
306
- value: item, writable: false });
307
- }
308
- }
309
- return detached === null ? candidate : Object.freeze(detached);
310
- } finally {
311
- ancestors.delete(candidate);
312
- }
313
- };
314
- try {
315
- return Object.freeze({ value: detach(value, 0) });
316
- } catch {
317
- return null;
318
- }
319
- }
320
-
321
- function preflightSyncIngressValueV1(
322
- value: unknown,
323
- aggregate: SyncIngressBudgetV1,
324
- ): Readonly<{ value: unknown }> | null {
325
- return boundedSyncIngressV1(value, {
326
- maximumBytes: OH_GRAPH_LIMITS_V1.recordBytes,
327
- maximumDepth: OH_SYNC_INGRESS_VALUE_DEPTH_V1,
328
- maximumNodes: OH_SYNC_INGRESS_VALUE_NODES_V1,
329
- }, aggregate, false);
330
- }
331
-
332
- function preflightSyncIngressDependenciesV1(
333
- value: unknown,
334
- aggregate: SyncIngressBudgetV1,
335
- ): Readonly<{ value: unknown }> | null {
336
- try {
337
- if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value)) {
338
- return null;
339
- }
340
- const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
341
- if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0
342
- || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord) return null;
343
- // Every valid dependency is an ASCII record key of at most 512 characters.
344
- const maximumBytes = length === 0 ? 2 : 1 + length * 515;
345
- return boundedSyncIngressV1(value, {
346
- maximumBytes,
347
- maximumDepth: 1,
348
- maximumNodes: length + 1,
349
- }, aggregate, false);
350
- } catch {
351
- return null;
352
- }
353
- }
354
-
355
- function syncIngressBundleBudgetV1(spaceId: string): SyncIngressBudgetV1 {
356
- const emptyBundle = {
357
- bundleSha256: "0".repeat(64),
358
- contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
359
- operations: [],
360
- protocol: OH_SYNC_PROTOCOL_V1,
361
- spaceId,
362
- v: 1,
363
- };
364
- return {
365
- bytes: utf8ByteLength(canonicalJson(emptyBundle)),
366
- maximumBytes: OH_SYNC_BUNDLE_MAX_BYTES_V1,
367
- maximumNodes: OH_SYNC_INGRESS_BUNDLE_NODES_V1,
368
- // Root, five scalar fields, and the operations array.
369
- nodes: 7,
370
- };
371
- }
372
-
373
- function spendSyncIngressBudgetV1(
374
- budget: SyncIngressBudgetV1,
375
- bytes: number,
376
- nodes = 0,
377
- ): boolean {
378
- if (budget.bytes + bytes > budget.maximumBytes
379
- || budget.nodes + nodes > budget.maximumNodes) return false;
380
- budget.bytes += bytes;
381
- budget.nodes += nodes;
382
- return true;
383
- }
5
+ export { OH_SYNC_PROTOCOL_V1, OH_SYNC_BUNDLE_MAX_BYTES_V1, createOhSyncBundleV1,
6
+ parseOhSyncHeadRefV1, parseOhSyncHeadV1, parseOhSyncBundleV1 } from "./sync-model";
7
+ export type { OhSyncHeadV1, OhSyncBundleV1, OhOperationSyncTransportV1, OhSyncResultV1,
8
+ LibSqlValueV1, LibSqlStatementV1, LibSqlResultV1, LibSqlClientV1 } from "./sync-model";
384
9
 
385
- function measureSyncOperationV1(operation: OhOperationV1): SyncIngressBudgetV1 | null {
386
- const measurement: SyncIngressBudgetV1 = {
387
- bytes: 0,
388
- maximumBytes: OH_OPERATION_MAX_BYTES_V1,
389
- maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
390
- nodes: 0,
391
- };
392
- return boundedSyncIngressV1(operation, {
393
- maximumBytes: OH_OPERATION_MAX_BYTES_V1,
394
- maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
395
- maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
396
- }, measurement, false) === null ? null : measurement;
397
- }
398
-
399
- function buildOhSyncBundleV1(
400
- parsedSpaceId: string,
401
- operations: readonly OhOperationV1[],
402
- largestFittingPrefix: boolean,
403
- ): OhSyncBundleV1 {
404
- let priorSequence: number | null = null;
405
- let priorSha256: Sha256Hex | null = null;
406
- const parsed: OhOperationV1[] = [];
407
- const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
408
- for (const candidate of operations) {
409
- const operation = parseOhOperationV1(candidate);
410
- if (operation === null || operation.spaceId !== parsedSpaceId
411
- || (priorSequence !== null && operation.sequence !== priorSequence + 1)
412
- || (priorSequence !== null && operation.parentOperationSha256 !== priorSha256)) {
413
- throw new TypeError("Sync operations must form one ordered chain.");
414
- }
415
- const measurement = measureSyncOperationV1(operation);
416
- if (measurement === null) {
417
- throw new RangeError("Sync operation exceeds its canonical byte, node, or depth limit.");
418
- }
419
- if (!spendSyncIngressBudgetV1(bundleBudget,
420
- measurement.bytes + (parsed.length === 0 ? 0 : 1), measurement.nodes)) {
421
- if (largestFittingPrefix && parsed.length > 0) break;
422
- throw new RangeError("Sync bundle exceeds its canonical byte or node limit.");
423
- }
424
- parsed.push(operation);
425
- priorSequence = operation.sequence;
426
- priorSha256 = operation.operationSha256;
427
- }
428
- const payload = { contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
429
- operations: parsed, protocol: OH_SYNC_PROTOCOL_V1, spaceId: parsedSpaceId, v: 1 as const };
430
- return { ...payload, bundleSha256: canonicalSha256(payload) };
431
- }
432
-
433
- function parseSyncHeadRefEnvelopeV1(value: Record<string, unknown>): OhHeadRefV1 | null {
434
- const operationSha256 = value.operationSha256 === null
435
- ? null
436
- : parseSha256Hex(value.operationSha256);
437
- const sequence = Number.isSafeInteger(value.sequence) && (value.sequence as number) >= 0
438
- && !Object.is(value.sequence, -0)
439
- ? value.sequence as number
440
- : null;
441
- return sequence !== null
442
- && (value.operationSha256 === null || operationSha256 !== null)
443
- && ((sequence === 0) === (operationSha256 === null))
444
- ? { operationSha256, sequence }
445
- : null;
446
- }
447
-
448
- export function parseOhSyncHeadRefV1(value: unknown): OhHeadRefV1 | null {
449
- const reference = exactDataRecordV1(value, OH_SYNC_HEAD_REF_KEYS_V1);
450
- return reference === null ? null : parseSyncHeadRefEnvelopeV1(reference);
451
- }
452
-
453
- export function parseOhSyncHeadV1(value: unknown): OhSyncHeadV1 | null {
454
- const envelope = exactDataRecordV1(value, OH_SYNC_HEAD_KEYS_V1);
455
- if (envelope === null || envelope.v !== 1) return null;
456
- const reference = parseSyncHeadRefEnvelopeV1(envelope);
457
- return reference === null ? null : { ...reference, v: 1 };
458
- }
459
-
460
- export type OhSyncBundleV1 = Readonly<{
461
- bundleSha256: Sha256Hex;
462
- contractSha256: Sha256Hex;
463
- operations: readonly OhOperationV1[];
464
- protocol: typeof OH_SYNC_PROTOCOL_V1;
465
- spaceId: string;
466
- v: 1;
467
- }>;
468
-
469
- export function createOhSyncBundleV1(
470
- spaceId: string,
471
- operations: readonly OhOperationV1[],
472
- options: Readonly<{ largestFittingPrefix?: boolean }> = {},
473
- ): OhSyncBundleV1 {
474
- const parsedSpaceId = safeCode(spaceId);
475
- const largestFittingPrefix = options.largestFittingPrefix ?? false;
476
- if (parsedSpaceId === null || operations.length > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1
477
- || typeof largestFittingPrefix !== "boolean") {
478
- throw new TypeError("Invalid sync bundle.");
479
- }
480
- return buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix);
481
- }
482
-
483
- export function parseOhSyncBundleV1(value: unknown): OhSyncBundleV1 | null {
484
- const envelope = exactDataRecordV1(value, OH_SYNC_BUNDLE_KEYS_V1);
485
- if (envelope === null || envelope.protocol !== OH_SYNC_PROTOCOL_V1 || envelope.v !== 1) return null;
486
- const bundleSha256 = parseSha256Hex(envelope.bundleSha256);
487
- const contractSha256 = parseSha256Hex(envelope.contractSha256);
488
- const spaceId = safeCode(envelope.spaceId);
489
- if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256
490
- || spaceId === null) return null;
491
- const operations = exactDataArrayV1(envelope.operations, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1);
492
- if (operations === null) return null;
493
- const detachedOperations: unknown[] = [];
494
- const bundleBudget = syncIngressBundleBudgetV1(spaceId);
495
- for (const operation of operations) {
496
- if (detachedOperations.length > 0 && !spendSyncIngressBudgetV1(bundleBudget, 1)) return null;
497
- const detached = stagedSyncOperationV1(operation, bundleBudget);
498
- if (detached === null) return null;
499
- detachedOperations.push(detached);
500
- }
501
- try {
502
- const created = createOhSyncBundleV1(spaceId, detachedOperations as OhOperationV1[]);
503
- if (detachedOperations.some((operation, index) =>
504
- canonicalJson(operation) !== canonicalJson(created.operations[index]))) return null;
505
- return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
506
- } catch { return null; }
507
- }
508
-
509
- export interface OhOperationSyncTransportV1 {
510
- handshake(manifest: OhContractManifestV1): Promise<void>;
511
- head(spaceId: string): Promise<OhSyncHeadV1>;
512
- pull(spaceId: string, afterSequence: number, limit: number): Promise<OhSyncBundleV1>;
513
- push(bundle: OhSyncBundleV1): Promise<OhSyncHeadV1>;
514
- }
515
-
516
- export type OhSyncResultV1 = Readonly<{
517
- head: OhSyncHeadV1;
518
- pulled: number;
519
- pushed: number;
520
- rounds: number;
521
- v: 1;
522
- }>;
523
-
524
- /**
525
- * Reconciles only fast-forward histories. Concurrent heads fail closed and leave
526
- * both logs intact for an explicit merge operation.
527
- */
10
+ /** Reconciles only fast-forward histories and preserves the original Promise boundary. */
528
11
  export async function synchronizeOhStoreV1(
529
12
  store: OhSqliteStore,
530
13
  transport: OhOperationSyncTransportV1,
531
14
  options: Readonly<{ batchSize?: number; maximumRounds?: number; remoteId?: string }> = {},
532
15
  ): Promise<OhSyncResultV1> {
533
- const batchSize = options.batchSize ?? 100;
534
- const maximumRounds = options.maximumRounds ?? 100;
535
- const remoteId = safeCode(options.remoteId ?? "default");
536
- if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1000
537
- || !Number.isSafeInteger(maximumRounds) || maximumRounds < 1 || maximumRounds > 10_000
538
- || remoteId === null) throw new TypeError("Invalid sync options.");
539
- await transport.handshake(OH_CONTRACT_MANIFEST_V1);
540
- let pulled = 0;
541
- let pushed = 0;
542
- const settled = (head: OhSyncHeadV1, rounds: number): OhSyncResultV1 => {
543
- store.updateSyncState(remoteId, { pulledSequence: head.sequence,
544
- pushedSequence: head.sequence, remoteHeadSha256: head.operationSha256 });
545
- return { head, pulled, pushed, rounds, v: 1 };
546
- };
547
- for (let round = 1; round <= maximumRounds; round += 1) {
548
- const remote = parseOhSyncHeadV1(await transport.head(store.spaceId));
549
- if (remote === null) throw new Error("The sync transport returned an invalid head.");
550
- const local = store.head();
551
- if (local.sequence === remote.sequence) {
552
- if (local.operationSha256 !== remote.operationSha256) {
553
- throw new Error("Sync conflict: equal sequence numbers have different heads.");
554
- }
555
- return settled(remote, round);
556
- }
557
- if (local.sequence < remote.sequence) {
558
- const bundle = parseOhSyncBundleV1(await transport.pull(store.spaceId, local.sequence, batchSize));
559
- const terminal = bundle?.operations.at(-1);
560
- if (bundle === null || bundle.operations.length === 0
561
- || bundle.operations.length > batchSize
562
- || bundle.spaceId !== store.spaceId
563
- || bundle.operations[0]?.sequence !== local.sequence + 1
564
- || bundle.operations[0]?.parentOperationSha256 !== local.operationSha256
565
- || terminal === undefined || terminal.sequence > remote.sequence
566
- || (terminal.sequence === remote.sequence
567
- && terminal.operationSha256 !== remote.operationSha256)) {
568
- throw new Error("Sync conflict: remote history does not extend the local head.");
569
- }
570
- store.importOperations({
571
- expectedHead: {
572
- operationSha256: local.operationSha256,
573
- sequence: local.sequence,
574
- },
575
- operations: bundle.operations,
576
- });
577
- pulled += bundle.operations.length;
578
- const afterPull = store.head();
579
- if (afterPull.sequence === remote.sequence
580
- && afterPull.operationSha256 === remote.operationSha256) {
581
- const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
582
- if (confirmed === null) throw new Error("The sync transport returned an invalid head.");
583
- const confirmedLocal = store.head();
584
- if (confirmed.sequence === remote.sequence
585
- && confirmed.operationSha256 === remote.operationSha256
586
- && confirmedLocal.sequence === confirmed.sequence
587
- && confirmedLocal.operationSha256 === confirmed.operationSha256) {
588
- return settled(confirmed, round);
589
- }
590
- }
591
- } else {
592
- const candidates = store.changesSince({
593
- operationSha256: remote.operationSha256,
594
- sequence: remote.sequence,
595
- }, {
596
- limit: batchSize,
597
- through: {
598
- operationSha256: local.operationSha256,
599
- sequence: local.sequence,
600
- },
601
- }).operations;
602
- if (candidates.length === 0 || candidates[0]?.sequence !== remote.sequence + 1
603
- || candidates[0]?.parentOperationSha256 !== remote.operationSha256) {
604
- throw new Error("Sync conflict: local history does not extend the remote head.");
605
- }
606
- const bundle = createOhSyncBundleV1(store.spaceId, candidates, {
607
- largestFittingPrefix: true,
608
- });
609
- const operations = bundle.operations;
610
- const head = parseOhSyncHeadV1(await transport.push(bundle));
611
- if (head === null) throw new Error("The sync transport returned an invalid push head.");
612
- if (head.sequence !== operations.at(-1)?.sequence
613
- || head.operationSha256 !== operations.at(-1)?.operationSha256) {
614
- throw new Error("The sync transport acknowledged a different head.");
615
- }
616
- pushed += operations.length;
617
- const afterPush = store.head();
618
- if (afterPush.sequence === head.sequence
619
- && afterPush.operationSha256 === head.operationSha256) {
620
- const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
621
- if (confirmed === null) throw new Error("The sync transport returned an invalid head.");
622
- const confirmedLocal = store.head();
623
- if (confirmed.sequence === head.sequence
624
- && confirmed.operationSha256 === head.operationSha256
625
- && confirmedLocal.sequence === confirmed.sequence
626
- && confirmedLocal.operationSha256 === confirmed.operationSha256) {
627
- return settled(confirmed, round);
628
- }
629
- }
630
- }
631
- }
632
- throw new Error("Sync did not settle within maximumRounds.");
633
- }
634
-
635
- export type LibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string;
636
- export type LibSqlStatementV1 = { args?: LibSqlValueV1[]; sql: string };
637
- export type LibSqlResultV1 = Readonly<{ rows: readonly (Readonly<Record<string, unknown>> | readonly unknown[])[] }>;
638
- export interface LibSqlClientV1 {
639
- execute(statement: LibSqlStatementV1 | string): Promise<LibSqlResultV1>;
640
- batch(statements: LibSqlStatementV1[], mode?: "deferred" | "read" | "write"): Promise<readonly LibSqlResultV1[]>;
641
- }
642
-
643
- function rowValue(row: Readonly<Record<string, unknown>> | readonly unknown[], key: string, index: number): unknown {
644
- return Array.isArray(row) ? row[index] : (row as Readonly<Record<string, unknown>>)[key];
16
+ return await runSyncBoundary(store, transport, options);
645
17
  }
646
18
 
647
- /** A zero-dependency adapter for clients implementing @libsql/client's execute/batch shape. */
19
+ /** Bundled-runtime adapter for clients implementing @libsql/client's execute/batch shape. */
648
20
  export function createLibSqlOperationSyncTransportV1(client: LibSqlClientV1): OhOperationSyncTransportV1 {
649
- let ready: Promise<void> | null = null;
650
- const setup = async (manifest: OhContractManifestV1): Promise<void> => {
651
- if (parseOhContractManifestV1(manifest) === null) throw new Error("Unsupported contract manifest.");
652
- await client.batch([
653
- { sql: `CREATE TABLE IF NOT EXISTS oh_sync_contracts (
654
- contract_id TEXT PRIMARY KEY, contract_sha256 TEXT NOT NULL, manifest_json TEXT NOT NULL
655
- ) STRICT` },
656
- { sql: `CREATE TABLE IF NOT EXISTS oh_sync_operations (
657
- space_id TEXT NOT NULL, sequence INTEGER NOT NULL, operation_sha256 TEXT NOT NULL UNIQUE,
658
- operation_json TEXT NOT NULL, PRIMARY KEY(space_id, sequence)
659
- ) STRICT` },
660
- { sql: "INSERT INTO oh_sync_contracts(contract_id, contract_sha256, manifest_json) VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING",
661
- args: [manifest.contractId, manifest.contractSha256, canonicalJson(manifest)] },
662
- ], "write");
663
- const result = await client.execute({ sql: "SELECT contract_sha256, manifest_json FROM oh_sync_contracts WHERE contract_id = ?",
664
- args: [manifest.contractId] });
665
- const row = result.rows[0];
666
- if (row === undefined || rowValue(row, "contract_sha256", 0) !== manifest.contractSha256
667
- || rowValue(row, "manifest_json", 1) !== canonicalJson(manifest)) {
668
- throw new Error("Remote contract manifest mismatch.");
669
- }
670
- };
671
- const ensure = (manifest = OH_CONTRACT_MANIFEST_V1): Promise<void> => {
672
- ready ??= setup(manifest).catch((error) => { ready = null; throw error; });
673
- return ready;
674
- };
675
- const head = async (spaceId: string): Promise<OhSyncHeadV1> => {
676
- await ensure();
677
- const result = await client.execute({ sql: `SELECT sequence, operation_sha256 FROM oh_sync_operations
678
- WHERE space_id = ? ORDER BY sequence DESC LIMIT 1`, args: [spaceId] });
679
- const row = result.rows[0];
680
- if (row === undefined) return { operationSha256: null, sequence: 0, v: 1 };
681
- const sequence = Number(rowValue(row, "sequence", 0));
682
- const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
683
- if (!Number.isSafeInteger(sequence) || sequence < 1 || operationSha256 === null) throw new Error("Invalid remote head.");
684
- return { operationSha256, sequence, v: 1 };
685
- };
686
- return {
687
- handshake: async (manifest) => {
688
- const parsed = parseOhContractManifestV1(manifest);
689
- if (parsed === null) throw new Error("Unsupported contract manifest.");
690
- await ensure(parsed);
691
- },
692
- head,
693
- pull: async (spaceId, afterSequence, limit) => {
694
- const parsedSpaceId = safeCode(spaceId);
695
- if (parsedSpaceId === null
696
- || !Number.isSafeInteger(afterSequence) || afterSequence < 0 || Object.is(afterSequence, -0)
697
- || !Number.isSafeInteger(limit) || limit < 1 || limit > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1) {
698
- throw new TypeError("Invalid sync pull request.");
699
- }
700
- await ensure();
701
- const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
702
- const result = await client.execute({ sql: `SELECT operation_json FROM (
703
- SELECT sequence, operation_json,
704
- row_number() OVER (ORDER BY sequence) AS ordinal,
705
- sum(length(CAST(operation_json AS BLOB))) OVER (
706
- ORDER BY sequence ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
707
- ) AS cumulative_bytes
708
- FROM (
709
- SELECT sequence, operation_json FROM oh_sync_operations
710
- WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?
711
- )
712
- ) WHERE ordinal = 1 OR cumulative_bytes + ordinal - 1 <= ? ORDER BY sequence`,
713
- args: [parsedSpaceId, afterSequence, limit,
714
- bundleBudget.maximumBytes - bundleBudget.bytes] });
715
- const rows = result.rows;
716
- if (!Array.isArray(rows) || rows.length > limit) {
717
- throw new Error("Remote sync pull exceeded its requested row limit.");
718
- }
719
- const operations = rows.map((row) => {
720
- const json = rowValue(row, "operation_json", 0);
721
- if (typeof json !== "string") throw new Error("Invalid remote operation JSON.");
722
- if (utf8ByteLength(json) > OH_OPERATION_MAX_BYTES_V1) {
723
- throw new Error("Remote operation JSON exceeds the canonical operation byte limit.");
724
- }
725
- const operation = parseOhOperationV1(JSON.parse(json));
726
- if (operation === null || canonicalJson(operation) !== json) throw new Error("Invalid remote operation.");
727
- return operation;
728
- });
729
- return createOhSyncBundleV1(parsedSpaceId, operations);
730
- },
731
- push: async (value) => {
732
- await ensure();
733
- const bundle = parseOhSyncBundleV1(value);
734
- if (bundle === null) throw new Error("Invalid outgoing sync bundle.");
735
- if (bundle.operations.length === 0) return head(bundle.spaceId);
736
- const remote = await head(bundle.spaceId);
737
- const first = bundle.operations[0] as OhOperationV1;
738
- const last = bundle.operations.at(-1) as OhOperationV1;
739
- if (remote.sequence >= last.sequence) {
740
- const result = await client.execute({ sql: `SELECT sequence, operation_sha256, operation_json
741
- FROM oh_sync_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ?
742
- ORDER BY sequence LIMIT ?`, args: [bundle.spaceId, first.sequence, last.sequence,
743
- bundle.operations.length] });
744
- const rows = result.rows;
745
- if (!Array.isArray(rows) || rows.length !== bundle.operations.length) {
746
- throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
747
- }
748
- for (let index = 0; index < bundle.operations.length; index += 1) {
749
- const operation = bundle.operations[index] as OhOperationV1;
750
- const row = rows[index];
751
- if (row === undefined) {
752
- throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
753
- }
754
- const sequence = Number(rowValue(row, "sequence", 0));
755
- const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
756
- const operationJson = rowValue(row, "operation_json", 2);
757
- const expectedJson = canonicalJson(operation);
758
- if (sequence !== operation.sequence || operationSha256 !== operation.operationSha256
759
- || typeof operationJson !== "string"
760
- || utf8ByteLength(operationJson) > OH_OPERATION_MAX_BYTES_V1
761
- || operationJson !== expectedJson) {
762
- throw new Error("Sync conflict: remote history differs from the pushed operations.");
763
- }
764
- }
765
- return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
766
- }
767
- if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
768
- throw new Error("Sync conflict: pushed history does not extend the remote head.");
769
- }
770
- await client.batch(bundle.operations.map((operation) => ({
771
- sql: "INSERT INTO oh_sync_operations(space_id, sequence, operation_sha256, operation_json) VALUES (?, ?, ?, ?)",
772
- args: [bundle.spaceId, operation.sequence, operation.operationSha256, canonicalJson(operation)],
773
- })), "write");
774
- return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
775
- },
776
- };
21
+ return makeLibSqlSyncBoundary(client);
777
22
  }