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 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 Y2.Map) return nested;
1266
- const fresh = new Y2.Map();
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 Y2, import_ulid, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
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
- Y2 = __toESM(require("yjs"), 1);
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 Y3.Map();
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 Y3.Map();
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 Y3.Map();
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 Y3.Map();
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 Y3.Map();
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 Y3.Map();
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 Y3, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1519
+ var Y4, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1457
1520
  var init_metaSync = __esm({
1458
1521
  "src/models/metaSync.ts"() {
1459
1522
  "use strict";
1460
- Y3 = __toESM(require("yjs"), 1);
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
- for (const entry of entries) {
3560
- if (this.cache.peek(model, entry.id)) {
3561
- await this.refreshRows(model, [entry.id]);
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
- completedChunks(buildId) {
3567
- return this.host.dispatch({ kind: "completedChunks", buildId });
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
- const text = decoder.decode(await gunzip(body));
11300
- const rows = [];
11301
- for (const line of text.split("\n")) {
11302
- if (line.length === 0) continue;
11303
- const tab = line.indexOf(" ");
11304
- if (tab < 0) {
11305
- throw new SnapshotChunkIntegrityError(
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
- if (entry && rows.length !== entry.rows) {
11317
- throw new SnapshotChunkIntegrityError(
11318
- entry.key,
11319
- `expected ${entry.rows} rows, got ${rows.length}`
11320
- );
11816
+ async getTableSchema(_tableName) {
11817
+ return void 0;
11321
11818
  }
11322
- if (entry && rows.length > 0) {
11323
- const seen = /* @__PURE__ */ new Set();
11324
- for (const row of rows) {
11325
- if (seen.has(row._id)) {
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
- return rows;
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 = { restored: [], unreproducible: [] };
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 selected = manifest.chunks.filter((chunk) => wanted(chunk.model));
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
- await store.applyChunk(
13677
- chunk.model,
13678
- decoded.map(
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 restored = restorePendingOverlay(yDoc, store, verdicts);
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
- await this.hydrateQueryTables(docId, store, { only: model });
14055
- await this.whenProjectionSettled();
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
- await this.hydrateQueryTables(docId, store, { except: hydrated });
14061
- await this.whenProjectionSettled();
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 carried = carryUnackedOverlay(
15617
- owed.ops,
15618
- (model, recordId) => owedOverlay.get(`${model} ${recordId}`) ?? readOverlayRecord(live.getMap(model), recordId),
15619
- { suppressDeletes: owed.suppressDeletes }
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 < target; 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);