js-bao 0.7.0-alpha.5 → 0.7.0-alpha.6
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 +1257 -409
- package/dist/browser.d.cts +732 -8
- package/dist/browser.d.ts +732 -8
- package/dist/browser.js +1254 -406
- package/dist/client.cjs +40 -15
- package/dist/client.d.cts +237 -0
- package/dist/client.d.ts +237 -0
- package/dist/client.js +37 -12
- package/dist/cloudflare-do.cjs +5419 -4951
- package/dist/cloudflare-do.d.cts +550 -1
- package/dist/cloudflare-do.d.ts +550 -1
- package/dist/cloudflare-do.js +5346 -4878
- package/dist/cloudflare.cjs +132 -77
- package/dist/cloudflare.d.cts +318 -36
- package/dist/cloudflare.d.ts +318 -36
- package/dist/cloudflare.js +129 -74
- package/dist/codegen-v2.cjs +78 -53
- package/dist/codegen.cjs +1 -1
- package/dist/format2-worker.js +232 -1
- package/dist/index.cjs +1467 -620
- package/dist/index.d.cts +896 -43
- package/dist/index.d.ts +896 -43
- package/dist/index.js +1454 -617
- package/dist/node.cjs +1475 -628
- package/dist/node.d.cts +875 -8
- package/dist/node.d.ts +875 -8
- package/dist/node.js +1462 -625
- 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");
|
|
@@ -1779,6 +1817,335 @@ var init_format2OfflineWindow = __esm({
|
|
|
1779
1817
|
}
|
|
1780
1818
|
});
|
|
1781
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.`
|
|
2133
|
+
);
|
|
2134
|
+
this.manifestVersion = manifestVersion;
|
|
2135
|
+
this.supportedVersion = supportedVersion;
|
|
2136
|
+
this.name = "SnapshotManifestUnsupportedError";
|
|
2137
|
+
}
|
|
2138
|
+
code = "SNAPSHOT_MANIFEST_UNSUPPORTED";
|
|
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;
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
|
|
1782
2149
|
// src/utils/overlaySql.ts
|
|
1783
2150
|
function statementsFor(tables) {
|
|
1784
2151
|
const cacheKey = `${tables.records}\0${tables.stringSetIndex}`;
|
|
@@ -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);
|
|
@@ -3545,26 +4132,142 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
3545
4132
|
(ops) => ops.commitLocalWrite(model, mutation, pending)
|
|
3546
4133
|
);
|
|
3547
4134
|
}
|
|
3548
|
-
async applyChunk(model, entries, buildId, ordinal) {
|
|
3549
|
-
const reply = await this.run(
|
|
3550
|
-
"applyChunk",
|
|
3551
|
-
() => this.host.dispatch({
|
|
3552
|
-
kind: "applyChunk",
|
|
3553
|
-
model,
|
|
3554
|
-
entries: [...entries],
|
|
3555
|
-
buildId,
|
|
3556
|
-
ordinal
|
|
3557
|
-
})
|
|
4135
|
+
async applyChunk(model, entries, buildId, ordinal) {
|
|
4136
|
+
const reply = await this.run(
|
|
4137
|
+
"applyChunk",
|
|
4138
|
+
() => this.host.dispatch({
|
|
4139
|
+
kind: "applyChunk",
|
|
4140
|
+
model,
|
|
4141
|
+
entries: [...entries],
|
|
4142
|
+
buildId,
|
|
4143
|
+
ordinal
|
|
4144
|
+
})
|
|
4145
|
+
);
|
|
4146
|
+
for (const entry of entries) {
|
|
4147
|
+
if (this.cache.peek(model, entry.id)) {
|
|
4148
|
+
await this.refreshRows(model, [entry.id]);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
return reply.applied;
|
|
4152
|
+
}
|
|
4153
|
+
completedChunks(buildId) {
|
|
4154
|
+
return this.host.dispatch({ kind: "completedChunks", buildId });
|
|
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 })
|
|
3558
4242
|
);
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
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
|
+
});
|
|
3563
4248
|
}
|
|
3564
|
-
return reply.applied;
|
|
3565
4249
|
}
|
|
3566
|
-
|
|
3567
|
-
|
|
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 };
|
|
3568
4271
|
}
|
|
3569
4272
|
async discardMergedView() {
|
|
3570
4273
|
await this.run(
|
|
@@ -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":
|
|
@@ -10265,6 +10997,7 @@ __export(browser_exports, {
|
|
|
10265
10997
|
Format2RecordStoreAsync: () => Format2RecordStoreAsync,
|
|
10266
10998
|
Format2RowCache: () => Format2RowCache,
|
|
10267
10999
|
Format2StorageCapabilityError: () => Format2StorageCapabilityError,
|
|
11000
|
+
INGEST_CHUNK_LIMITS: () => INGEST_CHUNK_LIMITS,
|
|
10268
11001
|
InProcessFormat2StoreHost: () => InProcessFormat2StoreHost,
|
|
10269
11002
|
LogLevel: () => LogLevel,
|
|
10270
11003
|
Logger: () => Logger,
|
|
@@ -10308,6 +11041,7 @@ __export(browser_exports, {
|
|
|
10308
11041
|
encodeOverlayMutation: () => encodeOverlayMutation,
|
|
10309
11042
|
estimateMaterializedBytes: () => estimateMaterializedBytes,
|
|
10310
11043
|
features: () => features,
|
|
11044
|
+
foldFormat2Overlay: () => foldFormat2Overlay,
|
|
10311
11045
|
followEpoch: () => followEpoch,
|
|
10312
11046
|
format2ClientDDL: () => format2ClientDDL,
|
|
10313
11047
|
format2EpochDocName: () => format2EpochDocName,
|
|
@@ -10330,6 +11064,7 @@ __export(browser_exports, {
|
|
|
10330
11064
|
loadSchemaFromTomlString: () => loadSchemaFromTomlString,
|
|
10331
11065
|
materializeOverlayRecord: () => materializeOverlayRecord,
|
|
10332
11066
|
observeFormat2Overlay: () => observeFormat2Overlay,
|
|
11067
|
+
offlineReplayRecordKey: () => offlineReplayRecordKey,
|
|
10333
11068
|
offlineWindowStatus: () => offlineWindowStatus,
|
|
10334
11069
|
overlayFieldKey: () => overlayFieldKey,
|
|
10335
11070
|
overlayMarkerKey: () => overlayMarkerKey,
|
|
@@ -10338,6 +11073,7 @@ __export(browser_exports, {
|
|
|
10338
11073
|
parseSnapshotChunkPath: () => parseSnapshotChunkPath,
|
|
10339
11074
|
planColdStart: () => planColdStart,
|
|
10340
11075
|
planFastForward: () => planFastForward,
|
|
11076
|
+
planRangeReplacement: () => planRangeReplacement,
|
|
10341
11077
|
planSnapshotHydration: () => planSnapshotHydration,
|
|
10342
11078
|
probeStorageCapability: () => probeStorageCapability,
|
|
10343
11079
|
projectOverlayEntry: () => projectOverlayEntry,
|
|
@@ -10353,6 +11089,8 @@ __export(browser_exports, {
|
|
|
10353
11089
|
schemaToToml: () => schemaToToml,
|
|
10354
11090
|
serveFormat2StoreHost: () => serveFormat2StoreHost,
|
|
10355
11091
|
snapshotChunkPath: () => snapshotChunkPath,
|
|
11092
|
+
statePendingOps: () => statePendingOps,
|
|
11093
|
+
suppressSupersededKeys: () => suppressSupersededKeys,
|
|
10356
11094
|
syncInferredMeta: () => syncInferredMeta,
|
|
10357
11095
|
syncModelMeta: () => syncModelMeta,
|
|
10358
11096
|
validateSnapshotManifest: () => validateSnapshotManifest,
|
|
@@ -11046,307 +11784,55 @@ var SqljsEngine = class _SqljsEngine extends SqliteQueryEngineBase {
|
|
|
11046
11784
|
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
11047
11785
|
return rows;
|
|
11048
11786
|
} 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
|
-
);
|
|
11232
|
-
}
|
|
11233
|
-
}
|
|
11234
|
-
function snapshotStringSetFields(manifest, model) {
|
|
11235
|
-
const declared = manifest.schema?.[model]?.stringSetFields;
|
|
11236
|
-
return new Set(Array.isArray(declared) ? declared.map(String) : []);
|
|
11237
|
-
}
|
|
11238
|
-
function snapshotRowAsOverlayEntry(id, data, stringSetFields) {
|
|
11239
|
-
const fields = {};
|
|
11240
|
-
const stringSets = {};
|
|
11241
|
-
for (const [field, value] of Object.entries(data)) {
|
|
11242
|
-
if (field === "id") continue;
|
|
11243
|
-
if (stringSetFields.has(field) && Array.isArray(value)) {
|
|
11244
|
-
const members = {};
|
|
11245
|
-
for (const member of value) members[String(member)] = true;
|
|
11246
|
-
stringSets[field] = members;
|
|
11247
|
-
continue;
|
|
11248
|
-
}
|
|
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
|
-
}
|
|
11270
|
-
const out = new Uint8Array(total);
|
|
11271
|
-
let at = 0;
|
|
11272
|
-
for (const part of parts) {
|
|
11273
|
-
out.set(part, at);
|
|
11274
|
-
at += part.byteLength;
|
|
11275
|
-
}
|
|
11276
|
-
return out;
|
|
11277
|
-
}
|
|
11278
|
-
async function sha256Hex(bytes) {
|
|
11279
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
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}`
|
|
11296
|
-
);
|
|
11297
|
-
}
|
|
11787
|
+
stmt.free();
|
|
11788
|
+
}
|
|
11789
|
+
},
|
|
11790
|
+
transaction(fn) {
|
|
11791
|
+
if (depth > 0) return fn();
|
|
11792
|
+
depth++;
|
|
11793
|
+
db.run("BEGIN TRANSACTION");
|
|
11794
|
+
try {
|
|
11795
|
+
const result = fn();
|
|
11796
|
+
db.run("COMMIT");
|
|
11797
|
+
return result;
|
|
11798
|
+
} catch (error) {
|
|
11799
|
+
db.run("ROLLBACK");
|
|
11800
|
+
throw error;
|
|
11801
|
+
} finally {
|
|
11802
|
+
depth--;
|
|
11803
|
+
}
|
|
11804
|
+
}
|
|
11805
|
+
};
|
|
11298
11806
|
}
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
entry?.key ?? descriptor.model,
|
|
11307
|
-
"a line carries no id/data separator"
|
|
11308
|
-
);
|
|
11807
|
+
async close() {
|
|
11808
|
+
await this.readyPromise;
|
|
11809
|
+
if (this.db) {
|
|
11810
|
+
this.db.close();
|
|
11811
|
+
this.db = null;
|
|
11812
|
+
this.sync = null;
|
|
11813
|
+
_SqljsEngine.SQL = null;
|
|
11309
11814
|
}
|
|
11310
|
-
rows.push({
|
|
11311
|
-
_type: descriptor.model,
|
|
11312
|
-
_id: line.slice(0, tab),
|
|
11313
|
-
_data: line.slice(tab + 1)
|
|
11314
|
-
});
|
|
11315
11815
|
}
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
entry.key,
|
|
11319
|
-
`expected ${entry.rows} rows, got ${rows.length}`
|
|
11320
|
-
);
|
|
11816
|
+
async getTableSchema(_tableName) {
|
|
11817
|
+
return void 0;
|
|
11321
11818
|
}
|
|
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
|
-
);
|
|
11819
|
+
async destroy() {
|
|
11820
|
+
if (this.db) {
|
|
11821
|
+
this.db.close();
|
|
11822
|
+
Logger.info("[SqljsEngine] Database closed.");
|
|
11344
11823
|
}
|
|
11345
11824
|
}
|
|
11346
|
-
|
|
11347
|
-
|
|
11825
|
+
getLastErrorMessage() {
|
|
11826
|
+
return void 0;
|
|
11827
|
+
}
|
|
11828
|
+
};
|
|
11829
|
+
|
|
11830
|
+
// src/engines/opfs/OpfsEngine.ts
|
|
11831
|
+
init_logger();
|
|
11832
|
+
init_sql();
|
|
11348
11833
|
|
|
11349
11834
|
// src/models/format2StorageCapability.ts
|
|
11835
|
+
init_snapshotChunks();
|
|
11350
11836
|
var SNAPSHOT_STORAGE_EXPANSION = 2;
|
|
11351
11837
|
var SNAPSHOT_COMPRESSED_EXPANSION = 8;
|
|
11352
11838
|
function estimateMaterializedBytes(chunk) {
|
|
@@ -13558,10 +14044,19 @@ function sameOverlayValue(a, b) {
|
|
|
13558
14044
|
if (a === null || a === void 0) return b === null || b === void 0;
|
|
13559
14045
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
13560
14046
|
}
|
|
13561
|
-
function restorePendingOverlay(doc, store, verdicts) {
|
|
13562
|
-
const result = {
|
|
14047
|
+
function restorePendingOverlay(doc, store, verdicts, options = {}) {
|
|
14048
|
+
const result = {
|
|
14049
|
+
restored: [],
|
|
14050
|
+
unreproducible: [],
|
|
14051
|
+
deferred: []
|
|
14052
|
+
};
|
|
14053
|
+
const defer = options.deferAtOrBelowEpoch ?? 0;
|
|
13563
14054
|
const missing = [];
|
|
13564
14055
|
for (const op of store.pendingOps()) {
|
|
14056
|
+
if (defer > 0 && op.baseEpoch > 0 && op.baseEpoch <= defer) {
|
|
14057
|
+
result.deferred.push(op.seq);
|
|
14058
|
+
continue;
|
|
14059
|
+
}
|
|
13565
14060
|
const verdict = verdicts?.get(op.seq);
|
|
13566
14061
|
if (verdict === "carried" || verdict === "superseded") continue;
|
|
13567
14062
|
if (verdict === void 0 && overlayCarries(doc, op)) continue;
|
|
@@ -13586,8 +14081,27 @@ function restorePendingOverlay(doc, store, verdicts) {
|
|
|
13586
14081
|
}, FORMAT2_LOCAL_WRITE_ORIGIN);
|
|
13587
14082
|
return result;
|
|
13588
14083
|
}
|
|
14084
|
+
function statePendingOps(doc, ops, options = {}) {
|
|
14085
|
+
const suppressed = new Set(
|
|
14086
|
+
(options.suppressDeletes ?? []).map(
|
|
14087
|
+
(entry) => `${entry.model} ${entry.recordId}`
|
|
14088
|
+
)
|
|
14089
|
+
);
|
|
14090
|
+
const stated = [...ops].sort((a, b) => a.seq - b.seq).filter(
|
|
14091
|
+
(op) => op.mutation !== null && !(op.op === "delete" && suppressed.has(`${op.model} ${op.recordId}`))
|
|
14092
|
+
);
|
|
14093
|
+
if (stated.length === 0) return 0;
|
|
14094
|
+
doc.transact(() => {
|
|
14095
|
+
for (const op of stated) {
|
|
14096
|
+
applyOverlayMutation(doc.getMap(op.model), op.mutation);
|
|
14097
|
+
}
|
|
14098
|
+
});
|
|
14099
|
+
return stated.length;
|
|
14100
|
+
}
|
|
13589
14101
|
|
|
13590
14102
|
// src/models/format2SnapshotLoader.ts
|
|
14103
|
+
init_snapshotChunks();
|
|
14104
|
+
var PRUNE_PAGE_ROWS = 500;
|
|
13591
14105
|
var SnapshotLoadIncompleteError = class extends Error {
|
|
13592
14106
|
constructor(buildId, missing, passes) {
|
|
13593
14107
|
super(
|
|
@@ -13629,6 +14143,7 @@ async function loadFormat2Snapshot(options) {
|
|
|
13629
14143
|
const { store, manifest } = options;
|
|
13630
14144
|
const retries = options.retries ?? 1;
|
|
13631
14145
|
validateSnapshotManifest(manifest);
|
|
14146
|
+
await store.beginBase(manifest.buildId, manifest.epoch);
|
|
13632
14147
|
const already = new Set(await store.completedChunks(manifest.buildId));
|
|
13633
14148
|
const ordinals = new Map(
|
|
13634
14149
|
manifest.chunks.map((chunk, index) => [chunk, resolveChunkOrdinal(chunk, index)])
|
|
@@ -13636,7 +14151,20 @@ async function loadFormat2Snapshot(options) {
|
|
|
13636
14151
|
await store.setHydrationScope(options.models ? [...options.models] : null);
|
|
13637
14152
|
const cap = options.models;
|
|
13638
14153
|
const wanted = (model) => cap === void 0 || cap.includes(model);
|
|
13639
|
-
const
|
|
14154
|
+
const plan = options.plan;
|
|
14155
|
+
const coverages = new Map(
|
|
14156
|
+
plan?.fetch.map((entry) => [entry.chunk, entry.coverage]) ?? []
|
|
14157
|
+
);
|
|
14158
|
+
const keptChunks = new Set(plan?.keep ?? []);
|
|
14159
|
+
const selected = (plan ? [...plan.fetch.map((entry) => entry.chunk), ...plan.keep].sort(
|
|
14160
|
+
(a, b) => manifest.chunks.indexOf(a) - manifest.chunks.indexOf(b)
|
|
14161
|
+
) : manifest.chunks).filter((chunk) => wanted(chunk.model));
|
|
14162
|
+
const announcedTouched = /* @__PURE__ */ new Set();
|
|
14163
|
+
const announceTouched = async (model) => {
|
|
14164
|
+
if (announcedTouched.has(model)) return;
|
|
14165
|
+
announcedTouched.add(model);
|
|
14166
|
+
await options.onModelTouched?.(model);
|
|
14167
|
+
};
|
|
13640
14168
|
const totalChunks = selected.length;
|
|
13641
14169
|
const totalRows = selected.reduce((total, chunk) => total + chunk.rows, 0);
|
|
13642
14170
|
const models = [];
|
|
@@ -13657,6 +14185,14 @@ async function loadFormat2Snapshot(options) {
|
|
|
13657
14185
|
);
|
|
13658
14186
|
if (!models.includes(chunk.model)) models.push(chunk.model);
|
|
13659
14187
|
}
|
|
14188
|
+
for (const model of plan?.pruneModels ?? []) {
|
|
14189
|
+
await announceTouched(model);
|
|
14190
|
+
for (; ; ) {
|
|
14191
|
+
const page = await store.deleteRange(model, null, null, PRUNE_PAGE_ROWS);
|
|
14192
|
+
if (page.done) break;
|
|
14193
|
+
}
|
|
14194
|
+
await options.onModelReady?.(model);
|
|
14195
|
+
}
|
|
13660
14196
|
const applyOne = async (chunk, outstanding) => {
|
|
13661
14197
|
let decoded = null;
|
|
13662
14198
|
let lastError = null;
|
|
@@ -13673,14 +14209,27 @@ async function loadFormat2Snapshot(options) {
|
|
|
13673
14209
|
}
|
|
13674
14210
|
if (!decoded) throw new SnapshotLoadIntegrityError(chunk, lastError);
|
|
13675
14211
|
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)
|
|
14212
|
+
const coverage = coverages.get(chunk);
|
|
14213
|
+
const entries = decoded.map(
|
|
14214
|
+
(row) => rowAsEntry(row._id, JSON.parse(row._data), stringSetFields)
|
|
13683
14215
|
);
|
|
14216
|
+
if (coverage) {
|
|
14217
|
+
await announceTouched(chunk.model);
|
|
14218
|
+
await store.replaceChunk(
|
|
14219
|
+
chunk.model,
|
|
14220
|
+
coverage,
|
|
14221
|
+
entries,
|
|
14222
|
+
manifest.buildId,
|
|
14223
|
+
ordinals.get(chunk)
|
|
14224
|
+
);
|
|
14225
|
+
} else {
|
|
14226
|
+
await store.applyChunk(
|
|
14227
|
+
chunk.model,
|
|
14228
|
+
entries,
|
|
14229
|
+
manifest.buildId,
|
|
14230
|
+
ordinals.get(chunk)
|
|
14231
|
+
);
|
|
14232
|
+
}
|
|
13684
14233
|
chunks++;
|
|
13685
14234
|
rows += decoded.length;
|
|
13686
14235
|
options.onProgress?.({
|
|
@@ -13692,6 +14241,7 @@ async function loadFormat2Snapshot(options) {
|
|
|
13692
14241
|
});
|
|
13693
14242
|
if (outstanding === 0) await options.onModelReady?.(chunk.model);
|
|
13694
14243
|
};
|
|
14244
|
+
let reused = 0;
|
|
13695
14245
|
for (const chunk of selected) {
|
|
13696
14246
|
const outstanding = (remainingPerModel.get(chunk.model) ?? 1) - 1;
|
|
13697
14247
|
remainingPerModel.set(chunk.model, outstanding);
|
|
@@ -13702,6 +14252,14 @@ async function loadFormat2Snapshot(options) {
|
|
|
13702
14252
|
if (outstanding === 0) await options.onModelReady?.(chunk.model);
|
|
13703
14253
|
continue;
|
|
13704
14254
|
}
|
|
14255
|
+
if (keptChunks.has(chunk)) {
|
|
14256
|
+
await store.applyChunk(chunk.model, [], manifest.buildId, ordinals.get(chunk));
|
|
14257
|
+
reused++;
|
|
14258
|
+
chunks++;
|
|
14259
|
+
rows += chunk.rows;
|
|
14260
|
+
if (outstanding === 0) await options.onModelReady?.(chunk.model);
|
|
14261
|
+
continue;
|
|
14262
|
+
}
|
|
13705
14263
|
await applyOne(chunk, outstanding);
|
|
13706
14264
|
}
|
|
13707
14265
|
const repaired = /* @__PURE__ */ new Set();
|
|
@@ -13723,6 +14281,7 @@ async function loadFormat2Snapshot(options) {
|
|
|
13723
14281
|
repaired.add(chunk.model);
|
|
13724
14282
|
}
|
|
13725
14283
|
}
|
|
14284
|
+
await store.completeBase(manifest.buildId);
|
|
13726
14285
|
return {
|
|
13727
14286
|
rows,
|
|
13728
14287
|
chunks,
|
|
@@ -13730,7 +14289,8 @@ async function loadFormat2Snapshot(options) {
|
|
|
13730
14289
|
refetched,
|
|
13731
14290
|
models,
|
|
13732
14291
|
skipped,
|
|
13733
|
-
repaired: [...repaired]
|
|
14292
|
+
repaired: [...repaired],
|
|
14293
|
+
reused
|
|
13734
14294
|
};
|
|
13735
14295
|
}
|
|
13736
14296
|
|
|
@@ -13848,7 +14408,20 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
13848
14408
|
await store.whenBatchesSettled();
|
|
13849
14409
|
if (options.restorePending !== false) {
|
|
13850
14410
|
const verdicts = await this.adoptOrphanedWrites(docId, yDoc, store);
|
|
13851
|
-
const
|
|
14411
|
+
const crossed = store.discontinuityEpochs();
|
|
14412
|
+
const boundary = crossed.length > 0 ? crossed[crossed.length - 1] : 0;
|
|
14413
|
+
const deferAtOrBelowEpoch = boundary > 0 && store.epoch() > boundary ? boundary : 0;
|
|
14414
|
+
const restored = restorePendingOverlay(
|
|
14415
|
+
yDoc,
|
|
14416
|
+
store,
|
|
14417
|
+
verdicts,
|
|
14418
|
+
deferAtOrBelowEpoch > 0 ? { deferAtOrBelowEpoch } : {}
|
|
14419
|
+
);
|
|
14420
|
+
if (restored.deferred.length > 0) {
|
|
14421
|
+
Logger.warn(
|
|
14422
|
+
`[DocumentManager] ${docId} is holding ${restored.deferred.length} unacknowledged write(s) off its overlay until it has converged on the bulk load at epoch ${deferAtOrBelowEpoch}.`
|
|
14423
|
+
);
|
|
14424
|
+
}
|
|
13852
14425
|
const adoptedWasRestored = verdicts !== void 0 && restored.restored.some((seq) => verdicts.has(seq));
|
|
13853
14426
|
if (adoptedWasRestored) {
|
|
13854
14427
|
foldFormat2Overlay(
|
|
@@ -14044,27 +14617,94 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
14044
14617
|
);
|
|
14045
14618
|
}
|
|
14046
14619
|
const hydrated = /* @__PURE__ */ new Set();
|
|
14620
|
+
const replacement = callbacks.plan;
|
|
14621
|
+
const touchedModels = /* @__PURE__ */ new Set();
|
|
14047
14622
|
const result = await loadFormat2Snapshot({
|
|
14048
14623
|
...plan.kind === "capped" ? { models: plan.models } : {},
|
|
14049
14624
|
store,
|
|
14050
14625
|
manifest,
|
|
14051
14626
|
fetchChunk,
|
|
14627
|
+
...replacement ? { plan: replacement } : {},
|
|
14052
14628
|
onProgress: callbacks.onProgress,
|
|
14629
|
+
onModelTouched: async (model) => {
|
|
14630
|
+
touchedModels.add(model);
|
|
14631
|
+
await store.forgetQueryProjection(model);
|
|
14632
|
+
await this.clearQueryRows(docId, store, model);
|
|
14633
|
+
await callbacks.onModelTouched?.(model);
|
|
14634
|
+
},
|
|
14053
14635
|
onModelReady: async (model) => {
|
|
14054
|
-
|
|
14055
|
-
|
|
14636
|
+
const owed = replacement !== void 0 && !touchedModels.has(model) && !await store.queryProjectionIsCurrent(model);
|
|
14637
|
+
if (owed) {
|
|
14638
|
+
await this.clearQueryRows(docId, store, model);
|
|
14639
|
+
}
|
|
14640
|
+
if (!replacement || touchedModels.has(model) || owed) {
|
|
14641
|
+
await this.hydrateQueryTables(docId, store, { only: model });
|
|
14642
|
+
await this.whenProjectionSettled();
|
|
14643
|
+
}
|
|
14056
14644
|
hydrated.add(model);
|
|
14057
14645
|
await callbacks.onModelReady?.(model);
|
|
14058
14646
|
}
|
|
14059
14647
|
});
|
|
14060
|
-
|
|
14061
|
-
|
|
14648
|
+
if (!replacement) {
|
|
14649
|
+
await this.hydrateQueryTables(docId, store, { except: hydrated });
|
|
14650
|
+
await this.whenProjectionSettled();
|
|
14651
|
+
}
|
|
14062
14652
|
for (const model of result.repaired) {
|
|
14063
14653
|
await this.hydrateQueryTables(docId, store, { only: model });
|
|
14064
14654
|
await this.whenProjectionSettled();
|
|
14065
14655
|
}
|
|
14066
14656
|
return result;
|
|
14067
14657
|
}
|
|
14658
|
+
/**
|
|
14659
|
+
* The models this client holds rows of for `docId` (#3435).
|
|
14660
|
+
*
|
|
14661
|
+
* The convergence planner needs an inventory, not a cap: a model the new
|
|
14662
|
+
* base has no chunk of is pruned whole, and `null` — "the whole document" —
|
|
14663
|
+
* cannot say which models those are. A capped device answers with its
|
|
14664
|
+
* hydration scope; an uncapped one answers with the models this instance
|
|
14665
|
+
* registered, which is every model whose rows could be here.
|
|
14666
|
+
*/
|
|
14667
|
+
localModels(docId) {
|
|
14668
|
+
const store = this.ormContext.getFormat2StoreAsync(docId);
|
|
14669
|
+
if (!store) return null;
|
|
14670
|
+
const scope = store.hydrationScope();
|
|
14671
|
+
if (scope !== null) return [...scope];
|
|
14672
|
+
const registered = [...this.modelRegistry.getActiveModels().keys()];
|
|
14673
|
+
return registered.length > 0 ? registered : null;
|
|
14674
|
+
}
|
|
14675
|
+
/**
|
|
14676
|
+
* Whether each of these records is in the merged view (#3435, behavior 21).
|
|
14677
|
+
*
|
|
14678
|
+
* What a convergence classifies its owed writes against. Read in one pass
|
|
14679
|
+
* per model, and only AFTER the ranges have been replaced and the overlays
|
|
14680
|
+
* refolded: before that, "absent" says nothing about what the bulk load did.
|
|
14681
|
+
*
|
|
14682
|
+
* `null` for a record of a model this device does not hold — no fact is
|
|
14683
|
+
* better than a wrong one, and the caller decides what a missing verdict
|
|
14684
|
+
* means.
|
|
14685
|
+
*/
|
|
14686
|
+
async recordPresence(docId, records) {
|
|
14687
|
+
const store = this.ormContext.getFormat2StoreAsync(docId);
|
|
14688
|
+
if (!store) return records.map(() => null);
|
|
14689
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
14690
|
+
records.forEach((record, index) => {
|
|
14691
|
+
const list = byModel.get(record.model);
|
|
14692
|
+
if (list) list.push(index);
|
|
14693
|
+
else byModel.set(record.model, [index]);
|
|
14694
|
+
});
|
|
14695
|
+
const answers = records.map(() => null);
|
|
14696
|
+
for (const [model, indexes] of byModel) {
|
|
14697
|
+
if (!store.isHydrated(model)) continue;
|
|
14698
|
+
const rows = await store.readMany(
|
|
14699
|
+
model,
|
|
14700
|
+
indexes.map((index) => records[index].id)
|
|
14701
|
+
);
|
|
14702
|
+
indexes.forEach((index, position) => {
|
|
14703
|
+
answers[index] = rows[position] !== null;
|
|
14704
|
+
});
|
|
14705
|
+
}
|
|
14706
|
+
return answers;
|
|
14707
|
+
}
|
|
14068
14708
|
/**
|
|
14069
14709
|
* Release `docId`. Safe on a document that was never format 2.
|
|
14070
14710
|
*
|
|
@@ -14248,6 +14888,32 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
14248
14888
|
* holds. The sweep after it (`except`) and an ordinary connect honor the
|
|
14249
14889
|
* mark.
|
|
14250
14890
|
*/
|
|
14891
|
+
/**
|
|
14892
|
+
* Delete one model's derived query rows for this document, a page at a time
|
|
14893
|
+
* (#3435).
|
|
14894
|
+
*
|
|
14895
|
+
* The clear half of clear-then-project. Paged from the record store while
|
|
14896
|
+
* the rows are still there, which is the same reason and the same shape as
|
|
14897
|
+
* the discard path: after a range replacement nothing names the records it
|
|
14898
|
+
* removed, and `hydrateQueryTables` only ever projects records that EXIST.
|
|
14899
|
+
*/
|
|
14900
|
+
async clearQueryRows(docId, store, modelName) {
|
|
14901
|
+
const modelClass = this.modelRegistry.getModelClass(modelName);
|
|
14902
|
+
if (!modelClass) return;
|
|
14903
|
+
if (!store.isHydrated(modelName)) return;
|
|
14904
|
+
let after = null;
|
|
14905
|
+
for (; ; ) {
|
|
14906
|
+
const page = await store.recordIdsAfter(
|
|
14907
|
+
modelName,
|
|
14908
|
+
after,
|
|
14909
|
+
QUERY_HYDRATION_PAGE_ROWS
|
|
14910
|
+
);
|
|
14911
|
+
if (page.length === 0) break;
|
|
14912
|
+
await modelClass.deleteFormat2QueryRows(docId, page);
|
|
14913
|
+
after = page[page.length - 1];
|
|
14914
|
+
if (page.length < QUERY_HYDRATION_PAGE_ROWS) break;
|
|
14915
|
+
}
|
|
14916
|
+
}
|
|
14251
14917
|
async hydrateQueryTables(docId, store, scope = {}) {
|
|
14252
14918
|
for (const [, modelClass] of this.modelRegistry.getActiveModels()) {
|
|
14253
14919
|
const modelName = modelClass.getSchema?.()?.options?.name;
|
|
@@ -14440,7 +15106,9 @@ function format2SyncMarks(docId, ctx) {
|
|
|
14440
15106
|
highestLocalSeq: store.highestLocalSeq(),
|
|
14441
15107
|
ackedSeq: store.ackedSeq(),
|
|
14442
15108
|
epoch: store.epoch(),
|
|
14443
|
-
clockOffsetKnown: store.clockOffsetKnown()
|
|
15109
|
+
clockOffsetKnown: store.clockOffsetKnown(),
|
|
15110
|
+
discontinuityEpochs: store.discontinuityEpochs(),
|
|
15111
|
+
baseComplete: store.baseComplete()
|
|
14444
15112
|
};
|
|
14445
15113
|
}
|
|
14446
15114
|
function format2NoteEpoch(docId, epoch, ctx) {
|
|
@@ -14483,6 +15151,17 @@ function format2NoteClockOffset(docId, offsetMs, ctx) {
|
|
|
14483
15151
|
if (!store) return;
|
|
14484
15152
|
store.noteClockOffset(offsetMs);
|
|
14485
15153
|
}
|
|
15154
|
+
async function format2NoteDiscontinuity(docId, epoch, ctx) {
|
|
15155
|
+
const store = (ctx ?? getDefaultOrmContext()).getFormat2StoreAsync(docId);
|
|
15156
|
+
if (!store) return;
|
|
15157
|
+
if (!Number.isInteger(epoch) || epoch <= 0) return;
|
|
15158
|
+
await store.noteDiscontinuity(epoch);
|
|
15159
|
+
}
|
|
15160
|
+
async function format2ClearDiscontinuities(docId, through, ctx) {
|
|
15161
|
+
const store = (ctx ?? getDefaultOrmContext()).getFormat2StoreAsync(docId);
|
|
15162
|
+
if (!store) return;
|
|
15163
|
+
await store.clearDiscontinuities(through);
|
|
15164
|
+
}
|
|
14486
15165
|
function format2ForgetPendingOps(docId, seqs, ctx) {
|
|
14487
15166
|
const store = (ctx ?? getDefaultOrmContext()).getFormat2StoreAsync(docId);
|
|
14488
15167
|
if (!store) return;
|
|
@@ -14666,6 +15345,23 @@ var DocumentManagerImpl = class {
|
|
|
14666
15345
|
this.largeDocumentStorage
|
|
14667
15346
|
);
|
|
14668
15347
|
}
|
|
15348
|
+
/**
|
|
15349
|
+
* The models this instance holds rows of for `docId` (#3435) — the inventory
|
|
15350
|
+
* a convergence plan decides its prunes against.
|
|
15351
|
+
*/
|
|
15352
|
+
localModels(docId) {
|
|
15353
|
+
const connected = this.connectedDocuments.get(docId);
|
|
15354
|
+
if (!connected || connected.documentFormat !== 2) return null;
|
|
15355
|
+
return this.format2.localModels(docId);
|
|
15356
|
+
}
|
|
15357
|
+
/** Whether each record is in `docId`'s merged view (#3435, behavior 21). */
|
|
15358
|
+
async recordPresence(docId, records) {
|
|
15359
|
+
const connected = this.connectedDocuments.get(docId);
|
|
15360
|
+
if (!connected || connected.documentFormat !== 2) {
|
|
15361
|
+
return records.map(() => null);
|
|
15362
|
+
}
|
|
15363
|
+
return await this.format2.recordPresence(docId, records);
|
|
15364
|
+
}
|
|
14669
15365
|
async disconnectDocument(docId) {
|
|
14670
15366
|
const connectedDoc = this.connectedDocuments.get(docId);
|
|
14671
15367
|
if (!connectedDoc) {
|
|
@@ -14785,6 +15481,8 @@ async function initJsBaoCore(options, getEngine) {
|
|
|
14785
15481
|
format2NoteWindow: (docId, windowDays) => format2NoteWindow(docId, windowDays, ormContext),
|
|
14786
15482
|
format2NoteClockOffset: (docId, offsetMs) => format2NoteClockOffset(docId, offsetMs, ormContext),
|
|
14787
15483
|
format2ForgetPendingOps: (docId, seqs) => format2ForgetPendingOps(docId, seqs, ormContext),
|
|
15484
|
+
format2NoteDiscontinuity: (docId, epoch) => format2NoteDiscontinuity(docId, epoch, ormContext),
|
|
15485
|
+
format2ClearDiscontinuities: (docId, through) => format2ClearDiscontinuities(docId, through, ormContext),
|
|
14788
15486
|
format2NarrowPendingOps: (docId, ops) => format2NarrowPendingOps(docId, ops, ormContext),
|
|
14789
15487
|
format2PendingOps: (docId) => format2PendingOps(docId, ormContext),
|
|
14790
15488
|
format2SetEpoch: (docId, epoch) => format2SetEpoch(docId, epoch, ormContext),
|
|
@@ -14799,6 +15497,8 @@ async function initJsBaoCore(options, getEngine) {
|
|
|
14799
15497
|
// from the room counts as applied.
|
|
14800
15498
|
whenFormat2ProjectionSettled: docManager.whenFormat2ProjectionSettled.bind(docManager),
|
|
14801
15499
|
format2LoadSnapshot: docManager.loadSnapshot.bind(docManager),
|
|
15500
|
+
format2LocalModels: (docId) => docManager.localModels(docId),
|
|
15501
|
+
format2RecordPresence: (docId, records) => docManager.recordPresence(docId, records),
|
|
14802
15502
|
getConnectedDocuments: docManager.getConnectedDocuments.bind(docManager),
|
|
14803
15503
|
isDocumentConnected: docManager.isDocumentConnected.bind(docManager),
|
|
14804
15504
|
onDocumentConnectionChange: docManager.onConnectionChange.bind(docManager),
|
|
@@ -14958,40 +15658,6 @@ function createModelClass(config) {
|
|
|
14958
15658
|
attachAndRegisterModel(GeneratedModel, schema);
|
|
14959
15659
|
return GeneratedModel;
|
|
14960
15660
|
}
|
|
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
15661
|
function attachSchemaToClass(modelClass, schema) {
|
|
14996
15662
|
modelClass.schema = schema;
|
|
14997
15663
|
modelClass.modelName = schema.options.name;
|
|
@@ -15542,6 +16208,49 @@ function writeCarriedOverlay(modelMap, entry) {
|
|
|
15542
16208
|
}
|
|
15543
16209
|
}
|
|
15544
16210
|
}
|
|
16211
|
+
function suppressSupersededKeys(carried, later) {
|
|
16212
|
+
if (later.length === 0 || carried.length === 0) return carried;
|
|
16213
|
+
const fields = /* @__PURE__ */ new Map();
|
|
16214
|
+
const revived = /* @__PURE__ */ new Set();
|
|
16215
|
+
for (const op of later) {
|
|
16216
|
+
const key = `${op.model} ${op.recordId}`;
|
|
16217
|
+
let owned = fields.get(key);
|
|
16218
|
+
if (!owned) fields.set(key, owned = /* @__PURE__ */ new Set());
|
|
16219
|
+
for (const field of op.fields) owned.add(field);
|
|
16220
|
+
if (op.op !== "delete") revived.add(key);
|
|
16221
|
+
}
|
|
16222
|
+
const kept = [];
|
|
16223
|
+
for (const { model, entry } of carried) {
|
|
16224
|
+
const key = `${model} ${entry.id}`;
|
|
16225
|
+
const owned = fields.get(key);
|
|
16226
|
+
if (owned === void 0) {
|
|
16227
|
+
kept.push({ model, entry });
|
|
16228
|
+
continue;
|
|
16229
|
+
}
|
|
16230
|
+
if (entry.deleted) {
|
|
16231
|
+
if (!revived.has(key)) kept.push({ model, entry });
|
|
16232
|
+
continue;
|
|
16233
|
+
}
|
|
16234
|
+
const survivingFields = Object.fromEntries(
|
|
16235
|
+
Object.entries(entry.fields).filter(([field]) => !owned.has(field))
|
|
16236
|
+
);
|
|
16237
|
+
const survivingSets = Object.fromEntries(
|
|
16238
|
+
Object.entries(entry.stringSets).filter(([field]) => !owned.has(field))
|
|
16239
|
+
);
|
|
16240
|
+
if (!entry.replace && Object.keys(survivingFields).length === 0 && Object.keys(survivingSets).length === 0) {
|
|
16241
|
+
continue;
|
|
16242
|
+
}
|
|
16243
|
+
kept.push({
|
|
16244
|
+
model,
|
|
16245
|
+
entry: {
|
|
16246
|
+
...entry,
|
|
16247
|
+
fields: survivingFields,
|
|
16248
|
+
stringSets: survivingSets
|
|
16249
|
+
}
|
|
16250
|
+
});
|
|
16251
|
+
}
|
|
16252
|
+
return kept;
|
|
16253
|
+
}
|
|
15545
16254
|
|
|
15546
16255
|
// src/models/format2EpochRotation.ts
|
|
15547
16256
|
var Y10 = __toESM(require("yjs"), 1);
|
|
@@ -15613,10 +16322,15 @@ async function writeDeferredReplay(host, ops, owedOverlay, resolveReplay) {
|
|
|
15613
16322
|
const live = host.overlayDoc();
|
|
15614
16323
|
if (!live) return 0;
|
|
15615
16324
|
const owed = resolveReplay(ops);
|
|
15616
|
-
const
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
16325
|
+
const held = new Set(ops.map((op) => op.seq));
|
|
16326
|
+
const later = host.pendingOps().filter((op) => !held.has(op.seq));
|
|
16327
|
+
const carried = suppressSupersededKeys(
|
|
16328
|
+
carryUnackedOverlay(
|
|
16329
|
+
owed.ops,
|
|
16330
|
+
(model, recordId) => owedOverlay.get(`${model} ${recordId}`) ?? readOverlayRecord(live.getMap(model), recordId),
|
|
16331
|
+
{ suppressDeletes: owed.suppressDeletes }
|
|
16332
|
+
),
|
|
16333
|
+
later
|
|
15620
16334
|
);
|
|
15621
16335
|
if (carried.length === 0) return 0;
|
|
15622
16336
|
live.transact(() => {
|
|
@@ -15640,18 +16354,22 @@ function planFastForward(options) {
|
|
|
15640
16354
|
if (target <= current) return { kind: "none", reason: "current" };
|
|
15641
16355
|
const byEpoch = /* @__PURE__ */ new Map();
|
|
15642
16356
|
for (const entry of options.sealed) byEpoch.set(entry.epoch, entry);
|
|
16357
|
+
const discontinuities = options.sealed.filter(
|
|
16358
|
+
(entry) => entry.baseDiscontinuity && entry.epoch >= current && entry.epoch < target
|
|
16359
|
+
).map((entry) => entry.epoch).sort((a, b) => a - b);
|
|
16360
|
+
const last = discontinuities.length > 0 ? discontinuities[0] : target - 1;
|
|
15643
16361
|
const steps = [];
|
|
15644
|
-
for (let epoch = current; epoch
|
|
16362
|
+
for (let epoch = current; epoch <= last; epoch++) {
|
|
15645
16363
|
const entry = byEpoch.get(epoch);
|
|
15646
16364
|
if (!entry) return { kind: "reload", reason: "gap", epoch };
|
|
15647
|
-
if (entry.baseDiscontinuity) {
|
|
15648
|
-
return { kind: "reload", reason: "discontinuity", epoch };
|
|
15649
|
-
}
|
|
15650
16365
|
if (entry.download === void 0 || entry.download === null) {
|
|
15651
16366
|
return { kind: "reload", reason: "unavailable", epoch };
|
|
15652
16367
|
}
|
|
15653
16368
|
steps.push({ epoch, download: entry.download });
|
|
15654
16369
|
}
|
|
16370
|
+
if (discontinuities.length > 0) {
|
|
16371
|
+
return { kind: "converge", from: current, discontinuities, steps };
|
|
16372
|
+
}
|
|
15655
16373
|
return { kind: "apply", from: current, to: target, steps };
|
|
15656
16374
|
}
|
|
15657
16375
|
async function runFastForward(host, options) {
|
|
@@ -15660,6 +16378,14 @@ async function runFastForward(host, options) {
|
|
|
15660
16378
|
if (plan.kind === "reload") {
|
|
15661
16379
|
return { outcome: "reload", reason: plan.reason, epoch: plan.epoch };
|
|
15662
16380
|
}
|
|
16381
|
+
if (plan.kind === "converge") {
|
|
16382
|
+
return {
|
|
16383
|
+
outcome: "converge",
|
|
16384
|
+
from: plan.from,
|
|
16385
|
+
discontinuities: plan.discontinuities,
|
|
16386
|
+
steps: plan.steps
|
|
16387
|
+
};
|
|
16388
|
+
}
|
|
15663
16389
|
let applied;
|
|
15664
16390
|
try {
|
|
15665
16391
|
applied = await host.hold(() => applyFastForward(plan, host));
|
|
@@ -15697,6 +16423,10 @@ async function applyFastForward(plan, host) {
|
|
|
15697
16423
|
}
|
|
15698
16424
|
|
|
15699
16425
|
// src/models/format2ColdStart.ts
|
|
16426
|
+
function isGranted(sealed, epoch) {
|
|
16427
|
+
const entry = sealed.find((each) => each.epoch === epoch);
|
|
16428
|
+
return !!entry && entry.download !== void 0 && entry.download !== null;
|
|
16429
|
+
}
|
|
15700
16430
|
function chainIsWhole(sealed, from, to) {
|
|
15701
16431
|
const byEpoch = /* @__PURE__ */ new Map();
|
|
15702
16432
|
for (const entry of sealed) byEpoch.set(entry.epoch, entry);
|
|
@@ -15722,6 +16452,15 @@ function planColdStart(input) {
|
|
|
15722
16452
|
if (first === 1 && chainIsWhole(sealed, 1, reported)) {
|
|
15723
16453
|
return { kind: "overlays", base: 1 };
|
|
15724
16454
|
}
|
|
16455
|
+
const start = Number.isInteger(base) && base > 0 ? base : 1;
|
|
16456
|
+
const discontinuities = sealed.filter(
|
|
16457
|
+
(entry) => entry.baseDiscontinuity && entry.epoch >= start && entry.epoch < reported
|
|
16458
|
+
).map((entry) => entry.epoch);
|
|
16459
|
+
if (discontinuities.length > 0 && // Whole and granted up to the first flagged epoch, and no further: past it
|
|
16460
|
+
// the base that is coming covers everything.
|
|
16461
|
+
chainIsWhole(sealed, start, discontinuities[0]) && isGranted(sealed, discontinuities[0])) {
|
|
16462
|
+
return { kind: "await-base", discontinuities };
|
|
16463
|
+
}
|
|
15725
16464
|
return { kind: "unavailable", reason: "no-base" };
|
|
15726
16465
|
}
|
|
15727
16466
|
async function runColdStart(host, plan, options = {}) {
|
|
@@ -15733,6 +16472,8 @@ async function runColdStart(host, plan, options = {}) {
|
|
|
15733
16472
|
return true;
|
|
15734
16473
|
case "unavailable":
|
|
15735
16474
|
return false;
|
|
16475
|
+
case "await-base":
|
|
16476
|
+
return false;
|
|
15736
16477
|
case "snapshot":
|
|
15737
16478
|
case "overlays": {
|
|
15738
16479
|
if (options.stale) await host.discard();
|
|
@@ -15753,11 +16494,69 @@ async function runColdStart(host, plan, options = {}) {
|
|
|
15753
16494
|
}
|
|
15754
16495
|
}
|
|
15755
16496
|
|
|
16497
|
+
// src/models/format2RangeReplacement.ts
|
|
16498
|
+
init_snapshotChunks();
|
|
16499
|
+
function planRangeReplacement(options) {
|
|
16500
|
+
const { manifest, touched } = options;
|
|
16501
|
+
const scope = options.localModels;
|
|
16502
|
+
const inScope = (model) => scope === null || scope.includes(model);
|
|
16503
|
+
const perModel = /* @__PURE__ */ new Map();
|
|
16504
|
+
for (const chunk of manifest.chunks) {
|
|
16505
|
+
if (!inScope(chunk.model)) continue;
|
|
16506
|
+
const list = perModel.get(chunk.model);
|
|
16507
|
+
if (list) list.push(chunk);
|
|
16508
|
+
else perModel.set(chunk.model, [chunk]);
|
|
16509
|
+
}
|
|
16510
|
+
const touchedPerModel = /* @__PURE__ */ new Map();
|
|
16511
|
+
for (const range of touched?.ranges ?? []) {
|
|
16512
|
+
const list = touchedPerModel.get(range.model);
|
|
16513
|
+
if (list) list.push(range);
|
|
16514
|
+
else touchedPerModel.set(range.model, [range]);
|
|
16515
|
+
}
|
|
16516
|
+
const fetch = [];
|
|
16517
|
+
const keep = [];
|
|
16518
|
+
const order = new Map(manifest.chunks.map((chunk, index) => [chunk, index]));
|
|
16519
|
+
for (const [model, chunks] of perModel) {
|
|
16520
|
+
const ranges = touchedPerModel.get(model) ?? [];
|
|
16521
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
16522
|
+
const chunk = chunks[index];
|
|
16523
|
+
const coverage = {
|
|
16524
|
+
after: index === 0 ? null : chunks[index - 1].lastId,
|
|
16525
|
+
through: index === chunks.length - 1 ? null : chunk.lastId
|
|
16526
|
+
};
|
|
16527
|
+
if (touched === null || ranges.some((range) => meets(coverage, range))) {
|
|
16528
|
+
fetch.push({ chunk, coverage });
|
|
16529
|
+
} else {
|
|
16530
|
+
keep.push(chunk);
|
|
16531
|
+
}
|
|
16532
|
+
}
|
|
16533
|
+
}
|
|
16534
|
+
fetch.sort((a, b) => order.get(a.chunk) - order.get(b.chunk));
|
|
16535
|
+
keep.sort((a, b) => order.get(a) - order.get(b));
|
|
16536
|
+
const absent = /* @__PURE__ */ new Set();
|
|
16537
|
+
for (const model of scope ?? []) absent.add(model);
|
|
16538
|
+
for (const model of touchedPerModel.keys()) {
|
|
16539
|
+
if (inScope(model)) absent.add(model);
|
|
16540
|
+
}
|
|
16541
|
+
const pruneModels = [...absent].filter((model) => !perModel.has(model)).sort();
|
|
16542
|
+
return { fetch, keep, pruneModels, full: touched === null };
|
|
16543
|
+
}
|
|
16544
|
+
function meets(coverage, range) {
|
|
16545
|
+
if (coverage.through !== null && compareRecordIds(range.firstId, coverage.through) > 0) {
|
|
16546
|
+
return false;
|
|
16547
|
+
}
|
|
16548
|
+
if (coverage.after !== null && compareRecordIds(range.lastId, coverage.after) <= 0) {
|
|
16549
|
+
return false;
|
|
16550
|
+
}
|
|
16551
|
+
return true;
|
|
16552
|
+
}
|
|
16553
|
+
|
|
15756
16554
|
// src/models/format2OfflineReplay.ts
|
|
15757
16555
|
var Y12 = __toESM(require("yjs"), 1);
|
|
15758
16556
|
init_largeDocuments();
|
|
15759
16557
|
init_format2Observer();
|
|
15760
16558
|
var recordKey = (model, recordId) => `${model}\0${recordId}`;
|
|
16559
|
+
var offlineReplayRecordKey = recordKey;
|
|
15761
16560
|
var OfflineConflictLedger = class {
|
|
15762
16561
|
/** Latest touch per record per epoch, newest epoch last. */
|
|
15763
16562
|
touches = /* @__PURE__ */ new Map();
|
|
@@ -15900,6 +16699,40 @@ function resolveOfflineReplay(options) {
|
|
|
15900
16699
|
};
|
|
15901
16700
|
for (const op of [...options.ops].sort((a, b) => a.seq - b.seq)) {
|
|
15902
16701
|
const base = { model: op.model, recordId: op.recordId, op: op.op, ts: op.ts };
|
|
16702
|
+
const ingest = options.ingest;
|
|
16703
|
+
if (ingest && op.baseEpoch <= ingest.through) {
|
|
16704
|
+
const presence = ingest.records.get(recordKey(op.model, op.recordId));
|
|
16705
|
+
const verdict = presence ?? (ingest.scope === "all" ? "present" : void 0);
|
|
16706
|
+
if (verdict === "present") {
|
|
16707
|
+
ops.push(op);
|
|
16708
|
+
notices.push({
|
|
16709
|
+
...base,
|
|
16710
|
+
outcome: "kept-ambiguous",
|
|
16711
|
+
reason: "bulkIngest",
|
|
16712
|
+
epoch: ingest.through
|
|
16713
|
+
});
|
|
16714
|
+
continue;
|
|
16715
|
+
}
|
|
16716
|
+
if (verdict === "absent") {
|
|
16717
|
+
if (op.op === "create") {
|
|
16718
|
+
ops.push(op);
|
|
16719
|
+
notices.push({
|
|
16720
|
+
...base,
|
|
16721
|
+
outcome: "kept-ambiguous",
|
|
16722
|
+
reason: "bulkIngest",
|
|
16723
|
+
epoch: ingest.through
|
|
16724
|
+
});
|
|
16725
|
+
continue;
|
|
16726
|
+
}
|
|
16727
|
+
notices.push({
|
|
16728
|
+
...base,
|
|
16729
|
+
outcome: "dropped",
|
|
16730
|
+
reason: "bulkIngest",
|
|
16731
|
+
epoch: ingest.through
|
|
16732
|
+
});
|
|
16733
|
+
continue;
|
|
16734
|
+
}
|
|
16735
|
+
}
|
|
15903
16736
|
if (!ledger.covers(op.baseEpoch, currentEpoch)) {
|
|
15904
16737
|
ops.push(op);
|
|
15905
16738
|
notices.push({
|
|
@@ -16021,8 +16854,10 @@ function resolveOfflineReplay(options) {
|
|
|
16021
16854
|
|
|
16022
16855
|
// src/browser.ts
|
|
16023
16856
|
init_format2OfflineWindow();
|
|
16857
|
+
init_snapshotChunks();
|
|
16024
16858
|
|
|
16025
16859
|
// src/utils/snapshotPartition.ts
|
|
16860
|
+
init_snapshotChunks();
|
|
16026
16861
|
var DEFAULT_SNAPSHOT_PARTITION = {
|
|
16027
16862
|
minBytes: 6 * 1024 * 1024,
|
|
16028
16863
|
maxBytes: 12 * 1024 * 1024,
|
|
@@ -16188,6 +17023,19 @@ var SnapshotPartitioner = class {
|
|
|
16188
17023
|
get pendingBytes() {
|
|
16189
17024
|
return this.bufferBytes;
|
|
16190
17025
|
}
|
|
17026
|
+
/**
|
|
17027
|
+
* Whether the buffer is a below-minimum range being carried forward (#3433).
|
|
17028
|
+
*
|
|
17029
|
+
* An incremental build walks only the ranges that changed, so it has to know
|
|
17030
|
+
* when a range it re-cut closed short and merged into its SUCCESSOR: that
|
|
17031
|
+
* successor is then part of the walk too, and cannot be reused by reference.
|
|
17032
|
+
* Reading it off the partitioner rather than inferring it from the emitted
|
|
17033
|
+
* chunks keeps one answer to "did this boundary move", in the object that
|
|
17034
|
+
* decided it.
|
|
17035
|
+
*/
|
|
17036
|
+
get carriedTail() {
|
|
17037
|
+
return this.merging;
|
|
17038
|
+
}
|
|
16191
17039
|
take(count) {
|
|
16192
17040
|
const taken = count === void 0 ? this.buffer : this.buffer.slice(0, count);
|
|
16193
17041
|
const rest = count === void 0 ? [] : this.buffer.slice(count);
|