@powerhousedao/reactor 6.2.3-dev.50 → 6.2.3-dev.52

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.
Files changed (30) hide show
  1. package/dist/{document-indexer-DwAMDVRI.js → document-indexer-3fkVWCaN.js} +26 -14
  2. package/dist/document-indexer-3fkVWCaN.js.map +1 -0
  3. package/dist/{drive-container-types-Ci1pdv4X.js → drive-container-types-BAfFzBOf.js} +133 -7
  4. package/dist/drive-container-types-BAfFzBOf.js.map +1 -0
  5. package/dist/entry.js +2 -2
  6. package/dist/{forwarding-logger-ByfBFgMS.js → forwarding-logger-B9LLXItm.js} +111 -79
  7. package/dist/forwarding-logger-B9LLXItm.js.map +1 -0
  8. package/dist/{index-CNhT69De.d.ts → index-dDm0fUBT.d.ts} +518 -68
  9. package/dist/index-dDm0fUBT.d.ts.map +1 -0
  10. package/dist/index.d.ts +1040 -138
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3110 -776
  13. package/dist/index.js.map +1 -1
  14. package/dist/projection-entry.js +2 -2
  15. package/dist/{projection-shard-manager-CQ5FwQmv.js → projection-shard-manager-Drl9w8Eo.js} +3 -13
  16. package/dist/projection-shard-manager-Drl9w8Eo.js.map +1 -0
  17. package/dist/rpc.d.ts +32 -6
  18. package/dist/rpc.d.ts.map +1 -1
  19. package/dist/rpc.js +8 -4
  20. package/dist/rpc.js.map +1 -1
  21. package/dist/{worker-handle-CN2-oRv-.js → worker-handle-Nso8i5eU.js} +2 -2
  22. package/dist/{worker-handle-CN2-oRv-.js.map → worker-handle-Nso8i5eU.js.map} +1 -1
  23. package/dist/worker-pool-router-BROOEQSE.js +35 -0
  24. package/dist/worker-pool-router-BROOEQSE.js.map +1 -0
  25. package/package.json +3 -3
  26. package/dist/document-indexer-DwAMDVRI.js.map +0 -1
  27. package/dist/drive-container-types-Ci1pdv4X.js.map +0 -1
  28. package/dist/forwarding-logger-ByfBFgMS.js.map +0 -1
  29. package/dist/index-CNhT69De.d.ts.map +0 -1
  30. package/dist/projection-shard-manager-CQ5FwQmv.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { $ as DocumentExistence, A as GATED_DOCUMENT_ACTIONS, C as describeWaitingSessions, Ct as matchesScope, D as CollectionMembershipCache, Dt as __exportAll, E as DocumentMetaCache, J as targetDocumentId, K as submittedActionIds, L as createConsistencyToken, Q as DocumentAlreadyExistsError, R as createDocumentFromAction, S as createKyselyWatermarkProbe, St as ReactorEventTypes, T as snapshotFunctionsFor, Tt as throwIfAborted, X as APPEND_CONDITION_FAILED_PREFIX, Z as AppendConditionFailedError, _ as acquirePurgeLocks, _t as ReservedActionError, a as REACTOR_SCHEMA, at as AuthorizationDeniedError, b as ProbeSettler, bt as UpgradePreconditionFailedError, c as createForwardingPoolInstrumentation, ct as DocumentNotDeletedError, d as KyselyKeyframeStore, dt as ExcessiveReshuffleError, et as DuplicateOperationError, f as EventBus, ft as GroupInUseError, g as PURGE_NS, gt as RelationshipNotFoundError, h as PURGE_LOCK_BUCKETS, ht as PurgeTooLargeError, it as AuthTimestampNotMonotonicError, l as instrumentPgPool, lt as DocumentNotFoundError, m as KyselyOperationIndex, mt as InvalidSignatureError, n as errorToInfo, nt as RevisionMismatchError, o as getMigrationStatus, ot as BatchJobFailedError, p as KyselyWriteCache, pt as InvalidOperationTimestampError, q as summarizeSubmittedActions, r as sanitizeArg, rt as AuthEnforcementDisabledError, s as runMigrations, st as DocumentDeletedError, t as createForwardingLogger, tt as OptimisticLockError, u as KyselyOperationStore, ut as DocumentPurgedError, v as findPurged, vt as UnsupportedProtocolVersionError, w as parseSnapshot, wt as parsePagingOptions, x as SettledWatermark, xt as EventBusAggregateError, y as listPurged, yt as UnsupportedStoredProtocolError, z as createEmptyConsistencyToken } from "./forwarding-logger-ByfBFgMS.js";
2
- import { _ as buildDecisionModel, a as KyselyExecutionScope, c as appliedDelete, d as DEFAULT_MAX_PURGE_OPERATIONS, f as JobExecutorEventTypes, g as authDecisionModel, h as documentDecisionModel, i as PassthroughSigner, l as resolveFeatureFlags, m as selectDecisionModel, n as SimpleJobExecutor, o as DEFAULT_PURGE_DELETE_BATCH, p as decideAtHead, r as verifyActionSignature, s as KyselyDocumentPurger, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as DEFAULT_DEFERRED_JOB_TTL_MS, v as DriveCollectionId } from "./drive-container-types-Ci1pdv4X.js";
1
+ import { $ as DocumentExistence, A as GATED_DOCUMENT_ACTIONS, C as describeWaitingSessions, Ct as ReactorEventTypes, D as CollectionMembershipCache, Dt as parsePagingOptions, E as DocumentMetaCache, Et as matchesScope, J as targetDocumentId, K as submittedActionIds, L as createConsistencyToken, Ot as throwIfAborted, Q as DocumentAlreadyExistsError, R as createDocumentFromAction, S as createKyselyWatermarkProbe, St as EventBusAggregateError, T as snapshotFunctionsFor, X as APPEND_CONDITION_FAILED_PREFIX, Z as AppendConditionFailedError, _ as acquirePurgeLocks, _t as RelationshipNotFoundError, a as REACTOR_SCHEMA, at as AuthorizationDeniedError, b as ProbeSettler, bt as UnsupportedStoredProtocolError, c as createForwardingPoolInstrumentation, ct as DocumentNotDeletedError, d as KyselyKeyframeStore, dt as ExcessiveReshuffleError, et as DuplicateOperationError, f as EventBus, ft as GroupInUseError, g as PURGE_NS, gt as PurgeTooLargeError, h as PURGE_LOCK_BUCKETS, ht as JOB_NOT_FOUND_ERROR_NAME, it as AuthTimestampNotMonotonicError, jt as __exportAll, kt as withDeadline, l as instrumentPgPool, lt as DocumentNotFoundError, m as KyselyOperationIndex, mt as InvalidSignatureError, n as errorToInfo, nt as RevisionMismatchError, o as getMigrationStatus, ot as BatchJobFailedError, p as KyselyWriteCache, pt as InvalidOperationTimestampError, q as summarizeSubmittedActions, r as sanitizeArg, rt as AuthEnforcementDisabledError, s as runMigrations, st as DocumentDeletedError, t as createForwardingLogger, tt as OptimisticLockError, u as KyselyOperationStore, ut as DocumentPurgedError, v as findPurged, vt as ReservedActionError, w as parseSnapshot, wt as TIMED_OUT, x as SettledWatermark, xt as UpgradePreconditionFailedError, y as listPurged, yt as UnsupportedProtocolVersionError, z as createEmptyConsistencyToken } from "./forwarding-logger-B9LLXItm.js";
2
+ import { C as PropagationMode, S as JobStatus, T as cursorProtectedLoadMeta, _ as selectDecisionModel, a as NoopStorageFlusher, b as buildDecisionModel, c as DEFAULT_PURGE_DELETE_BATCH, d as resolveFeatureFlags, f as DEFAULT_DEFERRED_JOB_TTL_MS, g as decideAtHead, h as JobExecutorEventTypes, i as PassthroughSigner, l as KyselyDocumentPurger, m as DEFAULT_MAX_PURGE_OPERATIONS, n as SimpleJobExecutor, o as StoragePoisonedError, p as DEFAULT_JOB_TIMEOUT_MS, r as verifyActionSignature, s as KyselyExecutionScope, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as appliedDelete, v as documentDecisionModel, w as RelationshipChangeType, x as DriveCollectionId, y as authDecisionModel } from "./drive-container-types-BAfFzBOf.js";
3
3
  import { a as DuplicateModuleError, c as ModuleNotFoundError, i as DuplicateManifestError, n as loadDocumentModelSpec, o as InvalidModuleError, r as DocumentModelRegistry, s as ModelNotWorkerImportableError } from "./load-spec-ZLjuyhxz.js";
4
4
  import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-BM_ULVWd.js";
5
- import { _ as ContiguousCursor, a as KyselyDocumentView, b as isCatchUpConsumer, c as ReadModelCoordinator, d as reserveBatch, f as BaseReadModel, g as unchunkedReadModelIndexingConfig, h as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as indexReserved, m as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as DEFAULT_COMMIT_CHUNK_SIZE, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as releaseBatch, v as defaultCatchUpConfig, y as CatchUpScheduler } from "./document-indexer-DwAMDVRI.js";
5
+ import { n as hashDocumentId, t as bucketFor } from "./worker-pool-router-BROOEQSE.js";
6
+ import { S as isCatchUpConsumer, _ as defaultReadModelIndexingConfig, a as KyselyDocumentView, b as defaultCatchUpConfig, c as ReadModelCoordinator, d as releaseBatch, f as reserveBatch, g as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, h as DEFAULT_COMMIT_CHUNK_SIZE, i as DeletedDocumentRead, l as indexReserved, m as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as spliceReadModel, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as indexStage, v as unchunkedReadModelIndexingConfig, x as CatchUpScheduler, y as ContiguousCursor } from "./document-indexer-3fkVWCaN.js";
6
7
  import { DEFAULT_SIGNATURE_POLICY, DOCUMENT_PURGE_PROTOCOL, DowngradeNotSupportedError, MARKER_REFUSAL_FEATURE, MAX_AUTH_GRANTS, PEER_CAPABILITIES, PURGE_DOCUMENT, UnsupportedDocumentModelVersionError, actionSignerIdentity, actionSigningTarget, actions, actions as documentActions, canonicalJson, coversLocal, createCopyHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, holdReason, isOlderManifest, isPurgeMarker, isPurgeMarker as isPurgeMarker$1, legacySupports, localPeerManifest, localSupports, mergePeerCapabilities, normalizeDocumentModelVersion, peerSupports, purgedProtocolVersions, readPeerManifest, requestedSignaturePolicy, selectProtocolVersions, sortOperations, toTransportAction, withSignaturePolicy } from "@powerhousedao/shared/document-model";
7
8
  import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
8
9
  import { v4 } from "uuid";
9
10
  import { ConsoleLogger, childLogger } from "document-model";
10
- import { CompiledQuery, sql } from "kysely";
11
+ import { CompiledQuery, Kysely, sql } from "kysely";
11
12
  import { PGliteDialect } from "kysely-pglite-dialect";
12
13
  import { RelationalDbProcessor, createRelationalDb } from "@powerhousedao/shared/processors";
13
14
  //#region src/actions/index.ts
@@ -271,49 +272,7 @@ function buildSingleJobMeta(jobId, callerMeta) {
271
272
  };
272
273
  }
273
274
  //#endregion
274
- //#region src/shared/types.ts
275
- /**
276
- * Enum that determines deletion propagation.
277
- */
278
- let PropagationMode = /* @__PURE__ */ function(PropagationMode) {
279
- PropagationMode["None"] = "none";
280
- PropagationMode["Cascade"] = "cascade";
281
- return PropagationMode;
282
- }({});
283
- /**
284
- * Enum that describes the type of relationship change.
285
- */
286
- let RelationshipChangeType = /* @__PURE__ */ function(RelationshipChangeType) {
287
- RelationshipChangeType["Added"] = "added";
288
- RelationshipChangeType["Removed"] = "removed";
289
- return RelationshipChangeType;
290
- }({});
291
- /**
292
- * Job execution statuses
293
- */
294
- let JobStatus = /* @__PURE__ */ function(JobStatus) {
295
- /** Job is queued but not yet started */
296
- JobStatus["PENDING"] = "PENDING";
297
- /** Job is currently being executed */
298
- JobStatus["RUNNING"] = "RUNNING";
299
- /** Operations have been written to the operation store (JOB_WRITE_READY event) */
300
- JobStatus["WRITE_READY"] = "WRITE_READY";
301
- /** Read models have finished indexing operations (JOB_READ_READY event) */
302
- JobStatus["READ_READY"] = "READ_READY";
303
- /** Job failed (may be retried) */
304
- JobStatus["FAILED"] = "FAILED";
305
- return JobStatus;
306
- }({});
307
- //#endregion
308
275
  //#region src/client/drive-client.ts
309
- /**
310
- * Implementation of {@link IDriveClient}.
311
- *
312
- * Holds a back-reference to its parent {@link IReactorClient} for read and
313
- * single-document write primitives, plus direct access to {@link IReactor}
314
- * for batch execution. The back-reference is captured but never invoked
315
- * during construction, so the partial-`this` hazard does not apply.
316
- */
317
276
  /** A copy keeps its source's versions; selection fills only the keys it lacks. */
