@powerhousedao/reactor 6.2.3-dev.31 → 6.2.3-dev.33

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.
@@ -1,6 +1,6 @@
1
- import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, actionSignerIdentity, actionSigningTarget, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, base58Decode, baseReducerVersion, buildOperationSignatureMessage, buildOperationSignatureParams, canonicalJson, createPresignedHeader, decide, defaultBaseState, deriveDocumentId, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashActionV2, hashBrowser, hashDocumentStateForScope, isDenied, isDerivedDocumentId, isUndoRedo, isV2ActionHash, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
1
+ import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, PEER_CAPABILITIES, actionSignerIdentity, actionSigningTarget, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, base58Decode, baseReducerVersion, buildOperationSignatureMessage, buildOperationSignatureParams, canonicalJson, createPresignedHeader, decide, defaultBaseState, deriveDocumentId, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashActionV2, hashBrowser, hashDocumentStateForScope, isDenied, isDerivedDocumentId, isUndoRedo, isV2ActionHash, localSupports, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
2
2
  import { v4 } from "uuid";
3
- import { Migrator, sql } from "kysely";
3
+ import { Migrator, TableNode, sql } from "kysely";
4
4
  //#region \0rolldown/runtime.js
5
5
  var __defProp = Object.defineProperty;
6
6
  var __exportAll = (all, no_symbols) => {
@@ -79,7 +79,8 @@ const ReactorEventTypes = {
79
79
  READMODEL_BATCH_COMPLETED: 10006,
80
80
  READMODEL_INDEXED: 10007,
81
81
  MODEL_LOADED: 10008,
82
- SIGNATURE_REFUSED: 10009
82
+ SIGNATURE_REFUSED: 10009,
83
+ CATCHUP_SWEPT: 10010
83
84
  };
84
85
  //#endregion
85
86
  //#region src/shared/errors.ts
@@ -173,6 +174,23 @@ var InvalidOperationTimestampError = class InvalidOperationTimestampError extend
173
174
  return Error.isError(error) && error.name === "InvalidOperationTimestampError";
174
175
  }
175
176
  };
177
+ /** A document requires a protocol version this reactor does not run. Terminal. */
178
+ var UnsupportedProtocolVersionError = class UnsupportedProtocolVersionError extends Error {
179
+ documentId;
180
+ protocol;
181
+ version;
182
+ constructor(documentId, protocol, version) {
183
+ super(`Document ${documentId} requires ${protocol} ${version}, which this reactor does not support`);
184
+ this.name = "UnsupportedProtocolVersionError";
185
+ this.documentId = documentId;
186
+ this.protocol = protocol;
187
+ this.version = version;
188
+ Error.captureStackTrace(this, UnsupportedProtocolVersionError);
189
+ }
190
+ static isError(error) {
191
+ return Error.isError(error) && error.name === "UnsupportedProtocolVersionError";
192
+ }
193
+ };
176
194
  /**
177
195
  * A load would move more operations than the bound allows, indicating a real
178
196
  * divergence between local and incoming history. Counts only first-time moves,
@@ -307,6 +325,22 @@ var RelationshipNotFoundError = class RelationshipNotFoundError extends Error {
307
325
  return Error.isError(error) && error.name === "RelationshipNotFoundError";
308
326
  }
309
327
  };
328
+ /** The store holds documents at protocol versions this reactor does not run. */
329
+ var UnsupportedStoredProtocolError = class UnsupportedStoredProtocolError extends Error {
330
+ versions;
331
+ documents;
332
+ constructor(versions, documents) {
333
+ const named = versions.map(({ protocol, version }) => `${protocol} ${version}`).join(", ");
334
+ super(`${documents} stored document(s) require ${named}, which this reactor does not run. Start a build that runs them, or accept them read-only with withUnsupportedStoredDocuments("read-only")`);
335
+ this.name = "UnsupportedStoredProtocolError";
336
+ this.versions = versions;
337
+ this.documents = documents;
338
+ Error.captureStackTrace(this, UnsupportedStoredProtocolError);
339
+ }
340
+ static isError(error) {
341
+ return Error.isError(error) && error.name === "UnsupportedStoredProtocolError";
342
+ }
343
+ };
310
344
  //#endregion
311
345
  //#region src/storage/interfaces.ts
312
346
  /**
@@ -393,6 +427,52 @@ let DocumentExistence = /* @__PURE__ */ function(DocumentExistence) {
393
427
  return DocumentExistence;
394
428
  }({});
395
429
  //#endregion
430
+ //#region src/cache/operation-index-types.ts
431
+ const DRIVE_COLLECTION_PREFIX = "drive.";
432
+ /**
433
+ * Identifies the collection a remote synchronizes. Collections are drive-level
434
+ * abstractions (document-drive and reactor-drive), so a collection id is the
435
+ * drive document id plus the branch it scopes to rather than an opaque string.
436
+ *
437
+ * The canonical string form (`drive.${branch}.${driveId}`) is produced only by
438
+ * `key` and parsed only by `fromKey`; that string is the wire and storage
439
+ * representation and is byte-for-byte identical to the legacy
440
+ * `driveCollectionId(branch, driveId)` output, so existing `document_collections`
441
+ * rows and persisted remotes remain valid without migration.
442
+ */
443
+ var DriveCollectionId = class DriveCollectionId {
444
+ driveId;
445
+ branch;
446
+ constructor(driveId, branch) {
447
+ this.driveId = driveId;
448
+ this.branch = branch;
449
+ }
450
+ static forDrive(driveId, branch = "main") {
451
+ return new DriveCollectionId(driveId, branch);
452
+ }
453
+ /**
454
+ * The single deserializer for the wire/storage form. `branch` may contain
455
+ * dots, while `driveId` is a dot-free document id, so the drive id is the
456
+ * final dot-delimited segment.
457
+ */
458
+ static fromKey(key) {
459
+ if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
460
+ const rest = key.slice(6);
461
+ const lastDot = rest.lastIndexOf(".");
462
+ if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
463
+ return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
464
+ }
465
+ get key() {
466
+ return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
467
+ }
468
+ toString() {
469
+ return this.key;
470
+ }
471
+ equals(other) {
472
+ return this.driveId === other.driveId && this.branch === other.branch;
473
+ }
474
+ };
475
+ //#endregion
396
476
  //#region src/decision/build-decision-model.ts
