@hraness/oh 0.3.1 → 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 +194 -127
- 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-semantic-v2.d.ts +160 -0
- package/dist/libsql-semantic-v2.d.ts.map +1 -0
- 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.d.ts +1 -0
- package/dist/semantic-cloud.d.ts.map +1 -1
- package/dist/semantic-cloud.js +1545 -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 +10 -3
- package/spec/manifest.json +5 -1
- package/spec/v1/libsql-semantic-cache-schema-v1.sql +114 -0
- package/spec/v1/libsql-semantic-digest-fixture-v1.json +13 -0
- 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/spec/v2/libsql-semantic-cache-schema-v2.sql +175 -0
- package/spec/v2/libsql-semantic-digest-fixture-v2.json +14 -0
- package/spec/v2/manifest.json +11 -0
- package/spec/v2/semantic-cloud.md +106 -0
- 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-semantic-v2.test.ts +1114 -0
- package/src/libsql-semantic-v2.ts +1891 -0
- package/src/libsql-semantic.test.ts +50 -1
- 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/semantic-cloud.ts +1 -0
- 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/cli.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
+
// src/sync.ts
|
|
5
|
+
import { isProxy } from "util/types";
|
|
6
|
+
|
|
4
7
|
// src/canonical.ts
|
|
5
8
|
import { createHash, randomBytes } from "crypto";
|
|
6
9
|
|
|
@@ -199,7 +202,7 @@ var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
|
199
202
|
recordBytes: 1024 * 1024,
|
|
200
203
|
recordsPerSnapshot: 65536
|
|
201
204
|
});
|
|
202
|
-
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
205
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = Object.freeze([
|
|
203
206
|
"activity",
|
|
204
207
|
"assertion",
|
|
205
208
|
"context",
|
|
@@ -218,7 +221,7 @@ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
|
218
221
|
"type-membership",
|
|
219
222
|
"view",
|
|
220
223
|
"vocabulary"
|
|
221
|
-
];
|
|
224
|
+
]);
|
|
222
225
|
var KNOWLEDGE_GRAPH_RECORD_KEYS_V1 = [
|
|
223
226
|
"dependencies",
|
|
224
227
|
"key",
|
|
@@ -1144,6 +1147,142 @@ class OhRecordCodecRegistry {
|
|
|
1144
1147
|
}
|
|
1145
1148
|
}
|
|
1146
1149
|
|
|
1150
|
+
// src/errors.ts
|
|
1151
|
+
var OH_OPERATION_SIZE_ERROR_CODE_V1 = "oh.operation-size.v1";
|
|
1152
|
+
var OH_CONFLICT_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhConflictError/v1");
|
|
1153
|
+
var OH_DEPENDENCY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhDependencyError/v1");
|
|
1154
|
+
var OH_INTEGRITY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhIntegrityError/v1");
|
|
1155
|
+
var OH_OPERATION_SIZE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhOperationSizeError/v1");
|
|
1156
|
+
var OH_PROFILE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhProfileError/v1");
|
|
1157
|
+
function immutableOwnValue(value, key) {
|
|
1158
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1159
|
+
return descriptor !== undefined && descriptor.get === undefined && descriptor.set === undefined && descriptor.configurable === false && descriptor.writable === false ? descriptor.value : undefined;
|
|
1160
|
+
}
|
|
1161
|
+
function brandNativeError(value, brand) {
|
|
1162
|
+
Object.defineProperty(value, brand, {
|
|
1163
|
+
configurable: false,
|
|
1164
|
+
enumerable: false,
|
|
1165
|
+
value: true,
|
|
1166
|
+
writable: false
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
function hasNativeErrorBrand(value, brand) {
|
|
1170
|
+
try {
|
|
1171
|
+
return Error.isError(value) && immutableOwnValue(value, brand) === true;
|
|
1172
|
+
} catch {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function hasNativeSubclassInstance(constructor, value) {
|
|
1177
|
+
return Function.prototype[Symbol.hasInstance].call(constructor, value);
|
|
1178
|
+
}
|
|
1179
|
+
function isOhConflictError(value) {
|
|
1180
|
+
return hasNativeErrorBrand(value, OH_CONFLICT_ERROR_BRAND_V1);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
class OhConflictError extends Error {
|
|
1184
|
+
static [Symbol.hasInstance](value) {
|
|
1185
|
+
return this === OhConflictError ? isOhConflictError(value) : hasNativeSubclassInstance(this, value);
|
|
1186
|
+
}
|
|
1187
|
+
constructor(message) {
|
|
1188
|
+
super(message);
|
|
1189
|
+
this.name = "OhConflictError";
|
|
1190
|
+
brandNativeError(this, OH_CONFLICT_ERROR_BRAND_V1);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function isOhIntegrityError(value) {
|
|
1194
|
+
return hasNativeErrorBrand(value, OH_INTEGRITY_ERROR_BRAND_V1);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
class OhIntegrityError extends Error {
|
|
1198
|
+
static [Symbol.hasInstance](value) {
|
|
1199
|
+
return this === OhIntegrityError ? isOhIntegrityError(value) : hasNativeSubclassInstance(this, value);
|
|
1200
|
+
}
|
|
1201
|
+
constructor(message) {
|
|
1202
|
+
super(message);
|
|
1203
|
+
this.name = "OhIntegrityError";
|
|
1204
|
+
brandNativeError(this, OH_INTEGRITY_ERROR_BRAND_V1);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function isOhDependencyError(value) {
|
|
1208
|
+
return hasNativeErrorBrand(value, OH_DEPENDENCY_ERROR_BRAND_V1);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
class OhDependencyError extends Error {
|
|
1212
|
+
static [Symbol.hasInstance](value) {
|
|
1213
|
+
return this === OhDependencyError ? isOhDependencyError(value) : hasNativeSubclassInstance(this, value);
|
|
1214
|
+
}
|
|
1215
|
+
constructor(message) {
|
|
1216
|
+
super(message);
|
|
1217
|
+
this.name = "OhDependencyError";
|
|
1218
|
+
brandNativeError(this, OH_DEPENDENCY_ERROR_BRAND_V1);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function isOhProfileError(value) {
|
|
1222
|
+
return hasNativeErrorBrand(value, OH_PROFILE_ERROR_BRAND_V1);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
class OhProfileError extends Error {
|
|
1226
|
+
static [Symbol.hasInstance](value) {
|
|
1227
|
+
return this === OhProfileError ? isOhProfileError(value) : hasNativeSubclassInstance(this, value);
|
|
1228
|
+
}
|
|
1229
|
+
constructor(message) {
|
|
1230
|
+
super(message);
|
|
1231
|
+
this.name = "OhProfileError";
|
|
1232
|
+
brandNativeError(this, OH_PROFILE_ERROR_BRAND_V1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function isOhOperationSizeError(value) {
|
|
1236
|
+
try {
|
|
1237
|
+
if (!Error.isError(value) || !(value instanceof RangeError))
|
|
1238
|
+
return false;
|
|
1239
|
+
const operationBytes = immutableOwnValue(value, "operationBytes");
|
|
1240
|
+
const maximumOperationBytes = immutableOwnValue(value, "maximumOperationBytes");
|
|
1241
|
+
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;
|
|
1242
|
+
} catch {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
class OhOperationSizeError extends RangeError {
|
|
1248
|
+
static [Symbol.hasInstance](value) {
|
|
1249
|
+
return this === OhOperationSizeError ? isOhOperationSizeError(value) : hasNativeSubclassInstance(this, value);
|
|
1250
|
+
}
|
|
1251
|
+
constructor(operationBytes, maximumOperationBytes) {
|
|
1252
|
+
if (!Number.isSafeInteger(operationBytes) || operationBytes < 1 || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || operationBytes <= maximumOperationBytes) {
|
|
1253
|
+
throw new TypeError("Invalid Oh operation size refusal.");
|
|
1254
|
+
}
|
|
1255
|
+
super(`The ${operationBytes}-byte operation exceeds the host-declared ${maximumOperationBytes}-byte canonical bound.`);
|
|
1256
|
+
this.name = "OhOperationSizeError";
|
|
1257
|
+
Object.defineProperties(this, {
|
|
1258
|
+
[OH_OPERATION_SIZE_ERROR_BRAND_V1]: {
|
|
1259
|
+
configurable: false,
|
|
1260
|
+
enumerable: false,
|
|
1261
|
+
value: true,
|
|
1262
|
+
writable: false
|
|
1263
|
+
},
|
|
1264
|
+
code: {
|
|
1265
|
+
configurable: false,
|
|
1266
|
+
enumerable: true,
|
|
1267
|
+
value: OH_OPERATION_SIZE_ERROR_CODE_V1,
|
|
1268
|
+
writable: false
|
|
1269
|
+
},
|
|
1270
|
+
maximumOperationBytes: {
|
|
1271
|
+
configurable: false,
|
|
1272
|
+
enumerable: true,
|
|
1273
|
+
value: maximumOperationBytes,
|
|
1274
|
+
writable: false
|
|
1275
|
+
},
|
|
1276
|
+
operationBytes: {
|
|
1277
|
+
configurable: false,
|
|
1278
|
+
enumerable: true,
|
|
1279
|
+
value: operationBytes,
|
|
1280
|
+
writable: false
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1147
1286
|
// src/operation.ts
|
|
1148
1287
|
var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
|
|
1149
1288
|
function parsePayload(value) {
|
|
@@ -1191,13 +1330,18 @@ function parsePayload(value) {
|
|
|
1191
1330
|
v: 1
|
|
1192
1331
|
} : null;
|
|
1193
1332
|
}
|
|
1194
|
-
function createOhOperationV1(input) {
|
|
1333
|
+
function createOhOperationV1(input, options = {}) {
|
|
1334
|
+
const maximumOperationBytes = options.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
|
|
1335
|
+
if (!Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
1336
|
+
throw new TypeError("Invalid Oh operation byte bound.");
|
|
1337
|
+
}
|
|
1195
1338
|
const payload = parsePayload(input);
|
|
1196
1339
|
if (payload === null)
|
|
1197
1340
|
throw new TypeError("Invalid Oh operation payload.");
|
|
1198
1341
|
const operation = { ...payload, operationSha256: canonicalSha256(payload) };
|
|
1199
|
-
|
|
1200
|
-
|
|
1342
|
+
const operationBytes = Buffer.byteLength(canonicalJson(operation), "utf8");
|
|
1343
|
+
if (operationBytes > maximumOperationBytes) {
|
|
1344
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
1201
1345
|
}
|
|
1202
1346
|
return operation;
|
|
1203
1347
|
}
|
|
@@ -1212,18 +1356,362 @@ function parseOhOperationV1(value) {
|
|
|
1212
1356
|
|
|
1213
1357
|
// src/sync.ts
|
|
1214
1358
|
var OH_SYNC_PROTOCOL_V1 = "oh.sync.v1";
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1359
|
+
var OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 = 1000;
|
|
1360
|
+
var OH_SYNC_BUNDLE_KEYS_V1 = [
|
|
1361
|
+
"bundleSha256",
|
|
1362
|
+
"contractSha256",
|
|
1363
|
+
"operations",
|
|
1364
|
+
"protocol",
|
|
1365
|
+
"spaceId",
|
|
1366
|
+
"v"
|
|
1367
|
+
];
|
|
1368
|
+
var OH_SYNC_HEAD_KEYS_V1 = ["operationSha256", "sequence", "v"];
|
|
1369
|
+
var OH_SYNC_HEAD_REF_KEYS_V1 = ["operationSha256", "sequence"];
|
|
1370
|
+
var OH_OPERATION_KEYS_V1 = [
|
|
1371
|
+
"actorId",
|
|
1372
|
+
"changes",
|
|
1373
|
+
"contractId",
|
|
1374
|
+
"graphRevisionSha256",
|
|
1375
|
+
"instant",
|
|
1376
|
+
"operationId",
|
|
1377
|
+
"operationSha256",
|
|
1378
|
+
"parentOperationSha256",
|
|
1379
|
+
"recordsSha256",
|
|
1380
|
+
"sequence",
|
|
1381
|
+
"spaceId",
|
|
1382
|
+
"v"
|
|
1383
|
+
];
|
|
1384
|
+
var OH_PUT_CHANGE_KEYS_V1 = ["kind", "record", "v"];
|
|
1385
|
+
var OH_TOMBSTONE_CHANGE_KEYS_V1 = ["key", "kind", "priorSha256", "v"];
|
|
1386
|
+
var OH_RECORD_KEYS_V1 = ["dependencies", "key", "kind", "recordSha256", "v", "value"];
|
|
1387
|
+
var OH_SYNC_INGRESS_VALUE_DEPTH_V1 = 128;
|
|
1388
|
+
var OH_SYNC_INGRESS_VALUE_NODES_V1 = OH_GRAPH_LIMITS_V1.recordBytes;
|
|
1389
|
+
var OH_SYNC_INGRESS_OPERATION_DEPTH_V1 = OH_SYNC_INGRESS_VALUE_DEPTH_V1 + 4;
|
|
1390
|
+
var OH_SYNC_INGRESS_OPERATION_NODES_V1 = OH_OPERATION_MAX_BYTES_V1;
|
|
1391
|
+
var OH_SYNC_BUNDLE_MAX_BYTES_V1 = OH_OPERATION_MAX_BYTES_V1 + 4 * 1024;
|
|
1392
|
+
var OH_SYNC_INGRESS_BUNDLE_NODES_V1 = OH_SYNC_INGRESS_OPERATION_NODES_V1 + 4 * 1024;
|
|
1393
|
+
function exactDataRecordV1(value, expectedKeys) {
|
|
1394
|
+
try {
|
|
1395
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || isProxy(value))
|
|
1396
|
+
return null;
|
|
1397
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1398
|
+
const keys = Reflect.ownKeys(value);
|
|
1399
|
+
if (prototype !== Object.prototype && prototype !== null || keys.length !== expectedKeys.length || keys.some((key) => typeof key !== "string") || expectedKeys.some((key) => !keys.includes(key)))
|
|
1400
|
+
return null;
|
|
1401
|
+
const detached = Object.create(null);
|
|
1402
|
+
for (const key of expectedKeys) {
|
|
1403
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1404
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1405
|
+
return null;
|
|
1406
|
+
Object.defineProperty(detached, key, {
|
|
1407
|
+
configurable: false,
|
|
1408
|
+
enumerable: true,
|
|
1409
|
+
value: descriptor.value,
|
|
1410
|
+
writable: false
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
return detached;
|
|
1414
|
+
} catch {
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function exactDataArrayV1(value, maximumLength, clone = true) {
|
|
1419
|
+
try {
|
|
1420
|
+
if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value))
|
|
1421
|
+
return null;
|
|
1422
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
1423
|
+
const length = lengthDescriptor?.value;
|
|
1424
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > maximumLength)
|
|
1425
|
+
return null;
|
|
1426
|
+
const keys = Reflect.ownKeys(value);
|
|
1427
|
+
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)))
|
|
1428
|
+
return null;
|
|
1429
|
+
const detached = clone ? [] : null;
|
|
1430
|
+
for (let index = 0;index < length; index += 1) {
|
|
1431
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
1432
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1433
|
+
return null;
|
|
1434
|
+
detached?.push(descriptor.value);
|
|
1435
|
+
}
|
|
1436
|
+
return detached ?? value;
|
|
1437
|
+
} catch {
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function stagedSyncOperationV1(value, bundleBudget) {
|
|
1442
|
+
const operation = exactDataRecordV1(value, OH_OPERATION_KEYS_V1);
|
|
1443
|
+
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)
|
|
1444
|
+
return null;
|
|
1445
|
+
const parentOperationSha256 = operation.parentOperationSha256 === null ? null : parseSha256Hex(operation.parentOperationSha256);
|
|
1446
|
+
const sequence = Number.isSafeInteger(operation.sequence) && operation.sequence > 0 ? operation.sequence : null;
|
|
1447
|
+
if (sequence === null || operation.parentOperationSha256 !== null && parentOperationSha256 === null || sequence === 1 !== (parentOperationSha256 === null))
|
|
1448
|
+
return null;
|
|
1449
|
+
const changes = exactDataArrayV1(operation.changes, OH_GRAPH_LIMITS_V1.changesPerOperation, false);
|
|
1450
|
+
if (changes === null || changes.length === 0)
|
|
1451
|
+
return null;
|
|
1452
|
+
const aggregateDataBudget = {
|
|
1453
|
+
bytes: 0,
|
|
1454
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1455
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
|
|
1456
|
+
nodes: 0
|
|
1457
|
+
};
|
|
1458
|
+
for (const change of changes) {
|
|
1459
|
+
const put = exactDataRecordV1(change, OH_PUT_CHANGE_KEYS_V1);
|
|
1460
|
+
if (put !== null && put.kind === "put" && put.v === 1) {
|
|
1461
|
+
const record = exactDataRecordV1(put.record, OH_RECORD_KEYS_V1);
|
|
1462
|
+
if (record === null)
|
|
1463
|
+
return null;
|
|
1464
|
+
const valuePreflight = preflightSyncIngressValueV1(record.value, aggregateDataBudget);
|
|
1465
|
+
const dependenciesPreflight = preflightSyncIngressDependenciesV1(record.dependencies, aggregateDataBudget);
|
|
1466
|
+
if (valuePreflight === null || dependenciesPreflight === null)
|
|
1467
|
+
return null;
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const tombstone = exactDataRecordV1(change, OH_TOMBSTONE_CHANGE_KEYS_V1);
|
|
1471
|
+
if (tombstone === null || tombstone.kind !== "tombstone" || tombstone.v !== 1)
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
const detached = boundedSyncIngressV1(operation, {
|
|
1475
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1476
|
+
maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
|
|
1477
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1
|
|
1478
|
+
}, bundleBudget, true);
|
|
1479
|
+
return detached === null ? null : detached.value;
|
|
1480
|
+
}
|
|
1481
|
+
function canonicalStringBytesV1(value, maximumBytes) {
|
|
1482
|
+
if (value.length + 2 > maximumBytes)
|
|
1483
|
+
throw new RangeError("String exceeds its canonical byte budget.");
|
|
1484
|
+
let bytes = 2;
|
|
1485
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
1486
|
+
const code = value.charCodeAt(index);
|
|
1487
|
+
if (code >= 55296 && code <= 56319) {
|
|
1488
|
+
const next = value.charCodeAt(index + 1);
|
|
1489
|
+
if (!(next >= 56320 && next <= 57343))
|
|
1490
|
+
throw new TypeError("Invalid Unicode string.");
|
|
1491
|
+
bytes += 4;
|
|
1492
|
+
index += 1;
|
|
1493
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
1494
|
+
throw new TypeError("Invalid Unicode string.");
|
|
1495
|
+
} else if (code === 34 || code === 92 || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) {
|
|
1496
|
+
bytes += 2;
|
|
1497
|
+
} else if (code <= 31) {
|
|
1498
|
+
bytes += 6;
|
|
1499
|
+
} else if (code <= 127) {
|
|
1500
|
+
bytes += 1;
|
|
1501
|
+
} else if (code <= 2047) {
|
|
1502
|
+
bytes += 2;
|
|
1503
|
+
} else {
|
|
1504
|
+
bytes += 3;
|
|
1505
|
+
}
|
|
1506
|
+
if (bytes > maximumBytes)
|
|
1507
|
+
throw new RangeError("String exceeds its canonical byte budget.");
|
|
1508
|
+
}
|
|
1509
|
+
return bytes;
|
|
1510
|
+
}
|
|
1511
|
+
function boundedSyncIngressV1(value, limits, aggregate, clone) {
|
|
1512
|
+
const ancestors = new Set;
|
|
1513
|
+
const budget = {
|
|
1514
|
+
bytes: 0,
|
|
1515
|
+
maximumBytes: limits.maximumBytes,
|
|
1516
|
+
maximumNodes: limits.maximumNodes,
|
|
1517
|
+
nodes: 0
|
|
1518
|
+
};
|
|
1519
|
+
const canSpendBytes = (count) => budget.bytes + count <= budget.maximumBytes && (aggregate === null || aggregate.bytes + count <= aggregate.maximumBytes);
|
|
1520
|
+
const spendBytes = (count) => {
|
|
1521
|
+
if (!canSpendBytes(count))
|
|
1522
|
+
throw new RangeError("Sync ingress exceeds its canonical byte budget.");
|
|
1523
|
+
budget.bytes += count;
|
|
1524
|
+
if (aggregate !== null)
|
|
1525
|
+
aggregate.bytes += count;
|
|
1526
|
+
};
|
|
1527
|
+
const canSpendNodes = (count) => budget.nodes + count <= budget.maximumNodes && (aggregate === null || aggregate.nodes + count <= aggregate.maximumNodes);
|
|
1528
|
+
const spendNode = () => {
|
|
1529
|
+
if (!canSpendNodes(1))
|
|
1530
|
+
throw new RangeError("Sync ingress exceeds its node budget.");
|
|
1531
|
+
budget.nodes += 1;
|
|
1532
|
+
if (aggregate !== null)
|
|
1533
|
+
aggregate.nodes += 1;
|
|
1534
|
+
};
|
|
1535
|
+
const detach = (candidate, depth) => {
|
|
1536
|
+
if (depth > limits.maximumDepth)
|
|
1537
|
+
throw new RangeError("Sync ingress exceeds its depth budget.");
|
|
1538
|
+
spendNode();
|
|
1539
|
+
if (candidate === null) {
|
|
1540
|
+
spendBytes(4);
|
|
1541
|
+
return candidate;
|
|
1542
|
+
}
|
|
1543
|
+
if (typeof candidate === "boolean") {
|
|
1544
|
+
spendBytes(candidate ? 4 : 5);
|
|
1545
|
+
return candidate;
|
|
1546
|
+
}
|
|
1547
|
+
if (typeof candidate === "string") {
|
|
1548
|
+
spendBytes(canonicalStringBytesV1(candidate, Math.min(budget.maximumBytes - budget.bytes, aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes)));
|
|
1549
|
+
return candidate;
|
|
1550
|
+
}
|
|
1551
|
+
if (typeof candidate === "number") {
|
|
1552
|
+
if (!Number.isFinite(candidate) || Object.is(candidate, -0))
|
|
1553
|
+
throw new TypeError("Invalid number.");
|
|
1554
|
+
spendBytes(utf8ByteLength(canonicalJson(candidate)));
|
|
1555
|
+
return candidate;
|
|
1556
|
+
}
|
|
1557
|
+
if (typeof candidate !== "object" || isProxy(candidate))
|
|
1558
|
+
throw new TypeError("Invalid data value.");
|
|
1559
|
+
if (ancestors.has(candidate))
|
|
1560
|
+
throw new TypeError("Cyclic data value.");
|
|
1561
|
+
ancestors.add(candidate);
|
|
1562
|
+
try {
|
|
1563
|
+
if (Array.isArray(candidate)) {
|
|
1564
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(candidate, "length");
|
|
1565
|
+
const length = lengthDescriptor?.value;
|
|
1566
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || !canSpendNodes(length) || !canSpendBytes(2 + Math.max(0, length - 1) + length)) {
|
|
1567
|
+
throw new TypeError("Invalid or over-budget data array.");
|
|
1568
|
+
}
|
|
1569
|
+
const keys2 = Reflect.ownKeys(candidate);
|
|
1570
|
+
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))) {
|
|
1571
|
+
throw new TypeError("Invalid data array.");
|
|
1572
|
+
}
|
|
1573
|
+
spendBytes(2 + Math.max(0, length - 1));
|
|
1574
|
+
const detached2 = clone ? [] : null;
|
|
1575
|
+
for (let index = 0;index < length; index += 1) {
|
|
1576
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
|
|
1577
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1578
|
+
throw new TypeError("Invalid data array entry.");
|
|
1579
|
+
}
|
|
1580
|
+
const item = detach(descriptor.value, depth + 1);
|
|
1581
|
+
detached2?.push(item);
|
|
1582
|
+
}
|
|
1583
|
+
return detached2 === null ? candidate : Object.freeze(detached2);
|
|
1584
|
+
}
|
|
1585
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
1586
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1587
|
+
throw new TypeError("Invalid data object.");
|
|
1588
|
+
}
|
|
1589
|
+
let containerBytes = 2;
|
|
1590
|
+
let properties = 0;
|
|
1591
|
+
for (const key in candidate) {
|
|
1592
|
+
if (!Object.hasOwn(candidate, key))
|
|
1593
|
+
continue;
|
|
1594
|
+
properties += 1;
|
|
1595
|
+
if (!canSpendNodes(properties))
|
|
1596
|
+
throw new RangeError("Sync ingress exceeds its node budget.");
|
|
1597
|
+
const remainingBytes = Math.min(budget.maximumBytes - budget.bytes - containerBytes - properties, aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes - containerBytes - properties);
|
|
1598
|
+
containerBytes += (properties === 1 ? 0 : 1) + canonicalStringBytesV1(key, remainingBytes) + 1;
|
|
1599
|
+
if (!canSpendBytes(containerBytes + properties)) {
|
|
1600
|
+
throw new RangeError("Sync ingress exceeds its canonical byte budget.");
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const keys = Reflect.ownKeys(candidate);
|
|
1604
|
+
if (keys.length !== properties || keys.some((key) => typeof key !== "string")) {
|
|
1605
|
+
throw new TypeError("Invalid data object.");
|
|
1606
|
+
}
|
|
1607
|
+
spendBytes(containerBytes);
|
|
1608
|
+
const detached = clone ? Object.create(null) : null;
|
|
1609
|
+
for (const key of keys) {
|
|
1610
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
|
|
1611
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1612
|
+
throw new TypeError("Invalid data property.");
|
|
1613
|
+
}
|
|
1614
|
+
const item = detach(descriptor.value, depth + 1);
|
|
1615
|
+
if (detached !== null) {
|
|
1616
|
+
Object.defineProperty(detached, key, {
|
|
1617
|
+
configurable: false,
|
|
1618
|
+
enumerable: true,
|
|
1619
|
+
value: item,
|
|
1620
|
+
writable: false
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return detached === null ? candidate : Object.freeze(detached);
|
|
1625
|
+
} finally {
|
|
1626
|
+
ancestors.delete(candidate);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
try {
|
|
1630
|
+
return Object.freeze({ value: detach(value, 0) });
|
|
1631
|
+
} catch {
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function preflightSyncIngressValueV1(value, aggregate) {
|
|
1636
|
+
return boundedSyncIngressV1(value, {
|
|
1637
|
+
maximumBytes: OH_GRAPH_LIMITS_V1.recordBytes,
|
|
1638
|
+
maximumDepth: OH_SYNC_INGRESS_VALUE_DEPTH_V1,
|
|
1639
|
+
maximumNodes: OH_SYNC_INGRESS_VALUE_NODES_V1
|
|
1640
|
+
}, aggregate, false);
|
|
1641
|
+
}
|
|
1642
|
+
function preflightSyncIngressDependenciesV1(value, aggregate) {
|
|
1643
|
+
try {
|
|
1644
|
+
if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value)) {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
|
|
1648
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
1649
|
+
return null;
|
|
1650
|
+
const maximumBytes = length === 0 ? 2 : 1 + length * 515;
|
|
1651
|
+
return boundedSyncIngressV1(value, {
|
|
1652
|
+
maximumBytes,
|
|
1653
|
+
maximumDepth: 1,
|
|
1654
|
+
maximumNodes: length + 1
|
|
1655
|
+
}, aggregate, false);
|
|
1656
|
+
} catch {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function syncIngressBundleBudgetV1(spaceId) {
|
|
1661
|
+
const emptyBundle = {
|
|
1662
|
+
bundleSha256: "0".repeat(64),
|
|
1663
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
1664
|
+
operations: [],
|
|
1665
|
+
protocol: OH_SYNC_PROTOCOL_V1,
|
|
1666
|
+
spaceId,
|
|
1667
|
+
v: 1
|
|
1668
|
+
};
|
|
1669
|
+
return {
|
|
1670
|
+
bytes: utf8ByteLength(canonicalJson(emptyBundle)),
|
|
1671
|
+
maximumBytes: OH_SYNC_BUNDLE_MAX_BYTES_V1,
|
|
1672
|
+
maximumNodes: OH_SYNC_INGRESS_BUNDLE_NODES_V1,
|
|
1673
|
+
nodes: 7
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function spendSyncIngressBudgetV1(budget, bytes, nodes = 0) {
|
|
1677
|
+
if (budget.bytes + bytes > budget.maximumBytes || budget.nodes + nodes > budget.maximumNodes)
|
|
1678
|
+
return false;
|
|
1679
|
+
budget.bytes += bytes;
|
|
1680
|
+
budget.nodes += nodes;
|
|
1681
|
+
return true;
|
|
1682
|
+
}
|
|
1683
|
+
function measureSyncOperationV1(operation) {
|
|
1684
|
+
const measurement = {
|
|
1685
|
+
bytes: 0,
|
|
1686
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1687
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
|
|
1688
|
+
nodes: 0
|
|
1689
|
+
};
|
|
1690
|
+
return boundedSyncIngressV1(operation, {
|
|
1691
|
+
maximumBytes: OH_OPERATION_MAX_BYTES_V1,
|
|
1692
|
+
maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
|
|
1693
|
+
maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1
|
|
1694
|
+
}, measurement, false) === null ? null : measurement;
|
|
1695
|
+
}
|
|
1696
|
+
function buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix) {
|
|
1219
1697
|
let priorSequence = null;
|
|
1220
1698
|
let priorSha256 = null;
|
|
1221
1699
|
const parsed = [];
|
|
1700
|
+
const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
|
|
1222
1701
|
for (const candidate of operations) {
|
|
1223
1702
|
const operation = parseOhOperationV1(candidate);
|
|
1224
1703
|
if (operation === null || operation.spaceId !== parsedSpaceId || priorSequence !== null && operation.sequence !== priorSequence + 1 || priorSequence !== null && operation.parentOperationSha256 !== priorSha256) {
|
|
1225
1704
|
throw new TypeError("Sync operations must form one ordered chain.");
|
|
1226
1705
|
}
|
|
1706
|
+
const measurement = measureSyncOperationV1(operation);
|
|
1707
|
+
if (measurement === null) {
|
|
1708
|
+
throw new RangeError("Sync operation exceeds its canonical byte, node, or depth limit.");
|
|
1709
|
+
}
|
|
1710
|
+
if (!spendSyncIngressBudgetV1(bundleBudget, measurement.bytes + (parsed.length === 0 ? 0 : 1), measurement.nodes)) {
|
|
1711
|
+
if (largestFittingPrefix && parsed.length > 0)
|
|
1712
|
+
break;
|
|
1713
|
+
throw new RangeError("Sync bundle exceeds its canonical byte or node limit.");
|
|
1714
|
+
}
|
|
1227
1715
|
parsed.push(operation);
|
|
1228
1716
|
priorSequence = operation.sequence;
|
|
1229
1717
|
priorSha256 = operation.operationSha256;
|
|
@@ -1237,15 +1725,56 @@ function createOhSyncBundleV1(spaceId, operations) {
|
|
|
1237
1725
|
};
|
|
1238
1726
|
return { ...payload, bundleSha256: canonicalSha256(payload) };
|
|
1239
1727
|
}
|
|
1728
|
+
function parseSyncHeadRefEnvelopeV1(value) {
|
|
1729
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
1730
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 && !Object.is(value.sequence, -0) ? value.sequence : null;
|
|
1731
|
+
return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null;
|
|
1732
|
+
}
|
|
1733
|
+
function parseOhSyncHeadRefV1(value) {
|
|
1734
|
+
const reference = exactDataRecordV1(value, OH_SYNC_HEAD_REF_KEYS_V1);
|
|
1735
|
+
return reference === null ? null : parseSyncHeadRefEnvelopeV1(reference);
|
|
1736
|
+
}
|
|
1737
|
+
function parseOhSyncHeadV1(value) {
|
|
1738
|
+
const envelope = exactDataRecordV1(value, OH_SYNC_HEAD_KEYS_V1);
|
|
1739
|
+
if (envelope === null || envelope.v !== 1)
|
|
1740
|
+
return null;
|
|
1741
|
+
const reference = parseSyncHeadRefEnvelopeV1(envelope);
|
|
1742
|
+
return reference === null ? null : { ...reference, v: 1 };
|
|
1743
|
+
}
|
|
1744
|
+
function createOhSyncBundleV1(spaceId, operations, options = {}) {
|
|
1745
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
1746
|
+
const largestFittingPrefix = options.largestFittingPrefix ?? false;
|
|
1747
|
+
if (parsedSpaceId === null || operations.length > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 || typeof largestFittingPrefix !== "boolean") {
|
|
1748
|
+
throw new TypeError("Invalid sync bundle.");
|
|
1749
|
+
}
|
|
1750
|
+
return buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix);
|
|
1751
|
+
}
|
|
1240
1752
|
function parseOhSyncBundleV1(value) {
|
|
1241
|
-
|
|
1753
|
+
const envelope = exactDataRecordV1(value, OH_SYNC_BUNDLE_KEYS_V1);
|
|
1754
|
+
if (envelope === null || envelope.protocol !== OH_SYNC_PROTOCOL_V1 || envelope.v !== 1)
|
|
1242
1755
|
return null;
|
|
1243
|
-
const bundleSha256 = parseSha256Hex(
|
|
1244
|
-
const contractSha256 = parseSha256Hex(
|
|
1245
|
-
|
|
1756
|
+
const bundleSha256 = parseSha256Hex(envelope.bundleSha256);
|
|
1757
|
+
const contractSha256 = parseSha256Hex(envelope.contractSha256);
|
|
1758
|
+
const spaceId = safeCode(envelope.spaceId);
|
|
1759
|
+
if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || spaceId === null)
|
|
1246
1760
|
return null;
|
|
1761
|
+
const operations = exactDataArrayV1(envelope.operations, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1);
|
|
1762
|
+
if (operations === null)
|
|
1763
|
+
return null;
|
|
1764
|
+
const detachedOperations = [];
|
|
1765
|
+
const bundleBudget = syncIngressBundleBudgetV1(spaceId);
|
|
1766
|
+
for (const operation of operations) {
|
|
1767
|
+
if (detachedOperations.length > 0 && !spendSyncIngressBudgetV1(bundleBudget, 1))
|
|
1768
|
+
return null;
|
|
1769
|
+
const detached = stagedSyncOperationV1(operation, bundleBudget);
|
|
1770
|
+
if (detached === null)
|
|
1771
|
+
return null;
|
|
1772
|
+
detachedOperations.push(detached);
|
|
1773
|
+
}
|
|
1247
1774
|
try {
|
|
1248
|
-
const created = createOhSyncBundleV1(
|
|
1775
|
+
const created = createOhSyncBundleV1(spaceId, detachedOperations);
|
|
1776
|
+
if (detachedOperations.some((operation, index) => canonicalJson(operation) !== canonicalJson(created.operations[index])))
|
|
1777
|
+
return null;
|
|
1249
1778
|
return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
|
|
1250
1779
|
} catch {
|
|
1251
1780
|
return null;
|
|
@@ -1260,39 +1789,84 @@ async function synchronizeOhStoreV1(store, transport, options = {}) {
|
|
|
1260
1789
|
await transport.handshake(OH_CONTRACT_MANIFEST_V1);
|
|
1261
1790
|
let pulled = 0;
|
|
1262
1791
|
let pushed = 0;
|
|
1792
|
+
const settled = (head, rounds) => {
|
|
1793
|
+
store.updateSyncState(remoteId, {
|
|
1794
|
+
pulledSequence: head.sequence,
|
|
1795
|
+
pushedSequence: head.sequence,
|
|
1796
|
+
remoteHeadSha256: head.operationSha256
|
|
1797
|
+
});
|
|
1798
|
+
return { head, pulled, pushed, rounds, v: 1 };
|
|
1799
|
+
};
|
|
1263
1800
|
for (let round = 1;round <= maximumRounds; round += 1) {
|
|
1801
|
+
const remote = parseOhSyncHeadV1(await transport.head(store.spaceId));
|
|
1802
|
+
if (remote === null)
|
|
1803
|
+
throw new Error("The sync transport returned an invalid head.");
|
|
1264
1804
|
const local = store.head();
|
|
1265
|
-
const remote = await transport.head(store.spaceId);
|
|
1266
1805
|
if (local.sequence === remote.sequence) {
|
|
1267
1806
|
if (local.operationSha256 !== remote.operationSha256) {
|
|
1268
1807
|
throw new Error("Sync conflict: equal sequence numbers have different heads.");
|
|
1269
1808
|
}
|
|
1270
|
-
|
|
1271
|
-
pulledSequence: local.sequence,
|
|
1272
|
-
pushedSequence: local.sequence,
|
|
1273
|
-
remoteHeadSha256: remote.operationSha256
|
|
1274
|
-
});
|
|
1275
|
-
return { head: remote, pulled, pushed, rounds: round, v: 1 };
|
|
1809
|
+
return settled(remote, round);
|
|
1276
1810
|
}
|
|
1277
1811
|
if (local.sequence < remote.sequence) {
|
|
1278
1812
|
const bundle = parseOhSyncBundleV1(await transport.pull(store.spaceId, local.sequence, batchSize));
|
|
1279
|
-
|
|
1813
|
+
const terminal = bundle?.operations.at(-1);
|
|
1814
|
+
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) {
|
|
1280
1815
|
throw new Error("Sync conflict: remote history does not extend the local head.");
|
|
1281
1816
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1817
|
+
store.importOperations({
|
|
1818
|
+
expectedHead: {
|
|
1819
|
+
operationSha256: local.operationSha256,
|
|
1820
|
+
sequence: local.sequence
|
|
1821
|
+
},
|
|
1822
|
+
operations: bundle.operations
|
|
1823
|
+
});
|
|
1824
|
+
pulled += bundle.operations.length;
|
|
1825
|
+
const afterPull = store.head();
|
|
1826
|
+
if (afterPull.sequence === remote.sequence && afterPull.operationSha256 === remote.operationSha256) {
|
|
1827
|
+
const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
|
|
1828
|
+
if (confirmed === null)
|
|
1829
|
+
throw new Error("The sync transport returned an invalid head.");
|
|
1830
|
+
const confirmedLocal = store.head();
|
|
1831
|
+
if (confirmed.sequence === remote.sequence && confirmed.operationSha256 === remote.operationSha256 && confirmedLocal.sequence === confirmed.sequence && confirmedLocal.operationSha256 === confirmed.operationSha256) {
|
|
1832
|
+
return settled(confirmed, round);
|
|
1833
|
+
}
|
|
1285
1834
|
}
|
|
1286
1835
|
} else {
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1836
|
+
const candidates = store.changesSince({
|
|
1837
|
+
operationSha256: remote.operationSha256,
|
|
1838
|
+
sequence: remote.sequence
|
|
1839
|
+
}, {
|
|
1840
|
+
limit: batchSize,
|
|
1841
|
+
through: {
|
|
1842
|
+
operationSha256: local.operationSha256,
|
|
1843
|
+
sequence: local.sequence
|
|
1844
|
+
}
|
|
1845
|
+
}).operations;
|
|
1846
|
+
if (candidates.length === 0 || candidates[0]?.sequence !== remote.sequence + 1 || candidates[0]?.parentOperationSha256 !== remote.operationSha256) {
|
|
1289
1847
|
throw new Error("Sync conflict: local history does not extend the remote head.");
|
|
1290
1848
|
}
|
|
1291
|
-
const
|
|
1849
|
+
const bundle = createOhSyncBundleV1(store.spaceId, candidates, {
|
|
1850
|
+
largestFittingPrefix: true
|
|
1851
|
+
});
|
|
1852
|
+
const operations = bundle.operations;
|
|
1853
|
+
const head = parseOhSyncHeadV1(await transport.push(bundle));
|
|
1854
|
+
if (head === null)
|
|
1855
|
+
throw new Error("The sync transport returned an invalid push head.");
|
|
1292
1856
|
if (head.sequence !== operations.at(-1)?.sequence || head.operationSha256 !== operations.at(-1)?.operationSha256) {
|
|
1293
1857
|
throw new Error("The sync transport acknowledged a different head.");
|
|
1294
1858
|
}
|
|
1295
1859
|
pushed += operations.length;
|
|
1860
|
+
const afterPush = store.head();
|
|
1861
|
+
if (afterPush.sequence === head.sequence && afterPush.operationSha256 === head.operationSha256) {
|
|
1862
|
+
const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
|
|
1863
|
+
if (confirmed === null)
|
|
1864
|
+
throw new Error("The sync transport returned an invalid head.");
|
|
1865
|
+
const confirmedLocal = store.head();
|
|
1866
|
+
if (confirmed.sequence === head.sequence && confirmed.operationSha256 === head.operationSha256 && confirmedLocal.sequence === confirmed.sequence && confirmedLocal.operationSha256 === confirmed.operationSha256) {
|
|
1867
|
+
return settled(confirmed, round);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1296
1870
|
}
|
|
1297
1871
|
}
|
|
1298
1872
|
throw new Error("Sync did not settle within maximumRounds.");
|
|
@@ -1356,19 +1930,48 @@ function createLibSqlOperationSyncTransportV1(client) {
|
|
|
1356
1930
|
},
|
|
1357
1931
|
head,
|
|
1358
1932
|
pull: async (spaceId, afterSequence, limit) => {
|
|
1933
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
1934
|
+
if (parsedSpaceId === null || !Number.isSafeInteger(afterSequence) || afterSequence < 0 || Object.is(afterSequence, -0) || !Number.isSafeInteger(limit) || limit < 1 || limit > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1) {
|
|
1935
|
+
throw new TypeError("Invalid sync pull request.");
|
|
1936
|
+
}
|
|
1359
1937
|
await ensure();
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1938
|
+
const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
|
|
1939
|
+
const result = await client.execute({
|
|
1940
|
+
sql: `SELECT operation_json FROM (
|
|
1941
|
+
SELECT sequence, operation_json,
|
|
1942
|
+
row_number() OVER (ORDER BY sequence) AS ordinal,
|
|
1943
|
+
sum(length(CAST(operation_json AS BLOB))) OVER (
|
|
1944
|
+
ORDER BY sequence ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
|
1945
|
+
) AS cumulative_bytes
|
|
1946
|
+
FROM (
|
|
1947
|
+
SELECT sequence, operation_json FROM oh_sync_operations
|
|
1948
|
+
WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?
|
|
1949
|
+
)
|
|
1950
|
+
) WHERE ordinal = 1 OR cumulative_bytes + ordinal - 1 <= ? ORDER BY sequence`,
|
|
1951
|
+
args: [
|
|
1952
|
+
parsedSpaceId,
|
|
1953
|
+
afterSequence,
|
|
1954
|
+
limit,
|
|
1955
|
+
bundleBudget.maximumBytes - bundleBudget.bytes
|
|
1956
|
+
]
|
|
1957
|
+
});
|
|
1958
|
+
const rows = result.rows;
|
|
1959
|
+
if (!Array.isArray(rows) || rows.length > limit) {
|
|
1960
|
+
throw new Error("Remote sync pull exceeded its requested row limit.");
|
|
1961
|
+
}
|
|
1962
|
+
const operations = rows.map((row) => {
|
|
1363
1963
|
const json = rowValue(row, "operation_json", 0);
|
|
1364
1964
|
if (typeof json !== "string")
|
|
1365
1965
|
throw new Error("Invalid remote operation JSON.");
|
|
1966
|
+
if (utf8ByteLength(json) > OH_OPERATION_MAX_BYTES_V1) {
|
|
1967
|
+
throw new Error("Remote operation JSON exceeds the canonical operation byte limit.");
|
|
1968
|
+
}
|
|
1366
1969
|
const operation = parseOhOperationV1(JSON.parse(json));
|
|
1367
1970
|
if (operation === null || canonicalJson(operation) !== json)
|
|
1368
1971
|
throw new Error("Invalid remote operation.");
|
|
1369
1972
|
return operation;
|
|
1370
1973
|
});
|
|
1371
|
-
return createOhSyncBundleV1(
|
|
1974
|
+
return createOhSyncBundleV1(parsedSpaceId, operations);
|
|
1372
1975
|
},
|
|
1373
1976
|
push: async (value) => {
|
|
1374
1977
|
await ensure();
|
|
@@ -1380,8 +1983,35 @@ function createLibSqlOperationSyncTransportV1(client) {
|
|
|
1380
1983
|
const remote = await head(bundle.spaceId);
|
|
1381
1984
|
const first = bundle.operations[0];
|
|
1382
1985
|
const last = bundle.operations.at(-1);
|
|
1383
|
-
if (remote.sequence
|
|
1384
|
-
|
|
1986
|
+
if (remote.sequence >= last.sequence) {
|
|
1987
|
+
const result = await client.execute({ sql: `SELECT sequence, operation_sha256, operation_json
|
|
1988
|
+
FROM oh_sync_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ?
|
|
1989
|
+
ORDER BY sequence LIMIT ?`, args: [
|
|
1990
|
+
bundle.spaceId,
|
|
1991
|
+
first.sequence,
|
|
1992
|
+
last.sequence,
|
|
1993
|
+
bundle.operations.length
|
|
1994
|
+
] });
|
|
1995
|
+
const rows = result.rows;
|
|
1996
|
+
if (!Array.isArray(rows) || rows.length !== bundle.operations.length) {
|
|
1997
|
+
throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
|
|
1998
|
+
}
|
|
1999
|
+
for (let index = 0;index < bundle.operations.length; index += 1) {
|
|
2000
|
+
const operation = bundle.operations[index];
|
|
2001
|
+
const row = rows[index];
|
|
2002
|
+
if (row === undefined) {
|
|
2003
|
+
throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
|
|
2004
|
+
}
|
|
2005
|
+
const sequence = Number(rowValue(row, "sequence", 0));
|
|
2006
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
|
|
2007
|
+
const operationJson = rowValue(row, "operation_json", 2);
|
|
2008
|
+
const expectedJson = canonicalJson(operation);
|
|
2009
|
+
if (sequence !== operation.sequence || operationSha256 !== operation.operationSha256 || typeof operationJson !== "string" || utf8ByteLength(operationJson) > OH_OPERATION_MAX_BYTES_V1 || operationJson !== expectedJson) {
|
|
2010
|
+
throw new Error("Sync conflict: remote history differs from the pushed operations.");
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
|
|
2014
|
+
}
|
|
1385
2015
|
if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
|
|
1386
2016
|
throw new Error("Sync conflict: pushed history does not extend the remote head.");
|
|
1387
2017
|
}
|
|
@@ -1443,35 +2073,7 @@ async function searchOhV1(input) {
|
|
|
1443
2073
|
// src/sqlite/store.ts
|
|
1444
2074
|
import { mkdirSync } from "fs";
|
|
1445
2075
|
import { dirname } from "path";
|
|
1446
|
-
|
|
1447
2076
|
// src/store.ts
|
|
1448
|
-
class OhConflictError extends Error {
|
|
1449
|
-
constructor(message) {
|
|
1450
|
-
super(message);
|
|
1451
|
-
this.name = "OhConflictError";
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
class OhIntegrityError extends Error {
|
|
1456
|
-
constructor(message) {
|
|
1457
|
-
super(message);
|
|
1458
|
-
this.name = "OhIntegrityError";
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
class OhDependencyError extends Error {
|
|
1463
|
-
constructor(message) {
|
|
1464
|
-
super(message);
|
|
1465
|
-
this.name = "OhDependencyError";
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
class OhProfileError extends Error {
|
|
1470
|
-
constructor(message) {
|
|
1471
|
-
super(message);
|
|
1472
|
-
this.name = "OhProfileError";
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
2077
|
var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
1476
2078
|
applicationProfileSha256: null,
|
|
1477
2079
|
capabilities: {
|
|
@@ -1784,6 +2386,8 @@ function transitionOhSnapshotV1(input) {
|
|
|
1784
2386
|
sequence: head.sequence + 1,
|
|
1785
2387
|
spaceId,
|
|
1786
2388
|
v: 1
|
|
2389
|
+
}, input.maximumOperationBytes === undefined ? {} : {
|
|
2390
|
+
maximumOperationBytes: input.maximumOperationBytes
|
|
1787
2391
|
});
|
|
1788
2392
|
const nextHead = {
|
|
1789
2393
|
generation: operation.sequence,
|
|
@@ -2359,6 +2963,9 @@ function normalizeLimit(value, fallback = 50, maximum = 1000) {
|
|
|
2359
2963
|
}
|
|
2360
2964
|
return value;
|
|
2361
2965
|
}
|
|
2966
|
+
function exactHeadRef(left, right) {
|
|
2967
|
+
return left.sequence === right.sequence && left.operationSha256 === right.operationSha256;
|
|
2968
|
+
}
|
|
2362
2969
|
function ftsQuery(value) {
|
|
2363
2970
|
const normalized = boundedText(value.normalize("NFC"), 4096);
|
|
2364
2971
|
if (normalized === null)
|
|
@@ -2475,8 +3082,7 @@ class OhSqliteStore {
|
|
|
2475
3082
|
}
|
|
2476
3083
|
return records;
|
|
2477
3084
|
}
|
|
2478
|
-
#transition(head, changes, operationId) {
|
|
2479
|
-
const records = this.#loadRecords();
|
|
3085
|
+
#transition(head, changes, operationId, records = this.#loadRecords()) {
|
|
2480
3086
|
for (const change of changes) {
|
|
2481
3087
|
if (change.kind === "put") {
|
|
2482
3088
|
records.set(change.record.key, change.record);
|
|
@@ -2533,7 +3139,7 @@ class OhSqliteStore {
|
|
|
2533
3139
|
if (operation.sequence < 1 || operation.sequence > head.sequence) {
|
|
2534
3140
|
throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head.");
|
|
2535
3141
|
}
|
|
2536
|
-
const rows = this.database.query(`SELECT
|
|
3142
|
+
const rows = this.database.query(`SELECT ${OPERATION_COLUMNS}
|
|
2537
3143
|
FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence);
|
|
2538
3144
|
if (rows.length !== head.sequence - operation.sequence + 1) {
|
|
2539
3145
|
throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head.");
|
|
@@ -2541,10 +3147,14 @@ class OhSqliteStore {
|
|
|
2541
3147
|
let priorSha256 = operation.parentOperationSha256;
|
|
2542
3148
|
for (let index = 0;index < rows.length; index += 1) {
|
|
2543
3149
|
const row = rows[index];
|
|
2544
|
-
if (row === undefined
|
|
3150
|
+
if (row === undefined) {
|
|
3151
|
+
throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
|
|
3152
|
+
}
|
|
3153
|
+
const reachable = parseStoredOperationRow(row, { spaceId: this.spaceId });
|
|
3154
|
+
if (reachable.sequence !== operation.sequence + index || reachable.parentOperationSha256 !== priorSha256 || index === 0 && reachable.operationSha256 !== operation.operationSha256) {
|
|
2545
3155
|
throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
|
|
2546
3156
|
}
|
|
2547
|
-
priorSha256 =
|
|
3157
|
+
priorSha256 = reachable.operationSha256;
|
|
2548
3158
|
}
|
|
2549
3159
|
if (priorSha256 !== head.operationSha256) {
|
|
2550
3160
|
throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest.");
|
|
@@ -2605,8 +3215,10 @@ class OhSqliteStore {
|
|
|
2605
3215
|
this.#assertOpen();
|
|
2606
3216
|
const actorId = safeCode(input.actorId);
|
|
2607
3217
|
const operationId = safeCode(input.operationId);
|
|
2608
|
-
|
|
2609
|
-
|
|
3218
|
+
const maximumOperationBytes = input.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
|
|
3219
|
+
if (actorId === null || operationId === null || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
3220
|
+
throw new TypeError("Invalid actor, operation ID, or operation byte bound.");
|
|
3221
|
+
}
|
|
2610
3222
|
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
2611
3223
|
if (changes.length === 0 || changes.length > 8192)
|
|
2612
3224
|
throw new TypeError("A commit needs 1 through 8192 changes.");
|
|
@@ -2620,6 +3232,10 @@ class OhSqliteStore {
|
|
|
2620
3232
|
if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) {
|
|
2621
3233
|
throw new OhConflictError("The operation ID is already bound to different content.");
|
|
2622
3234
|
}
|
|
3235
|
+
const operationBytes = Buffer.byteLength(canonicalJson(existing), "utf8");
|
|
3236
|
+
if (operationBytes > maximumOperationBytes) {
|
|
3237
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
3238
|
+
}
|
|
2623
3239
|
return existing;
|
|
2624
3240
|
}
|
|
2625
3241
|
if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) {
|
|
@@ -2638,6 +3254,8 @@ class OhSqliteStore {
|
|
|
2638
3254
|
sequence: head.sequence + 1,
|
|
2639
3255
|
spaceId: this.spaceId,
|
|
2640
3256
|
v: 1
|
|
3257
|
+
}, {
|
|
3258
|
+
maximumOperationBytes
|
|
2641
3259
|
});
|
|
2642
3260
|
this.#persist(operation);
|
|
2643
3261
|
return operation;
|
|
@@ -2649,30 +3267,84 @@ class OhSqliteStore {
|
|
|
2649
3267
|
const operation = parseOhOperationV1(value);
|
|
2650
3268
|
if (operation === null || operation.spaceId !== this.spaceId)
|
|
2651
3269
|
throw new OhIntegrityError("Invalid imported operation.");
|
|
3270
|
+
const result = this.importOperations({
|
|
3271
|
+
expectedHead: {
|
|
3272
|
+
operationSha256: operation.parentOperationSha256,
|
|
3273
|
+
sequence: operation.sequence - 1
|
|
3274
|
+
},
|
|
3275
|
+
operations: [operation]
|
|
3276
|
+
});
|
|
3277
|
+
return { imported: result.imported === 1, operation };
|
|
3278
|
+
}
|
|
3279
|
+
importOperations(input) {
|
|
3280
|
+
this.#assertOpen();
|
|
3281
|
+
this.#assertOperationReplication();
|
|
3282
|
+
const expectedHead = parseOhHeadRefV1(input.expectedHead);
|
|
3283
|
+
if (expectedHead === null || !Array.isArray(input.operations) || input.operations.length > 1000) {
|
|
3284
|
+
throw new TypeError("Invalid operation import interval.");
|
|
3285
|
+
}
|
|
3286
|
+
const operations = input.operations.map((value) => {
|
|
3287
|
+
const operation = parseOhOperationV1(value);
|
|
3288
|
+
if (operation === null || operation.spaceId !== this.spaceId) {
|
|
3289
|
+
throw new OhIntegrityError("Invalid imported operation.");
|
|
3290
|
+
}
|
|
3291
|
+
return operation;
|
|
3292
|
+
});
|
|
3293
|
+
let prior = expectedHead;
|
|
3294
|
+
for (const operation of operations) {
|
|
3295
|
+
if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) {
|
|
3296
|
+
throw new OhConflictError("Imported operations do not extend the expected head.");
|
|
3297
|
+
}
|
|
3298
|
+
prior = { operationSha256: operation.operationSha256, sequence: operation.sequence };
|
|
3299
|
+
}
|
|
2652
3300
|
return withImmediateTransaction(this.database, () => {
|
|
2653
|
-
const
|
|
2654
|
-
this.#assertCurrentHeadAuthority(
|
|
2655
|
-
|
|
2656
|
-
if (
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
spaceId: this.spaceId
|
|
2660
|
-
});
|
|
2661
|
-
this.#assertOperationReachable(existing, head);
|
|
2662
|
-
if (canonicalJson(existing) !== canonicalJson(operation)) {
|
|
2663
|
-
throw new OhIntegrityError("An operation digest is bound to different bytes.");
|
|
3301
|
+
const current = this.head();
|
|
3302
|
+
this.#assertCurrentHeadAuthority(current);
|
|
3303
|
+
this.#headAt(expectedHead);
|
|
3304
|
+
if (!exactHeadRef(current, expectedHead)) {
|
|
3305
|
+
if (operations.length === 0 || current.sequence < prior.sequence) {
|
|
3306
|
+
throw new OhConflictError("The imported operation interval does not extend the local head.");
|
|
2664
3307
|
}
|
|
2665
|
-
|
|
3308
|
+
for (const operation of operations) {
|
|
3309
|
+
const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, operation.sequence);
|
|
3310
|
+
if (row === null)
|
|
3311
|
+
throw new OhIntegrityError("An imported replay is missing from the authority chain.");
|
|
3312
|
+
const existing = parseStoredOperationRow(row, { spaceId: this.spaceId });
|
|
3313
|
+
if (existing.operationSha256 !== operation.operationSha256) {
|
|
3314
|
+
throw new OhConflictError("The imported operation interval diverges from the local authority chain.");
|
|
3315
|
+
}
|
|
3316
|
+
if (canonicalJson(existing) !== canonicalJson(operation)) {
|
|
3317
|
+
throw new OhIntegrityError("An operation digest is bound to different bytes.");
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
this.#assertOperationReachable(operations[operations.length - 1], current);
|
|
3321
|
+
return { head: current, imported: 0, status: "already-present", v: 1 };
|
|
2666
3322
|
}
|
|
2667
|
-
if (
|
|
2668
|
-
|
|
3323
|
+
if (operations.length === 0) {
|
|
3324
|
+
return { head: current, imported: 0, status: "already-present", v: 1 };
|
|
2669
3325
|
}
|
|
2670
|
-
const
|
|
2671
|
-
|
|
2672
|
-
|
|
3326
|
+
const records = this.#loadRecords();
|
|
3327
|
+
let head = current;
|
|
3328
|
+
for (const operation of operations) {
|
|
3329
|
+
const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operation.operationId);
|
|
3330
|
+
if (duplicate !== null) {
|
|
3331
|
+
throw new OhConflictError("An imported operation ID is already bound on the local authority chain.");
|
|
3332
|
+
}
|
|
3333
|
+
const transition = this.#transition(head, operation.changes, operation.operationId, records);
|
|
3334
|
+
if (transition.recordsSha256 !== operation.recordsSha256 || transition.graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
3335
|
+
throw new OhIntegrityError("An imported operation does not reproduce its declared graph head.");
|
|
3336
|
+
}
|
|
3337
|
+
this.#persist(operation);
|
|
3338
|
+
head = {
|
|
3339
|
+
generation: operation.sequence,
|
|
3340
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
3341
|
+
operationSha256: operation.operationSha256,
|
|
3342
|
+
recordsSha256: operation.recordsSha256,
|
|
3343
|
+
sequence: operation.sequence,
|
|
3344
|
+
v: 1
|
|
3345
|
+
};
|
|
2673
3346
|
}
|
|
2674
|
-
|
|
2675
|
-
return { imported: true, operation };
|
|
3347
|
+
return { head, imported: operations.length, status: "imported", v: 1 };
|
|
2676
3348
|
});
|
|
2677
3349
|
}
|
|
2678
3350
|
exportOperations(afterSequence = 0, limit = 1000) {
|
|
@@ -2890,6 +3562,10 @@ class OhSqliteStore {
|
|
|
2890
3562
|
const integrity = this.database.query("PRAGMA integrity_check").get();
|
|
2891
3563
|
if (integrity?.integrity_check !== "ok")
|
|
2892
3564
|
throw new OhIntegrityError("SQLite integrity_check failed.");
|
|
3565
|
+
const foreignKeyViolations = this.database.query("PRAGMA foreign_key_check").all();
|
|
3566
|
+
if (foreignKeyViolations.length !== 0) {
|
|
3567
|
+
throw new OhIntegrityError("SQLite foreign_key_check failed.");
|
|
3568
|
+
}
|
|
2893
3569
|
const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0;
|
|
2894
3570
|
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 }));
|
|
2895
3571
|
if (operations.length !== storedCount)
|
|
@@ -2966,6 +3642,22 @@ class OhSqliteStore {
|
|
|
2966
3642
|
if (canonicalJson(storedDependencies) !== canonicalJson(expectedDependencies)) {
|
|
2967
3643
|
throw new OhIntegrityError("Materialized dependencies do not match operation replay.");
|
|
2968
3644
|
}
|
|
3645
|
+
const expectedSearchDocuments = [...records.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((record) => ({
|
|
3646
|
+
record_key: record.key,
|
|
3647
|
+
record_sha256: record.recordSha256,
|
|
3648
|
+
text: `${record.key} ${record.kind} ${extractSearchText(record.value)}`
|
|
3649
|
+
}));
|
|
3650
|
+
const storedSearchDocuments = this.database.query(`SELECT record_key, record_sha256, text FROM oh_search_documents
|
|
3651
|
+
WHERE space_id = ? ORDER BY record_key`).all(this.spaceId);
|
|
3652
|
+
if (canonicalJson(storedSearchDocuments) !== canonicalJson(expectedSearchDocuments)) {
|
|
3653
|
+
throw new OhIntegrityError("Materialized search documents do not match operation replay.");
|
|
3654
|
+
}
|
|
3655
|
+
const expectedSearchFts = expectedSearchDocuments.map(({ record_key, text }) => ({ record_key, text }));
|
|
3656
|
+
const storedSearchFts = this.database.query(`SELECT record_key, text FROM oh_search_fts
|
|
3657
|
+
WHERE space_id = ? ORDER BY record_key, text, rowid`).all(this.spaceId);
|
|
3658
|
+
if (canonicalJson(storedSearchFts) !== canonicalJson(expectedSearchFts)) {
|
|
3659
|
+
throw new OhIntegrityError("Materialized full-text search rows do not match operation replay.");
|
|
3660
|
+
}
|
|
2969
3661
|
const storedOperationRecords = this.database.query(`SELECT materialized.operation_sha256, materialized.ordinal, materialized.record_key,
|
|
2970
3662
|
materialized.change_kind, materialized.record_sha256
|
|
2971
3663
|
FROM oh_operation_records AS materialized
|
|
@@ -3143,8 +3835,8 @@ class Oh {
|
|
|
3143
3835
|
}
|
|
3144
3836
|
|
|
3145
3837
|
// src/cli.ts
|
|
3146
|
-
import { readFile } from "fs/promises";
|
|
3147
|
-
var OH_PACKAGE_VERSION = "0.
|
|
3838
|
+
import { lstat, readFile } from "fs/promises";
|
|
3839
|
+
var OH_PACKAGE_VERSION = "0.4.0";
|
|
3148
3840
|
var KNOWN_OPTIONS = new Set([
|
|
3149
3841
|
"actor",
|
|
3150
3842
|
"after",
|
|
@@ -3314,7 +4006,7 @@ async function validateInvocation(command, parsed) {
|
|
|
3314
4006
|
const file = one(parsed, "file");
|
|
3315
4007
|
if (file === undefined)
|
|
3316
4008
|
throw new TypeError("sync import needs --file.");
|
|
3317
|
-
syncBundle = parseOhSyncBundleV1(
|
|
4009
|
+
syncBundle = parseOhSyncBundleV1(await readSyncBundleFile(file));
|
|
3318
4010
|
if (syncBundle === null)
|
|
3319
4011
|
throw new TypeError("Invalid sync bundle.");
|
|
3320
4012
|
} else {
|
|
@@ -3333,6 +4025,18 @@ function print(value) {
|
|
|
3333
4025
|
process.stdout.write(`${canonicalJson(value)}
|
|
3334
4026
|
`);
|
|
3335
4027
|
}
|
|
4028
|
+
async function readSyncBundleFile(path) {
|
|
4029
|
+
const maximumFileBytes = OH_SYNC_BUNDLE_MAX_BYTES_V1 + 1;
|
|
4030
|
+
const metadata = await lstat(path);
|
|
4031
|
+
if (!metadata.isFile() || !Number.isSafeInteger(metadata.size) || metadata.size > maximumFileBytes) {
|
|
4032
|
+
throw new RangeError(`Sync bundle file must be a regular file of at most ${maximumFileBytes} bytes.`);
|
|
4033
|
+
}
|
|
4034
|
+
const contents = await readFile(path);
|
|
4035
|
+
if (contents.byteLength > maximumFileBytes) {
|
|
4036
|
+
throw new RangeError(`Sync bundle file must be at most ${maximumFileBytes} bytes.`);
|
|
4037
|
+
}
|
|
4038
|
+
return JSON.parse(contents.toString("utf8"));
|
|
4039
|
+
}
|
|
3336
4040
|
var HELP = `oh ${OH_PACKAGE_VERSION}
|
|
3337
4041
|
|
|
3338
4042
|
Usage:
|
|
@@ -3455,18 +4159,28 @@ async function runOhCli(arguments_) {
|
|
|
3455
4159
|
if (action === "export") {
|
|
3456
4160
|
const after = integer(one(parsed, "after"), "after") ?? 0;
|
|
3457
4161
|
const limit = integer(one(parsed, "limit"), "limit") ?? 1000;
|
|
3458
|
-
print(createOhSyncBundleV1(oh.store.spaceId, oh.store.exportOperations(after, limit)
|
|
4162
|
+
print(createOhSyncBundleV1(oh.store.spaceId, oh.store.exportOperations(after, limit), {
|
|
4163
|
+
largestFittingPrefix: true
|
|
4164
|
+
}));
|
|
3459
4165
|
return 0;
|
|
3460
4166
|
}
|
|
3461
4167
|
if (action === "import") {
|
|
3462
4168
|
const bundle = validated.syncBundle;
|
|
3463
4169
|
if (bundle === null)
|
|
3464
4170
|
throw new TypeError("Invalid prepared sync import command.");
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
4171
|
+
const first = bundle.operations[0];
|
|
4172
|
+
if (first === undefined) {
|
|
4173
|
+
print({ head: oh.head(), imported: 0, v: 1 });
|
|
4174
|
+
return 0;
|
|
4175
|
+
}
|
|
4176
|
+
const imported = oh.store.importOperations({
|
|
4177
|
+
expectedHead: {
|
|
4178
|
+
operationSha256: first.parentOperationSha256,
|
|
4179
|
+
sequence: first.sequence - 1
|
|
4180
|
+
},
|
|
4181
|
+
operations: bundle.operations
|
|
4182
|
+
});
|
|
4183
|
+
print({ head: imported.head, imported: imported.imported, v: 1 });
|
|
3470
4184
|
return 0;
|
|
3471
4185
|
}
|
|
3472
4186
|
throw new TypeError("sync needs export or import.");
|