@hraness/oh 0.3.2 → 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/README.md +164 -114
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +811 -97
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/index.js +676 -61
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +162 -35
- package/dist/memory-page.js +2 -2
- package/dist/memory.d.ts +87 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1106 -148
- package/dist/operation.d.ts +3 -1
- package/dist/operation.d.ts.map +1 -1
- package/dist/projection-public.js +2 -2
- package/dist/projection-suss.js +2 -2
- package/dist/sdk.js +780 -88
- package/dist/semantic-cloud.js +2 -2
- package/dist/semantic.js +2 -2
- package/dist/sqlite/index.js +1251 -306
- package/dist/sqlite/port.d.ts +31 -3
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sqlite/store.d.ts +18 -2
- package/dist/sqlite/store.d.ts.map +1 -1
- package/dist/store.d.ts +3 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +154 -32
- package/dist/sync.d.ts +7 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +668 -35
- package/package.json +5 -1
- package/skills/oh/SKILL.md +42 -16
- package/spec/README.md +2 -2
- package/spec/v1/memory.md +134 -16
- package/spec/v1/storage.md +8 -5
- package/spec/v1/store.md +20 -0
- package/spec/v1/sync.md +77 -8
- package/src/cli.test.ts +53 -1
- package/src/cli.ts +34 -8
- package/src/errors.test.ts +87 -0
- package/src/errors.ts +185 -0
- package/src/graph.ts +2 -2
- package/src/libsql.test.ts +36 -0
- package/src/libsql.ts +26 -5
- package/src/memory.test.ts +1488 -18
- package/src/memory.ts +1199 -122
- package/src/operation.ts +13 -3
- package/src/sqlite/port.test.ts +209 -0
- package/src/sqlite/port.ts +118 -4
- package/src/sqlite/store.test.ts +106 -1
- package/src/sqlite/store.ts +168 -30
- package/src/store.test.ts +12 -0
- package/src/store.ts +30 -20
- package/src/sync.test.ts +570 -2
- package/src/sync.ts +586 -36
package/dist/sqlite/index.js
CHANGED
|
@@ -1,78 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
import {
|
|
4
|
-
import { Database } from "bun:sqlite";
|
|
5
|
-
|
|
6
|
-
// src/sqlite/runtime.ts
|
|
7
|
-
var MACOS_SQLITE_LIBRARY_CANDIDATES = Object.freeze([
|
|
8
|
-
"/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib",
|
|
9
|
-
"/usr/local/opt/sqlite/lib/libsqlite3.dylib"
|
|
10
|
-
]);
|
|
11
|
-
function macosSqliteLibraryCandidates() {
|
|
12
|
-
return MACOS_SQLITE_LIBRARY_CANDIDATES;
|
|
13
|
-
}
|
|
14
|
-
function createOhSqliteRuntime(dependencies) {
|
|
15
|
-
let customLibrary = null;
|
|
16
|
-
if (dependencies.platform === "darwin") {
|
|
17
|
-
for (const candidate of macosSqliteLibraryCandidates()) {
|
|
18
|
-
if (!dependencies.exists(candidate))
|
|
19
|
-
continue;
|
|
20
|
-
try {
|
|
21
|
-
if (!dependencies.setCustomSQLite(candidate))
|
|
22
|
-
continue;
|
|
23
|
-
customLibrary = candidate;
|
|
24
|
-
break;
|
|
25
|
-
} catch {}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return Object.freeze({
|
|
29
|
-
customLibrary,
|
|
30
|
-
open: (path) => dependencies.open(path, { create: true, strict: true })
|
|
31
|
-
});
|
|
32
|
-
}
|
|
2
|
+
// src/sync.ts
|
|
3
|
+
import { isProxy } from "util/types";
|
|
33
4
|
|
|
34
|
-
// src/sqlite/driver.ts
|
|
35
|
-
var SQLITE_RUNTIME = createOhSqliteRuntime({
|
|
36
|
-
exists: existsSync,
|
|
37
|
-
open: (path, options) => new Database(path, options),
|
|
38
|
-
platform: process.platform,
|
|
39
|
-
setCustomSQLite: (path) => Database.setCustomSQLite(path)
|
|
40
|
-
});
|
|
41
|
-
function openOhSqliteDatabase(path) {
|
|
42
|
-
const database = SQLITE_RUNTIME.open(path);
|
|
43
|
-
database.exec("PRAGMA foreign_keys = ON");
|
|
44
|
-
database.exec("PRAGMA journal_mode = WAL");
|
|
45
|
-
database.exec("PRAGMA synchronous = NORMAL");
|
|
46
|
-
database.exec("PRAGMA busy_timeout = 5000");
|
|
47
|
-
database.exec("PRAGMA trusted_schema = OFF");
|
|
48
|
-
return database;
|
|
49
|
-
}
|
|
50
|
-
function withImmediateTransaction(database, work) {
|
|
51
|
-
database.exec("BEGIN IMMEDIATE");
|
|
52
|
-
try {
|
|
53
|
-
const result = work();
|
|
54
|
-
database.exec("COMMIT");
|
|
55
|
-
return result;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
try {
|
|
58
|
-
database.exec("ROLLBACK");
|
|
59
|
-
} catch {}
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function withReadTransaction(database, work) {
|
|
64
|
-
database.exec("BEGIN");
|
|
65
|
-
try {
|
|
66
|
-
const result = work();
|
|
67
|
-
database.exec("COMMIT");
|
|
68
|
-
return result;
|
|
69
|
-
} catch (error) {
|
|
70
|
-
try {
|
|
71
|
-
database.exec("ROLLBACK");
|
|
72
|
-
} catch {}
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
5
|
// src/canonical.ts
|
|
77
6
|
import { createHash, randomBytes } from "crypto";
|
|
78
7
|
|
|
@@ -263,176 +192,6 @@ function sortUnique(values, key) {
|
|
|
263
192
|
return sorted;
|
|
264
193
|
}
|
|
265
194
|
|
|
266
|
-
// src/sqlite/migrations.ts
|
|
267
|
-
var OH_SQLITE_SCHEMA_VERSION = 2;
|
|
268
|
-
var OH_SQLITE_MIGRATIONS = Object.freeze([
|
|
269
|
-
Object.freeze({
|
|
270
|
-
name: "0001_oh_core",
|
|
271
|
-
version: 1,
|
|
272
|
-
sql: `
|
|
273
|
-
CREATE TABLE oh_contracts (
|
|
274
|
-
contract_id TEXT PRIMARY KEY,
|
|
275
|
-
contract_sha256 TEXT NOT NULL CHECK(length(contract_sha256) = 64),
|
|
276
|
-
manifest_json TEXT NOT NULL CHECK(json_valid(manifest_json)),
|
|
277
|
-
created_at TEXT NOT NULL
|
|
278
|
-
) STRICT;
|
|
279
|
-
|
|
280
|
-
CREATE TABLE oh_spaces (
|
|
281
|
-
space_id TEXT PRIMARY KEY,
|
|
282
|
-
contract_id TEXT NOT NULL REFERENCES oh_contracts(contract_id),
|
|
283
|
-
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
284
|
-
head_operation_sha256 TEXT CHECK(head_operation_sha256 IS NULL OR length(head_operation_sha256) = 64),
|
|
285
|
-
graph_revision_sha256 TEXT CHECK(graph_revision_sha256 IS NULL OR length(graph_revision_sha256) = 64),
|
|
286
|
-
records_sha256 TEXT NOT NULL CHECK(length(records_sha256) = 64),
|
|
287
|
-
sequence INTEGER NOT NULL CHECK(sequence >= 0),
|
|
288
|
-
created_at TEXT NOT NULL,
|
|
289
|
-
updated_at TEXT NOT NULL,
|
|
290
|
-
CHECK(generation = sequence),
|
|
291
|
-
CHECK((sequence = 0) = (head_operation_sha256 IS NULL)),
|
|
292
|
-
CHECK((sequence = 0) = (graph_revision_sha256 IS NULL))
|
|
293
|
-
) STRICT;
|
|
294
|
-
|
|
295
|
-
CREATE TABLE oh_operations (
|
|
296
|
-
operation_sha256 TEXT PRIMARY KEY CHECK(length(operation_sha256) = 64),
|
|
297
|
-
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
298
|
-
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
299
|
-
operation_id TEXT NOT NULL,
|
|
300
|
-
parent_operation_sha256 TEXT CHECK(parent_operation_sha256 IS NULL OR length(parent_operation_sha256) = 64),
|
|
301
|
-
graph_revision_sha256 TEXT NOT NULL CHECK(length(graph_revision_sha256) = 64),
|
|
302
|
-
records_sha256 TEXT NOT NULL CHECK(length(records_sha256) = 64),
|
|
303
|
-
operation_json TEXT NOT NULL CHECK(json_valid(operation_json)),
|
|
304
|
-
instant TEXT NOT NULL,
|
|
305
|
-
UNIQUE(space_id, sequence),
|
|
306
|
-
UNIQUE(space_id, operation_id)
|
|
307
|
-
) STRICT;
|
|
308
|
-
|
|
309
|
-
CREATE TABLE oh_operation_records (
|
|
310
|
-
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
311
|
-
ordinal INTEGER NOT NULL CHECK(ordinal >= 0),
|
|
312
|
-
record_key TEXT NOT NULL,
|
|
313
|
-
change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')),
|
|
314
|
-
record_sha256 TEXT CHECK(record_sha256 IS NULL OR length(record_sha256) = 64),
|
|
315
|
-
PRIMARY KEY(operation_sha256, ordinal),
|
|
316
|
-
UNIQUE(operation_sha256, record_key)
|
|
317
|
-
) STRICT;
|
|
318
|
-
|
|
319
|
-
CREATE TABLE oh_records (
|
|
320
|
-
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
321
|
-
record_key TEXT NOT NULL,
|
|
322
|
-
kind TEXT NOT NULL,
|
|
323
|
-
record_sha256 TEXT NOT NULL CHECK(length(record_sha256) = 64),
|
|
324
|
-
record_json TEXT NOT NULL CHECK(json_valid(record_json)),
|
|
325
|
-
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
326
|
-
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
327
|
-
PRIMARY KEY(space_id, record_key)
|
|
328
|
-
) STRICT;
|
|
329
|
-
|
|
330
|
-
CREATE TABLE oh_dependencies (
|
|
331
|
-
space_id TEXT NOT NULL,
|
|
332
|
-
record_key TEXT NOT NULL,
|
|
333
|
-
dependency_key TEXT NOT NULL,
|
|
334
|
-
PRIMARY KEY(space_id, record_key, dependency_key),
|
|
335
|
-
FOREIGN KEY(space_id, record_key) REFERENCES oh_records(space_id, record_key) ON DELETE CASCADE,
|
|
336
|
-
FOREIGN KEY(space_id, dependency_key) REFERENCES oh_records(space_id, record_key)
|
|
337
|
-
) STRICT;
|
|
338
|
-
|
|
339
|
-
CREATE TABLE oh_sync_outbox (
|
|
340
|
-
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
341
|
-
sequence INTEGER NOT NULL,
|
|
342
|
-
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
343
|
-
PRIMARY KEY(space_id, sequence),
|
|
344
|
-
UNIQUE(operation_sha256)
|
|
345
|
-
) STRICT;
|
|
346
|
-
|
|
347
|
-
CREATE TABLE oh_sync_state (
|
|
348
|
-
remote_id TEXT NOT NULL,
|
|
349
|
-
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
350
|
-
pulled_sequence INTEGER NOT NULL CHECK(pulled_sequence >= 0),
|
|
351
|
-
pushed_sequence INTEGER NOT NULL CHECK(pushed_sequence >= 0),
|
|
352
|
-
remote_head_sha256 TEXT CHECK(remote_head_sha256 IS NULL OR length(remote_head_sha256) = 64),
|
|
353
|
-
updated_at TEXT NOT NULL,
|
|
354
|
-
PRIMARY KEY(remote_id, space_id)
|
|
355
|
-
) STRICT;
|
|
356
|
-
|
|
357
|
-
CREATE TABLE oh_search_documents (
|
|
358
|
-
space_id TEXT NOT NULL,
|
|
359
|
-
record_key TEXT NOT NULL,
|
|
360
|
-
record_sha256 TEXT NOT NULL CHECK(length(record_sha256) = 64),
|
|
361
|
-
text TEXT NOT NULL,
|
|
362
|
-
PRIMARY KEY(space_id, record_key),
|
|
363
|
-
FOREIGN KEY(space_id, record_key) REFERENCES oh_records(space_id, record_key) ON DELETE CASCADE
|
|
364
|
-
) STRICT;
|
|
365
|
-
|
|
366
|
-
CREATE VIRTUAL TABLE oh_search_fts USING fts5(
|
|
367
|
-
space_id UNINDEXED,
|
|
368
|
-
record_key UNINDEXED,
|
|
369
|
-
text,
|
|
370
|
-
tokenize='unicode61 remove_diacritics 2'
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
CREATE INDEX oh_operations_space_sequence ON oh_operations(space_id, sequence);
|
|
374
|
-
CREATE INDEX oh_records_space_kind ON oh_records(space_id, kind, record_key);
|
|
375
|
-
CREATE INDEX oh_dependencies_dependency ON oh_dependencies(space_id, dependency_key);
|
|
376
|
-
`
|
|
377
|
-
}),
|
|
378
|
-
Object.freeze({
|
|
379
|
-
name: "0002_store_realms",
|
|
380
|
-
version: 2,
|
|
381
|
-
sql: `
|
|
382
|
-
CREATE TABLE oh_space_bindings (
|
|
383
|
-
space_id TEXT PRIMARY KEY REFERENCES oh_spaces(space_id),
|
|
384
|
-
realm_id TEXT NOT NULL,
|
|
385
|
-
profile_id TEXT NOT NULL,
|
|
386
|
-
profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')),
|
|
387
|
-
profile_sha256 TEXT NOT NULL CHECK(length(profile_sha256) = 64),
|
|
388
|
-
binding_sha256 TEXT NOT NULL UNIQUE CHECK(length(binding_sha256) = 64),
|
|
389
|
-
binding_json TEXT NOT NULL CHECK(json_valid(binding_json)),
|
|
390
|
-
created_at TEXT NOT NULL
|
|
391
|
-
) STRICT;
|
|
392
|
-
|
|
393
|
-
CREATE TABLE oh_space_purges (
|
|
394
|
-
space_id TEXT PRIMARY KEY,
|
|
395
|
-
binding_sha256 TEXT NOT NULL CHECK(length(binding_sha256) = 64),
|
|
396
|
-
prior_operation_sha256 TEXT CHECK(prior_operation_sha256 IS NULL OR length(prior_operation_sha256) = 64),
|
|
397
|
-
prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0),
|
|
398
|
-
purged_at TEXT NOT NULL,
|
|
399
|
-
receipt_sha256 TEXT NOT NULL UNIQUE CHECK(length(receipt_sha256) = 64),
|
|
400
|
-
receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json))
|
|
401
|
-
) STRICT;
|
|
402
|
-
`
|
|
403
|
-
})
|
|
404
|
-
]);
|
|
405
|
-
function applyOhSqliteMigrations(database) {
|
|
406
|
-
database.exec(`CREATE TABLE IF NOT EXISTS oh_migrations (
|
|
407
|
-
version INTEGER PRIMARY KEY,
|
|
408
|
-
name TEXT NOT NULL UNIQUE,
|
|
409
|
-
migration_sha256 TEXT NOT NULL CHECK(length(migration_sha256) = 64),
|
|
410
|
-
applied_at TEXT NOT NULL
|
|
411
|
-
) STRICT`);
|
|
412
|
-
const select = database.query("SELECT name, migration_sha256 FROM oh_migrations WHERE version = ?");
|
|
413
|
-
const insert = database.query("INSERT INTO oh_migrations(version, name, migration_sha256, applied_at) VALUES (?, ?, ?, ?)");
|
|
414
|
-
for (const migration of OH_SQLITE_MIGRATIONS) {
|
|
415
|
-
const digest = sha256Hex(migration.sql);
|
|
416
|
-
database.exec("BEGIN IMMEDIATE");
|
|
417
|
-
try {
|
|
418
|
-
const existing = select.get(migration.version);
|
|
419
|
-
if (existing !== null) {
|
|
420
|
-
if (existing.name !== migration.name || existing.migration_sha256 !== digest) {
|
|
421
|
-
throw new Error(`SQLite migration ${migration.version} does not match the applied migration.`);
|
|
422
|
-
}
|
|
423
|
-
} else {
|
|
424
|
-
database.exec(migration.sql);
|
|
425
|
-
insert.run(migration.version, migration.name, digest, canonicalNow());
|
|
426
|
-
}
|
|
427
|
-
database.exec("COMMIT");
|
|
428
|
-
} catch (error) {
|
|
429
|
-
try {
|
|
430
|
-
database.exec("ROLLBACK");
|
|
431
|
-
} catch {}
|
|
432
|
-
throw error;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
195
|
// src/graph.ts
|
|
437
196
|
var OH_GRAPH_FORMAT_VERSION_V1 = 1;
|
|
438
197
|
var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
@@ -441,7 +200,7 @@ var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
|
441
200
|
recordBytes: 1024 * 1024,
|
|
442
201
|
recordsPerSnapshot: 65536
|
|
443
202
|
});
|
|
444
|
-
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
203
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = Object.freeze([
|
|
445
204
|
"activity",
|
|
446
205
|
"assertion",
|
|
447
206
|
"context",
|
|
@@ -460,7 +219,7 @@ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
|
460
219
|
"type-membership",
|
|
461
220
|
"view",
|
|
462
221
|
"vocabulary"
|
|
463
|
-
];
|
|
222
|
+
]);
|
|
464
223
|
var KNOWLEDGE_GRAPH_RECORD_KEYS_V1 = [
|
|
465
224
|
"dependencies",
|
|
466
225
|
"key",
|
|
@@ -1386,17 +1145,153 @@ class OhRecordCodecRegistry {
|
|
|
1386
1145
|
}
|
|
1387
1146
|
}
|
|
1388
1147
|
|
|
1389
|
-
// src/
|
|
1390
|
-
var
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1148
|
+
// src/errors.ts
|
|
1149
|
+
var OH_OPERATION_SIZE_ERROR_CODE_V1 = "oh.operation-size.v1";
|
|
1150
|
+
var OH_CONFLICT_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhConflictError/v1");
|
|
1151
|
+
var OH_DEPENDENCY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhDependencyError/v1");
|
|
1152
|
+
var OH_INTEGRITY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhIntegrityError/v1");
|
|
1153
|
+
var OH_OPERATION_SIZE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhOperationSizeError/v1");
|
|
1154
|
+
var OH_PROFILE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhProfileError/v1");
|
|
1155
|
+
function immutableOwnValue(value, key) {
|
|
1156
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1157
|
+
return descriptor !== undefined && descriptor.get === undefined && descriptor.set === undefined && descriptor.configurable === false && descriptor.writable === false ? descriptor.value : undefined;
|
|
1158
|
+
}
|
|
1159
|
+
function brandNativeError(value, brand) {
|
|
1160
|
+
Object.defineProperty(value, brand, {
|
|
1161
|
+
configurable: false,
|
|
1162
|
+
enumerable: false,
|
|
1163
|
+
value: true,
|
|
1164
|
+
writable: false
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
function hasNativeErrorBrand(value, brand) {
|
|
1168
|
+
try {
|
|
1169
|
+
return Error.isError(value) && immutableOwnValue(value, brand) === true;
|
|
1170
|
+
} catch {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function hasNativeSubclassInstance(constructor, value) {
|
|
1175
|
+
return Function.prototype[Symbol.hasInstance].call(constructor, value);
|
|
1176
|
+
}
|
|
1177
|
+
function isOhConflictError(value) {
|
|
1178
|
+
return hasNativeErrorBrand(value, OH_CONFLICT_ERROR_BRAND_V1);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
class OhConflictError extends Error {
|
|
1182
|
+
static [Symbol.hasInstance](value) {
|
|
1183
|
+
return this === OhConflictError ? isOhConflictError(value) : hasNativeSubclassInstance(this, value);
|
|
1184
|
+
}
|
|
1185
|
+
constructor(message) {
|
|
1186
|
+
super(message);
|
|
1187
|
+
this.name = "OhConflictError";
|
|
1188
|
+
brandNativeError(this, OH_CONFLICT_ERROR_BRAND_V1);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function isOhIntegrityError(value) {
|
|
1192
|
+
return hasNativeErrorBrand(value, OH_INTEGRITY_ERROR_BRAND_V1);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
class OhIntegrityError extends Error {
|
|
1196
|
+
static [Symbol.hasInstance](value) {
|
|
1197
|
+
return this === OhIntegrityError ? isOhIntegrityError(value) : hasNativeSubclassInstance(this, value);
|
|
1198
|
+
}
|
|
1199
|
+
constructor(message) {
|
|
1200
|
+
super(message);
|
|
1201
|
+
this.name = "OhIntegrityError";
|
|
1202
|
+
brandNativeError(this, OH_INTEGRITY_ERROR_BRAND_V1);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function isOhDependencyError(value) {
|
|
1206
|
+
return hasNativeErrorBrand(value, OH_DEPENDENCY_ERROR_BRAND_V1);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
class OhDependencyError extends Error {
|
|
1210
|
+
static [Symbol.hasInstance](value) {
|
|
1211
|
+
return this === OhDependencyError ? isOhDependencyError(value) : hasNativeSubclassInstance(this, value);
|
|
1212
|
+
}
|
|
1213
|
+
constructor(message) {
|
|
1214
|
+
super(message);
|
|
1215
|
+
this.name = "OhDependencyError";
|
|
1216
|
+
brandNativeError(this, OH_DEPENDENCY_ERROR_BRAND_V1);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function isOhProfileError(value) {
|
|
1220
|
+
return hasNativeErrorBrand(value, OH_PROFILE_ERROR_BRAND_V1);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
class OhProfileError extends Error {
|
|
1224
|
+
static [Symbol.hasInstance](value) {
|
|
1225
|
+
return this === OhProfileError ? isOhProfileError(value) : hasNativeSubclassInstance(this, value);
|
|
1226
|
+
}
|
|
1227
|
+
constructor(message) {
|
|
1228
|
+
super(message);
|
|
1229
|
+
this.name = "OhProfileError";
|
|
1230
|
+
brandNativeError(this, OH_PROFILE_ERROR_BRAND_V1);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function isOhOperationSizeError(value) {
|
|
1234
|
+
try {
|
|
1235
|
+
if (!Error.isError(value) || !(value instanceof RangeError))
|
|
1236
|
+
return false;
|
|
1237
|
+
const operationBytes = immutableOwnValue(value, "operationBytes");
|
|
1238
|
+
const maximumOperationBytes = immutableOwnValue(value, "maximumOperationBytes");
|
|
1239
|
+
return immutableOwnValue(value, OH_OPERATION_SIZE_ERROR_BRAND_V1) === true && immutableOwnValue(value, "code") === OH_OPERATION_SIZE_ERROR_CODE_V1 && Number.isSafeInteger(operationBytes) && operationBytes > 0 && Number.isSafeInteger(maximumOperationBytes) && maximumOperationBytes > 0 && operationBytes > maximumOperationBytes;
|
|
1240
|
+
} catch {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
class OhOperationSizeError extends RangeError {
|
|
1246
|
+
static [Symbol.hasInstance](value) {
|
|
1247
|
+
return this === OhOperationSizeError ? isOhOperationSizeError(value) : hasNativeSubclassInstance(this, value);
|
|
1248
|
+
}
|
|
1249
|
+
constructor(operationBytes, maximumOperationBytes) {
|
|
1250
|
+
if (!Number.isSafeInteger(operationBytes) || operationBytes < 1 || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || operationBytes <= maximumOperationBytes) {
|
|
1251
|
+
throw new TypeError("Invalid Oh operation size refusal.");
|
|
1252
|
+
}
|
|
1253
|
+
super(`The ${operationBytes}-byte operation exceeds the host-declared ${maximumOperationBytes}-byte canonical bound.`);
|
|
1254
|
+
this.name = "OhOperationSizeError";
|
|
1255
|
+
Object.defineProperties(this, {
|
|
1256
|
+
[OH_OPERATION_SIZE_ERROR_BRAND_V1]: {
|
|
1257
|
+
configurable: false,
|
|
1258
|
+
enumerable: false,
|
|
1259
|
+
value: true,
|
|
1260
|
+
writable: false
|
|
1261
|
+
},
|
|
1262
|
+
code: {
|
|
1263
|
+
configurable: false,
|
|
1264
|
+
enumerable: true,
|
|
1265
|
+
value: OH_OPERATION_SIZE_ERROR_CODE_V1,
|
|
1266
|
+
writable: false
|
|
1267
|
+
},
|
|
1268
|
+
maximumOperationBytes: {
|
|
1269
|
+
configurable: false,
|
|
1270
|
+
enumerable: true,
|
|
1271
|
+
value: maximumOperationBytes,
|
|
1272
|
+
writable: false
|
|
1273
|
+
},
|
|
1274
|
+
operationBytes: {
|
|
1275
|
+
configurable: false,
|
|
1276
|
+
enumerable: true,
|
|
1277
|
+
value: operationBytes,
|
|
1278
|
+
writable: false
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/operation.ts
|
|
1285
|
+
var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
|
|
1286
|
+
function parsePayload(value) {
|
|
1287
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1288
|
+
"actorId",
|
|
1289
|
+
"changes",
|
|
1290
|
+
"contractId",
|
|
1291
|
+
"graphRevisionSha256",
|
|
1292
|
+
"instant",
|
|
1293
|
+
"operationId",
|
|
1294
|
+
"parentOperationSha256",
|
|
1400
1295
|
"recordsSha256",
|
|
1401
1296
|
"sequence",
|
|
1402
1297
|
"spaceId",
|
|
@@ -1433,13 +1328,18 @@ function parsePayload(value) {
|
|
|
1433
1328
|
v: 1
|
|
1434
1329
|
} : null;
|
|
1435
1330
|
}
|
|
1436
|
-
function createOhOperationV1(input) {
|
|
1331
|
+
function createOhOperationV1(input, options = {}) {
|
|
1332
|
+
const maximumOperationBytes = options.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
|
|
1333
|
+
if (!Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
1334
|
+
throw new TypeError("Invalid Oh operation byte bound.");
|
|
1335
|
+
}
|
|
1437
1336
|
const payload = parsePayload(input);
|
|
1438
1337
|
if (payload === null)
|
|
1439
1338
|
throw new TypeError("Invalid Oh operation payload.");
|
|
1440
1339
|
const operation = { ...payload, operationSha256: canonicalSha256(payload) };
|
|
1441
|
-
|
|
1442
|
-
|
|
1340
|
+
const operationBytes = Buffer.byteLength(canonicalJson(operation), "utf8");
|
|
1341
|
+
if (operationBytes > maximumOperationBytes) {
|
|
1342
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
1443
1343
|
}
|
|
1444
1344
|
return operation;
|
|
1445
1345
|
}
|
|
@@ -1452,34 +1352,923 @@ function parseOhOperationV1(value) {
|
|
|
1452
1352
|
return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null;
|
|
1453
1353
|
}
|
|
1454
1354
|
|
|
1455
|
-
// src/
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1355
|
+
// src/sync.ts
|
|
1356
|
+
var OH_SYNC_PROTOCOL_V1 = "oh.sync.v1";
|
|
1357
|
+
var OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 = 1000;
|
|
1358
|
+
var OH_SYNC_BUNDLE_KEYS_V1 = [
|
|
1359
|
+
"bundleSha256",
|
|
1360
|
+
"contractSha256",
|
|
1361
|
+
"operations",
|
|
1362
|
+
"protocol",
|
|
1363
|
+
"spaceId",
|
|
1364
|
+
"v"
|
|
1365
|
+
];
|
|
1366
|
+
var OH_SYNC_HEAD_KEYS_V1 = ["operationSha256", "sequence", "v"];
|
|
1367
|
+
var OH_SYNC_HEAD_REF_KEYS_V1 = ["operationSha256", "sequence"];
|
|
1368
|
+
var OH_OPERATION_KEYS_V1 = [
|
|
1369
|
+
"actorId",
|
|
1370
|
+
"changes",
|
|
1371
|
+
"contractId",
|
|
1372
|
+
"graphRevisionSha256",
|
|
1373
|
+
"instant",
|
|
1374
|
+
"operationId",
|
|
1375
|
+
"operationSha256",
|
|
1376
|
+
"parentOperationSha256",
|
|
1377
|
+
"recordsSha256",
|
|
1378
|
+
"sequence",
|
|
1379
|
+
"spaceId",
|
|
1380
|
+
"v"
|
|
1381
|
+
];
|
|
1382
|
+
var OH_PUT_CHANGE_KEYS_V1 = ["kind", "record", "v"];
|
|
1383
|
+
var OH_TOMBSTONE_CHANGE_KEYS_V1 = ["key", "kind", "priorSha256", "v"];
|
|
1384
|
+
var OH_RECORD_KEYS_V1 = ["dependencies", "key", "kind", "recordSha256", "v", "value"];
|
|
1385
|
+
var OH_SYNC_INGRESS_VALUE_DEPTH_V1 = 128;
|
|
1386
|
+
var OH_SYNC_INGRESS_VALUE_NODES_V1 = OH_GRAPH_LIMITS_V1.recordBytes;
|
|
1387
|
+
var OH_SYNC_INGRESS_OPERATION_DEPTH_V1 = OH_SYNC_INGRESS_VALUE_DEPTH_V1 + 4;
|
|
1388
|
+
var OH_SYNC_INGRESS_OPERATION_NODES_V1 = OH_OPERATION_MAX_BYTES_V1;
|
|
1389
|
+
var OH_SYNC_BUNDLE_MAX_BYTES_V1 = OH_OPERATION_MAX_BYTES_V1 + 4 * 1024;
|
|
1390
|
+
var OH_SYNC_INGRESS_BUNDLE_NODES_V1 = OH_SYNC_INGRESS_OPERATION_NODES_V1 + 4 * 1024;
|
|
1391
|
+
function exactDataRecordV1(value, expectedKeys) {
|
|
1392
|
+
try {
|
|
1393
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || isProxy(value))
|
|
1394
|
+
return null;
|
|
1395
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1396
|
+
const keys = Reflect.ownKeys(value);
|
|
1397
|
+
if (prototype !== Object.prototype && prototype !== null || keys.length !== expectedKeys.length || keys.some((key) => typeof key !== "string") || expectedKeys.some((key) => !keys.includes(key)))
|
|
1398
|
+
return null;
|
|
1399
|
+
const detached = Object.create(null);
|
|
1400
|
+
for (const key of expectedKeys) {
|
|
1401
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1402
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1403
|
+
return null;
|
|
1404
|
+
Object.defineProperty(detached, key, {
|
|
1405
|
+
configurable: false,
|
|
1406
|
+
enumerable: true,
|
|
1407
|
+
value: descriptor.value,
|
|
1408
|
+
writable: false
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
return detached;
|
|
1412
|
+
} catch {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function exactDataArrayV1(value, maximumLength, clone = true) {
|
|
1417
|
+
try {
|
|
1418
|
+
if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value))
|
|
1419
|
+
return null;
|
|
1420
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
1421
|
+
const length = lengthDescriptor?.value;
|
|
1422
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > maximumLength)
|
|
1423
|
+
return null;
|
|
1424
|
+
const keys = Reflect.ownKeys(value);
|
|
1425
|
+
if (keys.length !== length + 1 || !keys.includes("length") || keys.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length)))
|
|
1426
|
+
return null;
|
|
1427
|
+
const detached = clone ? [] : null;
|
|
1428
|
+
for (let index = 0;index < length; index += 1) {
|
|
1429
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
1430
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1431
|
+
return null;
|
|
1432
|
+
detached?.push(descriptor.value);
|
|
1433
|
+
}
|
|
1434
|
+
return detached ?? value;
|
|
1435
|
+
} catch {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function stagedSyncOperationV1(value, bundleBudget) {
|
|
1440
|
+
const operation = exactDataRecordV1(value, OH_OPERATION_KEYS_V1);
|
|
1441
|
+
if (operation === null || operation.v !== 1 || operation.contractId !== OH_CONTRACT_ID_V1 || safeCode(operation.actorId) === null || safeCode(operation.operationId) === null || safeCode(operation.spaceId) === null || parseCanonicalInstantV1(operation.instant) === null || parseSha256Hex(operation.operationSha256) === null || parseSha256Hex(operation.graphRevisionSha256) === null || parseSha256Hex(operation.recordsSha256) === null)
|
|
1442
|
+
return null;
|
|
1443
|
+
const parentOperationSha256 = operation.parentOperationSha256 === null ? null : parseSha256Hex(operation.parentOperationSha256);
|
|
1444
|
+
const sequence = Number.isSafeInteger(operation.sequence) && operation.sequence > 0 ? operation.sequence : null;
|
|
1445
|
+
if (sequence === null || operation.parentOperationSha256 !== null && parentOperationSha256 === null || sequence === 1 !== (parentOperationSha256 === null))
|
|
1446
|
+
return null;
|
|
1447
|
+
const changes = exactDataArrayV1(operation.changes, OH_GRAPH_LIMITS_V1.changesPerOperation, false);
|
|
1448
|
+
if (changes === null || changes.length === 0)
|
|
1449
|
+
return null;
|
|
1450
|
+
const aggregateDataBudget = {
|
|
1451
|
+
bytes: 0,
|
|
1452
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1453
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
|
|
1454
|
+
nodes: 0
|
|
1455
|
+
};
|
|
1456
|
+
for (const change of changes) {
|
|
1457
|
+
const put = exactDataRecordV1(change, OH_PUT_CHANGE_KEYS_V1);
|
|
1458
|
+
if (put !== null && put.kind === "put" && put.v === 1) {
|
|
1459
|
+
const record = exactDataRecordV1(put.record, OH_RECORD_KEYS_V1);
|
|
1460
|
+
if (record === null)
|
|
1461
|
+
return null;
|
|
1462
|
+
const valuePreflight = preflightSyncIngressValueV1(record.value, aggregateDataBudget);
|
|
1463
|
+
const dependenciesPreflight = preflightSyncIngressDependenciesV1(record.dependencies, aggregateDataBudget);
|
|
1464
|
+
if (valuePreflight === null || dependenciesPreflight === null)
|
|
1465
|
+
return null;
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
const tombstone = exactDataRecordV1(change, OH_TOMBSTONE_CHANGE_KEYS_V1);
|
|
1469
|
+
if (tombstone === null || tombstone.kind !== "tombstone" || tombstone.v !== 1)
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
const detached = boundedSyncIngressV1(operation, {
|
|
1473
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1474
|
+
maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
|
|
1475
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1
|
|
1476
|
+
}, bundleBudget, true);
|
|
1477
|
+
return detached === null ? null : detached.value;
|
|
1478
|
+
}
|
|
1479
|
+
function canonicalStringBytesV1(value, maximumBytes) {
|
|
1480
|
+
if (value.length + 2 > maximumBytes)
|
|
1481
|
+
throw new RangeError("String exceeds its canonical byte budget.");
|
|
1482
|
+
let bytes = 2;
|
|
1483
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
1484
|
+
const code = value.charCodeAt(index);
|
|
1485
|
+
if (code >= 55296 && code <= 56319) {
|
|
1486
|
+
const next = value.charCodeAt(index + 1);
|
|
1487
|
+
if (!(next >= 56320 && next <= 57343))
|
|
1488
|
+
throw new TypeError("Invalid Unicode string.");
|
|
1489
|
+
bytes += 4;
|
|
1490
|
+
index += 1;
|
|
1491
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
1492
|
+
throw new TypeError("Invalid Unicode string.");
|
|
1493
|
+
} else if (code === 34 || code === 92 || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) {
|
|
1494
|
+
bytes += 2;
|
|
1495
|
+
} else if (code <= 31) {
|
|
1496
|
+
bytes += 6;
|
|
1497
|
+
} else if (code <= 127) {
|
|
1498
|
+
bytes += 1;
|
|
1499
|
+
} else if (code <= 2047) {
|
|
1500
|
+
bytes += 2;
|
|
1501
|
+
} else {
|
|
1502
|
+
bytes += 3;
|
|
1503
|
+
}
|
|
1504
|
+
if (bytes > maximumBytes)
|
|
1505
|
+
throw new RangeError("String exceeds its canonical byte budget.");
|
|
1506
|
+
}
|
|
1507
|
+
return bytes;
|
|
1508
|
+
}
|
|
1509
|
+
function boundedSyncIngressV1(value, limits, aggregate, clone) {
|
|
1510
|
+
const ancestors = new Set;
|
|
1511
|
+
const budget = {
|
|
1512
|
+
bytes: 0,
|
|
1513
|
+
maximumBytes: limits.maximumBytes,
|
|
1514
|
+
maximumNodes: limits.maximumNodes,
|
|
1515
|
+
nodes: 0
|
|
1516
|
+
};
|
|
1517
|
+
const canSpendBytes = (count) => budget.bytes + count <= budget.maximumBytes && (aggregate === null || aggregate.bytes + count <= aggregate.maximumBytes);
|
|
1518
|
+
const spendBytes = (count) => {
|
|
1519
|
+
if (!canSpendBytes(count))
|
|
1520
|
+
throw new RangeError("Sync ingress exceeds its canonical byte budget.");
|
|
1521
|
+
budget.bytes += count;
|
|
1522
|
+
if (aggregate !== null)
|
|
1523
|
+
aggregate.bytes += count;
|
|
1524
|
+
};
|
|
1525
|
+
const canSpendNodes = (count) => budget.nodes + count <= budget.maximumNodes && (aggregate === null || aggregate.nodes + count <= aggregate.maximumNodes);
|
|
1526
|
+
const spendNode = () => {
|
|
1527
|
+
if (!canSpendNodes(1))
|
|
1528
|
+
throw new RangeError("Sync ingress exceeds its node budget.");
|
|
1529
|
+
budget.nodes += 1;
|
|
1530
|
+
if (aggregate !== null)
|
|
1531
|
+
aggregate.nodes += 1;
|
|
1532
|
+
};
|
|
1533
|
+
const detach = (candidate, depth) => {
|
|
1534
|
+
if (depth > limits.maximumDepth)
|
|
1535
|
+
throw new RangeError("Sync ingress exceeds its depth budget.");
|
|
1536
|
+
spendNode();
|
|
1537
|
+
if (candidate === null) {
|
|
1538
|
+
spendBytes(4);
|
|
1539
|
+
return candidate;
|
|
1540
|
+
}
|
|
1541
|
+
if (typeof candidate === "boolean") {
|
|
1542
|
+
spendBytes(candidate ? 4 : 5);
|
|
1543
|
+
return candidate;
|
|
1544
|
+
}
|
|
1545
|
+
if (typeof candidate === "string") {
|
|
1546
|
+
spendBytes(canonicalStringBytesV1(candidate, Math.min(budget.maximumBytes - budget.bytes, aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes)));
|
|
1547
|
+
return candidate;
|
|
1548
|
+
}
|
|
1549
|
+
if (typeof candidate === "number") {
|
|
1550
|
+
if (!Number.isFinite(candidate) || Object.is(candidate, -0))
|
|
1551
|
+
throw new TypeError("Invalid number.");
|
|
1552
|
+
spendBytes(utf8ByteLength(canonicalJson(candidate)));
|
|
1553
|
+
return candidate;
|
|
1554
|
+
}
|
|
1555
|
+
if (typeof candidate !== "object" || isProxy(candidate))
|
|
1556
|
+
throw new TypeError("Invalid data value.");
|
|
1557
|
+
if (ancestors.has(candidate))
|
|
1558
|
+
throw new TypeError("Cyclic data value.");
|
|
1559
|
+
ancestors.add(candidate);
|
|
1560
|
+
try {
|
|
1561
|
+
if (Array.isArray(candidate)) {
|
|
1562
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(candidate, "length");
|
|
1563
|
+
const length = lengthDescriptor?.value;
|
|
1564
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || !canSpendNodes(length) || !canSpendBytes(2 + Math.max(0, length - 1) + length)) {
|
|
1565
|
+
throw new TypeError("Invalid or over-budget data array.");
|
|
1566
|
+
}
|
|
1567
|
+
const keys2 = Reflect.ownKeys(candidate);
|
|
1568
|
+
if (keys2.length !== length + 1 || !keys2.includes("length") || keys2.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
|
|
1569
|
+
throw new TypeError("Invalid data array.");
|
|
1570
|
+
}
|
|
1571
|
+
spendBytes(2 + Math.max(0, length - 1));
|
|
1572
|
+
const detached2 = clone ? [] : null;
|
|
1573
|
+
for (let index = 0;index < length; index += 1) {
|
|
1574
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
|
|
1575
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1576
|
+
throw new TypeError("Invalid data array entry.");
|
|
1577
|
+
}
|
|
1578
|
+
const item = detach(descriptor.value, depth + 1);
|
|
1579
|
+
detached2?.push(item);
|
|
1580
|
+
}
|
|
1581
|
+
return detached2 === null ? candidate : Object.freeze(detached2);
|
|
1582
|
+
}
|
|
1583
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
1584
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1585
|
+
throw new TypeError("Invalid data object.");
|
|
1586
|
+
}
|
|
1587
|
+
let containerBytes = 2;
|
|
1588
|
+
let properties = 0;
|
|
1589
|
+
for (const key in candidate) {
|
|
1590
|
+
if (!Object.hasOwn(candidate, key))
|
|
1591
|
+
continue;
|
|
1592
|
+
properties += 1;
|
|
1593
|
+
if (!canSpendNodes(properties))
|
|
1594
|
+
throw new RangeError("Sync ingress exceeds its node budget.");
|
|
1595
|
+
const remainingBytes = Math.min(budget.maximumBytes - budget.bytes - containerBytes - properties, aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes - containerBytes - properties);
|
|
1596
|
+
containerBytes += (properties === 1 ? 0 : 1) + canonicalStringBytesV1(key, remainingBytes) + 1;
|
|
1597
|
+
if (!canSpendBytes(containerBytes + properties)) {
|
|
1598
|
+
throw new RangeError("Sync ingress exceeds its canonical byte budget.");
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const keys = Reflect.ownKeys(candidate);
|
|
1602
|
+
if (keys.length !== properties || keys.some((key) => typeof key !== "string")) {
|
|
1603
|
+
throw new TypeError("Invalid data object.");
|
|
1604
|
+
}
|
|
1605
|
+
spendBytes(containerBytes);
|
|
1606
|
+
const detached = clone ? Object.create(null) : null;
|
|
1607
|
+
for (const key of keys) {
|
|
1608
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
|
|
1609
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1610
|
+
throw new TypeError("Invalid data property.");
|
|
1611
|
+
}
|
|
1612
|
+
const item = detach(descriptor.value, depth + 1);
|
|
1613
|
+
if (detached !== null) {
|
|
1614
|
+
Object.defineProperty(detached, key, {
|
|
1615
|
+
configurable: false,
|
|
1616
|
+
enumerable: true,
|
|
1617
|
+
value: item,
|
|
1618
|
+
writable: false
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return detached === null ? candidate : Object.freeze(detached);
|
|
1623
|
+
} finally {
|
|
1624
|
+
ancestors.delete(candidate);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
try {
|
|
1628
|
+
return Object.freeze({ value: detach(value, 0) });
|
|
1629
|
+
} catch {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function preflightSyncIngressValueV1(value, aggregate) {
|
|
1634
|
+
return boundedSyncIngressV1(value, {
|
|
1635
|
+
maximumBytes: OH_GRAPH_LIMITS_V1.recordBytes,
|
|
1636
|
+
maximumDepth: OH_SYNC_INGRESS_VALUE_DEPTH_V1,
|
|
1637
|
+
maximumNodes: OH_SYNC_INGRESS_VALUE_NODES_V1
|
|
1638
|
+
}, aggregate, false);
|
|
1639
|
+
}
|
|
1640
|
+
function preflightSyncIngressDependenciesV1(value, aggregate) {
|
|
1641
|
+
try {
|
|
1642
|
+
if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value)) {
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
|
|
1646
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
1647
|
+
return null;
|
|
1648
|
+
const maximumBytes = length === 0 ? 2 : 1 + length * 515;
|
|
1649
|
+
return boundedSyncIngressV1(value, {
|
|
1650
|
+
maximumBytes,
|
|
1651
|
+
maximumDepth: 1,
|
|
1652
|
+
maximumNodes: length + 1
|
|
1653
|
+
}, aggregate, false);
|
|
1654
|
+
} catch {
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function syncIngressBundleBudgetV1(spaceId) {
|
|
1659
|
+
const emptyBundle = {
|
|
1660
|
+
bundleSha256: "0".repeat(64),
|
|
1661
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
1662
|
+
operations: [],
|
|
1663
|
+
protocol: OH_SYNC_PROTOCOL_V1,
|
|
1664
|
+
spaceId,
|
|
1665
|
+
v: 1
|
|
1666
|
+
};
|
|
1667
|
+
return {
|
|
1668
|
+
bytes: utf8ByteLength(canonicalJson(emptyBundle)),
|
|
1669
|
+
maximumBytes: OH_SYNC_BUNDLE_MAX_BYTES_V1,
|
|
1670
|
+
maximumNodes: OH_SYNC_INGRESS_BUNDLE_NODES_V1,
|
|
1671
|
+
nodes: 7
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
function spendSyncIngressBudgetV1(budget, bytes, nodes = 0) {
|
|
1675
|
+
if (budget.bytes + bytes > budget.maximumBytes || budget.nodes + nodes > budget.maximumNodes)
|
|
1676
|
+
return false;
|
|
1677
|
+
budget.bytes += bytes;
|
|
1678
|
+
budget.nodes += nodes;
|
|
1679
|
+
return true;
|
|
1680
|
+
}
|
|
1681
|
+
function measureSyncOperationV1(operation) {
|
|
1682
|
+
const measurement = {
|
|
1683
|
+
bytes: 0,
|
|
1684
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1685
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
|
|
1686
|
+
nodes: 0
|
|
1687
|
+
};
|
|
1688
|
+
return boundedSyncIngressV1(operation, {
|
|
1689
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1690
|
+
maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
|
|
1691
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1
|
|
1692
|
+
}, measurement, false) === null ? null : measurement;
|
|
1693
|
+
}
|
|
1694
|
+
function buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix) {
|
|
1695
|
+
let priorSequence = null;
|
|
1696
|
+
let priorSha256 = null;
|
|
1697
|
+
const parsed = [];
|
|
1698
|
+
const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
|
|
1699
|
+
for (const candidate of operations) {
|
|
1700
|
+
const operation = parseOhOperationV1(candidate);
|
|
1701
|
+
if (operation === null || operation.spaceId !== parsedSpaceId || priorSequence !== null && operation.sequence !== priorSequence + 1 || priorSequence !== null && operation.parentOperationSha256 !== priorSha256) {
|
|
1702
|
+
throw new TypeError("Sync operations must form one ordered chain.");
|
|
1703
|
+
}
|
|
1704
|
+
const measurement = measureSyncOperationV1(operation);
|
|
1705
|
+
if (measurement === null) {
|
|
1706
|
+
throw new RangeError("Sync operation exceeds its canonical byte, node, or depth limit.");
|
|
1707
|
+
}
|
|
1708
|
+
if (!spendSyncIngressBudgetV1(bundleBudget, measurement.bytes + (parsed.length === 0 ? 0 : 1), measurement.nodes)) {
|
|
1709
|
+
if (largestFittingPrefix && parsed.length > 0)
|
|
1710
|
+
break;
|
|
1711
|
+
throw new RangeError("Sync bundle exceeds its canonical byte or node limit.");
|
|
1712
|
+
}
|
|
1713
|
+
parsed.push(operation);
|
|
1714
|
+
priorSequence = operation.sequence;
|
|
1715
|
+
priorSha256 = operation.operationSha256;
|
|
1716
|
+
}
|
|
1717
|
+
const payload = {
|
|
1718
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
1719
|
+
operations: parsed,
|
|
1720
|
+
protocol: OH_SYNC_PROTOCOL_V1,
|
|
1721
|
+
spaceId: parsedSpaceId,
|
|
1722
|
+
v: 1
|
|
1723
|
+
};
|
|
1724
|
+
return { ...payload, bundleSha256: canonicalSha256(payload) };
|
|
1725
|
+
}
|
|
1726
|
+
function parseSyncHeadRefEnvelopeV1(value) {
|
|
1727
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
1728
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 && !Object.is(value.sequence, -0) ? value.sequence : null;
|
|
1729
|
+
return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null;
|
|
1730
|
+
}
|
|
1731
|
+
function parseOhSyncHeadRefV1(value) {
|
|
1732
|
+
const reference = exactDataRecordV1(value, OH_SYNC_HEAD_REF_KEYS_V1);
|
|
1733
|
+
return reference === null ? null : parseSyncHeadRefEnvelopeV1(reference);
|
|
1734
|
+
}
|
|
1735
|
+
function parseOhSyncHeadV1(value) {
|
|
1736
|
+
const envelope = exactDataRecordV1(value, OH_SYNC_HEAD_KEYS_V1);
|
|
1737
|
+
if (envelope === null || envelope.v !== 1)
|
|
1738
|
+
return null;
|
|
1739
|
+
const reference = parseSyncHeadRefEnvelopeV1(envelope);
|
|
1740
|
+
return reference === null ? null : { ...reference, v: 1 };
|
|
1741
|
+
}
|
|
1742
|
+
function createOhSyncBundleV1(spaceId, operations, options = {}) {
|
|
1743
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
1744
|
+
const largestFittingPrefix = options.largestFittingPrefix ?? false;
|
|
1745
|
+
if (parsedSpaceId === null || operations.length > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 || typeof largestFittingPrefix !== "boolean") {
|
|
1746
|
+
throw new TypeError("Invalid sync bundle.");
|
|
1747
|
+
}
|
|
1748
|
+
return buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix);
|
|
1749
|
+
}
|
|
1750
|
+
function parseOhSyncBundleV1(value) {
|
|
1751
|
+
const envelope = exactDataRecordV1(value, OH_SYNC_BUNDLE_KEYS_V1);
|
|
1752
|
+
if (envelope === null || envelope.protocol !== OH_SYNC_PROTOCOL_V1 || envelope.v !== 1)
|
|
1753
|
+
return null;
|
|
1754
|
+
const bundleSha256 = parseSha256Hex(envelope.bundleSha256);
|
|
1755
|
+
const contractSha256 = parseSha256Hex(envelope.contractSha256);
|
|
1756
|
+
const spaceId = safeCode(envelope.spaceId);
|
|
1757
|
+
if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || spaceId === null)
|
|
1758
|
+
return null;
|
|
1759
|
+
const operations = exactDataArrayV1(envelope.operations, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1);
|
|
1760
|
+
if (operations === null)
|
|
1761
|
+
return null;
|
|
1762
|
+
const detachedOperations = [];
|
|
1763
|
+
const bundleBudget = syncIngressBundleBudgetV1(spaceId);
|
|
1764
|
+
for (const operation of operations) {
|
|
1765
|
+
if (detachedOperations.length > 0 && !spendSyncIngressBudgetV1(bundleBudget, 1))
|
|
1766
|
+
return null;
|
|
1767
|
+
const detached = stagedSyncOperationV1(operation, bundleBudget);
|
|
1768
|
+
if (detached === null)
|
|
1769
|
+
return null;
|
|
1770
|
+
detachedOperations.push(detached);
|
|
1771
|
+
}
|
|
1772
|
+
try {
|
|
1773
|
+
const created = createOhSyncBundleV1(spaceId, detachedOperations);
|
|
1774
|
+
if (detachedOperations.some((operation, index) => canonicalJson(operation) !== canonicalJson(created.operations[index])))
|
|
1775
|
+
return null;
|
|
1776
|
+
return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
|
|
1777
|
+
} catch {
|
|
1778
|
+
return null;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
async function synchronizeOhStoreV1(store, transport, options = {}) {
|
|
1782
|
+
const batchSize = options.batchSize ?? 100;
|
|
1783
|
+
const maximumRounds = options.maximumRounds ?? 100;
|
|
1784
|
+
const remoteId = safeCode(options.remoteId ?? "default");
|
|
1785
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1000 || !Number.isSafeInteger(maximumRounds) || maximumRounds < 1 || maximumRounds > 1e4 || remoteId === null)
|
|
1786
|
+
throw new TypeError("Invalid sync options.");
|
|
1787
|
+
await transport.handshake(OH_CONTRACT_MANIFEST_V1);
|
|
1788
|
+
let pulled = 0;
|
|
1789
|
+
let pushed = 0;
|
|
1790
|
+
const settled = (head, rounds) => {
|
|
1791
|
+
store.updateSyncState(remoteId, {
|
|
1792
|
+
pulledSequence: head.sequence,
|
|
1793
|
+
pushedSequence: head.sequence,
|
|
1794
|
+
remoteHeadSha256: head.operationSha256
|
|
1795
|
+
});
|
|
1796
|
+
return { head, pulled, pushed, rounds, v: 1 };
|
|
1797
|
+
};
|
|
1798
|
+
for (let round = 1;round <= maximumRounds; round += 1) {
|
|
1799
|
+
const remote = parseOhSyncHeadV1(await transport.head(store.spaceId));
|
|
1800
|
+
if (remote === null)
|
|
1801
|
+
throw new Error("The sync transport returned an invalid head.");
|
|
1802
|
+
const local = store.head();
|
|
1803
|
+
if (local.sequence === remote.sequence) {
|
|
1804
|
+
if (local.operationSha256 !== remote.operationSha256) {
|
|
1805
|
+
throw new Error("Sync conflict: equal sequence numbers have different heads.");
|
|
1806
|
+
}
|
|
1807
|
+
return settled(remote, round);
|
|
1808
|
+
}
|
|
1809
|
+
if (local.sequence < remote.sequence) {
|
|
1810
|
+
const bundle = parseOhSyncBundleV1(await transport.pull(store.spaceId, local.sequence, batchSize));
|
|
1811
|
+
const terminal = bundle?.operations.at(-1);
|
|
1812
|
+
if (bundle === null || bundle.operations.length === 0 || bundle.operations.length > batchSize || bundle.spaceId !== store.spaceId || bundle.operations[0]?.sequence !== local.sequence + 1 || bundle.operations[0]?.parentOperationSha256 !== local.operationSha256 || terminal === undefined || terminal.sequence > remote.sequence || terminal.sequence === remote.sequence && terminal.operationSha256 !== remote.operationSha256) {
|
|
1813
|
+
throw new Error("Sync conflict: remote history does not extend the local head.");
|
|
1814
|
+
}
|
|
1815
|
+
store.importOperations({
|
|
1816
|
+
expectedHead: {
|
|
1817
|
+
operationSha256: local.operationSha256,
|
|
1818
|
+
sequence: local.sequence
|
|
1819
|
+
},
|
|
1820
|
+
operations: bundle.operations
|
|
1821
|
+
});
|
|
1822
|
+
pulled += bundle.operations.length;
|
|
1823
|
+
const afterPull = store.head();
|
|
1824
|
+
if (afterPull.sequence === remote.sequence && afterPull.operationSha256 === remote.operationSha256) {
|
|
1825
|
+
const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
|
|
1826
|
+
if (confirmed === null)
|
|
1827
|
+
throw new Error("The sync transport returned an invalid head.");
|
|
1828
|
+
const confirmedLocal = store.head();
|
|
1829
|
+
if (confirmed.sequence === remote.sequence && confirmed.operationSha256 === remote.operationSha256 && confirmedLocal.sequence === confirmed.sequence && confirmedLocal.operationSha256 === confirmed.operationSha256) {
|
|
1830
|
+
return settled(confirmed, round);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
} else {
|
|
1834
|
+
const candidates = store.changesSince({
|
|
1835
|
+
operationSha256: remote.operationSha256,
|
|
1836
|
+
sequence: remote.sequence
|
|
1837
|
+
}, {
|
|
1838
|
+
limit: batchSize,
|
|
1839
|
+
through: {
|
|
1840
|
+
operationSha256: local.operationSha256,
|
|
1841
|
+
sequence: local.sequence
|
|
1842
|
+
}
|
|
1843
|
+
}).operations;
|
|
1844
|
+
if (candidates.length === 0 || candidates[0]?.sequence !== remote.sequence + 1 || candidates[0]?.parentOperationSha256 !== remote.operationSha256) {
|
|
1845
|
+
throw new Error("Sync conflict: local history does not extend the remote head.");
|
|
1846
|
+
}
|
|
1847
|
+
const bundle = createOhSyncBundleV1(store.spaceId, candidates, {
|
|
1848
|
+
largestFittingPrefix: true
|
|
1849
|
+
});
|
|
1850
|
+
const operations = bundle.operations;
|
|
1851
|
+
const head = parseOhSyncHeadV1(await transport.push(bundle));
|
|
1852
|
+
if (head === null)
|
|
1853
|
+
throw new Error("The sync transport returned an invalid push head.");
|
|
1854
|
+
if (head.sequence !== operations.at(-1)?.sequence || head.operationSha256 !== operations.at(-1)?.operationSha256) {
|
|
1855
|
+
throw new Error("The sync transport acknowledged a different head.");
|
|
1856
|
+
}
|
|
1857
|
+
pushed += operations.length;
|
|
1858
|
+
const afterPush = store.head();
|
|
1859
|
+
if (afterPush.sequence === head.sequence && afterPush.operationSha256 === head.operationSha256) {
|
|
1860
|
+
const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
|
|
1861
|
+
if (confirmed === null)
|
|
1862
|
+
throw new Error("The sync transport returned an invalid head.");
|
|
1863
|
+
const confirmedLocal = store.head();
|
|
1864
|
+
if (confirmed.sequence === head.sequence && confirmed.operationSha256 === head.operationSha256 && confirmedLocal.sequence === confirmed.sequence && confirmedLocal.operationSha256 === confirmed.operationSha256) {
|
|
1865
|
+
return settled(confirmed, round);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1460
1869
|
}
|
|
1870
|
+
throw new Error("Sync did not settle within maximumRounds.");
|
|
1871
|
+
}
|
|
1872
|
+
function rowValue(row, key, index) {
|
|
1873
|
+
return Array.isArray(row) ? row[index] : row[key];
|
|
1874
|
+
}
|
|
1875
|
+
function createLibSqlOperationSyncTransportV1(client) {
|
|
1876
|
+
let ready = null;
|
|
1877
|
+
const setup = async (manifest) => {
|
|
1878
|
+
if (parseOhContractManifestV1(manifest) === null)
|
|
1879
|
+
throw new Error("Unsupported contract manifest.");
|
|
1880
|
+
await client.batch([
|
|
1881
|
+
{ sql: `CREATE TABLE IF NOT EXISTS oh_sync_contracts (
|
|
1882
|
+
contract_id TEXT PRIMARY KEY, contract_sha256 TEXT NOT NULL, manifest_json TEXT NOT NULL
|
|
1883
|
+
) STRICT` },
|
|
1884
|
+
{ sql: `CREATE TABLE IF NOT EXISTS oh_sync_operations (
|
|
1885
|
+
space_id TEXT NOT NULL, sequence INTEGER NOT NULL, operation_sha256 TEXT NOT NULL UNIQUE,
|
|
1886
|
+
operation_json TEXT NOT NULL, PRIMARY KEY(space_id, sequence)
|
|
1887
|
+
) STRICT` },
|
|
1888
|
+
{
|
|
1889
|
+
sql: "INSERT INTO oh_sync_contracts(contract_id, contract_sha256, manifest_json) VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING",
|
|
1890
|
+
args: [manifest.contractId, manifest.contractSha256, canonicalJson(manifest)]
|
|
1891
|
+
}
|
|
1892
|
+
], "write");
|
|
1893
|
+
const result = await client.execute({
|
|
1894
|
+
sql: "SELECT contract_sha256, manifest_json FROM oh_sync_contracts WHERE contract_id = ?",
|
|
1895
|
+
args: [manifest.contractId]
|
|
1896
|
+
});
|
|
1897
|
+
const row = result.rows[0];
|
|
1898
|
+
if (row === undefined || rowValue(row, "contract_sha256", 0) !== manifest.contractSha256 || rowValue(row, "manifest_json", 1) !== canonicalJson(manifest)) {
|
|
1899
|
+
throw new Error("Remote contract manifest mismatch.");
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
const ensure = (manifest = OH_CONTRACT_MANIFEST_V1) => {
|
|
1903
|
+
ready ??= setup(manifest).catch((error) => {
|
|
1904
|
+
ready = null;
|
|
1905
|
+
throw error;
|
|
1906
|
+
});
|
|
1907
|
+
return ready;
|
|
1908
|
+
};
|
|
1909
|
+
const head = async (spaceId) => {
|
|
1910
|
+
await ensure();
|
|
1911
|
+
const result = await client.execute({ sql: `SELECT sequence, operation_sha256 FROM oh_sync_operations
|
|
1912
|
+
WHERE space_id = ? ORDER BY sequence DESC LIMIT 1`, args: [spaceId] });
|
|
1913
|
+
const row = result.rows[0];
|
|
1914
|
+
if (row === undefined)
|
|
1915
|
+
return { operationSha256: null, sequence: 0, v: 1 };
|
|
1916
|
+
const sequence = Number(rowValue(row, "sequence", 0));
|
|
1917
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
|
|
1918
|
+
if (!Number.isSafeInteger(sequence) || sequence < 1 || operationSha256 === null)
|
|
1919
|
+
throw new Error("Invalid remote head.");
|
|
1920
|
+
return { operationSha256, sequence, v: 1 };
|
|
1921
|
+
};
|
|
1922
|
+
return {
|
|
1923
|
+
handshake: async (manifest) => {
|
|
1924
|
+
const parsed = parseOhContractManifestV1(manifest);
|
|
1925
|
+
if (parsed === null)
|
|
1926
|
+
throw new Error("Unsupported contract manifest.");
|
|
1927
|
+
await ensure(parsed);
|
|
1928
|
+
},
|
|
1929
|
+
head,
|
|
1930
|
+
pull: async (spaceId, afterSequence, limit) => {
|
|
1931
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
1932
|
+
if (parsedSpaceId === null || !Number.isSafeInteger(afterSequence) || afterSequence < 0 || Object.is(afterSequence, -0) || !Number.isSafeInteger(limit) || limit < 1 || limit > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1) {
|
|
1933
|
+
throw new TypeError("Invalid sync pull request.");
|
|
1934
|
+
}
|
|
1935
|
+
await ensure();
|
|
1936
|
+
const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
|
|
1937
|
+
const result = await client.execute({
|
|
1938
|
+
sql: `SELECT operation_json FROM (
|
|
1939
|
+
SELECT sequence, operation_json,
|
|
1940
|
+
row_number() OVER (ORDER BY sequence) AS ordinal,
|
|
1941
|
+
sum(length(CAST(operation_json AS BLOB))) OVER (
|
|
1942
|
+
ORDER BY sequence ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
|
1943
|
+
) AS cumulative_bytes
|
|
1944
|
+
FROM (
|
|
1945
|
+
SELECT sequence, operation_json FROM oh_sync_operations
|
|
1946
|
+
WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?
|
|
1947
|
+
)
|
|
1948
|
+
) WHERE ordinal = 1 OR cumulative_bytes + ordinal - 1 <= ? ORDER BY sequence`,
|
|
1949
|
+
args: [
|
|
1950
|
+
parsedSpaceId,
|
|
1951
|
+
afterSequence,
|
|
1952
|
+
limit,
|
|
1953
|
+
bundleBudget.maximumBytes - bundleBudget.bytes
|
|
1954
|
+
]
|
|
1955
|
+
});
|
|
1956
|
+
const rows = result.rows;
|
|
1957
|
+
if (!Array.isArray(rows) || rows.length > limit) {
|
|
1958
|
+
throw new Error("Remote sync pull exceeded its requested row limit.");
|
|
1959
|
+
}
|
|
1960
|
+
const operations = rows.map((row) => {
|
|
1961
|
+
const json = rowValue(row, "operation_json", 0);
|
|
1962
|
+
if (typeof json !== "string")
|
|
1963
|
+
throw new Error("Invalid remote operation JSON.");
|
|
1964
|
+
if (utf8ByteLength(json) > OH_OPERATION_MAX_BYTES_V1) {
|
|
1965
|
+
throw new Error("Remote operation JSON exceeds the canonical operation byte limit.");
|
|
1966
|
+
}
|
|
1967
|
+
const operation = parseOhOperationV1(JSON.parse(json));
|
|
1968
|
+
if (operation === null || canonicalJson(operation) !== json)
|
|
1969
|
+
throw new Error("Invalid remote operation.");
|
|
1970
|
+
return operation;
|
|
1971
|
+
});
|
|
1972
|
+
return createOhSyncBundleV1(parsedSpaceId, operations);
|
|
1973
|
+
},
|
|
1974
|
+
push: async (value) => {
|
|
1975
|
+
await ensure();
|
|
1976
|
+
const bundle = parseOhSyncBundleV1(value);
|
|
1977
|
+
if (bundle === null)
|
|
1978
|
+
throw new Error("Invalid outgoing sync bundle.");
|
|
1979
|
+
if (bundle.operations.length === 0)
|
|
1980
|
+
return head(bundle.spaceId);
|
|
1981
|
+
const remote = await head(bundle.spaceId);
|
|
1982
|
+
const first = bundle.operations[0];
|
|
1983
|
+
const last = bundle.operations.at(-1);
|
|
1984
|
+
if (remote.sequence >= last.sequence) {
|
|
1985
|
+
const result = await client.execute({ sql: `SELECT sequence, operation_sha256, operation_json
|
|
1986
|
+
FROM oh_sync_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ?
|
|
1987
|
+
ORDER BY sequence LIMIT ?`, args: [
|
|
1988
|
+
bundle.spaceId,
|
|
1989
|
+
first.sequence,
|
|
1990
|
+
last.sequence,
|
|
1991
|
+
bundle.operations.length
|
|
1992
|
+
] });
|
|
1993
|
+
const rows = result.rows;
|
|
1994
|
+
if (!Array.isArray(rows) || rows.length !== bundle.operations.length) {
|
|
1995
|
+
throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
|
|
1996
|
+
}
|
|
1997
|
+
for (let index = 0;index < bundle.operations.length; index += 1) {
|
|
1998
|
+
const operation = bundle.operations[index];
|
|
1999
|
+
const row = rows[index];
|
|
2000
|
+
if (row === undefined) {
|
|
2001
|
+
throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
|
|
2002
|
+
}
|
|
2003
|
+
const sequence = Number(rowValue(row, "sequence", 0));
|
|
2004
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
|
|
2005
|
+
const operationJson = rowValue(row, "operation_json", 2);
|
|
2006
|
+
const expectedJson = canonicalJson(operation);
|
|
2007
|
+
if (sequence !== operation.sequence || operationSha256 !== operation.operationSha256 || typeof operationJson !== "string" || utf8ByteLength(operationJson) > OH_OPERATION_MAX_BYTES_V1 || operationJson !== expectedJson) {
|
|
2008
|
+
throw new Error("Sync conflict: remote history differs from the pushed operations.");
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
|
|
2012
|
+
}
|
|
2013
|
+
if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
|
|
2014
|
+
throw new Error("Sync conflict: pushed history does not extend the remote head.");
|
|
2015
|
+
}
|
|
2016
|
+
await client.batch(bundle.operations.map((operation) => ({
|
|
2017
|
+
sql: "INSERT INTO oh_sync_operations(space_id, sequence, operation_sha256, operation_json) VALUES (?, ?, ?, ?)",
|
|
2018
|
+
args: [bundle.spaceId, operation.sequence, operation.operationSha256, canonicalJson(operation)]
|
|
2019
|
+
})), "write");
|
|
2020
|
+
return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/sqlite/driver.ts
|
|
2026
|
+
import { existsSync } from "fs";
|
|
2027
|
+
import { Database } from "bun:sqlite";
|
|
2028
|
+
|
|
2029
|
+
// src/sqlite/runtime.ts
|
|
2030
|
+
var MACOS_SQLITE_LIBRARY_CANDIDATES = Object.freeze([
|
|
2031
|
+
"/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib",
|
|
2032
|
+
"/usr/local/opt/sqlite/lib/libsqlite3.dylib"
|
|
2033
|
+
]);
|
|
2034
|
+
function macosSqliteLibraryCandidates() {
|
|
2035
|
+
return MACOS_SQLITE_LIBRARY_CANDIDATES;
|
|
2036
|
+
}
|
|
2037
|
+
function createOhSqliteRuntime(dependencies) {
|
|
2038
|
+
let customLibrary = null;
|
|
2039
|
+
if (dependencies.platform === "darwin") {
|
|
2040
|
+
for (const candidate of macosSqliteLibraryCandidates()) {
|
|
2041
|
+
if (!dependencies.exists(candidate))
|
|
2042
|
+
continue;
|
|
2043
|
+
try {
|
|
2044
|
+
if (!dependencies.setCustomSQLite(candidate))
|
|
2045
|
+
continue;
|
|
2046
|
+
customLibrary = candidate;
|
|
2047
|
+
break;
|
|
2048
|
+
} catch {}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
return Object.freeze({
|
|
2052
|
+
customLibrary,
|
|
2053
|
+
open: (path) => dependencies.open(path, { create: true, strict: true })
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// src/sqlite/driver.ts
|
|
2058
|
+
var SQLITE_RUNTIME = createOhSqliteRuntime({
|
|
2059
|
+
exists: existsSync,
|
|
2060
|
+
open: (path, options) => new Database(path, options),
|
|
2061
|
+
platform: process.platform,
|
|
2062
|
+
setCustomSQLite: (path) => Database.setCustomSQLite(path)
|
|
2063
|
+
});
|
|
2064
|
+
function openOhSqliteDatabase(path) {
|
|
2065
|
+
const database = SQLITE_RUNTIME.open(path);
|
|
2066
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
2067
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
2068
|
+
database.exec("PRAGMA synchronous = NORMAL");
|
|
2069
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
2070
|
+
database.exec("PRAGMA trusted_schema = OFF");
|
|
2071
|
+
return database;
|
|
1461
2072
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
2073
|
+
function withImmediateTransaction(database, work) {
|
|
2074
|
+
database.exec("BEGIN IMMEDIATE");
|
|
2075
|
+
try {
|
|
2076
|
+
const result = work();
|
|
2077
|
+
database.exec("COMMIT");
|
|
2078
|
+
return result;
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
try {
|
|
2081
|
+
database.exec("ROLLBACK");
|
|
2082
|
+
} catch {}
|
|
2083
|
+
throw error;
|
|
1467
2084
|
}
|
|
1468
2085
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
2086
|
+
function withReadTransaction(database, work) {
|
|
2087
|
+
database.exec("BEGIN");
|
|
2088
|
+
try {
|
|
2089
|
+
const result = work();
|
|
2090
|
+
database.exec("COMMIT");
|
|
2091
|
+
return result;
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
try {
|
|
2094
|
+
database.exec("ROLLBACK");
|
|
2095
|
+
} catch {}
|
|
2096
|
+
throw error;
|
|
1474
2097
|
}
|
|
1475
2098
|
}
|
|
2099
|
+
// src/sqlite/migrations.ts
|
|
2100
|
+
var OH_SQLITE_SCHEMA_VERSION = 2;
|
|
2101
|
+
var OH_SQLITE_MIGRATIONS = Object.freeze([
|
|
2102
|
+
Object.freeze({
|
|
2103
|
+
name: "0001_oh_core",
|
|
2104
|
+
version: 1,
|
|
2105
|
+
sql: `
|
|
2106
|
+
CREATE TABLE oh_contracts (
|
|
2107
|
+
contract_id TEXT PRIMARY KEY,
|
|
2108
|
+
contract_sha256 TEXT NOT NULL CHECK(length(contract_sha256) = 64),
|
|
2109
|
+
manifest_json TEXT NOT NULL CHECK(json_valid(manifest_json)),
|
|
2110
|
+
created_at TEXT NOT NULL
|
|
2111
|
+
) STRICT;
|
|
1476
2112
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
2113
|
+
CREATE TABLE oh_spaces (
|
|
2114
|
+
space_id TEXT PRIMARY KEY,
|
|
2115
|
+
contract_id TEXT NOT NULL REFERENCES oh_contracts(contract_id),
|
|
2116
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
2117
|
+
head_operation_sha256 TEXT CHECK(head_operation_sha256 IS NULL OR length(head_operation_sha256) = 64),
|
|
2118
|
+
graph_revision_sha256 TEXT CHECK(graph_revision_sha256 IS NULL OR length(graph_revision_sha256) = 64),
|
|
2119
|
+
records_sha256 TEXT NOT NULL CHECK(length(records_sha256) = 64),
|
|
2120
|
+
sequence INTEGER NOT NULL CHECK(sequence >= 0),
|
|
2121
|
+
created_at TEXT NOT NULL,
|
|
2122
|
+
updated_at TEXT NOT NULL,
|
|
2123
|
+
CHECK(generation = sequence),
|
|
2124
|
+
CHECK((sequence = 0) = (head_operation_sha256 IS NULL)),
|
|
2125
|
+
CHECK((sequence = 0) = (graph_revision_sha256 IS NULL))
|
|
2126
|
+
) STRICT;
|
|
2127
|
+
|
|
2128
|
+
CREATE TABLE oh_operations (
|
|
2129
|
+
operation_sha256 TEXT PRIMARY KEY CHECK(length(operation_sha256) = 64),
|
|
2130
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2131
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
2132
|
+
operation_id TEXT NOT NULL,
|
|
2133
|
+
parent_operation_sha256 TEXT CHECK(parent_operation_sha256 IS NULL OR length(parent_operation_sha256) = 64),
|
|
2134
|
+
graph_revision_sha256 TEXT NOT NULL CHECK(length(graph_revision_sha256) = 64),
|
|
2135
|
+
records_sha256 TEXT NOT NULL CHECK(length(records_sha256) = 64),
|
|
2136
|
+
operation_json TEXT NOT NULL CHECK(json_valid(operation_json)),
|
|
2137
|
+
instant TEXT NOT NULL,
|
|
2138
|
+
UNIQUE(space_id, sequence),
|
|
2139
|
+
UNIQUE(space_id, operation_id)
|
|
2140
|
+
) STRICT;
|
|
2141
|
+
|
|
2142
|
+
CREATE TABLE oh_operation_records (
|
|
2143
|
+
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
2144
|
+
ordinal INTEGER NOT NULL CHECK(ordinal >= 0),
|
|
2145
|
+
record_key TEXT NOT NULL,
|
|
2146
|
+
change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')),
|
|
2147
|
+
record_sha256 TEXT CHECK(record_sha256 IS NULL OR length(record_sha256) = 64),
|
|
2148
|
+
PRIMARY KEY(operation_sha256, ordinal),
|
|
2149
|
+
UNIQUE(operation_sha256, record_key)
|
|
2150
|
+
) STRICT;
|
|
2151
|
+
|
|
2152
|
+
CREATE TABLE oh_records (
|
|
2153
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2154
|
+
record_key TEXT NOT NULL,
|
|
2155
|
+
kind TEXT NOT NULL,
|
|
2156
|
+
record_sha256 TEXT NOT NULL CHECK(length(record_sha256) = 64),
|
|
2157
|
+
record_json TEXT NOT NULL CHECK(json_valid(record_json)),
|
|
2158
|
+
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
2159
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
2160
|
+
PRIMARY KEY(space_id, record_key)
|
|
2161
|
+
) STRICT;
|
|
2162
|
+
|
|
2163
|
+
CREATE TABLE oh_dependencies (
|
|
2164
|
+
space_id TEXT NOT NULL,
|
|
2165
|
+
record_key TEXT NOT NULL,
|
|
2166
|
+
dependency_key TEXT NOT NULL,
|
|
2167
|
+
PRIMARY KEY(space_id, record_key, dependency_key),
|
|
2168
|
+
FOREIGN KEY(space_id, record_key) REFERENCES oh_records(space_id, record_key) ON DELETE CASCADE,
|
|
2169
|
+
FOREIGN KEY(space_id, dependency_key) REFERENCES oh_records(space_id, record_key)
|
|
2170
|
+
) STRICT;
|
|
2171
|
+
|
|
2172
|
+
CREATE TABLE oh_sync_outbox (
|
|
2173
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2174
|
+
sequence INTEGER NOT NULL,
|
|
2175
|
+
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
2176
|
+
PRIMARY KEY(space_id, sequence),
|
|
2177
|
+
UNIQUE(operation_sha256)
|
|
2178
|
+
) STRICT;
|
|
2179
|
+
|
|
2180
|
+
CREATE TABLE oh_sync_state (
|
|
2181
|
+
remote_id TEXT NOT NULL,
|
|
2182
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2183
|
+
pulled_sequence INTEGER NOT NULL CHECK(pulled_sequence >= 0),
|
|
2184
|
+
pushed_sequence INTEGER NOT NULL CHECK(pushed_sequence >= 0),
|
|
2185
|
+
remote_head_sha256 TEXT CHECK(remote_head_sha256 IS NULL OR length(remote_head_sha256) = 64),
|
|
2186
|
+
updated_at TEXT NOT NULL,
|
|
2187
|
+
PRIMARY KEY(remote_id, space_id)
|
|
2188
|
+
) STRICT;
|
|
2189
|
+
|
|
2190
|
+
CREATE TABLE oh_search_documents (
|
|
2191
|
+
space_id TEXT NOT NULL,
|
|
2192
|
+
record_key TEXT NOT NULL,
|
|
2193
|
+
record_sha256 TEXT NOT NULL CHECK(length(record_sha256) = 64),
|
|
2194
|
+
text TEXT NOT NULL,
|
|
2195
|
+
PRIMARY KEY(space_id, record_key),
|
|
2196
|
+
FOREIGN KEY(space_id, record_key) REFERENCES oh_records(space_id, record_key) ON DELETE CASCADE
|
|
2197
|
+
) STRICT;
|
|
2198
|
+
|
|
2199
|
+
CREATE VIRTUAL TABLE oh_search_fts USING fts5(
|
|
2200
|
+
space_id UNINDEXED,
|
|
2201
|
+
record_key UNINDEXED,
|
|
2202
|
+
text,
|
|
2203
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
CREATE INDEX oh_operations_space_sequence ON oh_operations(space_id, sequence);
|
|
2207
|
+
CREATE INDEX oh_records_space_kind ON oh_records(space_id, kind, record_key);
|
|
2208
|
+
CREATE INDEX oh_dependencies_dependency ON oh_dependencies(space_id, dependency_key);
|
|
2209
|
+
`
|
|
2210
|
+
}),
|
|
2211
|
+
Object.freeze({
|
|
2212
|
+
name: "0002_store_realms",
|
|
2213
|
+
version: 2,
|
|
2214
|
+
sql: `
|
|
2215
|
+
CREATE TABLE oh_space_bindings (
|
|
2216
|
+
space_id TEXT PRIMARY KEY REFERENCES oh_spaces(space_id),
|
|
2217
|
+
realm_id TEXT NOT NULL,
|
|
2218
|
+
profile_id TEXT NOT NULL,
|
|
2219
|
+
profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')),
|
|
2220
|
+
profile_sha256 TEXT NOT NULL CHECK(length(profile_sha256) = 64),
|
|
2221
|
+
binding_sha256 TEXT NOT NULL UNIQUE CHECK(length(binding_sha256) = 64),
|
|
2222
|
+
binding_json TEXT NOT NULL CHECK(json_valid(binding_json)),
|
|
2223
|
+
created_at TEXT NOT NULL
|
|
2224
|
+
) STRICT;
|
|
2225
|
+
|
|
2226
|
+
CREATE TABLE oh_space_purges (
|
|
2227
|
+
space_id TEXT PRIMARY KEY,
|
|
2228
|
+
binding_sha256 TEXT NOT NULL CHECK(length(binding_sha256) = 64),
|
|
2229
|
+
prior_operation_sha256 TEXT CHECK(prior_operation_sha256 IS NULL OR length(prior_operation_sha256) = 64),
|
|
2230
|
+
prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0),
|
|
2231
|
+
purged_at TEXT NOT NULL,
|
|
2232
|
+
receipt_sha256 TEXT NOT NULL UNIQUE CHECK(length(receipt_sha256) = 64),
|
|
2233
|
+
receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json))
|
|
2234
|
+
) STRICT;
|
|
2235
|
+
`
|
|
2236
|
+
})
|
|
2237
|
+
]);
|
|
2238
|
+
function applyOhSqliteMigrations(database) {
|
|
2239
|
+
database.exec(`CREATE TABLE IF NOT EXISTS oh_migrations (
|
|
2240
|
+
version INTEGER PRIMARY KEY,
|
|
2241
|
+
name TEXT NOT NULL UNIQUE,
|
|
2242
|
+
migration_sha256 TEXT NOT NULL CHECK(length(migration_sha256) = 64),
|
|
2243
|
+
applied_at TEXT NOT NULL
|
|
2244
|
+
) STRICT`);
|
|
2245
|
+
const select = database.query("SELECT name, migration_sha256 FROM oh_migrations WHERE version = ?");
|
|
2246
|
+
const insert = database.query("INSERT INTO oh_migrations(version, name, migration_sha256, applied_at) VALUES (?, ?, ?, ?)");
|
|
2247
|
+
for (const migration of OH_SQLITE_MIGRATIONS) {
|
|
2248
|
+
const digest = sha256Hex(migration.sql);
|
|
2249
|
+
database.exec("BEGIN IMMEDIATE");
|
|
2250
|
+
try {
|
|
2251
|
+
const existing = select.get(migration.version);
|
|
2252
|
+
if (existing !== null) {
|
|
2253
|
+
if (existing.name !== migration.name || existing.migration_sha256 !== digest) {
|
|
2254
|
+
throw new Error(`SQLite migration ${migration.version} does not match the applied migration.`);
|
|
2255
|
+
}
|
|
2256
|
+
} else {
|
|
2257
|
+
database.exec(migration.sql);
|
|
2258
|
+
insert.run(migration.version, migration.name, digest, canonicalNow());
|
|
2259
|
+
}
|
|
2260
|
+
database.exec("COMMIT");
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
try {
|
|
2263
|
+
database.exec("ROLLBACK");
|
|
2264
|
+
} catch {}
|
|
2265
|
+
throw error;
|
|
2266
|
+
}
|
|
1481
2267
|
}
|
|
1482
2268
|
}
|
|
2269
|
+
// src/sqlite/port.ts
|
|
2270
|
+
import { isProxy as isProxy2 } from "util/types";
|
|
2271
|
+
// src/store.ts
|
|
1483
2272
|
var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
1484
2273
|
applicationProfileSha256: null,
|
|
1485
2274
|
capabilities: {
|
|
@@ -1792,6 +2581,8 @@ function transitionOhSnapshotV1(input) {
|
|
|
1792
2581
|
sequence: head.sequence + 1,
|
|
1793
2582
|
spaceId,
|
|
1794
2583
|
v: 1
|
|
2584
|
+
}, input.maximumOperationBytes === undefined ? {} : {
|
|
2585
|
+
maximumOperationBytes: input.maximumOperationBytes
|
|
1795
2586
|
});
|
|
1796
2587
|
const nextHead = {
|
|
1797
2588
|
generation: operation.sequence,
|
|
@@ -2123,6 +2914,9 @@ function normalizeLimit(value, fallback = 50, maximum = 1000) {
|
|
|
2123
2914
|
}
|
|
2124
2915
|
return value;
|
|
2125
2916
|
}
|
|
2917
|
+
function exactHeadRef(left, right) {
|
|
2918
|
+
return left.sequence === right.sequence && left.operationSha256 === right.operationSha256;
|
|
2919
|
+
}
|
|
2126
2920
|
function ftsQuery(value) {
|
|
2127
2921
|
const normalized = boundedText(value.normalize("NFC"), 4096);
|
|
2128
2922
|
if (normalized === null)
|
|
@@ -2239,8 +3033,7 @@ class OhSqliteStore {
|
|
|
2239
3033
|
}
|
|
2240
3034
|
return records;
|
|
2241
3035
|
}
|
|
2242
|
-
#transition(head, changes, operationId) {
|
|
2243
|
-
const records = this.#loadRecords();
|
|
3036
|
+
#transition(head, changes, operationId, records = this.#loadRecords()) {
|
|
2244
3037
|
for (const change of changes) {
|
|
2245
3038
|
if (change.kind === "put") {
|
|
2246
3039
|
records.set(change.record.key, change.record);
|
|
@@ -2297,7 +3090,7 @@ class OhSqliteStore {
|
|
|
2297
3090
|
if (operation.sequence < 1 || operation.sequence > head.sequence) {
|
|
2298
3091
|
throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head.");
|
|
2299
3092
|
}
|
|
2300
|
-
const rows = this.database.query(`SELECT
|
|
3093
|
+
const rows = this.database.query(`SELECT ${OPERATION_COLUMNS}
|
|
2301
3094
|
FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence);
|
|
2302
3095
|
if (rows.length !== head.sequence - operation.sequence + 1) {
|
|
2303
3096
|
throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head.");
|
|
@@ -2305,10 +3098,14 @@ class OhSqliteStore {
|
|
|
2305
3098
|
let priorSha256 = operation.parentOperationSha256;
|
|
2306
3099
|
for (let index = 0;index < rows.length; index += 1) {
|
|
2307
3100
|
const row = rows[index];
|
|
2308
|
-
if (row === undefined
|
|
3101
|
+
if (row === undefined) {
|
|
3102
|
+
throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
|
|
3103
|
+
}
|
|
3104
|
+
const reachable = parseStoredOperationRow(row, { spaceId: this.spaceId });
|
|
3105
|
+
if (reachable.sequence !== operation.sequence + index || reachable.parentOperationSha256 !== priorSha256 || index === 0 && reachable.operationSha256 !== operation.operationSha256) {
|
|
2309
3106
|
throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
|
|
2310
3107
|
}
|
|
2311
|
-
priorSha256 =
|
|
3108
|
+
priorSha256 = reachable.operationSha256;
|
|
2312
3109
|
}
|
|
2313
3110
|
if (priorSha256 !== head.operationSha256) {
|
|
2314
3111
|
throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest.");
|
|
@@ -2369,8 +3166,10 @@ class OhSqliteStore {
|
|
|
2369
3166
|
this.#assertOpen();
|
|
2370
3167
|
const actorId = safeCode(input.actorId);
|
|
2371
3168
|
const operationId = safeCode(input.operationId);
|
|
2372
|
-
|
|
2373
|
-
|
|
3169
|
+
const maximumOperationBytes = input.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
|
|
3170
|
+
if (actorId === null || operationId === null || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
3171
|
+
throw new TypeError("Invalid actor, operation ID, or operation byte bound.");
|
|
3172
|
+
}
|
|
2374
3173
|
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
2375
3174
|
if (changes.length === 0 || changes.length > 8192)
|
|
2376
3175
|
throw new TypeError("A commit needs 1 through 8192 changes.");
|
|
@@ -2384,6 +3183,10 @@ class OhSqliteStore {
|
|
|
2384
3183
|
if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) {
|
|
2385
3184
|
throw new OhConflictError("The operation ID is already bound to different content.");
|
|
2386
3185
|
}
|
|
3186
|
+
const operationBytes = Buffer.byteLength(canonicalJson(existing), "utf8");
|
|
3187
|
+
if (operationBytes > maximumOperationBytes) {
|
|
3188
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
3189
|
+
}
|
|
2387
3190
|
return existing;
|
|
2388
3191
|
}
|
|
2389
3192
|
if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) {
|
|
@@ -2402,6 +3205,8 @@ class OhSqliteStore {
|
|
|
2402
3205
|
sequence: head.sequence + 1,
|
|
2403
3206
|
spaceId: this.spaceId,
|
|
2404
3207
|
v: 1
|
|
3208
|
+
}, {
|
|
3209
|
+
maximumOperationBytes
|
|
2405
3210
|
});
|
|
2406
3211
|
this.#persist(operation);
|
|
2407
3212
|
return operation;
|
|
@@ -2413,30 +3218,84 @@ class OhSqliteStore {
|
|
|
2413
3218
|
const operation = parseOhOperationV1(value);
|
|
2414
3219
|
if (operation === null || operation.spaceId !== this.spaceId)
|
|
2415
3220
|
throw new OhIntegrityError("Invalid imported operation.");
|
|
3221
|
+
const result = this.importOperations({
|
|
3222
|
+
expectedHead: {
|
|
3223
|
+
operationSha256: operation.parentOperationSha256,
|
|
3224
|
+
sequence: operation.sequence - 1
|
|
3225
|
+
},
|
|
3226
|
+
operations: [operation]
|
|
3227
|
+
});
|
|
3228
|
+
return { imported: result.imported === 1, operation };
|
|
3229
|
+
}
|
|
3230
|
+
importOperations(input) {
|
|
3231
|
+
this.#assertOpen();
|
|
3232
|
+
this.#assertOperationReplication();
|
|
3233
|
+
const expectedHead = parseOhHeadRefV1(input.expectedHead);
|
|
3234
|
+
if (expectedHead === null || !Array.isArray(input.operations) || input.operations.length > 1000) {
|
|
3235
|
+
throw new TypeError("Invalid operation import interval.");
|
|
3236
|
+
}
|
|
3237
|
+
const operations = input.operations.map((value) => {
|
|
3238
|
+
const operation = parseOhOperationV1(value);
|
|
3239
|
+
if (operation === null || operation.spaceId !== this.spaceId) {
|
|
3240
|
+
throw new OhIntegrityError("Invalid imported operation.");
|
|
3241
|
+
}
|
|
3242
|
+
return operation;
|
|
3243
|
+
});
|
|
3244
|
+
let prior = expectedHead;
|
|
3245
|
+
for (const operation of operations) {
|
|
3246
|
+
if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) {
|
|
3247
|
+
throw new OhConflictError("Imported operations do not extend the expected head.");
|
|
3248
|
+
}
|
|
3249
|
+
prior = { operationSha256: operation.operationSha256, sequence: operation.sequence };
|
|
3250
|
+
}
|
|
2416
3251
|
return withImmediateTransaction(this.database, () => {
|
|
2417
|
-
const
|
|
2418
|
-
this.#assertCurrentHeadAuthority(
|
|
2419
|
-
|
|
2420
|
-
if (
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
3252
|
+
const current = this.head();
|
|
3253
|
+
this.#assertCurrentHeadAuthority(current);
|
|
3254
|
+
this.#headAt(expectedHead);
|
|
3255
|
+
if (!exactHeadRef(current, expectedHead)) {
|
|
3256
|
+
if (operations.length === 0 || current.sequence < prior.sequence) {
|
|
3257
|
+
throw new OhConflictError("The imported operation interval does not extend the local head.");
|
|
3258
|
+
}
|
|
3259
|
+
for (const operation of operations) {
|
|
3260
|
+
const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, operation.sequence);
|
|
3261
|
+
if (row === null)
|
|
3262
|
+
throw new OhIntegrityError("An imported replay is missing from the authority chain.");
|
|
3263
|
+
const existing = parseStoredOperationRow(row, { spaceId: this.spaceId });
|
|
3264
|
+
if (existing.operationSha256 !== operation.operationSha256) {
|
|
3265
|
+
throw new OhConflictError("The imported operation interval diverges from the local authority chain.");
|
|
3266
|
+
}
|
|
3267
|
+
if (canonicalJson(existing) !== canonicalJson(operation)) {
|
|
3268
|
+
throw new OhIntegrityError("An operation digest is bound to different bytes.");
|
|
3269
|
+
}
|
|
2428
3270
|
}
|
|
2429
|
-
|
|
3271
|
+
this.#assertOperationReachable(operations[operations.length - 1], current);
|
|
3272
|
+
return { head: current, imported: 0, status: "already-present", v: 1 };
|
|
2430
3273
|
}
|
|
2431
|
-
if (
|
|
2432
|
-
|
|
3274
|
+
if (operations.length === 0) {
|
|
3275
|
+
return { head: current, imported: 0, status: "already-present", v: 1 };
|
|
2433
3276
|
}
|
|
2434
|
-
const
|
|
2435
|
-
|
|
2436
|
-
|
|
3277
|
+
const records = this.#loadRecords();
|
|
3278
|
+
let head = current;
|
|
3279
|
+
for (const operation of operations) {
|
|
3280
|
+
const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operation.operationId);
|
|
3281
|
+
if (duplicate !== null) {
|
|
3282
|
+
throw new OhConflictError("An imported operation ID is already bound on the local authority chain.");
|
|
3283
|
+
}
|
|
3284
|
+
const transition = this.#transition(head, operation.changes, operation.operationId, records);
|
|
3285
|
+
if (transition.recordsSha256 !== operation.recordsSha256 || transition.graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
3286
|
+
throw new OhIntegrityError("An imported operation does not reproduce its declared graph head.");
|
|
3287
|
+
}
|
|
3288
|
+
this.#persist(operation);
|
|
3289
|
+
head = {
|
|
3290
|
+
generation: operation.sequence,
|
|
3291
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
3292
|
+
operationSha256: operation.operationSha256,
|
|
3293
|
+
recordsSha256: operation.recordsSha256,
|
|
3294
|
+
sequence: operation.sequence,
|
|
3295
|
+
v: 1
|
|
3296
|
+
};
|
|
2437
3297
|
}
|
|
2438
|
-
|
|
2439
|
-
return { imported: true, operation };
|
|
3298
|
+
return { head, imported: operations.length, status: "imported", v: 1 };
|
|
2440
3299
|
});
|
|
2441
3300
|
}
|
|
2442
3301
|
exportOperations(afterSequence = 0, limit = 1000) {
|
|
@@ -2654,6 +3513,10 @@ class OhSqliteStore {
|
|
|
2654
3513
|
const integrity = this.database.query("PRAGMA integrity_check").get();
|
|
2655
3514
|
if (integrity?.integrity_check !== "ok")
|
|
2656
3515
|
throw new OhIntegrityError("SQLite integrity_check failed.");
|
|
3516
|
+
const foreignKeyViolations = this.database.query("PRAGMA foreign_key_check").all();
|
|
3517
|
+
if (foreignKeyViolations.length !== 0) {
|
|
3518
|
+
throw new OhIntegrityError("SQLite foreign_key_check failed.");
|
|
3519
|
+
}
|
|
2657
3520
|
const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0;
|
|
2658
3521
|
const operations = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? ORDER BY sequence`).all(this.spaceId).map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId }));
|
|
2659
3522
|
if (operations.length !== storedCount)
|
|
@@ -2730,6 +3593,22 @@ class OhSqliteStore {
|
|
|
2730
3593
|
if (canonicalJson(storedDependencies) !== canonicalJson(expectedDependencies)) {
|
|
2731
3594
|
throw new OhIntegrityError("Materialized dependencies do not match operation replay.");
|
|
2732
3595
|
}
|
|
3596
|
+
const expectedSearchDocuments = [...records.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((record) => ({
|
|
3597
|
+
record_key: record.key,
|
|
3598
|
+
record_sha256: record.recordSha256,
|
|
3599
|
+
text: `${record.key} ${record.kind} ${extractSearchText(record.value)}`
|
|
3600
|
+
}));
|
|
3601
|
+
const storedSearchDocuments = this.database.query(`SELECT record_key, record_sha256, text FROM oh_search_documents
|
|
3602
|
+
WHERE space_id = ? ORDER BY record_key`).all(this.spaceId);
|
|
3603
|
+
if (canonicalJson(storedSearchDocuments) !== canonicalJson(expectedSearchDocuments)) {
|
|
3604
|
+
throw new OhIntegrityError("Materialized search documents do not match operation replay.");
|
|
3605
|
+
}
|
|
3606
|
+
const expectedSearchFts = expectedSearchDocuments.map(({ record_key, text }) => ({ record_key, text }));
|
|
3607
|
+
const storedSearchFts = this.database.query(`SELECT record_key, text FROM oh_search_fts
|
|
3608
|
+
WHERE space_id = ? ORDER BY record_key, text, rowid`).all(this.spaceId);
|
|
3609
|
+
if (canonicalJson(storedSearchFts) !== canonicalJson(expectedSearchFts)) {
|
|
3610
|
+
throw new OhIntegrityError("Materialized full-text search rows do not match operation replay.");
|
|
3611
|
+
}
|
|
2733
3612
|
const storedOperationRecords = this.database.query(`SELECT materialized.operation_sha256, materialized.ordinal, materialized.record_key,
|
|
2734
3613
|
materialized.change_kind, materialized.record_sha256
|
|
2735
3614
|
FROM oh_operation_records AS materialized
|
|
@@ -2821,6 +3700,24 @@ class OhSqliteStore {
|
|
|
2821
3700
|
}
|
|
2822
3701
|
|
|
2823
3702
|
// src/sqlite/port.ts
|
|
3703
|
+
function exactReplicationImportInputV1(value) {
|
|
3704
|
+
try {
|
|
3705
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || isProxy2(value))
|
|
3706
|
+
return null;
|
|
3707
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3708
|
+
const keys = Reflect.ownKeys(value);
|
|
3709
|
+
if (prototype !== Object.prototype && prototype !== null || keys.length !== 2 || !keys.includes("bundle") || !keys.includes("expectedHead") || keys.some((key) => typeof key !== "string"))
|
|
3710
|
+
return null;
|
|
3711
|
+
const bundle = Object.getOwnPropertyDescriptor(value, "bundle");
|
|
3712
|
+
const expectedHead = Object.getOwnPropertyDescriptor(value, "expectedHead");
|
|
3713
|
+
if (bundle === undefined || expectedHead === undefined || !bundle.enumerable || !expectedHead.enumerable || bundle.get !== undefined || bundle.set !== undefined || expectedHead.get !== undefined || expectedHead.set !== undefined)
|
|
3714
|
+
return null;
|
|
3715
|
+
return { bundle: bundle.value, expectedHead: expectedHead.value };
|
|
3716
|
+
} catch {
|
|
3717
|
+
return null;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
|
|
2824
3721
|
class OhSqliteStorePortV1 {
|
|
2825
3722
|
binding;
|
|
2826
3723
|
#authority;
|
|
@@ -2879,6 +3776,46 @@ function createOhSqliteStoreAuthorityV1(options = {}) {
|
|
|
2879
3776
|
});
|
|
2880
3777
|
const store = new OhSqliteStorePortV1(authority, binding);
|
|
2881
3778
|
let purge = null;
|
|
3779
|
+
const replication = profile.capabilities.operationReplication ? Object.freeze({
|
|
3780
|
+
binding,
|
|
3781
|
+
exportBundle: async (input) => {
|
|
3782
|
+
const page = authority.changesSince(input.after, {
|
|
3783
|
+
...input.limit === undefined ? {} : { limit: input.limit },
|
|
3784
|
+
through: input.through
|
|
3785
|
+
});
|
|
3786
|
+
const bundle = createOhSyncBundleV1(binding.spaceId, page.operations, {
|
|
3787
|
+
largestFittingPrefix: true
|
|
3788
|
+
});
|
|
3789
|
+
const last = bundle.operations.at(-1);
|
|
3790
|
+
return Object.freeze({
|
|
3791
|
+
bundle,
|
|
3792
|
+
from: page.from,
|
|
3793
|
+
hasMore: page.hasMore || bundle.operations.length < page.operations.length,
|
|
3794
|
+
through: page.through,
|
|
3795
|
+
to: last === undefined ? page.from : {
|
|
3796
|
+
operationSha256: last.operationSha256,
|
|
3797
|
+
sequence: last.sequence
|
|
3798
|
+
},
|
|
3799
|
+
v: 1
|
|
3800
|
+
});
|
|
3801
|
+
},
|
|
3802
|
+
head: async () => authority.head(),
|
|
3803
|
+
importBundle: async (input) => {
|
|
3804
|
+
const request = exactReplicationImportInputV1(input);
|
|
3805
|
+
const expectedHead = request === null ? null : parseOhSyncHeadRefV1(request.expectedHead);
|
|
3806
|
+
if (request === null || expectedHead === null) {
|
|
3807
|
+
throw new TypeError("Invalid canonical replication request.");
|
|
3808
|
+
}
|
|
3809
|
+
const bundle = parseOhSyncBundleV1(request.bundle);
|
|
3810
|
+
if (bundle === null || bundle.spaceId !== binding.spaceId) {
|
|
3811
|
+
throw new TypeError("Invalid canonical replication bundle.");
|
|
3812
|
+
}
|
|
3813
|
+
return authority.importOperations({
|
|
3814
|
+
expectedHead,
|
|
3815
|
+
operations: bundle.operations
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
}) : null;
|
|
2882
3819
|
const host = Object.freeze({
|
|
2883
3820
|
binding,
|
|
2884
3821
|
purgeWorkingSpace: async (input) => {
|
|
@@ -2890,7 +3827,8 @@ function createOhSqliteStoreAuthorityV1(options = {}) {
|
|
|
2890
3827
|
purge = authority.purgeWorkingSpace(binding, input.purgedAt);
|
|
2891
3828
|
authority.close();
|
|
2892
3829
|
return purge;
|
|
2893
|
-
}
|
|
3830
|
+
},
|
|
3831
|
+
replication
|
|
2894
3832
|
});
|
|
2895
3833
|
return Object.freeze({ host, store });
|
|
2896
3834
|
}
|
|
@@ -2898,15 +3836,22 @@ export {
|
|
|
2898
3836
|
withReadTransaction,
|
|
2899
3837
|
withImmediateTransaction,
|
|
2900
3838
|
openOhSqliteDatabase,
|
|
3839
|
+
isOhProfileError,
|
|
3840
|
+
isOhOperationSizeError,
|
|
3841
|
+
isOhIntegrityError,
|
|
3842
|
+
isOhDependencyError,
|
|
3843
|
+
isOhConflictError,
|
|
2901
3844
|
createOhSqliteStoreAuthorityV1,
|
|
2902
3845
|
applyOhSqliteMigrations,
|
|
2903
3846
|
OhSqliteStorePortV1,
|
|
2904
3847
|
OhSqliteStore,
|
|
2905
3848
|
OhPurgedSpaceError,
|
|
2906
3849
|
OhProfileError,
|
|
3850
|
+
OhOperationSizeError,
|
|
2907
3851
|
OhIntegrityError,
|
|
2908
3852
|
OhDependencyError,
|
|
2909
3853
|
OhConflictError,
|
|
2910
3854
|
OH_SQLITE_SCHEMA_VERSION,
|
|
2911
|
-
OH_SQLITE_MIGRATIONS
|
|
3855
|
+
OH_SQLITE_MIGRATIONS,
|
|
3856
|
+
OH_OPERATION_SIZE_ERROR_CODE_V1
|
|
2912
3857
|
};
|