397
477
  /**
398
478
  * Reads each projection's stream through the supplied reader, recording the
@@ -1283,6 +1363,212 @@ var DocumentMetaCache = class DocumentMetaCache {
1283
1363
  };
1284
1364
  }
1285
1365
  };
1366
+ //#endregion
1367
+ //#region src/catch-up/settled-watermark.ts
1368
+ const MAX_PENDING_PROBES = 64;
1369
+ /** pg_sequence_last_value needs 10; pg_current_* replaced txid_* in 13. */
1370
+ function snapshotFunctionsFor(serverVersionNum) {
1371
+ if (serverVersionNum >= 13e4) return {
1372
+ snapshot: "pg_current_snapshot",
1373
+ currentXid: "pg_current_xact_id",
1374
+ xidIfAssigned: "pg_current_xact_id_if_assigned"
1375
+ };
1376
+ if (serverVersionNum >= 1e5) return {
1377
+ snapshot: "txid_current_snapshot",
1378
+ currentXid: "txid_current",
1379
+ xidIfAssigned: "txid_current_if_assigned"
1380
+ };
1381
+ throw new Error(`PostgreSQL server_version_num ${serverVersionNum} is not supported: read-side catch-up needs 10 or later`);
1382
+ }
1383
+ /** Parses the `xmin:xmax:xip,...` text form of a snapshot. */
1384
+ function parseSnapshot(text) {
1385
+ const parts = text.split(":");
1386
+ if (parts.length !== 3) throw new Error(`Malformed snapshot: ${text}`);
1387
+ const [xmin, xmax, xip] = parts;
1388
+ return {
1389
+ xmin: BigInt(xmin),
1390
+ xmax: BigInt(xmax),
1391
+ xip: xip === "" ? [] : xip.split(",").map((xid) => BigInt(xid))
1392
+ };
1393
+ }
1394
+ async function readSnapshotFunctions(db) {
1395
+ const result = await sql`select current_setting('server_version_num') as version`.execute(db);
1396
+ return snapshotFunctionsFor(Number(result.rows[0].version));
1397
+ }
1398
+ /** A probe settles once an xmin passes the xid it took after its head. */
1399
+ var ProbeSettler = class {
1400
+ settled = 0;
1401
+ lastHead = 0;
1402
+ covered = 0;
1403
+ lastXip = [];
1404
+ pending = [];
1405
+ get settledThrough() {
1406
+ return this.settled;
1407
+ }
1408
+ get head() {
1409
+ return this.lastHead;
1410
+ }
1411
+ /** The highest head a settled or pending probe covers. */
1412
+ get probedHead() {
1413
+ return this.covered;
1414
+ }
1415
+ observe(head, xid, snapshot, nowUtcMs) {
1416
+ this.lastHead = Math.max(this.lastHead, head);
1417
+ this.lastXip = snapshot.xip;
1418
+ if (xid !== null && head > this.covered) {
1419
+ this.covered = head;
1420
+ this.pending.push({
1421
+ head,
1422
+ xid,
1423
+ takenAtUtcMs: nowUtcMs
1424
+ });
1425
+ if (this.pending.length > MAX_PENDING_PROBES) this.pending.shift();
1426
+ }
1427
+ const stillPending = [];
1428
+ for (const probe of this.pending) if (snapshot.xmin > probe.xid) this.settled = Math.max(this.settled, probe.head);
1429
+ else stillPending.push(probe);
1430
+ this.pending = stillPending.filter((probe) => probe.head > this.settled);
1431
+ return this.settled;
1432
+ }
1433
+ /** Xids an unsettled probe still waits on. */
1434
+ waitingOn() {
1435
+ if (this.pending.length === 0) return [];
1436
+ const xid = this.pending[0].xid;
1437
+ return this.lastXip.filter((open) => open < xid).map((open) => open.toString());
1438
+ }
1439
+ stalledSinceUtcMs() {
1440
+ return this.pending[0]?.takenAtUtcMs;
1441
+ }
1442
+ };
1443
+ function operationIndexTableName(db) {
1444
+ const from = db.selectFrom("operation_index_operations").selectAll().toOperationNode().from?.froms[0];
1445
+ const schema = from !== void 0 && TableNode.is(from) ? from.table.schema?.name : void 0;
1446
+ const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1447
+ return schema === void 0 ? quote("operation_index_operations") : `${quote(schema)}.${quote("operation_index_operations")}`;
1448
+ }
1449
+ /** Reads the head, takes and commits an xid, then reads a snapshot, in order. */
1450
+ function createKyselyWatermarkProbe(db) {
1451
+ let resolved = null;
1452
+ const resolve = async () => {
1453
+ const fns = await readSnapshotFunctions(db);
1454
+ const table = operationIndexTableName(db);
1455
+ const sequence = (await sql`select pg_get_serial_sequence(${table}, 'ordinal') as sequence`.execute(db)).rows[0]?.sequence;
1456
+ if (!sequence) throw new Error(`No ordinal sequence found for ${table}`);
1457
+ return {
1458
+ fns,
1459
+ sequence
1460
+ };
1461
+ };
1462
+ return async (probedHead) => {
1463
+ resolved ??= resolve().catch((error) => {
1464
+ resolved = null;
1465
+ throw error;
1466
+ });
1467
+ const { fns, sequence } = await resolved;
1468
+ const headRow = (await sql`select coalesce(pg_sequence_last_value(${sequence}::regclass), 0) as head, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
1469
+ const head = Number(headRow.head);
1470
+ let xid = null;
1471
+ if (headRow.outside_write && head > probedHead) {
1472
+ const xidResult = await sql`select ${sql.raw(fns.currentXid)}()::text as xid`.execute(db);
1473
+ xid = BigInt(xidResult.rows[0].xid);
1474
+ }
1475
+ const row = (await sql`select ${sql.raw(fns.snapshot)}()::text as snapshot, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
1476
+ return {
1477
+ head,
1478
+ xid,
1479
+ snapshot: parseSnapshot(row.snapshot),
1480
+ outsideWrite: headRow.outside_write && row.outside_write
1481
+ };
1482
+ };
1483
+ }
1484
+ /** Knows when an ordinal gap is final by asking which transactions are open. */
1485
+ var SettledWatermark = class {
1486
+ probe;
1487
+ logger;
1488
+ now;
1489
+ settler = new ProbeSettler();
1490
+ listeners = /* @__PURE__ */ new Set();
1491
+ current;
1492
+ next;
1493
+ warnedInsideWrite = false;
1494
+ constructor(probe, logger, now = Date.now) {
1495
+ this.probe = probe;
1496
+ this.logger = logger;
1497
+ this.now = now;
1498
+ }
1499
+ get settledThrough() {
1500
+ return this.settler.settledThrough;
1501
+ }
1502
+ refresh(signal) {
1503
+ signal?.throwIfAborted();
1504
+ if (this.next !== void 0) return this.next;
1505
+ if (this.current === void 0) return this.start();
1506
+ const next = this.current.catch(() => this.settledThrough).then(() => {
1507
+ this.next = void 0;
1508
+ return this.start();
1509
+ });
1510
+ this.next = next;
1511
+ return next;
1512
+ }
1513
+ onAdvance(listener) {
1514
+ this.listeners.add(listener);
1515
+ return () => {
1516
+ this.listeners.delete(listener);
1517
+ };
1518
+ }
1519
+ status() {
1520
+ const stalledSinceUtcMs = this.settler.head > this.settledThrough ? this.settler.stalledSinceUtcMs() : void 0;
1521
+ return {
1522
+ head: this.settler.head,
1523
+ settledThrough: this.settledThrough,
1524
+ waitingOn: this.settler.waitingOn(),
1525
+ ...stalledSinceUtcMs !== void 0 ? { stalledSinceUtcMs } : {}
1526
+ };
1527
+ }
1528
+ start() {
1529
+ const run = this.probeOnce().finally(() => {
1530
+ if (this.current === run) this.current = void 0;
1531
+ });
1532
+ this.current = run;
1533
+ return run;
1534
+ }
1535
+ async probeOnce() {
1536
+ const reading = await this.probe(this.settler.probedHead);
1537
+ if (!reading.outsideWrite) {
1538
+ if (!this.warnedInsideWrite) {
1539
+ this.warnedInsideWrite = true;
1540
+ this.logger.warn("settled watermark probe ran inside another session's write transaction; discarded");
1541
+ }
1542
+ return this.settledThrough;
1543
+ }
1544
+ const before = this.settledThrough;
1545
+ const after = this.settler.observe(reading.head, reading.xid, reading.snapshot, this.now());
1546
+ if (after > before) for (const listener of this.listeners) try {
1547
+ listener(after);
1548
+ } catch (error) {
1549
+ this.logger.error("settled watermark listener failed: @error", error);
1550
+ }
1551
+ return after;
1552
+ }
1553
+ };
1554
+ /** Sessions holding these xids open; empty without pg_stat_activity. */
1555
+ async function describeWaitingSessions(db, xids) {
1556
+ if (xids.length === 0) return [];
1557
+ const epochless = xids.map((xid) => (BigInt(xid) % 2n ** 32n).toString());
1558
+ try {
1559
+ return (await sql`select pid, application_name, state, xact_start, backend_xid::text as xid
1560
+ from pg_stat_activity
1561
+ where backend_xid::text in (${sql.join(epochless)})`.execute(db)).rows.map((row) => ({
1562
+ pid: Number(row.pid),
1563
+ applicationName: row.application_name ?? "",
1564
+ state: row.state ?? "",
1565
+ xactStart: row.xact_start === null ? null : new Date(row.xact_start).toISOString(),
1566
+ xid: row.xid
1567
+ }));
1568
+ } catch {
1569
+ return [];
1570
+ }
1571
+ }
1286
1572
  /** Bind slots per statement: 16-bit on the wire, read signed in-process. */
1287
1573
  const MAX_BIND_PARAMETERS = 32767;
1288
1574
  /** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
@@ -1295,6 +1581,8 @@ function chunkRows(rows) {
1295
1581
  for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
1296
1582
  return chunks;
1297
1583
  }
1584
+ /** Per database: the function that assigns the transaction's xid. */
1585
+ const xidFunctions = /* @__PURE__ */ new WeakMap();
1298
1586
  var KyselyOperationIndexTxn = class {
1299
1587
  collections = [];
1300
1588
  collectionMemberships = [];
@@ -1421,6 +1709,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1421
1709
  }
1422
1710
  let operationOrdinals = [];
1423
1711
  if (operations.length > 0) {
1712
+ await this.assignXid(trx);
1424
1713
  const operationRows = operations.map((op) => ({
1425
1714
  opId: op.id || "",
1426
1715
  documentId: op.documentId,
@@ -1471,6 +1760,27 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1471
1760
  }
1472
1761
  return operationOrdinals;
1473
1762
  }
1763
+ async getOrdinalsInRange(after, through, limit, signal) {
1764
+ signal?.throwIfAborted();
1765
+ if (through <= after || limit <= 0) return [];
1766
+ return (await this.queryExecutor.selectFrom("operation_index_operations").select("ordinal").where("ordinal", ">", after).where("ordinal", "<=", through).orderBy("ordinal", "asc").limit(limit).execute()).map((row) => Number(row.ordinal));
1767
+ }
1768
+ async getByOrdinals(ordinals, signal) {
1769
+ signal?.throwIfAborted();
1770
+ if (ordinals.length === 0) return [];
1771
+ const results = [];
1772
+ for (let i = 0; i < ordinals.length; i += MAX_BIND_PARAMETERS) {
1773
+ const chunk = ordinals.slice(i, i + MAX_BIND_PARAMETERS);
1774
+ const rows = await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", "in", chunk).orderBy("ordinal", "asc").execute();
1775
+ for (const row of rows) results.push(this.rowToOperationWithContext(row));
1776
+ }
1777
+ results.sort((a, b) => a.context.ordinal - b.context.ordinal);
1778
+ return results;
1779
+ }
1780
+ async getStreamAfter(stream, after, signal) {
1781
+ signal?.throwIfAborted();
1782
+ return (await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", stream.documentId).where("branch", "=", stream.branch).where("scope", "=", stream.scope).where("ordinal", ">", after).orderBy("ordinal", "asc").execute()).map((row) => this.rowToOperationWithContext(row));
1783
+ }
1474
1784
  async getGroupReferencers(groupId, signal) {
1475
1785
  if (signal?.aborted) throw new Error("Operation aborted");
1476
1786
  return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
@@ -1494,6 +1804,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1494
1804
  if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
1495
1805
  if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
1496
1806
  if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
1807
+ if (view?.throughOrdinal !== void 0) qb = qb.where("oi.ordinal", "<=", view.throughOrdinal).where("dc.joinedOrdinal", "<=", BigInt(view.throughOrdinal));
1497
1808
  return qb;
1498
1809
  };
1499
1810
  const rows = await buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc").limit(limit + 1).execute();
@@ -1582,6 +1893,17 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1582
1893
  }, signal) : void 0
1583
1894
  };
1584
1895
  }
