@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/index.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)
|
|
1240
1753
|
return null;
|
|
1241
|
-
const bundleSha256 = parseSha256Hex(
|
|
1242
|
-
const contractSha256 = parseSha256Hex(
|
|
1243
|
-
|
|
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)
|
|
1244
1758
|
return null;
|
|
1759
|
+
const operations = exactDataArrayV1(envelope.operations, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1);
|
|
1760
|
+
if (operations === null)
|
|
1761
|
+
return null;
|
|
1762
|
+
const detachedOperations = [];
|
|
1763
|
+
const bundleBudget = syncIngressBundleBudgetV1(spaceId);
|
|
1764
|
+
for (const operation of operations) {
|
|
1765
|
+
if (detachedOperations.length > 0 && !spendSyncIngressBudgetV1(bundleBudget, 1))
|
|
1766
|
+
return null;
|
|
1767
|
+
const detached = stagedSyncOperationV1(operation, bundleBudget);
|
|
1768
|
+
if (detached === null)
|
|
1769
|
+
return null;
|
|
1770
|
+
detachedOperations.push(detached);
|
|
1771
|
+
}
|
|
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
|
}
|
|
@@ -1392,33 +2022,6 @@ function createLibSqlOperationSyncTransportV1(client) {
|
|
|
1392
2022
|
};
|
|
1393
2023
|
}
|
|
1394
2024
|
// src/store.ts
|
|
1395
|
-
class OhConflictError extends Error {
|
|
1396
|
-
constructor(message) {
|
|
1397
|
-
super(message);
|
|
1398
|
-
this.name = "OhConflictError";
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
class OhIntegrityError extends Error {
|
|
1403
|
-
constructor(message) {
|
|
1404
|
-
super(message);
|
|
1405
|
-
this.name = "OhIntegrityError";
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
class OhDependencyError extends Error {
|
|
1410
|
-
constructor(message) {
|
|
1411
|
-
super(message);
|
|
1412
|
-
this.name = "OhDependencyError";
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
class OhProfileError extends Error {
|
|
1417
|
-
constructor(message) {
|
|
1418
|
-
super(message);
|
|
1419
|
-
this.name = "OhProfileError";
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
2025
|
var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
1423
2026
|
applicationProfileSha256: null,
|
|
1424
2027
|
capabilities: {
|
|
@@ -1731,6 +2334,8 @@ function transitionOhSnapshotV1(input) {
|
|
|
1731
2334
|
sequence: head.sequence + 1,
|
|
1732
2335
|
spaceId,
|
|
1733
2336
|
v: 1
|
|
2337
|
+
}, input.maximumOperationBytes === undefined ? {} : {
|
|
2338
|
+
maximumOperationBytes: input.maximumOperationBytes
|
|
1734
2339
|
});
|
|
1735
2340
|
const nextHead = {
|
|
1736
2341
|
generation: operation.sequence,
|
|
@@ -1971,6 +2576,8 @@ export {
|
|
|
1971
2576
|
replayOhOperationsV1,
|
|
1972
2577
|
reduceKnowledgeGraphRevisionsV1,
|
|
1973
2578
|
parseSha256Hex,
|
|
2579
|
+
parseOhSyncHeadV1,
|
|
2580
|
+
parseOhSyncHeadRefV1,
|
|
1974
2581
|
parseOhSyncBundleV1,
|
|
1975
2582
|
parseOhStoreProfileV1,
|
|
1976
2583
|
parseOhStoreBindingV1,
|
|
@@ -2004,6 +2611,11 @@ export {
|
|
|
2004
2611
|
knowledgeSchemaRefV1,
|
|
2005
2612
|
knowledgeGraphRecordRefV1,
|
|
2006
2613
|
isPlainRecord,
|
|
2614
|
+
isOhProfileError,
|
|
2615
|
+
isOhOperationSizeError,
|
|
2616
|
+
isOhIntegrityError,
|
|
2617
|
+
isOhDependencyError,
|
|
2618
|
+
isOhConflictError,
|
|
2007
2619
|
hasExactKeys,
|
|
2008
2620
|
graphRevisionSha256V1,
|
|
2009
2621
|
emptyOhHeadV1,
|
|
@@ -2034,13 +2646,16 @@ export {
|
|
|
2034
2646
|
OhRecordCodecRegistry,
|
|
2035
2647
|
OhPurgedSpaceError,
|
|
2036
2648
|
OhProfileError,
|
|
2649
|
+
OhOperationSizeError,
|
|
2037
2650
|
OhIntegrityError,
|
|
2038
2651
|
OhDependencyError,
|
|
2039
2652
|
OhConflictError,
|
|
2040
2653
|
OH_WORKING_STORE_PROFILE_V1,
|
|
2041
2654
|
OH_SYNC_PROTOCOL_V1,
|
|
2655
|
+
OH_SYNC_BUNDLE_MAX_BYTES_V1,
|
|
2042
2656
|
OH_SCHEMA_KINDS_V1,
|
|
2043
2657
|
OH_SCHEMA_FORMAT_VERSION_V1,
|
|
2658
|
+
OH_OPERATION_SIZE_ERROR_CODE_V1,
|
|
2044
2659
|
OH_OPERATION_MAX_BYTES_V1,
|
|
2045
2660
|
OH_ONTOLOGY_VERSION_V1,
|
|
2046
2661
|
OH_KNOWLEDGE_LIMITS_V1,
|