318
277
  function withSelectedVersions(header, selected) {
319
278
  return {
@@ -327,15 +286,16 @@ function withSelectedVersions(header, selected) {
327
286
  var DriveClient = class {
328
287
  client;
329
288
  logger;
330
- reactor;
331
289
  signer;
332
290
  resolveReference;
333
- constructor(client, logger, reactor, signer, resolveReference) {
291
+ executeBatch;
292
+ /** `submitter` may still be an IReactor, whose executeBatch is used. */
293
+ constructor(client, logger, submitter, signer, resolveReference) {
334
294
  this.client = client;
335
295
  this.logger = logger;
336
- this.reactor = reactor;
337
296
  this.signer = signer;
338
297
  this.resolveReference = resolveReference;
298
+ this.executeBatch = typeof submitter === "function" ? submitter : (request, signal) => submitter.executeBatch(request, signal);
339
299
  }
340
300
  async create(input, signal) {
341
301
  this.logger.verbose("drives.create(@input)", input);
@@ -535,7 +495,7 @@ var DriveClient = class {
535
495
  * collision has to be told apart from a transient failure.
536
496
  */
537
497
  async runJobs(jobs, signal) {
538
- const batchResult = await this.reactor.executeBatch({ jobs }, signal);
498
+ const batchResult = await this.executeBatch({ jobs }, signal);
539
499
  const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
540
500
  for (const job of completedJobs) if (job.status === "FAILED") throw this.toError(job);
541
501
  return completedJobs;
@@ -592,6 +552,139 @@ var DriveClient = class {
592
552
  }
593
553
  };
594
554
  //#endregion
555
+ //#region src/client/types.ts
556
+ /**
557
+ * Describes the types of document changes that can occur.
558
+ */
559
+ let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
560
+ DocumentChangeType["Created"] = "created";
561
+ DocumentChangeType["Deleted"] = "deleted";
562
+ DocumentChangeType["Updated"] = "updated";
563
+ DocumentChangeType["ParentAdded"] = "parent_added";
564
+ DocumentChangeType["ParentRemoved"] = "parent_removed";
565
+ DocumentChangeType["ChildAdded"] = "child_added";
566
+ DocumentChangeType["ChildRemoved"] = "child_removed";
567
+ return DocumentChangeType;
568
+ }({});
569
+ //#endregion
570
+ //#region src/client/derivations.ts
571
+ /** The signed jobs that create `document`, and attach it to `parentId`. */
572
+ async function buildCreateJobs(document, parentId, signer, signal) {
573
+ const documentId = document.header.id;
574
+ const branch = document.header.branch || "main";
575
+ const createInput = {
576
+ model: document.header.documentType,
577
+ version: 0,
578
+ documentId,
579
+ signing: {
580
+ signature: documentId,
581
+ publicKey: document.header.sig.publicKey,
582
+ nonce: document.header.sig.nonce,
583
+ createdAtUtcIso: document.header.createdAtUtcIso,
584
+ documentType: document.header.documentType
585
+ },
586
+ slug: document.header.slug,
587
+ name: document.header.name,
588
+ branch: document.header.branch,
589
+ meta: document.header.meta,
590
+ protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
591
+ };
592
+ const createActions = await signActions([createDocumentAction(createInput), upgradeDocumentAction({
593
+ documentId,
594
+ model: document.header.documentType,
595
+ fromVersion: 0,
596
+ toVersion: normalizeDocumentModelVersion(document.state.document?.version),
597
+ initialState: document.state
598
+ })], signer, {
599
+ documentId,
600
+ branch
601
+ }, signal);
602
+ const jobs = [{
603
+ key: "create",
604
+ documentId,
605
+ scope: getSharedActionScope(createActions),
606
+ branch,
607
+ actions: createActions,
608
+ dependsOn: []
609
+ }];
610
+ if (parentId) {
611
+ const parentActions = await signActions([addRelationshipAction(parentId, documentId, "child")], signer, {
612
+ documentId: parentId,
613
+ branch: "main"
614
+ }, signal);
615
+ jobs.push({
616
+ key: "parent",
617
+ documentId: parentId,
618
+ scope: getSharedActionScope(parentActions),
619
+ branch: "main",
620
+ actions: parentActions,
621
+ dependsOn: ["create"]
622
+ });
623
+ }
624
+ return jobs;
625
+ }
626
+ /** The registered module for a type: the requested version, else the latest. */
627
+ function selectDocumentModelModule(modules, documentModelType, documentModelVersion) {
628
+ const matching = modules.filter((m) => m.documentModel.global.id === documentModelType);
629
+ if (documentModelVersion !== void 0) {
630
+ const requested = normalizeDocumentModelVersion(documentModelVersion);
631
+ const module = matching.find((m) => normalizeDocumentModelVersion(m.version) === requested);
632
+ if (!module) throw new Error(`Document model not found for type: ${documentModelType} with version: ${documentModelVersion}`);
633
+ return module;
634
+ }
635
+ const latest = matching.reduce((best, current) => {
636
+ if (best === void 0) return current;
637
+ return normalizeDocumentModelVersion(current.version) > normalizeDocumentModelVersion(best.version) ? current : best;
638
+ }, void 0);
639
+ if (!latest) throw new Error(`Document model not found for type: ${documentModelType}`);
640
+ return latest;
641
+ }
642
+ /** A new, empty document of `module`, under the create defaults given. */
643
+ function createEmptyDocument(module, options, defaultPolicy, baseVersions) {
644
+ const document = withSignaturePolicy(module.utils.createDocument(), requestedSignaturePolicy(options, defaultPolicy), { protocolVersions: {
645
+ ...baseVersions,
646
+ ...options?.protocolVersions
647
+ } });
648
+ document.state.document.version = normalizeDocumentModelVersion(module.version);
649
+ return document;
650
+ }
651
+ /** Retries from a fresh read when a concurrent edit invalidates the snapshot. */
652
+ async function upgradeDocumentWith(deps, documentIdentifier, toVersion, options, signal) {
653
+ const maxConflictRetries = options?.maxConflictRetries ?? 3;
654
+ let lastConflictMessage = "";
655
+ for (let attempt = 0; attempt <= maxConflictRetries; attempt++) {
656
+ const document = await deps.read(documentIdentifier, void 0, void 0, signal);
657
+ const documentId = document.header.id;
658
+ const documentType = document.header.documentType;
659
+ const branch = document.header.branch || "main";
660
+ const fromVersion = normalizeDocumentModelVersion(document.state.document?.version);
661
+ let targetVersion = toVersion;
662
+ if (targetVersion === void 0) {
663
+ const module = await deps.getDocumentModelModule(documentType);
664
+ targetVersion = normalizeDocumentModelVersion(module.version);
665
+ }
666
+ if (targetVersion === fromVersion) return document;
667
+ if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
668
+ const action = upgradeDocumentAction({
669
+ documentId,
670
+ model: documentType,
671
+ fromVersion,
672
+ toVersion: targetVersion,
673
+ revision: { ...document.header.revision }
674
+ });
675
+ const signedActions = await signActions([action], deps.signer, {
676
+ documentId,
677
+ branch
678
+ }, signal);
679
+ const jobInfo = await deps.submit(documentId, branch, signedActions, signal);
680
+ const completedJob = await deps.waitForJob(jobInfo, signal);
681
+ if (completedJob.status !== "FAILED") return deps.read(documentId, branch, completedJob.consistencyToken, signal);
682
+ if (completedJob.error?.name !== "UpgradePreconditionFailedError") throw new Error(completedJob.error?.message);
683
+ lastConflictMessage = completedJob.error.message;
684
+ }
685
+ throw new Error(`Upgrade of document ${documentIdentifier} conflicted with concurrent edits after ${maxConflictRetries + 1} attempts: ${lastConflictMessage}`);
686
+ }
687
+ //#endregion
595
688
  //#region src/shared/awaiter.ts
596
689
  /**
597
690
  * Checks if a job status is terminal (job has finished).
@@ -726,21 +819,6 @@ function decodeCompositeCursor(cursor) {
726
819
  }
727
820
  }
728
821
  //#endregion
729
- //#region src/client/types.ts
730
- /**
731
- * Describes the types of document changes that can occur.
732
- */
733
- let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
734
- DocumentChangeType["Created"] = "created";
735
- DocumentChangeType["Deleted"] = "deleted";
736
- DocumentChangeType["Updated"] = "updated";
737
- DocumentChangeType["ParentAdded"] = "parent_added";
738
- DocumentChangeType["ParentRemoved"] = "parent_removed";
739
- DocumentChangeType["ChildAdded"] = "child_added";
740
- DocumentChangeType["ChildRemoved"] = "child_removed";
741
- return DocumentChangeType;
742
- }({});
743
- //#endregion
744
822
  //#region src/decision/read-gate.ts
745
823
  /**
746
824
  * Scopes every holder of a document may read, whatever the grants say. Denying
@@ -1293,7 +1371,7 @@ var ReactorClient = class {
1293
1371
  this.eventReads = new EventReadsSource(reactor, documentView, readGate);
1294
1372
  this.actionEvaluation = actionEvaluation;
1295
1373
  this.createSignaturePolicy = createSignaturePolicy;
1296
- this.drives = new DriveClient(this, logger, reactor, signer, (id, signal) => this.resolveReference(id, "main", signal));
1374
+ this.drives = new DriveClient(this, logger, (request, signal) => reactor.executeBatch(request, signal), signer, (id, signal) => this.resolveReference(id, "main", signal));
1297
1375
  this.logger.verbose("ReactorClient initialized");
1298
1376
  }
1299
1377
  /**
@@ -1555,58 +1633,7 @@ var ReactorClient = class {
1555
1633
  return this.submitCreate(document, parentIdentifier, signal);
1556
1634
  }
1557
1635
  async submitCreate(document, parentIdentifier, signal) {
1558
- const documentId = document.header.id;
1559
- const branch = document.header.branch || "main";
1560
- const parentId = parentIdentifier ? await this.resolveReference(parentIdentifier, "main", signal) : void 0;
1561
- const createInput = {
1562
- model: document.header.documentType,
1563
- version: 0,
1564
- documentId,
1565
- signing: {
1566
- signature: documentId,
1567
- publicKey: document.header.sig.publicKey,
1568
- nonce: document.header.sig.nonce,
1569
- createdAtUtcIso: document.header.createdAtUtcIso,
1570
- documentType: document.header.documentType
1571
- },
1572
- slug: document.header.slug,
1573
- name: document.header.name,
1574
- branch: document.header.branch,
1575
- meta: document.header.meta,
1576
- protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
1577
- };
1578
- const createActions = await signActions([createDocumentAction(createInput), upgradeDocumentAction({
1579
- documentId,
1580
- model: document.header.documentType,
1581
- fromVersion: 0,
1582
- toVersion: normalizeDocumentModelVersion(document.state.document?.version),
1583
- initialState: document.state
1584
- })], this.signer, {
1585
- documentId,
1586
- branch
1587
- }, signal);
1588
- const jobs = [{
1589
- key: "create",
1590
- documentId,
1591
- scope: getSharedActionScope(createActions),
1592
- branch,
1593
- actions: createActions,
1594
- dependsOn: []
1595
- }];
1596
- if (parentId) {
1597
- const parentActions = await signActions([addRelationshipAction(parentId, documentId, "child")], this.signer, {
1598
- documentId: parentId,
1599
- branch: "main"
1600
- }, signal);
1601
- jobs.push({
1602
- key: "parent",
1603
- documentId: parentId,
1604
- scope: getSharedActionScope(parentActions),
1605
- branch: "main",
1606
- actions: parentActions,
1607
- dependsOn: ["create"]
1608
- });
1609
- }
1636
+ const jobs = await buildCreateJobs(document, parentIdentifier ? await this.resolveReference(parentIdentifier, "main", signal) : void 0, this.signer, signal);
1610
1637
  return this.reactor.executeBatch({ jobs }, signal);
1611
1638
  }
1612
1639
  /**
@@ -1626,26 +1653,9 @@ var ReactorClient = class {
1626
1653
  return this.submitCreate(document, options?.parentIdentifier, signal);
1627
1654
  }
1628
1655
  async emptyDocument(documentModelType, options, signal) {
1629
- const matchingModules = (await this.reactor.getDocumentModels(void 0, void 0, signal)).results.filter((m) => m.documentModel.global.id === documentModelType);
1630
- let module;
1631
- if (options?.documentModelVersion !== void 0) {
1632
- const requestedVersion = normalizeDocumentModelVersion(options.documentModelVersion);
1633
- module = matchingModules.find((m) => normalizeDocumentModelVersion(m.version) === requestedVersion);
1634
- if (!module) throw new Error(`Document model not found for type: ${documentModelType} with version: ${options.documentModelVersion}`);
1635
- } else {
1636
- module = matchingModules.reduce((latest, current) => {
1637
- if (latest === void 0) return current;
1638
- return normalizeDocumentModelVersion(current.version) > normalizeDocumentModelVersion(latest.version) ? current : latest;
1639
- }, void 0);
1640
- if (!module) throw new Error(`Document model not found for type: ${documentModelType}`);
1641
- }
1656
+ const module = selectDocumentModelModule((await this.reactor.getDocumentModels(void 0, void 0, signal)).results, documentModelType, options?.documentModelVersion);
1642
1657
  const base = await this.getCreateProtocolVersions(options?.parentIdentifier, signal);
1643
- const document = withSignaturePolicy(module.utils.createDocument(), requestedSignaturePolicy(options, this.createSignaturePolicy), { protocolVersions: {
1644
- ...base,
1645
- ...options?.protocolVersions
1646
- } });
1647
- document.state.document.version = normalizeDocumentModelVersion(module.version);
1648
- return document;
1658
+ return createEmptyDocument(module, options, this.createSignaturePolicy, base);
1649
1659
  }
1650
1660
  /**
1651
1661
  * Upgrades a document to a newer document model version by dispatching an
@@ -1660,42 +1670,14 @@ var ReactorClient = class {
1660
1670
  */
1661
1671
  async upgradeDocument(documentIdentifier, toVersion, options, signal) {
1662
1672
  this.logger.verbose("upgradeDocument(@documentIdentifier, @toVersion)", documentIdentifier, toVersion);
1663
- const maxConflictRetries = options?.maxConflictRetries ?? 3;
1664
- let lastConflictMessage = "";
1665
- for (let attempt = 0; attempt <= maxConflictRetries; attempt++) {
1666
- const document = await this.reactor.getByIdOrSlug(documentIdentifier, void 0, void 0, signal);
1667
- const documentId = document.header.id;
1668
- const documentType = document.header.documentType;
1669
- const branch = document.header.branch || "main";
1670
- const fromVersion = normalizeDocumentModelVersion(document.state.document?.version);
1671
- let targetVersion = toVersion;
1672
- if (targetVersion === void 0) {
1673
- const module = await this.getDocumentModelModule(documentType);
1674
- targetVersion = normalizeDocumentModelVersion(module.version);
1675
- }
1676
- if (targetVersion === fromVersion) return this.gateDocument(document, { branch }, signal);
1677
- if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
1678
- const action = upgradeDocumentAction({
1679
- documentId,
1680
- model: documentType,
1681
- fromVersion,
1682
- toVersion: targetVersion,
1683
- revision: { ...document.header.revision }
1684
- });
1685
- const signedActions = await signActions([action], this.signer, {
1686
- documentId,
1687
- branch
1688
- }, signal);
1689
- const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
1690
- const completedJob = await this.waitForJob(jobInfo, signal);
1691
- if (completedJob.status !== "FAILED") {
1692
- const upgraded = await this.reactor.getByIdOrSlug(documentId, { branch }, completedJob.consistencyToken, signal);
1693
- return this.gateDocument(upgraded, { branch }, signal);
1694
- }
1695
- if (completedJob.error?.name !== "UpgradePreconditionFailedError") throw new Error(completedJob.error?.message);
1696
- lastConflictMessage = completedJob.error.message;
1697
- }
1698
- throw new Error(`Upgrade of document ${documentIdentifier} conflicted with concurrent edits after ${maxConflictRetries + 1} attempts: ${lastConflictMessage}`);
1673
+ const upgraded = await upgradeDocumentWith({
1674
+ signer: this.signer,
1675
+ read: (identifier, branch, consistencyToken, readSignal) => this.reactor.getByIdOrSlug(identifier, branch === void 0 ? void 0 : { branch }, consistencyToken, readSignal),
1676
+ getDocumentModelModule: (documentType) => this.getDocumentModelModule(documentType),
1677
+ submit: (documentId, branch, actions, submitSignal) => this.reactor.execute(documentId, branch, actions, submitSignal),
1678
+ waitForJob: (job, waitSignal) => this.waitForJob(job, waitSignal)
1679
+ }, documentIdentifier, toVersion, options, signal);
1680
+ return this.gateDocument(upgraded, { branch: upgraded.header.branch || "main" }, signal);
1699
1681
  }
1700
1682
  /**
1701
1683
  * Creates an empty document in a drive as a single batched operation.
@@ -2294,6 +2276,10 @@ function toErrorInfo(error) {
2294
2276
  stack: (/* @__PURE__ */ new Error()).stack || ""
2295
2277
  };
2296
2278
  }
2279
+ /** Whether a retry charged to the job's limit is still within it. */
2280
+ function hasRetriesLeft(job) {
2281
+ return (job.retryCount || 0) < (job.maxRetries || 0);
2282
+ }
2297
2283
  var JobResultHandler = class {
2298
2284
  queue;
2299
2285
  jobTracker;
@@ -2322,7 +2308,7 @@ var JobResultHandler = class {
2322
2308
  return;
2323
2309
  }
2324
2310
  }
2325
- if (modelLoaded) {
2311
+ if (modelLoaded && hasRetriesLeft(handle.job)) {
2326
2312
  const errorInfo = toErrorInfo(result.error);
2327
2313
  try {
2328
2314
  await this.queue.retryJob(handle.job.id, errorInfo);
@@ -2347,7 +2333,7 @@ var JobResultHandler = class {
2347
2333
  return;
2348
2334
  }
2349
2335
  const retryCount = handle.job.retryCount || 0;
2350
- if (retryCount < (handle.job.maxRetries || 0)) {
2336
+ if (hasRetriesLeft(handle.job)) {
2351
2337
  const currentErrorInfo = result.error ? toErrorInfo(result.error) : toErrorInfo("Unknown error");
2352
2338
  try {
2353
2339
  await this.queue.retryJob(handle.job.id, currentErrorInfo);
@@ -2530,38 +2516,6 @@ var DeferredJobs = class {
2530
2516
  }
2531
2517
  };
2532
2518
  //#endregion
2533
- //#region src/executor/worker-pool-router.ts
2534
- /**
2535
- * Sticky-by-document routing for the executor worker pool.
2536
- *
2537
- * Hashes `documentId` to a stable bucket so every job on a given document
2538
- * lands on the same worker. The queue already enforces "at most one job
2539
- * executing per document at a time", so a sticky worker always sees a fresh
2540
- * post-commit snapshot before its next job on that document — which keeps
2541
- * the per-worker `IWriteCache` and `IDocumentMetaCache` coherent for free.
2542
- *
2543
- * The hash is FNV-1a 32-bit: deterministic, dependency-free, and stable
2544
- * across processes (so the same routing decision can be reproduced anywhere
2545
- * the documentId is known).
2546
- *
2547
- * @see Executor Worker Pool Design wiki page
2548
- * (Powerhouse board wiki id: d400d711-f07e-4389-a226-4e9fdd4fa8ba)
2549
- */
2550
- const FNV_OFFSET_BASIS = 2166136261;
2551
- const FNV_PRIME = 16777619;
2552
- function hashDocumentId(documentId) {
2553
- let hash = FNV_OFFSET_BASIS;
2554
- for (let i = 0; i < documentId.length; i++) {
2555
- hash ^= documentId.charCodeAt(i);
2556
- hash = Math.imul(hash, FNV_PRIME);
2557
- }
2558
- return hash >>> 0;
2559
- }
2560
- function bucketFor(documentId, numWorkers) {
2561
- if (numWorkers < 1) throw new Error(`bucketFor: numWorkers must be >= 1 (got ${numWorkers})`);
2562
- return hashDocumentId(documentId) % numWorkers;
2563
- }
2564
- //#endregion
2565
2519
  //#region src/executor/worker-pool-job-executor-manager.ts
2566
2520
  /**
2567
2521
  * Manages a pool of executor workers and dispatches jobs across them with
@@ -2714,20 +2668,11 @@ var WorkerPoolJobExecutorManager = class {
2714
2668
  } catch (error) {
2715
2669
  const errorInfo = toErrorInfo(error instanceof Error ? error : String(error));
2716
2670
  if (isWorkerTransportError(error)) {
2717
- await this.handleWorkerTransportFailure(worker, handle.job, errorInfo);
2671
+ await this.handleWorkerTransportFailure(worker, handle, errorInfo);
2718
2672
  return;
2719
2673
  }
2720
- handle.fail(errorInfo);
2721
2674
  this.activeJobs--;
2722
- this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
2723
- const failedEvent = {
2724
- job: handle.job,
2725
- error: errorInfo.message,
2726
- willRetry: false,
2727
- retryCount: 0,
2728
- workerId
2729
- };
2730
- this.eventBus.emit(JobExecutorEventTypes.JOB_FAILED, failedEvent).catch(() => {});
2675
+ this.failInFlight(handle, errorInfo, workerId);
2731
2676
  await this.tryDispatchFor(worker);
2732
2677
  return;
2733
2678
  }
@@ -2790,20 +2735,41 @@ var WorkerPoolJobExecutorManager = class {
2790
2735
  this.logger.error("Failed to emit JOB_WRITE_READY event: @Error", error);
2791
2736
  }
2792
2737
  }
2738
+ failInFlight(handle, errorInfo, workerId) {
2739
+ handle.fail(errorInfo);
2740
+ this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
2741
+ const failedEvent = {
2742
+ job: handle.job,
2743
+ error: errorInfo.message,
2744
+ willRetry: false,
2745
+ retryCount: handle.job.retryCount || 0,
2746
+ workerId
2747
+ };
2748
+ this.eventBus.emit(JobExecutorEventTypes.JOB_FAILED, failedEvent).catch(() => {});
2749
+ }
2793
2750
  /**
2794
2751
  * Handle a worker-transport failure (worker exited / init failed / abort
2795
- * timed out) detected while `worker.execute` was in flight. Re-enqueues
2796
- * the in-flight job via `queue.retryJob` so it is retried on a healthy
2797
- * worker, then replaces the dead worker with a fresh handle and resumes
2798
- * dispatch on the same bucket. Does NOT emit JOB_FAILED — the job is
2799
- * not failed, only the worker is.
2752
+ * timed out) detected while `worker.execute` was in flight. The dead
2753
+ * worker is replaced and dispatch resumes on its bucket. The in-flight
2754
+ * job is retried via `queue.retryJob` while it has retries left, and
2755
+ * failed with the transport error once they are spent.
2800
2756
  */
2801
- async handleWorkerTransportFailure(dead, job, errorInfo) {
2757
+ async handleWorkerTransportFailure(dead, handle, errorInfo) {
2758
+ const job = handle.job;
2759
+ this.activeJobs--;
2760
+ if (!hasRetriesLeft(job)) {
2761
+ this.logger.error("worker transport error during execute; job @jobId is out of retries: @error", {
2762
+ jobId: job.id,
2763
+ workerId: dead.workerId
2764
+ }, errorInfo.message);
2765
+ this.failInFlight(handle, errorInfo, dead.workerId);
2766
+ await this.replaceWorker(dead);
2767
+ return;
2768
+ }
2802
2769
  this.logger.warn("worker transport error during execute; retrying job @jobId on a replacement worker: @error", {
2803
2770
  jobId: job.id,
2804
2771
  workerId: dead.workerId
2805
2772
  }, errorInfo.message);
2806
- this.activeJobs--;
2807
2773
  await this.replaceWorker(dead);
2808
2774
  try {
2809
2775
  await this.queue.retryJob(job.id, errorInfo);
@@ -3009,7 +2975,7 @@ var SimpleJobExecutorManager = class {
3009
2975
  deferredJobs;
3010
2976
  resultHandler;
3011
2977
  jobTimeoutMs;
3012
- constructor(executorFactory, eventBus, queue, jobTracker, logger, resolver, jobTimeoutMs = 3e4, deferredJobTtlMs = DEFAULT_DEFERRED_JOB_TTL_MS) {
2978
+ constructor(executorFactory, eventBus, queue, jobTracker, logger, resolver, jobTimeoutMs = DEFAULT_JOB_TIMEOUT_MS, deferredJobTtlMs = DEFAULT_DEFERRED_JOB_TTL_MS) {
3013
2979
  this.executorFactory = executorFactory;
3014
2980
  this.eventBus = eventBus;
3015
2981
  this.queue = queue;
@@ -3079,7 +3045,12 @@ var SimpleJobExecutorManager = class {
3079
3045
  workerId
3080
3046
  };
3081
3047
  this.eventBus.emit(JobExecutorEventTypes.JOB_STARTED, startedEvent).catch(() => {});
3082
- const signal = AbortSignal.timeout(this.jobTimeoutMs);
3048
+ const timeout = new AbortController();
3049
+ const timer = setTimeout(() => {
3050
+ timeout.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
3051
+ }, this.jobTimeoutMs);
3052
+ timer.unref?.();
3053
+ const signal = timeout.signal;
3083
3054
  const toError = (reason) => reason instanceof Error ? reason : new Error(String(reason));
3084
3055
  const abortPromise = new Promise((_, reject) => {
3085
3056
  if (signal.aborted) {
@@ -3090,8 +3061,9 @@ var SimpleJobExecutorManager = class {
3090
3061
  });
3091
3062
  let result;
3092
3063
  try {
3093
- result = await Promise.race([executor.executeJob(handle.job, signal), abortPromise]);
3064
+ result = await Promise.race([executor.executeJob(handle.job, signal, () => clearTimeout(timer)), abortPromise]);
3094
3065
  } catch (error) {
3066
+ clearTimeout(timer);
3095
3067
  const errorInfo = toErrorInfo(error instanceof Error ? error : String(error));
3096
3068
  handle.fail(errorInfo);
3097
3069
  this.activeJobs--;
@@ -3107,6 +3079,7 @@ var SimpleJobExecutorManager = class {
3107
3079
  await this.checkForMoreJobs();
3108
3080
  return;
3109
3081
  }
3082
+ clearTimeout(timer);
3110
3083
  if (result.success) this.totalJobsProcessed++;
3111
3084
  if (result.success) {
3112
3085
  const completedEvent = {
@@ -4542,6 +4515,8 @@ var InMemoryQueue = class {
4542
4515
  jobIdToDocId = /* @__PURE__ */ new Map();
4543
4516
  completedJobs = /* @__PURE__ */ new Set();
4544
4517
  jobIndex = /* @__PURE__ */ new Map();
4518
+ jobIdToSeq = /* @__PURE__ */ new Map();
4519
+ nextSeq = 0;
4545
4520
  isBlocked = false;
4546
4521
  onDrainedCallback;
4547
4522
  isPausedFlag = false;
@@ -4636,7 +4611,15 @@ var InMemoryQueue = class {
4636
4611
  async enqueue(job) {
4637
4612
  if (this.isBlocked) throw new Error("Queue is blocked");
4638
4613
  const queueKey = this.createQueueKey(job.documentId, job.scope, job.branch);
4639
- this.getQueue(queueKey).push(job);
4614
+ const queue = this.getQueue(queueKey);
4615
+ const seq = this.jobIdToSeq.get(job.id);
4616
+ if (seq === void 0) {
4617
+ this.jobIdToSeq.set(job.id, this.nextSeq++);
4618
+ queue.push(job);
4619
+ } else {
4620
+ const later = queue.findIndex((queued) => (this.jobIdToSeq.get(queued.id) ?? Infinity) > seq);
4621
+ queue.splice(later === -1 ? queue.length : later, 0, job);
4622
+ }
4640
4623
  this.jobIdToQueueKey.set(job.id, queueKey);
4641
4624
  this.jobIndex.set(job.id, job);
4642
4625
  const documentType = this.getCreateDocumentType(job);
@@ -4774,6 +4757,7 @@ var InMemoryQueue = class {
4774
4757
  queue.splice(jobIdx, 1);
4775
4758
  this.jobIdToQueueKey.delete(jobId);
4776
4759
  this.jobIndex.delete(jobId);
4760
+ this.jobIdToSeq.delete(jobId);
4777
4761
  if (queue.length === 0) this.queues.delete(queueKey);
4778
4762
  return Promise.resolve(true);
4779
4763
  }
@@ -4784,6 +4768,7 @@ var InMemoryQueue = class {
4784
4768
  for (const job of queue) {
4785
4769
  this.jobIdToQueueKey.delete(job.id);
4786
4770
  this.jobIndex.delete(job.id);
4771
+ this.jobIdToSeq.delete(job.id);
4787
4772
  }
4788
4773
  this.queues.delete(queueKey);
4789
4774
  }
@@ -4792,6 +4777,7 @@ var InMemoryQueue = class {
4792
4777
  clearAll() {
4793
4778
  this.jobIdToQueueKey.clear();
4794
4779
  this.jobIndex.clear();
4780
+ this.jobIdToSeq.clear();
4795
4781
  this.completedJobs.clear();
4796
4782
  this.queues.clear();
4797
4783
  return Promise.resolve();
@@ -4804,6 +4790,7 @@ var InMemoryQueue = class {
4804
4790
  if (documentId) this.markJobComplete(jobId, documentId);
4805
4791
  this.completedJobs.add(jobId);
4806
4792
  this.jobIndex.delete(jobId);
4793
+ this.jobIdToSeq.delete(jobId);
4807
4794
  await this.remove(jobId);
4808
4795
  this.checkDrained();
4809
4796
  }
@@ -4816,6 +4803,7 @@ var InMemoryQueue = class {
4816
4803
  if (error) job.errorHistory.push(error);
4817
4804
  }
4818
4805
  this.jobIndex.delete(jobId);
4806
+ this.jobIdToSeq.delete(jobId);
4819
4807
  this.completedJobs.add(jobId);
4820
4808
  await this.remove(jobId);
4821
4809
  if (job) {
@@ -5158,112 +5146,763 @@ var NullDocumentModelResolver = class {
5158
5146
  return this.ensureModelLoaded(documentType);
5159
5147
  }
5160
5148
  };
5161
- //#endregion
5162
- //#region src/subs/default-error-handler.ts
5163
- /**
5164
- * Default error handler that re-throws subscription errors.
5165
- * This ensures that errors are not silently swallowed.
5166
- */
5167
- var DefaultSubscriptionErrorHandler = class {
5168
- handleError(error, context) {
5169
- const errorMessage = `Subscription error in ${context.eventType} (${context.subscriptionId})`;
5170
- if (error instanceof Error) {
5171
- const enhancedError = /* @__PURE__ */ new Error(`${errorMessage}: ${error.message}`);
5172
- enhancedError.cause = error;
5173
- enhancedError.stack = error.stack;
5174
- throw enhancedError;
5175
- } else throw new Error(`${errorMessage}: ${String(error)}`);
5149
+ /** The filesystem sync never settled. */
5150
+ var PGliteFlushSyncTimeoutError = class extends Error {
5151
+ constructor(timeoutMs) {
5152
+ super(`The PGlite filesystem sync neither resolved nor rejected within ${timeoutMs}ms; no durability barrier was established.`);
5153
+ this.name = "PGliteFlushSyncTimeoutError";
5176
5154
  }
5177
5155
  };
5178
- //#endregion
5179
- //#region src/subs/react-subscription-manager.ts
5180
- var ReactorSubscriptionManager = class {
5181
- createdSubscriptions = /* @__PURE__ */ new Map();
5182
- deletedSubscriptions = /* @__PURE__ */ new Map();
5183
- updatedSubscriptions = /* @__PURE__ */ new Map();
5184
- relationshipSubscriptions = /* @__PURE__ */ new Map();
5185
- subscriptionCounter = 0;
5186
- errorHandler;
5187
- constructor(errorHandler) {
5188
- this.errorHandler = errorHandler;
5189
- }
5190
- onDocumentCreated(callback, search) {
5191
- const id = `created-${++this.subscriptionCounter}`;
5192
- this.createdSubscriptions.set(id, {
5193
- id,
5194
- callback,
5195
- search
5196
- });
5197
- return () => {
5198
- this.createdSubscriptions.delete(id);
5156
+ /** Group-commits PGlite's per-statement sync; see `withGroupCommitPGlite`. */
5157
+ var GroupCommitPGliteClient = class {
5158
+ instance;
5159
+ options;
5160
+ deferred;
5161
+ /** The instance's own `syncToFs`, captured before deferral replaces it. */
5162
+ instanceSync;
5163
+ /** Statements started. */
5164
+ sequence = 0;
5165
+ /** The highest {@link sequence} a completed flush made durable. */
5166
+ flushed = 0;
5167
+ /** `covers` is 0 until the snapshot starts; earlier requests join it. */
5168
+ running = void 0;
5169
+ syncFailures = 0;
5170
+ /** PGlite returns at once from a sync requested while one is scheduled. */
5171
+ lastSync = Promise.resolve();
5172
+ inFlight = 0;
5173
+ admittedRuns = 0;
5174
+ idleWaiters = [];
5175
+ gate = void 0;
5176
+ releaseGate = () => void 0;
5177
+ poisonCause = void 0;
5178
+ /** Rejects once the session is poisoned; every wait races it. */
5179
+ poisonedSignal;
5180
+ signalPoisoned = () => void 0;
5181
+ constructor(instance, options = {}) {
5182
+ this.instance = instance;
5183
+ this.options = {
5184
+ onDiagnostic: options.onDiagnostic ?? ((message, error) => {
5185
+ console.error(`[group-commit-pglite] ${message}`, error);
5186
+ }),
5187
+ onSyncStuck: options.onSyncStuck ?? (() => void 0),
5188
+ flushSyncTimeoutMs: options.flushSyncTimeoutMs ?? 12e4,
5189
+ maxConsecutiveSyncFailures: options.maxConsecutiveSyncFailures ?? 3,
5190
+ closeTimeoutMs: options.closeTimeoutMs ?? 3e4
5199
5191
  };
5200
- }
5201
- onDocumentDeleted(callback, search) {
5202
- const id = `deleted-${++this.subscriptionCounter}`;
5203
- this.deletedSubscriptions.set(id, {
5204
- id,
5205
- callback,
5206
- search
5192
+ this.poisonedSignal = new Promise((_resolve, reject) => {
5193
+ this.signalPoisoned = reject;
5207
5194
  });
5208
- return () => {
5209
- this.deletedSubscriptions.delete(id);
5210
- };
5195
+ this.poisonedSignal.catch(() => void 0);
5196
+ const candidate = instance.syncToFs;
5197
+ this.deferred = typeof candidate === "function";
5198
+ this.instanceSync = this.deferred ? candidate.bind(instance) : () => Promise.resolve();
5199
+ if (!this.deferred) this.options.onDiagnostic("this PGlite session exposes no filesystem sync, so statements keep their own durability");
5200
+ this.setStatementSync(!this.deferred);
5201
+ }
5202
+ query(sql, params) {
5203
+ const run = () => this.instance.query(sql, params);
5204
+ return this.admittedRuns > 0 ? run() : this.admit(run);
5205
+ }
5206
+ exec(sql) {
5207
+ const run = () => this.instance.exec(sql);
5208
+ return this.admittedRuns > 0 ? run() : this.admit(run);
5209
+ }
5210
+ /** Runs a statement past the flush gate; the calls it makes are not gated again. */
5211
+ async admit(run) {
5212
+ await this.enterStatement();
5213
+ this.admittedRuns += 1;
5214
+ try {
5215
+ return await run();
5216
+ } finally {
5217
+ this.admittedRuns -= 1;
5218
+ this.endStatement();
5219
+ }
5211
5220
  }
5212
- onDocumentStateUpdated(callback, search, view) {
5213
- const id = `updated-${++this.subscriptionCounter}`;
5214
- this.updatedSubscriptions.set(id, {
5215
- id,
5216
- callback,
5217
- search,
5218
- view
5219
- });
5220
- return () => {
5221
- this.updatedSubscriptions.delete(id);
5222
- };
5221
+ isInTransaction() {
5222
+ return this.instance.isInTransaction();
5223
5223
  }
5224
- onRelationshipChanged(callback, search) {
5225
- const id = `relationship-${++this.subscriptionCounter}`;
5226
- this.relationshipSubscriptions.set(id, {
5227
- id,
5228
- callback,
5229
- search
5230
- });
5231
- return () => {
5232
- this.relationshipSubscriptions.delete(id);
5233
- };
5224
+ /** Whether statements have stopped flushing themselves. */
5225
+ get deferringStatementFlush() {
5226
+ return this.deferred;
5234
5227
  }
5235
- /**
5236
- * Notify subscribers about created documents
5237
- */
5238
- notifyDocumentsCreated(documentIds, documentTypes, parentIds) {
5239
- const result = {
5240
- results: documentIds,
5241
- options: {
5242
- cursor: "",
5243
- limit: documentIds.length
5228
+ /** Every later and parked flush rejects: nothing can be made durable again. */
5229
+ markPoisoned(cause) {
5230
+ if (this.poisonCause) return;
5231
+ this.poisonCause = new StoragePoisonedError("The PGlite session is poisoned; nothing more can be made durable until the host restarts.", cause);
5232
+ this.releaseGate();
5233
+ this.idleWaiters = [];
5234
+ this.signalPoisoned(this.poisonCause);
5235
+ }
5236
+ /** Shares one sync between callers whose statements it covers. */
5237
+ async flush() {
5238
+ if (!this.deferred) return;
5239
+ const target = this.sequence;
5240
+ for (;;) {
5241
+ this.assertHealthy();
5242
+ if (this.flushed >= target) return;
5243
+ const running = this.running;
5244
+ if (running === void 0) {
5245
+ await this.startFlush();
5246
+ continue;
5244
5247
  }
5245
- };
5246
- for (const subscription of this.createdSubscriptions.values()) {
5247
- const filteredIds = this.filterDocumentIds(documentIds, subscription.search, documentTypes, parentIds);
5248
- if (filteredIds.length > 0) try {
5249
- subscription.callback({
5250
- ...result,
5251
- results: filteredIds
5252
- });
5253
- } catch (error) {
5254
- this.errorHandler.handleError(error, {
5255
- eventType: "created",
5256
- subscriptionId: subscription.id,
5257
- eventData: filteredIds
5258
- });
5248
+ if (running.covers === 0 || running.covers >= target) {
5249
+ await running.promise;
5250
+ continue;
5259
5251
  }
5252
+ await running.promise.catch(() => void 0);
5260
5253
  }
5261
5254
  }
5262
- /**
5263
- * Notify subscribers about deleted documents
5264
- */
5265
- notifyDocumentsDeleted(documentIds, documentTypes, parentIds, info) {
5266
- for (const subscription of this.deletedSubscriptions.values()) {
5255
+ /** PGlite's own close relies on the per-statement sync, so it is restored. */
5256
+ async close() {
5257
+ const flushing = this.flush();
5258
+ flushing.catch(() => void 0);
5259
+ let failure;
5260
+ try {
5261
+ if (await this.bounded(flushing) === TIMED_OUT) failure = /* @__PURE__ */ new Error("the closing flush did not settle");
5262
+ } catch (error) {
5263
+ failure = error;
5264
+ }
5265
+ if (failure !== void 0) this.options.onDiagnostic("the closing flush failed; writes since the last group commit are lost", failure);
5266
+ this.setStatementSync(true);
5267
+ const closing = this.instance.close();
5268
+ if (await this.bounded(closing) === TIMED_OUT) this.options.onDiagnostic("closing the PGlite instance did not settle");
5269
+ }
5270
+ /** Hands the instance back to per-statement durability, unclosed. */
5271
+ restoreStatementSync() {
5272
+ this.setStatementSync(true);
5273
+ }
5274
+ /** A statement that died in flight would otherwise hold a close forever. */
5275
+ bounded(pending) {
5276
+ const timeoutMs = this.options.closeTimeoutMs;
5277
+ return timeoutMs > 0 ? withDeadline(pending, timeoutMs) : pending;
5278
+ }
5279
+ assertHealthy() {
5280
+ if (this.poisonCause) throw this.poisonCause;
5281
+ }
5282
+ /** Restores by assignment so a prototype or own-field `syncToFs` both work. */
5283
+ setStatementSync(enabled) {
5284
+ if (!this.deferred) return;
5285
+ const target = this.instance;
5286
+ target.syncToFs = enabled ? this.instanceSync : () => Promise.resolve();
5287
+ }
5288
+ startFlush() {
5289
+ const running = {
5290
+ promise: Promise.resolve(),
5291
+ covers: 0
5292
+ };
5293
+ running.promise = this.runFlush(running).finally(() => {
5294
+ if (this.running === running) this.running = void 0;
5295
+ });
5296
+ this.running = running;
5297
+ return running.promise;
5298
+ }
5299
+ async runFlush(running) {
5300
+ const release = await this.holdStatements();
5301
+ running.covers = this.sequence;
5302
+ const covered = running.covers;
5303
+ try {
5304
+ await this.boundedSync();
5305
+ } finally {
5306
+ release();
5307
+ }
5308
+ this.flushed = Math.max(this.flushed, covered);
5309
+ }
5310
+ /** Chained, so PGlite's scheduled-sync shortcut never stands in for a sync. */
5311
+ async boundedSync() {
5312
+ const started = this.lastSync.then(() => this.instanceSync());
5313
+ this.lastSync = started.then(() => void 0, () => void 0);
5314
+ const syncing = Promise.race([started, this.poisonedSignal]);
5315
+ syncing.catch(() => void 0);
5316
+ let failure;
5317
+ try {
5318
+ const timeoutMs = this.options.flushSyncTimeoutMs;
5319
+ if ((timeoutMs > 0 ? await withDeadline(syncing, timeoutMs) : await syncing) !== TIMED_OUT) {
5320
+ this.syncFailures = 0;
5321
+ return;
5322
+ }
5323
+ failure = new PGliteFlushSyncTimeoutError(timeoutMs);
5324
+ } catch (error) {
5325
+ this.assertHealthy();
5326
+ this.syncFailures += 1;
5327
+ if (this.syncFailures < this.options.maxConsecutiveSyncFailures) throw error;
5328
+ failure = error instanceof Error ? error : new Error(String(error));
5329
+ }
5330
+ this.options.onDiagnostic("the PGlite filesystem sync is stuck; treating the session as poisoned", failure);
5331
+ this.markPoisoned(failure);
5332
+ try {
5333
+ this.options.onSyncStuck(failure);
5334
+ } catch (error) {
5335
+ this.options.onDiagnostic("onSyncStuck threw", error);
5336
+ }
5337
+ throw this.poisonCause ?? failure;
5338
+ }
5339
+ /** Waits for the statement in flight to finish and holds the next one back. */
5340
+ async holdStatements() {
5341
+ while (this.gate !== void 0) await Promise.race([this.gate, this.poisonedSignal]);
5342
+ this.assertHealthy();
5343
+ this.gate = new Promise((resolve) => {
5344
+ this.releaseGate = () => {
5345
+ this.gate = void 0;
5346
+ this.releaseGate = () => void 0;
5347
+ resolve();
5348
+ };
5349
+ });
5350
+ const release = () => this.releaseGate();
5351
+ if (this.inFlight === 0) return release;
5352
+ try {
5353
+ await Promise.race([new Promise((resolve) => this.idleWaiters.push(resolve)), this.poisonedSignal]);
5354
+ } catch (error) {
5355
+ release();
5356
+ throw error;
5357
+ }
5358
+ return release;
5359
+ }
5360
+ async enterStatement() {
5361
+ while (this.gate !== void 0) await this.gate;
5362
+ this.inFlight += 1;
5363
+ this.sequence += 1;
5364
+ }
5365
+ endStatement() {
5366
+ this.inFlight -= 1;
5367
+ if (this.inFlight > 0) return;
5368
+ const waiters = this.idleWaiters;
5369
+ this.idleWaiters = [];
5370
+ for (const waiter of waiters) waiter();
5371
+ }
5372
+ };
5373
+ //#endregion
5374
+ //#region src/storage/storage-health.ts
5375
+ /** Fed by the poison path so "connected" never reads green over a dead session. */
5376
+ var StorageHealthTracker = class {
5377
+ healthy = true;
5378
+ markPoisoned() {
5379
+ this.healthy = false;
5380
+ }
5381
+ getStorageHealth() {
5382
+ return { healthy: this.healthy };
5383
+ }
5384
+ };
5385
+ //#endregion
5386
+ //#region src/storage/kysely/pglite-dialect.ts
5387
+ var pglite_dialect_exports = /* @__PURE__ */ __exportAll({
5388
+ DEFAULT_LONG_STATEMENT_TIMEOUT_MS: () => DEFAULT_LONG_STATEMENT_TIMEOUT_MS,
5389
+ DEFAULT_RECOVERY_TIMEOUT_MS: () => DEFAULT_RECOVERY_TIMEOUT_MS,
5390
+ DEFAULT_STATEMENT_TIMEOUT_MS: () => DEFAULT_STATEMENT_TIMEOUT_MS,
5391
+ HardenedPGliteDialect: () => HardenedPGliteDialect,
5392
+ PGliteAbortedTransactionError: () => PGliteAbortedTransactionError,
5393
+ PGliteAcquireTimeoutError: () => PGliteAcquireTimeoutError,
5394
+ PGliteSessionError: () => PGliteSessionError,
5395
+ PGliteSessionPoisonedError: () => PGliteSessionPoisonedError,
5396
+ PGliteStatementTimeoutError: () => PGliteStatementTimeoutError,
5397
+ isLongRunningStatement: () => isLongRunningStatement,
5398
+ queryThroughDialect: () => queryThroughDialect
5399
+ });
5400
+ const DEFAULT_STATEMENT_TIMEOUT_MS = 12e4;
5401
+ const DEFAULT_LONG_STATEMENT_TIMEOUT_MS = 9e5;
5402
+ const DEFAULT_RECOVERY_TIMEOUT_MS = 15e3;
5403
+ const LONG_STATEMENT_PREFIXES = [
5404
+ "vacuum",
5405
+ "analyze",
5406
+ "reindex",
5407
+ "cluster",
5408
+ "checkpoint",
5409
+ "copy",
5410
+ "alter ",
5411
+ "create ",
5412
+ "drop ",
5413
+ "truncate",
5414
+ "refresh "
5415
+ ];
5416
+ /** Maintenance and DDL, matched on the leading keyword of the compiled SQL. */
5417
+ function isLongRunningStatement(statement) {
5418
+ const normalized = statement.trimStart().toLowerCase();
5419
+ return LONG_STATEMENT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
5420
+ }
5421
+ function summarize(statement) {
5422
+ const collapsed = statement.replace(/\s+/g, " ").trim();
5423
+ return collapsed.length > 160 ? `${collapsed.slice(0, 160)}...` : collapsed;
5424
+ }
5425
+ /** COMMIT in an aborted transaction silently rolls back; the guard raises first. */
5426
+ const COMMIT_WITH_GUARD = "select 1 as __commit_guard; commit";
5427
+ const PROBE = "select 1 as __session_probe";
5428
+ const ABORTED_TRANSACTION_CODE = "25P02";
5429
+ var PGliteSessionError = class extends Error {
5430
+ cause;
5431
+ constructor(message, cause) {
5432
+ super(message);
5433
+ this.cause = cause;
5434
+ this.name = "PGliteSessionError";
5435
+ }
5436
+ };
5437
+ var PGliteAcquireTimeoutError = class extends PGliteSessionError {
5438
+ constructor(timeoutMs) {
5439
+ super(`Timed out after ${timeoutMs}ms waiting for the PGlite connection. The single session is held by a statement that never completed: a transaction awaiting a nested query on the base handle, or an abandoned stream iterator.`);
5440
+ this.name = "PGliteAcquireTimeoutError";
5441
+ }
5442
+ };
5443
+ /** The call cannot be aborted, so it is abandoned and the session treated as poisoned. */
5444
+ var PGliteStatementTimeoutError = class extends PGliteSessionError {
5445
+ constructor(timeoutMs, statement) {
5446
+ super(`A PGlite statement neither resolved nor rejected within ${timeoutMs}ms, so its wasm call is presumed dead and the session poisoned: ${summarize(statement)}`);
5447
+ this.name = "PGliteStatementTimeoutError";
5448
+ }
5449
+ };
5450
+ var PGliteAbortedTransactionError = class extends PGliteSessionError {
5451
+ constructor(cause) {
5452
+ super(`Refusing to report a commit for an aborted transaction: COMMIT would have been answered with a ROLLBACK tag and nothing written. Underlying error: ${errorOf(cause).message}`, cause);
5453
+ this.name = "PGliteAbortedTransactionError";
5454
+ }
5455
+ };
5456
+ /** No statement can clear the session (e.g. a stuck active portal). */
5457
+ var PGliteSessionPoisonedError = class extends PGliteSessionError {
5458
+ constructor(cause) {
5459
+ super(`The PGlite session is unrecoverable and cannot be reused; the component owning it must be restarted. Original error: ${errorOf(cause).message}`, cause);
5460
+ this.name = "PGliteSessionPoisonedError";
5461
+ }
5462
+ };
5463
+ function errorOf(error) {
5464
+ return error instanceof Error ? error : new Error(String(error));
5465
+ }
5466
+ function isAbortedTransactionError(error) {
5467
+ if (error?.code === ABORTED_TRANSACTION_CODE) return true;
5468
+ return errorOf(error).message.includes("current transaction is aborted");
5469
+ }
5470
+ /** `kysely-pglite-dialect` hardened for one shared session that a fault must not brick. */
5471
+ var HardenedPGliteDialect = class {
5472
+ client;
5473
+ inner;
5474
+ options;
5475
+ constructor(client, options = {}) {
5476
+ this.client = client;
5477
+ this.inner = new PGliteDialect(client);
5478
+ this.options = {
5479
+ acquireTimeoutMs: options.acquireTimeoutMs ?? 0,
5480
+ statementTimeoutMs: options.statementTimeoutMs ?? 12e4,
5481
+ longStatementTimeoutMs: options.longStatementTimeoutMs ?? 9e5,
5482
+ recoveryTimeoutMs: options.recoveryTimeoutMs ?? 15e3,
5483
+ onDiagnostic: options.onDiagnostic ?? ((message, error) => {
5484
+ console.error(`[pglite-dialect] ${message}`, error);
5485
+ }),
5486
+ onPoisoned: options.onPoisoned ?? (() => void 0)
5487
+ };
5488
+ }
5489
+ createAdapter() {
5490
+ return this.inner.createAdapter();
5491
+ }
5492
+ createDriver() {
5493
+ return new HardenedPGliteDriver(this.inner.createDriver(), this.client, this.options);
5494
+ }
5495
+ createQueryCompiler() {
5496
+ return this.inner.createQueryCompiler();
5497
+ }
5498
+ createIntrospector(db) {
5499
+ return this.inner.createIntrospector(db);
5500
+ }
5501
+ };
5502
+ var HardenedPGliteConnection = class {
5503
+ inner;
5504
+ guard;
5505
+ failure = void 0;
5506
+ rollbackFailure = void 0;
5507
+ transactionOpen = false;
5508
+ constructor(inner, guard) {
5509
+ this.inner = inner;
5510
+ this.guard = guard;
5511
+ }
5512
+ /** Never retried: a replay could commit alone a write meant for another consumer's transaction. */
5513
+ async executeQuery(compiledQuery) {
5514
+ const generation = this.guard.generation;
5515
+ try {
5516
+ return await this.guard.runStatement(compiledQuery.sql, () => this.inner.executeQuery(compiledQuery));
5517
+ } catch (error) {
5518
+ this.guard.recordFailure(this, generation, error);
5519
+ throw error;
5520
+ }
5521
+ }
5522
+ streamQuery(compiledQuery, chunkSize) {
5523
+ const inner = this.inner.streamQuery(compiledQuery, chunkSize);
5524
+ return this.deadlinedStream(compiledQuery, inner);
5525
+ }
5526
+ /** Upstream issues the whole query on the first pull, so each pull is bounded like a statement. */
5527
+ async *deadlinedStream(compiledQuery, inner) {
5528
+ for (;;) {
5529
+ const generation = this.guard.generation;
5530
+ let next;
5531
+ try {
5532
+ next = await this.guard.runStatement(compiledQuery.sql, () => inner.next());
5533
+ } catch (error) {
5534
+ this.guard.recordFailure(this, generation, error);
5535
+ throw error;
5536
+ }
5537
+ if (next.done === true) return;
5538
+ yield next.value;
5539
+ }
5540
+ }
5541
+ get suspect() {
5542
+ return this.failure !== void 0 || this.rollbackFailure !== void 0 || this.transactionOpen;
5543
+ }
5544
+ };
5545
+ var HardenedPGliteDriver = class {
5546
+ inner;
5547
+ client;
5548
+ options;
5549
+ /** Set when release could not reset the session; each acquire retries the reset. */
5550
+ sessionFault = void 0;
5551
+ /** Lets an abandoned call's late settlement be told apart from current failures. */
5552
+ statementGeneration = 0;
5553
+ /** A call abandoned at its deadline; PGlite would queue every later call behind it, so none is issued. */
5554
+ deadCall = void 0;
5555
+ poisonReported = false;
5556
+ constructor(inner, client, options) {
5557
+ this.inner = inner;
5558
+ this.client = client;
5559
+ this.options = options;
5560
+ }
5561
+ get generation() {
5562
+ return this.statementGeneration;
5563
+ }
5564
+ /** Bounds one statement; an expiry is escalated as a poisoned session. */
5565
+ async runStatement(statement, execute) {
5566
+ if (this.deadCall !== void 0) throw new PGliteSessionPoisonedError(this.deadCall);
5567
+ const timeoutMs = this.timeoutFor(statement);
5568
+ if (timeoutMs <= 0) return execute();
5569
+ return this.admitted(async () => {
5570
+ if (this.deadCall !== void 0) throw new PGliteSessionPoisonedError(this.deadCall);
5571
+ const generation = this.statementGeneration;
5572
+ const outcome = await withDeadline(execute(), timeoutMs);
5573
+ if (outcome !== TIMED_OUT) return outcome;
5574
+ throw this.escalateHungStatement(new PGliteStatementTimeoutError(timeoutMs, statement), generation);
5575
+ });
5576
+ }
5577
+ /** Time spent waiting to be admitted is bounded by the session, not the deadline. */
5578
+ admitted(run) {
5579
+ return this.client.admit ? this.client.admit(run) : run();
5580
+ }
5581
+ recordFailure(connection, generation, error) {
5582
+ if (generation === this.statementGeneration) connection.failure = errorOf(error);
5583
+ }
5584
+ async init() {
5585
+ await this.inner.init();
5586
+ }
5587
+ async acquireConnection() {
5588
+ const innerConnection = await this.acquireWithTimeout();
5589
+ if (this.deadCall !== void 0) {
5590
+ await this.inner.releaseConnection(innerConnection);
5591
+ throw new PGliteSessionPoisonedError(this.deadCall);
5592
+ }
5593
+ if (this.sessionFault !== void 0) {
5594
+ const fault = this.sessionFault;
5595
+ let recovered = false;
5596
+ try {
5597
+ recovered = await this.tryRecoverSession(false);
5598
+ } finally {
5599
+ if (!recovered) await this.inner.releaseConnection(innerConnection);
5600
+ }
5601
+ if (!recovered) {
5602
+ this.reportPoisoned(fault);
5603
+ throw new PGliteSessionPoisonedError(fault);
5604
+ }
5605
+ this.sessionFault = void 0;
5606
+ }
5607
+ return new HardenedPGliteConnection(innerConnection, this);
5608
+ }
5609
+ /** Kysely never rolls back after a failed BEGIN, so an aborted session is reset here. */
5610
+ async beginTransaction(connection, settings) {
5611
+ const wrapper = asWrapper(connection);
5612
+ const generation = this.statementGeneration;
5613
+ try {
5614
+ await this.runStatement("begin", () => this.inner.beginTransaction(wrapper.inner, settings));
5615
+ } catch (error) {
5616
+ this.recordFailure(wrapper, generation, error);
5617
+ if (!isAbortedTransactionError(error)) throw error;
5618
+ if (!await this.tryRecoverSession(false)) {
5619
+ this.sessionFault = errorOf(error);
5620
+ throw error;
5621
+ }
5622
+ const retryGeneration = this.statementGeneration;
5623
+ try {
5624
+ await this.runStatement("begin", () => this.inner.beginTransaction(wrapper.inner, settings));
5625
+ } catch (retryError) {
5626
+ this.recordFailure(wrapper, retryGeneration, retryError);
5627
+ throw retryError;
5628
+ }
5629
+ wrapper.failure = void 0;
5630
+ }
5631
+ wrapper.transactionOpen = true;
5632
+ }
5633
+ async commitTransaction(connection) {
5634
+ const wrapper = asWrapper(connection);
5635
+ const generation = this.statementGeneration;
5636
+ try {
5637
+ await this.runStatement(COMMIT_WITH_GUARD, () => this.client.exec(COMMIT_WITH_GUARD));
5638
+ } catch (error) {
5639
+ this.recordFailure(wrapper, generation, error);
5640
+ if (isAbortedTransactionError(error)) throw new PGliteAbortedTransactionError(error);
5641
+ throw error;
5642
+ }
5643
+ wrapper.transactionOpen = false;
5644
+ wrapper.failure = void 0;
5645
+ }
5646
+ /** Swallowed so the original failure propagates; release resets the session. */
5647
+ async rollbackTransaction(connection) {
5648
+ const wrapper = asWrapper(connection);
5649
+ const generation = this.statementGeneration;
5650
+ try {
5651
+ await this.runStatement("rollback", () => this.inner.rollbackTransaction(wrapper.inner));
5652
+ wrapper.transactionOpen = false;
5653
+ } catch (error) {
5654
+ if (generation === this.statementGeneration) wrapper.rollbackFailure = errorOf(error);
5655
+ this.options.onDiagnostic("rollback failed; preserving the original failure and recovering the session on release", error);
5656
+ }
5657
+ }
5658
+ async releaseConnection(connection) {
5659
+ const wrapper = asWrapper(connection);
5660
+ try {
5661
+ if (wrapper.suspect && this.deadCall === void 0) {
5662
+ if (!await this.tryRecoverSession(wrapper.transactionOpen)) {
5663
+ this.sessionFault = wrapper.rollbackFailure ?? wrapper.failure ?? /* @__PURE__ */ new Error("session left in a transaction");
5664
+ this.options.onDiagnostic("the PGlite session could not be reset and is being marked unrecoverable", this.sessionFault);
5665
+ }
5666
+ }
5667
+ } finally {
5668
+ await this.inner.releaseConnection(wrapper.inner);
5669
+ }
5670
+ }
5671
+ async destroy() {
5672
+ await this.inner.destroy();
5673
+ }
5674
+ reportPoisoned(cause) {
5675
+ if (this.poisonReported) return;
5676
+ this.poisonReported = true;
5677
+ try {
5678
+ this.options.onPoisoned(cause);
5679
+ } catch (error) {
5680
+ this.options.onDiagnostic("onPoisoned threw", error);
5681
+ }
5682
+ }
5683
+ timeoutFor(statement) {
5684
+ return isLongRunningStatement(statement) ? this.options.longStatementTimeoutMs : this.options.statementTimeoutMs;
5685
+ }
5686
+ /** Marks the session faulted once per generation; a stale expiry reports only itself. */
5687
+ escalateHungStatement(cause, generation) {
5688
+ if (generation !== this.statementGeneration) return cause;
5689
+ this.statementGeneration += 1;
5690
+ this.deadCall = cause;
5691
+ this.reportPoisoned(cause);
5692
+ this.options.onDiagnostic("a PGlite statement never settled within its deadline; treating the session as poisoned", cause);
5693
+ return new PGliteSessionPoisonedError(cause);
5694
+ }
5695
+ /** A waiter that gave up still passes the lease on, so it cannot wedge the queue. */
5696
+ async acquireWithTimeout() {
5697
+ const timeoutMs = this.options.acquireTimeoutMs;
5698
+ if (timeoutMs <= 0) return this.inner.acquireConnection();
5699
+ let abandoned = false;
5700
+ const pending = this.inner.acquireConnection().then((connection) => {
5701
+ if (!abandoned) return connection;
5702
+ this.inner.releaseConnection(connection).catch(() => void 0);
5703
+ throw new PGliteAcquireTimeoutError(timeoutMs);
5704
+ });
5705
+ let handle;
5706
+ const expiry = new Promise((_resolve, reject) => {
5707
+ handle = setTimeout(() => {
5708
+ abandoned = true;
5709
+ reject(new PGliteAcquireTimeoutError(timeoutMs));
5710
+ }, timeoutMs);
5711
+ });
5712
+ try {
5713
+ return await Promise.race([pending, expiry]);
5714
+ } finally {
5715
+ clearTimeout(handle);
5716
+ }
5717
+ }
5718
+ /** {@link recoverSession}, with a recovery that throws counted as failed. */
5719
+ async tryRecoverSession(mustEndTransaction) {
5720
+ try {
5721
+ return await this.recoverSession(mustEndTransaction);
5722
+ } catch (error) {
5723
+ this.options.onDiagnostic("session recovery threw", error);
5724
+ return false;
5725
+ }
5726
+ }
5727
+ /** Rolls back only when told to or when the probe fails, sparing another consumer's live transaction. */
5728
+ async recoverSession(mustEndTransaction) {
5729
+ if (!mustEndTransaction && !this.client.isInTransaction() && await this.probeSession()) return true;
5730
+ if (!await this.recoveryExec("rollback")) this.options.onDiagnostic("session rollback did not complete; the session stays suspect");
5731
+ if (!await this.probeSession()) return false;
5732
+ return !this.client.isInTransaction();
5733
+ }
5734
+ async probeSession() {
5735
+ return this.recoveryExec(PROBE);
5736
+ }
5737
+ /** Kysely awaits release, so an unbounded recovery statement would hold the lease forever. */
5738
+ async recoveryExec(statement) {
5739
+ if (this.deadCall !== void 0) return false;
5740
+ const timeoutMs = this.options.recoveryTimeoutMs;
5741
+ const run = () => this.client.exec(statement).then(() => true, (error) => {
5742
+ this.options.onDiagnostic("recovery statement failed", error);
5743
+ return false;
5744
+ });
5745
+ if (timeoutMs <= 0) return run();
5746
+ return this.admitted(async () => {
5747
+ if (this.deadCall !== void 0) return false;
5748
+ const outcome = await withDeadline(run(), timeoutMs);
5749
+ if (outcome === TIMED_OUT) {
5750
+ this.deadCall ??= new PGliteStatementTimeoutError(timeoutMs, statement);
5751
+ this.reportPoisoned(this.deadCall);
5752
+ this.options.onDiagnostic(`a PGlite recovery statement did not settle within ${timeoutMs}ms; the session is unrecoverable from SQL`);
5753
+ return false;
5754
+ }
5755
+ return outcome;
5756
+ });
5757
+ }
5758
+ };
5759
+ function asWrapper(connection) {
5760
+ if (!(connection instanceof HardenedPGliteConnection)) throw new PGliteSessionError("Connection was not created by HardenedPGliteDialect");
5761
+ return connection;
5762
+ }
5763
+ /** Raw SQL through the dialect's queue, instead of into whatever transaction holds the session. */
5764
+ async function queryThroughDialect(db, sql, params) {
5765
+ return [...(await db.executeQuery(CompiledQuery.raw(sql, params ?? []))).rows];
5766
+ }
5767
+ //#endregion
5768
+ //#region src/storage/kysely/group-commit-storage.ts
5769
+ /** One unit, so the deferral never exists without its flusher and poison path. */
5770
+ function createGroupCommitStorage(options) {
5771
+ const onDiagnostic = options.onDiagnostic ?? ((message, error) => {
5772
+ console.error(`[group-commit-pglite] ${message}`, error);
5773
+ });
5774
+ const health = new StorageHealthTracker();
5775
+ let poisoned = false;
5776
+ const poison = (cause) => {
5777
+ if (poisoned) return;
5778
+ poisoned = true;
5779
+ health.markPoisoned();
5780
+ client.markPoisoned(cause);
5781
+ options.onUnrecoverable(cause);
5782
+ };
5783
+ const client = new GroupCommitPGliteClient(options.pg, {
5784
+ ...options.client,
5785
+ onDiagnostic,
5786
+ onSyncStuck: poison
5787
+ });
5788
+ return {
5789
+ db: new Kysely({ dialect: new HardenedPGliteDialect(client, {
5790
+ ...options.dialect,
5791
+ onDiagnostic,
5792
+ onPoisoned: poison
5793
+ }) }),
5794
+ flusher: client,
5795
+ health,
5796
+ close: () => client.close(),
5797
+ restoreStatementSync: () => client.restoreStatementSync()
5798
+ };
5799
+ }
5800
+ //#endregion
5801
+ //#region src/subs/default-error-handler.ts
5802
+ /**
5803
+ * Default error handler that re-throws subscription errors.
5804
+ * This ensures that errors are not silently swallowed.
5805
+ */
5806
+ var DefaultSubscriptionErrorHandler = class {
5807
+ handleError(error, context) {
5808
+ const errorMessage = `Subscription error in ${context.eventType} (${context.subscriptionId})`;
5809
+ if (error instanceof Error) {
5810
+ const enhancedError = /* @__PURE__ */ new Error(`${errorMessage}: ${error.message}`);
5811
+ enhancedError.cause = error;
5812
+ enhancedError.stack = error.stack;
5813
+ throw enhancedError;
5814
+ } else throw new Error(`${errorMessage}: ${String(error)}`);
5815
+ }
5816
+ };
5817
+ //#endregion
5818
+ //#region src/subs/react-subscription-manager.ts
5819
+ var ReactorSubscriptionManager = class {
5820
+ createdSubscriptions = /* @__PURE__ */ new Map();
5821
+ deletedSubscriptions = /* @__PURE__ */ new Map();
5822
+ updatedSubscriptions = /* @__PURE__ */ new Map();
5823
+ relationshipSubscriptions = /* @__PURE__ */ new Map();
5824
+ subscriptionCounter = 0;
5825
+ errorHandler;
5826
+ constructor(errorHandler) {
5827
+ this.errorHandler = errorHandler;
5828
+ }
5829
+ onDocumentCreated(callback, search) {
5830
+ const id = `created-${++this.subscriptionCounter}`;
5831
+ this.createdSubscriptions.set(id, {
5832
+ id,
5833
+ callback,
5834
+ search
5835
+ });
5836
+ return () => {
5837
+ this.createdSubscriptions.delete(id);
5838
+ };
5839
+ }
5840
+ onDocumentDeleted(callback, search) {
5841
+ const id = `deleted-${++this.subscriptionCounter}`;
5842
+ this.deletedSubscriptions.set(id, {
5843
+ id,
5844
+ callback,
5845
+ search
5846
+ });
5847
+ return () => {
5848
+ this.deletedSubscriptions.delete(id);
5849
+ };
5850
+ }
5851
+ onDocumentStateUpdated(callback, search, view) {
5852
+ const id = `updated-${++this.subscriptionCounter}`;
5853
+ this.updatedSubscriptions.set(id, {
5854
+ id,
5855
+ callback,
5856
+ search,
5857
+ view
5858
+ });
5859
+ return () => {
5860
+ this.updatedSubscriptions.delete(id);
5861
+ };
5862
+ }
5863
+ onRelationshipChanged(callback, search) {
5864
+ const id = `relationship-${++this.subscriptionCounter}`;
5865
+ this.relationshipSubscriptions.set(id, {
5866
+ id,
5867
+ callback,
5868
+ search
5869
+ });
5870
+ return () => {
5871
+ this.relationshipSubscriptions.delete(id);
5872
+ };
5873
+ }
5874
+ /**
5875
+ * Notify subscribers about created documents
5876
+ */
5877
+ notifyDocumentsCreated(documentIds, documentTypes, parentIds) {
5878
+ const result = {
5879
+ results: documentIds,
5880
+ options: {
5881
+ cursor: "",
5882
+ limit: documentIds.length
5883
+ }
5884
+ };
5885
+ for (const subscription of this.createdSubscriptions.values()) {
5886
+ const filteredIds = this.filterDocumentIds(documentIds, subscription.search, documentTypes, parentIds);
5887
+ if (filteredIds.length > 0) try {
5888
+ subscription.callback({
5889
+ ...result,
5890
+ results: filteredIds
5891
+ });
5892
+ } catch (error) {
5893
+ this.errorHandler.handleError(error, {
5894
+ eventType: "created",
5895
+ subscriptionId: subscription.id,
5896
+ eventData: filteredIds
5897
+ });
5898
+ }
5899
+ }
5900
+ }
5901
+ /**
5902
+ * Notify subscribers about deleted documents
5903
+ */
5904
+ notifyDocumentsDeleted(documentIds, documentTypes, parentIds, info) {
5905
+ for (const subscription of this.deletedSubscriptions.values()) {
5267
5906
  const filteredIds = this.filterDocumentIds(documentIds, subscription.search, documentTypes, parentIds);
5268
5907
  if (filteredIds.length > 0) try {
5269
5908
  if (info) subscription.callback(filteredIds, info);
@@ -5539,6 +6178,62 @@ var GroupReevaluationTrigger = class extends BaseReadModel {
5539
6178
  }
5540
6179
  };
5541
6180
  //#endregion
6181
+ //#region src/sync/channels/composite-channel-factory.ts
6182
+ /**
6183
+ * An {@link IChannelFactory} that dispatches to one of several factories on
6184
+ * {@link ChannelConfig.type}.
6185
+ *
6186
+ * This is the seam that lets ONE reactor hold remotes of more than one
6187
+ * transport at once -- gql remotes to a Switchboard AND brokered
6188
+ * `LocalChannel` peers (multi-reactor W3.0). Before it, a reactor wired a
6189
+ * single factory, so a local-sync reactor was local-only and a gql reactor
6190
+ * could not adopt a local peer; every mixed topology was out of reach.
6191
+ *
6192
+ * Routing is strict and total: a config whose type no registered factory
6193
+ * claims is refused by name rather than handed to whichever factory happens to
6194
+ * be first. That refusal is the point -- the single-factory world answered a
6195
+ * `{ type: "local" }` config on a gql reactor with a complaint about a missing
6196
+ * `url`, which says nothing about what is actually wrong.
6197
+ *
6198
+ * The composite adds no behaviour of its own: it holds no queue, no transport
6199
+ * and no logger, and every argument is passed through untouched. Which types a
6200
+ * reactor registers is the builder's decision (`ReactorBuilder`), not this
6201
+ * class's.
6202
+ */
6203
+ var CompositeChannelFactory = class {
6204
+ factories;
6205
+ /**
6206
+ * @param factories - (channel-config type -> factory) pairs. A duplicated
6207
+ * type is refused rather than silently resolved to the last entry: two
6208
+ * factories claiming one type is a configuration mistake with no correct
6209
+ * answer.
6210
+ * @throws Error if `factories` is empty or names a type twice
6211
+ */
6212
+ constructor(factories) {
6213
+ this.factories = /* @__PURE__ */ new Map();
6214
+ for (const [type, factory] of factories) {
6215
+ if (this.factories.has(type)) throw new Error(`CompositeChannelFactory was given two factories for the channel type "${type}"`);
6216
+ if (factory.channelTypes && !factory.channelTypes.includes(type)) throw new Error(`CompositeChannelFactory was given a factory for "${type}" that builds only [${factory.channelTypes.join(", ")}]`);
6217
+ this.factories.set(type, factory);
6218
+ }
6219
+ if (this.factories.size === 0) throw new Error("CompositeChannelFactory requires at least one (type, factory) entry");
6220
+ }
6221
+ /**
6222
+ * Creates a channel with the factory registered for `config.type`.
6223
+ *
6224
+ * @throws Error if no factory is registered for that type
6225
+ */
6226
+ instance(remoteId, remoteName, config, cursorStorage, collectionId, filter, operationIndex, options) {
6227
+ const factory = this.factories.get(config.type);
6228
+ if (!factory) throw new Error(`This reactor has no "${config.type}" channel factory: it composes factories for [${this.channelTypes.join(", ")}]`);
6229
+ return factory.instance(remoteId, remoteName, config, cursorStorage, collectionId, filter, operationIndex, options);
6230
+ }
6231
+ /** The types it routes, in registration order. */
6232
+ get channelTypes() {
6233
+ return [...this.factories.keys()];
6234
+ }
6235
+ };
6236
+ //#endregion
5542
6237
  //#region src/sync/types.ts
5543
6238
  let ChannelScheme = /* @__PURE__ */ function(ChannelScheme) {
5544
6239
  ChannelScheme["CONNECT"] = "connect";
@@ -5556,6 +6251,19 @@ let PollBehavior = /* @__PURE__ */ function(PollBehavior) {
5556
6251
  PollBehavior["Manual"] = "manual";
5557
6252
  return PollBehavior;
5558
6253
  }({});
6254
+ /**
6255
+ * Whether a remote outlives the process that added it.
6256
+ *
6257
+ * A `Session` remote's transport is a live object of this session, such as a
6258
+ * brokered `MessagePort`. Its record is written like any other, so its cursors,
6259
+ * holds and dead letters have a parent row, and the next `startup()` removes
6260
+ * it instead of rehydrating it.
6261
+ */
6262
+ let RemotePersistence = /* @__PURE__ */ function(RemotePersistence) {
6263
+ RemotePersistence["Durable"] = "durable";
6264
+ RemotePersistence["Session"] = "session";
6265
+ return RemotePersistence;
6266
+ }({});
5559
6267
  let SyncOperationStatus = /* @__PURE__ */ function(SyncOperationStatus) {
5560
6268
  SyncOperationStatus[SyncOperationStatus["Unknown"] = -1] = "Unknown";
5561
6269
  SyncOperationStatus[SyncOperationStatus["TransportPending"] = 0] = "TransportPending";
@@ -5599,10 +6307,14 @@ var MailboxAggregateError = class extends Error {
5599
6307
  };
5600
6308
  var Mailbox = class {
5601
6309
  holdAckBelowMarkers;
6310
+ holdAckBelowUnapplied;
5602
6311
  itemsMap = /* @__PURE__ */ new Map();
5603
6312
  /** Unapplied items carrying a marker, so the held ack reads only these. */
5604
6313
  heldMarkers = /* @__PURE__ */ new Set();
5605
- held = /* @__PURE__ */ new Set();
6314
+ /** Lowest ordinal of each item neither applied nor failed, read once at add. */
6315
+ unapplied = /* @__PURE__ */ new Map();
6316
+ /** Null when stale; a valid cache may be positive infinity. */
6317
+ unappliedFloorCache = Number.POSITIVE_INFINITY;
5606
6318
  addedCallbacks = [];
5607
6319
  removedCallbacks = [];
5608
6320
  paused = false;
@@ -5612,6 +6324,7 @@ var Mailbox = class {
5612
6324
  _latestOrdinal = 0;
5613
6325
  constructor(options = {}) {
5614
6326
  this.holdAckBelowMarkers = options.holdAckBelowMarkers ?? false;
6327
+ this.holdAckBelowUnapplied = options.holdAckBelowUnapplied ?? true;
5615
6328
  }
5616
6329
  init(ackOrdinal) {
5617
6330
  this._ack = this._latestOrdinal = ackOrdinal;
@@ -5623,22 +6336,17 @@ var Mailbox = class {
5623
6336
  return Array.from(this.itemsMap.values());
5624
6337
  }
5625
6338
  get ackOrdinal() {
5626
- if (!this.holdAckBelowMarkers && this.held.size === 0) return this._ack;
5627
- let floor = Number.POSITIVE_INFINITY;
5628
- for (const item of [...this.heldMarkers, ...this.held]) for (const op of item.operations) {
6339
+ let floor = this.unappliedFloor();
6340
+ for (const item of this.heldMarkers) for (const op of item.operations) {
5629
6341
  const ordinal = op.context.ordinal;
5630
6342
  if (ordinal > 0 && ordinal < floor) floor = ordinal;
5631
6343
  }
6344
+ if (floor === Number.POSITIVE_INFINITY) return this._ack;
5632
6345
  return Math.min(this._ack, floor - 1);
5633
6346
  }
5634
6347
  get latestOrdinal() {
5635
6348
  return this._latestOrdinal;
5636
6349
  }
5637
- hold(item) {
5638
- if (item.status === 2) return;
5639
- if (this.itemsMap.get(item.id) !== item) return;
5640
- this.held.add(item);
5641
- }
5642
6350
  get(id) {
5643
6351
  return this.itemsMap.get(id);
5644
6352
  }
@@ -5647,19 +6355,28 @@ var Mailbox = class {
5647
6355
  const replaced = this.itemsMap.get(item.id);
5648
6356
  if (replaced !== void 0 && replaced !== item) {
5649
6357
  this.heldMarkers.delete(replaced);
5650
- this.held.delete(replaced);
6358
+ this.forgetUnapplied(replaced);
5651
6359
  }
5652
6360
  this.itemsMap.set(item.id, item);
5653
6361
  let marker = false;
6362
+ let lowest = Number.POSITIVE_INFINITY;
5654
6363
  for (const op of item.operations) {
5655
- this._latestOrdinal = Math.max(this._latestOrdinal, op.context.ordinal);
6364
+ const ordinal = op.context.ordinal;
6365
+ this._latestOrdinal = Math.max(this._latestOrdinal, ordinal);
6366
+ if (ordinal > 0 && ordinal < lowest) lowest = ordinal;
5656
6367
  if (isPurgeMarker$1(op)) marker = true;
5657
6368
  }
6369
+ if (this.holdAckBelowUnapplied && lowest !== Number.POSITIVE_INFINITY && item.status !== 2 && item.status !== 3) {
6370
+ this.unapplied.set(item, lowest);
6371
+ const cached = this.unappliedFloorCache;
6372
+ if (cached !== null && lowest < cached) this.unappliedFloorCache = lowest;
6373
+ }
5658
6374
  if (this.holdAckBelowMarkers && marker && item.status !== 2) this.heldMarkers.add(item);
5659
6375
  item.on((syncOp, _, next) => {
6376
+ if (next === 3) this.forgetUnapplied(syncOp);
5660
6377
  if (next === 2) {
5661
6378
  this.heldMarkers.delete(syncOp);
5662
- this.held.delete(syncOp);
6379
+ this.forgetUnapplied(syncOp);
5663
6380
  for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
5664
6381
  }
5665
6382
  });
@@ -5679,9 +6396,14 @@ var Mailbox = class {
5679
6396
  }
5680
6397
  remove(...items) {
5681
6398
  for (const item of items) {
6399
+ const current = this.itemsMap.get(item.id);
6400
+ if (current !== void 0 && current !== item) {
6401
+ this.heldMarkers.delete(current);
6402
+ this.forgetUnapplied(current);
6403
+ }
5682
6404
  this.itemsMap.delete(item.id);
5683
6405
  this.heldMarkers.delete(item);
5684
- this.held.delete(item);
6406
+ this.forgetUnapplied(item);
5685
6407
  }
5686
6408
  if (this.paused) {
5687
6409
  this.removedBuffer.push(...items);
@@ -5736,6 +6458,20 @@ var Mailbox = class {
5736
6458
  isPaused() {
5737
6459
  return this.paused;
5738
6460
  }
6461
+ /** Recomputed only once the item that set it has resolved. */
6462
+ unappliedFloor() {
6463
+ if (this.unappliedFloorCache !== null) return this.unappliedFloorCache;
6464
+ let floor = Number.POSITIVE_INFINITY;
6465
+ for (const ordinal of this.unapplied.values()) if (ordinal < floor) floor = ordinal;
6466
+ this.unappliedFloorCache = floor;
6467
+ return floor;
6468
+ }
6469
+ forgetUnapplied(item) {
6470
+ const ordinal = this.unapplied.get(item);
6471
+ if (ordinal === void 0) return;
6472
+ this.unapplied.delete(item);
6473
+ if (ordinal === this.unappliedFloorCache) this.unappliedFloorCache = null;
6474
+ }
5739
6475
  };
5740
6476
  //#endregion
5741
6477
  //#region src/sync/buffered-mailbox.ts
@@ -5965,6 +6701,15 @@ var SyncRepairRefusedError = class extends Error {
5965
6701
  this.name = "SyncRepairRefusedError";
5966
6702
  }
5967
6703
  };
6704
+ /** A dead-letter page cursor that is not a row offset. */
6705
+ var InvalidDeadLetterCursorError = class extends Error {
6706
+ cursor;
6707
+ constructor(cursor) {
6708
+ super(`Invalid dead-letter cursor: ${JSON.stringify(cursor)}`);
6709
+ this.cursor = cursor;
6710
+ this.name = "InvalidDeadLetterCursorError";
6711
+ }
6712
+ };
5968
6713
  var PollingChannelError = class extends Error {
5969
6714
  constructor(message) {
5970
6715
  super(message);
@@ -7695,6 +8440,13 @@ var GqlRequestChannel = class {
7695
8440
  //#endregion
7696
8441
  //#region src/sync/channels/gql-request-channel-factory.ts
7697
8442
  /**
8443
+ * The {@link ChannelConfig.type} a GqlRequestChannel is created from.
8444
+ *
8445
+ * Named so {@link CompositeChannelFactory} registration and the callers that
8446
+ * build a `{ type: "gql" }` config agree on one spelling.
8447
+ */
8448
+ const GQL_CHANNEL_TYPE = "gql";
8449
+ /**
7698
8450
  * Factory for creating GqlRequestChannel instances.
7699
8451
  *
7700
8452
  * Extracts GraphQL-specific configuration from ChannelConfig.parameters and
@@ -7704,6 +8456,7 @@ var GqlRequestChannel = class {
7704
8456
  * which is useful for short-lived tokens with audience-specific claims.
7705
8457
  */
7706
8458
  var GqlRequestChannelFactory = class {
8459
+ channelTypes = ["gql"];
7707
8460
  logger;
7708
8461
  jwtHandler;
7709
8462
  queue;
@@ -7815,7 +8568,7 @@ var GqlResponseChannel = class {
7815
8568
  this.cursorStorage = cursorStorage;
7816
8569
  this.isShutdown = false;
7817
8570
  this.inbox = new Mailbox({ holdAckBelowMarkers: true });
7818
- this.outbox = new Mailbox();
8571
+ this.outbox = new Mailbox({ holdAckBelowUnapplied: false });
7819
8572
  this.deadLetter = new Mailbox();
7820
8573
  this.outbox.onRemoved((syncOps) => {
7821
8574
  this.rememberUnserved(syncOps);
@@ -7964,9 +8717,20 @@ var GqlResponseChannel = class {
7964
8717
  //#endregion
7965
8718
  //#region src/sync/channels/gql-response-channel-factory.ts
7966
8719
  /**
8720
+ * The {@link ChannelConfig.type} a GqlResponseChannel is created from.
8721
+ *
8722
+ * A response channel is never configured by this reactor: it is registered by
8723
+ * the peer that polls it, over the `registerChannel` mutation, whose resolver
8724
+ * (`packages/reactor-api`) writes exactly this type. Named here so
8725
+ * {@link CompositeChannelFactory} registration for the SWITCHBOARD scheme
8726
+ * routes the configs that actually arrive.
8727
+ */
8728
+ const POLLING_CHANNEL_TYPE = "polling";
8729
+ /**
7967
8730
  * Factory for creating GqlResponseChannel instances.
7968
8731
  */
7969
8732
  var GqlResponseChannelFactory = class {
8733
+ channelTypes = [POLLING_CHANNEL_TYPE];
7970
8734
  logger;
7971
8735
  constructor(logger) {
7972
8736
  this.logger = logger;
@@ -7976,6 +8740,36 @@ var GqlResponseChannelFactory = class {
7976
8740
  }
7977
8741
  };
7978
8742
  //#endregion
8743
+ //#region src/storage/flush-guarded-sync-cursor-storage.ts
8744
+ /**
8745
+ * Durability boundary 1 as a seam around whatever cursor storage sync uses: no
8746
+ * cursor row is written before a flush covering its operations, and a failed
8747
+ * flush takes the write with it. The row itself is not flushed after: a crash
8748
+ * then loses the advance, never the data, and a re-pull is idempotent.
8749
+ */
8750
+ var FlushGuardedSyncCursorStorage = class {
8751
+ inner;
8752
+ flusher;
8753
+ constructor(inner, flusher) {
8754
+ this.inner = inner;
8755
+ this.flusher = flusher;
8756
+ }
8757
+ list(remoteName, signal) {
8758
+ return this.inner.list(remoteName, signal);
8759
+ }
8760
+ get(remoteName, cursorType, signal) {
8761
+ return this.inner.get(remoteName, cursorType, signal);
8762
+ }
8763
+ async upsert(cursor, signal) {
8764
+ await this.flusher.flush();
8765
+ await this.inner.upsert(cursor, signal);
8766
+ }
8767
+ /** Forgetting an advance is the safe direction, so it needs no barrier. */
8768
+ remove(remoteName, signal) {
8769
+ return this.inner.remove(remoteName, signal);
8770
+ }
8771
+ };
8772
+ //#endregion
7979
8773
  //#region src/storage/kysely/delivery-lookup.ts
7980
8774
  /** The index row at `ordinal` and every membership of its document. */
7981
8775
  async function deliveryAt(db, documentId, ordinal) {
@@ -8315,7 +9109,8 @@ function rowToRemoteRecord(row) {
8315
9109
  },
8316
9110
  options: {
8317
9111
  sinceTimestampUtcMs: "0",
8318
- boundAddress: row.bound_address ?? void 0
9112
+ boundAddress: row.bound_address ?? void 0,
9113
+ ...row.persistence === "session" ? { persistence: "session" } : {}
8319
9114
  },
8320
9115
  status: {
8321
9116
  push: {
@@ -8358,6 +9153,7 @@ function remoteRecordToRow(remote) {
8358
9153
  pull_last_failure_utc_ms: remote.status.pull.lastFailureUtcMs ? new Date(remote.status.pull.lastFailureUtcMs).toISOString() : null,
8359
9154
  pull_failure_count: remote.status.pull.failureCount,
8360
9155
  bound_address: remote.options.boundAddress ?? null,
9156
+ persistence: remote.options.persistence ?? "durable",
8361
9157
  peer_manifest: remote.peer?.manifest ? canonicalJson(remote.peer.manifest, "peer manifest") : null,
8362
9158
  peer_manifest_at_utc_ms: remote.peer?.receivedAtUtcMs ?? null
8363
9159
  };
@@ -8789,6 +9585,15 @@ var SyncAwaiter = class {
8789
9585
  }
8790
9586
  };
8791
9587
  //#endregion
9588
+ //#region src/sync/sync-inspection.ts
9589
+ function deriveConnectionHealth(snapshot, nowMs = Date.now()) {
9590
+ return {
9591
+ snapshot,
9592
+ neverSucceeded: snapshot.state === "connected" && snapshot.lastSuccessUtcMs === 0,
9593
+ stalenessMs: snapshot.lastSuccessUtcMs > 0 ? Math.max(0, nowMs - snapshot.lastSuccessUtcMs) : void 0
9594
+ };
9595
+ }
9596
+ //#endregion
8792
9597
  //#region src/sync/sync-status-tracker.ts
8793
9598
  let SyncStatus = /* @__PURE__ */ function(SyncStatus) {
8794
9599
  SyncStatus["Synced"] = "SYNCED";
@@ -8904,7 +9709,8 @@ const defaultSyncManagerConfig = {
8904
9709
  maxHeldOperationsPerRemote: 1e4,
8905
9710
  staleRemotePollWindowMs: 3e5,
8906
9711
  markerRetryBaseDelayMs: 1e3,
8907
- markerRetryMaxDelayMs: 6e4
9712
+ markerRetryMaxDelayMs: 6e4,
9713
+ maxConcurrentInboxChunks: 8
8908
9714
  };
8909
9715
  const PLAN_KEY_TO_JOB_UUID_CAP = 1e4;
8910
9716
  /**
@@ -8916,6 +9722,9 @@ function isCredentialOrNetworkError(error) {
8916
9722
  if (isDriveAuthError(error)) return true;
8917
9723
  return error instanceof GraphQLRequestError && (error.category === "network" || error.category === "timeout");
8918
9724
  }
9725
+ function isSessionScoped(options) {
9726
+ return options.persistence === "session";
9727
+ }
8919
9728
  const holdKey = (documentId, branch) => `${documentId}\u0000${branch}`;
8920
9729
  const VERSION_CACHE_CAP = 1e4;
8921
9730
  /** The versions a CREATE_DOCUMENT among `operations` fixes for the document. */
@@ -8984,7 +9793,9 @@ var SyncManager = class {
8984
9793
  derivedThrough = /* @__PURE__ */ new Map();
8985
9794
  sweptThrough = 0;
8986
9795
  settledUnsubscribe;
8987
- inboxChunkChain = Promise.resolve();
9796
+ inboxLanes = /* @__PURE__ */ new Map();
9797
+ inboxChunksInFlight = 0;
9798
+ inboxSlotWaiters = [];
8988
9799
  capabilities;
8989
9800
  manifest;
8990
9801
  localSupport;
@@ -8998,6 +9809,7 @@ var SyncManager = class {
8998
9809
  peerUpdates = /* @__PURE__ */ new Map();
8999
9810
  peerUnsubscribes = /* @__PURE__ */ new Map();
9000
9811
  receivedMarkers = /* @__PURE__ */ new Map();
9812
+ appliedMarkers = /* @__PURE__ */ new Map();
9001
9813
  markerStorage;
9002
9814
  refusalStorage;
9003
9815
  markerWrites = /* @__PURE__ */ new Map();
@@ -9047,6 +9859,14 @@ var SyncManager = class {
9047
9859
  }
9048
9860
  async startup() {
9049
9861
  if (this.isShutdown) throw new Error("SyncManager is already shutdown and cannot be started");
9862
+ const remoteRecords = [];
9863
+ for (const record of await this.remoteStorage.list()) {
9864
+ if (isSessionScoped(record.options)) {
9865
+ await this.purgeSessionRemote(record.name);
9866
+ continue;
9867
+ }
9868
+ remoteRecords.push(record);
9869
+ }
9050
9870
  try {
9051
9871
  const quarantinedIds = await this.deadLetterStorage.listQuarantinedDocumentIds();
9052
9872
  for (const id of quarantinedIds) this.quarantinedDocumentIds.add(id);
@@ -9063,30 +9883,19 @@ var SyncManager = class {
9063
9883
  } catch (error) {
9064
9884
  this.logger.error("Failed to load sync holds (@error)", error instanceof Error ? error.message : String(error));
9065
9885
  }
9066
- const remoteRecords = await this.remoteStorage.list();
9067
9886
  const head = await this.watermarkHead();
9068
9887
  this.sweptThrough = Math.max(this.sweptThrough, head);
9069
9888
  for (const record of remoteRecords) {
9070
- const channel = this.channelFactory.instance(record.id, record.name, record.channelConfig, this.cursorStorage, record.collectionId, record.filter, this.operationIndex, record.options);
9071
- const remote = {
9072
- meta: {
9073
- id: record.id,
9074
- name: record.name,
9075
- collectionId: record.collectionId,
9076
- channelConfig: record.channelConfig,
9077
- filter: record.filter,
9078
- options: record.options,
9079
- peer: record.peer
9080
- },
9081
- channel
9082
- };
9083
- this.remotes.set(record.name, remote);
9084
- this.owe(record.name, head);
9085
- this.records.set(record.name, remote.meta);
9086
- await this.loadDeadLetters(remote);
9087
- await this.restoreReceivedMarkers(remote);
9088
- const unheard = [...remote.channel.inbox.items];
9089
- this.wireChannelCallbacks(remote);
9889
+ let rehydrated;
9890
+ try {
9891
+ rehydrated = await this.rehydrateRemote(record, head);
9892
+ } catch (error) {
9893
+ await this.degradeRemoteAfterFailedRehydration(record, error);
9894
+ continue;
9895
+ }
9896
+ const remote = rehydrated.remote;
9897
+ const channel = remote.channel;
9898
+ const unheard = rehydrated.unheard;
9090
9899
  try {
9091
9900
  await channel.init();
9092
9901
  } catch (error) {
@@ -9120,6 +9929,8 @@ var SyncManager = class {
9120
9929
  this.backfillAbortControllers.clear();
9121
9930
  this.planKeyToJobUuid.clear();
9122
9931
  this.lastEnqueuedJobIdByKey.clear();
9932
+ this.inboxLanes.clear();
9933
+ for (const waiter of this.inboxSlotWaiters.splice(0)) waiter();
9123
9934
  this.prunePending.clear();
9124
9935
  this.pruneDrainDeferred = false;
9125
9936
  this.batchAggregator.clear();
@@ -9510,6 +10321,16 @@ var SyncManager = class {
9510
10321
  }
9511
10322
  return kept;
9512
10323
  }
10324
+ /** A previous session's transport is gone, so its remote and rows go too. */
10325
+ async purgeSessionRemote(name) {
10326
+ try {
10327
+ await this.remoteStorage.remove(name);
10328
+ await this.cursorStorage.remove(name);
10329
+ await this.forgetRemote(name);
10330
+ } catch (error) {
10331
+ this.logger.error("Error removing a previous session's remote at startup (@name, @error)", name, error instanceof Error ? error.message : String(error));
10332
+ }
10333
+ }
9513
10334
  recordOf(meta) {
9514
10335
  return {
9515
10336
  id: meta.id,
@@ -9546,8 +10367,8 @@ var SyncManager = class {
9546
10367
  receivedAtUtcMs: Date.now()
9547
10368
  }
9548
10369
  };
9549
- await this.remoteStorage.upsert(this.recordOf(meta));
9550
10370
  const channel = this.channelFactory.instance(remoteId, name, channelConfig, this.cursorStorage, collectionId, filter, this.operationIndex, options);
10371
+ await this.remoteStorage.upsert(this.recordOf(meta));
9551
10372
  const remote = {
9552
10373
  meta,
9553
10374
  channel
@@ -9587,6 +10408,72 @@ var SyncManager = class {
9587
10408
  }
9588
10409
  }
9589
10410
  /**
10411
+ * Rebuilds one stored remote's channel and wires it up, short of init().
10412
+ *
10413
+ * Everything here can throw -- the factory rejects an unusable config, the
10414
+ * dead-letter and marker reads touch storage -- so startup() calls it inside
10415
+ * a guard and degrades just this remote when it does. Nothing is left half
10416
+ * registered on the throwing path that {@link degradeRemoteAfterFailedRehydration}
10417
+ * does not then clear.
10418
+ */
10419
+ async rehydrateRemote(record, head) {
10420
+ const channel = this.channelFactory.instance(record.id, record.name, record.channelConfig, this.cursorStorage, record.collectionId, record.filter, this.operationIndex, record.options);
10421
+ const remote = {
10422
+ meta: {
10423
+ id: record.id,
10424
+ name: record.name,
10425
+ collectionId: record.collectionId,
10426
+ channelConfig: record.channelConfig,
10427
+ filter: record.filter,
10428
+ options: record.options,
10429
+ peer: record.peer
10430
+ },
10431
+ channel
10432
+ };
10433
+ this.remotes.set(record.name, remote);
10434
+ this.owe(record.name, head);
10435
+ this.records.set(record.name, remote.meta);
10436
+ await this.loadDeadLetters(remote);
10437
+ await this.restoreReceivedMarkers(remote);
10438
+ const unheard = [...remote.channel.inbox.items];
10439
+ this.wireChannelCallbacks(remote);
10440
+ return {
10441
+ remote,
10442
+ unheard
10443
+ };
10444
+ }
10445
+ /**
10446
+ * Records a stored remote startup() could not bring up and moves on.
10447
+ *
10448
+ * Classified exactly as a failed init is: a credential or network failure
10449
+ * says nothing about the configuration, so the row stays and a retry after
10450
+ * sign-in can re-add it; anything else -- a factory with no scheme for this
10451
+ * config, a transport this session cannot resolve -- says the record itself
10452
+ * is unusable, so it is dropped rather than left to fail every boot. Either
10453
+ * way the boot carries on without it.
10454
+ */
10455
+ async degradeRemoteAfterFailedRehydration(record, error) {
10456
+ const message = error instanceof Error ? error.message : String(error);
10457
+ this.logger.error("Error rehydrating stored remote at startup (@name, @error)", record.name, message);
10458
+ const recordKept = isCredentialOrNetworkError(error);
10459
+ this.remotes.delete(record.name);
10460
+ this.owed.delete(record.name);
10461
+ this.derivedThrough.delete(record.name);
10462
+ this.connectionStateUnsubscribes.get(record.name)?.();
10463
+ this.connectionStateUnsubscribes.delete(record.name);
10464
+ if (recordKept) return;
10465
+ try {
10466
+ await this.remoteStorage.remove(record.name);
10467
+ } catch (removeError) {
10468
+ this.logger.error("Error removing unusable remote record at startup (@name, @error)", record.name, removeError instanceof Error ? removeError.message : String(removeError));
10469
+ }
10470
+ try {
10471
+ await this.forgetRemote(record.name);
10472
+ } catch (forgetError) {
10473
+ this.logger.error("Error forgetting unusable remote at startup (@name, @error)", record.name, forgetError instanceof Error ? forgetError.message : String(forgetError));
10474
+ }
10475
+ }
10476
+ /**
9590
10477
  * Drops a remote whose channel.init() rejected, optionally removing its
9591
10478
  * stored record. Ordered like remove(): the registry slot is released last,
9592
10479
  * so a concurrent add of the same name cannot slip in and have its record
@@ -9641,6 +10528,7 @@ var SyncManager = class {
9641
10528
  this.derivedThrough.delete(name);
9642
10529
  this.prunePending.delete(name);
9643
10530
  this.receivedMarkers.delete(name);
10531
+ this.appliedMarkers.delete(name);
9644
10532
  for (const [id, retry] of [...this.markerRetries]) {
9645
10533
  if (retry.remoteName !== name) continue;
9646
10534
  clearTimeout(retry.timer);
@@ -9654,6 +10542,55 @@ var SyncManager = class {
9654
10542
  list() {
9655
10543
  return Array.from(this.remotes.values());
9656
10544
  }
10545
+ async inspectRemote(remoteName) {
10546
+ const remote = this.getByName(remoteName);
10547
+ return this.inspectionOf(remote);
10548
+ }
10549
+ async inspectRemotes() {
10550
+ const out = [];
10551
+ for (const remote of [...this.remotes.values()]) out.push(await this.inspectionOf(remote));
10552
+ return out;
10553
+ }
10554
+ async listDeadLetters(remoteName, cursor, limit) {
10555
+ this.getByName(remoteName);
10556
+ if (cursor && !(/^\d+$/.test(cursor) && Number.isSafeInteger(Number(cursor)))) throw new InvalidDeadLetterCursorError(cursor);
10557
+ const max = this.config.maxDeadLettersPerRemote;
10558
+ const page = await this.deadLetterStorage.list(remoteName, {
10559
+ cursor: cursor || "0",
10560
+ limit: limit === void 0 || !Number.isFinite(limit) ? max : Math.min(Math.max(Math.trunc(limit), 1), max)
10561
+ });
10562
+ return {
10563
+ remoteName,
10564
+ results: page.results,
10565
+ nextCursor: page.nextCursor
10566
+ };
10567
+ }
10568
+ async inspectionOf(remote) {
10569
+ const cursors = await this.cursorStorage.list(remote.meta.name);
10570
+ const cursorInfo = (cursorType, mailbox) => {
10571
+ const stored = cursors.find((c) => c.cursorType === cursorType);
10572
+ return {
10573
+ cursorType,
10574
+ cursorOrdinal: stored?.cursorOrdinal ?? 0,
10575
+ lastSyncedAtUtcMs: stored?.lastSyncedAtUtcMs,
10576
+ liveAckOrdinal: mailbox.ackOrdinal,
10577
+ liveLatestOrdinal: mailbox.latestOrdinal
10578
+ };
10579
+ };
10580
+ const { channel } = remote;
10581
+ return {
10582
+ remoteName: remote.meta.name,
10583
+ remoteId: remote.meta.id,
10584
+ inboxCursor: cursorInfo("inbox", channel.inbox),
10585
+ outboxCursor: cursorInfo("outbox", channel.outbox),
10586
+ mailboxDepths: {
10587
+ inbox: channel.inbox.items.length,
10588
+ outbox: channel.outbox.items.length,
10589
+ deadLetter: channel.deadLetter.items.length
10590
+ },
10591
+ connection: deriveConnectionHealth(channel.getConnectionState())
10592
+ };
10593
+ }
9657
10594
  waitForSync(jobId, signal) {
9658
10595
  return this.syncAwaiter.waitForSync(jobId, signal);
9659
10596
  }
@@ -9667,6 +10604,13 @@ var SyncManager = class {
9667
10604
  async receiptsStored(remoteNames) {
9668
10605
  await Promise.all(this.markerWritesOf(remoteNames));
9669
10606
  }
10607
+ async resetSettled(name) {
10608
+ for (;;) {
10609
+ const pending = this.queuedResets.get(name) ?? this.resets.get(name);
10610
+ if (!pending) return;
10611
+ await pending.catch(() => void 0);
10612
+ }
10613
+ }
9670
10614
  /** Never joins a running rebuild: it may have read state older than the request. */
9671
10615
  resetChannel(remoteName) {
9672
10616
  const queued = this.queuedResets.get(remoteName);
@@ -9743,7 +10687,6 @@ var SyncManager = class {
9743
10687
  if (!this.parkedByQuarantine.has(item) || requeued.has(item.id)) continue;
9744
10688
  const copy = new SyncOperation(item.id, item.jobId, item.jobDependencies, item.remoteName, item.documentId, item.scopes, item.branch, item.operations);
9745
10689
  fresh.channel.inbox.add(copy);
9746
- fresh.channel.inbox.hold?.(copy);
9747
10690
  }
9748
10691
  }
9749
10692
  /** Before a fresh channel is registered: nothing to tear down. */
@@ -10107,13 +11050,15 @@ var SyncManager = class {
10107
11050
  const eligible = [];
10108
11051
  const dropped = [];
10109
11052
  const received = this.receivedMarkersOf(remote.meta.name);
11053
+ const applied = this.appliedMarkers.get(remote.meta.name);
10110
11054
  const loading = (id, syncOp) => {
10111
11055
  const item = received.get(id);
10112
11056
  return item !== void 0 && item !== syncOp && item.status !== 2 && remote.channel.inbox.get(item.id) === item;
10113
11057
  };
10114
11058
  for (const syncOp of syncOps) if (carriesMarker(syncOp)) {
10115
11059
  const ids = markerIdsOf(syncOp);
10116
- if (ids.every((id) => loading(id, syncOp))) {
11060
+ if (applied !== void 0 && ids.every((id) => applied.has(id))) dropped.push(syncOp);
11061
+ else if (ids.every((id) => loading(id, syncOp))) {
10117
11062
  this.storeReceivedMarker(remote, syncOp);
10118
11063
  dropped.push(syncOp);
10119
11064
  } else {
@@ -10126,10 +11071,7 @@ var SyncManager = class {
10126
11071
  this.dropRequeuedDeadLetter(syncOp.id, remote.meta.name);
10127
11072
  } else if (!this.quarantinedDocumentIds.has(syncOp.documentId)) eligible.push(syncOp);
10128
11073
  else if (!this.requeuedDeadLetterIds.has(syncOp.id) && this.parkedCopyOf(remote, syncOp)) dropped.push(syncOp);
10129
- else {
10130
- this.parkedByQuarantine.add(syncOp);
10131
- remote.channel.inbox.hold?.(syncOp);
10132
- }
11074
+ else this.parkedByQuarantine.add(syncOp);
10133
11075
  for (const syncOp of dropped) {
10134
11076
  this.logger.debug("Dropping received operations of a purged, already-loading or already-parked document (@remote, @documentId)", remote.meta.name, syncOp.documentId);
10135
11077
  syncOp.executed();
@@ -10156,14 +11098,34 @@ var SyncManager = class {
10156
11098
  }
10157
11099
  /** Forgets a marker once the item loading it leaves the inbox. */
10158
11100
  handleInboxRemoved(remote, syncOps) {
10159
- const received = this.receivedMarkers.get(remote.meta.name);
10160
- if (received === void 0 || received.size === 0) return;
10161
11101
  const name = remote.meta.name;
10162
- for (const syncOp of syncOps) for (const id of markerIdsOf(syncOp)) {
11102
+ const received = this.receivedMarkers.get(name);
11103
+ if (received !== void 0 && received.size > 0) for (const syncOp of syncOps) for (const id of markerIdsOf(syncOp)) {
10163
11104
  if (received.get(id) !== syncOp) continue;
10164
11105
  received.delete(id);
10165
11106
  this.writeMarker(name, id, () => this.markerStorage.remove(name, id));
11107
+ if (syncOp.status === 2) this.rememberAppliedMarker(name, id, syncOp);
10166
11108
  }
11109
+ this.forgetAckedMarkers(remote);
11110
+ }
11111
+ /** Kept until the ack passes it: the pusher re-pushes everything above the ack. */
11112
+ rememberAppliedMarker(name, id, syncOp) {
11113
+ let applied = this.appliedMarkers.get(name);
11114
+ if (applied === void 0) {
11115
+ applied = /* @__PURE__ */ new Map();
11116
+ this.appliedMarkers.set(name, applied);
11117
+ }
11118
+ let ordinal = 0;
11119
+ for (const op of syncOp.operations) ordinal = Math.max(ordinal, op.context.ordinal);
11120
+ applied.set(id, ordinal);
11121
+ }
11122
+ forgetAckedMarkers(remote) {
11123
+ const name = remote.meta.name;
11124
+ const applied = this.appliedMarkers.get(name);
11125
+ if (applied === void 0) return;
11126
+ const ack = remote.channel.inbox.ackOrdinal;
11127
+ for (const [id, ordinal] of applied) if (ordinal <= ack) applied.delete(id);
11128
+ if (applied.size === 0) this.appliedMarkers.delete(name);
10167
11129
  }
10168
11130
  markerWritesOf(remoteNames) {
10169
11131
  if (remoteNames === void 0) return [...this.markerWrites.values()];
@@ -10229,23 +11191,91 @@ var SyncManager = class {
10229
11191
  return received;
10230
11192
  }
10231
11193
  processInboxChunks(chunks) {
10232
- const next = this.inboxChunkChain.then(async () => {
10233
- for (const chunk of chunks) {
10234
- if (this.isShutdown) return;
10235
- await this.applyInboxBatch(chunk);
10236
- }
10237
- });
10238
- this.inboxChunkChain = next.catch((err) => {
11194
+ return Promise.all(chunks.map((chunk) => this.queueInboxChunk(chunk))).then(() => void 0);
11195
+ }
11196
+ /**
11197
+ * Queues a chunk behind every chunk it shares a lane key with and beside the
11198
+ * rest, so one document's stalled or failing load does not hold up another's.
11199
+ */
11200
+ queueInboxChunk(chunk) {
11201
+ const keys = this.inboxLaneKeys(chunk);
11202
+ const predecessors = [];
11203
+ for (const key of keys) {
11204
+ const tail = this.inboxLanes.get(key);
11205
+ if (tail !== void 0) predecessors.push(tail);
11206
+ }
11207
+ const lane = Promise.all(predecessors).then(() => this.runInboxChunk(chunk)).catch((err) => {
10239
11208
  this.logger.error("Inbox chunk processing failed (@error)", err instanceof Error ? err.message : String(err));
10240
11209
  });
10241
- return next;
11210
+ for (const key of keys) this.inboxLanes.set(key, lane);
11211
+ lane.then(() => {
11212
+ for (const key of keys) if (this.inboxLanes.get(key) === lane) this.inboxLanes.delete(key);
11213
+ });
11214
+ return lane;
11215
+ }
11216
+ /**
11217
+ * The documents a chunk writes, since the FIFO dependency reads the job last
11218
+ * enqueued for its document, and the plan keys its edges name, since a
11219
+ * cross-chunk edge resolves only once its provider is enqueued. Per document
11220
+ * rather than per scope or branch: the queue serialises a document across both.
11221
+ */
11222
+ inboxLaneKeys(chunk) {
11223
+ const keys = /* @__PURE__ */ new Set();
11224
+ for (const { syncOp } of chunk) {
11225
+ keys.add(`doc ${syncOp.documentId}`);
11226
+ keys.add(`plan ${syncOp.jobId}`);
11227
+ for (const dep of syncOp.jobDependencies) if (dep) keys.add(`plan ${dep}`);
11228
+ }
11229
+ return [...keys];
11230
+ }
11231
+ /**
11232
+ * Settles once the chunk is enqueued, which is all its lane waits for; the
11233
+ * queue orders the writes. The slot is released there too, and the items
11234
+ * then resolve beside one another, unbounded: each is a passive wait on a
11235
+ * job for an item the inbox already holds.
11236
+ */
11237
+ async runInboxChunk(chunk) {
11238
+ if (this.isShutdown) return;
11239
+ await this.acquireInboxSlot();
11240
+ if (this.isShutdown) {
11241
+ this.releaseInboxSlot();
11242
+ return;
11243
+ }
11244
+ let enqueued;
11245
+ try {
11246
+ enqueued = await this.enqueueInboxBatch(chunk);
11247
+ } finally {
11248
+ this.releaseInboxSlot();
11249
+ }
11250
+ if (enqueued === void 0) return;
11251
+ this.resolveInboxBatch(enqueued).catch((err) => {
11252
+ this.logger.error("Inbox chunk resolution failed (@error)", err instanceof Error ? err.message : String(err));
11253
+ });
11254
+ }
11255
+ acquireInboxSlot() {
11256
+ if (this.inboxChunksInFlight < this.config.maxConcurrentInboxChunks) {
11257
+ this.inboxChunksInFlight++;
11258
+ return Promise.resolve();
11259
+ }
11260
+ return new Promise((resolve) => {
11261
+ this.inboxSlotWaiters.push(resolve);
11262
+ });
11263
+ }
11264
+ /** Hands the slot straight to a waiter rather than freeing it. */
11265
+ releaseInboxSlot() {
11266
+ const waiter = this.inboxSlotWaiters.shift();
11267
+ if (waiter !== void 0) {
11268
+ waiter();
11269
+ return;
11270
+ }
11271
+ this.inboxChunksInFlight--;
10242
11272
  }
10243
11273
  async applyInboxJob(remote, syncOp) {
10244
11274
  if ((await this.refuseOnReceipt(remote, [syncOp])).size > 0) return;
10245
11275
  const operations = syncOp.operations.map((op) => op.operation);
10246
11276
  let jobInfo;
10247
11277
  try {
10248
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.meta.name });
11278
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, cursorProtectedLoadMeta(remote.meta.name));
10249
11279
  } catch (error) {
10250
11280
  if (this.isShutdown) return;
10251
11281
  const err = error instanceof Error ? error : new Error(String(error));
@@ -10335,14 +11365,15 @@ var SyncManager = class {
10335
11365
  }
10336
11366
  await this.applyInboxJob(remote, syncOp);
10337
11367
  }
10338
- async applyInboxBatch(received) {
11368
+ /** Enqueues one chunk's loads; the resolution runs outside the lane. */
11369
+ async enqueueInboxBatch(received) {
10339
11370
  const refused = /* @__PURE__ */ new Set();
10340
11371
  for (const { remote, syncOp } of received) {
10341
11372
  const refusals = await this.refuseOnReceipt(remote, [syncOp]);
10342
11373
  for (const refusal of refusals) refused.add(refusal);
10343
11374
  }
10344
11375
  const items = received.filter(({ syncOp }) => !refused.has(syncOp));
10345
- if (items.length === 0) return;
11376
+ if (items.length === 0) return void 0;
10346
11377
  const sourceRemote = items[0].remote.meta.name;
10347
11378
  const chunkKeys = new Set(items.map(({ syncOp }) => syncOp.jobId));
10348
11379
  const jobs = items.map(({ syncOp }) => {
@@ -10372,9 +11403,9 @@ var SyncManager = class {
10372
11403
  const request = { jobs };
10373
11404
  let result;
10374
11405
  try {
10375
- result = await this.reactor.loadBatch(request, this.abortController.signal, { sourceRemote });
11406
+ result = await this.reactor.loadBatch(request, this.abortController.signal, cursorProtectedLoadMeta(sourceRemote));
10376
11407
  } catch (error) {
10377
- if (this.isShutdown) return;
11408
+ if (this.isShutdown) return void 0;
10378
11409
  for (const { remote, syncOp } of items) {
10379
11410
  const err = error instanceof Error ? error : new Error(String(error));
10380
11411
  if (carriesMarker(syncOp)) {
@@ -10387,7 +11418,7 @@ var SyncManager = class {
10387
11418
  }
10388
11419
  return;
10389
11420
  }
10390
- if (this.isShutdown) return;
11421
+ if (this.isShutdown) return void 0;
10391
11422
  for (const plan of jobs) {
10392
11423
  if (!(plan.key in result.jobs)) continue;
10393
11424
  const info = result.jobs[plan.key];
@@ -10395,51 +11426,59 @@ var SyncManager = class {
10395
11426
  const fifoKey = `${plan.documentId}:${plan.scope}:${plan.branch}`;
10396
11427
  this.lastEnqueuedJobIdByKey.set(fifoKey, info.id);
10397
11428
  }
10398
- for (const { remote, syncOp } of items) {
10399
- if (!(syncOp.jobId in result.jobs)) {
10400
- this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.meta.name, syncOp.documentId, syncOp.jobId);
10401
- const error = new ChannelError("inbox", /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
10402
- syncOp.failed(error);
10403
- remote.channel.deadLetter.add(syncOp);
10404
- remote.channel.inbox.remove(syncOp);
10405
- continue;
10406
- }
10407
- const jobInfo = result.jobs[syncOp.jobId];
10408
- let completedJobInfo;
10409
- try {
10410
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
10411
- } catch (error) {
10412
- if (this.isShutdown) continue;
10413
- const err = error instanceof Error ? error : new Error(String(error));
10414
- if (carriesMarker(syncOp)) {
10415
- this.retryMarker(remote, syncOp, err.message);
10416
- continue;
10417
- }
10418
- syncOp.failed(new ChannelError("inbox", err));
10419
- remote.channel.deadLetter.add(syncOp);
10420
- remote.channel.inbox.remove(syncOp);
10421
- continue;
10422
- }
11429
+ return {
11430
+ items,
11431
+ result
11432
+ };
11433
+ }
11434
+ /** Each item awaits its own job, so one deferred load holds no other. */
11435
+ async resolveInboxBatch(enqueued) {
11436
+ await Promise.all(enqueued.items.map(({ remote, syncOp }) => this.resolveInboxItem(remote, syncOp, enqueued.result)));
11437
+ }
11438
+ async resolveInboxItem(remote, syncOp, result) {
11439
+ if (!(syncOp.jobId in result.jobs)) {
11440
+ this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.meta.name, syncOp.documentId, syncOp.jobId);
11441
+ const error = new ChannelError("inbox", /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
11442
+ syncOp.failed(error);
11443
+ remote.channel.deadLetter.add(syncOp);
11444
+ remote.channel.inbox.remove(syncOp);
11445
+ return;
11446
+ }
11447
+ const jobInfo = result.jobs[syncOp.jobId];
11448
+ let completedJobInfo;
11449
+ try {
11450
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
11451
+ } catch (error) {
10423
11452
  if (this.isShutdown) return;
10424
- let resolved = false;
10425
- if (completedJobInfo.status !== "FAILED") {
10426
- syncOp.executed();
10427
- if (carriesMarker(syncOp)) this.tombstone(syncOp.documentId);
10428
- resolved = true;
10429
- } else if (isPurgedFailure(completedJobInfo.error, syncOp.documentId)) {
10430
- this.tombstone(syncOp.documentId);
10431
- syncOp.executed();
10432
- resolved = true;
10433
- } else if (carriesMarker(syncOp) && !isRefusedMarker(completedJobInfo.error)) {
10434
- this.retryMarker(remote, syncOp, completedJobInfo.error?.message || "Unknown error");
10435
- continue;
10436
- } else {
10437
- syncOp.failed(this.inboxFailure(syncOp, completedJobInfo.error));
10438
- remote.channel.deadLetter.add(syncOp);
11453
+ const err = error instanceof Error ? error : new Error(String(error));
11454
+ if (carriesMarker(syncOp)) {
11455
+ this.retryMarker(remote, syncOp, err.message);
11456
+ return;
10439
11457
  }
11458
+ syncOp.failed(new ChannelError("inbox", err));
11459
+ remote.channel.deadLetter.add(syncOp);
10440
11460
  remote.channel.inbox.remove(syncOp);
10441
- if (resolved) await this.dropRequeuedDeadLetter(syncOp.id, remote.meta.name);
11461
+ return;
11462
+ }
11463
+ if (this.isShutdown) return;
11464
+ let resolved = false;
11465
+ if (completedJobInfo.status !== "FAILED") {
11466
+ syncOp.executed();
11467
+ if (carriesMarker(syncOp)) this.tombstone(syncOp.documentId);
11468
+ resolved = true;
11469
+ } else if (isPurgedFailure(completedJobInfo.error, syncOp.documentId)) {
11470
+ this.tombstone(syncOp.documentId);
11471
+ syncOp.executed();
11472
+ resolved = true;
11473
+ } else if (carriesMarker(syncOp) && !isRefusedMarker(completedJobInfo.error)) {
11474
+ this.retryMarker(remote, syncOp, completedJobInfo.error?.message || "Unknown error");
11475
+ return;
11476
+ } else {
11477
+ syncOp.failed(this.inboxFailure(syncOp, completedJobInfo.error));
11478
+ remote.channel.deadLetter.add(syncOp);
10442
11479
  }
11480
+ remote.channel.inbox.remove(syncOp);
11481
+ if (resolved) await this.dropRequeuedDeadLetter(syncOp.id, remote.meta.name);
10443
11482
  }
10444
11483
  /**
10445
11484
  * The dead letter for a load job the executor failed.
@@ -10671,6 +11710,7 @@ var SyncBuilder = class {
10671
11710
  holdStorage;
10672
11711
  receivedMarkerStorage;
10673
11712
  purgeRefusalStorage;
11713
+ storageFlusher = new NoopStorageFlusher();
10674
11714
  config = {};
10675
11715
  withChannelFactory(factory) {
10676
11716
  this.channelFactory = factory;
@@ -10684,6 +11724,11 @@ var SyncBuilder = class {
10684
11724
  this.cursorStorage = storage;
10685
11725
  return this;
10686
11726
  }
11727
+ /** The barrier every cursor write, including a custom storage's, goes behind. */
11728
+ withStorageFlusher(flusher) {
11729
+ this.storageFlusher = flusher;
11730
+ return this;
11731
+ }
10687
11732
  withDeadLetterStorage(storage) {
10688
11733
  this.deadLetterStorage = storage;
10689
11734
  return this;
@@ -10708,6 +11753,10 @@ var SyncBuilder = class {
10708
11753
  this.config.maxInboxBatchSize = limit;
10709
11754
  return this;
10710
11755
  }
11756
+ withMaxConcurrentInboxChunks(limit) {
11757
+ this.config.maxConcurrentInboxChunks = limit;
11758
+ return this;
11759
+ }
10711
11760
  withMaxHeldOperationsPerRemote(limit) {
10712
11761
  this.config.maxHeldOperationsPerRemote = limit;
10713
11762
  return this;
@@ -10722,7 +11771,7 @@ var SyncBuilder = class {
10722
11771
  buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes, watermark, localPeer) {
10723
11772
  if (!this.channelFactory) throw new Error("Channel factory is required");
10724
11773
  const remoteStorage = this.remoteStorage ?? new KyselySyncRemoteStorage(db);
10725
- const cursorStorage = this.cursorStorage ?? new KyselySyncCursorStorage(db);
11774
+ const cursorStorage = new FlushGuardedSyncCursorStorage(this.cursorStorage ?? new KyselySyncCursorStorage(db), this.storageFlusher);
10726
11775
  const deadLetterStorage = this.deadLetterStorage ?? new KyselySyncDeadLetterStorage(db);
10727
11776
  const holdStorage = this.holdStorage ?? new KyselySyncHoldStorage(db);
10728
11777
  const receivedMarkerStorage = this.receivedMarkerStorage ?? new KyselySyncReceivedMarkerStorage(db);
@@ -10735,7 +11784,8 @@ var SyncBuilder = class {
10735
11784
  holdStorage,
10736
11785
  channelFactory: this.channelFactory,
10737
11786
  syncManager,
10738
- syncAdmin: syncManager
11787
+ syncAdmin: syncManager,
11788
+ syncInspector: syncManager
10739
11789
  };
10740
11790
  }
10741
11791
  };
@@ -10748,6 +11798,38 @@ async function createDefaultDatabase() {
10748
11798
  return new Kysely({ dialect: new HardenedPGliteDialect(new PGlite()) });
10749
11799
  }
10750
11800
  //#endregion
11801
+ //#region src/inspector/storage-facts.ts
11802
+ const UNKNOWN_STORAGE_FACTS = Object.freeze({
11803
+ engine: "unknown",
11804
+ persistence: "unknown",
11805
+ durable: false,
11806
+ selfHeal: false
11807
+ });
11808
+ const POSTGRES_STORAGE_FACTS = Object.freeze({
11809
+ engine: "postgres",
11810
+ persistence: "server",
11811
+ durable: true,
11812
+ selfHeal: false
11813
+ });
11814
+ const IN_MEMORY_PGLITE_STORAGE_FACTS = Object.freeze({
11815
+ engine: "pglite",
11816
+ persistence: "memory",
11817
+ durable: false,
11818
+ selfHeal: false
11819
+ });
11820
+ const PGLITE_PATH_STORAGE_FACTS = Object.freeze({
11821
+ engine: "pglite",
11822
+ persistence: "path",
11823
+ durable: true,
11824
+ selfHeal: false
11825
+ });
11826
+ const PGLITE_IDB_STORAGE_FACTS = Object.freeze({
11827
+ engine: "pglite",
11828
+ persistence: "idb",
11829
+ durable: true,
11830
+ selfHeal: false
11831
+ });
11832
+ //#endregion
10751
11833
  //#region src/shared/factories.ts
10752
11834
  /**
10753
11835
  * Factory method to create a ShutdownStatus that can be updated
@@ -11322,7 +12404,10 @@ var Reactor = class {
11322
12404
  status: "FAILED",
11323
12405
  createdAtUtcIso: now,
11324
12406
  completedAtUtcIso: now,
11325
- error: toErrorInfo$1("Job not found"),
12407
+ error: {
12408
+ ...toErrorInfo$1("Job not found"),
12409
+ name: JOB_NOT_FOUND_ERROR_NAME
12410
+ },
11326
12411
  consistencyToken: {
11327
12412
  version: 1,
11328
12413
  createdAtUtcIso: now,
@@ -11401,6 +12486,19 @@ function validateBuiltInKindCoverage(preReadyKinds, postReadyKinds) {
11401
12486
  const problems = [missing.length > 0 ? `never named: ${missing.join(", ")} (would be indexed by no shard, leaving those reads permanently stale and their consistency-token waits unresolvable)` : void 0, duplicated.length > 0 ? `named more than once: ${duplicated.join(", ")} (would be indexed twice per operation)` : void 0].filter((problem) => problem !== void 0);
11402
12487
  throw new Error(`withProjectionShards requires preReadyKinds and postReadyKinds to name each built-in read model (${BUILT_IN_READ_MODEL_KINDS.join(", ")}) exactly once between them; ${problems.join("; ")}`);
11403
12488
  }
12489
+ function schemeChannelDescriptor(scheme) {
12490
+ switch (scheme) {
12491
+ case "connect": return {
12492
+ type: "gql",
12493
+ create: (logger, jwtHandler, queue) => new GqlRequestChannelFactory(logger, jwtHandler, queue)
12494
+ };
12495
+ case "switchboard": return {
12496
+ type: POLLING_CHANNEL_TYPE,
12497
+ create: (logger) => new GqlResponseChannelFactory(logger)
12498
+ };
12499
+ default: throw new Error(`Unsupported channel scheme: ${JSON.stringify(scheme)}`);
12500
+ }
12501
+ }
11404
12502
  var ReactorBuilder = class {
11405
12503
  logger;
11406
12504
  documentModelSources = [];
@@ -11421,6 +12519,8 @@ var ReactorBuilder = class {
11421
12519
  readModelCoordinator;
11422
12520
  readModelCoordinatorFactory;
11423
12521
  kyselyInstance;
12522
+ storageFactsOverride;
12523
+ groupCommitPGlite;
11424
12524
  signer;
11425
12525
  workerSigner;
11426
12526
  trustPolicy;
@@ -11428,6 +12528,7 @@ var ReactorBuilder = class {
11428
12528
  signalHandlersEnabled = false;
11429
12529
  queueInstance;
11430
12530
  channelScheme;
12531
+ additionalChannelFactories = /* @__PURE__ */ new Map();
11431
12532
  jwtHandler;
11432
12533
  documentModelLoader;
11433
12534
  shutdownHooks = [];
@@ -11585,6 +12686,23 @@ var ReactorBuilder = class {
11585
12686
  this.kyselyInstance = kysely;
11586
12687
  return this;
11587
12688
  }
12689
+ /** Declares the store's facts; without it a caller-supplied store is unknown. */
12690
+ withStorageFacts(facts) {
12691
+ this.storageFactsOverride = facts;
12692
+ return this;
12693
+ }
12694
+ /**
12695
+ * Runs the reactor on an embedded PGlite with group commit: statements stop
12696
+ * syncing the filesystem, and the flush moves to a job's write-ready and
12697
+ * every sync cursor write. A poisoned session goes to `onUnrecoverable`,
12698
+ * since a fallback would leave in-memory read-model positions ahead of the
12699
+ * store. The caller keeps `pg` if the build fails; once built, the module's
12700
+ * `groupCommitStorage` closes it.
12701
+ */
12702
+ withGroupCommitPGlite(options) {
12703
+ this.groupCommitPGlite = options;
12704
+ return this;
12705
+ }
11588
12706
  /**
11589
12707
  * Register an externally-constructed pg.Pool's {@link PoolInstrumentation}
11590
12708
  * so it surfaces through {@link ReactorModule.pools}. Use this when the
@@ -11600,11 +12718,24 @@ var ReactorBuilder = class {
11600
12718
  this.queueInstance = queue;
11601
12719
  return this;
11602
12720
  }
11603
- /** Mutually exclusive with {@link withSync}; setting both fails the build. */
12721
+ /**
12722
+ * Mutually exclusive with {@link withSync}; setting both fails the build.
12723
+ * Compose further transports onto the scheme with {@link withAdditionalChannelFactory}.
12724
+ */
11604
12725
  withChannelScheme(scheme) {
11605
12726
  this.channelScheme = scheme;
11606
12727
  return this;
11607
12728
  }
12729
+ /**
12730
+ * Routes `type` to `factory` beside the {@link withChannelScheme} scheme's
12731
+ * own factory, through a {@link CompositeChannelFactory}. Build refuses it
12732
+ * without a scheme, or when `type` is the scheme's own.
12733
+ */
12734
+ withAdditionalChannelFactory(type, factory) {
12735
+ if (this.additionalChannelFactories.has(type)) throw new Error(`A channel factory for the type "${type}" is already registered on this ReactorBuilder`);
12736
+ this.additionalChannelFactories.set(type, factory);
12737
+ return this;
12738
+ }
11608
12739
  withJwtHandler(handler) {
11609
12740
  this.jwtHandler = handler;
11610
12741
  return this;
@@ -11680,6 +12811,43 @@ var ReactorBuilder = class {
11680
12811
  return (await this.buildModule()).reactor;
11681
12812
  }
11682
12813
  async buildModule() {
12814
+ this.assertStorageConfiguration();
12815
+ const groupCommit = this.groupCommitPGlite ? createGroupCommitStorage(this.groupCommitPGlite) : void 0;
12816
+ try {
12817
+ return await this.assembleModule(groupCommit);
12818
+ } catch (error) {
12819
+ groupCommit?.restoreStatementSync();
12820
+ throw error;
12821
+ }
12822
+ }
12823
+ /** Refuses what would bypass the flush or leave a poison with no owner. */
12824
+ assertStorageConfiguration() {
12825
+ const options = this.groupCommitPGlite;
12826
+ if (options === void 0) return;
12827
+ if (typeof options.onUnrecoverable !== "function") throw new Error("withGroupCommitPGlite needs onUnrecoverable: a poisoned session is never reused, so without it the store stays wedged");
12828
+ if (this.kyselyInstance !== void 0) throw new Error("withGroupCommitPGlite and withKysely are mutually exclusive: both supply the reactor's database");
12829
+ if (this.workerPool !== void 0 || this.projectionShardConfig) throw new Error("withGroupCommitPGlite cannot be combined with withWorkerPool or withProjectionShards: those open their own Postgres connections and never see the embedded store or its flush");
12830
+ if (this.executorManager !== void 0) throw new Error("withExecutor cannot be combined with withGroupCommitPGlite: only the executor this builder constructs flushes before announcing a job write-ready");
12831
+ this.fitJobTimeoutToDurability(options);
12832
+ }
12833
+ /**
12834
+ * A committed job waits for its flush, the statement in flight then one
12835
+ * sync, and withholds past that wait. Statements before the commit queue
12836
+ * behind the same flush, so the job timeout outlasts the wait too.
12837
+ */
12838
+ fitJobTimeoutToDurability(options) {
12839
+ const requested = this.executorConfig.durabilityWaitMs;
12840
+ const durabilityWaitMs = requested ?? (options.dialect?.statementTimeoutMs ?? 12e4) + (options.client?.flushSyncTimeoutMs ?? 12e4);
12841
+ if (!(Number.isFinite(durabilityWaitMs) && durabilityWaitMs > 0)) throw new Error(requested === void 0 ? `withGroupCommitPGlite needs a positive, finite durabilityWaitMs when its statement and flush sync bounds add up to ${durabilityWaitMs}: otherwise a committed job whose flush never lands waits forever` : `withGroupCommitPGlite needs a positive, finite durabilityWaitMs (got ${requested}): otherwise a committed job whose flush never lands waits forever`);
12842
+ const configured = this.executorConfig.jobTimeoutMs;
12843
+ if (configured !== void 0 && configured <= durabilityWaitMs) throw new Error(`jobTimeoutMs (${configured}) must exceed withGroupCommitPGlite's durability wait (${durabilityWaitMs}ms: statement + flush sync bounds): statements before a job's commit queue behind the same flush`);
12844
+ this.executorConfig = {
12845
+ ...this.executorConfig,
12846
+ durabilityWaitMs,
12847
+ jobTimeoutMs: configured ?? durabilityWaitMs + 3e4
12848
+ };
12849
+ }
12850
+ async assembleModule(groupCommit) {
11683
12851
  if (!this.logger) this.logger = new ConsoleLogger(["reactor"]);
11684
12852
  const featureFlags = resolveFeatureFlags(this.executorConfig.featureFlags);
11685
12853
  if (this.executorConfig.protocolSupport === void 0) this.executorConfig = {
@@ -11688,6 +12856,7 @@ var ReactorBuilder = class {
11688
12856
  };
11689
12857
  if (this.channelScheme !== void 0 && this.syncBuilder !== void 0) throw new Error("withChannelScheme and withSync are mutually exclusive; use one (the scheme builds its own SyncBuilder)");
11690
12858
  if (this.readModelCoordinator !== void 0 && this.readModelCoordinatorFactory !== void 0) throw new Error("withReadModelCoordinator and withReadModelCoordinatorFactory are mutually exclusive; register one coordinator source");
12859
+ this.assertAdditionalChannelFactories();
11691
12860
  if (this.projectionShardConfig !== void 0 && this.readModelCoordinatorFactory !== void 0) throw new Error("withProjectionShards and withReadModelCoordinatorFactory are mutually exclusive; the factory owns the projection worker through its createProjectionShardManager dependency");
11692
12861
  if (this.projectionShardConfig !== void 0 && this.readModelFactories.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModelFactory; projection workers cannot receive host-only factory dependencies");
11693
12862
  if (this.projectionShardConfig !== void 0 && this.readModels.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModel; projection workers build their own read models from the shard config and would silently omit these");
@@ -11712,11 +12881,13 @@ var ReactorBuilder = class {
11712
12881
  for (const result of results) if (result.status === "error") this.logger.error("Failed to register document model: @error", result.error.message);
11713
12882
  }
11714
12883
  const reactorDbConfig = this.resolveReactorDbConfig();
11715
- const baseDatabase = this.kyselyInstance ?? (reactorDbConfig ? await this.createPostgresDatabase(reactorDbConfig) : await createDefaultDatabase());
12884
+ const baseDatabase = this.kyselyInstance ?? groupCommit?.db ?? (reactorDbConfig ? await this.createPostgresDatabase(reactorDbConfig) : await createDefaultDatabase());
12885
+ const storageFacts = this.storageFactsOverride ?? (this.kyselyInstance || groupCommit ? UNKNOWN_STORAGE_FACTS : reactorDbConfig ? POSTGRES_STORAGE_FACTS : IN_MEMORY_PGLITE_STORAGE_FACTS);
11716
12886
  if (this.migrationStrategy === "auto") {
11717
12887
  const result = await runMigrations(baseDatabase, REACTOR_SCHEMA);
11718
12888
  if (!result.success && result.error) throw new Error(`Database migration failed: ${result.error.message}`);
11719
12889
  }
12890
+ await groupCommit?.flusher.flush();
11720
12891
  await checkStoredProtocols(baseDatabase, REACTOR_SCHEMA, this.executorConfig.protocolSupport ?? {}, this.unsupportedStoredDocuments, this.logger);
11721
12892
  const database = baseDatabase.withSchema(REACTOR_SCHEMA);
11722
12893
  const operationStore = new KyselyOperationStore(database);
@@ -11773,7 +12944,7 @@ var ReactorBuilder = class {
11773
12944
  executorManager = poolManager;
11774
12945
  executorStartCount = pool.numWorkers;
11775
12946
  if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
11776
- } else executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.driveContainerTypes, this.executorConfig, executionScope, this.signer, this.trustPolicy), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
12947
+ } else executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.driveContainerTypes, this.executorConfig, executionScope, this.signer, this.trustPolicy, groupCommit?.flusher), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
11777
12948
  }
11778
12949
  await executorManager.start(executorStartCount);
11779
12950
  const callerReadModels = Array.from(/* @__PURE__ */ new Set([...this.readModels]));
@@ -11868,10 +13039,12 @@ var ReactorBuilder = class {
11868
13039
  };
11869
13040
  let syncModule = void 0;
11870
13041
  if (this.channelScheme) {
11871
- const factory = this.channelScheme === "connect" ? new GqlRequestChannelFactory(this.logger, this.jwtHandler, queue) : new GqlResponseChannelFactory(this.logger);
11872
- syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes, settledWatermark, localPeer);
13042
+ const syncBuilder = new SyncBuilder().withChannelFactory(this.buildSchemeChannelFactory(this.channelScheme, this.logger, queue));
13043
+ if (groupCommit) syncBuilder.withStorageFlusher(groupCommit.flusher);
13044
+ syncModule = syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes, settledWatermark, localPeer);
11873
13045
  await syncModule.syncManager.startup();
11874
13046
  } else if (this.syncBuilder) {
13047
+ if (groupCommit) this.syncBuilder.withStorageFlusher(groupCommit.flusher);
11875
13048
  syncModule = this.syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes, settledWatermark, localPeer);
11876
13049
  await syncModule.syncManager.startup();
11877
13050
  }
@@ -11909,13 +13082,34 @@ var ReactorBuilder = class {
11909
13082
  degradedComponents,
11910
13083
  catchUp,
11911
13084
  settledWatermark,
11912
- documentPurgeService: new DocumentPurgeService(queue, jobTracker, eventBus)
13085
+ documentPurgeService: new DocumentPurgeService(queue, jobTracker, eventBus),
13086
+ storageFacts: { ...storageFacts },
13087
+ groupCommitStorage: groupCommit && {
13088
+ health: groupCommit.health,
13089
+ close: () => groupCommit.close()
13090
+ }
11913
13091
  };
11914
13092
  catchUp.start();
11915
13093
  if (degradedComponents.length > 0) this.logger.warn("Reactor started with @count degraded component(s): @components", degradedComponents.length, degradedComponents.map(({ component }) => component).join(", "));
11916
13094
  if (this.signalHandlersEnabled) this.attachSignalHandlers(module);
11917
13095
  return module;
11918
13096
  }
13097
+ assertAdditionalChannelFactories() {
13098
+ if (this.additionalChannelFactories.size === 0) return;
13099
+ if (!this.channelScheme) {
13100
+ const types = [...this.additionalChannelFactories.keys()].join(", ");
13101
+ throw new Error(`withAdditionalChannelFactory([${types}]) needs a withChannelScheme to compose with: without a scheme there is no factory to compose, and a withSync SyncBuilder owns its own. Pass a CompositeChannelFactory to that SyncBuilder instead.`);
13102
+ }
13103
+ const { type } = schemeChannelDescriptor(this.channelScheme);
13104
+ if (this.additionalChannelFactories.has(type)) throw new Error(`withAdditionalChannelFactory("${type}", ...) collides with the "${this.channelScheme}" channel scheme, which already serves that channel type`);
13105
+ }
13106
+ /** Bare without additional factories, so an existing reactor routes as before. */
13107
+ buildSchemeChannelFactory(scheme, logger, queue) {
13108
+ const { type, create } = schemeChannelDescriptor(scheme);
13109
+ const schemeFactory = create(logger, this.jwtHandler, queue);
13110
+ if (this.additionalChannelFactories.size === 0) return schemeFactory;
13111
+ return new CompositeChannelFactory([[type, schemeFactory], ...this.additionalChannelFactories]);
13112
+ }
11919
13113
  /**
11920
13114
  * The single Postgres config for the parent, executor workers, and
11921
13115
  * projection shards. They must share one physical database (the parent
@@ -11976,7 +13170,7 @@ var ReactorBuilder = class {
11976
13170
  poolInstrumentations.push(forwarder);
11977
13171
  this.instrumentedPools.push(forwarder);
11978
13172
  }
11979
- const { ProjectionShardManager } = await import("./projection-shard-manager-CQ5FwQmv.js");
13173
+ const { ProjectionShardManager } = await import("./projection-shard-manager-Drl9w8Eo.js");
11980
13174
  const manager = new ProjectionShardManager({
11981
13175
  shardCount: config.shardCount,
11982
13176
  db,
@@ -12021,7 +13215,7 @@ var ReactorBuilder = class {
12021
13215
  */
12022
13216
  async createDefaultWorkerFactory(numWorkers, db) {
12023
13217
  const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
12024
- import("./worker-handle-CN2-oRv-.js"),
13218
+ import("./worker-handle-Nso8i5eU.js"),
12025
13219
  import("./transport-CZDHzfvy.js"),
12026
13220
  Promise.resolve().then(() => worker_exports)
12027
13221
  ]);
@@ -12178,6 +13372,9 @@ var HybridProjectionCoordinator = class {
12178
13372
  else this.postReady.push(readModel);
12179
13373
  this.readModels.push(readModel);
12180
13374
  }
13375
+ removeReadModel(readModel) {
13376
+ return spliceReadModel(readModel, this.preReady, this.postReady, this.readModels);
13377
+ }
12181
13378
  getChainDepth() {
12182
13379
  return this.manager.getChainDepth() + this.chains.size;
12183
13380
  }
@@ -12214,11 +13411,7 @@ var HybridProjectionCoordinator = class {
12214
13411
  async runHostBatch(event, enqueuedAt, reservations) {
12215
13412
  const chainWaitDurationMs = performance.now() - enqueuedAt;
12216
13413
  const preReadyStart = performance.now();
12217
- try {
12218
- await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event, reservations)));
12219
- } catch (error) {
12220
- this.logger.error("Host pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
12221
- }
13414
+ await indexStage([...this.preReady], (readModel) => this.indexWithTiming(readModel, "pre_ready", event, reservations), (error) => this.logger.error("Host pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error));
12222
13415
  const preReadyDurationMs = performance.now() - preReadyStart;
12223
13416
  const emitStart = performance.now();
12224
13417
  try {
@@ -12228,11 +13421,7 @@ var HybridProjectionCoordinator = class {
12228
13421
  }
12229
13422
  const emitDurationMs = performance.now() - emitStart;
12230
13423
  const postReadyStart = performance.now();
12231
- try {
12232
- await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event, reservations)));
12233
- } catch (error) {
12234
- this.logger.error("Host post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
12235
- }
13424
+ await indexStage([...this.postReady], (readModel) => this.indexWithTiming(readModel, "post_ready", event, reservations), (error) => this.logger.error("Host post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error));
12236
13425
  const postReadyDurationMs = performance.now() - postReadyStart;
12237
13426
  this.emitBatchCompleted({
12238
13427
  jobId: event.jobId,
@@ -12618,394 +13807,813 @@ function firstOutOfOrderPair(operations, options) {
12618
13807
  }
12619
13808
  }
12620
13809
  //#endregion
12621
- //#region src/storage/kysely/pglite-dialect.ts
12622
- var pglite_dialect_exports = /* @__PURE__ */ __exportAll({
12623
- DEFAULT_LONG_STATEMENT_TIMEOUT_MS: () => DEFAULT_LONG_STATEMENT_TIMEOUT_MS,
12624
- DEFAULT_RECOVERY_TIMEOUT_MS: () => DEFAULT_RECOVERY_TIMEOUT_MS,
12625
- DEFAULT_STATEMENT_TIMEOUT_MS: () => DEFAULT_STATEMENT_TIMEOUT_MS,
12626
- HardenedPGliteDialect: () => HardenedPGliteDialect,
12627
- PGliteAbortedTransactionError: () => PGliteAbortedTransactionError,
12628
- PGliteAcquireTimeoutError: () => PGliteAcquireTimeoutError,
12629
- PGliteSessionError: () => PGliteSessionError,
12630
- PGliteSessionPoisonedError: () => PGliteSessionPoisonedError,
12631
- PGliteStatementTimeoutError: () => PGliteStatementTimeoutError,
12632
- isLongRunningStatement: () => isLongRunningStatement,
12633
- queryThroughDialect: () => queryThroughDialect
12634
- });
12635
- const DEFAULT_STATEMENT_TIMEOUT_MS = 12e4;
12636
- const DEFAULT_LONG_STATEMENT_TIMEOUT_MS = 9e5;
12637
- const DEFAULT_RECOVERY_TIMEOUT_MS = 15e3;
12638
- const LONG_STATEMENT_PREFIXES = [
12639
- "vacuum",
12640
- "analyze",
12641
- "reindex",
12642
- "cluster",
12643
- "checkpoint",
12644
- "copy",
12645
- "alter ",
12646
- "create ",
12647
- "drop ",
12648
- "truncate",
12649
- "refresh "
12650
- ];
12651
- /** Maintenance and DDL, matched on the leading keyword of the compiled SQL. */
12652
- function isLongRunningStatement(statement) {
12653
- const normalized = statement.trimStart().toLowerCase();
12654
- return LONG_STATEMENT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
13810
+ //#region src/read-models/interfaces.ts
13811
+ function supportsLiveReadModelRegistration(coordinator) {
13812
+ return "addReadModel" in coordinator && typeof coordinator.addReadModel === "function" && "removeReadModel" in coordinator && typeof coordinator.removeReadModel === "function";
12655
13813
  }
12656
- function summarize(statement) {
12657
- const collapsed = statement.replace(/\s+/g, " ").trim();
12658
- return collapsed.length > 160 ? `${collapsed.slice(0, 160)}...` : collapsed;
13814
+ //#endregion
13815
+ //#region src/sync/channels/channel-factory-types.ts
13816
+ /** The channel types a built factory declares; empty when it declares none. */
13817
+ function channelFactoryTypes(factory) {
13818
+ return factory.channelTypes ?? [];
12659
13819
  }
12660
- /** COMMIT in an aborted transaction silently rolls back; the guard raises first. */
12661
- const COMMIT_WITH_GUARD = "select 1 as __commit_guard; commit";
12662
- const PROBE = "select 1 as __session_probe";
12663
- const ABORTED_TRANSACTION_CODE = "25P02";
12664
- var PGliteSessionError = class extends Error {
12665
- cause;
12666
- constructor(message, cause) {
12667
- super(message);
12668
- this.cause = cause;
12669
- this.name = "PGliteSessionError";
12670
- }
12671
- };
12672
- var PGliteAcquireTimeoutError = class extends PGliteSessionError {
12673
- constructor(timeoutMs) {
12674
- super(`Timed out after ${timeoutMs}ms waiting for the PGlite connection. The single session is held by a statement that never completed: a transaction awaiting a nested query on the base handle, or an abandoned stream iterator.`);
12675
- this.name = "PGliteAcquireTimeoutError";
12676
- }
12677
- };
12678
- /** The call cannot be aborted, so it is abandoned and the session treated as poisoned. */
12679
- var PGliteStatementTimeoutError = class extends PGliteSessionError {
12680
- constructor(timeoutMs, statement) {
12681
- super(`A PGlite statement neither resolved nor rejected within ${timeoutMs}ms, so its wasm call is presumed dead and the session poisoned: ${summarize(statement)}`);
12682
- this.name = "PGliteStatementTimeoutError";
13820
+ //#endregion
13821
+ //#region src/sync/channels/local-wire.ts
13822
+ /**
13823
+ * Whether a received value is a well-formed wire message.
13824
+ *
13825
+ * A push is validated down to what `envelopesToSyncOperations` dereferences:
13826
+ * each envelope's `channelMeta.id` and, per operation, the `operation.action`
13827
+ * and the `context` fields it batches on.
13828
+ */
13829
+ function isLocalWireMessage(data) {
13830
+ if (!isObject(data)) return false;
13831
+ const kind = data.kind;
13832
+ if (kind === "push") return Array.isArray(data.envelopes) && data.envelopes.every(isEnvelope);
13833
+ return kind === "hello" || kind === "ack" || kind === "resend";
13834
+ }
13835
+ function isObject(value) {
13836
+ return typeof value === "object" && value !== null;
13837
+ }
13838
+ function isEnvelope(envelope) {
13839
+ if (!isObject(envelope)) return false;
13840
+ if (!isObject(envelope.channelMeta)) return false;
13841
+ if (typeof envelope.channelMeta.id !== "string") return false;
13842
+ const operations = envelope.operations;
13843
+ return operations === void 0 || operations === null || Array.isArray(operations) && operations.every(isWireOperation);
13844
+ }
13845
+ function isWireOperation(entry) {
13846
+ if (!isObject(entry)) return false;
13847
+ const { operation, context } = entry;
13848
+ return isObject(operation) && isObject(operation.action) && isObject(context) && typeof context.documentId === "string" && typeof context.scope === "string" && typeof context.branch === "string" && typeof context.ordinal === "number";
13849
+ }
13850
+ //#endregion
13851
+ //#region src/sync/channels/local-channel.ts
13852
+ /** Base delay for the backoff that re-pushes after a transient transport fault. */
13853
+ const PUSH_RETRY_BASE_DELAY_MS = 500;
13854
+ /** Ceiling for that backoff. */
13855
+ const PUSH_RETRY_MAX_DELAY_MS = 3e4;
13856
+ /**
13857
+ * A symmetric, push-capable synchronization channel between two reactors over a
13858
+ * message port.
13859
+ *
13860
+ * Unlike the gql channels -- one polls and pushes over HTTP, the other never
13861
+ * initiates and is filled by resolvers -- a LocalChannel is peer-to-peer: either
13862
+ * side may push envelopes to the other at any time, with no GraphQL, no HTTP and
13863
+ * no Switchboard. It is the productionized sibling of the test channel: a real
13864
+ * wire protocol over an injected {@link LocalChannelPort}, the peer-manifest
13865
+ * handshake wired into the sync manager, cursor persistence, and ACK-driven
13866
+ * outbox trimming.
13867
+ *
13868
+ * The inbox holds its ack below unapplied purge markers, as the gql channels'
13869
+ * inboxes do.
13870
+ */
13871
+ var LocalChannel = class {
13872
+ inbox;
13873
+ outbox;
13874
+ deadLetter;
13875
+ logger;
13876
+ channelId;
13877
+ remoteName;
13878
+ cursorStorage;
13879
+ port;
13880
+ collectionId;
13881
+ filter;
13882
+ isShutdown = false;
13883
+ unsubscribeTransport;
13884
+ localManifestProvider;
13885
+ /** Undefined until the first handshake; null for a peer that announces nothing. */
13886
+ peerManifest = void 0;
13887
+ peerManifestCallbacks = /* @__PURE__ */ new Set();
13888
+ connectionState = "connecting";
13889
+ /** Whether any valid frame has arrived since init. */
13890
+ heardPeer = false;
13891
+ failureCount = 0;
13892
+ lastSuccessUtcMs;
13893
+ lastFailureUtcMs;
13894
+ /** The highest inbox ack already reported to the peer; only an advance re-posts. */
13895
+ lastPostedAckOrdinal = 0;
13896
+ pushFailureCount = 0;
13897
+ pushRetryTimer;
13898
+ connectionStateCallbacks = /* @__PURE__ */ new Set();
13899
+ cursorWriters = {
13900
+ inbox: {
13901
+ persisted: 0,
13902
+ requested: 0,
13903
+ tail: Promise.resolve()
13904
+ },
13905
+ outbox: {
13906
+ persisted: 0,
13907
+ requested: 0,
13908
+ tail: Promise.resolve()
13909
+ }
13910
+ };
13911
+ constructor(logger, channelId, remoteName, cursorStorage, port, collectionId, filter) {
13912
+ this.logger = logger;
13913
+ this.channelId = channelId;
13914
+ this.remoteName = remoteName;
13915
+ this.cursorStorage = cursorStorage;
13916
+ this.port = port;
13917
+ this.collectionId = collectionId;
13918
+ this.filter = filter;
13919
+ this.inbox = new Mailbox({ holdAckBelowMarkers: true });
13920
+ this.outbox = new Mailbox();
13921
+ this.deadLetter = new Mailbox();
13922
+ this.outbox.onAdded((added) => this.onOutboxAdded(added));
13923
+ this.outbox.onRemoved((syncOps) => this.onOutboxRemoved(syncOps));
13924
+ this.inbox.onRemoved(() => this.onInboxRemoved());
12683
13925
  }
12684
- };
12685
- var PGliteAbortedTransactionError = class extends PGliteSessionError {
12686
- constructor(cause) {
12687
- super(`Refusing to report a commit for an aborted transaction: COMMIT would have been answered with a ROLLBACK tag and nothing written. Underlying error: ${errorOf(cause).message}`, cause);
12688
- this.name = "PGliteAbortedTransactionError";
13926
+ /**
13927
+ * Loads the persisted cursors and initialises both mailboxes BEFORE
13928
+ * subscribing to the transport, so no inbound message is ever processed
13929
+ * against a zeroed mailbox. A frame processed before `inbox.init` runs would
13930
+ * be added below an ack of 0, and the init that followed would overwrite the
13931
+ * ack while leaving that frame pinning the floor -- dragging the live ack
13932
+ * below the durable cursor and re-loading already-applied ops as fresh jobs.
13933
+ *
13934
+ * Loading first relies on the port holding frames until a listener attaches.
13935
+ * A `LocalChannelPortRegistry` port does: it keeps one listener on the
13936
+ * raw port for its whole registration and replays, in order, what arrived
13937
+ * while no channel was attached. A raw browser MessagePort handed straight to
13938
+ * a channel does not once started, so a frame arriving between a shutdown
13939
+ * and the next init on it is lost.
13940
+ */
13941
+ async init() {
13942
+ const cursors = await this.cursorStorage.list(this.remoteName);
13943
+ if (this.isShutdown) return;
13944
+ const inboxOrdinal = cursors.find((c) => c.cursorType === "inbox")?.cursorOrdinal ?? 0;
13945
+ const outboxOrdinal = cursors.find((c) => c.cursorType === "outbox")?.cursorOrdinal ?? 0;
13946
+ this.inbox.init(inboxOrdinal);
13947
+ this.outbox.init(outboxOrdinal);
13948
+ this.cursorWriters.inbox.persisted = inboxOrdinal;
13949
+ this.cursorWriters.outbox.persisted = outboxOrdinal;
13950
+ this.lastPostedAckOrdinal = inboxOrdinal;
13951
+ this.unsubscribeTransport = this.port.onMessage((data) => this.receive(data));
13952
+ this.sendHello(false);
12689
13953
  }
12690
- };
12691
- /** No statement can clear the session (e.g. a stuck active portal). */
12692
- var PGliteSessionPoisonedError = class extends PGliteSessionError {
12693
- constructor(cause) {
12694
- super(`The PGlite session is unrecoverable and cannot be reused; the component owning it must be restarted. Original error: ${errorOf(cause).message}`, cause);
12695
- this.name = "PGliteSessionPoisonedError";
13954
+ /** Leaves the port open: whoever registered it closes it. */
13955
+ async shutdown() {
13956
+ this.isShutdown = true;
13957
+ this.clearPushRetry();
13958
+ this.unsubscribeTransport?.();
13959
+ this.unsubscribeTransport = void 0;
13960
+ this.transitionConnectionState("disconnected");
13961
+ return Promise.resolve();
12696
13962
  }
12697
- };
12698
- function errorOf(error) {
12699
- return error instanceof Error ? error : new Error(String(error));
12700
- }
12701
- const TIMED_OUT = Symbol("pglite-deadline-expired");
12702
- /** Races a call that cannot be cancelled; on {@link TIMED_OUT} the call may still settle later. */
12703
- async function withDeadline(pending, timeoutMs) {
12704
- let handle;
12705
- const expiry = new Promise((resolve) => {
12706
- handle = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
12707
- });
12708
- try {
12709
- return await Promise.race([pending, expiry]);
12710
- } finally {
12711
- clearTimeout(handle);
13963
+ getConnectionState() {
13964
+ return {
13965
+ state: this.connectionState,
13966
+ failureCount: this.failureCount,
13967
+ lastSuccessUtcMs: this.lastSuccessUtcMs ?? 0,
13968
+ lastFailureUtcMs: this.lastFailureUtcMs ?? 0,
13969
+ pushBlocked: this.pushRetryTimer !== void 0,
13970
+ pushFailureCount: this.pushFailureCount,
13971
+ receivingPages: false,
13972
+ requiresAuth: false
13973
+ };
12712
13974
  }
12713
- }
12714
- function isAbortedTransactionError(error) {
12715
- if (error?.code === ABORTED_TRANSACTION_CODE) return true;
12716
- return errorOf(error).message.includes("current transaction is aborted");
12717
- }
12718
- /** `kysely-pglite-dialect` hardened for one shared session that a fault must not brick. */
12719
- var HardenedPGliteDialect = class {
12720
- client;
12721
- inner;
12722
- options;
12723
- constructor(client, options = {}) {
12724
- this.client = client;
12725
- this.inner = new PGliteDialect(client);
12726
- this.options = {
12727
- acquireTimeoutMs: options.acquireTimeoutMs ?? 0,
12728
- statementTimeoutMs: options.statementTimeoutMs ?? 12e4,
12729
- longStatementTimeoutMs: options.longStatementTimeoutMs ?? 9e5,
12730
- recoveryTimeoutMs: options.recoveryTimeoutMs ?? 15e3,
12731
- onDiagnostic: options.onDiagnostic ?? ((message, error) => {
12732
- console.error(`[pglite-dialect] ${message}`, error);
12733
- }),
12734
- onPoisoned: options.onPoisoned ?? (() => void 0)
13975
+ onConnectionStateChange(callback) {
13976
+ this.connectionStateCallbacks.add(callback);
13977
+ return () => {
13978
+ this.connectionStateCallbacks.delete(callback);
13979
+ };
13980
+ }
13981
+ /**
13982
+ * A push transport has nothing to poll, so this asks the peer to re-push
13983
+ * whatever it still holds unacked for this side, from this side's inbox ack.
13984
+ */
13985
+ triggerPull() {
13986
+ if (this.isShutdown) return;
13987
+ this.post({
13988
+ kind: "resend",
13989
+ channelId: this.channelId,
13990
+ sinceOrdinal: this.inbox.ackOrdinal
13991
+ });
13992
+ }
13993
+ /** This channel pushes to its peer; it has no holder to hear from. */
13994
+ notePoll() {}
13995
+ /** No holder, so nothing this channel reports may strand one. */
13996
+ lastHolderPollUtcMs() {}
13997
+ setLocalManifest(provider) {
13998
+ this.localManifestProvider = provider;
13999
+ }
14000
+ onPeerManifest(callback) {
14001
+ this.peerManifestCallbacks.add(callback);
14002
+ return () => {
14003
+ this.peerManifestCallbacks.delete(callback);
12735
14004
  };
12736
14005
  }
12737
- createAdapter() {
12738
- return this.inner.createAdapter();
14006
+ /** Pushes freshly added outbox items that have not since been removed. */
14007
+ onOutboxAdded(added) {
14008
+ if (this.isShutdown) return;
14009
+ const syncOps = added.filter((op) => this.outbox.get(op.id) === op);
14010
+ if (syncOps.length > 0) this.pushOutbox(syncOps);
12739
14011
  }
12740
- createDriver() {
12741
- return new HardenedPGliteDriver(this.inner.createDriver(), this.client, this.options);
14012
+ /**
14013
+ * Persists the outbox cursor from the applied ordinal of what was removed,
14014
+ * clamped below the lowest ordinal still in flight. Items for different
14015
+ * documents apply out of order, so the highest applied ordinal can pass one
14016
+ * still unacked; a restart from it would skip that one.
14017
+ */
14018
+ onOutboxRemoved(syncOps) {
14019
+ const ordinal = Math.min(getLatestAppliedOrdinal(syncOps), this.unappliedOutboxFloor() - 1);
14020
+ this.persistCursor("outbox", ordinal);
12742
14021
  }
12743
- createQueryCompiler() {
12744
- return this.inner.createQueryCompiler();
14022
+ /**
14023
+ * Persists the inbox ack and reports it to the peer so it trims its outbox.
14024
+ * The ack is posted only when it strictly advances, so a burst of removals
14025
+ * that does not move the floor does not flood the peer with duplicate acks.
14026
+ */
14027
+ onInboxRemoved() {
14028
+ const ackOrdinal = this.inbox.ackOrdinal;
14029
+ this.persistCursor("inbox", ackOrdinal);
14030
+ if (ackOrdinal > this.lastPostedAckOrdinal) this.postAck();
12745
14031
  }
12746
- createIntrospector(db) {
12747
- return this.inner.createIntrospector(db);
14032
+ /** Unconditional, so a peer re-pushing what this side applied can trim. */
14033
+ postAck() {
14034
+ const ackOrdinal = this.inbox.ackOrdinal;
14035
+ this.lastPostedAckOrdinal = Math.max(this.lastPostedAckOrdinal, ackOrdinal);
14036
+ this.post({
14037
+ kind: "ack",
14038
+ channelId: this.channelId,
14039
+ ackOrdinal
14040
+ });
12748
14041
  }
12749
- };
12750
- var HardenedPGliteConnection = class {
12751
- inner;
12752
- guard;
12753
- failure = void 0;
12754
- rollbackFailure = void 0;
12755
- transactionOpen = false;
12756
- constructor(inner, guard) {
12757
- this.inner = inner;
12758
- this.guard = guard;
14042
+ receive(data) {
14043
+ if (this.isShutdown) return;
14044
+ if (!isLocalWireMessage(data)) {
14045
+ this.recordFailure(/* @__PURE__ */ new Error("LocalChannel received a malformed wire message"));
14046
+ return;
14047
+ }
14048
+ this.heardPeer = true;
14049
+ this.markSuccess();
14050
+ this.transitionConnectionState("connected");
14051
+ switch (data.kind) {
14052
+ case "hello":
14053
+ this.receiveHello(data);
14054
+ return;
14055
+ case "push":
14056
+ this.receivePush(data);
14057
+ return;
14058
+ case "ack":
14059
+ if (data.ackOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, data.ackOrdinal);
14060
+ return;
14061
+ case "resend":
14062
+ this.receiveResend(data);
14063
+ return;
14064
+ }
12759
14065
  }
12760
- /** Never retried: a replay could commit alone a write meant for another consumer's transaction. */
12761
- async executeQuery(compiledQuery) {
12762
- const generation = this.guard.generation;
14066
+ /**
14067
+ * A HELLO's sinceOrdinal is the peer's inbox ack, so the outbox is trimmed to
14068
+ * it exactly as an ACK would, letting a reconnect resume without re-serving
14069
+ * applied ops. An opening hello then re-pushes the remaining unacked items in
14070
+ * case the peer reconnected and lost them. A reply does not: it answers this
14071
+ * side's own opening, so the peer did not reset, and re-pushing would hand
14072
+ * it every unacked op a second time.
14073
+ */
14074
+ receiveHello(message) {
14075
+ if (message.sinceOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, message.sinceOrdinal);
14076
+ if (!message.reply) this.sendHello(true);
14077
+ this.hearPeer(message.manifest);
14078
+ if (!message.reply) this.rePushUnacked();
14079
+ }
14080
+ /**
14081
+ * Ingests a push into the inbox, dropping operations the inbox has already
14082
+ * acked. A reconnecting peer may re-push durable ops before it hears this
14083
+ * side's ack; adding one back as a fresh sync job would transiently drag the
14084
+ * ack floor below the persisted cursor, so a batch entirely at or below the
14085
+ * ack is skipped, and the ack is posted again so the peer can trim them. A
14086
+ * frame {@link isLocalWireMessage} rejects never reaches here; one that still
14087
+ * fails conversion is recorded as a failure.
14088
+ */
14089
+ receivePush(message) {
14090
+ const ackFloor = this.inbox.ackOrdinal;
14091
+ const syncOps = [];
14092
+ let dropped = false;
14093
+ let converted;
12763
14094
  try {
12764
- return await this.guard.runStatement(compiledQuery.sql, () => this.inner.executeQuery(compiledQuery));
14095
+ converted = message.envelopes.flatMap((envelope) => envelopesToSyncOperations(envelope, this.remoteName));
12765
14096
  } catch (error) {
12766
- this.guard.recordFailure(this, generation, error);
12767
- throw error;
14097
+ this.recordFailure(error instanceof Error ? error : new Error(String(error)));
14098
+ return;
12768
14099
  }
12769
- }
12770
- streamQuery(compiledQuery, chunkSize) {
12771
- const inner = this.inner.streamQuery(compiledQuery, chunkSize);
12772
- return this.deadlinedStream(compiledQuery, inner);
12773
- }
12774
- /** Upstream issues the whole query on the first pull, so each pull is bounded like a statement. */
12775
- async *deadlinedStream(compiledQuery, inner) {
12776
- for (;;) {
12777
- const generation = this.guard.generation;
12778
- let next;
12779
- try {
12780
- next = await this.guard.runStatement(compiledQuery.sql, () => inner.next());
12781
- } catch (error) {
12782
- this.guard.recordFailure(this, generation, error);
12783
- throw error;
14100
+ for (const syncOp of converted) {
14101
+ if (this.highestOrdinal(syncOp) <= ackFloor) {
14102
+ dropped = true;
14103
+ continue;
12784
14104
  }
12785
- if (next.done === true) return;
12786
- yield next.value;
14105
+ syncOp.transported();
14106
+ syncOps.push(syncOp);
14107
+ }
14108
+ if (dropped) this.postAck();
14109
+ if (syncOps.length === 0) return;
14110
+ try {
14111
+ this.inbox.add(...syncOps);
14112
+ } catch (error) {
14113
+ this.recordFailure(error instanceof Error ? error : new Error(String(error)));
12787
14114
  }
12788
14115
  }
12789
- get suspect() {
12790
- return this.failure !== void 0 || this.rollbackFailure !== void 0 || this.transactionOpen;
14116
+ receiveResend(message) {
14117
+ if (message.sinceOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, message.sinceOrdinal);
14118
+ this.rePushUnacked();
12791
14119
  }
12792
- };
12793
- var HardenedPGliteDriver = class {
12794
- inner;
12795
- client;
12796
- options;
12797
- /** Set when release could not reset the session; each acquire retries the reset. */
12798
- sessionFault = void 0;
12799
- /** Lets an abandoned call's late settlement be told apart from current failures. */
12800
- statementGeneration = 0;
12801
- /** A call abandoned at its deadline; PGlite would queue every later call behind it, so none is issued. */
12802
- deadCall = void 0;
12803
- poisonReported = false;
12804
- constructor(inner, client, options) {
12805
- this.inner = inner;
12806
- this.client = client;
12807
- this.options = options;
14120
+ sendHello(reply) {
14121
+ this.post({
14122
+ kind: "hello",
14123
+ channelId: this.channelId,
14124
+ collectionId: this.collectionId.key,
14125
+ filter: this.filter,
14126
+ sinceOrdinal: this.inbox.ackOrdinal,
14127
+ manifest: this.localManifestProvider?.() ?? null,
14128
+ ...reply ? { reply } : {}
14129
+ });
12808
14130
  }
12809
- get generation() {
12810
- return this.statementGeneration;
14131
+ /**
14132
+ * Pushes the given outbox items to the peer, marking them in flight.
14133
+ *
14134
+ * A transport throw is classified rather than treated as terminal: only a
14135
+ * structured-clone failure, which recurs identically on every retry, dead-
14136
+ * letters its ops. Any other fault is transient -- the port is momentarily
14137
+ * unusable -- so the ops stay in the outbox and {@link handlePushFailure}
14138
+ * schedules a backoff re-push, keeping behaviour in step with the gql
14139
+ * sibling's recoverable/unrecoverable split.
14140
+ */
14141
+ pushOutbox(syncOps) {
14142
+ if (this.isShutdown || syncOps.length === 0) return;
14143
+ for (const syncOp of syncOps) syncOp.started();
14144
+ try {
14145
+ this.port.postMessage({
14146
+ kind: "push",
14147
+ channelId: this.channelId,
14148
+ envelopes: this.envelopesFor(syncOps)
14149
+ });
14150
+ } catch (error) {
14151
+ this.handlePushFailure(syncOps, error instanceof Error ? error : new Error(String(error)));
14152
+ return;
14153
+ }
14154
+ this.clearPushRetry();
14155
+ if (this.heardPeer) {
14156
+ this.markSuccess();
14157
+ this.transitionConnectionState("connected");
14158
+ } else this.transitionConnectionState("connecting");
12811
14159
  }
12812
- /** Bounds one statement; an expiry is escalated as a poisoned session. */
12813
- async runStatement(statement, execute) {
12814
- if (this.deadCall !== void 0) throw new PGliteSessionPoisonedError(this.deadCall);
12815
- const timeoutMs = this.timeoutFor(statement);
12816
- if (timeoutMs <= 0) return execute();
12817
- const generation = this.statementGeneration;
12818
- const outcome = await withDeadline(execute(), timeoutMs);
12819
- if (outcome !== TIMED_OUT) return outcome;
12820
- throw this.escalateHungStatement(new PGliteStatementTimeoutError(timeoutMs, statement), generation);
14160
+ /** Re-pushes every outbox item the peer has not acknowledged. */
14161
+ rePushUnacked() {
14162
+ if (this.isShutdown) return;
14163
+ const unacked = this.outbox.items.filter((syncOp) => syncOp.status !== 2);
14164
+ this.pushOutbox(unacked);
14165
+ }
14166
+ /**
14167
+ * Routes a push transport failure: a structured-clone (`DataCloneError`)
14168
+ * failure is unrecoverable and dead-letters its ops; any other error is a
14169
+ * transient transport fault, so the ops are left in the outbox to be
14170
+ * re-pushed by the backoff retry and by the next hello/resend recovery.
14171
+ */
14172
+ handlePushFailure(syncOps, error) {
14173
+ if (this.classifyPostError(error) === "unrecoverable") {
14174
+ const channelError = new ChannelError("outbox", error);
14175
+ for (const syncOp of syncOps) syncOp.failed(channelError);
14176
+ this.deadLetter.add(...syncOps);
14177
+ this.outbox.remove(...syncOps);
14178
+ this.recordFailure(error);
14179
+ return;
14180
+ }
14181
+ this.recordFailure(error);
14182
+ this.schedulePushRetry();
12821
14183
  }
12822
- recordFailure(connection, generation, error) {
12823
- if (generation === this.statementGeneration) connection.failure = errorOf(error);
14184
+ /** A structured-clone failure cannot self-heal; every other fault can. */
14185
+ classifyPostError(error) {
14186
+ return error.name === "DataCloneError" ? "unrecoverable" : "recoverable";
12824
14187
  }
12825
- async init() {
12826
- await this.inner.init();
14188
+ /** Schedules a single backoff re-push of the unacked outbox. */
14189
+ schedulePushRetry() {
14190
+ if (this.isShutdown || this.pushRetryTimer !== void 0) return;
14191
+ const delay = calculateBackoffDelay(this.pushFailureCount, PUSH_RETRY_BASE_DELAY_MS, PUSH_RETRY_MAX_DELAY_MS, Math.random());
14192
+ this.pushFailureCount++;
14193
+ this.pushRetryTimer = setTimeout(() => {
14194
+ this.pushRetryTimer = void 0;
14195
+ this.rePushUnacked();
14196
+ }, delay);
12827
14197
  }
12828
- async acquireConnection() {
12829
- const innerConnection = await this.acquireWithTimeout();
12830
- if (this.deadCall !== void 0) {
12831
- await this.inner.releaseConnection(innerConnection);
12832
- throw new PGliteSessionPoisonedError(this.deadCall);
14198
+ /** Clears the backoff state once a push lands or the channel stops. */
14199
+ clearPushRetry() {
14200
+ this.pushFailureCount = 0;
14201
+ if (this.pushRetryTimer !== void 0) {
14202
+ clearTimeout(this.pushRetryTimer);
14203
+ this.pushRetryTimer = void 0;
12833
14204
  }
12834
- if (this.sessionFault !== void 0) {
12835
- const fault = this.sessionFault;
12836
- let recovered = false;
12837
- try {
12838
- recovered = await this.tryRecoverSession(false);
12839
- } finally {
12840
- if (!recovered) await this.inner.releaseConnection(innerConnection);
14205
+ }
14206
+ /** One envelope per SyncOperation, with key/dependsOn for batch ordering. */
14207
+ envelopesFor(syncOps) {
14208
+ const jobIdToKeys = /* @__PURE__ */ new Map();
14209
+ const envelopes = [];
14210
+ for (let i = 0; i < syncOps.length; i++) {
14211
+ const syncOp = syncOps[i];
14212
+ const key = String(i);
14213
+ if (syncOp.jobId) {
14214
+ const keys = jobIdToKeys.get(syncOp.jobId) ?? [];
14215
+ keys.push(key);
14216
+ jobIdToKeys.set(syncOp.jobId, keys);
12841
14217
  }
12842
- if (!recovered) {
12843
- this.reportPoisoned(fault);
12844
- throw new PGliteSessionPoisonedError(fault);
14218
+ const dependsOn = [];
14219
+ for (const dep of syncOp.jobDependencies) {
14220
+ const depKeys = jobIdToKeys.get(dep);
14221
+ if (depKeys) dependsOn.push(...depKeys);
12845
14222
  }
12846
- this.sessionFault = void 0;
14223
+ envelopes.push({
14224
+ type: "operations",
14225
+ channelMeta: { id: this.channelId },
14226
+ operations: syncOp.operations,
14227
+ key,
14228
+ dependsOn
14229
+ });
12847
14230
  }
12848
- return new HardenedPGliteConnection(innerConnection, this);
14231
+ return envelopes.map((envelope) => serializeEnvelope(envelope));
12849
14232
  }
12850
- /** Kysely never rolls back after a failed BEGIN, so an aborted session is reset here. */
12851
- async beginTransaction(connection, settings) {
12852
- const wrapper = asWrapper(connection);
12853
- const generation = this.statementGeneration;
12854
- try {
12855
- await this.runStatement("begin", () => this.inner.beginTransaction(wrapper.inner, settings));
12856
- } catch (error) {
12857
- this.recordFailure(wrapper, generation, error);
12858
- if (!isAbortedTransactionError(error)) throw error;
12859
- if (!await this.tryRecoverSession(false)) {
12860
- this.sessionFault = errorOf(error);
12861
- throw error;
12862
- }
12863
- const retryGeneration = this.statementGeneration;
14233
+ /**
14234
+ * Hears the peer's manifest, deduped like the gql channel: an older or
14235
+ * identical revision is ignored, so a reconnect that re-announces the same
14236
+ * manifest does not re-run the sync manager's hold reconciliation.
14237
+ */
14238
+ async hearPeer(manifest) {
14239
+ if (this.peerManifest !== void 0 && (this.peerManifest?.revision ?? null) === (manifest?.revision ?? null) || isOlderManifest(manifest, this.peerManifest)) return;
14240
+ this.peerManifest = manifest;
14241
+ await Promise.all([...this.peerManifestCallbacks].map(async (callback) => {
12864
14242
  try {
12865
- await this.runStatement("begin", () => this.inner.beginTransaction(wrapper.inner, settings));
12866
- } catch (retryError) {
12867
- this.recordFailure(wrapper, retryGeneration, retryError);
12868
- throw retryError;
14243
+ await callback(manifest);
14244
+ } catch (error) {
14245
+ this.logger.error("LocalChannel @ChannelId peer manifest callback error: @Error", this.channelId, error);
12869
14246
  }
12870
- wrapper.failure = void 0;
12871
- }
12872
- wrapper.transactionOpen = true;
14247
+ }));
12873
14248
  }
12874
- async commitTransaction(connection) {
12875
- const wrapper = asWrapper(connection);
12876
- const generation = this.statementGeneration;
14249
+ /** Sends a message, returning whether the transport accepted it. */
14250
+ post(message) {
14251
+ if (this.isShutdown) return false;
12877
14252
  try {
12878
- await this.runStatement(COMMIT_WITH_GUARD, () => this.client.exec(COMMIT_WITH_GUARD));
14253
+ this.port.postMessage(message);
14254
+ return true;
12879
14255
  } catch (error) {
12880
- this.recordFailure(wrapper, generation, error);
12881
- if (isAbortedTransactionError(error)) throw new PGliteAbortedTransactionError(error);
12882
- throw error;
14256
+ this.recordFailure(error instanceof Error ? error : new Error(String(error)));
14257
+ return false;
12883
14258
  }
12884
- wrapper.transactionOpen = false;
12885
- wrapper.failure = void 0;
12886
14259
  }
12887
- /** Swallowed so the original failure propagates; release resets the session. */
12888
- async rollbackTransaction(connection) {
12889
- const wrapper = asWrapper(connection);
12890
- const generation = this.statementGeneration;
12891
- try {
12892
- await this.runStatement("rollback", () => this.inner.rollbackTransaction(wrapper.inner));
12893
- wrapper.transactionOpen = false;
14260
+ /** Leaves the push retry alone: only a push that lands clears it. */
14261
+ markSuccess() {
14262
+ this.lastSuccessUtcMs = Date.now();
14263
+ this.failureCount = 0;
14264
+ }
14265
+ recordFailure(error) {
14266
+ this.failureCount++;
14267
+ this.lastFailureUtcMs = Date.now();
14268
+ this.logger.error("LocalChannel @ChannelId transport error (@FailureCount): @Error", this.channelId, this.failureCount, error);
14269
+ this.transitionConnectionState("error");
14270
+ }
14271
+ transitionConnectionState(next) {
14272
+ if (this.connectionState === next) return;
14273
+ this.connectionState = next;
14274
+ const snapshot = this.getConnectionState();
14275
+ for (const callback of this.connectionStateCallbacks) try {
14276
+ callback(snapshot);
12894
14277
  } catch (error) {
12895
- if (generation === this.statementGeneration) wrapper.rollbackFailure = errorOf(error);
12896
- this.options.onDiagnostic("rollback failed; preserving the original failure and recovering the session on release", error);
14278
+ this.logger.error("LocalChannel @ChannelId connection state callback error: @Error", this.channelId, error);
12897
14279
  }
12898
14280
  }
12899
- async releaseConnection(connection) {
12900
- const wrapper = asWrapper(connection);
12901
- try {
12902
- if (wrapper.suspect && this.deadCall === void 0) {
12903
- if (!await this.tryRecoverSession(wrapper.transactionOpen)) {
12904
- this.sessionFault = wrapper.rollbackFailure ?? wrapper.failure ?? /* @__PURE__ */ new Error("session left in a transaction");
12905
- this.options.onDiagnostic("the PGlite session could not be reset and is being marked unrecoverable", this.sessionFault);
12906
- }
14281
+ /** The greatest operation ordinal a sync op carries, or 0 when it carries none. */
14282
+ highestOrdinal(syncOp) {
14283
+ let highest = 0;
14284
+ for (const op of syncOp.operations) if (op.context.ordinal > highest) highest = op.context.ordinal;
14285
+ return highest;
14286
+ }
14287
+ /** The lowest ordinal of an outbox item the peer has not applied. */
14288
+ unappliedOutboxFloor() {
14289
+ let floor = Number.POSITIVE_INFINITY;
14290
+ for (const syncOp of this.outbox.items) {
14291
+ if (syncOp.status === 2) continue;
14292
+ for (const op of syncOp.operations) {
14293
+ const ordinal = op.context.ordinal;
14294
+ if (ordinal > 0 && ordinal < floor) floor = ordinal;
12907
14295
  }
12908
- } finally {
12909
- await this.inner.releaseConnection(wrapper.inner);
12910
14296
  }
14297
+ return floor;
12911
14298
  }
12912
- async destroy() {
12913
- await this.inner.destroy();
14299
+ /**
14300
+ * Advances a persisted cursor only once the write has landed, and never with
14301
+ * two writes for the same row outstanding. See the gql request channel for
14302
+ * the full argument: concurrent writers for one row are last-writer-wins, and
14303
+ * the storage promises no FIFO, so a burst of removals is coalesced into one
14304
+ * ordered write rather than a race.
14305
+ */
14306
+ persistCursor(cursorType, ordinal) {
14307
+ const writer = this.cursorWriters[cursorType];
14308
+ if (ordinal <= Math.max(writer.persisted, writer.requested)) return;
14309
+ writer.requested = ordinal;
14310
+ writer.tail = writer.tail.then(() => this.writeCursor(cursorType));
12914
14311
  }
12915
- reportPoisoned(cause) {
12916
- if (this.poisonReported) return;
12917
- this.poisonReported = true;
14312
+ /** One link of a cursor write chain; never rejects, so the chain survives. */
14313
+ async writeCursor(cursorType) {
14314
+ const writer = this.cursorWriters[cursorType];
14315
+ const ordinal = writer.requested;
14316
+ if (ordinal <= writer.persisted) return;
14317
+ writer.requested = 0;
12918
14318
  try {
12919
- this.options.onPoisoned(cause);
14319
+ await this.cursorStorage.upsert({
14320
+ remoteName: this.remoteName,
14321
+ cursorType,
14322
+ cursorOrdinal: ordinal,
14323
+ lastSyncedAtUtcMs: Date.now()
14324
+ });
12920
14325
  } catch (error) {
12921
- this.options.onDiagnostic("onPoisoned threw", error);
14326
+ this.logger.error("LocalChannel @ChannelId failed to persist @CursorType cursor at @Ordinal; the watermark stays put so the next advance retries it: @Error", this.channelId, cursorType, ordinal, error);
14327
+ return;
12922
14328
  }
14329
+ writer.persisted = Math.max(writer.persisted, ordinal);
12923
14330
  }
12924
- timeoutFor(statement) {
12925
- return isLongRunningStatement(statement) ? this.options.longStatementTimeoutMs : this.options.statementTimeoutMs;
14331
+ };
14332
+ //#endregion
14333
+ //#region src/sync/channels/local-channel-factory.ts
14334
+ /** The {@link ChannelConfig.type} a LocalChannel is created from. */
14335
+ const LOCAL_CHANNEL_TYPE = "local";
14336
+ /**
14337
+ * Factory for {@link LocalChannel} instances.
14338
+ *
14339
+ * The channel's config is `{ type: "local", parameters: { peerId, channelName } }`
14340
+ * -- no url, because there is no server. The factory does not take a port
14341
+ * directly: {@link IChannelFactory.instance} has no seam for a live object, and
14342
+ * a MessagePort is not clone-safe config that could ride in `parameters`.
14343
+ * Instead the factory holds a {@link LocalChannelTransportProvider} and resolves
14344
+ * the port from the (peerId, channelName) the config names. W1.2 supplies a real
14345
+ * brokered MessagePort by registering it with the provider under that key, with
14346
+ * no change to the channel or this factory.
14347
+ */
14348
+ var LocalChannelFactory = class {
14349
+ channelTypes = [LOCAL_CHANNEL_TYPE];
14350
+ logger;
14351
+ transportProvider;
14352
+ constructor(logger, transportProvider) {
14353
+ this.logger = logger;
14354
+ this.transportProvider = transportProvider;
14355
+ }
14356
+ instance(remoteId, remoteName, config, cursorStorage, collectionId, filter, _operationIndex, _options) {
14357
+ if (config.type !== "local") throw new Error(`This reactor has no "${config.type}" channel factory: it is provisioned local-only, and LocalChannelFactory builds only "${LOCAL_CHANNEL_TYPE}" channels`);
14358
+ const peerId = config.parameters.peerId;
14359
+ if (typeof peerId !== "string" || !peerId) throw new Error("LocalChannelFactory requires a non-empty \"peerId\" string in config.parameters");
14360
+ const channelName = config.parameters.channelName;
14361
+ if (typeof channelName !== "string" || !channelName) throw new Error("LocalChannelFactory requires a non-empty \"channelName\" string in config.parameters");
14362
+ const port = this.transportProvider(peerId, channelName);
14363
+ if (!port) throw new Error(`LocalChannelFactory has no transport for peer '${peerId}' channel '${channelName}'`);
14364
+ return new LocalChannel(this.logger, remoteId, remoteName, cursorStorage, port, collectionId, filter);
12926
14365
  }
12927
- /** Marks the session faulted once per generation; a stale expiry reports only itself. */
12928
- escalateHungStatement(cause, generation) {
12929
- if (generation !== this.statementGeneration) return cause;
12930
- this.statementGeneration += 1;
12931
- this.deadCall = cause;
12932
- this.reportPoisoned(cause);
12933
- this.options.onDiagnostic("a PGlite statement never settled within its deadline; treating the session as poisoned", cause);
12934
- return new PGliteSessionPoisonedError(cause);
14366
+ };
14367
+ //#endregion
14368
+ //#region src/sync/channels/local-channel-registry.ts
14369
+ const DEFAULT_MAX_QUEUED_FRAMES = 1024;
14370
+ function frameKind(data) {
14371
+ return typeof data === "object" && data !== null ? data.kind : void 0;
14372
+ }
14373
+ /**
14374
+ * The port a channel sees. It holds the registry's one listener on the raw port
14375
+ * for the whole registration, so a frame that arrives between two channels
14376
+ * (a reset) is queued and replayed to the next one instead of lost.
14377
+ *
14378
+ * Past the bound, every queued push is discarded, and so is every later one
14379
+ * until a channel is attached and hears a live hello: the inbox ack is the
14380
+ * highest applied ordinal, so delivering pushes after a gap would ack past the
14381
+ * missing ones. The peer still holds them unacked and re-pushes them right
14382
+ * after the hello it answers the fresh channel's hello with, or after its own
14383
+ * opening hello's backfill.
14384
+ */
14385
+ var RegisteredPort = class {
14386
+ raw;
14387
+ label;
14388
+ logger;
14389
+ maxQueuedFrames;
14390
+ subscriber;
14391
+ queued = [];
14392
+ discardingPushes = false;
14393
+ detachRaw;
14394
+ constructor(raw, label, logger, maxQueuedFrames) {
14395
+ this.raw = raw;
14396
+ this.label = label;
14397
+ this.logger = logger;
14398
+ this.maxQueuedFrames = maxQueuedFrames;
14399
+ this.detachRaw = raw.onMessage((data) => this.dispatch(data));
12935
14400
  }
12936
- /** A waiter that gave up still passes the lease on, so it cannot wedge the queue. */
12937
- async acquireWithTimeout() {
12938
- const timeoutMs = this.options.acquireTimeoutMs;
12939
- if (timeoutMs <= 0) return this.inner.acquireConnection();
12940
- let abandoned = false;
12941
- const pending = this.inner.acquireConnection().then((connection) => {
12942
- if (!abandoned) return connection;
12943
- this.inner.releaseConnection(connection).catch(() => void 0);
12944
- throw new PGliteAcquireTimeoutError(timeoutMs);
12945
- });
12946
- let handle;
12947
- const expiry = new Promise((_resolve, reject) => {
12948
- handle = setTimeout(() => {
12949
- abandoned = true;
12950
- reject(new PGliteAcquireTimeoutError(timeoutMs));
12951
- }, timeoutMs);
12952
- });
12953
- try {
12954
- return await Promise.race([pending, expiry]);
12955
- } finally {
12956
- clearTimeout(handle);
12957
- }
14401
+ postMessage(data) {
14402
+ this.raw.postMessage(data);
12958
14403
  }
12959
- /** {@link recoverSession}, with a recovery that throws counted as failed. */
12960
- async tryRecoverSession(mustEndTransaction) {
12961
- try {
12962
- return await this.recoverSession(mustEndTransaction);
12963
- } catch (error) {
12964
- this.options.onDiagnostic("session recovery threw", error);
12965
- return false;
14404
+ /**
14405
+ * One channel at a time; a newer attach supersedes an older one. A receive
14406
+ * that throws during replay detaches it and leaves the rest queued.
14407
+ */
14408
+ onMessage(callback) {
14409
+ this.subscriber = callback;
14410
+ const detach = () => {
14411
+ if (this.subscriber === callback) this.subscriber = void 0;
14412
+ };
14413
+ while (this.queued.length > 0 && this.subscriber === callback) {
14414
+ const frame = this.queued.shift();
14415
+ try {
14416
+ callback(frame);
14417
+ } catch (error) {
14418
+ detach();
14419
+ if (frameKind(frame) === "push") this.discardPushes();
14420
+ throw error;
14421
+ }
12966
14422
  }
14423
+ return detach;
12967
14424
  }
12968
- /** Rolls back only when told to or when the probe fails, sparing another consumer's live transaction. */
12969
- async recoverSession(mustEndTransaction) {
12970
- if (!mustEndTransaction && !this.client.isInTransaction() && await this.probeSession()) return true;
12971
- if (!await this.recoveryExec("rollback")) this.options.onDiagnostic("session rollback did not complete; the session stays suspect");
12972
- if (!await this.probeSession()) return false;
12973
- return !this.client.isInTransaction();
12974
- }
12975
- async probeSession() {
12976
- return this.recoveryExec(PROBE);
14425
+ /** The registry closes the raw port on unregister; a channel cannot. */
14426
+ close() {}
14427
+ release() {
14428
+ this.subscriber = void 0;
14429
+ this.queued.length = 0;
14430
+ this.detachRaw();
14431
+ this.raw.close();
12977
14432
  }
12978
- /** Kysely awaits release, so an unbounded recovery statement would hold the lease forever. */
12979
- async recoveryExec(statement) {
12980
- if (this.deadCall !== void 0) return false;
12981
- const timeoutMs = this.options.recoveryTimeoutMs;
12982
- const pending = this.client.exec(statement).then(() => true, (error) => {
12983
- this.options.onDiagnostic("recovery statement failed", error);
12984
- return false;
14433
+ dispatch(data) {
14434
+ if (this.discardingPushes) {
14435
+ const kind = frameKind(data);
14436
+ if (kind === "push") return;
14437
+ if (kind === "hello" && this.subscriber) this.discardingPushes = false;
14438
+ }
14439
+ if (this.subscriber) {
14440
+ this.subscriber(data);
14441
+ return;
14442
+ }
14443
+ this.queued.push(data);
14444
+ if (this.queued.length <= this.maxQueuedFrames) return;
14445
+ if (!this.discardingPushes) this.logger.warn("Local sync port @Label queued more than @Max frames with no channel attached; discarding queued pushes for the peer to re-push", this.label, this.maxQueuedFrames);
14446
+ this.discardPushes();
14447
+ this.compact();
14448
+ }
14449
+ /** Until a live hello, so no later push lands across the gap. */
14450
+ discardPushes() {
14451
+ this.discardingPushes = true;
14452
+ this.queued = this.queued.filter((frame) => frameKind(frame) !== "push");
14453
+ }
14454
+ /** Keeps only the latest hello or resend and the latest ack, in order. */
14455
+ compact() {
14456
+ const handshake = this.queued.findLast((frame) => {
14457
+ const kind = frameKind(frame);
14458
+ return kind === "hello" || kind === "resend";
12985
14459
  });
12986
- if (timeoutMs <= 0) return pending;
12987
- const outcome = await withDeadline(pending, timeoutMs);
12988
- if (outcome === TIMED_OUT) {
12989
- this.deadCall ??= new PGliteStatementTimeoutError(timeoutMs, statement);
12990
- this.reportPoisoned(this.deadCall);
12991
- this.options.onDiagnostic(`a PGlite recovery statement did not settle within ${timeoutMs}ms; the session is unrecoverable from SQL`);
12992
- return false;
14460
+ const ack = this.queued.findLast((frame) => frameKind(frame) === "ack");
14461
+ this.queued = this.queued.filter((frame) => frame === handshake || frame === ack);
14462
+ }
14463
+ };
14464
+ /** Brokered local-sync ports by (peerId, channelName); owns closing them. */
14465
+ var LocalChannelPortRegistry = class {
14466
+ ports = /* @__PURE__ */ new Map();
14467
+ closedKeys = /* @__PURE__ */ new Set();
14468
+ closed = false;
14469
+ logger;
14470
+ maxQueuedFrames;
14471
+ constructor(options = {}) {
14472
+ this.logger = options.logger ?? new ConsoleLogger(["local-sync"]);
14473
+ this.maxQueuedFrames = options.maxQueuedFrames ?? DEFAULT_MAX_QUEUED_FRAMES;
14474
+ }
14475
+ /** Hand this to the {@link LocalChannelFactory}. */
14476
+ provider = (peerId, channelName) => {
14477
+ const key = this.key(peerId, channelName);
14478
+ const port = this.ports.get(key);
14479
+ if (port) return port;
14480
+ if (this.closedKeys.has(key)) throw new Error(`Local sync port for peer '${peerId}' channel '${channelName}' has been closed; the link is severed and must be brokered again`);
14481
+ };
14482
+ /** Refuses a live key; a closed key may be registered again (a re-link). */
14483
+ register(peerId, channelName, port) {
14484
+ if (this.closed) throw new Error(`This reactor's local sync ports are closed; peer '${peerId}' channel '${channelName}' cannot be brokered here`);
14485
+ const key = this.key(peerId, channelName);
14486
+ if (this.ports.has(key)) throw new Error(`A local sync port is already registered for peer '${peerId}' channel '${channelName}'; unlink the existing link before brokering another`);
14487
+ this.closedKeys.delete(key);
14488
+ this.ports.set(key, new RegisteredPort(port, key, this.logger, this.maxQueuedFrames));
14489
+ }
14490
+ has(peerId, channelName) {
14491
+ return this.ports.has(this.key(peerId, channelName));
14492
+ }
14493
+ isClosed(peerId, channelName) {
14494
+ return this.closedKeys.has(this.key(peerId, channelName));
14495
+ }
14496
+ /** Detaches the registry's listener and closes the port. */
14497
+ unregister(peerId, channelName) {
14498
+ const key = this.key(peerId, channelName);
14499
+ const port = this.ports.get(key);
14500
+ this.ports.delete(key);
14501
+ this.closedKeys.add(key);
14502
+ port?.release();
14503
+ }
14504
+ /** Closes every port and refuses later registrations; the host is going away. */
14505
+ close() {
14506
+ this.closed = true;
14507
+ for (const [key, port] of [...this.ports]) {
14508
+ this.ports.delete(key);
14509
+ this.closedKeys.add(key);
14510
+ try {
14511
+ port.release();
14512
+ } catch (error) {
14513
+ this.logger.error("Closing local sync port @Label failed: @Error", key, error);
14514
+ }
12993
14515
  }
12994
- return outcome;
14516
+ }
14517
+ key(peerId, channelName) {
14518
+ return JSON.stringify([peerId, channelName]);
12995
14519
  }
12996
14520
  };
12997
- function asWrapper(connection) {
12998
- if (!(connection instanceof HardenedPGliteConnection)) throw new PGliteSessionError("Connection was not created by HardenedPGliteDialect");
12999
- return connection;
14521
+ /** `branch: ""` matches every branch. */
14522
+ const DEFAULT_LOCAL_FILTER = {
14523
+ documentId: [],
14524
+ scope: [],
14525
+ branch: ""
14526
+ };
14527
+ /** Session-scoped: no later boot can rebuild a brokered port. */
14528
+ const LOCAL_REMOTE_OPTIONS = {
14529
+ sinceTimestampUtcMs: "0",
14530
+ persistence: "session"
14531
+ };
14532
+ function localChannelConfig(peerId, channelName) {
14533
+ return {
14534
+ type: LOCAL_CHANNEL_TYPE,
14535
+ parameters: {
14536
+ peerId,
14537
+ channelName
14538
+ }
14539
+ };
13000
14540
  }
13001
- /** Raw SQL through the dialect's queue, instead of into whatever transaction holds the session. */
13002
- async function queryThroughDialect(db, sql, params) {
13003
- return [...(await db.executeQuery(CompiledQuery.raw(sql, params ?? []))).rows];
14541
+ /** `fromKey` splits on the last dot, so neither part may hold one. */
14542
+ function assertCollectionIdParts(driveId, branch) {
14543
+ if (driveId.includes(".")) throw new Error(`Drive id ${JSON.stringify(driveId)} contains a "." which the collection id format cannot carry; local sync needs a dot-free drive id`);
14544
+ if (branch.includes(".")) throw new Error(`Branch ${JSON.stringify(branch)} contains a "." which the collection id format cannot carry; local sync needs a dot-free branch`);
14545
+ }
14546
+ /** A dotted drive id parses as a dotted branch, so that is refused here. */
14547
+ function collectionIdFromKey(key) {
14548
+ const collectionId = DriveCollectionId.fromKey(key);
14549
+ assertCollectionIdParts(collectionId.driveId, collectionId.branch);
14550
+ return collectionId;
14551
+ }
14552
+ /** Registers before `add`, because `add` resolves the port through the factory. */
14553
+ async function registerLocalPeer(syncManager, registry, spec, port) {
14554
+ if (registry.has(spec.peerId, spec.channelName)) throw new Error(`This reactor already holds a local sync port for peer '${spec.peerId}' channel '${spec.channelName}'; unlink the existing link first`);
14555
+ if (syncManager.list().some((remote) => remote.meta.name === spec.remoteName)) throw new Error(`This reactor already has a remote named '${spec.remoteName}'; unlink the existing link first`);
14556
+ registry.register(spec.peerId, spec.channelName, port);
14557
+ try {
14558
+ return await syncManager.add(spec.remoteName, spec.collectionId, localChannelConfig(spec.peerId, spec.channelName), spec.filter, LOCAL_REMOTE_OPTIONS);
14559
+ } catch (error) {
14560
+ registry.unregister(spec.peerId, spec.channelName);
14561
+ throw error;
14562
+ }
14563
+ }
14564
+ /**
14565
+ * Waits out a running reset, then removes. The port is closed only once the
14566
+ * remote is gone, so a failed remove (a reset that started meanwhile) leaves
14567
+ * the link intact for a retry.
14568
+ */
14569
+ async function removeLocalPeer(syncManager, registry, spec) {
14570
+ await syncManager.resetSettled?.(spec.remoteName);
14571
+ try {
14572
+ await syncManager.remove(spec.remoteName);
14573
+ } catch (error) {
14574
+ if (!syncManager.list().some((remote) => remote.meta.name === spec.remoteName)) registry.unregister(spec.peerId, spec.channelName);
14575
+ throw error;
14576
+ }
14577
+ registry.unregister(spec.peerId, spec.channelName);
13004
14578
  }
13005
14579
  //#endregion
13006
- //#region src/read-models/interfaces.ts
13007
- function supportsLiveReadModelRegistration(coordinator) {
13008
- return "addReadModel" in coordinator && typeof coordinator.addReadModel === "function";
14580
+ //#region src/sync/channels/local-channel-transport.ts
14581
+ /**
14582
+ * Wraps a real MessagePort as a {@link LocalChannelPort}.
14583
+ *
14584
+ * A `node:worker_threads` port is an EventEmitter, so the EventEmitter surface
14585
+ * (`on`/`off`) is preferred when present. A browser port is driven through
14586
+ * `addEventListener` and an explicit `start()`, whose event carries the payload
14587
+ * on `.data`; once started it drops what arrives with no listener attached.
14588
+ * Register the result with a `LocalChannelPortRegistry`, which holds one
14589
+ * listener for the port's life and queues frames between channels.
14590
+ */
14591
+ function messagePortTransport(port) {
14592
+ return {
14593
+ postMessage(data) {
14594
+ port.postMessage(data);
14595
+ },
14596
+ onMessage(callback) {
14597
+ if (typeof port.on === "function" && typeof port.off === "function") {
14598
+ const listener = (value) => callback(value);
14599
+ port.on("message", listener);
14600
+ return () => {
14601
+ port.off?.("message", listener);
14602
+ };
14603
+ }
14604
+ const listener = (event) => {
14605
+ callback(event.data);
14606
+ };
14607
+ port.addEventListener?.("message", listener);
14608
+ port.start?.();
14609
+ return () => {
14610
+ port.removeEventListener?.("message", listener);
14611
+ };
14612
+ },
14613
+ close() {
14614
+ port.close();
14615
+ }
14616
+ };
13009
14617
  }
13010
14618
  //#endregion
13011
14619
  //#region src/processors/host-module.ts
@@ -13156,6 +14764,732 @@ var DocumentIntegrityService = class {
13156
14764
  }
13157
14765
  };
13158
14766
  //#endregion
13159
- export { ALWAYS_READABLE_SCOPES, APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, AuthEnforcementDisabledError, BareReadGate, BaseReadModel, BatchJobFailedError, CatchUpScheduler, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, ContiguousCursor, DEFAULT_COMMIT_CHUNK_SIZE, DEFAULT_DRIVE_CONTAINER_TYPES, DEFAULT_LONG_STATEMENT_TIMEOUT_MS, DEFAULT_MAX_PURGE_OPERATIONS, DEFAULT_PURGE_DELETE_BATCH, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_STATEMENT_TIMEOUT_MS, DOCUMENT_INDEXER_READ_MODEL, DOCUMENT_VIEW_READ_MODEL, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DeletedDocumentRead, DocumentAlreadyExistsError, DocumentChangeType, DocumentExistence, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DocumentNotDeletedError, DocumentPurgeService, DocumentPurgedError, DriveClient, DriveCollectionId, DriveRequestError, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, GroupInUseError, HardenedPGliteDialect, HybridProjectionCoordinator, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, InMemorySyncHoldStorage, InMemorySyncPurgeRefusalStorage, InMemorySyncReceivedMarkerStorage, IntervalPollTimer, InvalidModuleError, InvalidSignatureError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentPurger, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncHoldStorage, KyselySyncPurgeRefusalStorage, KyselySyncReceivedMarkerStorage, KyselySyncRemoteStorage, KyselyWriteCache, MAX_POLLED_REFUSALS, Mailbox, ModelNotWorkerImportableError, ModelReadGate, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PGliteAbortedTransactionError, PGliteAcquireTimeoutError, PGliteSessionError, PGliteSessionPoisonedError, PGliteStatementTimeoutError, PURGE_DOCUMENT, PURGE_LOCK_BUCKETS, PURGE_NS, PollBehavior, PollingChannelError, ProbeSettler, ProcessorManager, PropagationMode, PurgeTooLargeError, QueueEventTypes, REACTOR_SCHEMA, RECOVERABLE_GRAPHQL_ERROR_CODES, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RelationshipNotFoundError, ReservedActionError, RetryAccounting, RevisionMismatchError, SIGNATURE_REFUSAL_CODES, SeededStateReader, SettledWatermark, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncRepairRefusedError, SyncScopeGate, SyncStatus, SyncStatusTracker, UnsupportedStoredProtocolError, acquirePurgeLocks, addRelationshipAction, appliedDelete, authDecisionModel, batchOperationsByDocument, buildDecisionModel, classifyJobFailure, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createHybridProjectionCoordinatorFactory, createKyselyWatermarkProbe, createMutableShutdownStatus, createPeerAgreement, createReactorHostModuleBase, createRelationalDb, decideAtHead, defaultCatchUpConfig, defaultReadModelIndexingConfig, deleteDocumentAction, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, findPurged, firstOutOfOrderPair, getMigrationStatus, instrumentPgPool, isCatchUpConsumer, isDriveAuthError, isLongRunningStatement, isPurgeMarker, isRecoverableGraphQLError, listPurged, loadDocumentModelSpec, makeConsistencyKey, parseDriveUrl, parsePagingOptions, parseSnapshot, quarantinesDocument, queryThroughDialect, readDecisionModel, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, snapshotFunctionsFor, supportsDeliveryTracking, supportsLiveReadModelRegistration, supportsPurgeRefusals, syncOperationErrorType, trimMailboxFromAckOrdinal, unchunkedReadModelIndexingConfig, updateRelationshipAction, upgradeDocumentAction, verifyActionSignature, workerEntryPath };
14767
+ //#region src/inspector/drive-inspection.ts
14768
+ const DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive";
14769
+ const INTEGRITY_FIND_BATCH_SIZE = 500;
14770
+ const DEFAULT_DRIVE_PAGE_SIZE = 100;
14771
+ function readDriveState(doc) {
14772
+ const global = doc.state?.global;
14773
+ const rawNodes = Array.isArray(global?.nodes) ? global.nodes : [];
14774
+ const nodes = [];
14775
+ let folderCount = 0;
14776
+ let otherNodeCount = 0;
14777
+ let unreadableNodeCount = 0;
14778
+ for (const raw of rawNodes) {
14779
+ if (typeof raw !== "object" || raw === null) {
14780
+ unreadableNodeCount += 1;
14781
+ continue;
14782
+ }
14783
+ const node = raw;
14784
+ if (typeof node.id !== "string" || typeof node.kind !== "string") {
14785
+ unreadableNodeCount += 1;
14786
+ continue;
14787
+ }
14788
+ nodes.push({
14789
+ id: node.id,
14790
+ kind: node.kind,
14791
+ documentType: typeof node.documentType === "string" ? node.documentType : void 0
14792
+ });
14793
+ if (node.kind === "folder") folderCount += 1;
14794
+ else if (node.kind !== "file") otherNodeCount += 1;
14795
+ }
14796
+ return {
14797
+ name: typeof global?.name === "string" ? global.name : doc.header.name,
14798
+ icon: typeof global?.icon === "string" ? global.icon : void 0,
14799
+ nodes,
14800
+ totalNodeCount: rawNodes.length,
14801
+ folderCount,
14802
+ otherNodeCount,
14803
+ unreadableNodeCount
14804
+ };
14805
+ }
14806
+ function fileNodesOf(nodes) {
14807
+ return nodes.filter((node) => node.kind === "file");
14808
+ }
14809
+ function toDriveInfo(doc) {
14810
+ const driveId = doc.header.id;
14811
+ const branch = doc.header.branch;
14812
+ const state = readDriveState(doc);
14813
+ return {
14814
+ driveId,
14815
+ name: state.name,
14816
+ branch,
14817
+ collectionId: DriveCollectionId.forDrive(driveId, branch).key,
14818
+ documentType: doc.header.documentType,
14819
+ nodeCount: state.totalNodeCount,
14820
+ fileCount: fileNodesOf(state.nodes).length,
14821
+ folderCount: state.folderCount,
14822
+ otherNodeCount: state.otherNodeCount,
14823
+ unreadableNodeCount: state.unreadableNodeCount,
14824
+ icon: state.icon
14825
+ };
14826
+ }
14827
+ /** Drive listing and integrity, every read made through one reader and view. */
14828
+ var DriveInspection = class {
14829
+ reader;
14830
+ registry;
14831
+ view;
14832
+ constructor(reader, registry, view = {}) {
14833
+ this.reader = reader;
14834
+ this.registry = registry;
14835
+ this.view = view;
14836
+ }
14837
+ async listDrives(cursor, limit) {
14838
+ const page = await this.reader.find({ type: DRIVE_DOCUMENT_TYPE }, this.view, {
14839
+ cursor: cursor ?? "",
14840
+ limit: limit ?? DEFAULT_DRIVE_PAGE_SIZE
14841
+ });
14842
+ return {
14843
+ results: page.results.map(toDriveInfo),
14844
+ nextCursor: page.nextCursor
14845
+ };
14846
+ }
14847
+ async checkDriveIntegrity(driveId, branch) {
14848
+ const fileNodes = fileNodesOf(readDriveState(await this.reader.get(driveId, {
14849
+ ...this.view,
14850
+ branch
14851
+ })).nodes);
14852
+ const present = await this.presentDocumentIds(fileNodes.map((node) => node.id), branch);
14853
+ const supported = this.supportedDocumentTypes();
14854
+ const missingDocuments = [];
14855
+ const unsupportedTypes = [];
14856
+ for (const node of fileNodes) {
14857
+ if (!present.has(node.id)) missingDocuments.push({
14858
+ id: node.id,
14859
+ documentType: node.documentType ?? ""
14860
+ });
14861
+ if (supported !== void 0 && node.documentType !== void 0 && !supported.has(node.documentType)) unsupportedTypes.push({
14862
+ id: node.id,
14863
+ documentType: node.documentType
14864
+ });
14865
+ }
14866
+ return {
14867
+ driveId,
14868
+ checkedNodeCount: fileNodes.length,
14869
+ totalFileNodeCount: fileNodes.length,
14870
+ missingDocuments,
14871
+ unsupportedTypes
14872
+ };
14873
+ }
14874
+ async presentDocumentIds(ids, branch) {
14875
+ const present = /* @__PURE__ */ new Set();
14876
+ for (let start = 0; start < ids.length; start += INTEGRITY_FIND_BATCH_SIZE) {
14877
+ const batch = ids.slice(start, start + INTEGRITY_FIND_BATCH_SIZE);
14878
+ let page = await this.reader.find({ ids: batch }, {
14879
+ ...this.view,
14880
+ branch
14881
+ }, {
14882
+ cursor: "",
14883
+ limit: batch.length
14884
+ });
14885
+ for (const doc of page.results) present.add(doc.header.id);
14886
+ while (page.nextCursor !== void 0 && page.next) {
14887
+ page = await page.next();
14888
+ for (const doc of page.results) present.add(doc.header.id);
14889
+ }
14890
+ }
14891
+ return present;
14892
+ }
14893
+ supportedDocumentTypes() {
14894
+ if (!this.registry) return;
14895
+ return new Set(this.registry.getAllModules().map((module) => module.documentModel.global.id));
14896
+ }
14897
+ };
14898
+ //#endregion
14899
+ //#region src/inspector/ops.ts
14900
+ /** The `inspector-op` channel, one row per member. */
14901
+ const INSPECTOR_OPS = {
14902
+ info: {
14903
+ rpc: "reactor.info",
14904
+ tier: "read",
14905
+ gql: "info"
14906
+ },
14907
+ listDocumentModels: {
14908
+ rpc: "registry.listDocumentModels",
14909
+ tier: "read",
14910
+ gql: "documentModels"
14911
+ },
14912
+ listDrives: {
14913
+ rpc: "drives.list",
14914
+ tier: "read",
14915
+ gql: "drives"
14916
+ },
14917
+ checkDriveIntegrity: {
14918
+ rpc: "drives.checkIntegrity",
14919
+ tier: "read",
14920
+ gql: "driveIntegrity"
14921
+ },
14922
+ getAttachmentInfo: {
14923
+ rpc: "attachments.info",
14924
+ tier: "read",
14925
+ gql: "attachmentInfo"
14926
+ },
14927
+ getQueueState: {
14928
+ rpc: "queue.getState",
14929
+ tier: "read",
14930
+ gql: "queueState"
14931
+ },
14932
+ getProcessors: {
14933
+ rpc: "processors.getAll",
14934
+ tier: "read",
14935
+ gql: "processors"
14936
+ },
14937
+ getCatchUpStatus: {
14938
+ rpc: "catchUp.status",
14939
+ tier: "read",
14940
+ gql: "catchUpStatus"
14941
+ },
14942
+ getStorageHealth: {
14943
+ rpc: "storage.health",
14944
+ tier: "read",
14945
+ gql: "storageHealth"
14946
+ },
14947
+ validateDocument: {
14948
+ rpc: "integrity.validate",
14949
+ tier: "read",
14950
+ gql: "validateDocument"
14951
+ },
14952
+ pauseQueue: {
14953
+ rpc: "queue.pause",
14954
+ tier: "admin"
14955
+ },
14956
+ resumeQueue: {
14957
+ rpc: "queue.resume",
14958
+ tier: "admin"
14959
+ },
14960
+ retryProcessor: {
14961
+ rpc: "processors.retry",
14962
+ tier: "admin"
14963
+ },
14964
+ sweepCatchUp: {
14965
+ rpc: "catchUp.sweepNow",
14966
+ tier: "admin"
14967
+ },
14968
+ rebuildKeyframes: {
14969
+ rpc: "integrity.rebuildKeyframes",
14970
+ tier: "admin"
14971
+ },
14972
+ rebuildSnapshots: {
14973
+ rpc: "integrity.rebuildSnapshots",
14974
+ tier: "admin"
14975
+ },
14976
+ queryDb: {
14977
+ rpc: "db.query",
14978
+ tier: "sql"
14979
+ }
14980
+ };
14981
+ /** The inspection and repair rows of the `sync-op` channel. */
14982
+ const SYNC_INSPECTION_OPS = {
14983
+ inspectRemote: {
14984
+ rpc: "inspect.remote",
14985
+ tier: "read",
14986
+ gql: "remote"
14987
+ },
14988
+ inspectRemotes: {
14989
+ rpc: "inspect.remotes",
14990
+ tier: "read",
14991
+ gql: "remotes"
14992
+ },
14993
+ listDeadLetters: {
14994
+ rpc: "deadLetters.list",
14995
+ tier: "read",
14996
+ gql: "deadLetters"
14997
+ },
14998
+ resetChannel: {
14999
+ rpc: "repair.resetChannel",
15000
+ tier: "admin"
15001
+ },
15002
+ requeueDeadLetter: {
15003
+ rpc: "deadLetters.requeue",
15004
+ tier: "admin"
15005
+ },
15006
+ clearDeadLetter: {
15007
+ rpc: "deadLetters.clear",
15008
+ tier: "admin"
15009
+ }
15010
+ };
15011
+ /** Reads only, unless a host grants more. */
15012
+ const READ_ONLY_ACCESS = Object.freeze({
15013
+ admin: false,
15014
+ sql: false
15015
+ });
15016
+ function tierAllowed(tier, access) {
15017
+ switch (tier) {
15018
+ case "read": return true;
15019
+ case "admin": return access.admin;
15020
+ case "sql": return access.sql;
15021
+ }
15022
+ }
15023
+ /** The table key whose rpc name is `method`, or undefined. */
15024
+ function opKeyOf(table, method) {
15025
+ for (const key of Object.keys(table)) if (table[key].rpc === method) return key;
15026
+ }
15027
+ /** The read rows' GraphQL root field names, keyed by member. */
15028
+ function readOpFields(table) {
15029
+ const fields = {};
15030
+ for (const key of Object.keys(table)) {
15031
+ const spec = table[key];
15032
+ if (spec.tier === "read") fields[key] = spec.gql;
15033
+ }
15034
+ return fields;
15035
+ }
15036
+ var UnknownInspectorOpError = class extends Error {
15037
+ method;
15038
+ constructor(method) {
15039
+ super(`Unknown inspector op: ${method}`);
15040
+ this.method = method;
15041
+ this.name = "UnknownInspectorOpError";
15042
+ }
15043
+ };
15044
+ var InspectorOpRefusedError = class extends Error {
15045
+ method;
15046
+ tier;
15047
+ constructor(method, tier) {
15048
+ super(`This host does not serve ${tier}-tier inspection ops (${method})`);
15049
+ this.method = method;
15050
+ this.tier = tier;
15051
+ this.name = "InspectorOpRefusedError";
15052
+ }
15053
+ };
15054
+ var InspectorOpUnavailableError = class extends Error {
15055
+ method;
15056
+ constructor(method) {
15057
+ super(`This host has nothing to serve ${method} with`);
15058
+ this.method = method;
15059
+ this.name = "InspectorOpUnavailableError";
15060
+ }
15061
+ };
15062
+ /** Refuses unknown methods and ungranted tiers before resolving a handler. */
15063
+ async function dispatchTableOp(table, resolve, access, method, args) {
15064
+ const key = opKeyOf(table, method);
15065
+ if (key === void 0) throw new UnknownInspectorOpError(method);
15066
+ const tier = table[key].tier;
15067
+ if (!tierAllowed(tier, access)) throw new InspectorOpRefusedError(method, tier);
15068
+ const handler = resolve(key);
15069
+ if (!handler) throw new InspectorOpUnavailableError(method);
15070
+ return await handler(...args);
15071
+ }
15072
+ /** Binds `key` on `target` when it is a method there. */
15073
+ function methodOf(target, key) {
15074
+ const member = target?.[key];
15075
+ return typeof member === "function" ? member.bind(target) : void 0;
15076
+ }
15077
+ //#endregion
15078
+ //#region src/inspector/reactor-inspector.ts
15079
+ const untrackedStorageHealth = {
15080
+ tracked: false,
15081
+ healthy: false,
15082
+ everRecreated: false,
15083
+ recreateCount: 0
15084
+ };
15085
+ const emptyQueueState = {
15086
+ isPaused: false,
15087
+ pendingJobs: [],
15088
+ executingJobs: [],
15089
+ totalPending: 0,
15090
+ totalExecuting: 0
15091
+ };
15092
+ const noAttachmentStore = {
15093
+ present: false,
15094
+ storeKind: "none",
15095
+ hasReplicator: false,
15096
+ replicatorRunning: false,
15097
+ backlogScanned: false,
15098
+ refsSeen: 0,
15099
+ held: 0,
15100
+ bytesHeld: 0,
15101
+ queued: 0,
15102
+ fetching: 0,
15103
+ pendingFetches: 0,
15104
+ waiting: 0,
15105
+ notFound: 0,
15106
+ failed: 0,
15107
+ lastError: void 0
15108
+ };
15109
+ function unsupported(what, why) {
15110
+ return /* @__PURE__ */ new Error(`${what} is unsupported on this host: ${why}`);
15111
+ }
15112
+ const NO_INSPECTABLE_QUEUE = "its queue is not the in-memory one, so it cannot be paused or resumed";
15113
+ const NO_CATCH_UP = "it was built without catch-up";
15114
+ const NO_INTEGRITY = "it has no document integrity service";
15115
+ const NO_PROCESSORS = "it was built without a processor manager";
15116
+ /** In-process inspector over a reactor module's live components. */
15117
+ var ReactorInspector = class {
15118
+ components;
15119
+ storageFacts;
15120
+ syncChannels;
15121
+ access;
15122
+ workflows;
15123
+ constructor(components) {
15124
+ this.components = components;
15125
+ const facts = components.facts ?? {};
15126
+ this.storageFacts = facts.storage ?? UNKNOWN_STORAGE_FACTS;
15127
+ this.syncChannels = Object.freeze([...facts.syncChannels ?? []]);
15128
+ this.access = facts.access ?? READ_ONLY_ACCESS;
15129
+ this.workflows = facts.workflows ?? false;
15130
+ }
15131
+ setWorkflows(composed) {
15132
+ this.workflows = composed;
15133
+ }
15134
+ info() {
15135
+ return Promise.resolve({
15136
+ storage: { ...this.storageFacts },
15137
+ workflows: this.workflows,
15138
+ syncChannels: [...this.syncChannels],
15139
+ access: { ...this.access }
15140
+ });
15141
+ }
15142
+ listDocumentModels() {
15143
+ const registry = this.components.documentModelRegistry;
15144
+ if (!registry) return Promise.resolve([]);
15145
+ const byType = /* @__PURE__ */ new Map();
15146
+ for (const module of registry.getAllModules()) {
15147
+ const documentType = module.documentModel.global.id;
15148
+ if (byType.has(documentType)) continue;
15149
+ const supportedVersions = registry.getSupportedVersions(documentType);
15150
+ byType.set(documentType, {
15151
+ documentType,
15152
+ name: module.documentModel.global.name,
15153
+ version: supportedVersions.at(-1) ?? module.version ?? 1,
15154
+ supportedVersions
15155
+ });
15156
+ }
15157
+ return Promise.resolve([...byType.values()]);
15158
+ }
15159
+ listDrives(cursor, limit) {
15160
+ const drives = this.components.drives;
15161
+ if (!drives) return Promise.resolve({
15162
+ results: [],
15163
+ nextCursor: void 0
15164
+ });
15165
+ return drives.listDrives(cursor, limit);
15166
+ }
15167
+ checkDriveIntegrity(driveId, branch) {
15168
+ const drives = this.components.drives;
15169
+ if (!drives) return Promise.resolve({
15170
+ driveId,
15171
+ checkedNodeCount: 0,
15172
+ totalFileNodeCount: 0,
15173
+ missingDocuments: [],
15174
+ unsupportedTypes: []
15175
+ });
15176
+ return drives.checkDriveIntegrity(driveId, branch);
15177
+ }
15178
+ getAttachmentInfo() {
15179
+ const store = this.components.attachmentStore;
15180
+ if (!store) return Promise.resolve({ ...noAttachmentStore });
15181
+ return store.getAttachmentInfo();
15182
+ }
15183
+ getQueueState() {
15184
+ const queue = this.components.queue;
15185
+ if (!queue) return Promise.resolve({ ...emptyQueueState });
15186
+ const pendingJobs = queue.getPendingJobs();
15187
+ const executingJobs = [];
15188
+ for (const jobIds of queue.getExecutingJobIds().values()) for (const jobId of jobIds) {
15189
+ const job = queue.getJob(jobId);
15190
+ if (job) executingJobs.push(job);
15191
+ }
15192
+ return Promise.resolve({
15193
+ isPaused: queue.paused,
15194
+ pendingJobs,
15195
+ executingJobs,
15196
+ totalPending: pendingJobs.length,
15197
+ totalExecuting: executingJobs.length
15198
+ });
15199
+ }
15200
+ getProcessors() {
15201
+ const tracked = this.components.processorManager?.getAll() ?? [];
15202
+ return Promise.resolve(tracked.map((processor) => ({
15203
+ processorId: processor.processorId,
15204
+ factoryId: processor.factoryId,
15205
+ driveId: processor.driveId,
15206
+ processorIndex: processor.processorIndex,
15207
+ lastOrdinal: processor.lastOrdinal,
15208
+ status: processor.status,
15209
+ lastError: processor.lastError,
15210
+ lastErrorTimestamp: processor.lastErrorTimestamp
15211
+ })));
15212
+ }
15213
+ getCatchUpStatus() {
15214
+ const catchUp = this.components.catchUp;
15215
+ if (!catchUp) return Promise.reject(unsupported("Reading catch-up", NO_CATCH_UP));
15216
+ return Promise.resolve(catchUp.status());
15217
+ }
15218
+ getStorageHealth() {
15219
+ const provider = this.components.storageHealth;
15220
+ if (!provider) return Promise.resolve({ ...untrackedStorageHealth });
15221
+ return Promise.resolve(provider.getStorageHealth());
15222
+ }
15223
+ validateDocument(documentId, branch) {
15224
+ const integrity = this.components.integrity;
15225
+ if (!integrity) return Promise.reject(unsupported("Validating a document", NO_INTEGRITY));
15226
+ return integrity.validateDocument(documentId, branch);
15227
+ }
15228
+ pauseQueue() {
15229
+ const queue = this.components.queue;
15230
+ if (!queue) return Promise.reject(unsupported("Pausing the queue", NO_INSPECTABLE_QUEUE));
15231
+ queue.pause();
15232
+ return Promise.resolve();
15233
+ }
15234
+ resumeQueue() {
15235
+ const queue = this.components.queue;
15236
+ if (!queue) return Promise.reject(unsupported("Resuming the queue", NO_INSPECTABLE_QUEUE));
15237
+ return queue.resume();
15238
+ }
15239
+ retryProcessor(processorId) {
15240
+ const manager = this.components.processorManager;
15241
+ if (!manager) return Promise.reject(unsupported("Retrying a processor", NO_PROCESSORS));
15242
+ const processor = manager.get(processorId);
15243
+ if (!processor) return Promise.reject(/* @__PURE__ */ new Error(`Cannot retry processor ${JSON.stringify(processorId)}: this reactor is not tracking it`));
15244
+ return processor.retry();
15245
+ }
15246
+ sweepCatchUp() {
15247
+ const catchUp = this.components.catchUp;
15248
+ if (!catchUp) return Promise.reject(unsupported("Sweeping catch-up", NO_CATCH_UP));
15249
+ return catchUp.sweepNow();
15250
+ }
15251
+ rebuildKeyframes(documentId, branch) {
15252
+ const integrity = this.components.integrity;
15253
+ if (!integrity) return Promise.reject(unsupported("Rebuilding keyframes", NO_INTEGRITY));
15254
+ return integrity.rebuildKeyframes(documentId, branch);
15255
+ }
15256
+ rebuildSnapshots(documentId, branch) {
15257
+ const integrity = this.components.integrity;
15258
+ if (!integrity) return Promise.reject(unsupported("Rebuilding snapshots", NO_INTEGRITY));
15259
+ return integrity.rebuildSnapshots(documentId, branch);
15260
+ }
15261
+ };
15262
+ //#endregion
15263
+ //#region src/inspector/from-module.ts
15264
+ /** A group-commit store is never recreated in place; a poison needs a restart. */
15265
+ function groupCommitHealth(storage) {
15266
+ if (!storage) return;
15267
+ return { getStorageHealth: () => ({
15268
+ tracked: true,
15269
+ healthy: storage.health.getStorageHealth().healthy,
15270
+ everRecreated: false,
15271
+ recreateCount: 0
15272
+ }) };
15273
+ }
15274
+ /** The in-process inspector over a built module; drive reads run as the host. */
15275
+ function createReactorInspector(module, options = {}) {
15276
+ return new ReactorInspector({
15277
+ queue: module.queue instanceof InMemoryQueue ? module.queue : void 0,
15278
+ processorManager: module.processorManager,
15279
+ catchUp: module.catchUp,
15280
+ integrity: new DocumentIntegrityService(module.keyframeStore, module.operationStore, module.writeCache, module.documentView, module.documentModelRegistry),
15281
+ storageHealth: options.storageHealth ?? groupCommitHealth(module.groupCommitStorage),
15282
+ documentModelRegistry: module.documentModelRegistry,
15283
+ drives: new DriveInspection(module.reactor, module.documentModelRegistry),
15284
+ attachmentStore: options.attachmentStore,
15285
+ facts: {
15286
+ storage: module.storageFacts,
15287
+ syncChannels: module.syncModule ? channelFactoryTypes(module.syncModule.channelFactory) : [],
15288
+ access: options.access,
15289
+ workflows: options.workflows
15290
+ }
15291
+ });
15292
+ }
15293
+ //#endregion
15294
+ //#region src/inspector/wire.ts
15295
+ function toWireQueueJob(job, status) {
15296
+ return {
15297
+ id: job.id,
15298
+ kind: job.kind,
15299
+ documentId: job.documentId,
15300
+ scope: job.scope,
15301
+ branch: job.branch,
15302
+ status,
15303
+ actionCount: job.actions.length,
15304
+ operationCount: job.operations.length,
15305
+ retryCount: job.retryCount ?? 0
15306
+ };
15307
+ }
15308
+ /** The queue as the inspection surface serves it: no document content. */
15309
+ function toWireQueueState(snapshot) {
15310
+ return {
15311
+ isPaused: snapshot.isPaused,
15312
+ totalPending: snapshot.totalPending,
15313
+ totalExecuting: snapshot.totalExecuting,
15314
+ pendingJobs: snapshot.pendingJobs.map((job) => toWireQueueJob(job, "pending")),
15315
+ executingJobs: snapshot.executingJobs.map((job) => toWireQueueJob(job, "executing"))
15316
+ };
15317
+ }
15318
+ /** A dead letter as the inspection surface serves it: no document content. */
15319
+ function toWireDeadLetter(record) {
15320
+ return {
15321
+ id: record.id,
15322
+ jobId: record.jobId,
15323
+ documentId: record.documentId,
15324
+ branch: record.branch,
15325
+ scopes: [...record.scopes],
15326
+ errorType: record.errorType,
15327
+ errorMessage: record.errorMessage,
15328
+ operationCount: record.operations.length
15329
+ };
15330
+ }
15331
+ /** Compile error unless `names` lists every key of `T` and nothing else. */
15332
+ function fieldsOf() {
15333
+ return (names) => names;
15334
+ }
15335
+ /** Every wire record's fields, by the GraphQL type that serves it. */
15336
+ const INSPECTION_WIRE_FIELDS = {
15337
+ ReactorInfo: fieldsOf()([
15338
+ "storage",
15339
+ "workflows",
15340
+ "syncChannels",
15341
+ "access"
15342
+ ]),
15343
+ ReactorStorageFacts: fieldsOf()([
15344
+ "engine",
15345
+ "persistence",
15346
+ "durable",
15347
+ "selfHeal"
15348
+ ]),
15349
+ InspectorAccess: fieldsOf()(["admin", "sql"]),
15350
+ InspectionDocumentModel: fieldsOf()([
15351
+ "documentType",
15352
+ "name",
15353
+ "version",
15354
+ "supportedVersions"
15355
+ ]),
15356
+ InspectionDrive: fieldsOf()([
15357
+ "driveId",
15358
+ "name",
15359
+ "branch",
15360
+ "collectionId",
15361
+ "documentType",
15362
+ "nodeCount",
15363
+ "fileCount",
15364
+ "folderCount",
15365
+ "otherNodeCount",
15366
+ "unreadableNodeCount",
15367
+ "icon"
15368
+ ]),
15369
+ InspectionDrivePage: fieldsOf()(["results", "nextCursor"]),
15370
+ InspectionDriveIntegrityRef: fieldsOf()(["id", "documentType"]),
15371
+ InspectionDriveIntegrity: fieldsOf()([
15372
+ "driveId",
15373
+ "checkedNodeCount",
15374
+ "totalFileNodeCount",
15375
+ "missingDocuments",
15376
+ "unsupportedTypes"
15377
+ ]),
15378
+ InspectionAttachmentInfo: fieldsOf()([
15379
+ "present",
15380
+ "storeKind",
15381
+ "hasReplicator",
15382
+ "replicatorRunning",
15383
+ "backlogScanned",
15384
+ "refsSeen",
15385
+ "held",
15386
+ "bytesHeld",
15387
+ "queued",
15388
+ "fetching",
15389
+ "pendingFetches",
15390
+ "waiting",
15391
+ "notFound",
15392
+ "failed",
15393
+ "lastError"
15394
+ ]),
15395
+ InspectionQueueJob: fieldsOf()([
15396
+ "id",
15397
+ "kind",
15398
+ "documentId",
15399
+ "scope",
15400
+ "branch",
15401
+ "status",
15402
+ "actionCount",
15403
+ "operationCount",
15404
+ "retryCount"
15405
+ ]),
15406
+ InspectionQueueState: fieldsOf()([
15407
+ "isPaused",
15408
+ "totalPending",
15409
+ "totalExecuting",
15410
+ "pendingJobs",
15411
+ "executingJobs"
15412
+ ]),
15413
+ InspectionProcessor: fieldsOf()([
15414
+ "processorId",
15415
+ "factoryId",
15416
+ "driveId",
15417
+ "processorIndex",
15418
+ "lastOrdinal",
15419
+ "status",
15420
+ "lastError",
15421
+ "lastErrorTimestampUtcMs"
15422
+ ]),
15423
+ InspectionStorageHealth: fieldsOf()([
15424
+ "tracked",
15425
+ "healthy",
15426
+ "everRecreated",
15427
+ "recreateCount"
15428
+ ]),
15429
+ InspectionValidation: fieldsOf()([
15430
+ "documentId",
15431
+ "isConsistent",
15432
+ "keyframeIssues",
15433
+ "snapshotIssues",
15434
+ "streamOrderIssues"
15435
+ ]),
15436
+ InspectionCursor: fieldsOf()([
15437
+ "cursorType",
15438
+ "cursorOrdinal",
15439
+ "lastSyncedAtUtcMs",
15440
+ "liveAckOrdinal",
15441
+ "liveLatestOrdinal"
15442
+ ]),
15443
+ InspectionMailboxDepths: fieldsOf()([
15444
+ "inbox",
15445
+ "outbox",
15446
+ "deadLetter"
15447
+ ]),
15448
+ InspectionConnectionHealth: fieldsOf()([
15449
+ "snapshot",
15450
+ "neverSucceeded",
15451
+ "stalenessMs"
15452
+ ]),
15453
+ InspectionRemote: fieldsOf()([
15454
+ "remoteName",
15455
+ "remoteId",
15456
+ "inboxCursor",
15457
+ "outboxCursor",
15458
+ "mailboxDepths",
15459
+ "connection",
15460
+ "meta"
15461
+ ]),
15462
+ InspectionDeadLetter: fieldsOf()([
15463
+ "id",
15464
+ "jobId",
15465
+ "documentId",
15466
+ "branch",
15467
+ "scopes",
15468
+ "errorType",
15469
+ "errorMessage",
15470
+ "operationCount"
15471
+ ]),
15472
+ InspectionDeadLetterPage: fieldsOf()([
15473
+ "remoteName",
15474
+ "results",
15475
+ "nextCursor"
15476
+ ])
15477
+ };
15478
+ function rootFieldsOf(...tables) {
15479
+ return tables.flatMap((table) => Object.values(readOpFields(table)).filter((field) => field !== void 0));
15480
+ }
15481
+ /** The root fields the inspection subgraph serves: the read rows only. */
15482
+ const INSPECTION_ROOT_FIELDS = Object.freeze({ ReactorInspection: Object.freeze(rootFieldsOf(INSPECTOR_OPS, SYNC_INSPECTION_OPS)) });
15483
+ /** Ordinals pass 2^31, so these are `Float` rather than `Int`. */
15484
+ const INSPECTION_ORDINAL_FIELDS = {
15485
+ InspectionProcessor: ["lastOrdinal"],
15486
+ InspectionCursor: [
15487
+ "cursorOrdinal",
15488
+ "liveAckOrdinal",
15489
+ "liveLatestOrdinal"
15490
+ ]
15491
+ };
15492
+ //#endregion
15493
+ export { ALWAYS_READABLE_SCOPES, APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, AuthEnforcementDisabledError, BareReadGate, BaseReadModel, BatchJobFailedError, CatchUpScheduler, ChannelError, ChannelErrorSource, ChannelScheme, CompositeChannelFactory, ConsistencyTracker, ContiguousCursor, DEFAULT_COMMIT_CHUNK_SIZE, DEFAULT_DRIVE_CONTAINER_TYPES, DEFAULT_LOCAL_FILTER, DEFAULT_LONG_STATEMENT_TIMEOUT_MS, DEFAULT_MAX_PURGE_OPERATIONS, DEFAULT_PURGE_DELETE_BATCH, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_STATEMENT_TIMEOUT_MS, DOCUMENT_INDEXER_READ_MODEL, DOCUMENT_VIEW_READ_MODEL, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DeletedDocumentRead, DocumentAlreadyExistsError, DocumentChangeType, DocumentExistence, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DocumentNotDeletedError, DocumentPurgeService, DocumentPurgedError, DriveClient, DriveCollectionId, DriveInspection, DriveRequestError, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GQL_CHANNEL_TYPE, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, GroupInUseError, HardenedPGliteDialect, HybridProjectionCoordinator, INSPECTION_ORDINAL_FIELDS, INSPECTION_ROOT_FIELDS, INSPECTION_WIRE_FIELDS, INSPECTOR_OPS, IN_MEMORY_PGLITE_STORAGE_FACTS, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, InMemorySyncHoldStorage, InMemorySyncPurgeRefusalStorage, InMemorySyncReceivedMarkerStorage, InspectorOpRefusedError, InspectorOpUnavailableError, IntervalPollTimer, InvalidDeadLetterCursorError, InvalidModuleError, InvalidSignatureError, JOB_NOT_FOUND_ERROR_NAME, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentPurger, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncHoldStorage, KyselySyncPurgeRefusalStorage, KyselySyncReceivedMarkerStorage, KyselySyncRemoteStorage, KyselyWriteCache, LOCAL_CHANNEL_TYPE, LOCAL_REMOTE_OPTIONS, LocalChannel, LocalChannelFactory, LocalChannelPortRegistry, MAX_POLLED_REFUSALS, Mailbox, ModelNotWorkerImportableError, ModelReadGate, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PGLITE_IDB_STORAGE_FACTS, PGLITE_PATH_STORAGE_FACTS, PGliteAbortedTransactionError, PGliteAcquireTimeoutError, PGliteFlushSyncTimeoutError, PGliteSessionError, PGliteSessionPoisonedError, PGliteStatementTimeoutError, POLLING_CHANNEL_TYPE, POSTGRES_STORAGE_FACTS, PURGE_DOCUMENT, PURGE_LOCK_BUCKETS, PURGE_NS, PollBehavior, PollingChannelError, ProbeSettler, ProcessorManager, PropagationMode, PurgeTooLargeError, QueueEventTypes, REACTOR_SCHEMA, READ_ONLY_ACCESS, RECOVERABLE_GRAPHQL_ERROR_CODES, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorInspector, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RelationshipNotFoundError, RemotePersistence, ReservedActionError, RetryAccounting, RevisionMismatchError, SIGNATURE_REFUSAL_CODES, SYNC_INSPECTION_OPS, SeededStateReader, SettledWatermark, SimpleJobExecutorManager, StoragePoisonedError, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncRepairRefusedError, SyncScopeGate, SyncStatus, SyncStatusTracker, UNKNOWN_STORAGE_FACTS, UnknownInspectorOpError, UnsupportedStoredProtocolError, acquirePurgeLocks, addRelationshipAction, appliedDelete, assertCollectionIdParts, authDecisionModel, batchOperationsByDocument, bucketFor, buildCreateJobs, buildDecisionModel, channelFactoryTypes, classifyJobFailure, collectionIdFromKey, consolidateSyncOperations, createDocumentAction, createEmptyDocument, createForwardingLogger, createHybridProjectionCoordinatorFactory, createKyselyWatermarkProbe, createMutableShutdownStatus, createPeerAgreement, createReactorHostModuleBase, createReactorInspector, createRelationalDb, decideAtHead, defaultCatchUpConfig, defaultReadModelIndexingConfig, deleteDocumentAction, deriveConnectionHealth, dispatchTableOp, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, findPurged, firstOutOfOrderPair, getMigrationStatus, hashDocumentId, instrumentPgPool, isCatchUpConsumer, isDriveAuthError, isLocalWireMessage, isLongRunningStatement, isPurgeMarker, isRecoverableGraphQLError, listPurged, loadDocumentModelSpec, localChannelConfig, makeConsistencyKey, messagePortTransport, methodOf, opKeyOf, parseDriveUrl, parsePagingOptions, parseSnapshot, quarantinesDocument, queryThroughDialect, readDecisionModel, readOpFields, registerLocalPeer, removeLocalPeer, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, selectDocumentModelModule, snapshotFunctionsFor, supportsDeliveryTracking, supportsLiveReadModelRegistration, supportsPurgeRefusals, syncOperationErrorType, tierAllowed, toWireDeadLetter, toWireQueueState, trimMailboxFromAckOrdinal, unchunkedReadModelIndexingConfig, updateRelationshipAction, upgradeDocumentAction, upgradeDocumentWith, verifyActionSignature, workerEntryPath };
13160
15494
 
13161
15495
  //# sourceMappingURL=index.js.map