1896
+ /** Takes the xid before the first ordinal, as the settled watermark needs. */
1897
+ async assignXid(trx) {
1898
+ let fn = xidFunctions.get(this.db);
1899
+ if (fn === void 0) {
1900
+ fn = readSnapshotFunctions(trx).then((fns) => fns.currentXid);
1901
+ xidFunctions.set(this.db, fn);
1902
+ fn.catch(() => xidFunctions.delete(this.db));
1903
+ }
1904
+ const name = await fn;
1905
+ await sql`select ${sql.raw(name)}()`.execute(trx);
1906
+ }
1585
1907
  rowToOperationWithContext(row) {
1586
1908
  return {
1587
1909
  operation: {
@@ -2004,9 +2326,9 @@ var KyselyWriteCache = class KyselyWriteCache {
2004
2326
  * the last replayed operation are applied at the end. Creation-time 0->N seed
2005
2327
  * upgrades carry the initial state, so they still apply immediately.
2006
2328
  */
2007
- async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
2329
+ async coldMissRebuild(documentId, scope, branch, targetRevision, signal, fromKeyframe = true) {
2008
2330
  const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
2009
- const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
2331
+ const keyframe = fromKeyframe ? await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal) : void 0;
2010
2332
  const documentScopeBound = scope === "document" ? targetRevision : void 0;
2011
2333
  let document;
2012
2334
  let startRevision;
@@ -2143,6 +2465,7 @@ var KyselyWriteCache = class KyselyWriteCache {
2143
2465
  let cursor = void 0;
2144
2466
  const pageSize = 100;
2145
2467
  let hasMorePages;
2468
+ let needsFullReplay = false;
2146
2469
  do {
2147
2470
  if (signal?.aborted) throw new Error("Operation aborted");
2148
2471
  const paging = {
@@ -2153,6 +2476,10 @@ var KyselyWriteCache = class KyselyWriteCache {
2153
2476
  const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
2154
2477
  for (const operation of result.results) {
2155
2478
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
2479
+ if (keyframe && operation.skip > 0) {
2480
+ needsFullReplay = true;
2481
+ break;
2482
+ }
2156
2483
  const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
2157
2484
  document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
2158
2485
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
@@ -2167,12 +2494,13 @@ var KyselyWriteCache = class KyselyWriteCache {
2167
2494
  }
2168
2495
  }
2169
2496
  const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
2170
- hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
2497
+ hasMorePages = Boolean(result.nextCursor) && !reachedTarget && !needsFullReplay;
2171
2498
  if (hasMorePages) cursor = result.nextCursor;
2172
2499
  } catch (err) {
2173
2500
  throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2174
2501
  }
2175
2502
  } while (hasMorePages);
2503
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2176
2504
  document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
2177
2505
  document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
2178
2506
  if (pendingUpgrades.length > 0) {
@@ -2279,11 +2607,16 @@ var KyselyWriteCache = class KyselyWriteCache {
2279
2607
  if ((await this.operationStore.getSince(documentId, "document", branch, docScopeNextIndex - 1, void 0, void 0, signal)).results.length > 0) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
2280
2608
  const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
2281
2609
  let document = copyDocument(baseDocument);
2610
+ let needsFullReplay = false;
2282
2611
  try {
2283
2612
  const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
2284
2613
  for (const operation of pagedResults.results) {
2285
2614
  if (signal?.aborted) throw new Error("Operation aborted");
2286
2615
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
2616
+ if (operation.skip > 0) {
2617
+ needsFullReplay = true;
2618
+ break;
2619
+ }
2287
2620
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2288
2621
  else {
2289
2622
  const protocolVersion = baseReducerVersion(document.header);
@@ -2299,6 +2632,7 @@ var KyselyWriteCache = class KyselyWriteCache {
2299
2632
  } catch (err) {
2300
2633
  throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2301
2634
  }
2635
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2302
2636
  const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
2303
2637
  document.header.revision = revisions.revision;
2304
2638
  if (targetRevision !== void 0) document.header.revision = {
@@ -2794,52 +3128,6 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2794
3128
  return operations.map((operation) => reasons.get(operation.id));
2795
3129
  }
2796
3130
  //#endregion
2797
- //#region src/cache/operation-index-types.ts
2798
- const DRIVE_COLLECTION_PREFIX = "drive.";
2799
- /**
2800
- * Identifies the collection a remote synchronizes. Collections are drive-level
2801
- * abstractions (document-drive and reactor-drive), so a collection id is the
2802
- * drive document id plus the branch it scopes to rather than an opaque string.
2803
- *
2804
- * The canonical string form (`drive.${branch}.${driveId}`) is produced only by
2805
- * `key` and parsed only by `fromKey`; that string is the wire and storage
2806
- * representation and is byte-for-byte identical to the legacy
2807
- * `driveCollectionId(branch, driveId)` output, so existing `document_collections`
2808
- * rows and persisted remotes remain valid without migration.
2809
- */
2810
- var DriveCollectionId = class DriveCollectionId {
2811
- driveId;
2812
- branch;
2813
- constructor(driveId, branch) {
2814
- this.driveId = driveId;
2815
- this.branch = branch;
2816
- }
2817
- static forDrive(driveId, branch = "main") {
2818
- return new DriveCollectionId(driveId, branch);
2819
- }
2820
- /**
2821
- * The single deserializer for the wire/storage form. `branch` may contain
2822
- * dots, while `driveId` is a dot-free document id, so the drive id is the
2823
- * final dot-delimited segment.
2824
- */
2825
- static fromKey(key) {
2826
- if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
2827
- const rest = key.slice(6);
2828
- const lastDot = rest.lastIndexOf(".");
2829
- if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
2830
- return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
2831
- }
2832
- get key() {
2833
- return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
2834
- }
2835
- toString() {
2836
- return this.key;
2837
- }
2838
- equals(other) {
2839
- return this.driveId === other.driveId && this.branch === other.branch;
2840
- }
2841
- };
2842
- //#endregion
2843
3131
  //#region src/executor/document-action-handler.ts
2844
3132
  var DocumentActionHandler = class {
2845
3133
  registry;
@@ -2847,12 +3135,29 @@ var DocumentActionHandler = class {
2847
3135
  driveContainerTypes;
2848
3136
  featureFlags;
2849
3137
  decisionModel;
2850
- constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel) {
3138
+ protocolSupport;
3139
+ loggedUnregisteredProtocols = /* @__PURE__ */ new Set();
3140
+ constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel, protocolSupport) {
2851
3141
  this.registry = registry;
2852
3142
  this.logger = logger;
2853
3143
  this.driveContainerTypes = driveContainerTypes;
2854
3144
  this.featureFlags = featureFlags;
2855
3145
  this.decisionModel = decisionModel;
3146
+ this.protocolSupport = protocolSupport;
3147
+ }
3148
+ /** Keys this reactor does not register are admitted: it cannot judge them. */
3149
+ unsupportedProtocol(documentId, protocolVersions) {
3150
+ for (const [protocol, version] of Object.entries(protocolVersions ?? {})) {
3151
+ const supported = this.protocolSupport[protocol];
3152
+ if (supported === void 0) {
3153
+ if (!this.loggedUnregisteredProtocols.has(protocol)) {
3154
+ this.loggedUnregisteredProtocols.add(protocol);
3155
+ this.logger.info("Admitting document @documentId with unregistered protocol @protocol @version", documentId, protocol, version);
3156
+ }
3157
+ continue;
3158
+ }
3159
+ if (!supported.includes(version)) return new UnsupportedProtocolVersionError(documentId, protocol, version);
3160
+ }
2856
3161
  }
2857
3162
  /** Whether the write arrives with its evaluation already decided. */
2858
3163
  alreadyEvaluated(executing) {
@@ -2968,6 +3273,9 @@ var DocumentActionHandler = class {
2968
3273
  error: /* @__PURE__ */ new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
2969
3274
  duration: Date.now() - startTime
2970
3275
  };
3276
+ const input = action.input;
3277
+ const unsupported = this.unsupportedProtocol(input.documentId, input.protocolVersions);
3278
+ if (unsupported) return buildErrorResult(job, unsupported, startTime);
2971
3279
  const document = createDocumentFromAction(action);
2972
3280
  let operation = createOperation(action, 0, skip, {
2973
3281
  documentId: document.header.id,
@@ -4006,6 +4314,7 @@ var SimpleJobExecutor = class {
4006
4314
  this.collectionMembershipCache = collectionMembershipCache;
4007
4315
  this.driveContainerTypes = driveContainerTypes;
4008
4316
  this.signer = signer ?? new PassthroughSigner();
4317
+ this.featureFlags = resolveFeatureFlags(config.featureFlags);
4009
4318
  this.config = {
4010
4319
  featureFlags: config.featureFlags ?? {},
4011
4320
  maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
@@ -4016,9 +4325,9 @@ var SimpleJobExecutor = class {
4016
4325
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
4017
4326
  yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
4018
4327
  batchApplies: config.batchApplies ?? true,
4019
- signatureVerification: config.signatureVerification ?? "enforce"
4328
+ signatureVerification: config.signatureVerification ?? "enforce",
4329
+ protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols
4020
4330
  };
4021
- this.featureFlags = resolveFeatureFlags(config.featureFlags);
4022
4331
  this.decisionModel = selectDecisionModel(this.featureFlags, registry);
4023
4332
  this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
4024
4333
  signer: this.signer,
@@ -4026,7 +4335,7 @@ var SimpleJobExecutor = class {
4026
4335
  policy: trustPolicy,
4027
4336
  timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
4028
4337
  });
4029
- this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
4338
+ this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel, this.config.protocolSupport);
4030
4339
  this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
4031
4340
  }
4032
4341
  /**
@@ -4092,6 +4401,8 @@ var SimpleJobExecutor = class {
4092
4401
  async executeInScope(params) {
4093
4402
  const { job, startTime, stores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
4094
4403
  let pendingEvent;
4404
+ const unsupported = await this.unsupportedStoredProtocol(job, stores, signal);
4405
+ if (unsupported) return { result: buildErrorResult(job, unsupported, startTime) };
4095
4406
  const indexTxn = stores.operationIndex.start();
4096
4407
  if (job.kind === "load") {
4097
4408
  const loadResult = await this.executeLoadJob({
@@ -4210,6 +4521,16 @@ var SimpleJobExecutor = class {
4210
4521
  for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
4211
4522
  return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
4212
4523
  }
4524
+ /** A stored document at a version this reactor does not run is read-only here. */
4525
+ async unsupportedStoredProtocol(job, stores, signal) {
4526
+ let versions;
4527
+ try {
4528
+ versions = this.featureFlags.documentDecisions && job.kind !== "load" ? (await stores.writeCache.getState(job.documentId, "document", job.branch, void 0, signal)).header.protocolVersions : (await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal)).protocolVersions;
4529
+ } catch {
4530
+ return;
4531
+ }
4532
+ return this.documentActionHandler.unsupportedProtocol(job.documentId, versions);
4533
+ }
4213
4534
  /**
4214
4535
  * A successful mutation's result and the write-ready event it owes, carrying
4215
4536
  * what earlier attempts committed ahead of what this one wrote.
@@ -5841,8 +6162,8 @@ function createForwardingPoolInstrumentation(name) {
5841
6162
  }
5842
6163
  //#endregion
5843
6164
  //#region src/storage/migrations/001_create_operation_table.ts
5844
- var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
5845
- async function up$19(db) {
6165
+ var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
6166
+ async function up$22(db) {
5846
6167
  await db.schema.createTable("Operation").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("jobId", "text", (col) => col.notNull()).addColumn("opId", "text", (col) => col.notNull()).addColumn("prevOpId", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("timestampUtcMs", "timestamptz", (col) => col.notNull()).addColumn("index", "integer", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("error", "text").addColumn("hash", "text", (col) => col.notNull()).addUniqueConstraint("unique_revision", [
5847
6168
  "documentId",
5848
6169
  "scope",
@@ -5867,8 +6188,8 @@ async function up$19(db) {
5867
6188
  }
5868
6189
  //#endregion
5869
6190
  //#region src/storage/migrations/002_create_keyframe_table.ts
5870
- var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
5871
- async function up$18(db) {
6191
+ var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
6192
+ async function up$21(db) {
5872
6193
  await db.schema.createTable("Keyframe").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("revision", "integer", (col) => col.notNull()).addColumn("document", "jsonb", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_keyframe", [
5873
6194
  "documentId",
5874
6195
  "scope",
@@ -5884,14 +6205,14 @@ async function up$18(db) {
5884
6205
  }
5885
6206
  //#endregion
5886
6207
  //#region src/storage/migrations/003_create_document_table.ts
5887
- var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
5888
- async function up$17(db) {
6208
+ var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
6209
+ async function up$20(db) {
5889
6210
  await db.schema.createTable("Document").addColumn("id", "text", (col) => col.primaryKey()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5890
6211
  }
5891
6212
  //#endregion
5892
6213
  //#region src/storage/migrations/004_create_document_relationship_table.ts
5893
- var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
5894
- async function up$16(db) {
6214
+ var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
6215
+ async function up$19(db) {
5895
6216
  await db.schema.createTable("DocumentRelationship").addColumn("id", "text", (col) => col.primaryKey()).addColumn("sourceId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("targetId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("relationshipType", "text", (col) => col.notNull()).addColumn("metadata", "jsonb").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_source_target_type", [
5896
6217
  "sourceId",
5897
6218
  "targetId",
@@ -5903,14 +6224,14 @@ async function up$16(db) {
5903
6224
  }
5904
6225
  //#endregion
5905
6226
  //#region src/storage/migrations/005_create_indexer_state_table.ts
5906
- var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
5907
- async function up$15(db) {
6227
+ var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
6228
+ async function up$18(db) {
5908
6229
  await db.schema.createTable("IndexerState").addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()).addColumn("lastOperationId", "integer", (col) => col.notNull()).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5909
6230
  }
5910
6231
  //#endregion
5911
6232
  //#region src/storage/migrations/006_create_document_snapshot_table.ts
5912
- var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
5913
- async function up$14(db) {
6233
+ var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
6234
+ async function up$17(db) {
5914
6235
  await db.schema.createTable("DocumentSnapshot").addColumn("id", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("slug", "text").addColumn("name", "text").addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("content", "jsonb", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("lastOperationIndex", "integer", (col) => col.notNull()).addColumn("lastOperationHash", "text", (col) => col.notNull()).addColumn("lastUpdatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("snapshotVersion", "integer", (col) => col.notNull().defaultTo(1)).addColumn("identifiers", "jsonb").addColumn("metadata", "jsonb").addColumn("isDeleted", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("deletedAt", "timestamptz").addUniqueConstraint("unique_doc_scope_branch", [
5915
6236
  "documentId",
5916
6237
  "scope",
@@ -5931,8 +6252,8 @@ async function up$14(db) {
5931
6252
  }
5932
6253
  //#endregion
5933
6254
  //#region src/storage/migrations/007_create_slug_mapping_table.ts
5934
- var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
5935
- async function up$13(db) {
6255
+ var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
6256
+ async function up$16(db) {
5936
6257
  await db.schema.createTable("SlugMapping").addColumn("slug", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_docid_scope_branch", [
5937
6258
  "documentId",
5938
6259
  "scope",
@@ -5942,14 +6263,14 @@ async function up$13(db) {
5942
6263
  }
5943
6264
  //#endregion
5944
6265
  //#region src/storage/migrations/008_create_view_state_table.ts
5945
- var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
5946
- async function up$12(db) {
6266
+ var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
6267
+ async function up$15(db) {
5947
6268
  await db.schema.createTable("ViewState").addColumn("readModelId", "text", (col) => col.primaryKey()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(0)).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5948
6269
  }
5949
6270
  //#endregion
5950
6271
  //#region src/storage/migrations/009_create_operation_index_tables.ts
5951
- var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
5952
- async function up$11(db) {
6272
+ var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
6273
+ async function up$14(db) {
5953
6274
  await db.schema.createTable("document_collections").addColumn("documentId", "text", (col) => col.notNull()).addColumn("collectionId", "text", (col) => col.notNull()).addColumn("joinedOrdinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("leftOrdinal", "bigint").addPrimaryKeyConstraint("document_collections_pkey", ["documentId", "collectionId"]).execute();
5954
6275
  await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
5955
6276
  await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
@@ -5963,8 +6284,8 @@ async function up$11(db) {
5963
6284
  }
5964
6285
  //#endregion
5965
6286
  //#region src/storage/migrations/010_create_sync_tables.ts
5966
- var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
5967
- async function up$10(db) {
6287
+ var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
6288
+ async function up$13(db) {
5968
6289
  await db.schema.createTable("sync_remotes").addColumn("name", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("channel_type", "text", (col) => col.notNull()).addColumn("channel_id", "text", (col) => col.notNull().defaultTo("")).addColumn("remote_name", "text", (col) => col.notNull().defaultTo("")).addColumn("channel_parameters", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("filter_document_ids", "jsonb").addColumn("filter_scopes", "jsonb").addColumn("filter_branch", "text", (col) => col.notNull().defaultTo("main")).addColumn("push_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("push_last_success_utc_ms", "text").addColumn("push_last_failure_utc_ms", "text").addColumn("push_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("pull_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("pull_last_success_utc_ms", "text").addColumn("pull_last_failure_utc_ms", "text").addColumn("pull_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5969
6290
  await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
5970
6291
  await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.primaryKey().references("sync_remotes.name").onDelete("cascade")).addColumn("cursor_ordinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_synced_at_utc_ms", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
@@ -5972,8 +6293,8 @@ async function up$10(db) {
5972
6293
  }
5973
6294
  //#endregion
5974
6295
  //#region src/storage/migrations/011_add_cursor_type_column.ts
5975
- var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
5976
- async function up$9(db) {
6296
+ var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
6297
+ async function up$12(db) {
5977
6298
  await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
5978
6299
  await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
5979
6300
  await db.schema.dropTable("sync_cursors").execute();
@@ -5982,64 +6303,64 @@ async function up$9(db) {
5982
6303
  }
5983
6304
  //#endregion
5984
6305
  //#region src/storage/migrations/012_add_source_remote_column.ts
5985
- var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
5986
- async function up$8(db) {
6306
+ var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
6307
+ async function up$11(db) {
5987
6308
  await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
5988
6309
  }
5989
6310
  //#endregion
5990
6311
  //#region src/storage/migrations/013_create_sync_dead_letters_table.ts
5991
- var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
5992
- async function up$7(db) {
6312
+ var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
6313
+ async function up$10(db) {
5993
6314
  await db.schema.createTable("sync_dead_letters").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("id", "text", (col) => col.unique().notNull()).addColumn("job_id", "text", (col) => col.notNull()).addColumn("job_dependencies", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("scopes", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("branch", "text", (col) => col.notNull()).addColumn("operations", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("error_source", "text", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5994
6315
  await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
5995
6316
  }
5996
6317
  //#endregion
5997
6318
  //#region src/storage/migrations/014_create_processor_cursor_table.ts
5998
- var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
5999
- async function up$6(db) {
6319
+ var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
6320
+ async function up$9(db) {
6000
6321
  await db.schema.createTable("ProcessorCursor").addColumn("processorId", "text", (col) => col.primaryKey()).addColumn("factoryId", "text", (col) => col.notNull()).addColumn("driveId", "text", (col) => col.notNull()).addColumn("processorIndex", "integer", (col) => col.notNull()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(sql`0`)).addColumn("status", "text", (col) => col.notNull().defaultTo(sql`'active'`)).addColumn("lastError", "text").addColumn("lastErrorTimestamp", "timestamptz").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
6001
6322
  }
6002
6323
  //#endregion
6003
6324
  //#region src/storage/migrations/015_add_operation_denied_reason.ts
6004
6325
  var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
6005
- down: () => down$5,
6006
- up: () => up$5
6326
+ down: () => down$8,
6327
+ up: () => up$8
6007
6328
  });
6008
6329
  /**
6009
6330
  * Records why authorization refused an operation. Separate from `error` so a
6010
6331
  * denial is distinguishable from a reducer failure without matching on a
6011
6332
  * message. Null for every operation written before decisions were enforced.
6012
6333
  */
6013
- async function up$5(db) {
6334
+ async function up$8(db) {
6014
6335
  await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
6015
6336
  await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
6016
6337
  }
6017
- async function down$5(db) {
6338
+ async function down$8(db) {
6018
6339
  await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
6019
6340
  await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
6020
6341
  }
6021
6342
  //#endregion
6022
6343
  //#region src/storage/migrations/016_add_dead_letter_error_type.ts
6023
6344
  var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
6024
- down: () => down$4,
6025
- up: () => up$4
6345
+ down: () => down$7,
6346
+ up: () => up$7
6026
6347
  });
6027
6348
  /**
6028
6349
  * The classification a dead letter falls into, stored because it decides whether
6029
6350
  * the document stays quarantined and the in-memory error is gone after a restart.
6030
6351
  * Defaulted rather than nullable, so a pre-existing row rehydrates.
6031
6352
  */
6032
- async function up$4(db) {
6353
+ async function up$7(db) {
6033
6354
  await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
6034
6355
  }
6035
- async function down$4(db) {
6356
+ async function down$7(db) {
6036
6357
  await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
6037
6358
  }
6038
6359
  //#endregion
6039
6360
  //#region src/storage/migrations/017_create_group_references.ts
6040
6361
  var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
6041
- down: () => down$3,
6042
- up: () => up$3
6362
+ down: () => down$6,
6363
+ up: () => up$6
6043
6364
  });
6044
6365
  /**
6045
6366
  * One row per (document, group) reference ever discovered from an auth
@@ -6050,18 +6371,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
6050
6371
  * requires (sync), and by groupId for the documents a group change affects
6051
6372
  * (re-evaluation).
6052
6373
  */
6053
- async function up$3(db) {
6374
+ async function up$6(db) {
6054
6375
  await db.schema.createTable("group_references").addColumn("documentId", "text", (col) => col.notNull()).addColumn("groupId", "text", (col) => col.notNull()).addPrimaryKeyConstraint("group_references_pkey", ["documentId", "groupId"]).execute();
6055
6376
  await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
6056
6377
  }
6057
- async function down$3(db) {
6378
+ async function down$6(db) {
6058
6379
  await db.schema.dropTable("group_references").execute();
6059
6380
  }
6060
6381
  //#endregion
6061
6382
  //#region src/storage/migrations/018_add_sync_remote_bound_address.ts
6062
6383
  var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
6063
- down: () => down$2,
6064
- up: () => up$2
6384
+ down: () => down$5,
6385
+ up: () => up$5
6065
6386
  });
6066
6387
  /**
6067
6388
  * The address a sync channel is bound to, so a channel created by one subject
@@ -6072,17 +6393,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
6072
6393
  * until its first authenticated poll adopts it. A default would claim them all
6073
6394
  * for one address.
6074
6395
  */
6075
- async function up$2(db) {
6396
+ async function up$5(db) {
6076
6397
  await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
6077
6398
  }
6078
- async function down$2(db) {
6399
+ async function down$5(db) {
6079
6400
  await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
6080
6401
  }
6081
6402
  //#endregion
6082
6403
  //#region src/storage/migrations/019_require_action_id.ts
6083
6404
  var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
6084
- down: () => down$1,
6085
- up: () => up$1
6405
+ down: () => down$4,
6406
+ up: () => up$4
6086
6407
  });
6087
6408
  /**
6088
6409
  * Makes an operation whose action carries no id physically unstorable.
@@ -6109,7 +6430,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
6109
6430
  * The empty string is rejected alongside null. It derives the same colliding
6110
6431
  * operation id as an absent id, so admitting it would leave the hole open.
6111
6432
  */
6112
- async function up$1(db) {
6433
+ async function up$4(db) {
6113
6434
  await db.updateTable("Operation").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
6114
6435
  await db.updateTable("operation_index_operations as oio").from("Operation as op").set({ action: sql`jsonb_set(oio.action, '{id}', to_jsonb(op.action->>'id'))` }).whereRef("oio.opId", "=", "op.opId").whereRef("oio.index", "=", "op.index").whereRef("oio.skip", "=", "op.skip").where(sql`jsonb_typeof(oio.action) = 'object' and coalesce(oio.action->>'id', '') = ''`).where(sql`coalesce(op.action->>'id', '') <> ''`).execute();
6115
6436
  await db.updateTable("operation_index_operations").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
@@ -6121,22 +6442,72 @@ async function up$1(db) {
6121
6442
  * their operations are now known by, and reverting them would reintroduce the
6122
6443
  * collision the migration removed.
6123
6444
  */
6124
- async function down$1(db) {
6445
+ async function down$4(db) {
6125
6446
  await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
6126
6447
  await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
6127
6448
  }
6128
6449
  //#endregion
6129
6450
  //#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
6130
6451
  var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
6452
+ down: () => down$3,
6453
+ up: () => up$3
6454
+ });
6455
+ /** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
6456
+ async function up$3(db) {
6457
+ await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
6458
+ }
6459
+ async function down$3(db) {
6460
+ await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
6461
+ }
6462
+ //#endregion
6463
+ //#region src/storage/migrations/021_add_sync_remote_peer.ts
6464
+ var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
6465
+ down: () => down$2,
6466
+ up: () => up$2
6467
+ });
6468
+ /**
6469
+ * The peer's manifest as canonical JSON, and when it was heard. Both null: not
6470
+ * yet heard. A time with no manifest: a silent peer, which gets the baselines.
6471
+ */
6472
+ async function up$2(db) {
6473
+ await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
6474
+ }
6475
+ async function down$2(db) {
6476
+ await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
6477
+ }
6478
+ //#endregion
6479
+ //#region src/storage/migrations/022_create_sync_holds.ts
6480
+ var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
6481
+ down: () => down$1,
6482
+ up: () => up$1
6483
+ });
6484
+ /** Documents held back from a remote whose peer cannot run their versions. */
6485
+ async function up$1(db) {
6486
+ await db.schema.createTable("sync_holds").addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("protocol", "text", (col) => col.notNull()).addColumn("version", "integer", (col) => col.notNull()).addColumn("held_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_holds_pkey", [
6487
+ "remote_name",
6488
+ "document_id",
6489
+ "branch"
6490
+ ]).execute();
6491
+ }
6492
+ async function down$1(db) {
6493
+ await db.schema.dropTable("sync_holds").execute();
6494
+ }
6495
+ //#endregion
6496
+ //#region src/storage/migrations/023_index_created_protocol_versions.ts
6497
+ var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
6131
6498
  down: () => down,
6132
6499
  up: () => up
6133
6500
  });
6134
- /** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
6501
+ /**
6502
+ * One entry per document created with protocolVersions, keyed by a hash so an
6503
+ * oversized header cannot make its creation unstorable. Startup reads the
6504
+ * distinct values without visiting every document.
6505
+ */
6135
6506
  async function up(db) {
6136
- await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
6507
+ await db.schema.createIndex("idx_operation_created_protocol_versions").on("Operation").expression(sql`md5((action->'input'->'protocolVersions')::text)`).where(sql`scope = 'document' and "index" = 0 and (action->'input'->'protocolVersions') is not null`).execute();
6137
6508
  }
6138
6509
  async function down(db) {
6139
- await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
6510
+ await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
6140
6511
  }
6141
6512
  //#endregion
6142
6513
  //#region src/storage/migrations/migrator.ts
@@ -6161,7 +6532,10 @@ const migrations = {
6161
6532
  "017_create_group_references": _017_create_group_references_exports,
6162
6533
  "018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
6163
6534
  "019_require_action_id": _019_require_action_id_exports,
6164
- "020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports
6535
+ "020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
6536
+ "021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
6537
+ "022_create_sync_holds": _022_create_sync_holds_exports,
6538
+ "023_index_created_protocol_versions": _023_index_created_protocol_versions_exports
6165
6539
  };
6166
6540
  var ProgrammaticMigrationProvider = class {
6167
6541
  getMigrations() {
@@ -6361,6 +6735,6 @@ function createForwardingLogger(post, level = "info", tags = []) {
6361
6735
  };
6362
6736
  }
6363
6737
  //#endregion
6364
- export { DocumentNotFoundError as $, ModuleNotFoundError as A, authDecisionModel as B, DocumentMetaCache as C, DuplicateManifestError as D, JobExecutorEventTypes as E, summarizeSubmittedActions as F, DocumentExistence as G, APPEND_CONDITION_FAILED_PREFIX as H, targetDocumentId as I, RevisionMismatchError as J, DuplicateOperationError as K, decideAtHead as L, createConsistencyToken as M, createEmptyConsistencyToken as N, DuplicateModuleError as O, submittedActionIds as P, DocumentDeletedError as Q, selectDecisionModel as R, KyselyOperationIndex as S, DEFAULT_DEFERRED_JOB_TTL_MS as T, AppendConditionFailedError as U, buildDecisionModel as V, DocumentAlreadyExistsError as W, AuthTimestampNotMonotonicError as X, AuthEnforcementDisabledError as Y, AuthorizationDeniedError as Z, DriveCollectionId as _, DEFAULT_DRIVE_CONTAINER_TYPES as a, EventBusAggregateError as at, EventBus as b, runMigrations as c, parsePagingOptions as ct, KyselyOperationStore as d, __exportAll as dt, ExcessiveReshuffleError as et, KyselyKeyframeStore as f, PassthroughSigner as g, verifyActionSignature as h, fromErrorInfo as i, UpgradePreconditionFailedError as it, GATED_DOCUMENT_ACTIONS as j, InvalidModuleError as k, createForwardingPoolInstrumentation as l, throwIfAborted as lt, SimpleJobExecutor as m, errorToInfo as n, InvalidSignatureError as nt, REACTOR_SCHEMA as o, ReactorEventTypes as ot, DocumentModelRegistry as p, OptimisticLockError as q, sanitizeArg as r, RelationshipNotFoundError as rt, getMigrationStatus as s, matchesScope as st, createForwardingLogger as t, InvalidOperationTimestampError as tt, instrumentPgPool as u, yieldToMain as ut, KyselyExecutionScope as v, CollectionMembershipCache as w, KyselyWriteCache as x, resolveFeatureFlags as y, documentDecisionModel as z };
6738
+ export { OptimisticLockError as $, DEFAULT_DEFERRED_JOB_TTL_MS as A, summarizeSubmittedActions as B, SettledWatermark as C, snapshotFunctionsFor as D, parseSnapshot as E, ModuleNotFoundError as F, authDecisionModel as G, decideAtHead as H, GATED_DOCUMENT_ACTIONS as I, APPEND_CONDITION_FAILED_PREFIX as J, buildDecisionModel as K, createConsistencyToken as L, DuplicateManifestError as M, DuplicateModuleError as N, DocumentMetaCache as O, InvalidModuleError as P, DuplicateOperationError as Q, createEmptyConsistencyToken as R, ProbeSettler as S, describeWaitingSessions as T, selectDecisionModel as U, targetDocumentId as V, documentDecisionModel as W, DocumentAlreadyExistsError as X, AppendConditionFailedError as Y, DocumentExistence as Z, KyselyExecutionScope as _, throwIfAborted as _t, DEFAULT_DRIVE_CONTAINER_TYPES as a, DocumentNotFoundError as at, KyselyWriteCache as b, runMigrations as c, InvalidSignatureError as ct, KyselyOperationStore as d, UnsupportedStoredProtocolError as dt, RevisionMismatchError as et, KyselyKeyframeStore as f, UpgradePreconditionFailedError as ft, PassthroughSigner as g, parsePagingOptions as gt, verifyActionSignature as h, matchesScope as ht, fromErrorInfo as i, DocumentDeletedError as it, JobExecutorEventTypes as j, CollectionMembershipCache as k, createForwardingPoolInstrumentation as l, RelationshipNotFoundError as lt, SimpleJobExecutor as m, ReactorEventTypes as mt, errorToInfo as n, AuthTimestampNotMonotonicError as nt, REACTOR_SCHEMA as o, ExcessiveReshuffleError as ot, DocumentModelRegistry as p, EventBusAggregateError as pt, DriveCollectionId as q, sanitizeArg as r, AuthorizationDeniedError as rt, getMigrationStatus as s, InvalidOperationTimestampError as st, createForwardingLogger as t, AuthEnforcementDisabledError as tt, instrumentPgPool as u, UnsupportedProtocolVersionError as ut, resolveFeatureFlags as v, yieldToMain as vt, createKyselyWatermarkProbe as w, KyselyOperationIndex as x, EventBus as y, __exportAll as yt, submittedActionIds as z };
6365
6739
 
6366
- //# sourceMappingURL=forwarding-logger-CnuVDVze.js.map
6740
+ //# sourceMappingURL=forwarding-logger-BkwlvD9W.js.map