@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/sdk.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// src/sync.ts
|
|
3
|
+
import { isProxy } from "util/types";
|
|
4
|
+
|
|
2
5
|
// src/canonical.ts
|
|
3
6
|
import { createHash, randomBytes } from "crypto";
|
|
4
7
|
|
|
@@ -197,7 +200,7 @@ var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
|
197
200
|
recordBytes: 1024 * 1024,
|
|
198
201
|
recordsPerSnapshot: 65536
|
|
199
202
|
});
|
|
200
|
-
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
203
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = Object.freeze([
|
|
201
204
|
"activity",
|
|
202
205
|
"assertion",
|
|
203
206
|
"context",
|
|
@@ -216,7 +219,7 @@ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
|
216
219
|
"type-membership",
|
|
217
220
|
"view",
|
|
218
221
|
"vocabulary"
|
|
219
|
-
];
|
|
222
|
+
]);
|
|
220
223
|
var KNOWLEDGE_GRAPH_RECORD_KEYS_V1 = [
|
|
221
224
|
"dependencies",
|
|
222
225
|
"key",
|
|
@@ -1142,6 +1145,142 @@ class OhRecordCodecRegistry {
|
|
|
1142
1145
|
}
|
|
1143
1146
|
}
|
|
1144
1147
|
|
|
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
|
+
|
|
1145
1284
|
// src/operation.ts
|
|
1146
1285
|
var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
|
|
1147
1286
|
function parsePayload(value) {
|
|
@@ -1189,13 +1328,18 @@ function parsePayload(value) {
|
|
|
1189
1328
|
v: 1
|
|
1190
1329
|
} : null;
|
|
1191
1330
|
}
|
|
1192
|
-
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
|
+
}
|
|
1193
1336
|
const payload = parsePayload(input);
|
|
1194
1337
|
if (payload === null)
|
|
1195
1338
|
throw new TypeError("Invalid Oh operation payload.");
|
|
1196
1339
|
const operation = { ...payload, operationSha256: canonicalSha256(payload) };
|
|
1197
|
-
|
|
1198
|
-
|
|
1340
|
+
const operationBytes = Buffer.byteLength(canonicalJson(operation), "utf8");
|
|
1341
|
+
if (operationBytes > maximumOperationBytes) {
|
|
1342
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
1199
1343
|
}
|
|
1200
1344
|
return operation;
|
|
1201
1345
|
}
|
|
@@ -1210,18 +1354,362 @@ function parseOhOperationV1(value) {
|
|
|
1210
1354
|
|
|
1211
1355
|
// src/sync.ts
|
|
1212
1356
|
var OH_SYNC_PROTOCOL_V1 = "oh.sync.v1";
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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) {
|
|
1217
1695
|
let priorSequence = null;
|
|
1218
1696
|
let priorSha256 = null;
|
|
1219
1697
|
const parsed = [];
|
|
1698
|
+
const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
|
|
1220
1699
|
for (const candidate of operations) {
|
|
1221
1700
|
const operation = parseOhOperationV1(candidate);
|
|
1222
1701
|
if (operation === null || operation.spaceId !== parsedSpaceId || priorSequence !== null && operation.sequence !== priorSequence + 1 || priorSequence !== null && operation.parentOperationSha256 !== priorSha256) {
|
|
1223
1702
|
throw new TypeError("Sync operations must form one ordered chain.");
|
|
1224
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
|
+
}
|
|
1225
1713
|
parsed.push(operation);
|
|
1226
1714
|
priorSequence = operation.sequence;
|
|
1227
1715
|
priorSha256 = operation.operationSha256;
|
|
@@ -1235,15 +1723,56 @@ function createOhSyncBundleV1(spaceId, operations) {
|
|
|
1235
1723
|
};
|
|
1236
1724
|
return { ...payload, bundleSha256: canonicalSha256(payload) };
|
|
1237
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
|
+
}
|
|
1238
1750
|
function parseOhSyncBundleV1(value) {
|
|
1239
|
-
|
|
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)
|
|
1240
1758
|
return null;
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256)
|
|
1759
|
+
const operations = exactDataArrayV1(envelope.operations, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1);
|
|
1760
|
+
if (operations === null)
|
|
1244
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
|
+
}
|
|
1245
1772
|
try {
|
|
1246
|
-
const created = createOhSyncBundleV1(
|
|
1773
|
+
const created = createOhSyncBundleV1(spaceId, detachedOperations);
|
|
1774
|
+
if (detachedOperations.some((operation, index) => canonicalJson(operation) !== canonicalJson(created.operations[index])))
|
|
1775
|
+
return null;
|
|
1247
1776
|
return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
|
|
1248
1777
|
} catch {
|
|
1249
1778
|
return null;
|
|
@@ -1258,39 +1787,84 @@ async function synchronizeOhStoreV1(store, transport, options = {}) {
|
|
|
1258
1787
|
await transport.handshake(OH_CONTRACT_MANIFEST_V1);
|
|
1259
1788
|
let pulled = 0;
|
|
1260
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
|
+
};
|
|
1261
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.");
|
|
1262
1802
|
const local = store.head();
|
|
1263
|
-
const remote = await transport.head(store.spaceId);
|
|
1264
1803
|
if (local.sequence === remote.sequence) {
|
|
1265
1804
|
if (local.operationSha256 !== remote.operationSha256) {
|
|
1266
1805
|
throw new Error("Sync conflict: equal sequence numbers have different heads.");
|
|
1267
1806
|
}
|
|
1268
|
-
|
|
1269
|
-
pulledSequence: local.sequence,
|
|
1270
|
-
pushedSequence: local.sequence,
|
|
1271
|
-
remoteHeadSha256: remote.operationSha256
|
|
1272
|
-
});
|
|
1273
|
-
return { head: remote, pulled, pushed, rounds: round, v: 1 };
|
|
1807
|
+
return settled(remote, round);
|
|
1274
1808
|
}
|
|
1275
1809
|
if (local.sequence < remote.sequence) {
|
|
1276
1810
|
const bundle = parseOhSyncBundleV1(await transport.pull(store.spaceId, local.sequence, batchSize));
|
|
1277
|
-
|
|
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) {
|
|
1278
1813
|
throw new Error("Sync conflict: remote history does not extend the local head.");
|
|
1279
1814
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
+
}
|
|
1283
1832
|
}
|
|
1284
1833
|
} else {
|
|
1285
|
-
const
|
|
1286
|
-
|
|
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) {
|
|
1287
1845
|
throw new Error("Sync conflict: local history does not extend the remote head.");
|
|
1288
1846
|
}
|
|
1289
|
-
const
|
|
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.");
|
|
1290
1854
|
if (head.sequence !== operations.at(-1)?.sequence || head.operationSha256 !== operations.at(-1)?.operationSha256) {
|
|
1291
1855
|
throw new Error("The sync transport acknowledged a different head.");
|
|
1292
1856
|
}
|
|
1293
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
|
+
}
|
|
1294
1868
|
}
|
|
1295
1869
|
}
|
|
1296
1870
|
throw new Error("Sync did not settle within maximumRounds.");
|
|
@@ -1354,19 +1928,48 @@ function createLibSqlOperationSyncTransportV1(client) {
|
|
|
1354
1928
|
},
|
|
1355
1929
|
head,
|
|
1356
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
|
+
}
|
|
1357
1935
|
await ensure();
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
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) => {
|
|
1361
1961
|
const json = rowValue(row, "operation_json", 0);
|
|
1362
1962
|
if (typeof json !== "string")
|
|
1363
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
|
+
}
|
|
1364
1967
|
const operation = parseOhOperationV1(JSON.parse(json));
|
|
1365
1968
|
if (operation === null || canonicalJson(operation) !== json)
|
|
1366
1969
|
throw new Error("Invalid remote operation.");
|
|
1367
1970
|
return operation;
|
|
1368
1971
|
});
|
|
1369
|
-
return createOhSyncBundleV1(
|
|
1972
|
+
return createOhSyncBundleV1(parsedSpaceId, operations);
|
|
1370
1973
|
},
|
|
1371
1974
|
push: async (value) => {
|
|
1372
1975
|
await ensure();
|
|
@@ -1378,8 +1981,35 @@ function createLibSqlOperationSyncTransportV1(client) {
|
|
|
1378
1981
|
const remote = await head(bundle.spaceId);
|
|
1379
1982
|
const first = bundle.operations[0];
|
|
1380
1983
|
const last = bundle.operations.at(-1);
|
|
1381
|
-
if (remote.sequence
|
|
1382
|
-
|
|
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
|
+
}
|
|
1383
2013
|
if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
|
|
1384
2014
|
throw new Error("Sync conflict: pushed history does not extend the remote head.");
|
|
1385
2015
|
}
|
|
@@ -1441,35 +2071,7 @@ async function searchOhV1(input) {
|
|
|
1441
2071
|
// src/sqlite/store.ts
|
|
1442
2072
|
import { mkdirSync } from "fs";
|
|
1443
2073
|
import { dirname } from "path";
|
|
1444
|
-
|
|
1445
2074
|
// src/store.ts
|
|
1446
|
-
class OhConflictError extends Error {
|
|
1447
|
-
constructor(message) {
|
|
1448
|
-
super(message);
|
|
1449
|
-
this.name = "OhConflictError";
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
class OhIntegrityError extends Error {
|
|
1454
|
-
constructor(message) {
|
|
1455
|
-
super(message);
|
|
1456
|
-
this.name = "OhIntegrityError";
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
class OhDependencyError extends Error {
|
|
1461
|
-
constructor(message) {
|
|
1462
|
-
super(message);
|
|
1463
|
-
this.name = "OhDependencyError";
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
class OhProfileError extends Error {
|
|
1468
|
-
constructor(message) {
|
|
1469
|
-
super(message);
|
|
1470
|
-
this.name = "OhProfileError";
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
2075
|
var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
1474
2076
|
applicationProfileSha256: null,
|
|
1475
2077
|
capabilities: {
|
|
@@ -1782,6 +2384,8 @@ function transitionOhSnapshotV1(input) {
|
|
|
1782
2384
|
sequence: head.sequence + 1,
|
|
1783
2385
|
spaceId,
|
|
1784
2386
|
v: 1
|
|
2387
|
+
}, input.maximumOperationBytes === undefined ? {} : {
|
|
2388
|
+
maximumOperationBytes: input.maximumOperationBytes
|
|
1785
2389
|
});
|
|
1786
2390
|
const nextHead = {
|
|
1787
2391
|
generation: operation.sequence,
|
|
@@ -2357,6 +2961,9 @@ function normalizeLimit(value, fallback = 50, maximum = 1000) {
|
|
|
2357
2961
|
}
|
|
2358
2962
|
return value;
|
|
2359
2963
|
}
|
|
2964
|
+
function exactHeadRef(left, right) {
|
|
2965
|
+
return left.sequence === right.sequence && left.operationSha256 === right.operationSha256;
|
|
2966
|
+
}
|
|
2360
2967
|
function ftsQuery(value) {
|
|
2361
2968
|
const normalized = boundedText(value.normalize("NFC"), 4096);
|
|
2362
2969
|
if (normalized === null)
|
|
@@ -2473,8 +3080,7 @@ class OhSqliteStore {
|
|
|
2473
3080
|
}
|
|
2474
3081
|
return records;
|
|
2475
3082
|
}
|
|
2476
|
-
#transition(head, changes, operationId) {
|
|
2477
|
-
const records = this.#loadRecords();
|
|
3083
|
+
#transition(head, changes, operationId, records = this.#loadRecords()) {
|
|
2478
3084
|
for (const change of changes) {
|
|
2479
3085
|
if (change.kind === "put") {
|
|
2480
3086
|
records.set(change.record.key, change.record);
|
|
@@ -2531,7 +3137,7 @@ class OhSqliteStore {
|
|
|
2531
3137
|
if (operation.sequence < 1 || operation.sequence > head.sequence) {
|
|
2532
3138
|
throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head.");
|
|
2533
3139
|
}
|
|
2534
|
-
const rows = this.database.query(`SELECT
|
|
3140
|
+
const rows = this.database.query(`SELECT ${OPERATION_COLUMNS}
|
|
2535
3141
|
FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence);
|
|
2536
3142
|
if (rows.length !== head.sequence - operation.sequence + 1) {
|
|
2537
3143
|
throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head.");
|
|
@@ -2539,10 +3145,14 @@ class OhSqliteStore {
|
|
|
2539
3145
|
let priorSha256 = operation.parentOperationSha256;
|
|
2540
3146
|
for (let index = 0;index < rows.length; index += 1) {
|
|
2541
3147
|
const row = rows[index];
|
|
2542
|
-
if (row === undefined
|
|
3148
|
+
if (row === undefined) {
|
|
3149
|
+
throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
|
|
3150
|
+
}
|
|
3151
|
+
const reachable = parseStoredOperationRow(row, { spaceId: this.spaceId });
|
|
3152
|
+
if (reachable.sequence !== operation.sequence + index || reachable.parentOperationSha256 !== priorSha256 || index === 0 && reachable.operationSha256 !== operation.operationSha256) {
|
|
2543
3153
|
throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
|
|
2544
3154
|
}
|
|
2545
|
-
priorSha256 =
|
|
3155
|
+
priorSha256 = reachable.operationSha256;
|
|
2546
3156
|
}
|
|
2547
3157
|
if (priorSha256 !== head.operationSha256) {
|
|
2548
3158
|
throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest.");
|
|
@@ -2603,8 +3213,10 @@ class OhSqliteStore {
|
|
|
2603
3213
|
this.#assertOpen();
|
|
2604
3214
|
const actorId = safeCode(input.actorId);
|
|
2605
3215
|
const operationId = safeCode(input.operationId);
|
|
2606
|
-
|
|
2607
|
-
|
|
3216
|
+
const maximumOperationBytes = input.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
|
|
3217
|
+
if (actorId === null || operationId === null || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
3218
|
+
throw new TypeError("Invalid actor, operation ID, or operation byte bound.");
|
|
3219
|
+
}
|
|
2608
3220
|
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
2609
3221
|
if (changes.length === 0 || changes.length > 8192)
|
|
2610
3222
|
throw new TypeError("A commit needs 1 through 8192 changes.");
|
|
@@ -2618,6 +3230,10 @@ class OhSqliteStore {
|
|
|
2618
3230
|
if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) {
|
|
2619
3231
|
throw new OhConflictError("The operation ID is already bound to different content.");
|
|
2620
3232
|
}
|
|
3233
|
+
const operationBytes = Buffer.byteLength(canonicalJson(existing), "utf8");
|
|
3234
|
+
if (operationBytes > maximumOperationBytes) {
|
|
3235
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
3236
|
+
}
|
|
2621
3237
|
return existing;
|
|
2622
3238
|
}
|
|
2623
3239
|
if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) {
|
|
@@ -2636,6 +3252,8 @@ class OhSqliteStore {
|
|
|
2636
3252
|
sequence: head.sequence + 1,
|
|
2637
3253
|
spaceId: this.spaceId,
|
|
2638
3254
|
v: 1
|
|
3255
|
+
}, {
|
|
3256
|
+
maximumOperationBytes
|
|
2639
3257
|
});
|
|
2640
3258
|
this.#persist(operation);
|
|
2641
3259
|
return operation;
|
|
@@ -2647,30 +3265,84 @@ class OhSqliteStore {
|
|
|
2647
3265
|
const operation = parseOhOperationV1(value);
|
|
2648
3266
|
if (operation === null || operation.spaceId !== this.spaceId)
|
|
2649
3267
|
throw new OhIntegrityError("Invalid imported operation.");
|
|
3268
|
+
const result = this.importOperations({
|
|
3269
|
+
expectedHead: {
|
|
3270
|
+
operationSha256: operation.parentOperationSha256,
|
|
3271
|
+
sequence: operation.sequence - 1
|
|
3272
|
+
},
|
|
3273
|
+
operations: [operation]
|
|
3274
|
+
});
|
|
3275
|
+
return { imported: result.imported === 1, operation };
|
|
3276
|
+
}
|
|
3277
|
+
importOperations(input) {
|
|
3278
|
+
this.#assertOpen();
|
|
3279
|
+
this.#assertOperationReplication();
|
|
3280
|
+
const expectedHead = parseOhHeadRefV1(input.expectedHead);
|
|
3281
|
+
if (expectedHead === null || !Array.isArray(input.operations) || input.operations.length > 1000) {
|
|
3282
|
+
throw new TypeError("Invalid operation import interval.");
|
|
3283
|
+
}
|
|
3284
|
+
const operations = input.operations.map((value) => {
|
|
3285
|
+
const operation = parseOhOperationV1(value);
|
|
3286
|
+
if (operation === null || operation.spaceId !== this.spaceId) {
|
|
3287
|
+
throw new OhIntegrityError("Invalid imported operation.");
|
|
3288
|
+
}
|
|
3289
|
+
return operation;
|
|
3290
|
+
});
|
|
3291
|
+
let prior = expectedHead;
|
|
3292
|
+
for (const operation of operations) {
|
|
3293
|
+
if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) {
|
|
3294
|
+
throw new OhConflictError("Imported operations do not extend the expected head.");
|
|
3295
|
+
}
|
|
3296
|
+
prior = { operationSha256: operation.operationSha256, sequence: operation.sequence };
|
|
3297
|
+
}
|
|
2650
3298
|
return withImmediateTransaction(this.database, () => {
|
|
2651
|
-
const
|
|
2652
|
-
this.#assertCurrentHeadAuthority(
|
|
2653
|
-
|
|
2654
|
-
if (
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
spaceId: this.spaceId
|
|
2658
|
-
});
|
|
2659
|
-
this.#assertOperationReachable(existing, head);
|
|
2660
|
-
if (canonicalJson(existing) !== canonicalJson(operation)) {
|
|
2661
|
-
throw new OhIntegrityError("An operation digest is bound to different bytes.");
|
|
3299
|
+
const current = this.head();
|
|
3300
|
+
this.#assertCurrentHeadAuthority(current);
|
|
3301
|
+
this.#headAt(expectedHead);
|
|
3302
|
+
if (!exactHeadRef(current, expectedHead)) {
|
|
3303
|
+
if (operations.length === 0 || current.sequence < prior.sequence) {
|
|
3304
|
+
throw new OhConflictError("The imported operation interval does not extend the local head.");
|
|
2662
3305
|
}
|
|
2663
|
-
|
|
3306
|
+
for (const operation of operations) {
|
|
3307
|
+
const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, operation.sequence);
|
|
3308
|
+
if (row === null)
|
|
3309
|
+
throw new OhIntegrityError("An imported replay is missing from the authority chain.");
|
|
3310
|
+
const existing = parseStoredOperationRow(row, { spaceId: this.spaceId });
|
|
3311
|
+
if (existing.operationSha256 !== operation.operationSha256) {
|
|
3312
|
+
throw new OhConflictError("The imported operation interval diverges from the local authority chain.");
|
|
3313
|
+
}
|
|
3314
|
+
if (canonicalJson(existing) !== canonicalJson(operation)) {
|
|
3315
|
+
throw new OhIntegrityError("An operation digest is bound to different bytes.");
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
this.#assertOperationReachable(operations[operations.length - 1], current);
|
|
3319
|
+
return { head: current, imported: 0, status: "already-present", v: 1 };
|
|
2664
3320
|
}
|
|
2665
|
-
if (
|
|
2666
|
-
|
|
3321
|
+
if (operations.length === 0) {
|
|
3322
|
+
return { head: current, imported: 0, status: "already-present", v: 1 };
|
|
2667
3323
|
}
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2670
|
-
|
|
3324
|
+
const records = this.#loadRecords();
|
|
3325
|
+
let head = current;
|
|
3326
|
+
for (const operation of operations) {
|
|
3327
|
+
const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operation.operationId);
|
|
3328
|
+
if (duplicate !== null) {
|
|
3329
|
+
throw new OhConflictError("An imported operation ID is already bound on the local authority chain.");
|
|
3330
|
+
}
|
|
3331
|
+
const transition = this.#transition(head, operation.changes, operation.operationId, records);
|
|
3332
|
+
if (transition.recordsSha256 !== operation.recordsSha256 || transition.graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
3333
|
+
throw new OhIntegrityError("An imported operation does not reproduce its declared graph head.");
|
|
3334
|
+
}
|
|
3335
|
+
this.#persist(operation);
|
|
3336
|
+
head = {
|
|
3337
|
+
generation: operation.sequence,
|
|
3338
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
3339
|
+
operationSha256: operation.operationSha256,
|
|
3340
|
+
recordsSha256: operation.recordsSha256,
|
|
3341
|
+
sequence: operation.sequence,
|
|
3342
|
+
v: 1
|
|
3343
|
+
};
|
|
2671
3344
|
}
|
|
2672
|
-
|
|
2673
|
-
return { imported: true, operation };
|
|
3345
|
+
return { head, imported: operations.length, status: "imported", v: 1 };
|
|
2674
3346
|
});
|
|
2675
3347
|
}
|
|
2676
3348
|
exportOperations(afterSequence = 0, limit = 1000) {
|
|
@@ -2888,6 +3560,10 @@ class OhSqliteStore {
|
|
|
2888
3560
|
const integrity = this.database.query("PRAGMA integrity_check").get();
|
|
2889
3561
|
if (integrity?.integrity_check !== "ok")
|
|
2890
3562
|
throw new OhIntegrityError("SQLite integrity_check failed.");
|
|
3563
|
+
const foreignKeyViolations = this.database.query("PRAGMA foreign_key_check").all();
|
|
3564
|
+
if (foreignKeyViolations.length !== 0) {
|
|
3565
|
+
throw new OhIntegrityError("SQLite foreign_key_check failed.");
|
|
3566
|
+
}
|
|
2891
3567
|
const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0;
|
|
2892
3568
|
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 }));
|
|
2893
3569
|
if (operations.length !== storedCount)
|
|
@@ -2964,6 +3640,22 @@ class OhSqliteStore {
|
|
|
2964
3640
|
if (canonicalJson(storedDependencies) !== canonicalJson(expectedDependencies)) {
|
|
2965
3641
|
throw new OhIntegrityError("Materialized dependencies do not match operation replay.");
|
|
2966
3642
|
}
|
|
3643
|
+
const expectedSearchDocuments = [...records.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((record) => ({
|
|
3644
|
+
record_key: record.key,
|
|
3645
|
+
record_sha256: record.recordSha256,
|
|
3646
|
+
text: `${record.key} ${record.kind} ${extractSearchText(record.value)}`
|
|
3647
|
+
}));
|
|
3648
|
+
const storedSearchDocuments = this.database.query(`SELECT record_key, record_sha256, text FROM oh_search_documents
|
|
3649
|
+
WHERE space_id = ? ORDER BY record_key`).all(this.spaceId);
|
|
3650
|
+
if (canonicalJson(storedSearchDocuments) !== canonicalJson(expectedSearchDocuments)) {
|
|
3651
|
+
throw new OhIntegrityError("Materialized search documents do not match operation replay.");
|
|
3652
|
+
}
|
|
3653
|
+
const expectedSearchFts = expectedSearchDocuments.map(({ record_key, text }) => ({ record_key, text }));
|
|
3654
|
+
const storedSearchFts = this.database.query(`SELECT record_key, text FROM oh_search_fts
|
|
3655
|
+
WHERE space_id = ? ORDER BY record_key, text, rowid`).all(this.spaceId);
|
|
3656
|
+
if (canonicalJson(storedSearchFts) !== canonicalJson(expectedSearchFts)) {
|
|
3657
|
+
throw new OhIntegrityError("Materialized full-text search rows do not match operation replay.");
|
|
3658
|
+
}
|
|
2967
3659
|
const storedOperationRecords = this.database.query(`SELECT materialized.operation_sha256, materialized.ordinal, materialized.record_key,
|
|
2968
3660
|
materialized.change_kind, materialized.record_sha256
|
|
2969
3661
|
FROM oh_operation_records AS materialized
|