js-bao 0.7.0-alpha.5 → 0.7.0-alpha.7
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/dist/browser.cjs +2006 -508
- package/dist/browser.d.cts +971 -9
- package/dist/browser.d.ts +971 -9
- package/dist/browser.js +2003 -505
- package/dist/client.cjs +104 -15
- package/dist/client.d.cts +417 -1
- package/dist/client.d.ts +417 -1
- package/dist/client.js +101 -12
- package/dist/cloudflare-do.cjs +5522 -4981
- package/dist/cloudflare-do.d.cts +766 -14
- package/dist/cloudflare-do.d.ts +766 -14
- package/dist/cloudflare-do.js +5429 -4888
- package/dist/cloudflare.cjs +233 -79
- package/dist/cloudflare.d.cts +520 -33
- package/dist/cloudflare.d.ts +520 -33
- package/dist/cloudflare.js +230 -76
- package/dist/codegen-v2.cjs +170 -56
- package/dist/codegen.cjs +1 -1
- package/dist/format2-worker.js +232 -1
- package/dist/index.cjs +1993 -495
- package/dist/index.d.cts +1135 -44
- package/dist/index.d.ts +1135 -44
- package/dist/index.js +1979 -492
- package/dist/node.cjs +2293 -795
- package/dist/node.d.cts +1114 -9
- package/dist/node.d.ts +1114 -9
- package/dist/node.js +2279 -792
- package/package.json +1 -1
package/dist/browser.cjs
CHANGED
|
@@ -1259,11 +1259,71 @@ var init_yjsValues = __esm({
|
|
|
1259
1259
|
}
|
|
1260
1260
|
});
|
|
1261
1261
|
|
|
1262
|
+
// src/utils/uniqueIndex.ts
|
|
1263
|
+
function buildUniqueKey(recordData, fields) {
|
|
1264
|
+
const keyParts = [];
|
|
1265
|
+
for (const field of fields) {
|
|
1266
|
+
const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
|
|
1267
|
+
if (value === null || value === void 0) {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
keyParts.push(String(value));
|
|
1271
|
+
}
|
|
1272
|
+
return keyParts.join("|");
|
|
1273
|
+
}
|
|
1274
|
+
function uniqueIndexMapName(modelName, constraintName) {
|
|
1275
|
+
return `_uniqueIdx_${modelName}_${constraintName}`;
|
|
1276
|
+
}
|
|
1277
|
+
function fieldUniqueConstraintName(modelName, fieldName) {
|
|
1278
|
+
return `${modelName}_${fieldName}_unique`;
|
|
1279
|
+
}
|
|
1280
|
+
function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
1281
|
+
const resolved = [];
|
|
1282
|
+
for (const [fieldName, options] of fields.entries()) {
|
|
1283
|
+
if (options.unique) {
|
|
1284
|
+
resolved.push({
|
|
1285
|
+
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
1286
|
+
fields: [fieldName]
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (customConstraints) {
|
|
1291
|
+
for (const constraint of customConstraints) {
|
|
1292
|
+
const missingField = constraint.fields.find(
|
|
1293
|
+
(field) => !fields.has(field)
|
|
1294
|
+
);
|
|
1295
|
+
if (missingField) {
|
|
1296
|
+
console.warn(
|
|
1297
|
+
`[defineModelSchema] Unique constraint "${constraint.name}" for model "${modelName}" references unknown field "${missingField}". Skipping.`
|
|
1298
|
+
);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
if (resolved.some((item) => item.name === constraint.name)) {
|
|
1302
|
+
console.warn(
|
|
1303
|
+
`[defineModelSchema] Duplicate unique constraint name "${constraint.name}" in model "${modelName}".`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
resolved.push({
|
|
1307
|
+
name: constraint.name,
|
|
1308
|
+
fields: [...constraint.fields]
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return resolved;
|
|
1313
|
+
}
|
|
1314
|
+
var Y2;
|
|
1315
|
+
var init_uniqueIndex = __esm({
|
|
1316
|
+
"src/utils/uniqueIndex.ts"() {
|
|
1317
|
+
"use strict";
|
|
1318
|
+
Y2 = __toESM(require("yjs"), 1);
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1262
1322
|
// src/models/writeLifecycle.ts
|
|
1263
1323
|
function ensureStringSetYMap(rm, fieldName) {
|
|
1264
1324
|
const nested = rm.get(fieldName);
|
|
1265
|
-
if (nested instanceof
|
|
1266
|
-
const fresh = new
|
|
1325
|
+
if (nested instanceof Y3.Map) return nested;
|
|
1326
|
+
const fresh = new Y3.Map();
|
|
1267
1327
|
if (nested && typeof nested === "object") {
|
|
1268
1328
|
for (const [member, marker] of Object.entries(
|
|
1269
1329
|
nested
|
|
@@ -1301,11 +1361,11 @@ function registerFunctionDefaultGenerator(name, fn) {
|
|
|
1301
1361
|
function resolveLiteralOrFunctionDefault(rawDefault) {
|
|
1302
1362
|
return typeof rawDefault === "function" ? rawDefault() : rawDefault;
|
|
1303
1363
|
}
|
|
1304
|
-
var
|
|
1364
|
+
var Y3, import_ulid, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
|
|
1305
1365
|
var init_writeLifecycle = __esm({
|
|
1306
1366
|
"src/models/writeLifecycle.ts"() {
|
|
1307
1367
|
"use strict";
|
|
1308
|
-
|
|
1368
|
+
Y3 = __toESM(require("yjs"), 1);
|
|
1309
1369
|
import_ulid = require("ulid");
|
|
1310
1370
|
KNOWN_FIELD_TYPE_MAP = {
|
|
1311
1371
|
string: true,
|
|
@@ -1362,6 +1422,10 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
1362
1422
|
synced = /* @__PURE__ */ new Set();
|
|
1363
1423
|
_syncedCache.set(yDoc, synced);
|
|
1364
1424
|
}
|
|
1425
|
+
writeModelMeta(yDoc, modelName, schema);
|
|
1426
|
+
synced.add(modelName);
|
|
1427
|
+
}
|
|
1428
|
+
function writeModelMeta(yDoc, modelName, schema) {
|
|
1365
1429
|
const meta = yDoc.getMap(`_meta_${modelName}`);
|
|
1366
1430
|
for (const [fieldName, fieldOpts] of schema.fields.entries()) {
|
|
1367
1431
|
syncFieldMeta(meta, fieldName, fieldOpts);
|
|
@@ -1372,7 +1436,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
1372
1436
|
if (compoundConstraints.length > 0) {
|
|
1373
1437
|
let constraints = meta.get("_constraints");
|
|
1374
1438
|
if (!constraints) {
|
|
1375
|
-
constraints = new
|
|
1439
|
+
constraints = new Y4.Map();
|
|
1376
1440
|
meta.set("_constraints", constraints);
|
|
1377
1441
|
}
|
|
1378
1442
|
for (const constraint of compoundConstraints) {
|
|
@@ -1383,19 +1447,18 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
1383
1447
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
1384
1448
|
let rels = meta.get("_relationships");
|
|
1385
1449
|
if (!rels) {
|
|
1386
|
-
rels = new
|
|
1450
|
+
rels = new Y4.Map();
|
|
1387
1451
|
meta.set("_relationships", rels);
|
|
1388
1452
|
}
|
|
1389
1453
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
1390
1454
|
syncRelationshipMeta(rels, relName, relConfig);
|
|
1391
1455
|
}
|
|
1392
1456
|
}
|
|
1393
|
-
synced.add(modelName);
|
|
1394
1457
|
}
|
|
1395
1458
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
1396
1459
|
let fieldMeta = metaMap.get(fieldName);
|
|
1397
1460
|
if (!fieldMeta) {
|
|
1398
|
-
fieldMeta = new
|
|
1461
|
+
fieldMeta = new Y4.Map();
|
|
1399
1462
|
metaMap.set(fieldName, fieldMeta);
|
|
1400
1463
|
}
|
|
1401
1464
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -1412,7 +1475,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
1412
1475
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
1413
1476
|
let cMeta = constraintsMap.get(constraint.name);
|
|
1414
1477
|
if (!cMeta) {
|
|
1415
|
-
cMeta = new
|
|
1478
|
+
cMeta = new Y4.Map();
|
|
1416
1479
|
constraintsMap.set(constraint.name, cMeta);
|
|
1417
1480
|
}
|
|
1418
1481
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -1422,7 +1485,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
1422
1485
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
1423
1486
|
let relMeta = relsMap.get(relName);
|
|
1424
1487
|
if (!relMeta) {
|
|
1425
|
-
relMeta = new
|
|
1488
|
+
relMeta = new Y4.Map();
|
|
1426
1489
|
relsMap.set(relName, relMeta);
|
|
1427
1490
|
}
|
|
1428
1491
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -1437,7 +1500,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
1437
1500
|
if (fieldName.startsWith("_")) continue;
|
|
1438
1501
|
let fieldMeta = meta.get(fieldName);
|
|
1439
1502
|
if (!fieldMeta) {
|
|
1440
|
-
fieldMeta = new
|
|
1503
|
+
fieldMeta = new Y4.Map();
|
|
1441
1504
|
meta.set(fieldName, fieldMeta);
|
|
1442
1505
|
}
|
|
1443
1506
|
if (!fieldMeta.has("type")) {
|
|
@@ -1453,44 +1516,19 @@ function setIfChanged(map, key, value) {
|
|
|
1453
1516
|
map.set(key, value);
|
|
1454
1517
|
}
|
|
1455
1518
|
}
|
|
1456
|
-
var
|
|
1519
|
+
var Y4, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
|
|
1457
1520
|
var init_metaSync = __esm({
|
|
1458
1521
|
"src/models/metaSync.ts"() {
|
|
1459
1522
|
"use strict";
|
|
1460
|
-
|
|
1523
|
+
Y4 = __toESM(require("yjs"), 1);
|
|
1461
1524
|
init_yjsValues();
|
|
1525
|
+
init_uniqueIndex();
|
|
1462
1526
|
init_writeLifecycle();
|
|
1463
1527
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
1464
1528
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
1465
1529
|
}
|
|
1466
1530
|
});
|
|
1467
1531
|
|
|
1468
|
-
// src/utils/uniqueIndex.ts
|
|
1469
|
-
function buildUniqueKey(recordData, fields) {
|
|
1470
|
-
const keyParts = [];
|
|
1471
|
-
for (const field of fields) {
|
|
1472
|
-
const value = recordData instanceof Y4.Map ? recordData.get(field) : recordData[field];
|
|
1473
|
-
if (value === null || value === void 0) {
|
|
1474
|
-
return null;
|
|
1475
|
-
}
|
|
1476
|
-
keyParts.push(String(value));
|
|
1477
|
-
}
|
|
1478
|
-
return keyParts.join("|");
|
|
1479
|
-
}
|
|
1480
|
-
function uniqueIndexMapName(modelName, constraintName) {
|
|
1481
|
-
return `_uniqueIdx_${modelName}_${constraintName}`;
|
|
1482
|
-
}
|
|
1483
|
-
function fieldUniqueConstraintName(modelName, fieldName) {
|
|
1484
|
-
return `${modelName}_${fieldName}_unique`;
|
|
1485
|
-
}
|
|
1486
|
-
var Y4;
|
|
1487
|
-
var init_uniqueIndex = __esm({
|
|
1488
|
-
"src/utils/uniqueIndex.ts"() {
|
|
1489
|
-
"use strict";
|
|
1490
|
-
Y4 = __toESM(require("yjs"), 1);
|
|
1491
|
-
}
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
1532
|
// src/utils/largeDocuments.ts
|
|
1495
1533
|
function encodeOverlaySegment(segment) {
|
|
1496
1534
|
const escaped = segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
@@ -1767,15 +1805,344 @@ var init_format2OfflineWindow = __esm({
|
|
|
1767
1805
|
overdueMs;
|
|
1768
1806
|
constructor(documentId, status) {
|
|
1769
1807
|
super(
|
|
1770
|
-
`Document '${documentId}' has not synced for ${Math.floor((status.windowMs + status.overdueMs) / DAY_MS)} days, past its ${status.windowDays}-day offline write window. It stays readable, and writes are accepted again once it syncs.`
|
|
1808
|
+
`Document '${documentId}' has not synced for ${Math.floor((status.windowMs + status.overdueMs) / DAY_MS)} days, past its ${status.windowDays}-day offline write window. It stays readable, and writes are accepted again once it syncs.`
|
|
1809
|
+
);
|
|
1810
|
+
this.name = "DocumentOfflineWindowError";
|
|
1811
|
+
this.documentId = documentId;
|
|
1812
|
+
this.lastSyncAt = status.lastSyncAt;
|
|
1813
|
+
this.windowDays = status.windowDays;
|
|
1814
|
+
this.overdueMs = status.overdueMs;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
// src/utils/snapshotChunks.ts
|
|
1821
|
+
function parseSnapshotChunkPath(path) {
|
|
1822
|
+
if (!path || typeof path !== "string") return null;
|
|
1823
|
+
const own = CHUNK_PATH_OWN.exec(path);
|
|
1824
|
+
if (own) return { model: own[1], index: Number(own[2]) };
|
|
1825
|
+
const sourced = CHUNK_PATH_SOURCED.exec(path);
|
|
1826
|
+
if (!sourced) return null;
|
|
1827
|
+
return {
|
|
1828
|
+
model: sourced[3],
|
|
1829
|
+
index: Number(sourced[4]),
|
|
1830
|
+
source: { epoch: Number(sourced[1]), buildId: sourced[2] }
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
function resolveChunkOrdinal(entry, index) {
|
|
1834
|
+
return typeof entry.ordinal === "number" ? entry.ordinal : index;
|
|
1835
|
+
}
|
|
1836
|
+
function collationRank(unit) {
|
|
1837
|
+
if (unit >= 57344) return unit - 2048;
|
|
1838
|
+
if (unit >= 55296) return unit + 8192;
|
|
1839
|
+
return unit;
|
|
1840
|
+
}
|
|
1841
|
+
function compareRecordIds(left, right) {
|
|
1842
|
+
if (left === right) return 0;
|
|
1843
|
+
const shared = Math.min(left.length, right.length);
|
|
1844
|
+
for (let index = 0; index < shared; index++) {
|
|
1845
|
+
const a = left.charCodeAt(index);
|
|
1846
|
+
const b = right.charCodeAt(index);
|
|
1847
|
+
if (a === b) continue;
|
|
1848
|
+
return collationRank(a) < collationRank(b) ? -1 : 1;
|
|
1849
|
+
}
|
|
1850
|
+
return left.length < right.length ? -1 : left.length > right.length ? 1 : 0;
|
|
1851
|
+
}
|
|
1852
|
+
function validateSnapshotManifest(manifest) {
|
|
1853
|
+
if (!manifest || typeof manifest !== "object") {
|
|
1854
|
+
throw new SnapshotManifestInvalidError("it is not an object");
|
|
1855
|
+
}
|
|
1856
|
+
const kind = manifest.kind;
|
|
1857
|
+
if (kind !== void 0 && kind !== "snapshot") {
|
|
1858
|
+
throw new SnapshotManifestInvalidError(
|
|
1859
|
+
`it is a ${String(kind)} manifest, not a snapshot manifest`
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
const version = manifest.version;
|
|
1863
|
+
if (!Number.isInteger(version)) {
|
|
1864
|
+
throw new SnapshotManifestInvalidError(`version ${String(version)} is not an integer`);
|
|
1865
|
+
}
|
|
1866
|
+
if (version > FORMAT2_MANIFEST_VERSION) {
|
|
1867
|
+
throw new SnapshotManifestUnsupportedError(version);
|
|
1868
|
+
}
|
|
1869
|
+
if (version < FORMAT2_MIN_MANIFEST_VERSION) {
|
|
1870
|
+
throw new SnapshotManifestInvalidError(
|
|
1871
|
+
`version ${version} is older than the large-document format (${FORMAT2_MIN_MANIFEST_VERSION})`
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
const chunks = manifest.chunks;
|
|
1875
|
+
if (!Array.isArray(chunks)) {
|
|
1876
|
+
throw new SnapshotManifestInvalidError("its chunk list is not an array");
|
|
1877
|
+
}
|
|
1878
|
+
const seenOrdinals = /* @__PURE__ */ new Set();
|
|
1879
|
+
const lastRangeEndPerModel = /* @__PURE__ */ new Map();
|
|
1880
|
+
let rows = 0;
|
|
1881
|
+
let bytes = 0;
|
|
1882
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
1883
|
+
const entry = chunks[index];
|
|
1884
|
+
const where = `chunk ${index} (${entry?.model ?? "?"})`;
|
|
1885
|
+
if (!entry || typeof entry !== "object") {
|
|
1886
|
+
throw new SnapshotManifestInvalidError(`${where} is not an object`);
|
|
1887
|
+
}
|
|
1888
|
+
if (entry.ordinal !== void 0 && !Number.isSafeInteger(entry.ordinal)) {
|
|
1889
|
+
throw new SnapshotManifestInvalidError(
|
|
1890
|
+
`${where} has ordinal ${String(entry.ordinal)}, which is not an integer`
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
const ordinal = resolveChunkOrdinal(entry, index);
|
|
1894
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 0) {
|
|
1895
|
+
throw new SnapshotManifestInvalidError(
|
|
1896
|
+
`${where} resolves to ordinal ${String(ordinal)}, which is not a non-negative integer`
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
if (seenOrdinals.has(ordinal)) {
|
|
1900
|
+
throw new SnapshotManifestInvalidError(
|
|
1901
|
+
`${where} shares ordinal ${ordinal} with an earlier chunk`
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
seenOrdinals.add(ordinal);
|
|
1905
|
+
if (entry.path !== void 0 && !parseSnapshotChunkPath(entry.path)) {
|
|
1906
|
+
throw new SnapshotManifestInvalidError(
|
|
1907
|
+
`${where} has a path that is not addressable: ${String(entry.path)}`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
if (entry.rows > 0 && compareRecordIds(entry.firstId, entry.lastId) > 0) {
|
|
1911
|
+
throw new SnapshotManifestInvalidError(
|
|
1912
|
+
`${where} spans ${entry.firstId}..${entry.lastId}, which runs backwards`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
if (entry.rows > 0) {
|
|
1916
|
+
const previousEnd = lastRangeEndPerModel.get(entry.model);
|
|
1917
|
+
if (previousEnd !== void 0 && compareRecordIds(entry.firstId, previousEnd) <= 0) {
|
|
1918
|
+
throw new SnapshotManifestInvalidError(
|
|
1919
|
+
`${where} starts at ${entry.firstId}, at or before ${previousEnd} where the previous chunk of ${entry.model} ended`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
lastRangeEndPerModel.set(entry.model, entry.lastId);
|
|
1923
|
+
}
|
|
1924
|
+
rows += Number(entry.rows) || 0;
|
|
1925
|
+
bytes += Number(entry.bytes) || 0;
|
|
1926
|
+
}
|
|
1927
|
+
const totals = manifest.totals;
|
|
1928
|
+
if (!totals || totals.rows !== rows || totals.bytes !== bytes) {
|
|
1929
|
+
throw new SnapshotManifestInvalidError(
|
|
1930
|
+
`its totals say ${totals?.rows}/${totals?.bytes} rows/bytes but its chunks add up to ${rows}/${bytes}`
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
validateIngestInfo(manifest.ingest);
|
|
1934
|
+
}
|
|
1935
|
+
function validateIngestInfo(ingest) {
|
|
1936
|
+
if (ingest === void 0) return;
|
|
1937
|
+
if (!ingest || typeof ingest !== "object" || Array.isArray(ingest)) {
|
|
1938
|
+
throw new SnapshotManifestInvalidError("its ingest block is not an object");
|
|
1939
|
+
}
|
|
1940
|
+
if (typeof ingest.sessionId !== "string" || ingest.sessionId.length === 0) {
|
|
1941
|
+
throw new SnapshotManifestInvalidError(
|
|
1942
|
+
`its ingest block names session ${String(ingest.sessionId)}`
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
if (!Number.isSafeInteger(ingest.epoch) || ingest.epoch <= 0) {
|
|
1946
|
+
throw new SnapshotManifestInvalidError(
|
|
1947
|
+
`its ingest block names epoch ${String(ingest.epoch)}, which is not an epoch`
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
if (!Array.isArray(ingest.ranges)) {
|
|
1951
|
+
throw new SnapshotManifestInvalidError(
|
|
1952
|
+
"its ingest block's range list is not an array"
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
const lastPerModel = /* @__PURE__ */ new Map();
|
|
1956
|
+
for (let index = 0; index < ingest.ranges.length; index++) {
|
|
1957
|
+
const range = ingest.ranges[index];
|
|
1958
|
+
const where = `ingest range ${index} (${range?.model ?? "?"})`;
|
|
1959
|
+
if (!range || typeof range !== "object") {
|
|
1960
|
+
throw new SnapshotManifestInvalidError(`${where} is not an object`);
|
|
1961
|
+
}
|
|
1962
|
+
if (typeof range.model !== "string" || !MODEL_NAME.test(range.model)) {
|
|
1963
|
+
throw new SnapshotManifestInvalidError(
|
|
1964
|
+
`${where} names ${String(range.model)}, which is not a model name`
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
if (typeof range.firstId !== "string" || typeof range.lastId !== "string") {
|
|
1968
|
+
throw new SnapshotManifestInvalidError(`${where} has no id bounds`);
|
|
1969
|
+
}
|
|
1970
|
+
if (compareRecordIds(range.firstId, range.lastId) > 0) {
|
|
1971
|
+
throw new SnapshotManifestInvalidError(
|
|
1972
|
+
`${where} spans ${range.firstId}..${range.lastId}, which runs backwards`
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
const previous = lastPerModel.get(range.model);
|
|
1976
|
+
if (previous !== void 0 && compareRecordIds(range.firstId, previous) <= 0) {
|
|
1977
|
+
throw new SnapshotManifestInvalidError(
|
|
1978
|
+
`${where} starts at ${range.firstId}, at or before ${previous} where the previous range of ${range.model} ended`
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
lastPerModel.set(range.model, range.lastId);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
function snapshotStringSetFields(manifest, model) {
|
|
1985
|
+
const declared = manifest.schema?.[model]?.stringSetFields;
|
|
1986
|
+
return new Set(Array.isArray(declared) ? declared.map(String) : []);
|
|
1987
|
+
}
|
|
1988
|
+
function snapshotRowAsOverlayEntry(id, data, stringSetFields) {
|
|
1989
|
+
const fields = {};
|
|
1990
|
+
const stringSets = {};
|
|
1991
|
+
for (const [field, value] of Object.entries(data)) {
|
|
1992
|
+
if (field === "id") continue;
|
|
1993
|
+
if (stringSetFields.has(field) && Array.isArray(value)) {
|
|
1994
|
+
const members = {};
|
|
1995
|
+
for (const member of value) members[String(member)] = true;
|
|
1996
|
+
stringSets[field] = members;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
fields[field] = value;
|
|
2000
|
+
}
|
|
2001
|
+
return { id, fields, stringSets, replace: true, deleted: false };
|
|
2002
|
+
}
|
|
2003
|
+
async function gunzip(bytes) {
|
|
2004
|
+
return await collect(
|
|
2005
|
+
new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
async function collect(stream) {
|
|
2009
|
+
const parts = [];
|
|
2010
|
+
let total = 0;
|
|
2011
|
+
const reader = stream.getReader();
|
|
2012
|
+
for (; ; ) {
|
|
2013
|
+
const { done, value } = await reader.read();
|
|
2014
|
+
if (done) break;
|
|
2015
|
+
parts.push(value);
|
|
2016
|
+
total += value.byteLength;
|
|
2017
|
+
}
|
|
2018
|
+
const out = new Uint8Array(total);
|
|
2019
|
+
let at = 0;
|
|
2020
|
+
for (const part of parts) {
|
|
2021
|
+
out.set(part, at);
|
|
2022
|
+
at += part.byteLength;
|
|
2023
|
+
}
|
|
2024
|
+
return out;
|
|
2025
|
+
}
|
|
2026
|
+
async function sha256Hex(bytes) {
|
|
2027
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
2028
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2029
|
+
}
|
|
2030
|
+
async function readSnapshotChunk(body, descriptor) {
|
|
2031
|
+
const entry = "sha256" in descriptor ? descriptor : null;
|
|
2032
|
+
if (entry) {
|
|
2033
|
+
if (body.byteLength !== entry.bytes) {
|
|
2034
|
+
throw new SnapshotChunkIntegrityError(
|
|
2035
|
+
entry.key,
|
|
2036
|
+
`expected ${entry.bytes} bytes, got ${body.byteLength}`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
const digest = await sha256Hex(body);
|
|
2040
|
+
if (digest !== entry.sha256) {
|
|
2041
|
+
throw new SnapshotChunkIntegrityError(
|
|
2042
|
+
entry.key,
|
|
2043
|
+
`expected sha256 ${entry.sha256}, got ${digest}`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const text = decoder.decode(await gunzip(body));
|
|
2048
|
+
const rows = [];
|
|
2049
|
+
for (const line of text.split("\n")) {
|
|
2050
|
+
if (line.length === 0) continue;
|
|
2051
|
+
const tab = line.indexOf(" ");
|
|
2052
|
+
if (tab < 0) {
|
|
2053
|
+
throw new SnapshotChunkIntegrityError(
|
|
2054
|
+
entry?.key ?? descriptor.model,
|
|
2055
|
+
"a line carries no id/data separator"
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
rows.push({
|
|
2059
|
+
_type: descriptor.model,
|
|
2060
|
+
_id: line.slice(0, tab),
|
|
2061
|
+
_data: line.slice(tab + 1)
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
if (entry && rows.length !== entry.rows) {
|
|
2065
|
+
throw new SnapshotChunkIntegrityError(
|
|
2066
|
+
entry.key,
|
|
2067
|
+
`expected ${entry.rows} rows, got ${rows.length}`
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
if (entry && rows.length > 0) {
|
|
2071
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2072
|
+
for (const row of rows) {
|
|
2073
|
+
if (seen.has(row._id)) {
|
|
2074
|
+
throw new SnapshotChunkIntegrityError(
|
|
2075
|
+
entry.key,
|
|
2076
|
+
`duplicate id ${row._id}`
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
seen.add(row._id);
|
|
2080
|
+
}
|
|
2081
|
+
if (rows[0]._id !== entry.firstId) {
|
|
2082
|
+
throw new SnapshotChunkIntegrityError(
|
|
2083
|
+
entry.key,
|
|
2084
|
+
`expected it to begin at ${entry.firstId}, got ${rows[0]._id}`
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
if (rows[rows.length - 1]._id !== entry.lastId) {
|
|
2088
|
+
throw new SnapshotChunkIntegrityError(
|
|
2089
|
+
entry.key,
|
|
2090
|
+
`expected it to end at ${entry.lastId}, got ${rows[rows.length - 1]._id}`
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return rows;
|
|
2095
|
+
}
|
|
2096
|
+
var FORMAT2_MANIFEST_VERSION, FORMAT2_MIN_MANIFEST_VERSION, INGEST_CHUNK_LIMITS, SnapshotChunkIntegrityError, SnapshotManifestInvalidError, SnapshotManifestUnsupportedError, CHUNK_PATH_OWN, CHUNK_PATH_SOURCED, MODEL_NAME, encoder, decoder, SNAPSHOT_ENCODE_SEGMENT_BYTES;
|
|
2097
|
+
var init_snapshotChunks = __esm({
|
|
2098
|
+
"src/utils/snapshotChunks.ts"() {
|
|
2099
|
+
"use strict";
|
|
2100
|
+
FORMAT2_MANIFEST_VERSION = 3;
|
|
2101
|
+
FORMAT2_MIN_MANIFEST_VERSION = 2;
|
|
2102
|
+
INGEST_CHUNK_LIMITS = {
|
|
2103
|
+
/** Stored (compressed) bytes of one chunk. */
|
|
2104
|
+
maxChunkBytes: 8 * 1024 * 1024,
|
|
2105
|
+
/** Decompressed bytes of one chunk — the bound the reader stops at. */
|
|
2106
|
+
maxChunkRawBytes: 16 * 1024 * 1024,
|
|
2107
|
+
/** Lines in one chunk. */
|
|
2108
|
+
maxChunkRows: 5e4,
|
|
2109
|
+
/** Chunks in one session. */
|
|
2110
|
+
maxChunks: 4096,
|
|
2111
|
+
/** Stored bytes across the whole session. */
|
|
2112
|
+
maxTotalBytes: 2 * 1024 * 1024 * 1024
|
|
2113
|
+
};
|
|
2114
|
+
SnapshotChunkIntegrityError = class extends Error {
|
|
2115
|
+
constructor(key, reason) {
|
|
2116
|
+
super(`Snapshot chunk ${key} does not match its manifest entry: ${reason}`);
|
|
2117
|
+
this.key = key;
|
|
2118
|
+
this.reason = reason;
|
|
2119
|
+
this.name = "SnapshotChunkIntegrityError";
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
SnapshotManifestInvalidError = class extends Error {
|
|
2123
|
+
code = "SNAPSHOT_MANIFEST_INVALID";
|
|
2124
|
+
constructor(reason) {
|
|
2125
|
+
super(`Snapshot manifest is not valid: ${reason}`);
|
|
2126
|
+
this.name = "SnapshotManifestInvalidError";
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
SnapshotManifestUnsupportedError = class extends Error {
|
|
2130
|
+
constructor(manifestVersion, supportedVersion = FORMAT2_MANIFEST_VERSION) {
|
|
2131
|
+
super(
|
|
2132
|
+
`Snapshot manifest is version ${manifestVersion}; this client reads up to version ${supportedVersion}. Upgrade the client library.`
|
|
1771
2133
|
);
|
|
1772
|
-
this.
|
|
1773
|
-
this.
|
|
1774
|
-
this.
|
|
1775
|
-
this.windowDays = status.windowDays;
|
|
1776
|
-
this.overdueMs = status.overdueMs;
|
|
2134
|
+
this.manifestVersion = manifestVersion;
|
|
2135
|
+
this.supportedVersion = supportedVersion;
|
|
2136
|
+
this.name = "SnapshotManifestUnsupportedError";
|
|
1777
2137
|
}
|
|
2138
|
+
code = "SNAPSHOT_MANIFEST_UNSUPPORTED";
|
|
1778
2139
|
};
|
|
2140
|
+
CHUNK_PATH_OWN = /^([A-Za-z][A-Za-z0-9_]*)\/(\d+)$/;
|
|
2141
|
+
CHUNK_PATH_SOURCED = /^(\d+)-([A-Za-z0-9_-]{1,64})\/([A-Za-z][A-Za-z0-9_]*)\/(\d+)$/;
|
|
2142
|
+
MODEL_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
2143
|
+
encoder = new TextEncoder();
|
|
2144
|
+
decoder = new TextDecoder();
|
|
2145
|
+
SNAPSHOT_ENCODE_SEGMENT_BYTES = 64 * 1024;
|
|
1779
2146
|
}
|
|
1780
2147
|
});
|
|
1781
2148
|
|
|
@@ -1918,6 +2285,28 @@ function format2ClientDDL(docId) {
|
|
|
1918
2285
|
build_id TEXT NOT NULL,
|
|
1919
2286
|
ordinal INTEGER NOT NULL,
|
|
1920
2287
|
PRIMARY KEY (doc_id, build_id, ordinal)
|
|
2288
|
+
)`,
|
|
2289
|
+
// #3435 — which base this merged view is on, and whether its load ever
|
|
2290
|
+
// FINISHED. The second half is what makes a partial replacement safe: a
|
|
2291
|
+
// convergence may keep a chunk by marking it rather than fetching it only
|
|
2292
|
+
// when the rows it is keeping are a complete baseline. A client that is
|
|
2293
|
+
// cold, or whose last load was interrupted, has no such guarantee and
|
|
2294
|
+
// takes the full plan (finding 3435-R04).
|
|
2295
|
+
`CREATE TABLE IF NOT EXISTS _snapshot_base (
|
|
2296
|
+
doc_id TEXT PRIMARY KEY,
|
|
2297
|
+
build_id TEXT NOT NULL,
|
|
2298
|
+
epoch INTEGER NOT NULL,
|
|
2299
|
+
complete INTEGER NOT NULL DEFAULT 0
|
|
2300
|
+
)`,
|
|
2301
|
+
// #3435 — every bulk-load epoch this document crossed and has not yet
|
|
2302
|
+
// converged past. Durable, because a connected client that followed a
|
|
2303
|
+
// flagged seal onto E+1 has an epoch mark that reads perfectly current:
|
|
2304
|
+
// if the debt lived only in memory, a restart would clear it and the
|
|
2305
|
+
// client would serve rows the server replaced, for good.
|
|
2306
|
+
`CREATE TABLE IF NOT EXISTS _discontinuities (
|
|
2307
|
+
doc_id TEXT NOT NULL,
|
|
2308
|
+
epoch INTEGER NOT NULL,
|
|
2309
|
+
PRIMARY KEY (doc_id, epoch)
|
|
1921
2310
|
)`
|
|
1922
2311
|
];
|
|
1923
2312
|
}
|
|
@@ -2752,6 +3141,185 @@ var init_format2RecordStore = __esm({
|
|
|
2752
3141
|
ordinal
|
|
2753
3142
|
);
|
|
2754
3143
|
}
|
|
3144
|
+
// ── The base this view is on, and the bulk loads it owes (#3435) ───
|
|
3145
|
+
/**
|
|
3146
|
+
* Which base the merged view is on, and which bulk-load epochs it has
|
|
3147
|
+
* crossed and not yet converged past.
|
|
3148
|
+
*
|
|
3149
|
+
* One read, because the two are one decision: a convergence may keep a
|
|
3150
|
+
* chunk by marking it only when the base it is keeping rows from is
|
|
3151
|
+
* complete AND exactly one discontinuity stands between it and the
|
|
3152
|
+
* announced base.
|
|
3153
|
+
*/
|
|
3154
|
+
baseState() {
|
|
3155
|
+
const rows = this.rows(
|
|
3156
|
+
"SELECT build_id, epoch, complete FROM _snapshot_base WHERE doc_id = ?",
|
|
3157
|
+
this.docId
|
|
3158
|
+
);
|
|
3159
|
+
return {
|
|
3160
|
+
base: rows.length > 0 ? {
|
|
3161
|
+
buildId: String(rows[0].build_id),
|
|
3162
|
+
epoch: Number(rows[0].epoch),
|
|
3163
|
+
complete: Boolean(Number(rows[0].complete))
|
|
3164
|
+
} : null,
|
|
3165
|
+
discontinuities: this.discontinuityEpochs()
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
/** Every unconverged bulk-load epoch, ascending. */
|
|
3169
|
+
discontinuityEpochs() {
|
|
3170
|
+
return this.rows(
|
|
3171
|
+
"SELECT epoch FROM _discontinuities WHERE doc_id = ? ORDER BY epoch",
|
|
3172
|
+
this.docId
|
|
3173
|
+
).map((row) => Number(row.epoch));
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Start loading `buildId` as this view's base.
|
|
3177
|
+
*
|
|
3178
|
+
* The load marks of OTHER builds go in the same transaction. They are
|
|
3179
|
+
* progress on a load nothing will ever finish, and a device that later came
|
|
3180
|
+
* back to that build would skip chunks whose rows this base has replaced.
|
|
3181
|
+
* One transaction because the row and the marks describe one thing: what
|
|
3182
|
+
* this view is being made into.
|
|
3183
|
+
*/
|
|
3184
|
+
beginBase(buildId, epoch) {
|
|
3185
|
+
this.sql.transaction(() => {
|
|
3186
|
+
this.sql.exec(
|
|
3187
|
+
`INSERT INTO _snapshot_base (doc_id, build_id, epoch, complete)
|
|
3188
|
+
VALUES (?, ?, ?, 0)
|
|
3189
|
+
ON CONFLICT(doc_id) DO UPDATE SET build_id = ?, epoch = ?, complete = 0`,
|
|
3190
|
+
this.docId,
|
|
3191
|
+
buildId,
|
|
3192
|
+
epoch,
|
|
3193
|
+
buildId,
|
|
3194
|
+
epoch
|
|
3195
|
+
);
|
|
3196
|
+
this.sql.exec(
|
|
3197
|
+
"DELETE FROM _snapshot_load WHERE doc_id = ? AND build_id <> ?",
|
|
3198
|
+
this.docId,
|
|
3199
|
+
buildId
|
|
3200
|
+
);
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
/** The load of `buildId` finished: this view is a complete baseline. */
|
|
3204
|
+
completeBase(buildId) {
|
|
3205
|
+
this.sql.exec(
|
|
3206
|
+
"UPDATE _snapshot_base SET complete = 1 WHERE doc_id = ? AND build_id = ?",
|
|
3207
|
+
this.docId,
|
|
3208
|
+
buildId
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
/** Record a bulk-load epoch this document crossed. Idempotent. */
|
|
3212
|
+
noteDiscontinuity(epoch) {
|
|
3213
|
+
this.sql.exec(
|
|
3214
|
+
"INSERT OR IGNORE INTO _discontinuities (doc_id, epoch) VALUES (?, ?)",
|
|
3215
|
+
this.docId,
|
|
3216
|
+
epoch
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
/** Forget every crossed epoch at or below `through`: converged past them. */
|
|
3220
|
+
clearDiscontinuities(through) {
|
|
3221
|
+
this.sql.exec(
|
|
3222
|
+
"DELETE FROM _discontinuities WHERE doc_id = ? AND epoch <= ?",
|
|
3223
|
+
this.docId,
|
|
3224
|
+
through
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* Replace one chunk's COVERAGE with its rows, in one commit (#3435,
|
|
3229
|
+
* behavior 11; finding 3435-R01).
|
|
3230
|
+
*
|
|
3231
|
+
* The atomic range replacement the design doc requires, and the reason it
|
|
3232
|
+
* is one store method rather than a delete followed by {@link applyChunk}.
|
|
3233
|
+
* Two tabs share this database and each run their own loader over the same
|
|
3234
|
+
* plan. With the delete committed apart from the insert, this interleaving
|
|
3235
|
+
* loses the chunk permanently: the first holder deletes, inserts and marks;
|
|
3236
|
+
* the second then deletes the same coverage and its `applyChunk` skips the
|
|
3237
|
+
* insert because the chunk is already marked. The rows are gone under a mark
|
|
3238
|
+
* that says they landed, and nothing ever fetches them again.
|
|
3239
|
+
*
|
|
3240
|
+
* So the mark check, the coverage delete, the insert and the mark are one
|
|
3241
|
+
* transaction, and a chunk another holder already landed changes nothing.
|
|
3242
|
+
*
|
|
3243
|
+
* The bounds are `(after, through]`, a null bound open, compared by the
|
|
3244
|
+
* TABLE's collation. That is SQLite's `BINARY` — UTF-8 byte order — which is
|
|
3245
|
+
* what `compareRecordIds` models and what the manifest's ranges were cut in.
|
|
3246
|
+
* A JavaScript `<` would disagree with both wherever an id mixes a
|
|
3247
|
+
* three-byte character with a supplementary one.
|
|
3248
|
+
*/
|
|
3249
|
+
replaceChunk(modelName, coverage, entries, buildId, ordinal) {
|
|
3250
|
+
let applied = 0;
|
|
3251
|
+
let removed = 0;
|
|
3252
|
+
let skipped = false;
|
|
3253
|
+
this.sql.transaction(() => {
|
|
3254
|
+
if (this.chunkIsComplete(buildId, ordinal)) {
|
|
3255
|
+
skipped = true;
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
if (!this.isHydrated(modelName)) {
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
removed = this.deleteCoverage(modelName, coverage);
|
|
3262
|
+
for (const entry of entries) {
|
|
3263
|
+
projectOverlayEntry(this.sql, modelName, entry, this.tables);
|
|
3264
|
+
applied++;
|
|
3265
|
+
}
|
|
3266
|
+
this.markChunkComplete(buildId, ordinal);
|
|
3267
|
+
});
|
|
3268
|
+
return { applied, removed, skipped };
|
|
3269
|
+
}
|
|
3270
|
+
/**
|
|
3271
|
+
* Delete up to `limit` rows of one model's range, and say whether that was
|
|
3272
|
+
* the last of them (#3435, behavior 11a).
|
|
3273
|
+
*
|
|
3274
|
+
* Paged because pruning a whole model of a multi-hundred-megabyte document
|
|
3275
|
+
* in one statement is the invocation-length failure the rest of this design
|
|
3276
|
+
* avoids; idempotent between two holders because it only ever deletes what
|
|
3277
|
+
* is there. Used for a whole-model prune — a model the new base does not
|
|
3278
|
+
* carry at all — and nothing else: a chunk's range is replaced by
|
|
3279
|
+
* {@link replaceChunk}, atomically, for the reason above.
|
|
3280
|
+
*/
|
|
3281
|
+
deleteRange(modelName, after, through, limit) {
|
|
3282
|
+
if (!this.isHydrated(modelName)) return { removed: 0, done: true };
|
|
3283
|
+
let removed = 0;
|
|
3284
|
+
this.sql.transaction(() => {
|
|
3285
|
+
removed = this.deleteCoverage(modelName, { after, through }, limit);
|
|
3286
|
+
});
|
|
3287
|
+
return { removed, done: removed < limit };
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Delete a model's rows over `(after, through]` and their stringset
|
|
3291
|
+
* members, up to `limit` rows. Callers hold the transaction.
|
|
3292
|
+
*/
|
|
3293
|
+
deleteCoverage(modelName, coverage, limit) {
|
|
3294
|
+
const bounds = [];
|
|
3295
|
+
const bindings = [modelName];
|
|
3296
|
+
if (coverage.after !== null) {
|
|
3297
|
+
bounds.push("_id > ?");
|
|
3298
|
+
bindings.push(coverage.after);
|
|
3299
|
+
}
|
|
3300
|
+
if (coverage.through !== null) {
|
|
3301
|
+
bounds.push("_id <= ?");
|
|
3302
|
+
bindings.push(coverage.through);
|
|
3303
|
+
}
|
|
3304
|
+
const where = ["_type = ?", ...bounds].join(" AND ");
|
|
3305
|
+
const doomed = this.rows(
|
|
3306
|
+
`SELECT _id FROM ${this.tables.records} WHERE ${where} ORDER BY _id` + (limit === void 0 ? "" : " LIMIT ?"),
|
|
3307
|
+
...limit === void 0 ? bindings : [...bindings, limit]
|
|
3308
|
+
).map((row) => String(row._id));
|
|
3309
|
+
for (const id of doomed) {
|
|
3310
|
+
this.sql.exec(
|
|
3311
|
+
`DELETE FROM ${this.tables.stringSetIndex} WHERE _type = ? AND _record_id = ?`,
|
|
3312
|
+
modelName,
|
|
3313
|
+
id
|
|
3314
|
+
);
|
|
3315
|
+
this.sql.exec(
|
|
3316
|
+
`DELETE FROM ${this.tables.records} WHERE _type = ? AND _id = ?`,
|
|
3317
|
+
modelName,
|
|
3318
|
+
id
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
return doomed.length;
|
|
3322
|
+
}
|
|
2755
3323
|
/**
|
|
2756
3324
|
* Throw away a merged view that cannot be repaired (#2816, behavior 12).
|
|
2757
3325
|
*
|
|
@@ -2773,6 +3341,7 @@ var init_format2RecordStore = __esm({
|
|
|
2773
3341
|
this.sql.exec(`DELETE FROM ${this.tables.records}`);
|
|
2774
3342
|
this.sql.exec(`DELETE FROM ${this.tables.stringSetIndex}`);
|
|
2775
3343
|
this.sql.exec("DELETE FROM _snapshot_load WHERE doc_id = ?", this.docId);
|
|
3344
|
+
this.sql.exec("DELETE FROM _snapshot_base WHERE doc_id = ?", this.docId);
|
|
2776
3345
|
});
|
|
2777
3346
|
}
|
|
2778
3347
|
// ── helpers ───────────────────────────────────────────────────────
|
|
@@ -3008,6 +3577,7 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
3008
3577
|
init_BaseModel();
|
|
3009
3578
|
init_documentTypes();
|
|
3010
3579
|
init_format2OfflineWindow();
|
|
3580
|
+
init_snapshotChunks();
|
|
3011
3581
|
init_format2RecordStore();
|
|
3012
3582
|
init_format2RowCache();
|
|
3013
3583
|
init_format2Observer();
|
|
@@ -3346,6 +3916,23 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
3346
3916
|
const store = this.direct();
|
|
3347
3917
|
return store ? store.hydrationScope() : this.mirrored().hydrationScope;
|
|
3348
3918
|
}
|
|
3919
|
+
/**
|
|
3920
|
+
* The bulk-load epochs this document crossed and has not converged past
|
|
3921
|
+
* (#3435), ascending.
|
|
3922
|
+
*
|
|
3923
|
+
* Synchronous because the transport asks it at the bind, in the same breath
|
|
3924
|
+
* as the epoch mark, to decide whether the handshake it is holding means a
|
|
3925
|
+
* catch-up or a convergence.
|
|
3926
|
+
*/
|
|
3927
|
+
discontinuityEpochs() {
|
|
3928
|
+
const store = this.direct();
|
|
3929
|
+
return store ? store.discontinuityEpochs() : [...this.mirrored().discontinuityEpochs];
|
|
3930
|
+
}
|
|
3931
|
+
/** Whether the base this view is on finished loading (#3435). */
|
|
3932
|
+
baseComplete() {
|
|
3933
|
+
const store = this.direct();
|
|
3934
|
+
return store ? store.baseState().base?.complete ?? false : this.mirrored().baseComplete;
|
|
3935
|
+
}
|
|
3349
3936
|
isHydrated(model) {
|
|
3350
3937
|
const scope = this.hydrationScope();
|
|
3351
3938
|
return scope === null || scope.includes(model);
|
|
@@ -3566,6 +4153,122 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
3566
4153
|
completedChunks(buildId) {
|
|
3567
4154
|
return this.host.dispatch({ kind: "completedChunks", buildId });
|
|
3568
4155
|
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Replace one chunk's coverage with its rows, atomically (#3435).
|
|
4158
|
+
*
|
|
4159
|
+
* The live cache entries the write could have moved are re-read afterwards:
|
|
4160
|
+
* the command DELETES a range as well as inserting, so an entry inside that
|
|
4161
|
+
* range may name a record that no longer exists, and the ids it removed do
|
|
4162
|
+
* not cross the boundary.
|
|
4163
|
+
*
|
|
4164
|
+
* The range is the bound, not the whole cache. A convergence replaces named
|
|
4165
|
+
* ranges of named models precisely so that everything else keeps its rows —
|
|
4166
|
+
* including the chunks it kept, which are the saving the design exists for —
|
|
4167
|
+
* and a blanket invalidation would tell every live instance of every model
|
|
4168
|
+
* that its record is gone, with `loaded` set so nothing reads it back
|
|
4169
|
+
* (finding 3435-C03).
|
|
4170
|
+
*/
|
|
4171
|
+
async replaceChunk(model, coverage, entries, buildId, ordinal) {
|
|
4172
|
+
const reply = await this.run(
|
|
4173
|
+
"replaceChunk",
|
|
4174
|
+
() => this.host.dispatch({
|
|
4175
|
+
kind: "replaceChunk",
|
|
4176
|
+
model,
|
|
4177
|
+
coverage,
|
|
4178
|
+
entries: [...entries],
|
|
4179
|
+
buildId,
|
|
4180
|
+
ordinal
|
|
4181
|
+
})
|
|
4182
|
+
);
|
|
4183
|
+
if (!reply.skipped) {
|
|
4184
|
+
await this.refreshRange(
|
|
4185
|
+
model,
|
|
4186
|
+
coverage,
|
|
4187
|
+
entries.map((entry) => entry.id)
|
|
4188
|
+
);
|
|
4189
|
+
}
|
|
4190
|
+
return reply;
|
|
4191
|
+
}
|
|
4192
|
+
/** One page of a whole-model prune (#3435). */
|
|
4193
|
+
async deleteRange(model, after, through, limit) {
|
|
4194
|
+
const reply = await this.run(
|
|
4195
|
+
"deleteRange",
|
|
4196
|
+
() => this.host.dispatch({ kind: "deleteRange", model, after, through, limit })
|
|
4197
|
+
);
|
|
4198
|
+
if (reply.removed > 0) {
|
|
4199
|
+
await this.refreshRange(model, { after, through }, []);
|
|
4200
|
+
}
|
|
4201
|
+
return reply;
|
|
4202
|
+
}
|
|
4203
|
+
/**
|
|
4204
|
+
* Re-read the live entries a range write could have moved (#3435).
|
|
4205
|
+
*
|
|
4206
|
+
* One model, one range, plus whatever the write inserted — a record outside
|
|
4207
|
+
* both is exactly what the plan kept, and its instances go on answering. The
|
|
4208
|
+
* bound is `compareRecordIds`, the collation the store deleted the range
|
|
4209
|
+
* under, so the two agree about which ids the write covered.
|
|
4210
|
+
*/
|
|
4211
|
+
async refreshRange(model, coverage, written) {
|
|
4212
|
+
const covered = (id) => (coverage.after === null || compareRecordIds(id, coverage.after) > 0) && (coverage.through === null || compareRecordIds(id, coverage.through) <= 0);
|
|
4213
|
+
const ids = new Set(this.cache.liveIds(model).filter(covered));
|
|
4214
|
+
for (const id of written) {
|
|
4215
|
+
if (this.cache.peek(model, id)) ids.add(id);
|
|
4216
|
+
}
|
|
4217
|
+
if (ids.size === 0) return;
|
|
4218
|
+
await this.refreshRows(model, [...ids]);
|
|
4219
|
+
}
|
|
4220
|
+
/** Which base this view is on, and which bulk loads it owes (#3435). */
|
|
4221
|
+
baseState() {
|
|
4222
|
+
return this.host.dispatch({ kind: "baseState" });
|
|
4223
|
+
}
|
|
4224
|
+
async beginBase(buildId, epoch) {
|
|
4225
|
+
await this.run(
|
|
4226
|
+
"beginBase",
|
|
4227
|
+
() => this.host.dispatch({ kind: "beginBase", buildId, epoch })
|
|
4228
|
+
);
|
|
4229
|
+
this.refreshMirror({ baseComplete: false });
|
|
4230
|
+
}
|
|
4231
|
+
async completeBase(buildId) {
|
|
4232
|
+
await this.run(
|
|
4233
|
+
"completeBase",
|
|
4234
|
+
() => this.host.dispatch({ kind: "completeBase", buildId })
|
|
4235
|
+
);
|
|
4236
|
+
this.refreshMirror({ baseComplete: true });
|
|
4237
|
+
}
|
|
4238
|
+
async noteDiscontinuity(epoch) {
|
|
4239
|
+
await this.run(
|
|
4240
|
+
"noteDiscontinuity",
|
|
4241
|
+
() => this.host.dispatch({ kind: "noteDiscontinuity", epoch })
|
|
4242
|
+
);
|
|
4243
|
+
const held = this.mirror?.discontinuityEpochs;
|
|
4244
|
+
if (held && !held.includes(epoch)) {
|
|
4245
|
+
this.refreshMirror({
|
|
4246
|
+
discontinuityEpochs: [...held, epoch].sort((a, b) => a - b)
|
|
4247
|
+
});
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
async clearDiscontinuities(through) {
|
|
4251
|
+
await this.run(
|
|
4252
|
+
"clearDiscontinuities",
|
|
4253
|
+
() => this.host.dispatch({ kind: "clearDiscontinuities", through })
|
|
4254
|
+
);
|
|
4255
|
+
const held = this.mirror?.discontinuityEpochs;
|
|
4256
|
+
if (held) {
|
|
4257
|
+
this.refreshMirror({
|
|
4258
|
+
discontinuityEpochs: held.filter((epoch) => epoch > through)
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Keep the synchronous mirror in step with a durable change.
|
|
4264
|
+
*
|
|
4265
|
+
* A no-op where the store runs in this thread: there the store IS the
|
|
4266
|
+
* mirror, and a copy would go stale the moment anything wrote through it.
|
|
4267
|
+
*/
|
|
4268
|
+
refreshMirror(patch) {
|
|
4269
|
+
if (!this.mirror) return;
|
|
4270
|
+
this.mirror = { ...this.mirror, ...patch };
|
|
4271
|
+
}
|
|
3569
4272
|
async discardMergedView() {
|
|
3570
4273
|
await this.run(
|
|
3571
4274
|
"discardMergedView",
|
|
@@ -4029,7 +4732,11 @@ function readFormat2StoreMarks(store) {
|
|
|
4029
4732
|
clockOffset: store.clockOffset(),
|
|
4030
4733
|
clockOffsetKnown: store.clockOffsetKnown(),
|
|
4031
4734
|
hydrationScope: store.hydrationScope(),
|
|
4032
|
-
pendingOps: store.pendingOps()
|
|
4735
|
+
pendingOps: store.pendingOps(),
|
|
4736
|
+
// #3435 — the two facts a document that crossed a bulk load has to know
|
|
4737
|
+
// at bind, before it decides what the handshake means.
|
|
4738
|
+
discontinuityEpochs: store.discontinuityEpochs(),
|
|
4739
|
+
baseComplete: store.baseState().base?.complete ?? false
|
|
4033
4740
|
};
|
|
4034
4741
|
}
|
|
4035
4742
|
function dispatchFormat2StoreCommand(store, sql, command) {
|
|
@@ -4112,6 +4819,31 @@ function run(store, sql, command) {
|
|
|
4112
4819
|
};
|
|
4113
4820
|
case "completedChunks":
|
|
4114
4821
|
return store.completedChunks(command.buildId);
|
|
4822
|
+
case "replaceChunk":
|
|
4823
|
+
return store.replaceChunk(
|
|
4824
|
+
command.model,
|
|
4825
|
+
command.coverage,
|
|
4826
|
+
command.entries,
|
|
4827
|
+
command.buildId,
|
|
4828
|
+
command.ordinal
|
|
4829
|
+
);
|
|
4830
|
+
case "deleteRange":
|
|
4831
|
+
return store.deleteRange(
|
|
4832
|
+
command.model,
|
|
4833
|
+
command.after,
|
|
4834
|
+
command.through,
|
|
4835
|
+
command.limit
|
|
4836
|
+
);
|
|
4837
|
+
case "baseState":
|
|
4838
|
+
return store.baseState();
|
|
4839
|
+
case "beginBase":
|
|
4840
|
+
return store.beginBase(command.buildId, command.epoch);
|
|
4841
|
+
case "completeBase":
|
|
4842
|
+
return store.completeBase(command.buildId);
|
|
4843
|
+
case "noteDiscontinuity":
|
|
4844
|
+
return store.noteDiscontinuity(command.epoch);
|
|
4845
|
+
case "clearDiscontinuities":
|
|
4846
|
+
return store.clearDiscontinuities(command.through);
|
|
4115
4847
|
case "foreignClients":
|
|
4116
4848
|
return store.foreignClientIds();
|
|
4117
4849
|
case "adoptPendingOps":
|
|
@@ -4493,6 +5225,19 @@ var init_OrmContext = __esm({
|
|
|
4493
5225
|
format2Documents = /* @__PURE__ */ new Map();
|
|
4494
5226
|
/** Format-2 epoch moves in flight (#2816). */
|
|
4495
5227
|
format2Transitions = /* @__PURE__ */ new Map();
|
|
5228
|
+
/**
|
|
5229
|
+
* This instance's document observers' database writes that have not finished
|
|
5230
|
+
* yet, by document id (#3023).
|
|
5231
|
+
*
|
|
5232
|
+
* A Yjs observer fires synchronously with the update but writes to the
|
|
5233
|
+
* database asynchronously, so an update that has been APPLIED to a document
|
|
5234
|
+
* is not yet QUERYABLE. Whoever needs the difference — an `open()` that
|
|
5235
|
+
* promises its caller the synced records — waits on
|
|
5236
|
+
* {@link whenDocumentIndexed}. Per instance, not process-wide: two clients
|
|
5237
|
+
* holding the same document id write to their own engines, and neither
|
|
5238
|
+
* should be held up by the other's writes.
|
|
5239
|
+
*/
|
|
5240
|
+
documentIndexingWork = /* @__PURE__ */ new Map();
|
|
4496
5241
|
/**
|
|
4497
5242
|
* Who this ORM instance is in a format-2 store's unacknowledged-write log
|
|
4498
5243
|
* (#3431).
|
|
@@ -4670,6 +5415,56 @@ var init_OrmContext = __esm({
|
|
|
4670
5415
|
release();
|
|
4671
5416
|
}
|
|
4672
5417
|
}
|
|
5418
|
+
/**
|
|
5419
|
+
* Record one observer's asynchronous write of `docId` (#3023), so a caller
|
|
5420
|
+
* that needs the document queryable can wait for it. Failures are the
|
|
5421
|
+
* observer's own to log; a tracked promise never rejects, because the point
|
|
5422
|
+
* of the barrier is only to know when the write is over.
|
|
5423
|
+
*
|
|
5424
|
+
* A destroyed context tracks nothing: its observers are already inert, and a
|
|
5425
|
+
* torn-down instance must not leave a barrier that never drains.
|
|
5426
|
+
*/
|
|
5427
|
+
trackDocumentIndexing(docId, work) {
|
|
5428
|
+
if (this.destroyed) return;
|
|
5429
|
+
let inFlight = this.documentIndexingWork.get(docId);
|
|
5430
|
+
if (!inFlight) {
|
|
5431
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
5432
|
+
this.documentIndexingWork.set(docId, inFlight);
|
|
5433
|
+
}
|
|
5434
|
+
const pending = work.catch(() => {
|
|
5435
|
+
}).then(() => {
|
|
5436
|
+
inFlight.delete(pending);
|
|
5437
|
+
if (inFlight.size === 0 && this.documentIndexingWork.get(docId) === inFlight) {
|
|
5438
|
+
this.documentIndexingWork.delete(docId);
|
|
5439
|
+
}
|
|
5440
|
+
});
|
|
5441
|
+
inFlight.add(pending);
|
|
5442
|
+
}
|
|
5443
|
+
/**
|
|
5444
|
+
* Resolve once `docId`'s applied changes have reached this instance's query
|
|
5445
|
+
* tables (#3023). Answers `true` when the work drained, `false` when it gave
|
|
5446
|
+
* up waiting — the caller owns the logging, because this module holds no
|
|
5447
|
+
* runtime imports.
|
|
5448
|
+
*
|
|
5449
|
+
* An update applied to the Y.Doc is visible to a reader of the document
|
|
5450
|
+
* immediately, but the query tables only catch up when the observers' writes
|
|
5451
|
+
* land. An `open()` that waited for the initial sync waits for this too —
|
|
5452
|
+
* otherwise it resolves on the sync signal and the caller's first `query()`
|
|
5453
|
+
* sees a partly-written table.
|
|
5454
|
+
*
|
|
5455
|
+
* A write can enqueue more work (a nested map observed as it arrives), so
|
|
5456
|
+
* this drains in passes rather than once, and gives up after enough of them
|
|
5457
|
+
* that only a document under continuous change could still be busy — a
|
|
5458
|
+
* barrier is not a reason to hang.
|
|
5459
|
+
*/
|
|
5460
|
+
async whenDocumentIndexed(docId) {
|
|
5461
|
+
for (let pass = 0; pass < 50; pass++) {
|
|
5462
|
+
const inFlight = this.documentIndexingWork.get(docId);
|
|
5463
|
+
if (!inFlight || inFlight.size === 0) return true;
|
|
5464
|
+
await Promise.all([...inFlight]);
|
|
5465
|
+
}
|
|
5466
|
+
return false;
|
|
5467
|
+
}
|
|
4673
5468
|
/** Notify `subscribe()` callbacks registered on this context for `modelName`. */
|
|
4674
5469
|
notifyListeners(modelName, log, onError) {
|
|
4675
5470
|
const listeners = this.listenersMap.get(modelName);
|
|
@@ -4795,6 +5590,7 @@ var init_OrmContext = __esm({
|
|
|
4795
5590
|
this.modelDocMappingChangedListeners.clear();
|
|
4796
5591
|
this.format2Documents.clear();
|
|
4797
5592
|
this.format2Transitions.clear();
|
|
5593
|
+
this.documentIndexingWork.clear();
|
|
4798
5594
|
this.observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
4799
5595
|
liveOrmContexts.delete(this);
|
|
4800
5596
|
if (defaultOrmContext === this) {
|
|
@@ -5165,6 +5961,10 @@ var init_ModelRegistry = __esm({
|
|
|
5165
5961
|
* undefined the legacy session semantics are kept exactly: the global
|
|
5166
5962
|
* registry is SNAPSHOT at init time, so a model registered afterwards is not
|
|
5167
5963
|
* silently adopted by an already-running instance.
|
|
5964
|
+
*
|
|
5965
|
+
* The one way a running instance's registry grows is {@link adoptModel},
|
|
5966
|
+
* behind `registerModel` on the init result (#3615): an EXPLICIT call on the
|
|
5967
|
+
* instance the app means, not a relaxation of the snapshot rule.
|
|
5168
5968
|
*/
|
|
5169
5969
|
static createInstanceRegistry(modelClasses) {
|
|
5170
5970
|
const registry = new _ModelRegistry();
|
|
@@ -5188,6 +5988,64 @@ var init_ModelRegistry = __esm({
|
|
|
5188
5988
|
registry.setExplicitModelsForSession(modelClasses);
|
|
5189
5989
|
return registry;
|
|
5190
5990
|
}
|
|
5991
|
+
/**
|
|
5992
|
+
* The name `modelClass` may be adopted under on THIS instance, or a refusal
|
|
5993
|
+
* that names what to change (#3615).
|
|
5994
|
+
*
|
|
5995
|
+
* Both checks are made before anything is mutated, so a registration that
|
|
5996
|
+
* cannot go through leaves the registry exactly as it was.
|
|
5997
|
+
*/
|
|
5998
|
+
assertAdoptable(modelClass, instanceId) {
|
|
5999
|
+
const schema = modelClass?.getSchema?.();
|
|
6000
|
+
const modelName = modelClass?.modelName ?? schema?.options?.name;
|
|
6001
|
+
if (!modelName || !schema?.options?.name || !schema?.resolvedUniqueConstraints) {
|
|
6002
|
+
throw new Error(
|
|
6003
|
+
`[initJsBao] Cannot register a model class with no name. Build it with defineModelSchema and attachAndRegisterModel (or createModelClass) so it carries a static modelName and a schema with resolvedUniqueConstraints.`
|
|
6004
|
+
);
|
|
6005
|
+
}
|
|
6006
|
+
const existing = this.models.get(modelName);
|
|
6007
|
+
if (existing && existing !== modelClass) {
|
|
6008
|
+
throw new Error(
|
|
6009
|
+
`[initJsBao] Model '${modelName}' is already registered on js-bao instance #${instanceId} with a different class. Register the existing class, or give the new model a different name.`
|
|
6010
|
+
);
|
|
6011
|
+
}
|
|
6012
|
+
return modelName;
|
|
6013
|
+
}
|
|
6014
|
+
/**
|
|
6015
|
+
* Adopt `modelClass` into THIS instance's registry at runtime (#3615).
|
|
6016
|
+
*
|
|
6017
|
+
* The counterpart to the snapshot rule on
|
|
6018
|
+
* {@link ModelRegistry.createInstanceRegistry}: an instance grows only where
|
|
6019
|
+
* the app says so, and the global singleton is deliberately not touched —
|
|
6020
|
+
* registering here makes the model visible to this instance and to no other.
|
|
6021
|
+
*
|
|
6022
|
+
* Returns `{ added: false }` when the very same class is already ACTIVE on
|
|
6023
|
+
* this instance, which is what makes a repeat `registerModel` a no-op rather
|
|
6024
|
+
* than a warning about overwriting. Active, not merely present: an instance
|
|
6025
|
+
* built with `models: []` carries the whole global registry in `models` with
|
|
6026
|
+
* nothing active, and a class only the backing snapshot holds has neither
|
|
6027
|
+
* been validated for this session nor bound to its context, so it still has
|
|
6028
|
+
* an adoption to go through.
|
|
6029
|
+
*/
|
|
6030
|
+
adoptModel(modelClass, options = {}) {
|
|
6031
|
+
const modelName = this.assertAdoptable(modelClass, options.instanceId);
|
|
6032
|
+
if (this.getActiveModels().get(modelName) === modelClass) {
|
|
6033
|
+
return { added: false };
|
|
6034
|
+
}
|
|
6035
|
+
const candidate = new Map(this.getActiveModels());
|
|
6036
|
+
candidate.set(modelName, modelClass);
|
|
6037
|
+
this.validateModels(candidate);
|
|
6038
|
+
const schema = modelClass.getSchema?.();
|
|
6039
|
+
this.models.set(modelName, modelClass);
|
|
6040
|
+
if (schema?.options) this.modelOptions.set(modelName, schema.options);
|
|
6041
|
+
if (this.activeSessionModels) {
|
|
6042
|
+
this.activeSessionModels.set(modelName, modelClass);
|
|
6043
|
+
}
|
|
6044
|
+
Logger.debug(
|
|
6045
|
+
`[ModelRegistry] Model "${modelName}" adopted by instance #${options.instanceId} at runtime.`
|
|
6046
|
+
);
|
|
6047
|
+
return { added: true };
|
|
6048
|
+
}
|
|
5191
6049
|
registerModel(modelClass, options, fields) {
|
|
5192
6050
|
if (!options.name) {
|
|
5193
6051
|
throw new Error(
|
|
@@ -5390,7 +6248,8 @@ var init_ModelRegistry = __esm({
|
|
|
5390
6248
|
Logger.debug(`[ModelRegistry] Data removal complete for document ${docId}`);
|
|
5391
6249
|
}
|
|
5392
6250
|
// New method to initialize relationships
|
|
5393
|
-
async initializeRelationships() {
|
|
6251
|
+
async initializeRelationships(fallbackDbEngine) {
|
|
6252
|
+
if (!this.dbEngine && fallbackDbEngine) this.dbEngine = fallbackDbEngine;
|
|
5394
6253
|
if (!this.dbEngine) {
|
|
5395
6254
|
console.error(
|
|
5396
6255
|
"[ModelRegistry] DB engine not available for initializing relationships. Ensure initializeAll has been called."
|
|
@@ -5542,7 +6401,16 @@ var init_ModelRegistry = __esm({
|
|
|
5542
6401
|
return this.activeSessionModels || this.models;
|
|
5543
6402
|
}
|
|
5544
6403
|
validateSessionModels() {
|
|
5545
|
-
|
|
6404
|
+
this.validateModels(this.getActiveModels());
|
|
6405
|
+
}
|
|
6406
|
+
/**
|
|
6407
|
+
* Every relationship of `activeModels` names a model that is also in it.
|
|
6408
|
+
*
|
|
6409
|
+
* Taken as an argument rather than read off the registry so a runtime
|
|
6410
|
+
* adoption can be validated against what the instance WOULD hold before it
|
|
6411
|
+
* holds it (#3615).
|
|
6412
|
+
*/
|
|
6413
|
+
validateModels(activeModels) {
|
|
5546
6414
|
if (!activeModels || activeModels.size === 0) {
|
|
5547
6415
|
return;
|
|
5548
6416
|
}
|
|
@@ -5994,6 +6862,14 @@ var init_IncludeResolver = __esm({
|
|
|
5994
6862
|
function generateULID() {
|
|
5995
6863
|
return (0, import_ulid2.ulid)();
|
|
5996
6864
|
}
|
|
6865
|
+
function reindexFailure(modelConstructor, modelName, recordId, docId, error) {
|
|
6866
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6867
|
+
const failure = new Error(
|
|
6868
|
+
`[${modelConstructor.name}] Error re-indexing ${modelName}/${recordId} of document ${docId}: ${message}`
|
|
6869
|
+
);
|
|
6870
|
+
failure.cause = error;
|
|
6871
|
+
return failure;
|
|
6872
|
+
}
|
|
5997
6873
|
var Y7, import_ulid2, FORMAT2_ROW_PAGE, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
|
|
5998
6874
|
var init_BaseModel = __esm({
|
|
5999
6875
|
"src/models/BaseModel.ts"() {
|
|
@@ -6866,19 +7742,31 @@ var init_BaseModel = __esm({
|
|
|
6866
7742
|
if (fieldOptions?.default === void 0) return void 0;
|
|
6867
7743
|
return typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
|
|
6868
7744
|
}
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
7745
|
+
/**
|
|
7746
|
+
* The members of a stringset as it is STORED, in stored order.
|
|
7747
|
+
*
|
|
7748
|
+
* Static and value-only so that the two readers of a stored stringset — the
|
|
7749
|
+
* instance's `getStringSetFromYjs`, which feeds the value a save builds its
|
|
7750
|
+
* unique key from, and `_readableUniqueOwner`, which rebuilds another
|
|
7751
|
+
* record's key from the map — cannot drift apart (#3252).
|
|
7752
|
+
*/
|
|
7753
|
+
static _stringSetMembersFromStored(stored) {
|
|
7754
|
+
if (stored instanceof Y7.Map) {
|
|
7755
|
+
return Array.from(stored.keys());
|
|
6873
7756
|
}
|
|
6874
|
-
if (Array.isArray(
|
|
6875
|
-
return
|
|
7757
|
+
if (Array.isArray(stored)) {
|
|
7758
|
+
return stored.filter((member) => typeof member === "string");
|
|
6876
7759
|
}
|
|
6877
|
-
if (
|
|
6878
|
-
return Object.keys(
|
|
7760
|
+
if (stored && typeof stored === "object") {
|
|
7761
|
+
return Object.keys(stored).filter((k) => Boolean(stored[k]));
|
|
6879
7762
|
}
|
|
6880
7763
|
return [];
|
|
6881
7764
|
}
|
|
7765
|
+
getStringSetFromYjs(fieldName) {
|
|
7766
|
+
return this.constructor._stringSetMembersFromStored(
|
|
7767
|
+
this.getFromYjs(fieldName)
|
|
7768
|
+
);
|
|
7769
|
+
}
|
|
6882
7770
|
getOrCreateStringSet(fieldName) {
|
|
6883
7771
|
if (!this._stringSetFields.has(fieldName)) {
|
|
6884
7772
|
const initialValues = this.getStringSetFromYjs(fieldName);
|
|
@@ -6907,6 +7795,30 @@ var init_BaseModel = __esm({
|
|
|
6907
7795
|
`[${this.name}] initialize(yDoc, db) is no longer supported. Use initializeForDocument(yDoc, db, docId, permission)`
|
|
6908
7796
|
);
|
|
6909
7797
|
}
|
|
7798
|
+
/**
|
|
7799
|
+
* Record one observer's asynchronous write of `docId` on this class's ORM
|
|
7800
|
+
* instance (#3023), so a caller that needs the document queryable can wait
|
|
7801
|
+
* for it. The observers themselves track through the context they captured
|
|
7802
|
+
* at install time; this static is for callers that only hold a model class.
|
|
7803
|
+
*/
|
|
7804
|
+
static trackDocumentIndexing(docId, work) {
|
|
7805
|
+
resolveOrmContext(this).trackDocumentIndexing(docId, work);
|
|
7806
|
+
}
|
|
7807
|
+
/**
|
|
7808
|
+
* Resolve once `docId`'s applied changes have reached this class's instance
|
|
7809
|
+
* of the query tables (#3023). See
|
|
7810
|
+
* {@link OrmContext.whenDocumentIndexed} — an `open()` that waited for the
|
|
7811
|
+
* initial sync waits for this too, otherwise it resolves on the sync signal
|
|
7812
|
+
* and the caller's first `query()` sees a partly-written table.
|
|
7813
|
+
*/
|
|
7814
|
+
static async whenDocumentIndexed(docId) {
|
|
7815
|
+
const drained = await resolveOrmContext(this).whenDocumentIndexed(docId);
|
|
7816
|
+
if (!drained) {
|
|
7817
|
+
Logger.warn(
|
|
7818
|
+
`[BaseModel] Document ${docId} is still indexing after 50 drain passes; not waiting any longer`
|
|
7819
|
+
);
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
6910
7822
|
/**
|
|
6911
7823
|
* Initialize this model for a connected document.
|
|
6912
7824
|
*
|
|
@@ -7253,7 +8165,7 @@ var init_BaseModel = __esm({
|
|
|
7253
8165
|
return recordIdsToDiscard;
|
|
7254
8166
|
};
|
|
7255
8167
|
const observerContext = resolveOrmContext(this);
|
|
7256
|
-
const
|
|
8168
|
+
const indexDocumentYMapChange = async (event) => {
|
|
7257
8169
|
if (observerContext.destroyed) return;
|
|
7258
8170
|
Logger.verbose(
|
|
7259
8171
|
`[${this.name}] Document YMap change detected for ${modelName}/${docId}:`,
|
|
@@ -7422,6 +8334,12 @@ var init_BaseModel = __esm({
|
|
|
7422
8334
|
);
|
|
7423
8335
|
observerContext.notifyListeners(modelName);
|
|
7424
8336
|
};
|
|
8337
|
+
const documentYMapObserver = (event) => {
|
|
8338
|
+
observerContext.trackDocumentIndexing(
|
|
8339
|
+
docId,
|
|
8340
|
+
indexDocumentYMapChange(event)
|
|
8341
|
+
);
|
|
8342
|
+
};
|
|
7425
8343
|
documentYMap.observe(documentYMapObserver);
|
|
7426
8344
|
observerContext.registerObserverDisposer(
|
|
7427
8345
|
docId,
|
|
@@ -7447,6 +8365,116 @@ var init_BaseModel = __esm({
|
|
|
7447
8365
|
`[${this.name}] Model ${modelName} initialization complete for document ${docId}.`
|
|
7448
8366
|
);
|
|
7449
8367
|
}
|
|
8368
|
+
/**
|
|
8369
|
+
* Re-index ONE record of a document from its current Y.Map state (#3615).
|
|
8370
|
+
*
|
|
8371
|
+
* A legacy document's initial load walks the Y.Map before the permanent
|
|
8372
|
+
* observers are installed, so an update that lands in that window reaches
|
|
8373
|
+
* Yjs and misses the query tables. For a document connected long ago — which
|
|
8374
|
+
* is exactly what a model registered at runtime is initialized against —
|
|
8375
|
+
* that window is open on a live document. `registerModel` collects the
|
|
8376
|
+
* record ids touched while it runs and brings each of them here afterwards,
|
|
8377
|
+
* writing the same row the permanent per-record observer would have written.
|
|
8378
|
+
*
|
|
8379
|
+
* A record the window DELETED is deleted here for the same reason.
|
|
8380
|
+
*
|
|
8381
|
+
* A storage failure REJECTS rather than being logged and passed over, the
|
|
8382
|
+
* way the permanent observers' writes are: this write is part of what a
|
|
8383
|
+
* registration promises before it resolves, so swallowing it would resolve a
|
|
8384
|
+
* registration whose query tables are missing the very updates this call
|
|
8385
|
+
* exists to catch, and mark the pair complete so no retry would repair it.
|
|
8386
|
+
*/
|
|
8387
|
+
static async reindexDocumentRecord(docId, recordId) {
|
|
8388
|
+
const modelConstructor = this;
|
|
8389
|
+
const schema = modelConstructor.getSchema?.();
|
|
8390
|
+
const modelName = schema?.options?.name;
|
|
8391
|
+
if (!modelName) return;
|
|
8392
|
+
const ctx = resolveOrmContext(modelConstructor);
|
|
8393
|
+
if (ctx.destroyed) return;
|
|
8394
|
+
const documentYMap = modelConstructor.documentYMaps.get(
|
|
8395
|
+
`${docId}_${modelName}`
|
|
8396
|
+
);
|
|
8397
|
+
if (!documentYMap) return;
|
|
8398
|
+
const engine = ctx.dbEngine ? await modelConstructor.queryEngineFor([docId], modelName) : null;
|
|
8399
|
+
if (!engine) return;
|
|
8400
|
+
const recordData = documentYMap.get(recordId);
|
|
8401
|
+
if (recordData === void 0 || recordData === null) {
|
|
8402
|
+
try {
|
|
8403
|
+
await engine.delete(modelName, recordId);
|
|
8404
|
+
} catch (error) {
|
|
8405
|
+
throw reindexFailure(
|
|
8406
|
+
modelConstructor,
|
|
8407
|
+
modelName,
|
|
8408
|
+
recordId,
|
|
8409
|
+
docId,
|
|
8410
|
+
error
|
|
8411
|
+
);
|
|
8412
|
+
}
|
|
8413
|
+
ctx.notifyListeners(modelName);
|
|
8414
|
+
return;
|
|
8415
|
+
}
|
|
8416
|
+
const entries = recordData instanceof Y7.Map ? Array.from(recordData.entries()) : Object.entries(recordData);
|
|
8417
|
+
const rowData = {};
|
|
8418
|
+
for (const [fieldKey, value] of entries) {
|
|
8419
|
+
const fieldOptions = schema.fields?.get(fieldKey);
|
|
8420
|
+
if (!fieldOptions || fieldOptions.type === "stringset") continue;
|
|
8421
|
+
if (value === void 0) continue;
|
|
8422
|
+
rowData[fieldKey] = isYjsComposite(value) ? normalizeYjsValueForStorage(value) : value;
|
|
8423
|
+
}
|
|
8424
|
+
if (!rowData.id) return;
|
|
8425
|
+
const permissionHint = modelConstructor.connectedDocuments.get(docId)?.permissionHint ?? "read-write";
|
|
8426
|
+
try {
|
|
8427
|
+
await engine.insert(modelName, {
|
|
8428
|
+
...rowData,
|
|
8429
|
+
type: modelName,
|
|
8430
|
+
_meta_doc_id: docId,
|
|
8431
|
+
_meta_permission_hint: permissionHint
|
|
8432
|
+
});
|
|
8433
|
+
} catch (error) {
|
|
8434
|
+
throw reindexFailure(
|
|
8435
|
+
modelConstructor,
|
|
8436
|
+
modelName,
|
|
8437
|
+
recordId,
|
|
8438
|
+
docId,
|
|
8439
|
+
error
|
|
8440
|
+
);
|
|
8441
|
+
}
|
|
8442
|
+
ctx.notifyListeners(modelName);
|
|
8443
|
+
}
|
|
8444
|
+
/**
|
|
8445
|
+
* Withdraw ONE document's registration of this model (#3615).
|
|
8446
|
+
*
|
|
8447
|
+
* A `registerModel` that fails part-way through a document keeps the class
|
|
8448
|
+
* registered, because retrying is the recovery. This is what keeps that
|
|
8449
|
+
* half-finished pair from LOOKING finished in the meantime: without the
|
|
8450
|
+
* Y.Map mapping a save raises the same `YMap not found` an unregistered
|
|
8451
|
+
* model raises, instead of writing into a document whose observers were
|
|
8452
|
+
* never installed, and clearing the rows the failed load managed to write
|
|
8453
|
+
* keeps a query from answering out of a document nothing is watching. The
|
|
8454
|
+
* retry's `initializeForDocument` publishes both again.
|
|
8455
|
+
*
|
|
8456
|
+
* `connectedDocuments` is deliberately left alone: it is the CONTEXT's map,
|
|
8457
|
+
* shared by every model on the instance, so the document itself is still
|
|
8458
|
+
* connected for everything else.
|
|
8459
|
+
*/
|
|
8460
|
+
static async withdrawDocumentRegistration(docId) {
|
|
8461
|
+
const modelConstructor = this;
|
|
8462
|
+
const schema = modelConstructor.getSchema?.();
|
|
8463
|
+
const modelName = schema?.options?.name;
|
|
8464
|
+
if (!modelName) return;
|
|
8465
|
+
modelConstructor.documentYMaps.delete(`${docId}_${modelName}`);
|
|
8466
|
+
const ctx = resolveOrmContext(modelConstructor);
|
|
8467
|
+
if (ctx.destroyed || !ctx.dbEngine) return;
|
|
8468
|
+
try {
|
|
8469
|
+
const engine = await modelConstructor.queryEngineFor([docId], modelName);
|
|
8470
|
+
await engine.deleteByDocumentId(modelName, docId);
|
|
8471
|
+
} catch (error) {
|
|
8472
|
+
Logger.warn(
|
|
8473
|
+
`[${modelConstructor.name}] Could not clear ${modelName} rows of document ${docId} while withdrawing an incomplete registration:`,
|
|
8474
|
+
error
|
|
8475
|
+
);
|
|
8476
|
+
}
|
|
8477
|
+
}
|
|
7450
8478
|
// New method to clean up data for a disconnected document
|
|
7451
8479
|
static async cleanupDocumentData(docId) {
|
|
7452
8480
|
const modelConstructor = this;
|
|
@@ -7702,8 +8730,50 @@ var init_BaseModel = __esm({
|
|
|
7702
8730
|
fields,
|
|
7703
8731
|
values,
|
|
7704
8732
|
modelName,
|
|
7705
|
-
constraintName
|
|
8733
|
+
constraintName
|
|
8734
|
+
);
|
|
8735
|
+
}
|
|
8736
|
+
/**
|
|
8737
|
+
* The record that READABLY owns `uniqueKey` under `constraint` in one
|
|
8738
|
+
* document's model map, or `null`.
|
|
8739
|
+
*
|
|
8740
|
+
* The `_uniqueIdx_` map is a cache over the records, not the record store,
|
|
8741
|
+
* and the two can disagree (#3252): a replica can hold an index entry for a
|
|
8742
|
+
* record whose structs have not landed yet, or an entry a field change or
|
|
8743
|
+
* delete left behind. `save()` used to take the entry at its word, so the
|
|
8744
|
+
* app saw a `query` that returned no row for a value and a save refused for
|
|
8745
|
+
* that same value — the index knew a row nothing else could read.
|
|
8746
|
+
*
|
|
8747
|
+
* An entry counts only when its owner is readable: the record map exists
|
|
8748
|
+
* under that id in `modelYMap`, and the key built from the record's stored
|
|
8749
|
+
* fields is `uniqueKey`. Anything else is a stale entry, and the caller
|
|
8750
|
+
* treats it as an index miss — the engine fallback decides, exactly as it
|
|
8751
|
+
* does for a missing entry — so the write that follows lands and rewrites
|
|
8752
|
+
* the entry to the record that really holds the value.
|
|
8753
|
+
*
|
|
8754
|
+
* "The key built from the record's stored fields" has to mean the key that
|
|
8755
|
+
* record's own save built, so the stored value is read the way the save
|
|
8756
|
+
* read it: a stringset is stored as a nested Y.Map of its members but keyed
|
|
8757
|
+
* as the array `getCurrentJSState` puts in `dataToSave` (finding 3252-01).
|
|
8758
|
+
* Comparing the raw Y.Map would call every readable owner of a unique
|
|
8759
|
+
* stringset stale and let a duplicate straight through the constraint.
|
|
8760
|
+
*/
|
|
8761
|
+
static _readableUniqueOwner(modelYMap, constraintMap, constraint, uniqueKey, modelName) {
|
|
8762
|
+
const ownerId = constraintMap.get(uniqueKey);
|
|
8763
|
+
if (!ownerId) return null;
|
|
8764
|
+
const recordYMap = modelYMap?.get(ownerId);
|
|
8765
|
+
if (!(recordYMap instanceof Y7.Map)) return null;
|
|
8766
|
+
const fields = this.getSchema?.()?.fields;
|
|
8767
|
+
const heldKey = this._buildKeyFromValues(
|
|
8768
|
+
constraint.fields,
|
|
8769
|
+
constraint.fields.map((field) => {
|
|
8770
|
+
const stored = recordYMap.get(field);
|
|
8771
|
+
return fields?.get(field)?.type === "stringset" ? this._stringSetMembersFromStored(stored) : stored;
|
|
8772
|
+
}),
|
|
8773
|
+
modelName,
|
|
8774
|
+
constraint.name
|
|
7706
8775
|
);
|
|
8776
|
+
return heldKey === uniqueKey ? ownerId : null;
|
|
7707
8777
|
}
|
|
7708
8778
|
/**
|
|
7709
8779
|
* The connected format-2 document that holds this instance's record, for an
|
|
@@ -7902,7 +8972,14 @@ var init_BaseModel = __esm({
|
|
|
7902
8972
|
const constraintMap = yDoc.getMap(
|
|
7903
8973
|
uniqueIndexMapName(modelName, constraint.name)
|
|
7904
8974
|
);
|
|
7905
|
-
|
|
8975
|
+
const readableOwner = modelConstructor._readableUniqueOwner(
|
|
8976
|
+
targetYMap,
|
|
8977
|
+
constraintMap,
|
|
8978
|
+
constraint,
|
|
8979
|
+
newUniqueKey,
|
|
8980
|
+
modelName
|
|
8981
|
+
);
|
|
8982
|
+
if (!readableOwner) {
|
|
7906
8983
|
const constraintKeyValues = constraint.fields.map(
|
|
7907
8984
|
(field) => dataToSave[field]
|
|
7908
8985
|
);
|
|
@@ -7920,7 +8997,13 @@ var init_BaseModel = __esm({
|
|
|
7920
8997
|
if (upsertOnConstraint && upsertOnKey) {
|
|
7921
8998
|
const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
|
|
7922
8999
|
const constraintMap = yDoc.getMap(constraintMapName);
|
|
7923
|
-
const existingId =
|
|
9000
|
+
const existingId = modelConstructor._readableUniqueOwner(
|
|
9001
|
+
targetYMap ?? void 0,
|
|
9002
|
+
constraintMap,
|
|
9003
|
+
upsertOnConstraint,
|
|
9004
|
+
upsertOnKey,
|
|
9005
|
+
modelName
|
|
9006
|
+
);
|
|
7924
9007
|
if (existingId) {
|
|
7925
9008
|
if (this._constructorProvidedId && this.id !== existingId) {
|
|
7926
9009
|
throw new Error(
|
|
@@ -8020,7 +9103,13 @@ var init_BaseModel = __esm({
|
|
|
8020
9103
|
50
|
|
8021
9104
|
)}'`
|
|
8022
9105
|
);
|
|
8023
|
-
const existingRecordIdWithNewKey =
|
|
9106
|
+
const existingRecordIdWithNewKey = modelConstructor._readableUniqueOwner(
|
|
9107
|
+
targetYMap ?? void 0,
|
|
9108
|
+
constraintMap,
|
|
9109
|
+
constraint,
|
|
9110
|
+
newUniqueKey,
|
|
9111
|
+
modelName
|
|
9112
|
+
);
|
|
8024
9113
|
Logger.verbose(
|
|
8025
9114
|
`[${modelName}] Save Check (Item '${this.id}'): Found Existing ID: '${existingRecordIdWithNewKey}' for Key '${newUniqueKey.substring(
|
|
8026
9115
|
0,
|
|
@@ -10158,7 +11247,7 @@ var init_BaseModel = __esm({
|
|
|
10158
11247
|
);
|
|
10159
11248
|
modelConstructor.attachStringSetObserversToRecord(recordYMap, schema, docId);
|
|
10160
11249
|
const observerContext = resolveOrmContext(modelConstructor);
|
|
10161
|
-
const
|
|
11250
|
+
const indexNestedYMapChange = async (event) => {
|
|
10162
11251
|
if (observerContext.destroyed) return;
|
|
10163
11252
|
Logger.verbose(
|
|
10164
11253
|
`[${modelName}] Nested YMap change detected for record ${recordId} in document ${docId}:`,
|
|
@@ -10234,6 +11323,12 @@ var init_BaseModel = __esm({
|
|
|
10234
11323
|
}
|
|
10235
11324
|
observerContext.notifyListeners(modelName);
|
|
10236
11325
|
};
|
|
11326
|
+
const recordObserver = (event) => {
|
|
11327
|
+
observerContext.trackDocumentIndexing(
|
|
11328
|
+
docId,
|
|
11329
|
+
indexNestedYMapChange(event)
|
|
11330
|
+
);
|
|
11331
|
+
};
|
|
10237
11332
|
recordYMap.observe(recordObserver);
|
|
10238
11333
|
observerContext.registerObserverDisposer(
|
|
10239
11334
|
docId,
|
|
@@ -10265,6 +11360,7 @@ __export(browser_exports, {
|
|
|
10265
11360
|
Format2RecordStoreAsync: () => Format2RecordStoreAsync,
|
|
10266
11361
|
Format2RowCache: () => Format2RowCache,
|
|
10267
11362
|
Format2StorageCapabilityError: () => Format2StorageCapabilityError,
|
|
11363
|
+
INGEST_CHUNK_LIMITS: () => INGEST_CHUNK_LIMITS,
|
|
10268
11364
|
InProcessFormat2StoreHost: () => InProcessFormat2StoreHost,
|
|
10269
11365
|
LogLevel: () => LogLevel,
|
|
10270
11366
|
Logger: () => Logger,
|
|
@@ -10308,6 +11404,8 @@ __export(browser_exports, {
|
|
|
10308
11404
|
encodeOverlayMutation: () => encodeOverlayMutation,
|
|
10309
11405
|
estimateMaterializedBytes: () => estimateMaterializedBytes,
|
|
10310
11406
|
features: () => features,
|
|
11407
|
+
findReservedFieldNameInToml: () => findReservedFieldNameInToml,
|
|
11408
|
+
foldFormat2Overlay: () => foldFormat2Overlay,
|
|
10311
11409
|
followEpoch: () => followEpoch,
|
|
10312
11410
|
format2ClientDDL: () => format2ClientDDL,
|
|
10313
11411
|
format2EpochDocName: () => format2EpochDocName,
|
|
@@ -10330,6 +11428,7 @@ __export(browser_exports, {
|
|
|
10330
11428
|
loadSchemaFromTomlString: () => loadSchemaFromTomlString,
|
|
10331
11429
|
materializeOverlayRecord: () => materializeOverlayRecord,
|
|
10332
11430
|
observeFormat2Overlay: () => observeFormat2Overlay,
|
|
11431
|
+
offlineReplayRecordKey: () => offlineReplayRecordKey,
|
|
10333
11432
|
offlineWindowStatus: () => offlineWindowStatus,
|
|
10334
11433
|
overlayFieldKey: () => overlayFieldKey,
|
|
10335
11434
|
overlayMarkerKey: () => overlayMarkerKey,
|
|
@@ -10338,6 +11437,7 @@ __export(browser_exports, {
|
|
|
10338
11437
|
parseSnapshotChunkPath: () => parseSnapshotChunkPath,
|
|
10339
11438
|
planColdStart: () => planColdStart,
|
|
10340
11439
|
planFastForward: () => planFastForward,
|
|
11440
|
+
planRangeReplacement: () => planRangeReplacement,
|
|
10341
11441
|
planSnapshotHydration: () => planSnapshotHydration,
|
|
10342
11442
|
probeStorageCapability: () => probeStorageCapability,
|
|
10343
11443
|
projectOverlayEntry: () => projectOverlayEntry,
|
|
@@ -10353,6 +11453,8 @@ __export(browser_exports, {
|
|
|
10353
11453
|
schemaToToml: () => schemaToToml,
|
|
10354
11454
|
serveFormat2StoreHost: () => serveFormat2StoreHost,
|
|
10355
11455
|
snapshotChunkPath: () => snapshotChunkPath,
|
|
11456
|
+
statePendingOps: () => statePendingOps,
|
|
11457
|
+
suppressSupersededKeys: () => suppressSupersededKeys,
|
|
10356
11458
|
syncInferredMeta: () => syncInferredMeta,
|
|
10357
11459
|
syncModelMeta: () => syncModelMeta,
|
|
10358
11460
|
validateSnapshotManifest: () => validateSnapshotManifest,
|
|
@@ -10966,387 +12068,135 @@ var SqljsEngine = class _SqljsEngine extends SqliteQueryEngineBase {
|
|
|
10966
12068
|
throw error;
|
|
10967
12069
|
}
|
|
10968
12070
|
}
|
|
10969
|
-
/** The open connection, once the wasm has loaded. */
|
|
10970
|
-
async connection() {
|
|
10971
|
-
await this.readyPromise;
|
|
10972
|
-
if (!this.db || !this.sync) throw new Error("SQL.js not initialized");
|
|
10973
|
-
return this.sync;
|
|
10974
|
-
}
|
|
10975
|
-
async withTransaction(callback) {
|
|
10976
|
-
await this.readyPromise;
|
|
10977
|
-
if (!this.db)
|
|
10978
|
-
throw new Error("SQL.js Database instance not available for transaction");
|
|
10979
|
-
let transactionStartedHere = false;
|
|
10980
|
-
await this.mutex.runExclusive(async () => {
|
|
10981
|
-
if (this.numOpenTransactions === 0) {
|
|
10982
|
-
if (!this.db) throw new Error("SQL.js DB not available for BEGIN");
|
|
10983
|
-
this.db.run("BEGIN TRANSACTION");
|
|
10984
|
-
transactionStartedHere = true;
|
|
10985
|
-
}
|
|
10986
|
-
this.numOpenTransactions++;
|
|
10987
|
-
});
|
|
10988
|
-
const transactionalOps = new TransactionalSQLJSOperations(this.db, this);
|
|
10989
|
-
try {
|
|
10990
|
-
const result = await callback(transactionalOps);
|
|
10991
|
-
await this.mutex.runExclusive(async () => {
|
|
10992
|
-
this.numOpenTransactions--;
|
|
10993
|
-
if (this.numOpenTransactions === 0) {
|
|
10994
|
-
if (!this.db) throw new Error("SQL.js DB not available for COMMIT");
|
|
10995
|
-
this.db.run("COMMIT");
|
|
10996
|
-
}
|
|
10997
|
-
});
|
|
10998
|
-
return result;
|
|
10999
|
-
} catch (error) {
|
|
11000
|
-
Logger.error("[SqljsEngine] Error in transaction, rolling back:", error);
|
|
11001
|
-
await this.mutex.runExclusive(async () => {
|
|
11002
|
-
if (this.db && this.numOpenTransactions > 0 && transactionStartedHere) {
|
|
11003
|
-
try {
|
|
11004
|
-
if (!this.db)
|
|
11005
|
-
throw new Error("SQL.js DB not available for ROLLBACK");
|
|
11006
|
-
this.db.run("ROLLBACK");
|
|
11007
|
-
} catch (rollbackError) {
|
|
11008
|
-
Logger.error("[SqljsEngine] Error during ROLLBACK:", rollbackError);
|
|
11009
|
-
}
|
|
11010
|
-
}
|
|
11011
|
-
this.numOpenTransactions = 0;
|
|
11012
|
-
});
|
|
11013
|
-
throw error;
|
|
11014
|
-
}
|
|
11015
|
-
}
|
|
11016
|
-
/**
|
|
11017
|
-
* Synchronous executor for the format-2 record store (#2816). SQL.js has no
|
|
11018
|
-
* savepoints exposed here, so nesting reuses the open transaction rather
|
|
11019
|
-
* than issuing a second BEGIN.
|
|
11020
|
-
*/
|
|
11021
|
-
/**
|
|
11022
|
-
* SQL.js is an in-memory database (#2816): `new SQL.Database()` and nothing
|
|
11023
|
-
* behind it. It can host a large document's merged view for the life of a
|
|
11024
|
-
* page, and cannot BE one — a reload would take the document and every write
|
|
11025
|
-
* the server has not acknowledged with it. Until the worker-hosted OPFS
|
|
11026
|
-
* engine lands, saying so is what turns that into a typed refusal before a
|
|
11027
|
-
* cold load starts writing, instead of a document that quietly evaporates.
|
|
11028
|
-
*/
|
|
11029
|
-
format2StorageIsDurable() {
|
|
11030
|
-
return false;
|
|
11031
|
-
}
|
|
11032
|
-
createFormat2Sql() {
|
|
11033
|
-
if (!this.db) {
|
|
11034
|
-
throw new Error(
|
|
11035
|
-
"[SqljsEngine] SQL.js not initialized; await ensureReady() before creating the format-2 executor"
|
|
11036
|
-
);
|
|
11037
|
-
}
|
|
11038
|
-
const db = this.db;
|
|
11039
|
-
let depth = 0;
|
|
11040
|
-
return {
|
|
11041
|
-
exec(query, ...bindings) {
|
|
11042
|
-
const stmt = db.prepare(query);
|
|
11043
|
-
try {
|
|
11044
|
-
if (bindings.length > 0) stmt.bind(bindings);
|
|
11045
|
-
const rows = [];
|
|
11046
|
-
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
11047
|
-
return rows;
|
|
11048
|
-
} finally {
|
|
11049
|
-
stmt.free();
|
|
11050
|
-
}
|
|
11051
|
-
},
|
|
11052
|
-
transaction(fn) {
|
|
11053
|
-
if (depth > 0) return fn();
|
|
11054
|
-
depth++;
|
|
11055
|
-
db.run("BEGIN TRANSACTION");
|
|
11056
|
-
try {
|
|
11057
|
-
const result = fn();
|
|
11058
|
-
db.run("COMMIT");
|
|
11059
|
-
return result;
|
|
11060
|
-
} catch (error) {
|
|
11061
|
-
db.run("ROLLBACK");
|
|
11062
|
-
throw error;
|
|
11063
|
-
} finally {
|
|
11064
|
-
depth--;
|
|
11065
|
-
}
|
|
11066
|
-
}
|
|
11067
|
-
};
|
|
11068
|
-
}
|
|
11069
|
-
async close() {
|
|
11070
|
-
await this.readyPromise;
|
|
11071
|
-
if (this.db) {
|
|
11072
|
-
this.db.close();
|
|
11073
|
-
this.db = null;
|
|
11074
|
-
this.sync = null;
|
|
11075
|
-
_SqljsEngine.SQL = null;
|
|
11076
|
-
}
|
|
11077
|
-
}
|
|
11078
|
-
async getTableSchema(_tableName) {
|
|
11079
|
-
return void 0;
|
|
11080
|
-
}
|
|
11081
|
-
async destroy() {
|
|
11082
|
-
if (this.db) {
|
|
11083
|
-
this.db.close();
|
|
11084
|
-
Logger.info("[SqljsEngine] Database closed.");
|
|
11085
|
-
}
|
|
11086
|
-
}
|
|
11087
|
-
getLastErrorMessage() {
|
|
11088
|
-
return void 0;
|
|
11089
|
-
}
|
|
11090
|
-
};
|
|
11091
|
-
|
|
11092
|
-
// src/engines/opfs/OpfsEngine.ts
|
|
11093
|
-
init_logger();
|
|
11094
|
-
init_sql();
|
|
11095
|
-
|
|
11096
|
-
// src/utils/snapshotChunks.ts
|
|
11097
|
-
var FORMAT2_MANIFEST_VERSION = 3;
|
|
11098
|
-
var FORMAT2_MIN_MANIFEST_VERSION = 2;
|
|
11099
|
-
var SnapshotChunkIntegrityError = class extends Error {
|
|
11100
|
-
constructor(key, reason) {
|
|
11101
|
-
super(`Snapshot chunk ${key} does not match its manifest entry: ${reason}`);
|
|
11102
|
-
this.key = key;
|
|
11103
|
-
this.reason = reason;
|
|
11104
|
-
this.name = "SnapshotChunkIntegrityError";
|
|
11105
|
-
}
|
|
11106
|
-
};
|
|
11107
|
-
var SnapshotManifestInvalidError = class extends Error {
|
|
11108
|
-
code = "SNAPSHOT_MANIFEST_INVALID";
|
|
11109
|
-
constructor(reason) {
|
|
11110
|
-
super(`Snapshot manifest is not valid: ${reason}`);
|
|
11111
|
-
this.name = "SnapshotManifestInvalidError";
|
|
11112
|
-
}
|
|
11113
|
-
};
|
|
11114
|
-
var SnapshotManifestUnsupportedError = class extends Error {
|
|
11115
|
-
constructor(manifestVersion, supportedVersion = FORMAT2_MANIFEST_VERSION) {
|
|
11116
|
-
super(
|
|
11117
|
-
`Snapshot manifest is version ${manifestVersion}; this client reads up to version ${supportedVersion}. Upgrade the client library.`
|
|
11118
|
-
);
|
|
11119
|
-
this.manifestVersion = manifestVersion;
|
|
11120
|
-
this.supportedVersion = supportedVersion;
|
|
11121
|
-
this.name = "SnapshotManifestUnsupportedError";
|
|
11122
|
-
}
|
|
11123
|
-
code = "SNAPSHOT_MANIFEST_UNSUPPORTED";
|
|
11124
|
-
};
|
|
11125
|
-
var CHUNK_PATH_OWN = /^([A-Za-z][A-Za-z0-9_]*)\/(\d+)$/;
|
|
11126
|
-
var CHUNK_PATH_SOURCED = /^(\d+)-([A-Za-z0-9_-]{1,64})\/([A-Za-z][A-Za-z0-9_]*)\/(\d+)$/;
|
|
11127
|
-
function parseSnapshotChunkPath(path) {
|
|
11128
|
-
if (!path || typeof path !== "string") return null;
|
|
11129
|
-
const own = CHUNK_PATH_OWN.exec(path);
|
|
11130
|
-
if (own) return { model: own[1], index: Number(own[2]) };
|
|
11131
|
-
const sourced = CHUNK_PATH_SOURCED.exec(path);
|
|
11132
|
-
if (!sourced) return null;
|
|
11133
|
-
return {
|
|
11134
|
-
model: sourced[3],
|
|
11135
|
-
index: Number(sourced[4]),
|
|
11136
|
-
source: { epoch: Number(sourced[1]), buildId: sourced[2] }
|
|
11137
|
-
};
|
|
11138
|
-
}
|
|
11139
|
-
function resolveChunkOrdinal(entry, index) {
|
|
11140
|
-
return typeof entry.ordinal === "number" ? entry.ordinal : index;
|
|
11141
|
-
}
|
|
11142
|
-
function collationRank(unit) {
|
|
11143
|
-
if (unit >= 57344) return unit - 2048;
|
|
11144
|
-
if (unit >= 55296) return unit + 8192;
|
|
11145
|
-
return unit;
|
|
11146
|
-
}
|
|
11147
|
-
function compareRecordIds(left, right) {
|
|
11148
|
-
if (left === right) return 0;
|
|
11149
|
-
const shared = Math.min(left.length, right.length);
|
|
11150
|
-
for (let index = 0; index < shared; index++) {
|
|
11151
|
-
const a = left.charCodeAt(index);
|
|
11152
|
-
const b = right.charCodeAt(index);
|
|
11153
|
-
if (a === b) continue;
|
|
11154
|
-
return collationRank(a) < collationRank(b) ? -1 : 1;
|
|
11155
|
-
}
|
|
11156
|
-
return left.length < right.length ? -1 : left.length > right.length ? 1 : 0;
|
|
11157
|
-
}
|
|
11158
|
-
function validateSnapshotManifest(manifest) {
|
|
11159
|
-
if (!manifest || typeof manifest !== "object") {
|
|
11160
|
-
throw new SnapshotManifestInvalidError("it is not an object");
|
|
11161
|
-
}
|
|
11162
|
-
const version = manifest.version;
|
|
11163
|
-
if (!Number.isInteger(version)) {
|
|
11164
|
-
throw new SnapshotManifestInvalidError(`version ${String(version)} is not an integer`);
|
|
11165
|
-
}
|
|
11166
|
-
if (version > FORMAT2_MANIFEST_VERSION) {
|
|
11167
|
-
throw new SnapshotManifestUnsupportedError(version);
|
|
11168
|
-
}
|
|
11169
|
-
if (version < FORMAT2_MIN_MANIFEST_VERSION) {
|
|
11170
|
-
throw new SnapshotManifestInvalidError(
|
|
11171
|
-
`version ${version} is older than the large-document format (${FORMAT2_MIN_MANIFEST_VERSION})`
|
|
11172
|
-
);
|
|
11173
|
-
}
|
|
11174
|
-
const chunks = manifest.chunks;
|
|
11175
|
-
if (!Array.isArray(chunks)) {
|
|
11176
|
-
throw new SnapshotManifestInvalidError("its chunk list is not an array");
|
|
11177
|
-
}
|
|
11178
|
-
const seenOrdinals = /* @__PURE__ */ new Set();
|
|
11179
|
-
const lastRangeEndPerModel = /* @__PURE__ */ new Map();
|
|
11180
|
-
let rows = 0;
|
|
11181
|
-
let bytes = 0;
|
|
11182
|
-
for (let index = 0; index < chunks.length; index++) {
|
|
11183
|
-
const entry = chunks[index];
|
|
11184
|
-
const where = `chunk ${index} (${entry?.model ?? "?"})`;
|
|
11185
|
-
if (!entry || typeof entry !== "object") {
|
|
11186
|
-
throw new SnapshotManifestInvalidError(`${where} is not an object`);
|
|
11187
|
-
}
|
|
11188
|
-
if (entry.ordinal !== void 0 && !Number.isSafeInteger(entry.ordinal)) {
|
|
11189
|
-
throw new SnapshotManifestInvalidError(
|
|
11190
|
-
`${where} has ordinal ${String(entry.ordinal)}, which is not an integer`
|
|
11191
|
-
);
|
|
11192
|
-
}
|
|
11193
|
-
const ordinal = resolveChunkOrdinal(entry, index);
|
|
11194
|
-
if (!Number.isSafeInteger(ordinal) || ordinal < 0) {
|
|
11195
|
-
throw new SnapshotManifestInvalidError(
|
|
11196
|
-
`${where} resolves to ordinal ${String(ordinal)}, which is not a non-negative integer`
|
|
11197
|
-
);
|
|
11198
|
-
}
|
|
11199
|
-
if (seenOrdinals.has(ordinal)) {
|
|
11200
|
-
throw new SnapshotManifestInvalidError(
|
|
11201
|
-
`${where} shares ordinal ${ordinal} with an earlier chunk`
|
|
11202
|
-
);
|
|
11203
|
-
}
|
|
11204
|
-
seenOrdinals.add(ordinal);
|
|
11205
|
-
if (entry.path !== void 0 && !parseSnapshotChunkPath(entry.path)) {
|
|
11206
|
-
throw new SnapshotManifestInvalidError(
|
|
11207
|
-
`${where} has a path that is not addressable: ${String(entry.path)}`
|
|
11208
|
-
);
|
|
11209
|
-
}
|
|
11210
|
-
if (entry.rows > 0 && compareRecordIds(entry.firstId, entry.lastId) > 0) {
|
|
11211
|
-
throw new SnapshotManifestInvalidError(
|
|
11212
|
-
`${where} spans ${entry.firstId}..${entry.lastId}, which runs backwards`
|
|
11213
|
-
);
|
|
11214
|
-
}
|
|
11215
|
-
if (entry.rows > 0) {
|
|
11216
|
-
const previousEnd = lastRangeEndPerModel.get(entry.model);
|
|
11217
|
-
if (previousEnd !== void 0 && compareRecordIds(entry.firstId, previousEnd) <= 0) {
|
|
11218
|
-
throw new SnapshotManifestInvalidError(
|
|
11219
|
-
`${where} starts at ${entry.firstId}, at or before ${previousEnd} where the previous chunk of ${entry.model} ended`
|
|
11220
|
-
);
|
|
11221
|
-
}
|
|
11222
|
-
lastRangeEndPerModel.set(entry.model, entry.lastId);
|
|
11223
|
-
}
|
|
11224
|
-
rows += Number(entry.rows) || 0;
|
|
11225
|
-
bytes += Number(entry.bytes) || 0;
|
|
11226
|
-
}
|
|
11227
|
-
const totals = manifest.totals;
|
|
11228
|
-
if (!totals || totals.rows !== rows || totals.bytes !== bytes) {
|
|
11229
|
-
throw new SnapshotManifestInvalidError(
|
|
11230
|
-
`its totals say ${totals?.rows}/${totals?.bytes} rows/bytes but its chunks add up to ${rows}/${bytes}`
|
|
11231
|
-
);
|
|
12071
|
+
/** The open connection, once the wasm has loaded. */
|
|
12072
|
+
async connection() {
|
|
12073
|
+
await this.readyPromise;
|
|
12074
|
+
if (!this.db || !this.sync) throw new Error("SQL.js not initialized");
|
|
12075
|
+
return this.sync;
|
|
11232
12076
|
}
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
12077
|
+
async withTransaction(callback) {
|
|
12078
|
+
await this.readyPromise;
|
|
12079
|
+
if (!this.db)
|
|
12080
|
+
throw new Error("SQL.js Database instance not available for transaction");
|
|
12081
|
+
let transactionStartedHere = false;
|
|
12082
|
+
await this.mutex.runExclusive(async () => {
|
|
12083
|
+
if (this.numOpenTransactions === 0) {
|
|
12084
|
+
if (!this.db) throw new Error("SQL.js DB not available for BEGIN");
|
|
12085
|
+
this.db.run("BEGIN TRANSACTION");
|
|
12086
|
+
transactionStartedHere = true;
|
|
12087
|
+
}
|
|
12088
|
+
this.numOpenTransactions++;
|
|
12089
|
+
});
|
|
12090
|
+
const transactionalOps = new TransactionalSQLJSOperations(this.db, this);
|
|
12091
|
+
try {
|
|
12092
|
+
const result = await callback(transactionalOps);
|
|
12093
|
+
await this.mutex.runExclusive(async () => {
|
|
12094
|
+
this.numOpenTransactions--;
|
|
12095
|
+
if (this.numOpenTransactions === 0) {
|
|
12096
|
+
if (!this.db) throw new Error("SQL.js DB not available for COMMIT");
|
|
12097
|
+
this.db.run("COMMIT");
|
|
12098
|
+
}
|
|
12099
|
+
});
|
|
12100
|
+
return result;
|
|
12101
|
+
} catch (error) {
|
|
12102
|
+
Logger.error("[SqljsEngine] Error in transaction, rolling back:", error);
|
|
12103
|
+
await this.mutex.runExclusive(async () => {
|
|
12104
|
+
if (this.db && this.numOpenTransactions > 0 && transactionStartedHere) {
|
|
12105
|
+
try {
|
|
12106
|
+
if (!this.db)
|
|
12107
|
+
throw new Error("SQL.js DB not available for ROLLBACK");
|
|
12108
|
+
this.db.run("ROLLBACK");
|
|
12109
|
+
} catch (rollbackError) {
|
|
12110
|
+
Logger.error("[SqljsEngine] Error during ROLLBACK:", rollbackError);
|
|
12111
|
+
}
|
|
12112
|
+
}
|
|
12113
|
+
this.numOpenTransactions = 0;
|
|
12114
|
+
});
|
|
12115
|
+
throw error;
|
|
11248
12116
|
}
|
|
11249
|
-
fields[field] = value;
|
|
11250
|
-
}
|
|
11251
|
-
return { id, fields, stringSets, replace: true, deleted: false };
|
|
11252
|
-
}
|
|
11253
|
-
var encoder = new TextEncoder();
|
|
11254
|
-
var decoder = new TextDecoder();
|
|
11255
|
-
async function gunzip(bytes) {
|
|
11256
|
-
return await collect(
|
|
11257
|
-
new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
|
|
11258
|
-
);
|
|
11259
|
-
}
|
|
11260
|
-
async function collect(stream) {
|
|
11261
|
-
const parts = [];
|
|
11262
|
-
let total = 0;
|
|
11263
|
-
const reader = stream.getReader();
|
|
11264
|
-
for (; ; ) {
|
|
11265
|
-
const { done, value } = await reader.read();
|
|
11266
|
-
if (done) break;
|
|
11267
|
-
parts.push(value);
|
|
11268
|
-
total += value.byteLength;
|
|
11269
12117
|
}
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
12118
|
+
/**
|
|
12119
|
+
* Synchronous executor for the format-2 record store (#2816). SQL.js has no
|
|
12120
|
+
* savepoints exposed here, so nesting reuses the open transaction rather
|
|
12121
|
+
* than issuing a second BEGIN.
|
|
12122
|
+
*/
|
|
12123
|
+
/**
|
|
12124
|
+
* SQL.js is an in-memory database (#2816): `new SQL.Database()` and nothing
|
|
12125
|
+
* behind it. It can host a large document's merged view for the life of a
|
|
12126
|
+
* page, and cannot BE one — a reload would take the document and every write
|
|
12127
|
+
* the server has not acknowledged with it. Until the worker-hosted OPFS
|
|
12128
|
+
* engine lands, saying so is what turns that into a typed refusal before a
|
|
12129
|
+
* cold load starts writing, instead of a document that quietly evaporates.
|
|
12130
|
+
*/
|
|
12131
|
+
format2StorageIsDurable() {
|
|
12132
|
+
return false;
|
|
11275
12133
|
}
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
11281
|
-
}
|
|
11282
|
-
async function readSnapshotChunk(body, descriptor) {
|
|
11283
|
-
const entry = "sha256" in descriptor ? descriptor : null;
|
|
11284
|
-
if (entry) {
|
|
11285
|
-
if (body.byteLength !== entry.bytes) {
|
|
11286
|
-
throw new SnapshotChunkIntegrityError(
|
|
11287
|
-
entry.key,
|
|
11288
|
-
`expected ${entry.bytes} bytes, got ${body.byteLength}`
|
|
11289
|
-
);
|
|
11290
|
-
}
|
|
11291
|
-
const digest = await sha256Hex(body);
|
|
11292
|
-
if (digest !== entry.sha256) {
|
|
11293
|
-
throw new SnapshotChunkIntegrityError(
|
|
11294
|
-
entry.key,
|
|
11295
|
-
`expected sha256 ${entry.sha256}, got ${digest}`
|
|
12134
|
+
createFormat2Sql() {
|
|
12135
|
+
if (!this.db) {
|
|
12136
|
+
throw new Error(
|
|
12137
|
+
"[SqljsEngine] SQL.js not initialized; await ensureReady() before creating the format-2 executor"
|
|
11296
12138
|
);
|
|
11297
12139
|
}
|
|
12140
|
+
const db = this.db;
|
|
12141
|
+
let depth = 0;
|
|
12142
|
+
return {
|
|
12143
|
+
exec(query, ...bindings) {
|
|
12144
|
+
const stmt = db.prepare(query);
|
|
12145
|
+
try {
|
|
12146
|
+
if (bindings.length > 0) stmt.bind(bindings);
|
|
12147
|
+
const rows = [];
|
|
12148
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
12149
|
+
return rows;
|
|
12150
|
+
} finally {
|
|
12151
|
+
stmt.free();
|
|
12152
|
+
}
|
|
12153
|
+
},
|
|
12154
|
+
transaction(fn) {
|
|
12155
|
+
if (depth > 0) return fn();
|
|
12156
|
+
depth++;
|
|
12157
|
+
db.run("BEGIN TRANSACTION");
|
|
12158
|
+
try {
|
|
12159
|
+
const result = fn();
|
|
12160
|
+
db.run("COMMIT");
|
|
12161
|
+
return result;
|
|
12162
|
+
} catch (error) {
|
|
12163
|
+
db.run("ROLLBACK");
|
|
12164
|
+
throw error;
|
|
12165
|
+
} finally {
|
|
12166
|
+
depth--;
|
|
12167
|
+
}
|
|
12168
|
+
}
|
|
12169
|
+
};
|
|
11298
12170
|
}
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
entry?.key ?? descriptor.model,
|
|
11307
|
-
"a line carries no id/data separator"
|
|
11308
|
-
);
|
|
12171
|
+
async close() {
|
|
12172
|
+
await this.readyPromise;
|
|
12173
|
+
if (this.db) {
|
|
12174
|
+
this.db.close();
|
|
12175
|
+
this.db = null;
|
|
12176
|
+
this.sync = null;
|
|
12177
|
+
_SqljsEngine.SQL = null;
|
|
11309
12178
|
}
|
|
11310
|
-
rows.push({
|
|
11311
|
-
_type: descriptor.model,
|
|
11312
|
-
_id: line.slice(0, tab),
|
|
11313
|
-
_data: line.slice(tab + 1)
|
|
11314
|
-
});
|
|
11315
12179
|
}
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
entry.key,
|
|
11319
|
-
`expected ${entry.rows} rows, got ${rows.length}`
|
|
11320
|
-
);
|
|
12180
|
+
async getTableSchema(_tableName) {
|
|
12181
|
+
return void 0;
|
|
11321
12182
|
}
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
throw new SnapshotChunkIntegrityError(
|
|
11327
|
-
entry.key,
|
|
11328
|
-
`duplicate id ${row._id}`
|
|
11329
|
-
);
|
|
11330
|
-
}
|
|
11331
|
-
seen.add(row._id);
|
|
11332
|
-
}
|
|
11333
|
-
if (rows[0]._id !== entry.firstId) {
|
|
11334
|
-
throw new SnapshotChunkIntegrityError(
|
|
11335
|
-
entry.key,
|
|
11336
|
-
`expected it to begin at ${entry.firstId}, got ${rows[0]._id}`
|
|
11337
|
-
);
|
|
11338
|
-
}
|
|
11339
|
-
if (rows[rows.length - 1]._id !== entry.lastId) {
|
|
11340
|
-
throw new SnapshotChunkIntegrityError(
|
|
11341
|
-
entry.key,
|
|
11342
|
-
`expected it to end at ${entry.lastId}, got ${rows[rows.length - 1]._id}`
|
|
11343
|
-
);
|
|
12183
|
+
async destroy() {
|
|
12184
|
+
if (this.db) {
|
|
12185
|
+
this.db.close();
|
|
12186
|
+
Logger.info("[SqljsEngine] Database closed.");
|
|
11344
12187
|
}
|
|
11345
12188
|
}
|
|
11346
|
-
|
|
11347
|
-
|
|
12189
|
+
getLastErrorMessage() {
|
|
12190
|
+
return void 0;
|
|
12191
|
+
}
|
|
12192
|
+
};
|
|
12193
|
+
|
|
12194
|
+
// src/engines/opfs/OpfsEngine.ts
|
|
12195
|
+
init_logger();
|
|
12196
|
+
init_sql();
|
|
11348
12197
|
|
|
11349
12198
|
// src/models/format2StorageCapability.ts
|
|
12199
|
+
init_snapshotChunks();
|
|
11350
12200
|
var SNAPSHOT_STORAGE_EXPANSION = 2;
|
|
11351
12201
|
var SNAPSHOT_COMPRESSED_EXPANSION = 8;
|
|
11352
12202
|
function estimateMaterializedBytes(chunk) {
|
|
@@ -13558,10 +14408,19 @@ function sameOverlayValue(a, b) {
|
|
|
13558
14408
|
if (a === null || a === void 0) return b === null || b === void 0;
|
|
13559
14409
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
13560
14410
|
}
|
|
13561
|
-
function restorePendingOverlay(doc, store, verdicts) {
|
|
13562
|
-
const result = {
|
|
14411
|
+
function restorePendingOverlay(doc, store, verdicts, options = {}) {
|
|
14412
|
+
const result = {
|
|
14413
|
+
restored: [],
|
|
14414
|
+
unreproducible: [],
|
|
14415
|
+
deferred: []
|
|
14416
|
+
};
|
|
14417
|
+
const defer = options.deferAtOrBelowEpoch ?? 0;
|
|
13563
14418
|
const missing = [];
|
|
13564
14419
|
for (const op of store.pendingOps()) {
|
|
14420
|
+
if (defer > 0 && op.baseEpoch > 0 && op.baseEpoch <= defer) {
|
|
14421
|
+
result.deferred.push(op.seq);
|
|
14422
|
+
continue;
|
|
14423
|
+
}
|
|
13565
14424
|
const verdict = verdicts?.get(op.seq);
|
|
13566
14425
|
if (verdict === "carried" || verdict === "superseded") continue;
|
|
13567
14426
|
if (verdict === void 0 && overlayCarries(doc, op)) continue;
|
|
@@ -13586,8 +14445,27 @@ function restorePendingOverlay(doc, store, verdicts) {
|
|
|
13586
14445
|
}, FORMAT2_LOCAL_WRITE_ORIGIN);
|
|
13587
14446
|
return result;
|
|
13588
14447
|
}
|
|
14448
|
+
function statePendingOps(doc, ops, options = {}) {
|
|
14449
|
+
const suppressed = new Set(
|
|
14450
|
+
(options.suppressDeletes ?? []).map(
|
|
14451
|
+
(entry) => `${entry.model} ${entry.recordId}`
|
|
14452
|
+
)
|
|
14453
|
+
);
|
|
14454
|
+
const stated = [...ops].sort((a, b) => a.seq - b.seq).filter(
|
|
14455
|
+
(op) => op.mutation !== null && !(op.op === "delete" && suppressed.has(`${op.model} ${op.recordId}`))
|
|
14456
|
+
);
|
|
14457
|
+
if (stated.length === 0) return 0;
|
|
14458
|
+
doc.transact(() => {
|
|
14459
|
+
for (const op of stated) {
|
|
14460
|
+
applyOverlayMutation(doc.getMap(op.model), op.mutation);
|
|
14461
|
+
}
|
|
14462
|
+
});
|
|
14463
|
+
return stated.length;
|
|
14464
|
+
}
|
|
13589
14465
|
|
|
13590
14466
|
// src/models/format2SnapshotLoader.ts
|
|
14467
|
+
init_snapshotChunks();
|
|
14468
|
+
var PRUNE_PAGE_ROWS = 500;
|
|
13591
14469
|
var SnapshotLoadIncompleteError = class extends Error {
|
|
13592
14470
|
constructor(buildId, missing, passes) {
|
|
13593
14471
|
super(
|
|
@@ -13629,6 +14507,7 @@ async function loadFormat2Snapshot(options) {
|
|
|
13629
14507
|
const { store, manifest } = options;
|
|
13630
14508
|
const retries = options.retries ?? 1;
|
|
13631
14509
|
validateSnapshotManifest(manifest);
|
|
14510
|
+
await store.beginBase(manifest.buildId, manifest.epoch);
|
|
13632
14511
|
const already = new Set(await store.completedChunks(manifest.buildId));
|
|
13633
14512
|
const ordinals = new Map(
|
|
13634
14513
|
manifest.chunks.map((chunk, index) => [chunk, resolveChunkOrdinal(chunk, index)])
|
|
@@ -13636,7 +14515,20 @@ async function loadFormat2Snapshot(options) {
|
|
|
13636
14515
|
await store.setHydrationScope(options.models ? [...options.models] : null);
|
|
13637
14516
|
const cap = options.models;
|
|
13638
14517
|
const wanted = (model) => cap === void 0 || cap.includes(model);
|
|
13639
|
-
const
|
|
14518
|
+
const plan = options.plan;
|
|
14519
|
+
const coverages = new Map(
|
|
14520
|
+
plan?.fetch.map((entry) => [entry.chunk, entry.coverage]) ?? []
|
|
14521
|
+
);
|
|
14522
|
+
const keptChunks = new Set(plan?.keep ?? []);
|
|
14523
|
+
const selected = (plan ? [...plan.fetch.map((entry) => entry.chunk), ...plan.keep].sort(
|
|
14524
|
+
(a, b) => manifest.chunks.indexOf(a) - manifest.chunks.indexOf(b)
|
|
14525
|
+
) : manifest.chunks).filter((chunk) => wanted(chunk.model));
|
|
14526
|
+
const announcedTouched = /* @__PURE__ */ new Set();
|
|
14527
|
+
const announceTouched = async (model) => {
|
|
14528
|
+
if (announcedTouched.has(model)) return;
|
|
14529
|
+
announcedTouched.add(model);
|
|
14530
|
+
await options.onModelTouched?.(model);
|
|
14531
|
+
};
|
|
13640
14532
|
const totalChunks = selected.length;
|
|
13641
14533
|
const totalRows = selected.reduce((total, chunk) => total + chunk.rows, 0);
|
|
13642
14534
|
const models = [];
|
|
@@ -13657,6 +14549,14 @@ async function loadFormat2Snapshot(options) {
|
|
|
13657
14549
|
);
|
|
13658
14550
|
if (!models.includes(chunk.model)) models.push(chunk.model);
|
|
13659
14551
|
}
|
|
14552
|
+
for (const model of plan?.pruneModels ?? []) {
|
|
14553
|
+
await announceTouched(model);
|
|
14554
|
+
for (; ; ) {
|
|
14555
|
+
const page = await store.deleteRange(model, null, null, PRUNE_PAGE_ROWS);
|
|
14556
|
+
if (page.done) break;
|
|
14557
|
+
}
|
|
14558
|
+
await options.onModelReady?.(model);
|
|
14559
|
+
}
|
|
13660
14560
|
const applyOne = async (chunk, outstanding) => {
|
|
13661
14561
|
let decoded = null;
|
|
13662
14562
|
let lastError = null;
|
|
@@ -13673,14 +14573,27 @@ async function loadFormat2Snapshot(options) {
|
|
|
13673
14573
|
}
|
|
13674
14574
|
if (!decoded) throw new SnapshotLoadIntegrityError(chunk, lastError);
|
|
13675
14575
|
const stringSetFields = snapshotStringSetFields(manifest, chunk.model);
|
|
13676
|
-
|
|
13677
|
-
|
|
13678
|
-
|
|
13679
|
-
(row) => rowAsEntry(row._id, JSON.parse(row._data), stringSetFields)
|
|
13680
|
-
),
|
|
13681
|
-
manifest.buildId,
|
|
13682
|
-
ordinals.get(chunk)
|
|
14576
|
+
const coverage = coverages.get(chunk);
|
|
14577
|
+
const entries = decoded.map(
|
|
14578
|
+
(row) => rowAsEntry(row._id, JSON.parse(row._data), stringSetFields)
|
|
13683
14579
|
);
|
|
14580
|
+
if (coverage) {
|
|
14581
|
+
await announceTouched(chunk.model);
|
|
14582
|
+
await store.replaceChunk(
|
|
14583
|
+
chunk.model,
|
|
14584
|
+
coverage,
|
|
14585
|
+
entries,
|
|
14586
|
+
manifest.buildId,
|
|
14587
|
+
ordinals.get(chunk)
|
|
14588
|
+
);
|
|
14589
|
+
} else {
|
|
14590
|
+
await store.applyChunk(
|
|
14591
|
+
chunk.model,
|
|
14592
|
+
entries,
|
|
14593
|
+
manifest.buildId,
|
|
14594
|
+
ordinals.get(chunk)
|
|
14595
|
+
);
|
|
14596
|
+
}
|
|
13684
14597
|
chunks++;
|
|
13685
14598
|
rows += decoded.length;
|
|
13686
14599
|
options.onProgress?.({
|
|
@@ -13692,6 +14605,7 @@ async function loadFormat2Snapshot(options) {
|
|
|
13692
14605
|
});
|
|
13693
14606
|
if (outstanding === 0) await options.onModelReady?.(chunk.model);
|
|
13694
14607
|
};
|
|
14608
|
+
let reused = 0;
|
|
13695
14609
|
for (const chunk of selected) {
|
|
13696
14610
|
const outstanding = (remainingPerModel.get(chunk.model) ?? 1) - 1;
|
|
13697
14611
|
remainingPerModel.set(chunk.model, outstanding);
|
|
@@ -13702,6 +14616,14 @@ async function loadFormat2Snapshot(options) {
|
|
|
13702
14616
|
if (outstanding === 0) await options.onModelReady?.(chunk.model);
|
|
13703
14617
|
continue;
|
|
13704
14618
|
}
|
|
14619
|
+
if (keptChunks.has(chunk)) {
|
|
14620
|
+
await store.applyChunk(chunk.model, [], manifest.buildId, ordinals.get(chunk));
|
|
14621
|
+
reused++;
|
|
14622
|
+
chunks++;
|
|
14623
|
+
rows += chunk.rows;
|
|
14624
|
+
if (outstanding === 0) await options.onModelReady?.(chunk.model);
|
|
14625
|
+
continue;
|
|
14626
|
+
}
|
|
13705
14627
|
await applyOne(chunk, outstanding);
|
|
13706
14628
|
}
|
|
13707
14629
|
const repaired = /* @__PURE__ */ new Set();
|
|
@@ -13723,6 +14645,7 @@ async function loadFormat2Snapshot(options) {
|
|
|
13723
14645
|
repaired.add(chunk.model);
|
|
13724
14646
|
}
|
|
13725
14647
|
}
|
|
14648
|
+
await store.completeBase(manifest.buildId);
|
|
13726
14649
|
return {
|
|
13727
14650
|
rows,
|
|
13728
14651
|
chunks,
|
|
@@ -13730,7 +14653,8 @@ async function loadFormat2Snapshot(options) {
|
|
|
13730
14653
|
refetched,
|
|
13731
14654
|
models,
|
|
13732
14655
|
skipped,
|
|
13733
|
-
repaired: [...repaired]
|
|
14656
|
+
repaired: [...repaired],
|
|
14657
|
+
reused
|
|
13734
14658
|
};
|
|
13735
14659
|
}
|
|
13736
14660
|
|
|
@@ -13848,7 +14772,20 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
13848
14772
|
await store.whenBatchesSettled();
|
|
13849
14773
|
if (options.restorePending !== false) {
|
|
13850
14774
|
const verdicts = await this.adoptOrphanedWrites(docId, yDoc, store);
|
|
13851
|
-
const
|
|
14775
|
+
const crossed = store.discontinuityEpochs();
|
|
14776
|
+
const boundary = crossed.length > 0 ? crossed[crossed.length - 1] : 0;
|
|
14777
|
+
const deferAtOrBelowEpoch = boundary > 0 && store.epoch() > boundary ? boundary : 0;
|
|
14778
|
+
const restored = restorePendingOverlay(
|
|
14779
|
+
yDoc,
|
|
14780
|
+
store,
|
|
14781
|
+
verdicts,
|
|
14782
|
+
deferAtOrBelowEpoch > 0 ? { deferAtOrBelowEpoch } : {}
|
|
14783
|
+
);
|
|
14784
|
+
if (restored.deferred.length > 0) {
|
|
14785
|
+
Logger.warn(
|
|
14786
|
+
`[DocumentManager] ${docId} is holding ${restored.deferred.length} unacknowledged write(s) off its overlay until it has converged on the bulk load at epoch ${deferAtOrBelowEpoch}.`
|
|
14787
|
+
);
|
|
14788
|
+
}
|
|
13852
14789
|
const adoptedWasRestored = verdicts !== void 0 && restored.restored.some((seq) => verdicts.has(seq));
|
|
13853
14790
|
if (adoptedWasRestored) {
|
|
13854
14791
|
foldFormat2Overlay(
|
|
@@ -14044,27 +14981,119 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
14044
14981
|
);
|
|
14045
14982
|
}
|
|
14046
14983
|
const hydrated = /* @__PURE__ */ new Set();
|
|
14984
|
+
const replacement = callbacks.plan;
|
|
14985
|
+
const touchedModels = /* @__PURE__ */ new Set();
|
|
14047
14986
|
const result = await loadFormat2Snapshot({
|
|
14048
14987
|
...plan.kind === "capped" ? { models: plan.models } : {},
|
|
14049
14988
|
store,
|
|
14050
14989
|
manifest,
|
|
14051
14990
|
fetchChunk,
|
|
14991
|
+
...replacement ? { plan: replacement } : {},
|
|
14052
14992
|
onProgress: callbacks.onProgress,
|
|
14993
|
+
onModelTouched: async (model) => {
|
|
14994
|
+
touchedModels.add(model);
|
|
14995
|
+
await store.forgetQueryProjection(model);
|
|
14996
|
+
await this.clearQueryRows(docId, store, model);
|
|
14997
|
+
await callbacks.onModelTouched?.(model);
|
|
14998
|
+
},
|
|
14053
14999
|
onModelReady: async (model) => {
|
|
14054
|
-
|
|
14055
|
-
|
|
15000
|
+
const owed = replacement !== void 0 && !touchedModels.has(model) && !await store.queryProjectionIsCurrent(model);
|
|
15001
|
+
if (owed) {
|
|
15002
|
+
await this.clearQueryRows(docId, store, model);
|
|
15003
|
+
}
|
|
15004
|
+
if (!replacement || touchedModels.has(model) || owed) {
|
|
15005
|
+
await this.hydrateQueryTables(docId, store, { only: model });
|
|
15006
|
+
await this.whenProjectionSettled();
|
|
15007
|
+
}
|
|
14056
15008
|
hydrated.add(model);
|
|
14057
15009
|
await callbacks.onModelReady?.(model);
|
|
14058
15010
|
}
|
|
14059
15011
|
});
|
|
14060
|
-
|
|
14061
|
-
|
|
15012
|
+
if (!replacement) {
|
|
15013
|
+
await this.hydrateQueryTables(docId, store, { except: hydrated });
|
|
15014
|
+
await this.whenProjectionSettled();
|
|
15015
|
+
}
|
|
14062
15016
|
for (const model of result.repaired) {
|
|
14063
15017
|
await this.hydrateQueryTables(docId, store, { only: model });
|
|
14064
15018
|
await this.whenProjectionSettled();
|
|
14065
15019
|
}
|
|
14066
15020
|
return result;
|
|
14067
15021
|
}
|
|
15022
|
+
/**
|
|
15023
|
+
* The models this client holds rows of for `docId` (#3435).
|
|
15024
|
+
*
|
|
15025
|
+
* The convergence planner needs an inventory, not a cap: a model the new
|
|
15026
|
+
* base has no chunk of is pruned whole, and `null` — "the whole document" —
|
|
15027
|
+
* cannot say which models those are. A capped device answers with its
|
|
15028
|
+
* hydration scope; an uncapped one answers with the models this instance
|
|
15029
|
+
* registered, which is every model whose rows could be here.
|
|
15030
|
+
*/
|
|
15031
|
+
/**
|
|
15032
|
+
* Hydrate ONE model's derived query tables for a bound large document
|
|
15033
|
+
* (#3615).
|
|
15034
|
+
*
|
|
15035
|
+
* What a model registered on a running instance needs: the merged view
|
|
15036
|
+
* already holds its rows — the fold and the overlay observer are keyed by
|
|
15037
|
+
* the model name the overlay carries, not by the registry — but the derived
|
|
15038
|
+
* tables `query()` reads were never built, because the model was not in the
|
|
15039
|
+
* registry when the document was connected.
|
|
15040
|
+
*
|
|
15041
|
+
* Forced, because the point is a rebuild: whatever mark a previous session
|
|
15042
|
+
* left says nothing about tables this instance has never projected into.
|
|
15043
|
+
*/
|
|
15044
|
+
async hydrateModel(docId, modelName) {
|
|
15045
|
+
const store = this.opened.get(docId);
|
|
15046
|
+
if (!store) return;
|
|
15047
|
+
if (!store.isHydrated(modelName)) {
|
|
15048
|
+
Logger.warn(
|
|
15049
|
+
`[DocumentManager] Large document ${docId} does not hold model '${modelName}' on this device (its hydration scope excludes it), so the query tables for '${modelName}' were not hydrated. Reads of '${modelName}' on ${docId} answer as they do for any model this device declined.`
|
|
15050
|
+
);
|
|
15051
|
+
return;
|
|
15052
|
+
}
|
|
15053
|
+
await this.hydrateQueryTables(docId, store, { only: modelName });
|
|
15054
|
+
await this.whenProjectionSettled();
|
|
15055
|
+
}
|
|
15056
|
+
localModels(docId) {
|
|
15057
|
+
const store = this.ormContext.getFormat2StoreAsync(docId);
|
|
15058
|
+
if (!store) return null;
|
|
15059
|
+
const scope = store.hydrationScope();
|
|
15060
|
+
if (scope !== null) return [...scope];
|
|
15061
|
+
const registered = [...this.modelRegistry.getActiveModels().keys()];
|
|
15062
|
+
return registered.length > 0 ? registered : null;
|
|
15063
|
+
}
|
|
15064
|
+
/**
|
|
15065
|
+
* Whether each of these records is in the merged view (#3435, behavior 21).
|
|
15066
|
+
*
|
|
15067
|
+
* What a convergence classifies its owed writes against. Read in one pass
|
|
15068
|
+
* per model, and only AFTER the ranges have been replaced and the overlays
|
|
15069
|
+
* refolded: before that, "absent" says nothing about what the bulk load did.
|
|
15070
|
+
*
|
|
15071
|
+
* `null` for a record of a model this device does not hold — no fact is
|
|
15072
|
+
* better than a wrong one, and the caller decides what a missing verdict
|
|
15073
|
+
* means.
|
|
15074
|
+
*/
|
|
15075
|
+
async recordPresence(docId, records) {
|
|
15076
|
+
const store = this.ormContext.getFormat2StoreAsync(docId);
|
|
15077
|
+
if (!store) return records.map(() => null);
|
|
15078
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
15079
|
+
records.forEach((record, index) => {
|
|
15080
|
+
const list = byModel.get(record.model);
|
|
15081
|
+
if (list) list.push(index);
|
|
15082
|
+
else byModel.set(record.model, [index]);
|
|
15083
|
+
});
|
|
15084
|
+
const answers = records.map(() => null);
|
|
15085
|
+
for (const [model, indexes] of byModel) {
|
|
15086
|
+
if (!store.isHydrated(model)) continue;
|
|
15087
|
+
const rows = await store.readMany(
|
|
15088
|
+
model,
|
|
15089
|
+
indexes.map((index) => records[index].id)
|
|
15090
|
+
);
|
|
15091
|
+
indexes.forEach((index, position) => {
|
|
15092
|
+
answers[index] = rows[position] !== null;
|
|
15093
|
+
});
|
|
15094
|
+
}
|
|
15095
|
+
return answers;
|
|
15096
|
+
}
|
|
14068
15097
|
/**
|
|
14069
15098
|
* Release `docId`. Safe on a document that was never format 2.
|
|
14070
15099
|
*
|
|
@@ -14248,6 +15277,32 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
14248
15277
|
* holds. The sweep after it (`except`) and an ordinary connect honor the
|
|
14249
15278
|
* mark.
|
|
14250
15279
|
*/
|
|
15280
|
+
/**
|
|
15281
|
+
* Delete one model's derived query rows for this document, a page at a time
|
|
15282
|
+
* (#3435).
|
|
15283
|
+
*
|
|
15284
|
+
* The clear half of clear-then-project. Paged from the record store while
|
|
15285
|
+
* the rows are still there, which is the same reason and the same shape as
|
|
15286
|
+
* the discard path: after a range replacement nothing names the records it
|
|
15287
|
+
* removed, and `hydrateQueryTables` only ever projects records that EXIST.
|
|
15288
|
+
*/
|
|
15289
|
+
async clearQueryRows(docId, store, modelName) {
|
|
15290
|
+
const modelClass = this.modelRegistry.getModelClass(modelName);
|
|
15291
|
+
if (!modelClass) return;
|
|
15292
|
+
if (!store.isHydrated(modelName)) return;
|
|
15293
|
+
let after = null;
|
|
15294
|
+
for (; ; ) {
|
|
15295
|
+
const page = await store.recordIdsAfter(
|
|
15296
|
+
modelName,
|
|
15297
|
+
after,
|
|
15298
|
+
QUERY_HYDRATION_PAGE_ROWS
|
|
15299
|
+
);
|
|
15300
|
+
if (page.length === 0) break;
|
|
15301
|
+
await modelClass.deleteFormat2QueryRows(docId, page);
|
|
15302
|
+
after = page[page.length - 1];
|
|
15303
|
+
if (page.length < QUERY_HYDRATION_PAGE_ROWS) break;
|
|
15304
|
+
}
|
|
15305
|
+
}
|
|
14251
15306
|
async hydrateQueryTables(docId, store, scope = {}) {
|
|
14252
15307
|
for (const [, modelClass] of this.modelRegistry.getActiveModels()) {
|
|
14253
15308
|
const modelName = modelClass.getSchema?.()?.options?.name;
|
|
@@ -14440,7 +15495,9 @@ function format2SyncMarks(docId, ctx) {
|
|
|
14440
15495
|
highestLocalSeq: store.highestLocalSeq(),
|
|
14441
15496
|
ackedSeq: store.ackedSeq(),
|
|
14442
15497
|
epoch: store.epoch(),
|
|
14443
|
-
clockOffsetKnown: store.clockOffsetKnown()
|
|
15498
|
+
clockOffsetKnown: store.clockOffsetKnown(),
|
|
15499
|
+
discontinuityEpochs: store.discontinuityEpochs(),
|
|
15500
|
+
baseComplete: store.baseComplete()
|
|
14444
15501
|
};
|
|
14445
15502
|
}
|
|
14446
15503
|
function format2NoteEpoch(docId, epoch, ctx) {
|
|
@@ -14483,6 +15540,17 @@ function format2NoteClockOffset(docId, offsetMs, ctx) {
|
|
|
14483
15540
|
if (!store) return;
|
|
14484
15541
|
store.noteClockOffset(offsetMs);
|
|
14485
15542
|
}
|
|
15543
|
+
async function format2NoteDiscontinuity(docId, epoch, ctx) {
|
|
15544
|
+
const store = (ctx ?? getDefaultOrmContext()).getFormat2StoreAsync(docId);
|
|
15545
|
+
if (!store) return;
|
|
15546
|
+
if (!Number.isInteger(epoch) || epoch <= 0) return;
|
|
15547
|
+
await store.noteDiscontinuity(epoch);
|
|
15548
|
+
}
|
|
15549
|
+
async function format2ClearDiscontinuities(docId, through, ctx) {
|
|
15550
|
+
const store = (ctx ?? getDefaultOrmContext()).getFormat2StoreAsync(docId);
|
|
15551
|
+
if (!store) return;
|
|
15552
|
+
await store.clearDiscontinuities(through);
|
|
15553
|
+
}
|
|
14486
15554
|
function format2ForgetPendingOps(docId, seqs, ctx) {
|
|
14487
15555
|
const store = (ctx ?? getDefaultOrmContext()).getFormat2StoreAsync(docId);
|
|
14488
15556
|
if (!store) return;
|
|
@@ -14520,12 +15588,70 @@ var DocumentManagerImpl = class {
|
|
|
14520
15588
|
modelRegistry;
|
|
14521
15589
|
/** The ORM instance this manager belongs to (#3039). */
|
|
14522
15590
|
ormContext;
|
|
15591
|
+
/**
|
|
15592
|
+
* The registration gate (#3615).
|
|
15593
|
+
*
|
|
15594
|
+
* `connectDocument` finishes its model loop BEFORE it publishes the document
|
|
15595
|
+
* in `connectedDocuments`, so a registration that read the map in that
|
|
15596
|
+
* interval would miss the document while its own loop had already run —
|
|
15597
|
+
* leaving it connected with no table, no rows and no observer for the new
|
|
15598
|
+
* model. Registrations therefore run one at a time on this chain; a connect,
|
|
15599
|
+
* a rebind and a disconnect wait for it at entry, and a registration waits
|
|
15600
|
+
* for the connects that got in before it ({@link activeConnects}) before it
|
|
15601
|
+
* reads the map. Connects of different documents that no registration
|
|
15602
|
+
* overlaps still run concurrently, as they always have.
|
|
15603
|
+
*/
|
|
15604
|
+
registrationChain = Promise.resolve();
|
|
15605
|
+
/** Connects past the gate and not yet published — what a registration waits for. */
|
|
15606
|
+
activeConnects = /* @__PURE__ */ new Set();
|
|
15607
|
+
/**
|
|
15608
|
+
* The `(model, document)` pairs whose initialization for this instance
|
|
15609
|
+
* COMPLETED (#3615).
|
|
15610
|
+
*
|
|
15611
|
+
* Registry membership alone cannot say it: a document-level failure leaves
|
|
15612
|
+
* the class registered but that document unusable, and `initializeForDocument`
|
|
15613
|
+
* clears a document's rows and installs a fresh observer on every run, so a
|
|
15614
|
+
* blanket retry would wipe the documents that had succeeded and double their
|
|
15615
|
+
* observers. A retry resumes with the pairs that are not in here.
|
|
15616
|
+
*/
|
|
15617
|
+
runtimeInitialized = /* @__PURE__ */ new Map();
|
|
15618
|
+
/**
|
|
15619
|
+
* Pass the registration gate, then run `work` as an in-flight connect
|
|
15620
|
+
* (#3615).
|
|
15621
|
+
*
|
|
15622
|
+
* The re-read after the await is what closes the window: a registration
|
|
15623
|
+
* publishes its chain synchronously when it is called, so a caller that
|
|
15624
|
+
* waited on the chain it saw first has to check whether another one was
|
|
15625
|
+
* queued behind it. Joining {@link activeConnects} happens with no await in
|
|
15626
|
+
* between, so nothing can slip between the check and the join.
|
|
15627
|
+
*/
|
|
15628
|
+
async enterConnect(work) {
|
|
15629
|
+
for (; ; ) {
|
|
15630
|
+
const chain = this.registrationChain;
|
|
15631
|
+
await chain;
|
|
15632
|
+
if (this.registrationChain === chain) break;
|
|
15633
|
+
}
|
|
15634
|
+
let settle;
|
|
15635
|
+
const ticket = new Promise((resolve) => settle = resolve);
|
|
15636
|
+
this.activeConnects.add(ticket);
|
|
15637
|
+
try {
|
|
15638
|
+
return await work();
|
|
15639
|
+
} finally {
|
|
15640
|
+
this.activeConnects.delete(ticket);
|
|
15641
|
+
settle();
|
|
15642
|
+
}
|
|
15643
|
+
}
|
|
14523
15644
|
async connectDocument(docId, yDoc, permissionHint, options) {
|
|
15645
|
+
return this.enterConnect(
|
|
15646
|
+
() => this.connectDocumentInner(docId, yDoc, permissionHint, options)
|
|
15647
|
+
);
|
|
15648
|
+
}
|
|
15649
|
+
async connectDocumentInner(docId, yDoc, permissionHint, options) {
|
|
14524
15650
|
if (this.connectedDocuments.has(docId)) {
|
|
14525
15651
|
console.warn(
|
|
14526
15652
|
`[DocumentManager] Document ${docId} is already connected. Disconnecting old version.`
|
|
14527
15653
|
);
|
|
14528
|
-
await this.
|
|
15654
|
+
await this.disconnectDocumentInner(docId);
|
|
14529
15655
|
}
|
|
14530
15656
|
const documentFormat = options?.documentFormat ?? 1;
|
|
14531
15657
|
const connectedDoc = {
|
|
@@ -14552,6 +15678,9 @@ var DocumentManagerImpl = class {
|
|
|
14552
15678
|
await this.format2.connect(docId, yDoc, options?.epoch);
|
|
14553
15679
|
}
|
|
14554
15680
|
this.connectedDocuments.set(docId, connectedDoc);
|
|
15681
|
+
for (const modelName of this.modelRegistry.getActiveModels().keys()) {
|
|
15682
|
+
this.markRuntimeInitialized(modelName, docId);
|
|
15683
|
+
}
|
|
14555
15684
|
this.notifyConnectionChange({
|
|
14556
15685
|
type: "connect",
|
|
14557
15686
|
docId,
|
|
@@ -14570,6 +15699,11 @@ var DocumentManagerImpl = class {
|
|
|
14570
15699
|
* models read and write through is exchanged.
|
|
14571
15700
|
*/
|
|
14572
15701
|
async rebindEpochDocument(docId, yDoc, options = {}) {
|
|
15702
|
+
return this.enterConnect(
|
|
15703
|
+
() => this.rebindEpochDocumentInner(docId, yDoc, options)
|
|
15704
|
+
);
|
|
15705
|
+
}
|
|
15706
|
+
async rebindEpochDocumentInner(docId, yDoc, options = {}) {
|
|
14573
15707
|
const connected = this.connectedDocuments.get(docId);
|
|
14574
15708
|
if (!connected) {
|
|
14575
15709
|
throw new Error(
|
|
@@ -14666,7 +15800,27 @@ var DocumentManagerImpl = class {
|
|
|
14666
15800
|
this.largeDocumentStorage
|
|
14667
15801
|
);
|
|
14668
15802
|
}
|
|
15803
|
+
/**
|
|
15804
|
+
* The models this instance holds rows of for `docId` (#3435) — the inventory
|
|
15805
|
+
* a convergence plan decides its prunes against.
|
|
15806
|
+
*/
|
|
15807
|
+
localModels(docId) {
|
|
15808
|
+
const connected = this.connectedDocuments.get(docId);
|
|
15809
|
+
if (!connected || connected.documentFormat !== 2) return null;
|
|
15810
|
+
return this.format2.localModels(docId);
|
|
15811
|
+
}
|
|
15812
|
+
/** Whether each record is in `docId`'s merged view (#3435, behavior 21). */
|
|
15813
|
+
async recordPresence(docId, records) {
|
|
15814
|
+
const connected = this.connectedDocuments.get(docId);
|
|
15815
|
+
if (!connected || connected.documentFormat !== 2) {
|
|
15816
|
+
return records.map(() => null);
|
|
15817
|
+
}
|
|
15818
|
+
return await this.format2.recordPresence(docId, records);
|
|
15819
|
+
}
|
|
14669
15820
|
async disconnectDocument(docId) {
|
|
15821
|
+
return this.enterConnect(() => this.disconnectDocumentInner(docId));
|
|
15822
|
+
}
|
|
15823
|
+
async disconnectDocumentInner(docId) {
|
|
14670
15824
|
const connectedDoc = this.connectedDocuments.get(docId);
|
|
14671
15825
|
if (!connectedDoc) {
|
|
14672
15826
|
Logger.warn(`[DocumentManager] Document ${docId} is not connected`);
|
|
@@ -14683,6 +15837,9 @@ var DocumentManagerImpl = class {
|
|
|
14683
15837
|
wipeModels ? { wipeIfLastModels: wipeModels } : {}
|
|
14684
15838
|
);
|
|
14685
15839
|
this.connectedDocuments.delete(docId);
|
|
15840
|
+
for (const docIds of this.runtimeInitialized.values()) {
|
|
15841
|
+
docIds.delete(docId);
|
|
15842
|
+
}
|
|
14686
15843
|
this.ormContext.disposeObserversForDocument(docId);
|
|
14687
15844
|
this.ormContext.clearMappingsForDocId(docId);
|
|
14688
15845
|
this.notifyConnectionChange({
|
|
@@ -14693,10 +15850,150 @@ var DocumentManagerImpl = class {
|
|
|
14693
15850
|
`[DocumentManager] Document ${docId} disconnected successfully`
|
|
14694
15851
|
);
|
|
14695
15852
|
}
|
|
15853
|
+
markRuntimeInitialized(modelName, docId) {
|
|
15854
|
+
let docIds = this.runtimeInitialized.get(modelName);
|
|
15855
|
+
if (!docIds) {
|
|
15856
|
+
docIds = /* @__PURE__ */ new Set();
|
|
15857
|
+
this.runtimeInitialized.set(modelName, docIds);
|
|
15858
|
+
}
|
|
15859
|
+
docIds.add(docId);
|
|
15860
|
+
}
|
|
15861
|
+
/**
|
|
15862
|
+
* Add a model to this running instance (#3615). See
|
|
15863
|
+
* {@link InitJsBaoResult.registerModel}.
|
|
15864
|
+
*/
|
|
15865
|
+
async registerModel(modelClass) {
|
|
15866
|
+
const previous = this.registrationChain;
|
|
15867
|
+
let release;
|
|
15868
|
+
this.registrationChain = new Promise((resolve) => release = resolve);
|
|
15869
|
+
try {
|
|
15870
|
+
await previous;
|
|
15871
|
+
while (this.activeConnects.size > 0) {
|
|
15872
|
+
await Promise.all(Array.from(this.activeConnects));
|
|
15873
|
+
}
|
|
15874
|
+
await this.runRegistration(modelClass);
|
|
15875
|
+
} finally {
|
|
15876
|
+
release();
|
|
15877
|
+
}
|
|
15878
|
+
}
|
|
15879
|
+
async runRegistration(modelClass) {
|
|
15880
|
+
if (this.ormContext.destroyed) {
|
|
15881
|
+
throw new Error(
|
|
15882
|
+
`[initJsBao] Cannot register model on destroyed js-bao instance #${this.ormContext.instanceId}. Create a new instance with initJsBao.`
|
|
15883
|
+
);
|
|
15884
|
+
}
|
|
15885
|
+
const modelName = this.modelRegistry.assertAdoptable(
|
|
15886
|
+
modelClass,
|
|
15887
|
+
this.ormContext.instanceId
|
|
15888
|
+
);
|
|
15889
|
+
const boundTo = modelClass?._ormContext;
|
|
15890
|
+
if (boundTo && boundTo !== this.ormContext && !boundTo.destroyed) {
|
|
15891
|
+
throw new Error(
|
|
15892
|
+
`[initJsBao] Model '${modelName}' is bound to live js-bao instance #${boundTo.instanceId}. Give instance #${this.ormContext.instanceId} its own model class (a fresh createModelClass / attachAndRegisterModel result).`
|
|
15893
|
+
);
|
|
15894
|
+
}
|
|
15895
|
+
const { added } = this.modelRegistry.adoptModel(modelClass, {
|
|
15896
|
+
instanceId: this.ormContext.instanceId
|
|
15897
|
+
});
|
|
15898
|
+
bindModelClass(this.ormContext, modelName, modelClass, "throw");
|
|
15899
|
+
let didDocumentWork = false;
|
|
15900
|
+
for (const connected of Array.from(this.connectedDocuments.values())) {
|
|
15901
|
+
if (this.runtimeInitialized.get(modelName)?.has(connected.docId)) {
|
|
15902
|
+
continue;
|
|
15903
|
+
}
|
|
15904
|
+
didDocumentWork = true;
|
|
15905
|
+
try {
|
|
15906
|
+
await this.initializeModelForConnectedDocument(
|
|
15907
|
+
modelClass,
|
|
15908
|
+
modelName,
|
|
15909
|
+
connected
|
|
15910
|
+
);
|
|
15911
|
+
} catch (error) {
|
|
15912
|
+
await modelClass.withdrawDocumentRegistration(connected.docId);
|
|
15913
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15914
|
+
const wrapped = new Error(
|
|
15915
|
+
`[initJsBao] registerModel('${modelName}') failed on document ${connected.docId}; registration is incomplete and can be retried: ` + message
|
|
15916
|
+
);
|
|
15917
|
+
wrapped.cause = error;
|
|
15918
|
+
throw wrapped;
|
|
15919
|
+
}
|
|
15920
|
+
this.markRuntimeInitialized(modelName, connected.docId);
|
|
15921
|
+
}
|
|
15922
|
+
if (added || didDocumentWork) {
|
|
15923
|
+
await this.modelRegistry.initializeRelationships(this.dbEngine);
|
|
15924
|
+
}
|
|
15925
|
+
}
|
|
15926
|
+
/**
|
|
15927
|
+
* Initialize one model for one already-connected document (#3615).
|
|
15928
|
+
*
|
|
15929
|
+
* The legacy half carries the window `initializeForDocument` leaves open: it
|
|
15930
|
+
* loads the Y.Map as records in an asynchronous transaction and installs its
|
|
15931
|
+
* observers afterwards, which on a document the app connected long ago is a
|
|
15932
|
+
* live document still receiving peer updates. A temporary deep observer
|
|
15933
|
+
* collects the record ids touched while that runs; once the instance's
|
|
15934
|
+
* indexing for the document has drained, each of them is written from its
|
|
15935
|
+
* CURRENT state, so the call resolves with the query tables holding every
|
|
15936
|
+
* update that arrived before it did.
|
|
15937
|
+
*/
|
|
15938
|
+
async initializeModelForConnectedDocument(modelClass, modelName, connected) {
|
|
15939
|
+
const { docId, yDoc, permissionHint } = connected;
|
|
15940
|
+
const documentFormat = connected.documentFormat ?? 1;
|
|
15941
|
+
if (documentFormat === 2) {
|
|
15942
|
+
await modelClass.initializeForDocument(
|
|
15943
|
+
yDoc,
|
|
15944
|
+
this.dbEngine,
|
|
15945
|
+
docId,
|
|
15946
|
+
permissionHint,
|
|
15947
|
+
2
|
|
15948
|
+
);
|
|
15949
|
+
await this.format2.hydrateModel(docId, modelName);
|
|
15950
|
+
return;
|
|
15951
|
+
}
|
|
15952
|
+
const touched = /* @__PURE__ */ new Set();
|
|
15953
|
+
const documentYMap = yDoc.getMap(modelName);
|
|
15954
|
+
const collect2 = (events) => {
|
|
15955
|
+
for (const event of events) {
|
|
15956
|
+
if (event.target === documentYMap) {
|
|
15957
|
+
for (const key of event.changes.keys.keys()) touched.add(key);
|
|
15958
|
+
} else if (event.path.length > 0) {
|
|
15959
|
+
touched.add(String(event.path[0]));
|
|
15960
|
+
}
|
|
15961
|
+
}
|
|
15962
|
+
};
|
|
15963
|
+
documentYMap.observeDeep(collect2);
|
|
15964
|
+
try {
|
|
15965
|
+
await modelClass.initializeForDocument(
|
|
15966
|
+
yDoc,
|
|
15967
|
+
this.dbEngine,
|
|
15968
|
+
docId,
|
|
15969
|
+
permissionHint,
|
|
15970
|
+
1
|
|
15971
|
+
);
|
|
15972
|
+
await this.whenDocumentIndexed(docId);
|
|
15973
|
+
for (const recordId of touched) {
|
|
15974
|
+
await modelClass.reindexDocumentRecord(docId, recordId);
|
|
15975
|
+
}
|
|
15976
|
+
} finally {
|
|
15977
|
+
documentYMap.unobserveDeep(collect2);
|
|
15978
|
+
}
|
|
15979
|
+
}
|
|
14696
15980
|
/** Resolves once every enqueued query-projection refresh has been applied. */
|
|
14697
15981
|
whenFormat2ProjectionSettled() {
|
|
14698
15982
|
return this.format2.whenProjectionSettled();
|
|
14699
15983
|
}
|
|
15984
|
+
/**
|
|
15985
|
+
* Resolves once THIS instance's query tables have caught up with `docId`
|
|
15986
|
+
* (#3023). Scoped to the instance's own context, so a second client holding
|
|
15987
|
+
* the same document id never waits on this one's writes.
|
|
15988
|
+
*/
|
|
15989
|
+
async whenDocumentIndexed(docId) {
|
|
15990
|
+
const drained = await this.ormContext.whenDocumentIndexed(docId);
|
|
15991
|
+
if (!drained) {
|
|
15992
|
+
Logger.warn(
|
|
15993
|
+
`[DocumentManager] Document ${docId} is still indexing after 50 drain passes; not waiting any longer`
|
|
15994
|
+
);
|
|
15995
|
+
}
|
|
15996
|
+
}
|
|
14700
15997
|
getConnectedDocuments() {
|
|
14701
15998
|
return new Map(this.connectedDocuments);
|
|
14702
15999
|
}
|
|
@@ -14724,22 +16021,33 @@ var DocumentManagerImpl = class {
|
|
|
14724
16021
|
};
|
|
14725
16022
|
function bindSessionModels(ctx, registry) {
|
|
14726
16023
|
for (const [modelName, modelClass] of registry.getActiveModels()) {
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
16024
|
+
bindModelClass(ctx, modelName, modelClass, "warn");
|
|
16025
|
+
}
|
|
16026
|
+
}
|
|
16027
|
+
function bindModelClass(ctx, modelName, modelClass, onLiveConflict) {
|
|
16028
|
+
const current = modelClass?._ormContext;
|
|
16029
|
+
if (current === ctx) {
|
|
16030
|
+
ctx.boundModels.add(modelClass);
|
|
16031
|
+
return;
|
|
16032
|
+
}
|
|
16033
|
+
if (!current || current.destroyed) {
|
|
16034
|
+
Object.defineProperty(modelClass, "_ormContext", {
|
|
16035
|
+
value: ctx,
|
|
16036
|
+
writable: true,
|
|
16037
|
+
configurable: true,
|
|
16038
|
+
enumerable: false
|
|
16039
|
+
});
|
|
16040
|
+
ctx.boundModels.add(modelClass);
|
|
16041
|
+
return;
|
|
16042
|
+
}
|
|
16043
|
+
if (onLiveConflict === "throw") {
|
|
16044
|
+
throw new Error(
|
|
16045
|
+
`[initJsBao] Model '${modelName}' is bound to live js-bao instance #${current.instanceId}. Give instance #${ctx.instanceId} its own model class (a fresh createModelClass / attachAndRegisterModel result).`
|
|
14741
16046
|
);
|
|
14742
16047
|
}
|
|
16048
|
+
console.warn(
|
|
16049
|
+
`[initJsBao] Model '${modelName}' is already bound to a live js-bao instance #${current.instanceId} and stays with it; instance #${ctx.instanceId}'s queries, writes and subscriptions for '${modelName}' will route to instance #${current.instanceId}. Give each client its own model classes (the schemaToml path already does) if you need them isolated.`
|
|
16050
|
+
);
|
|
14743
16051
|
}
|
|
14744
16052
|
async function initJsBaoCore(options, getEngine) {
|
|
14745
16053
|
const { databaseConfig, models: modelClasses } = options;
|
|
@@ -14768,6 +16076,7 @@ async function initJsBaoCore(options, getEngine) {
|
|
|
14768
16076
|
destroy: () => ormContext.destroy(),
|
|
14769
16077
|
connectDocument: docManager.connectDocument.bind(docManager),
|
|
14770
16078
|
disconnectDocument: docManager.disconnectDocument.bind(docManager),
|
|
16079
|
+
registerModel: docManager.registerModel.bind(docManager),
|
|
14771
16080
|
format2SyncMarks: (docId) => format2SyncMarks(docId, ormContext),
|
|
14772
16081
|
format2StoredEpoch: (docId) => format2StoredEpoch(dbEngine, docId, ormContext),
|
|
14773
16082
|
// The pre-bind epoch read opens the same pool the bind will, so it needs
|
|
@@ -14785,6 +16094,8 @@ async function initJsBaoCore(options, getEngine) {
|
|
|
14785
16094
|
format2NoteWindow: (docId, windowDays) => format2NoteWindow(docId, windowDays, ormContext),
|
|
14786
16095
|
format2NoteClockOffset: (docId, offsetMs) => format2NoteClockOffset(docId, offsetMs, ormContext),
|
|
14787
16096
|
format2ForgetPendingOps: (docId, seqs) => format2ForgetPendingOps(docId, seqs, ormContext),
|
|
16097
|
+
format2NoteDiscontinuity: (docId, epoch) => format2NoteDiscontinuity(docId, epoch, ormContext),
|
|
16098
|
+
format2ClearDiscontinuities: (docId, through) => format2ClearDiscontinuities(docId, through, ormContext),
|
|
14788
16099
|
format2NarrowPendingOps: (docId, ops) => format2NarrowPendingOps(docId, ops, ormContext),
|
|
14789
16100
|
format2PendingOps: (docId) => format2PendingOps(docId, ormContext),
|
|
14790
16101
|
format2SetEpoch: (docId, epoch) => format2SetEpoch(docId, epoch, ormContext),
|
|
@@ -14798,7 +16109,10 @@ async function initJsBaoCore(options, getEngine) {
|
|
|
14798
16109
|
// queue updates after it). The transport awaits it before an update
|
|
14799
16110
|
// from the room counts as applied.
|
|
14800
16111
|
whenFormat2ProjectionSettled: docManager.whenFormat2ProjectionSettled.bind(docManager),
|
|
16112
|
+
whenDocumentIndexed: docManager.whenDocumentIndexed.bind(docManager),
|
|
14801
16113
|
format2LoadSnapshot: docManager.loadSnapshot.bind(docManager),
|
|
16114
|
+
format2LocalModels: (docId) => docManager.localModels(docId),
|
|
16115
|
+
format2RecordPresence: (docId, records) => docManager.recordPresence(docId, records),
|
|
14802
16116
|
getConnectedDocuments: docManager.getConnectedDocuments.bind(docManager),
|
|
14803
16117
|
isDocumentConnected: docManager.isDocumentConnected.bind(docManager),
|
|
14804
16118
|
onDocumentConnectionChange: docManager.onConnectionChange.bind(docManager),
|
|
@@ -14958,40 +16272,6 @@ function createModelClass(config) {
|
|
|
14958
16272
|
attachAndRegisterModel(GeneratedModel, schema);
|
|
14959
16273
|
return GeneratedModel;
|
|
14960
16274
|
}
|
|
14961
|
-
function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
14962
|
-
const resolved = [];
|
|
14963
|
-
for (const [fieldName, options] of fields.entries()) {
|
|
14964
|
-
if (options.unique) {
|
|
14965
|
-
resolved.push({
|
|
14966
|
-
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
14967
|
-
fields: [fieldName]
|
|
14968
|
-
});
|
|
14969
|
-
}
|
|
14970
|
-
}
|
|
14971
|
-
if (customConstraints) {
|
|
14972
|
-
for (const constraint of customConstraints) {
|
|
14973
|
-
const missingField = constraint.fields.find(
|
|
14974
|
-
(field) => !fields.has(field)
|
|
14975
|
-
);
|
|
14976
|
-
if (missingField) {
|
|
14977
|
-
console.warn(
|
|
14978
|
-
`[defineModelSchema] Unique constraint "${constraint.name}" for model "${modelName}" references unknown field "${missingField}". Skipping.`
|
|
14979
|
-
);
|
|
14980
|
-
continue;
|
|
14981
|
-
}
|
|
14982
|
-
if (resolved.some((item) => item.name === constraint.name)) {
|
|
14983
|
-
console.warn(
|
|
14984
|
-
`[defineModelSchema] Duplicate unique constraint name "${constraint.name}" in model "${modelName}".`
|
|
14985
|
-
);
|
|
14986
|
-
}
|
|
14987
|
-
resolved.push({
|
|
14988
|
-
name: constraint.name,
|
|
14989
|
-
fields: [...constraint.fields]
|
|
14990
|
-
});
|
|
14991
|
-
}
|
|
14992
|
-
}
|
|
14993
|
-
return resolved;
|
|
14994
|
-
}
|
|
14995
16275
|
function attachSchemaToClass(modelClass, schema) {
|
|
14996
16276
|
modelClass.schema = schema;
|
|
14997
16277
|
modelClass.modelName = schema.options.name;
|
|
@@ -15233,6 +16513,26 @@ function schemaToToml(schema) {
|
|
|
15233
16513
|
|
|
15234
16514
|
// src/models/tomlLoader.ts
|
|
15235
16515
|
var import_smol_toml = require("smol-toml");
|
|
16516
|
+
|
|
16517
|
+
// src/utils/reservedFields.ts
|
|
16518
|
+
function checkReservedFieldName(fieldName) {
|
|
16519
|
+
if (fieldName === "id") return null;
|
|
16520
|
+
if (fieldName === "type") {
|
|
16521
|
+
return `Field 'type' is reserved (maps to internal _type column in queries)`;
|
|
16522
|
+
}
|
|
16523
|
+
if (fieldName.startsWith("_")) {
|
|
16524
|
+
return `Field '${fieldName}' is reserved (fields starting with '_' are internal)`;
|
|
16525
|
+
}
|
|
16526
|
+
return null;
|
|
16527
|
+
}
|
|
16528
|
+
var ReservedFieldNameError = class extends Error {
|
|
16529
|
+
constructor(message) {
|
|
16530
|
+
super(message);
|
|
16531
|
+
this.name = "ReservedFieldNameError";
|
|
16532
|
+
}
|
|
16533
|
+
};
|
|
16534
|
+
|
|
16535
|
+
// src/models/tomlLoader.ts
|
|
15236
16536
|
var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
15237
16537
|
"string",
|
|
15238
16538
|
"number",
|
|
@@ -15379,6 +16679,7 @@ function parseRelationship(raw, context, strict) {
|
|
|
15379
16679
|
}
|
|
15380
16680
|
function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
15381
16681
|
const strict = options.strict !== false;
|
|
16682
|
+
const rejectReserved = options.rejectReservedFieldNames === true;
|
|
15382
16683
|
const parsed = (0, import_smol_toml.parse)(tomlString);
|
|
15383
16684
|
const models = parsed.models;
|
|
15384
16685
|
if (!models || typeof models !== "object") {
|
|
@@ -15395,11 +16696,14 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
15395
16696
|
const fields = {};
|
|
15396
16697
|
if (modelDef.fields) {
|
|
15397
16698
|
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
15398
|
-
fields
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
|
|
16699
|
+
const context = `[models.${modelName}.fields.${fieldName}]`;
|
|
16700
|
+
if (rejectReserved) {
|
|
16701
|
+
const reserved = checkReservedFieldName(fieldName);
|
|
16702
|
+
if (reserved) {
|
|
16703
|
+
throw new ReservedFieldNameError(`${context}: ${reserved}`);
|
|
16704
|
+
}
|
|
16705
|
+
}
|
|
16706
|
+
fields[fieldName] = parseFieldOptions(fieldDef, context, strict);
|
|
15403
16707
|
}
|
|
15404
16708
|
}
|
|
15405
16709
|
let relationships;
|
|
@@ -15446,6 +16750,18 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
15446
16750
|
}
|
|
15447
16751
|
return schemas;
|
|
15448
16752
|
}
|
|
16753
|
+
function findReservedFieldNameInToml(tomlString) {
|
|
16754
|
+
try {
|
|
16755
|
+
loadSchemaFromTomlString(tomlString, {
|
|
16756
|
+
strict: false,
|
|
16757
|
+
rejectReservedFieldNames: true
|
|
16758
|
+
});
|
|
16759
|
+
return null;
|
|
16760
|
+
} catch (error) {
|
|
16761
|
+
if (error instanceof ReservedFieldNameError) return error.message;
|
|
16762
|
+
return null;
|
|
16763
|
+
}
|
|
16764
|
+
}
|
|
15449
16765
|
|
|
15450
16766
|
// src/browser.ts
|
|
15451
16767
|
init_metaSync();
|
|
@@ -15542,6 +16858,49 @@ function writeCarriedOverlay(modelMap, entry) {
|
|
|
15542
16858
|
}
|
|
15543
16859
|
}
|
|
15544
16860
|
}
|
|
16861
|
+
function suppressSupersededKeys(carried, later) {
|
|
16862
|
+
if (later.length === 0 || carried.length === 0) return carried;
|
|
16863
|
+
const fields = /* @__PURE__ */ new Map();
|
|
16864
|
+
const revived = /* @__PURE__ */ new Set();
|
|
16865
|
+
for (const op of later) {
|
|
16866
|
+
const key = `${op.model} ${op.recordId}`;
|
|
16867
|
+
let owned = fields.get(key);
|
|
16868
|
+
if (!owned) fields.set(key, owned = /* @__PURE__ */ new Set());
|
|
16869
|
+
for (const field of op.fields) owned.add(field);
|
|
16870
|
+
if (op.op !== "delete") revived.add(key);
|
|
16871
|
+
}
|
|
16872
|
+
const kept = [];
|
|
16873
|
+
for (const { model, entry } of carried) {
|
|
16874
|
+
const key = `${model} ${entry.id}`;
|
|
16875
|
+
const owned = fields.get(key);
|
|
16876
|
+
if (owned === void 0) {
|
|
16877
|
+
kept.push({ model, entry });
|
|
16878
|
+
continue;
|
|
16879
|
+
}
|
|
16880
|
+
if (entry.deleted) {
|
|
16881
|
+
if (!revived.has(key)) kept.push({ model, entry });
|
|
16882
|
+
continue;
|
|
16883
|
+
}
|
|
16884
|
+
const survivingFields = Object.fromEntries(
|
|
16885
|
+
Object.entries(entry.fields).filter(([field]) => !owned.has(field))
|
|
16886
|
+
);
|
|
16887
|
+
const survivingSets = Object.fromEntries(
|
|
16888
|
+
Object.entries(entry.stringSets).filter(([field]) => !owned.has(field))
|
|
16889
|
+
);
|
|
16890
|
+
if (!entry.replace && Object.keys(survivingFields).length === 0 && Object.keys(survivingSets).length === 0) {
|
|
16891
|
+
continue;
|
|
16892
|
+
}
|
|
16893
|
+
kept.push({
|
|
16894
|
+
model,
|
|
16895
|
+
entry: {
|
|
16896
|
+
...entry,
|
|
16897
|
+
fields: survivingFields,
|
|
16898
|
+
stringSets: survivingSets
|
|
16899
|
+
}
|
|
16900
|
+
});
|
|
16901
|
+
}
|
|
16902
|
+
return kept;
|
|
16903
|
+
}
|
|
15545
16904
|
|
|
15546
16905
|
// src/models/format2EpochRotation.ts
|
|
15547
16906
|
var Y10 = __toESM(require("yjs"), 1);
|
|
@@ -15613,10 +16972,15 @@ async function writeDeferredReplay(host, ops, owedOverlay, resolveReplay) {
|
|
|
15613
16972
|
const live = host.overlayDoc();
|
|
15614
16973
|
if (!live) return 0;
|
|
15615
16974
|
const owed = resolveReplay(ops);
|
|
15616
|
-
const
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
16975
|
+
const held = new Set(ops.map((op) => op.seq));
|
|
16976
|
+
const later = host.pendingOps().filter((op) => !held.has(op.seq));
|
|
16977
|
+
const carried = suppressSupersededKeys(
|
|
16978
|
+
carryUnackedOverlay(
|
|
16979
|
+
owed.ops,
|
|
16980
|
+
(model, recordId) => owedOverlay.get(`${model} ${recordId}`) ?? readOverlayRecord(live.getMap(model), recordId),
|
|
16981
|
+
{ suppressDeletes: owed.suppressDeletes }
|
|
16982
|
+
),
|
|
16983
|
+
later
|
|
15620
16984
|
);
|
|
15621
16985
|
if (carried.length === 0) return 0;
|
|
15622
16986
|
live.transact(() => {
|
|
@@ -15640,18 +17004,22 @@ function planFastForward(options) {
|
|
|
15640
17004
|
if (target <= current) return { kind: "none", reason: "current" };
|
|
15641
17005
|
const byEpoch = /* @__PURE__ */ new Map();
|
|
15642
17006
|
for (const entry of options.sealed) byEpoch.set(entry.epoch, entry);
|
|
17007
|
+
const discontinuities = options.sealed.filter(
|
|
17008
|
+
(entry) => entry.baseDiscontinuity && entry.epoch >= current && entry.epoch < target
|
|
17009
|
+
).map((entry) => entry.epoch).sort((a, b) => a - b);
|
|
17010
|
+
const last = discontinuities.length > 0 ? discontinuities[0] : target - 1;
|
|
15643
17011
|
const steps = [];
|
|
15644
|
-
for (let epoch = current; epoch
|
|
17012
|
+
for (let epoch = current; epoch <= last; epoch++) {
|
|
15645
17013
|
const entry = byEpoch.get(epoch);
|
|
15646
17014
|
if (!entry) return { kind: "reload", reason: "gap", epoch };
|
|
15647
|
-
if (entry.baseDiscontinuity) {
|
|
15648
|
-
return { kind: "reload", reason: "discontinuity", epoch };
|
|
15649
|
-
}
|
|
15650
17015
|
if (entry.download === void 0 || entry.download === null) {
|
|
15651
17016
|
return { kind: "reload", reason: "unavailable", epoch };
|
|
15652
17017
|
}
|
|
15653
17018
|
steps.push({ epoch, download: entry.download });
|
|
15654
17019
|
}
|
|
17020
|
+
if (discontinuities.length > 0) {
|
|
17021
|
+
return { kind: "converge", from: current, discontinuities, steps };
|
|
17022
|
+
}
|
|
15655
17023
|
return { kind: "apply", from: current, to: target, steps };
|
|
15656
17024
|
}
|
|
15657
17025
|
async function runFastForward(host, options) {
|
|
@@ -15660,6 +17028,14 @@ async function runFastForward(host, options) {
|
|
|
15660
17028
|
if (plan.kind === "reload") {
|
|
15661
17029
|
return { outcome: "reload", reason: plan.reason, epoch: plan.epoch };
|
|
15662
17030
|
}
|
|
17031
|
+
if (plan.kind === "converge") {
|
|
17032
|
+
return {
|
|
17033
|
+
outcome: "converge",
|
|
17034
|
+
from: plan.from,
|
|
17035
|
+
discontinuities: plan.discontinuities,
|
|
17036
|
+
steps: plan.steps
|
|
17037
|
+
};
|
|
17038
|
+
}
|
|
15663
17039
|
let applied;
|
|
15664
17040
|
try {
|
|
15665
17041
|
applied = await host.hold(() => applyFastForward(plan, host));
|
|
@@ -15697,6 +17073,10 @@ async function applyFastForward(plan, host) {
|
|
|
15697
17073
|
}
|
|
15698
17074
|
|
|
15699
17075
|
// src/models/format2ColdStart.ts
|
|
17076
|
+
function isGranted(sealed, epoch) {
|
|
17077
|
+
const entry = sealed.find((each) => each.epoch === epoch);
|
|
17078
|
+
return !!entry && entry.download !== void 0 && entry.download !== null;
|
|
17079
|
+
}
|
|
15700
17080
|
function chainIsWhole(sealed, from, to) {
|
|
15701
17081
|
const byEpoch = /* @__PURE__ */ new Map();
|
|
15702
17082
|
for (const entry of sealed) byEpoch.set(entry.epoch, entry);
|
|
@@ -15722,6 +17102,15 @@ function planColdStart(input) {
|
|
|
15722
17102
|
if (first === 1 && chainIsWhole(sealed, 1, reported)) {
|
|
15723
17103
|
return { kind: "overlays", base: 1 };
|
|
15724
17104
|
}
|
|
17105
|
+
const start = Number.isInteger(base) && base > 0 ? base : 1;
|
|
17106
|
+
const discontinuities = sealed.filter(
|
|
17107
|
+
(entry) => entry.baseDiscontinuity && entry.epoch >= start && entry.epoch < reported
|
|
17108
|
+
).map((entry) => entry.epoch);
|
|
17109
|
+
if (discontinuities.length > 0 && // Whole and granted up to the first flagged epoch, and no further: past it
|
|
17110
|
+
// the base that is coming covers everything.
|
|
17111
|
+
chainIsWhole(sealed, start, discontinuities[0]) && isGranted(sealed, discontinuities[0])) {
|
|
17112
|
+
return { kind: "await-base", discontinuities };
|
|
17113
|
+
}
|
|
15725
17114
|
return { kind: "unavailable", reason: "no-base" };
|
|
15726
17115
|
}
|
|
15727
17116
|
async function runColdStart(host, plan, options = {}) {
|
|
@@ -15733,6 +17122,8 @@ async function runColdStart(host, plan, options = {}) {
|
|
|
15733
17122
|
return true;
|
|
15734
17123
|
case "unavailable":
|
|
15735
17124
|
return false;
|
|
17125
|
+
case "await-base":
|
|
17126
|
+
return false;
|
|
15736
17127
|
case "snapshot":
|
|
15737
17128
|
case "overlays": {
|
|
15738
17129
|
if (options.stale) await host.discard();
|
|
@@ -15753,11 +17144,69 @@ async function runColdStart(host, plan, options = {}) {
|
|
|
15753
17144
|
}
|
|
15754
17145
|
}
|
|
15755
17146
|
|
|
17147
|
+
// src/models/format2RangeReplacement.ts
|
|
17148
|
+
init_snapshotChunks();
|
|
17149
|
+
function planRangeReplacement(options) {
|
|
17150
|
+
const { manifest, touched } = options;
|
|
17151
|
+
const scope = options.localModels;
|
|
17152
|
+
const inScope = (model) => scope === null || scope.includes(model);
|
|
17153
|
+
const perModel = /* @__PURE__ */ new Map();
|
|
17154
|
+
for (const chunk of manifest.chunks) {
|
|
17155
|
+
if (!inScope(chunk.model)) continue;
|
|
17156
|
+
const list = perModel.get(chunk.model);
|
|
17157
|
+
if (list) list.push(chunk);
|
|
17158
|
+
else perModel.set(chunk.model, [chunk]);
|
|
17159
|
+
}
|
|
17160
|
+
const touchedPerModel = /* @__PURE__ */ new Map();
|
|
17161
|
+
for (const range of touched?.ranges ?? []) {
|
|
17162
|
+
const list = touchedPerModel.get(range.model);
|
|
17163
|
+
if (list) list.push(range);
|
|
17164
|
+
else touchedPerModel.set(range.model, [range]);
|
|
17165
|
+
}
|
|
17166
|
+
const fetch = [];
|
|
17167
|
+
const keep = [];
|
|
17168
|
+
const order = new Map(manifest.chunks.map((chunk, index) => [chunk, index]));
|
|
17169
|
+
for (const [model, chunks] of perModel) {
|
|
17170
|
+
const ranges = touchedPerModel.get(model) ?? [];
|
|
17171
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
17172
|
+
const chunk = chunks[index];
|
|
17173
|
+
const coverage = {
|
|
17174
|
+
after: index === 0 ? null : chunks[index - 1].lastId,
|
|
17175
|
+
through: index === chunks.length - 1 ? null : chunk.lastId
|
|
17176
|
+
};
|
|
17177
|
+
if (touched === null || ranges.some((range) => meets(coverage, range))) {
|
|
17178
|
+
fetch.push({ chunk, coverage });
|
|
17179
|
+
} else {
|
|
17180
|
+
keep.push(chunk);
|
|
17181
|
+
}
|
|
17182
|
+
}
|
|
17183
|
+
}
|
|
17184
|
+
fetch.sort((a, b) => order.get(a.chunk) - order.get(b.chunk));
|
|
17185
|
+
keep.sort((a, b) => order.get(a) - order.get(b));
|
|
17186
|
+
const absent = /* @__PURE__ */ new Set();
|
|
17187
|
+
for (const model of scope ?? []) absent.add(model);
|
|
17188
|
+
for (const model of touchedPerModel.keys()) {
|
|
17189
|
+
if (inScope(model)) absent.add(model);
|
|
17190
|
+
}
|
|
17191
|
+
const pruneModels = [...absent].filter((model) => !perModel.has(model)).sort();
|
|
17192
|
+
return { fetch, keep, pruneModels, full: touched === null };
|
|
17193
|
+
}
|
|
17194
|
+
function meets(coverage, range) {
|
|
17195
|
+
if (coverage.through !== null && compareRecordIds(range.firstId, coverage.through) > 0) {
|
|
17196
|
+
return false;
|
|
17197
|
+
}
|
|
17198
|
+
if (coverage.after !== null && compareRecordIds(range.lastId, coverage.after) <= 0) {
|
|
17199
|
+
return false;
|
|
17200
|
+
}
|
|
17201
|
+
return true;
|
|
17202
|
+
}
|
|
17203
|
+
|
|
15756
17204
|
// src/models/format2OfflineReplay.ts
|
|
15757
17205
|
var Y12 = __toESM(require("yjs"), 1);
|
|
15758
17206
|
init_largeDocuments();
|
|
15759
17207
|
init_format2Observer();
|
|
15760
17208
|
var recordKey = (model, recordId) => `${model}\0${recordId}`;
|
|
17209
|
+
var offlineReplayRecordKey = recordKey;
|
|
15761
17210
|
var OfflineConflictLedger = class {
|
|
15762
17211
|
/** Latest touch per record per epoch, newest epoch last. */
|
|
15763
17212
|
touches = /* @__PURE__ */ new Map();
|
|
@@ -15900,6 +17349,40 @@ function resolveOfflineReplay(options) {
|
|
|
15900
17349
|
};
|
|
15901
17350
|
for (const op of [...options.ops].sort((a, b) => a.seq - b.seq)) {
|
|
15902
17351
|
const base = { model: op.model, recordId: op.recordId, op: op.op, ts: op.ts };
|
|
17352
|
+
const ingest = options.ingest;
|
|
17353
|
+
if (ingest && op.baseEpoch <= ingest.through) {
|
|
17354
|
+
const presence = ingest.records.get(recordKey(op.model, op.recordId));
|
|
17355
|
+
const verdict = presence ?? (ingest.scope === "all" ? "present" : void 0);
|
|
17356
|
+
if (verdict === "present") {
|
|
17357
|
+
ops.push(op);
|
|
17358
|
+
notices.push({
|
|
17359
|
+
...base,
|
|
17360
|
+
outcome: "kept-ambiguous",
|
|
17361
|
+
reason: "bulkIngest",
|
|
17362
|
+
epoch: ingest.through
|
|
17363
|
+
});
|
|
17364
|
+
continue;
|
|
17365
|
+
}
|
|
17366
|
+
if (verdict === "absent") {
|
|
17367
|
+
if (op.op === "create") {
|
|
17368
|
+
ops.push(op);
|
|
17369
|
+
notices.push({
|
|
17370
|
+
...base,
|
|
17371
|
+
outcome: "kept-ambiguous",
|
|
17372
|
+
reason: "bulkIngest",
|
|
17373
|
+
epoch: ingest.through
|
|
17374
|
+
});
|
|
17375
|
+
continue;
|
|
17376
|
+
}
|
|
17377
|
+
notices.push({
|
|
17378
|
+
...base,
|
|
17379
|
+
outcome: "dropped",
|
|
17380
|
+
reason: "bulkIngest",
|
|
17381
|
+
epoch: ingest.through
|
|
17382
|
+
});
|
|
17383
|
+
continue;
|
|
17384
|
+
}
|
|
17385
|
+
}
|
|
15903
17386
|
if (!ledger.covers(op.baseEpoch, currentEpoch)) {
|
|
15904
17387
|
ops.push(op);
|
|
15905
17388
|
notices.push({
|
|
@@ -16021,8 +17504,10 @@ function resolveOfflineReplay(options) {
|
|
|
16021
17504
|
|
|
16022
17505
|
// src/browser.ts
|
|
16023
17506
|
init_format2OfflineWindow();
|
|
17507
|
+
init_snapshotChunks();
|
|
16024
17508
|
|
|
16025
17509
|
// src/utils/snapshotPartition.ts
|
|
17510
|
+
init_snapshotChunks();
|
|
16026
17511
|
var DEFAULT_SNAPSHOT_PARTITION = {
|
|
16027
17512
|
minBytes: 6 * 1024 * 1024,
|
|
16028
17513
|
maxBytes: 12 * 1024 * 1024,
|
|
@@ -16188,6 +17673,19 @@ var SnapshotPartitioner = class {
|
|
|
16188
17673
|
get pendingBytes() {
|
|
16189
17674
|
return this.bufferBytes;
|
|
16190
17675
|
}
|
|
17676
|
+
/**
|
|
17677
|
+
* Whether the buffer is a below-minimum range being carried forward (#3433).
|
|
17678
|
+
*
|
|
17679
|
+
* An incremental build walks only the ranges that changed, so it has to know
|
|
17680
|
+
* when a range it re-cut closed short and merged into its SUCCESSOR: that
|
|
17681
|
+
* successor is then part of the walk too, and cannot be reused by reference.
|
|
17682
|
+
* Reading it off the partitioner rather than inferring it from the emitted
|
|
17683
|
+
* chunks keeps one answer to "did this boundary move", in the object that
|
|
17684
|
+
* decided it.
|
|
17685
|
+
*/
|
|
17686
|
+
get carriedTail() {
|
|
17687
|
+
return this.merging;
|
|
17688
|
+
}
|
|
16191
17689
|
take(count) {
|
|
16192
17690
|
const taken = count === void 0 ? this.buffer : this.buffer.slice(0, count);
|
|
16193
17691
|
const rest = count === void 0 ? [] : this.buffer.slice(count);
|