@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
@@ -0,0 +1,530 @@
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
+ import type { OhHeadRefV1 } from "./store";
17
+
18
+ export const OH_SYNC_PROTOCOL_V1 = "oh.sync.v1" as const;
19
+
20
+ export type OhSyncHeadV1 = Readonly<{
21
+ operationSha256: Sha256Hex | null;
22
+ sequence: number;
23
+ v: 1;
24
+ }>;
25
+
26
+ export const OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 = 1000;
27
+ const OH_SYNC_BUNDLE_KEYS_V1 = [
28
+ "bundleSha256", "contractSha256", "operations", "protocol", "spaceId", "v",
29
+ ] as const;
30
+ const OH_SYNC_HEAD_KEYS_V1 = ["operationSha256", "sequence", "v"] as const;
31
+ const OH_SYNC_HEAD_REF_KEYS_V1 = ["operationSha256", "sequence"] as const;
32
+ const OH_OPERATION_KEYS_V1 = [
33
+ "actorId", "changes", "contractId", "graphRevisionSha256", "instant", "operationId",
34
+ "operationSha256", "parentOperationSha256", "recordsSha256", "sequence", "spaceId", "v",
35
+ ] as const;
36
+ const OH_PUT_CHANGE_KEYS_V1 = ["kind", "record", "v"] as const;
37
+ const OH_TOMBSTONE_CHANGE_KEYS_V1 = ["key", "kind", "priorSha256", "v"] as const;
38
+ const OH_RECORD_KEYS_V1 = ["dependencies", "key", "kind", "recordSha256", "v", "value"] as const;
39
+ const OH_SYNC_INGRESS_VALUE_DEPTH_V1 = 128;
40
+ const OH_SYNC_INGRESS_VALUE_NODES_V1 = OH_GRAPH_LIMITS_V1.recordBytes;
41
+ const OH_SYNC_INGRESS_OPERATION_DEPTH_V1 = OH_SYNC_INGRESS_VALUE_DEPTH_V1 + 4;
42
+ const OH_SYNC_INGRESS_OPERATION_NODES_V1 = OH_OPERATION_MAX_BYTES_V1;
43
+ export const OH_SYNC_BUNDLE_MAX_BYTES_V1 = OH_OPERATION_MAX_BYTES_V1 + 4 * 1024;
44
+ const OH_SYNC_INGRESS_BUNDLE_NODES_V1 = OH_SYNC_INGRESS_OPERATION_NODES_V1 + 4 * 1024;
45
+
46
+ type SyncIngressBudgetV1 = {
47
+ bytes: number;
48
+ maximumBytes: number;
49
+ maximumNodes: number;
50
+ nodes: number;
51
+ };
52
+
53
+ type SyncIngressTraversalLimitsV1 = Readonly<{
54
+ maximumBytes: number;
55
+ maximumDepth: number;
56
+ maximumNodes: number;
57
+ }>;
58
+
59
+ function exactDataRecordV1(value: unknown, expectedKeys: readonly string[]): Record<string, unknown> | null {
60
+ try {
61
+ if (typeof value !== "object" || value === null || Array.isArray(value) || isProxy(value)) return null;
62
+ const prototype = Object.getPrototypeOf(value);
63
+ const keys = Reflect.ownKeys(value);
64
+ if ((prototype !== Object.prototype && prototype !== null)
65
+ || keys.length !== expectedKeys.length || keys.some((key) => typeof key !== "string")
66
+ || expectedKeys.some((key) => !keys.includes(key))) return null;
67
+ const detached = Object.create(null) as Record<string, unknown>;
68
+ for (const key of expectedKeys) {
69
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
70
+ if (descriptor === undefined || !descriptor.enumerable
71
+ || descriptor.get !== undefined || descriptor.set !== undefined) return null;
72
+ Object.defineProperty(detached, key, { configurable: false, enumerable: true,
73
+ value: descriptor.value, writable: false });
74
+ }
75
+ return detached;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ function exactDataArrayV1(
82
+ value: unknown,
83
+ maximumLength: number,
84
+ clone = true,
85
+ ): readonly unknown[] | null {
86
+ try {
87
+ if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value)) return null;
88
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
89
+ const length = lengthDescriptor?.value;
90
+ if (typeof length !== "number" || !Number.isSafeInteger(length)
91
+ || length < 0 || length > maximumLength) return null;
92
+ const keys = Reflect.ownKeys(value);
93
+ if (keys.length !== length + 1 || !keys.includes("length")
94
+ || keys.some((key) => key !== "length" && (typeof key !== "string"
95
+ || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) return null;
96
+ const detached: unknown[] | null = clone ? [] : null;
97
+ for (let index = 0; index < length; index += 1) {
98
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
99
+ if (descriptor === undefined || !descriptor.enumerable
100
+ || descriptor.get !== undefined || descriptor.set !== undefined) return null;
101
+ detached?.push(descriptor.value);
102
+ }
103
+ return detached ?? value as readonly unknown[];
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function stagedSyncOperationV1(
110
+ value: unknown,
111
+ bundleBudget: SyncIngressBudgetV1,
112
+ ): Record<string, unknown> | null {
113
+ const operation = exactDataRecordV1(value, OH_OPERATION_KEYS_V1);
114
+ if (operation === null || operation.v !== 1 || operation.contractId !== OH_CONTRACT_ID_V1
115
+ || safeCode(operation.actorId) === null || safeCode(operation.operationId) === null
116
+ || safeCode(operation.spaceId) === null || parseCanonicalInstantV1(operation.instant) === null
117
+ || parseSha256Hex(operation.operationSha256) === null
118
+ || parseSha256Hex(operation.graphRevisionSha256) === null
119
+ || parseSha256Hex(operation.recordsSha256) === null) return null;
120
+ const parentOperationSha256 = operation.parentOperationSha256 === null
121
+ ? null : parseSha256Hex(operation.parentOperationSha256);
122
+ const sequence = Number.isSafeInteger(operation.sequence) && (operation.sequence as number) > 0
123
+ ? operation.sequence as number : null;
124
+ if (sequence === null || (operation.parentOperationSha256 !== null && parentOperationSha256 === null)
125
+ || ((sequence === 1) !== (parentOperationSha256 === null))) return null;
126
+ const changes = exactDataArrayV1(
127
+ operation.changes, OH_GRAPH_LIMITS_V1.changesPerOperation, false);
128
+ if (changes === null || changes.length === 0) return null;
129
+ const aggregateDataBudget: SyncIngressBudgetV1 = {
130
+ bytes: 0,
131
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
132
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
133
+ nodes: 0,
134
+ };
135
+ for (const change of changes) {
136
+ const put = exactDataRecordV1(change, OH_PUT_CHANGE_KEYS_V1);
137
+ if (put !== null && put.kind === "put" && put.v === 1) {
138
+ const record = exactDataRecordV1(put.record, OH_RECORD_KEYS_V1);
139
+ if (record === null) return null;
140
+ const valuePreflight = preflightSyncIngressValueV1(record.value, aggregateDataBudget);
141
+ const dependenciesPreflight = preflightSyncIngressDependenciesV1(
142
+ record.dependencies, aggregateDataBudget);
143
+ if (valuePreflight === null || dependenciesPreflight === null) return null;
144
+ continue;
145
+ }
146
+ const tombstone = exactDataRecordV1(change, OH_TOMBSTONE_CHANGE_KEYS_V1);
147
+ if (tombstone === null || tombstone.kind !== "tombstone" || tombstone.v !== 1) return null;
148
+ }
149
+ const detached = boundedSyncIngressV1(operation, {
150
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
151
+ maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
152
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
153
+ }, bundleBudget, true);
154
+ return detached === null ? null : detached.value as Record<string, unknown>;
155
+ }
156
+
157
+ function canonicalStringBytesV1(value: string, maximumBytes: number): number {
158
+ if (value.length + 2 > maximumBytes) throw new RangeError("String exceeds its canonical byte budget.");
159
+ let bytes = 2;
160
+ for (let index = 0; index < value.length; index += 1) {
161
+ const code = value.charCodeAt(index);
162
+ if (code >= 0xd800 && code <= 0xdbff) {
163
+ const next = value.charCodeAt(index + 1);
164
+ if (!(next >= 0xdc00 && next <= 0xdfff)) throw new TypeError("Invalid Unicode string.");
165
+ bytes += 4;
166
+ index += 1;
167
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
168
+ throw new TypeError("Invalid Unicode string.");
169
+ } else if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09
170
+ || code === 0x0a || code === 0x0c || code === 0x0d) {
171
+ bytes += 2;
172
+ } else if (code <= 0x1f) {
173
+ bytes += 6;
174
+ } else if (code <= 0x7f) {
175
+ bytes += 1;
176
+ } else if (code <= 0x7ff) {
177
+ bytes += 2;
178
+ } else {
179
+ bytes += 3;
180
+ }
181
+ if (bytes > maximumBytes) throw new RangeError("String exceeds its canonical byte budget.");
182
+ }
183
+ return bytes;
184
+ }
185
+
186
+ function boundedSyncIngressV1(
187
+ value: unknown,
188
+ limits: SyncIngressTraversalLimitsV1,
189
+ aggregate: SyncIngressBudgetV1 | null,
190
+ clone: boolean,
191
+ ): Readonly<{ value: unknown }> | null {
192
+ const ancestors = new Set<object>();
193
+ const budget: SyncIngressBudgetV1 = {
194
+ bytes: 0,
195
+ maximumBytes: limits.maximumBytes,
196
+ maximumNodes: limits.maximumNodes,
197
+ nodes: 0,
198
+ };
199
+ const canSpendBytes = (count: number): boolean => budget.bytes + count <= budget.maximumBytes
200
+ && (aggregate === null || aggregate.bytes + count <= aggregate.maximumBytes);
201
+ const spendBytes = (count: number): void => {
202
+ if (!canSpendBytes(count)) throw new RangeError("Sync ingress exceeds its canonical byte budget.");
203
+ budget.bytes += count;
204
+ if (aggregate !== null) aggregate.bytes += count;
205
+ };
206
+ const canSpendNodes = (count: number): boolean => budget.nodes + count <= budget.maximumNodes
207
+ && (aggregate === null || aggregate.nodes + count <= aggregate.maximumNodes);
208
+ const spendNode = (): void => {
209
+ if (!canSpendNodes(1)) throw new RangeError("Sync ingress exceeds its node budget.");
210
+ budget.nodes += 1;
211
+ if (aggregate !== null) aggregate.nodes += 1;
212
+ };
213
+ const detach = (candidate: unknown, depth: number): unknown => {
214
+ if (depth > limits.maximumDepth) throw new RangeError("Sync ingress exceeds its depth budget.");
215
+ spendNode();
216
+ if (candidate === null) {
217
+ spendBytes(4);
218
+ return candidate;
219
+ }
220
+ if (typeof candidate === "boolean") {
221
+ spendBytes(candidate ? 4 : 5);
222
+ return candidate;
223
+ }
224
+ if (typeof candidate === "string") {
225
+ spendBytes(canonicalStringBytesV1(candidate, Math.min(
226
+ budget.maximumBytes - budget.bytes,
227
+ aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes,
228
+ )));
229
+ return candidate;
230
+ }
231
+ if (typeof candidate === "number") {
232
+ if (!Number.isFinite(candidate) || Object.is(candidate, -0)) throw new TypeError("Invalid number.");
233
+ spendBytes(utf8ByteLength(canonicalJson(candidate)));
234
+ return candidate;
235
+ }
236
+ if (typeof candidate !== "object" || isProxy(candidate)) throw new TypeError("Invalid data value.");
237
+ if (ancestors.has(candidate)) throw new TypeError("Cyclic data value.");
238
+ ancestors.add(candidate);
239
+ try {
240
+ if (Array.isArray(candidate)) {
241
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(candidate, "length");
242
+ const length = lengthDescriptor?.value;
243
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0
244
+ || !canSpendNodes(length)
245
+ || !canSpendBytes(2 + Math.max(0, length - 1) + length)) {
246
+ throw new TypeError("Invalid or over-budget data array.");
247
+ }
248
+ const keys = Reflect.ownKeys(candidate);
249
+ if (keys.length !== length + 1 || !keys.includes("length")
250
+ || keys.some((key) => key !== "length" && (typeof key !== "string"
251
+ || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
252
+ throw new TypeError("Invalid data array.");
253
+ }
254
+ spendBytes(2 + Math.max(0, length - 1));
255
+ const detached: unknown[] | null = clone ? [] : null;
256
+ for (let index = 0; index < length; index += 1) {
257
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
258
+ if (descriptor === undefined || !descriptor.enumerable
259
+ || descriptor.get !== undefined || descriptor.set !== undefined) {
260
+ throw new TypeError("Invalid data array entry.");
261
+ }
262
+ const item = detach(descriptor.value, depth + 1);
263
+ detached?.push(item);
264
+ }
265
+ return detached === null ? candidate : Object.freeze(detached);
266
+ }
267
+ const prototype = Object.getPrototypeOf(candidate);
268
+ if (prototype !== Object.prototype && prototype !== null) {
269
+ throw new TypeError("Invalid data object.");
270
+ }
271
+ let containerBytes = 2;
272
+ let properties = 0;
273
+ for (const key in candidate) {
274
+ if (!Object.hasOwn(candidate, key)) continue;
275
+ properties += 1;
276
+ if (!canSpendNodes(properties)) throw new RangeError("Sync ingress exceeds its node budget.");
277
+ const remainingBytes = Math.min(
278
+ budget.maximumBytes - budget.bytes - containerBytes - properties,
279
+ aggregate === null ? Number.MAX_SAFE_INTEGER
280
+ : aggregate.maximumBytes - aggregate.bytes - containerBytes - properties,
281
+ );
282
+ containerBytes += (properties === 1 ? 0 : 1)
283
+ + canonicalStringBytesV1(key, remainingBytes) + 1;
284
+ if (!canSpendBytes(containerBytes + properties)) {
285
+ throw new RangeError("Sync ingress exceeds its canonical byte budget.");
286
+ }
287
+ }
288
+ const keys = Reflect.ownKeys(candidate);
289
+ if (keys.length !== properties || keys.some((key) => typeof key !== "string")) {
290
+ throw new TypeError("Invalid data object.");
291
+ }
292
+ spendBytes(containerBytes);
293
+ const detached: Record<string, unknown> | null = clone
294
+ ? Object.create(null) as Record<string, unknown>
295
+ : null;
296
+ for (const key of keys as string[]) {
297
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
298
+ if (descriptor === undefined || !descriptor.enumerable
299
+ || descriptor.get !== undefined || descriptor.set !== undefined) {
300
+ throw new TypeError("Invalid data property.");
301
+ }
302
+ const item = detach(descriptor.value, depth + 1);
303
+ if (detached !== null) {
304
+ Object.defineProperty(detached, key, { configurable: false, enumerable: true,
305
+ value: item, writable: false });
306
+ }
307
+ }
308
+ return detached === null ? candidate : Object.freeze(detached);
309
+ } finally {
310
+ ancestors.delete(candidate);
311
+ }
312
+ };
313
+ try {
314
+ return Object.freeze({ value: detach(value, 0) });
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+
320
+ function preflightSyncIngressValueV1(
321
+ value: unknown,
322
+ aggregate: SyncIngressBudgetV1,
323
+ ): Readonly<{ value: unknown }> | null {
324
+ return boundedSyncIngressV1(value, {
325
+ maximumBytes: OH_GRAPH_LIMITS_V1.recordBytes,
326
+ maximumDepth: OH_SYNC_INGRESS_VALUE_DEPTH_V1,
327
+ maximumNodes: OH_SYNC_INGRESS_VALUE_NODES_V1,
328
+ }, aggregate, false);
329
+ }
330
+
331
+ function preflightSyncIngressDependenciesV1(
332
+ value: unknown,
333
+ aggregate: SyncIngressBudgetV1,
334
+ ): Readonly<{ value: unknown }> | null {
335
+ try {
336
+ if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value)) {
337
+ return null;
338
+ }
339
+ const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
340
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0
341
+ || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord) return null;
342
+ // Every valid dependency is an ASCII record key of at most 512 characters.
343
+ const maximumBytes = length === 0 ? 2 : 1 + length * 515;
344
+ return boundedSyncIngressV1(value, {
345
+ maximumBytes,
346
+ maximumDepth: 1,
347
+ maximumNodes: length + 1,
348
+ }, aggregate, false);
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+
354
+ export function syncIngressBundleBudgetV1(spaceId: string): SyncIngressBudgetV1 {
355
+ const emptyBundle = {
356
+ bundleSha256: "0".repeat(64),
357
+ contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
358
+ operations: [],
359
+ protocol: OH_SYNC_PROTOCOL_V1,
360
+ spaceId,
361
+ v: 1,
362
+ };
363
+ return {
364
+ bytes: utf8ByteLength(canonicalJson(emptyBundle)),
365
+ maximumBytes: OH_SYNC_BUNDLE_MAX_BYTES_V1,
366
+ maximumNodes: OH_SYNC_INGRESS_BUNDLE_NODES_V1,
367
+ // Root, five scalar fields, and the operations array.
368
+ nodes: 7,
369
+ };
370
+ }
371
+
372
+ function spendSyncIngressBudgetV1(
373
+ budget: SyncIngressBudgetV1,
374
+ bytes: number,
375
+ nodes = 0,
376
+ ): boolean {
377
+ if (budget.bytes + bytes > budget.maximumBytes
378
+ || budget.nodes + nodes > budget.maximumNodes) return false;
379
+ budget.bytes += bytes;
380
+ budget.nodes += nodes;
381
+ return true;
382
+ }
383
+
384
+ function measureSyncOperationV1(operation: OhOperationV1): SyncIngressBudgetV1 | null {
385
+ const measurement: SyncIngressBudgetV1 = {
386
+ bytes: 0,
387
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
388
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
389
+ nodes: 0,
390
+ };
391
+ return boundedSyncIngressV1(operation, {
392
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
393
+ maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
394
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
395
+ }, measurement, false) === null ? null : measurement;
396
+ }
397
+
398
+ function buildOhSyncBundleV1(
399
+ parsedSpaceId: string,
400
+ operations: readonly OhOperationV1[],
401
+ largestFittingPrefix: boolean,
402
+ ): OhSyncBundleV1 {
403
+ let priorSequence: number | null = null;
404
+ let priorSha256: Sha256Hex | null = null;
405
+ const parsed: OhOperationV1[] = [];
406
+ const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
407
+ for (const candidate of operations) {
408
+ const operation = parseOhOperationV1(candidate);
409
+ if (operation === null || operation.spaceId !== parsedSpaceId
410
+ || (priorSequence !== null && operation.sequence !== priorSequence + 1)
411
+ || (priorSequence !== null && operation.parentOperationSha256 !== priorSha256)) {
412
+ throw new TypeError("Sync operations must form one ordered chain.");
413
+ }
414
+ const measurement = measureSyncOperationV1(operation);
415
+ if (measurement === null) {
416
+ throw new RangeError("Sync operation exceeds its canonical byte, node, or depth limit.");
417
+ }
418
+ if (!spendSyncIngressBudgetV1(bundleBudget,
419
+ measurement.bytes + (parsed.length === 0 ? 0 : 1), measurement.nodes)) {
420
+ if (largestFittingPrefix && parsed.length > 0) break;
421
+ throw new RangeError("Sync bundle exceeds its canonical byte or node limit.");
422
+ }
423
+ parsed.push(operation);
424
+ priorSequence = operation.sequence;
425
+ priorSha256 = operation.operationSha256;
426
+ }
427
+ const payload = { contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
428
+ operations: parsed, protocol: OH_SYNC_PROTOCOL_V1, spaceId: parsedSpaceId, v: 1 as const };
429
+ return { ...payload, bundleSha256: canonicalSha256(payload) };
430
+ }
431
+
432
+ function parseSyncHeadRefEnvelopeV1(value: Record<string, unknown>): OhHeadRefV1 | null {
433
+ const operationSha256 = value.operationSha256 === null
434
+ ? null
435
+ : parseSha256Hex(value.operationSha256);
436
+ const sequence = Number.isSafeInteger(value.sequence) && (value.sequence as number) >= 0
437
+ && !Object.is(value.sequence, -0)
438
+ ? value.sequence as number
439
+ : null;
440
+ return sequence !== null
441
+ && (value.operationSha256 === null || operationSha256 !== null)
442
+ && ((sequence === 0) === (operationSha256 === null))
443
+ ? { operationSha256, sequence }
444
+ : null;
445
+ }
446
+
447
+ export function parseOhSyncHeadRefV1(value: unknown): OhHeadRefV1 | null {
448
+ const reference = exactDataRecordV1(value, OH_SYNC_HEAD_REF_KEYS_V1);
449
+ return reference === null ? null : parseSyncHeadRefEnvelopeV1(reference);
450
+ }
451
+
452
+ export function parseOhSyncHeadV1(value: unknown): OhSyncHeadV1 | null {
453
+ const envelope = exactDataRecordV1(value, OH_SYNC_HEAD_KEYS_V1);
454
+ if (envelope === null || envelope.v !== 1) return null;
455
+ const reference = parseSyncHeadRefEnvelopeV1(envelope);
456
+ return reference === null ? null : { ...reference, v: 1 };
457
+ }
458
+
459
+ export type OhSyncBundleV1 = Readonly<{
460
+ bundleSha256: Sha256Hex;
461
+ contractSha256: Sha256Hex;
462
+ operations: readonly OhOperationV1[];
463
+ protocol: typeof OH_SYNC_PROTOCOL_V1;
464
+ spaceId: string;
465
+ v: 1;
466
+ }>;
467
+
468
+ export function createOhSyncBundleV1(
469
+ spaceId: string,
470
+ operations: readonly OhOperationV1[],
471
+ options: Readonly<{ largestFittingPrefix?: boolean }> = {},
472
+ ): OhSyncBundleV1 {
473
+ const parsedSpaceId = safeCode(spaceId);
474
+ const largestFittingPrefix = options.largestFittingPrefix ?? false;
475
+ if (parsedSpaceId === null || operations.length > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1
476
+ || typeof largestFittingPrefix !== "boolean") {
477
+ throw new TypeError("Invalid sync bundle.");
478
+ }
479
+ return buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix);
480
+ }
481
+
482
+ export function parseOhSyncBundleV1(value: unknown): OhSyncBundleV1 | null {
483
+ const envelope = exactDataRecordV1(value, OH_SYNC_BUNDLE_KEYS_V1);
484
+ if (envelope === null || envelope.protocol !== OH_SYNC_PROTOCOL_V1 || envelope.v !== 1) return null;
485
+ const bundleSha256 = parseSha256Hex(envelope.bundleSha256);
486
+ const contractSha256 = parseSha256Hex(envelope.contractSha256);
487
+ const spaceId = safeCode(envelope.spaceId);
488
+ if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256
489
+ || spaceId === null) return null;
490
+ const operations = exactDataArrayV1(envelope.operations, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1);
491
+ if (operations === null) return null;
492
+ const detachedOperations: unknown[] = [];
493
+ const bundleBudget = syncIngressBundleBudgetV1(spaceId);
494
+ for (const operation of operations) {
495
+ if (detachedOperations.length > 0 && !spendSyncIngressBudgetV1(bundleBudget, 1)) return null;
496
+ const detached = stagedSyncOperationV1(operation, bundleBudget);
497
+ if (detached === null) return null;
498
+ detachedOperations.push(detached);
499
+ }
500
+ try {
501
+ const created = createOhSyncBundleV1(spaceId, detachedOperations as OhOperationV1[]);
502
+ if (detachedOperations.some((operation, index) =>
503
+ canonicalJson(operation) !== canonicalJson(created.operations[index]))) return null;
504
+ return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
505
+ } catch { return null; }
506
+ }
507
+
508
+ export interface OhOperationSyncTransportV1 {
509
+ handshake(manifest: OhContractManifestV1): Promise<void>;
510
+ head(spaceId: string): Promise<OhSyncHeadV1>;
511
+ pull(spaceId: string, afterSequence: number, limit: number): Promise<OhSyncBundleV1>;
512
+ push(bundle: OhSyncBundleV1): Promise<OhSyncHeadV1>;
513
+ }
514
+
515
+ export type OhSyncResultV1 = Readonly<{
516
+ head: OhSyncHeadV1;
517
+ pulled: number;
518
+ pushed: number;
519
+ rounds: number;
520
+ v: 1;
521
+ }>;
522
+
523
+
524
+ export type LibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string;
525
+ export type LibSqlStatementV1 = { args?: LibSqlValueV1[]; sql: string };
526
+ export type LibSqlResultV1 = Readonly<{ rows: readonly (Readonly<Record<string, unknown>> | readonly unknown[])[] }>;
527
+ export interface LibSqlClientV1 {
528
+ execute(statement: LibSqlStatementV1 | string): Promise<LibSqlResultV1>;
529
+ batch(statements: LibSqlStatementV1[], mode?: "deferred" | "read" | "write"): Promise<readonly LibSqlResultV1[]>;
530
+ }
@@ -0,0 +1,87 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+
3
+ import type { OhSqliteStore } from "./sqlite/store";
4
+ import type { LibSqlClientV1, LibSqlResultV1, LibSqlStatementV1,
5
+ OhOperationSyncTransportV1, OhSyncBundleV1, OhSyncHeadV1 } from "./sync-model";
6
+ import type { OhContractManifestV1 } from "./contract";
7
+
8
+ export type SyncFailure = Readonly<{
9
+ _tag: "StoreFailure" | "TransportFailure" | "ValidationFailure" | "SyncConflict" | "RoundLimit";
10
+ /** Original rejection identity is retained only at the public boundary. */
11
+ cause: unknown;
12
+ }>;
13
+
14
+ export function syncInvalid(cause: unknown): SyncFailure {
15
+ return { _tag: "ValidationFailure", cause };
16
+ }
17
+
18
+ export interface SyncTransportService {
19
+ handshake(manifest: OhContractManifestV1): Effect.Effect<void, SyncFailure>;
20
+ head(spaceId: string): Effect.Effect<OhSyncHeadV1, SyncFailure>;
21
+ pull(spaceId: string, afterSequence: number, limit: number): Effect.Effect<OhSyncBundleV1, SyncFailure>;
22
+ push(bundle: OhSyncBundleV1): Effect.Effect<OhSyncHeadV1, SyncFailure>;
23
+ }
24
+
25
+ export class SyncTransport extends Context.Tag("@hraness/oh/SyncTransport")<
26
+ SyncTransport, SyncTransportService
27
+ >() {}
28
+
29
+ export interface SyncStoreService {
30
+ readonly spaceId: string;
31
+ readonly head: Effect.Effect<ReturnType<OhSqliteStore["head"]>, SyncFailure>;
32
+ importOperations(input: Parameters<OhSqliteStore["importOperations"]>[0]):
33
+ Effect.Effect<ReturnType<OhSqliteStore["importOperations"]>, SyncFailure>;
34
+ changesSince(head: Parameters<OhSqliteStore["changesSince"]>[0],
35
+ options: Parameters<OhSqliteStore["changesSince"]>[1]):
36
+ Effect.Effect<ReturnType<OhSqliteStore["changesSince"]>, SyncFailure>;
37
+ updateSyncState(remoteId: string, state: Parameters<OhSqliteStore["updateSyncState"]>[1]):
38
+ Effect.Effect<void, SyncFailure>;
39
+ }
40
+
41
+ export class SyncStore extends Context.Tag("@hraness/oh/SyncStore")<SyncStore, SyncStoreService>() {}
42
+
43
+ function storeCall<A>(operation: () => A): Effect.Effect<A, SyncFailure> {
44
+ // The transaction body executes synchronously, without an Effect yield.
45
+ return Effect.try({ try: operation, catch: (cause): SyncFailure => ({ _tag: "StoreFailure", cause }) });
46
+ }
47
+
48
+ function transportCall<A>(operation: () => Promise<A>): Effect.Effect<A, SyncFailure> {
49
+ return Effect.tryPromise({ try: operation,
50
+ catch: (cause): SyncFailure => ({ _tag: "TransportFailure", cause }) });
51
+ }
52
+
53
+ export function syncStoreLive(store: OhSqliteStore): Layer.Layer<SyncStore> {
54
+ return Layer.succeed(SyncStore, {
55
+ spaceId: store.spaceId,
56
+ head: storeCall(() => store.head()),
57
+ importOperations: (input) => storeCall(() => store.importOperations(input)),
58
+ changesSince: (head, options) => storeCall(() => store.changesSince(head, options)),
59
+ updateSyncState: (remoteId, state) => storeCall(() => store.updateSyncState(remoteId, state)),
60
+ });
61
+ }
62
+
63
+ export function syncTransportLive(transport: OhOperationSyncTransportV1): Layer.Layer<SyncTransport> {
64
+ return Layer.succeed(SyncTransport, {
65
+ handshake: (manifest) => transportCall(() => transport.handshake(manifest)),
66
+ head: (spaceId) => transportCall(() => transport.head(spaceId)),
67
+ pull: (spaceId, afterSequence, limit) => transportCall(() => transport.pull(spaceId, afterSequence, limit)),
68
+ push: (bundle) => transportCall(() => transport.push(bundle)),
69
+ });
70
+ }
71
+
72
+ export interface LibSqlSyncClientService {
73
+ execute(statement: LibSqlStatementV1 | string): Effect.Effect<LibSqlResultV1, SyncFailure>;
74
+ batch(statements: LibSqlStatementV1[], mode: "deferred" | "read" | "write"):
75
+ Effect.Effect<readonly LibSqlResultV1[], SyncFailure>;
76
+ }
77
+
78
+ export class LibSqlSyncClient extends Context.Tag("@hraness/oh/LibSqlSyncClient")<
79
+ LibSqlSyncClient, LibSqlSyncClientService
80
+ >() {}
81
+
82
+ export function libSqlSyncClientLive(client: LibSqlClientV1): Layer.Layer<LibSqlSyncClient> {
83
+ return Layer.succeed(LibSqlSyncClient, {
84
+ execute: (statement) => transportCall(() => client.execute(statement)),
85
+ batch: (statements, mode) => transportCall(() => client.batch(statements, mode)),
86
+ });
87
+ }