@powerhousedao/reactor 6.2.3-dev.24 → 6.2.3-dev.26

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 (39) hide show
  1. package/dist/{build-worker-executor-DxmDJ4QQ.js → build-worker-executor-Bvoba-AV.js} +4 -4
  2. package/dist/{build-worker-executor-DxmDJ4QQ.js.map → build-worker-executor-Bvoba-AV.js.map} +1 -1
  3. package/dist/{document-indexer-Dn9RB0sg.js → document-indexer-BVTGbEB0.js} +22 -21
  4. package/dist/document-indexer-BVTGbEB0.js.map +1 -0
  5. package/dist/entry.d.ts +2 -1
  6. package/dist/entry.js +2 -3
  7. package/dist/entry.js.map +1 -1
  8. package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
  9. package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
  10. package/dist/{drive-container-types-Be5-EY9W.js → forwarding-logger-CnuVDVze.js} +303 -53
  11. package/dist/forwarding-logger-CnuVDVze.js.map +1 -0
  12. package/dist/index.d.ts +474 -307
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +700 -366
  15. package/dist/index.js.map +1 -1
  16. package/dist/projection-entry.d.ts +2 -1
  17. package/dist/projection-entry.js +7 -13
  18. package/dist/projection-entry.js.map +1 -1
  19. package/dist/{projection-shard-manager-mQhfVipO.js → projection-shard-manager-BpcqDtnQ.js} +2 -3
  20. package/dist/{projection-shard-manager-mQhfVipO.js.map → projection-shard-manager-BpcqDtnQ.js.map} +1 -1
  21. package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
  22. package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
  23. package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
  24. package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
  25. package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
  26. package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
  27. package/dist/{worker-handle-B3p-nuqG.js → worker-handle-C2rLUHwv.js} +9 -8
  28. package/dist/{worker-handle-B3p-nuqG.js.map → worker-handle-C2rLUHwv.js.map} +1 -1
  29. package/package.json +14 -7
  30. package/dist/document-indexer-Dn9RB0sg.js.map +0 -1
  31. package/dist/drive-container-types-Be5-EY9W.js.map +0 -1
  32. package/dist/error-info-Cpu4OY3o.js +0 -62
  33. package/dist/error-info-Cpu4OY3o.js.map +0 -1
  34. package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
  35. package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
  36. package/dist/types-DxDCmtId.js +0 -34
  37. package/dist/types-DxDCmtId.js.map +0 -1
  38. package/dist/worker-BDVzCtuf.js +0 -22
  39. package/dist/worker-BDVzCtuf.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,6 @@
1
- import { $ as RelationshipNotFoundError, A as submittedActionIds, B as DocumentAlreadyExistsError, C as DuplicateManifestError, D as GATED_DOCUMENT_ACTIONS, E as ModuleNotFoundError, F as documentDecisionModel, G as AuthEnforcementDisabledError, H as DuplicateOperationError, I as authDecisionModel, J as DocumentDeletedError, K as AuthTimestampNotMonotonicError, L as buildDecisionModel, M as targetDocumentId, N as decideAtHead, O as createConsistencyToken, P as selectDecisionModel, Q as InvalidSignatureError, R as APPEND_CONDITION_FAILED_PREFIX, S as JobExecutorEventTypes, T as InvalidModuleError, U as OptimisticLockError, V as DocumentExistence, W as RevisionMismatchError, X as ExcessiveReshuffleError, Y as DocumentNotFoundError, Z as InvalidOperationTimestampError, _ as KyselyWriteCache, a as createForwardingPoolInstrumentation, b as CollectionMembershipCache, c as KyselyKeyframeStore, d as verifyActionSignature, et as UpgradePreconditionFailedError, f as PassthroughSigner, g as EventBus, h as resolveFeatureFlags, i as runMigrations, j as summarizeSubmittedActions, k as createEmptyConsistencyToken, l as DocumentModelRegistry, m as KyselyExecutionScope, n as REACTOR_SCHEMA, nt as parsePagingOptions, o as instrumentPgPool, p as DriveCollectionId, q as AuthorizationDeniedError, r as getMigrationStatus, rt as throwIfAborted, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, tt as matchesScope, u as SimpleJobExecutor, v as KyselyOperationIndex, w as DuplicateModuleError, x as DEFAULT_DEFERRED_JOB_TTL_MS, y as DocumentMetaCache, z as AppendConditionFailedError } from "./drive-container-types-Be5-EY9W.js";
2
- import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DxDCmtId.js";
3
- import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-D3S6Eysd.js";
4
- import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-Dn9RB0sg.js";
5
- import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
6
- import { t as workerEntryPath } from "./worker-BDVzCtuf.js";
1
+ import { $ as DocumentNotFoundError, A as ModuleNotFoundError, B as authDecisionModel, C as DocumentMetaCache, D as DuplicateManifestError, E as JobExecutorEventTypes, F as summarizeSubmittedActions, G as DocumentExistence, H as APPEND_CONDITION_FAILED_PREFIX, I as targetDocumentId, J as RevisionMismatchError, K as DuplicateOperationError, L as decideAtHead, M as createConsistencyToken, N as createEmptyConsistencyToken, O as DuplicateModuleError, P as submittedActionIds, Q as DocumentDeletedError, R as selectDecisionModel, S as KyselyOperationIndex, T as DEFAULT_DEFERRED_JOB_TTL_MS, U as AppendConditionFailedError, V as buildDecisionModel, W as DocumentAlreadyExistsError, X as AuthTimestampNotMonotonicError, Y as AuthEnforcementDisabledError, Z as AuthorizationDeniedError, _ as DriveCollectionId, a as DEFAULT_DRIVE_CONTAINER_TYPES, at as EventBusAggregateError, b as EventBus, c as runMigrations, ct as parsePagingOptions, d as KyselyOperationStore, dt as __exportAll, et as ExcessiveReshuffleError, f as KyselyKeyframeStore, g as PassthroughSigner, h as verifyActionSignature, it as UpgradePreconditionFailedError, j as GATED_DOCUMENT_ACTIONS, k as InvalidModuleError, l as createForwardingPoolInstrumentation, lt as throwIfAborted, m as SimpleJobExecutor, n as errorToInfo, nt as InvalidSignatureError, o as REACTOR_SCHEMA, ot as ReactorEventTypes, p as DocumentModelRegistry, q as OptimisticLockError, r as sanitizeArg, rt as RelationshipNotFoundError, s as getMigrationStatus, st as matchesScope, t as createForwardingLogger, tt as InvalidOperationTimestampError, u as instrumentPgPool, v as KyselyExecutionScope, w as CollectionMembershipCache, x as KyselyWriteCache, y as resolveFeatureFlags, z as documentDecisionModel } from "./forwarding-logger-CnuVDVze.js";
2
+ import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-BM_ULVWd.js";
3
+ import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-BVTGbEB0.js";
7
4
  import { DEFAULT_SIGNATURE_POLICY, DowngradeNotSupportedError, MAX_AUTH_GRANTS, UnsupportedDocumentModelVersionError, actionSignerIdentity, actionSigningTarget, actions, actions as documentActions, createCopyHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, normalizeDocumentModelVersion, requestedSignaturePolicy, sortOperations, toTransportAction, withSignaturePolicy } from "@powerhousedao/shared/document-model";
8
5
  import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
9
6
  import { v4 } from "uuid";
@@ -315,6 +312,10 @@ let JobStatus = /* @__PURE__ */ function(JobStatus) {
315
312
  * during construction, so the partial-`this` hazard does not apply.
316
313
  */
317
314
  var DriveClient = class {
315
+ client;
316
+ logger;
317
+ reactor;
318
+ signer;
318
319
  constructor(client, logger, reactor, signer) {
319
320
  this.client = client;
320
321
  this.logger = logger;
@@ -337,24 +338,25 @@ var DriveClient = class {
337
338
  async addFile(driveIdentifier, document, parentFolder, signal) {
338
339
  this.logger.verbose("drives.addFile(@driveIdentifier, @document, @parentFolder)", driveIdentifier, document.header.id, parentFolder);
339
340
  const documentId = document.header.id;
341
+ const createInput = {
342
+ model: document.header.documentType,
343
+ version: 0,
344
+ documentId: document.header.id,
345
+ signing: {
346
+ signature: document.header.id,
347
+ publicKey: document.header.sig.publicKey,
348
+ nonce: document.header.sig.nonce,
349
+ createdAtUtcIso: document.header.createdAtUtcIso,
350
+ documentType: document.header.documentType
351
+ },
352
+ slug: document.header.slug,
353
+ name: document.header.name,
354
+ branch: document.header.branch,
355
+ meta: document.header.meta,
356
+ protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
357
+ };
340
358
  const documentActions = await signActions([
341
- createDocumentAction({
342
- model: document.header.documentType,
343
- version: 0,
344
- documentId: document.header.id,
345
- signing: {
346
- signature: document.header.id,
347
- publicKey: document.header.sig.publicKey,
348
- nonce: document.header.sig.nonce,
349
- createdAtUtcIso: document.header.createdAtUtcIso,
350
- documentType: document.header.documentType
351
- },
352
- slug: document.header.slug,
353
- name: document.header.name,
354
- branch: document.header.branch,
355
- meta: document.header.meta,
356
- protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
357
- }),
359
+ createDocumentAction(createInput),
358
360
  upgradeDocumentAction({
359
361
  documentId: document.header.id,
360
362
  model: document.header.documentType,
@@ -502,8 +504,7 @@ var DriveClient = class {
502
504
  return {
503
505
  results: filtered.slice(startIndex, endIndex),
504
506
  options: effective,
505
- ...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {},
506
- totalCount: filtered.length
507
+ ...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {}
507
508
  };
508
509
  }
509
510
  /**
@@ -514,7 +515,7 @@ var DriveClient = class {
514
515
  async runJobs(jobs, signal) {
515
516
  const batchResult = await this.reactor.executeBatch({ jobs }, signal);
516
517
  const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
517
- for (const job of completedJobs) if (job.status === JobStatus.FAILED) throw this.toError(job);
518
+ for (const job of completedJobs) if (job.status === "FAILED") throw this.toError(job);
518
519
  return completedJobs;
519
520
  }
520
521
  toError(job) {
@@ -555,10 +556,10 @@ var DriveClient = class {
555
556
  dependsOn: ["relationship"]
556
557
  }] }, signal);
557
558
  const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
558
- for (const job of completedJobs) if (job.status === JobStatus.FAILED) throw new Error(job.error?.message);
559
+ for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
559
560
  const deleteJob = await this.reactor.deleteDocument(fileId, this.signer, signal);
560
561
  const deleteCompleted = await this.client.waitForJob(deleteJob, signal);
561
- if (deleteCompleted.status === JobStatus.FAILED) throw new Error(deleteCompleted.error?.message);
562
+ if (deleteCompleted.status === "FAILED") throw new Error(deleteCompleted.error?.message);
562
563
  }
563
564
  };
564
565
  //#endregion
@@ -569,13 +570,15 @@ var DriveClient = class {
569
570
  * Only READ_READY and FAILED are truly terminal.
570
571
  */
571
572
  function isTerminalStatus(status) {
572
- return status === JobStatus.READ_READY || status === JobStatus.FAILED;
573
+ return status === "READ_READY" || status === "FAILED";
573
574
  }
574
575
  /**
575
576
  * Event-driven implementation of IJobAwaiter.
576
577
  * Subscribes to operation events to detect job completion without polling.
577
578
  */
578
579
  var JobAwaiter = class {
580
+ eventBus;
581
+ getJobStatus;
579
582
  pendingJobs = /* @__PURE__ */ new Map();
580
583
  unsubscribers = [];
581
584
  constructor(eventBus, getJobStatus) {
@@ -717,7 +720,54 @@ let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
717
720
  * would diverge permanently. The document scope carries the metadata the same
718
721
  * argument covers. Grants gate domain-scope reads only.
719
722
  */
720
- const ALWAYS_READABLE_SCOPES = new Set(["auth", "document"]);
723
+ const ALWAYS_READABLE_SCOPES = /* @__PURE__ */ new Set(["auth", "document"]);
724
+ const DECLARED_DOMAIN_SCOPES = ["global", "local"];
725
+ /**
726
+ * The scopes grants gate: the declared ones, and any other the document holds.
727
+ * A document fetched for a narrowed view holds only that view's scopes.
728
+ */
729
+ function domainScopesOf(document) {
730
+ const state = document.state;
731
+ const held = Object.keys(state ?? {}).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope));
732
+ return [.../* @__PURE__ */ new Set([...DECLARED_DOMAIN_SCOPES, ...held])];
733
+ }
734
+ function allowsSomeDomainScope(document, readable) {
735
+ return domainScopesOf(document).some(readable);
736
+ }
737
+ /** A document with no state at all is not policied, so nothing withholds it. */
738
+ function refusesEveryDomainScope(document, readable) {
739
+ return document.state !== void 0 && !allowsSomeDomainScope(document, readable);
740
+ }
741
+ /**
742
+ * The domain scopes the document does not hold whose read decision reads their
743
+ * own state. A condition's `doc.<scope>` path resolves only for that scope, so
744
+ * every other scope is decided on its name and the policy alone.
745
+ */
746
+ function unheldScopesReadOnState(document) {
747
+ const grants = authOf(document)?.grants ?? [];
748
+ const onState = /* @__PURE__ */ new Set();
749
+ for (const grant of grants) {
750
+ collectStateScopes(grant.where, onState);
751
+ collectStateScopes(grant.principal, onState);
752
+ }
753
+ if (onState.size === 0) return [];
754
+ const held = document.state ?? {};
755
+ return domainScopesOf(document).filter((scope) => onState.has(scope) && !(scope in held));
756
+ }
757
+ function collectStateScopes(node, into) {
758
+ if (Array.isArray(node)) {
759
+ for (const child of node) collectStateScopes(child, into);
760
+ return;
761
+ }
762
+ if (typeof node !== "object" || node === null) return;
763
+ const { attr } = node;
764
+ if (typeof attr === "string") {
765
+ const [root, scope] = attr.split(".");
766
+ if (root === "doc" && scope) into.add(scope);
767
+ return;
768
+ }
769
+ for (const child of Object.values(node)) collectStateScopes(child, into);
770
+ }
721
771
  /** What a gate does with an uninitialized policy when told nothing. */
722
772
  const OPEN_BY_DEFAULT = { withholdUninitialized: false };
723
773
  /**
@@ -780,8 +830,26 @@ var BareReadGate = class {
780
830
  scope
781
831
  }) === "allow");
782
832
  }
833
+ prepare(document) {
834
+ return Promise.resolve((subject) => this.scopePredicate(document, subject));
835
+ }
783
836
  };
784
837
  /**
838
+ * Rethrows a failed read unless the document is confirmed not live. The read
839
+ * side reports absence as a plain Error, so absence is checked rather than
840
+ * inferred from the message, and a failed check surfaces the read's own error.
841
+ */
842
+ async function assertAbsent(documentView, documentId, error, signal) {
843
+ if (DocumentNotFoundError.isError(error)) return;
844
+ let exists;
845
+ try {
846
+ exists = await documentView.exists([documentId], "LiveOnly", void 0, signal);
847
+ } catch {
848
+ throw error;
849
+ }
850
+ if (exists[0]) throw error;
851
+ }
852
+ /**
785
853
  * Answers a stream read from the document already fetched, and anything else
786
854
  * through the read side.
787
855
  *
@@ -791,6 +859,9 @@ var BareReadGate = class {
791
859
  * list names is fetched.
792
860
  */
793
861
  var SeededStateReader = class {
862
+ documentView;
863
+ seed;
864
+ branch;
794
865
  constructor(documentView, seed, branch) {
795
866
  this.documentView = documentView;
796
867
  this.seed = seed;
@@ -812,20 +883,10 @@ var SeededStateReader = class {
812
883
  scopes: [scope]
813
884
  }, void 0, signal);
814
885
  } catch (error) {
815
- await this.assertAbsent(documentId, error, signal);
886
+ await assertAbsent(this.documentView, documentId, error, signal);
816
887
  throw new DocumentNotFoundError(documentId);
817
888
  }
818
889
  }
819
- async assertAbsent(documentId, error, signal) {
820
- if (DocumentNotFoundError.isError(error)) return;
821
- let exists;
822
- try {
823
- exists = await this.documentView.exists([documentId], DocumentExistence.LiveOnly, void 0, signal);
824
- } catch {
825
- throw error;
826
- }
827
- if (exists[0]) throw error;
828
- }
829
890
  };
830
891
  /**
831
892
  * Evaluates a read against the registered decision model, built at the stream
@@ -841,6 +902,12 @@ var SeededStateReader = class {
841
902
  * from state no commit ever invalidates.
842
903
  */
843
904
  var ModelReadGate = class {
905
+ model;
906
+ documentView;
907
+ servesGroups;
908
+ operationIndex;
909
+ logger;
910
+ options;
844
911
  constructor(model, documentView, servesGroups, operationIndex, logger, options = OPEN_BY_DEFAULT) {
845
912
  this.model = model;
846
913
  this.documentView = documentView;
@@ -855,10 +922,18 @@ var ModelReadGate = class {
855
922
  * group's other scopes are its own business and stay behind its own grants.
856
923
  */
857
924
  async scopePredicate(document, subject, branch, signal) {
858
- const own = await this.ownPolicyPredicate(document, subject, branch, signal);
859
- if (!this.servesGroup(document) || own(GROUP_MEMBERSHIP_SCOPE)) return own;
860
- const audience = await this.servesGroupTo(document.header.id, subject, signal);
861
- return (scope) => own(scope) || audience && scope === GROUP_MEMBERSHIP_SCOPE;
925
+ return (await this.prepare(document, branch, signal))(subject);
926
+ }
927
+ async prepare(document, branch, signal) {
928
+ const policy = await this.ownPolicy(document, branch, signal);
929
+ if (!this.servesGroup(document)) return (subject) => Promise.resolve(policy(subject));
930
+ const referencers = this.referencerReads(document.header.id, signal);
931
+ return async (subject) => {
932
+ const own = policy(subject);
933
+ if (own(GROUP_MEMBERSHIP_SCOPE)) return own;
934
+ const audience = await this.servesGroupTo(subject, referencers);
935
+ return (scope) => own(scope) || audience && scope === GROUP_MEMBERSHIP_SCOPE;
936
+ };
862
937
  }
863
938
  /**
864
939
  * Whether the subject is served this group because a policy names it.
@@ -892,11 +967,8 @@ var ModelReadGate = class {
892
967
  * running with nobody awaiting it, which is where unhandled rejections come
893
968
  * from.
894
969
  */
895
- async servesGroupTo(groupId, subject, signal) {
896
- if (!this.operationIndex) return false;
897
- const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
898
- const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
899
- if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
970
+ async servesGroupTo(subject, referencers) {
971
+ const examined = await referencers.examined();
900
972
  const walk = {
901
973
  next: 0,
902
974
  served: false,
@@ -907,7 +979,7 @@ var ModelReadGate = class {
907
979
  const referencerId = examined[walk.next++];
908
980
  let serves = false;
909
981
  try {
910
- serves = await this.servesThrough(referencerId, subject, signal);
982
+ serves = (await referencers.serves(referencerId))(subject);
911
983
  } catch (error) {
912
984
  walk.failure ??= Error.isError(error) ? error : /* @__PURE__ */ new Error(`Probing referencer ${referencerId} failed`);
913
985
  }
@@ -918,21 +990,44 @@ var ModelReadGate = class {
918
990
  if (!walk.served && walk.failure) throw walk.failure;
919
991
  return walk.served;
920
992
  }
993
+ /** Each read made once, however many subjects the walk runs for. */
994
+ referencerReads(groupId, signal) {
995
+ let examined;
996
+ const serves = /* @__PURE__ */ new Map();
997
+ return {
998
+ examined: () => examined ??= this.examinedReferencers(groupId, signal),
999
+ serves: (referencerId) => {
1000
+ let read = serves.get(referencerId);
1001
+ if (!read) {
1002
+ read = this.servesThrough(referencerId, signal);
1003
+ serves.set(referencerId, read);
1004
+ }
1005
+ return read;
1006
+ }
1007
+ };
1008
+ }
1009
+ async examinedReferencers(groupId, signal) {
1010
+ if (!this.operationIndex) return [];
1011
+ const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
1012
+ const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
1013
+ if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
1014
+ return examined;
1015
+ }
921
1016
  /**
922
- * Whether one referencing document serves the subject any domain scope. A
1017
+ * Whether one referencing document serves a subject any domain scope. A
923
1018
  * referencer this replica does not hold serves nothing, which fails closed
924
1019
  * the same way a group it does not hold does.
925
1020
  */
926
- async servesThrough(referencerId, subject, signal) {
1021
+ async servesThrough(referencerId, signal) {
927
1022
  let referencer;
928
1023
  try {
929
1024
  referencer = await this.documentView.get(referencerId, { branch: GROUP_BRANCH }, void 0, signal);
930
1025
  } catch {
931
- return false;
1026
+ return () => false;
932
1027
  }
933
- if (this.servesGroup(referencer)) return false;
934
- const readable = await this.ownPolicyPredicate(referencer, subject, GROUP_BRANCH, signal);
935
- return Object.keys(referencer.state ?? {}).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope)).some((scope) => readable(scope));
1028
+ if (this.servesGroup(referencer)) return () => false;
1029
+ const policy = await this.ownPolicy(referencer, GROUP_BRANCH, signal);
1030
+ return (subject) => allowsSomeDomainScope(referencer, policy(subject));
936
1031
  }
937
1032
  servesGroup(document) {
938
1033
  return this.servesGroups && this.operationIndex !== void 0 && document.header.documentType === groupDocumentType;
@@ -950,9 +1045,12 @@ var ModelReadGate = class {
950
1045
  * publish an unpoliced group it names: the referencer walk asks this question
951
1046
  * of the referencing document, whose real policy answers it.
952
1047
  */
953
- async ownPolicyPredicate(document, subject, branch, signal) {
1048
+ async ownPolicy(document, branch, signal) {
954
1049
  const auth = authOf(document);
955
- if (!auth || !auth.version) return this.options.withholdUninitialized ? (scope) => ALWAYS_READABLE_SCOPES.has(scope) : () => true;
1050
+ if (!auth || !auth.version) {
1051
+ const readable = this.options.withholdUninitialized ? (scope) => ALWAYS_READABLE_SCOPES.has(scope) : () => true;
1052
+ return () => readable;
1053
+ }
956
1054
  const target = {
957
1055
  documentId: document.header.id,
958
1056
  branch
@@ -960,7 +1058,7 @@ var ModelReadGate = class {
960
1058
  const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), this.model, target, signal);
961
1059
  const definition = this.model(target);
962
1060
  const scopeStates = document.state ?? {};
963
- return (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
1061
+ return (subject) => (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
964
1062
  verb: "read",
965
1063
  scope
966
1064
  }, {
@@ -970,6 +1068,78 @@ var ModelReadGate = class {
970
1068
  }
971
1069
  };
972
1070
  //#endregion
1071
+ //#region src/client/event-reads.ts
1072
+ /** One change event's reads, shared by every subscription it reaches. */
1073
+ var EventReads = class {
1074
+ reactor;
1075
+ documentView;
1076
+ readGate;
1077
+ documents = /* @__PURE__ */ new Map();
1078
+ existence = /* @__PURE__ */ new Map();
1079
+ prepared = /* @__PURE__ */ new WeakMap();
1080
+ constructor(reactor, documentView, readGate) {
1081
+ this.reactor = reactor;
1082
+ this.documentView = documentView;
1083
+ this.readGate = readGate;
1084
+ }
1085
+ get(id, view) {
1086
+ const key = JSON.stringify([
1087
+ id,
1088
+ view?.branch,
1089
+ view?.scopes
1090
+ ]);
1091
+ let read = this.documents.get(key);
1092
+ if (!read) {
1093
+ read = this.reactor.get(id, view);
1094
+ this.documents.set(key, read);
1095
+ }
1096
+ return read;
1097
+ }
1098
+ exists(ids, existence) {
1099
+ const key = JSON.stringify([ids, existence]);
1100
+ let read = this.existence.get(key);
1101
+ if (!read) {
1102
+ read = this.documentView.exists(ids, existence);
1103
+ this.existence.set(key, read);
1104
+ }
1105
+ return read;
1106
+ }
1107
+ async scopePredicate(document, subject, branch) {
1108
+ if (!this.readGate.prepare) return this.readGate.scopePredicate(document, subject, branch);
1109
+ let byBranch = this.prepared.get(document);
1110
+ if (!byBranch) {
1111
+ byBranch = /* @__PURE__ */ new Map();
1112
+ this.prepared.set(document, byBranch);
1113
+ }
1114
+ let prepared = byBranch.get(branch);
1115
+ if (!prepared) {
1116
+ prepared = this.readGate.prepare(document, branch);
1117
+ byBranch.set(branch, prepared);
1118
+ }
1119
+ return (await prepared)(subject);
1120
+ }
1121
+ };
1122
+ var EventReadsSource = class {
1123
+ reactor;
1124
+ documentView;
1125
+ readGate;
1126
+ current;
1127
+ constructor(reactor, documentView, readGate) {
1128
+ this.reactor = reactor;
1129
+ this.documentView = documentView;
1130
+ this.readGate = readGate;
1131
+ }
1132
+ forEvent() {
1133
+ if (!this.current) {
1134
+ this.current = new EventReads(this.reactor, this.documentView, this.readGate);
1135
+ queueMicrotask(() => {
1136
+ this.current = void 0;
1137
+ });
1138
+ }
1139
+ return this.current;
1140
+ }
1141
+ };
1142
+ //#endregion
973
1143
  //#region src/client/util.ts
974
1144
  function authSubjectFromSigner(signer) {
975
1145
  return {
@@ -980,10 +1150,23 @@ function authSubjectFromSigner(signer) {
980
1150
  function withAuthScope(view) {
981
1151
  if (view?.scopes && view.scopes.length > 0) return {
982
1152
  ...view,
983
- scopes: [...new Set([...view.scopes, "auth"])]
1153
+ scopes: [.../* @__PURE__ */ new Set([...view.scopes, "auth"])]
984
1154
  };
985
1155
  return view;
986
1156
  }
1157
+ function withAllScopes(view) {
1158
+ if (!view?.scopes) return view;
1159
+ const { scopes: _narrowed, ...whole } = view;
1160
+ return whole;
1161
+ }
1162
+ function narrowedScopes(view) {
1163
+ if (!view?.scopes || view.scopes.length === 0) return;
1164
+ return /* @__PURE__ */ new Set([
1165
+ "document",
1166
+ "auth",
1167
+ ...view.scopes
1168
+ ]);
1169
+ }
987
1170
  /**
988
1171
  * Drops the scopes the predicate refuses. The predicate is resolved by the read
989
1172
  * gate, which decides against the whole policy once per document; this only
@@ -1056,6 +1239,7 @@ var ReactorClient = class {
1056
1239
  documentIndexer;
1057
1240
  documentView;
1058
1241
  readGate;
1242
+ eventReads;
1059
1243
  actionEvaluation;
1060
1244
  createSignaturePolicy;
1061
1245
  drives;
@@ -1068,33 +1252,12 @@ var ReactorClient = class {
1068
1252
  this.documentIndexer = documentIndexer;
1069
1253
  this.documentView = documentView;
1070
1254
  this.readGate = readGate;
1255
+ this.eventReads = new EventReadsSource(reactor, documentView, readGate);
1071
1256
  this.actionEvaluation = actionEvaluation;
1072
1257
  this.createSignaturePolicy = createSignaturePolicy;
1073
1258
  this.drives = new DriveClient(this, logger, reactor, signer);
1074
1259
  this.logger.verbose("ReactorClient initialized");
1075
1260
  }
1076
- readSubject(subject) {
1077
- return subject ?? authSubjectFromSigner(this.signer);
1078
- }
1079
- /**
1080
- * Which scopes of one document the subject may read. Resolved once per
1081
- * document, so the gate builds its model once however many scopes are then
1082
- * tested, and the filtering itself stays synchronous.
1083
- */
1084
- readableScopes(document, view, signal) {
1085
- return this.readGate.scopePredicate(document, this.readSubject(view?.subject), view?.branch ?? "main", signal);
1086
- }
1087
- /**
1088
- * One document, filtered to the scopes the subject may read. Every method
1089
- * that hands a document back goes through here, including the ones that
1090
- * follow a write: a document returned from a mutation is a read like any
1091
- * other, and returning it whole served scopes the same subject would be
1092
- * refused by `get`. Its author still sees what it wrote, because an allow on
1093
- * execute confers read of that scope.
1094
- */
1095
- async gateDocument(document, view, signal) {
1096
- return filterReadableScopes(document, await this.readableScopes(document, view, signal));
1097
- }
1098
1261
  /**
1099
1262
  * Retrieves a list of document model modules.
1100
1263
  */
@@ -1160,7 +1323,7 @@ var ReactorClient = class {
1160
1323
  /** True when the id is taken; a soft-deleted document's id is still taken. */
1161
1324
  async isDocumentIdTaken(documentId, signal) {
1162
1325
  this.logger.verbose("isDocumentIdTaken(@documentId)", documentId);
1163
- return (await this.documentView.exists([documentId], DocumentExistence.IncludingDeleted, void 0, signal))[0];
1326
+ return (await this.documentView.exists([documentId], "IncludingDeleted", void 0, signal))[0];
1164
1327
  }
1165
1328
  /**
1166
1329
  * Retrieves operations for a document
@@ -1200,31 +1363,6 @@ var ReactorClient = class {
1200
1363
  nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
1201
1364
  };
1202
1365
  }
1203
- async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
1204
- const scopeCursors = decodeCompositeCursor(paging.cursor);
1205
- const allOperations = [];
1206
- const activeCursors = {};
1207
- for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
1208
- if (!canRead(scopeName)) continue;
1209
- const scopeView = {
1210
- ...view,
1211
- scopes: [scopeName]
1212
- };
1213
- const scopePaging = {
1214
- cursor,
1215
- limit: paging.limit
1216
- };
1217
- const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
1218
- allOperations.push(...scopeResult.results);
1219
- if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
1220
- }
1221
- allOperations.sort((a, b) => a.index - b.index);
1222
- return {
1223
- results: allOperations,
1224
- options: paging,
1225
- nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
1226
- };
1227
- }
1228
1366
  /**
1229
1367
  * Retrieves outgoing relationships of a given type from a source document.
1230
1368
  */
@@ -1278,68 +1416,24 @@ var ReactorClient = class {
1278
1416
  return this.gateEdges(edges, "sourceId", view, signal);
1279
1417
  }
1280
1418
  /**
1281
- * Drops the edges whose far-end document the subject may read no domain scope
1282
- * of. An edge is withheld whole rather than stripped of its metadata: the
1283
- * document-shaped relationship reads already answer with the far end stripped
1284
- * to the scopes the gate allows, so the far end's existence is disclosed
1285
- * either way, but an edge's metadata is content about the pair that the far
1286
- * end's own reads would refuse. An edge to a far end stripped to nothing
1287
- * therefore carries content past a refusal, and there is no useful shell to
1288
- * hand back in its place.
1289
- *
1290
- * `nextCursor` and `options` are left as the underlying stream reported them,
1291
- * because a caller must feed them back to resume from the right position. A
1292
- * gated page can therefore be shorter than the limit it asked for.
1293
- */
1294
- async gateEdges(page, farEnd, view, signal) {
1295
- const ids = [...new Set(page.results.map((edge) => edge[farEnd]))];
1296
- if (ids.length === 0) return page;
1297
- const farEndView = {
1298
- subject: view?.subject,
1299
- branch: view?.branch
1300
- };
1301
- const documents = await this.reactor.find({ ids }, farEndView, {
1302
- cursor: "0",
1303
- limit: ids.length
1304
- }, void 0, signal);
1305
- const readable = new Map(await Promise.all(documents.results.map(async (doc) => {
1306
- const allows = await this.readableScopes(doc, view, signal);
1307
- const domainScopes = Object.keys(doc.state).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope));
1308
- return [doc.header.id, domainScopes.length === 0 || domainScopes.some(allows)];
1309
- })));
1310
- const results = page.results.filter((edge) => readable.get(edge[farEnd]) !== false);
1311
- if (results.length === page.results.length) return page;
1312
- const nextPage = page.next;
1313
- return {
1314
- ...page,
1315
- results,
1316
- next: nextPage ? async () => this.gateEdges(await nextPage(), farEnd, view, signal) : void 0
1317
- };
1318
- }
1319
- /**
1320
- * One relationship edge, or undefined when it does not exist. A point lookup:
1321
- * the pair is filtered in SQL rather than scanned out of the source's edge
1322
- * list, which on a drive with thousands of children is the difference between
1323
- * one query and dozens.
1324
- */
1325
- async readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, view, signal) {
1326
- const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
1327
- const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
1328
- return (await this.documentIndexer.getDirectedRelationships(sourceId, targetId, [relationshipType], {
1329
- cursor: "0",
1330
- limit: 1
1331
- }, void 0, signal)).results[0];
1332
- }
1333
- /**
1334
1419
  * Filters documents by criteria and returns a list of them
1335
1420
  */
1336
1421
  async find(search, view, paging, signal) {
1337
1422
  this.logger.verbose("find(@search, @view, @paging)", search, view, paging);
1338
1423
  const results = await this.reactor.find(search, withAuthScope(view), paging, void 0, signal);
1339
- return {
1340
- ...results,
1341
- results: await Promise.all(results.results.map(async (doc) => filterReadableScopes(doc, await this.readableScopes(doc, view, signal))))
1342
- };
1424
+ return this.gateListing(results, view, signal);
1425
+ }
1426
+ /** Whether `find` would serve the document; false when it is absent. */
1427
+ async isServed(identifier, view, signal) {
1428
+ this.logger.verbose("isServed(@identifier, @view)", identifier, view);
1429
+ let document;
1430
+ try {
1431
+ document = await this.reactor.getByIdOrSlug(identifier, withAuthScope(view), void 0, signal);
1432
+ } catch (error) {
1433
+ await assertAbsent(this.documentView, identifier, error, signal);
1434
+ return false;
1435
+ }
1436
+ return await this.gateListed(document, view, signal) !== void 0;
1343
1437
  }
1344
1438
  /**
1345
1439
  * Predicts the admission verdict for each candidate. See
@@ -1380,39 +1474,6 @@ var ReactorClient = class {
1380
1474
  anyDenied: allowed < candidates.length
1381
1475
  };
1382
1476
  }
1383
- /**
1384
- * The decision model for one target document, built at its stream heads.
1385
- *
1386
- * The document is fetched unfiltered, because the policy is what decides:
1387
- * reading it through the read gate would withhold the very scopes the
1388
- * decision is about. A deleted document is served at its deletion boundary,
1389
- * which is what lets the model refuse an execute against it -- authEnforcement
1390
- * requires documentDecisions, so that read is available whenever this runs.
1391
- *
1392
- * Reading past the gate discloses nothing a submit does not. The `auth` and
1393
- * `document` scopes are readable by every holder, so a verdict resting on the
1394
- * policy alone is one the caller could compute unaided; and a verdict resting
1395
- * on a conditional grant reads the executing scope's state exactly as
1396
- * admission reads it, so the answer here is what submitting and being refused
1397
- * would have revealed anyway.
1398
- *
1399
- * The append condition the build records is dropped. It guards a write, and
1400
- * this makes none; reproducing it is also what the preflight cannot do, which
1401
- * is why the answer is a prediction.
1402
- */
1403
- async buildEvaluationTarget(config, documentId, branch, signal) {
1404
- const document = await this.documentView.get(documentId, { branch }, void 0, signal);
1405
- const target = {
1406
- documentId,
1407
- branch
1408
- };
1409
- const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
1410
- return {
1411
- definition: config.model(target),
1412
- model: built.model,
1413
- scopeStates: document.state ?? {}
1414
- };
1415
- }
1416
1477
  getCreateSignaturePolicy() {
1417
1478
  return Promise.resolve(this.createSignaturePolicy);
1418
1479
  }
@@ -1423,7 +1484,7 @@ var ReactorClient = class {
1423
1484
  this.logger.verbose("create(@id, @parentIdentifier)", document.header.id, parentIdentifier);
1424
1485
  const documentId = document.header.id;
1425
1486
  const branch = document.header.branch || "main";
1426
- const createActions = await signActions([createDocumentAction({
1487
+ const createInput = {
1427
1488
  model: document.header.documentType,
1428
1489
  version: 0,
1429
1490
  documentId,
@@ -1439,7 +1500,8 @@ var ReactorClient = class {
1439
1500
  branch: document.header.branch,
1440
1501
  meta: document.header.meta,
1441
1502
  protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
1442
- }), upgradeDocumentAction({
1503
+ };
1504
+ const createActions = await signActions([createDocumentAction(createInput), upgradeDocumentAction({
1443
1505
  documentId,
1444
1506
  model: document.header.documentType,
1445
1507
  fromVersion: 0,
@@ -1473,7 +1535,7 @@ var ReactorClient = class {
1473
1535
  }
1474
1536
  const batchResult = await this.reactor.executeBatch({ jobs }, signal);
1475
1537
  const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.waitForJob(job, signal)));
1476
- for (const job of completedJobs) if (job.status === JobStatus.FAILED) throw new Error(job.error?.message);
1538
+ for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
1477
1539
  const created = await this.reactor.get(documentId);
1478
1540
  return this.gateDocument(created, void 0, signal);
1479
1541
  }
@@ -1521,22 +1583,26 @@ var ReactorClient = class {
1521
1583
  const branch = document.header.branch || "main";
1522
1584
  const fromVersion = normalizeDocumentModelVersion(document.state.document?.version);
1523
1585
  let targetVersion = toVersion;
1524
- if (targetVersion === void 0) targetVersion = normalizeDocumentModelVersion((await this.getDocumentModelModule(documentType)).version);
1586
+ if (targetVersion === void 0) {
1587
+ const module = await this.getDocumentModelModule(documentType);
1588
+ targetVersion = normalizeDocumentModelVersion(module.version);
1589
+ }
1525
1590
  if (targetVersion === fromVersion) return this.gateDocument(document, { branch }, signal);
1526
1591
  if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
1527
- const signedActions = await signActions([upgradeDocumentAction({
1592
+ const action = upgradeDocumentAction({
1528
1593
  documentId,
1529
1594
  model: documentType,
1530
1595
  fromVersion,
1531
1596
  toVersion: targetVersion,
1532
1597
  revision: { ...document.header.revision }
1533
- })], this.signer, {
1598
+ });
1599
+ const signedActions = await signActions([action], this.signer, {
1534
1600
  documentId,
1535
1601
  branch
1536
1602
  }, signal);
1537
1603
  const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
1538
1604
  const completedJob = await this.waitForJob(jobInfo, signal);
1539
- if (completedJob.status !== JobStatus.FAILED) {
1605
+ if (completedJob.status !== "FAILED") {
1540
1606
  const upgraded = await this.reactor.getByIdOrSlug(documentId, { branch }, completedJob.consistencyToken, signal);
1541
1607
  return this.gateDocument(upgraded, { branch }, signal);
1542
1608
  }
@@ -1570,7 +1636,7 @@ var ReactorClient = class {
1570
1636
  }, signal);
1571
1637
  const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
1572
1638
  const completedJob = await this.waitForJob(jobInfo, signal);
1573
- if (completedJob.status === JobStatus.FAILED) throw new Error(completedJob.error?.message);
1639
+ if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
1574
1640
  const view = {
1575
1641
  branch,
1576
1642
  subject
@@ -1590,18 +1656,6 @@ var ReactorClient = class {
1590
1656
  }, signal);
1591
1657
  return this.reactor.execute(documentId, branch, signedActions, signal);
1592
1658
  }
1593
- /**
1594
- * The id a write on `identifier` is stored under, which its signatures bind.
1595
- * A create names its own id, and an id no slug maps to is taken as given, so
1596
- * a document still in flight resolves to itself.
1597
- */
1598
- async resolveWriteTarget(identifier, branch, actions, signal) {
1599
- if (actions.some((action) => action.type === "CREATE_DOCUMENT")) return identifier;
1600
- const view = { branch };
1601
- const bySlug = await this.documentView.resolveSlug(identifier, view, void 0, signal);
1602
- if (bySlug === void 0 || bySlug === identifier) return identifier;
1603
- return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
1604
- }
1605
1659
  async executeBatch(request, signal) {
1606
1660
  this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
1607
1661
  const signedJobs = await Promise.all(request.jobs.map(async (job) => {
@@ -1617,7 +1671,7 @@ var ReactorClient = class {
1617
1671
  }));
1618
1672
  const batchResult = await this.reactor.executeBatch({ jobs: signedJobs }, signal);
1619
1673
  const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.waitForJob(job, signal)));
1620
- for (const job of completedJobs) if (job.status === JobStatus.FAILED) throw new Error(job.error?.message);
1674
+ for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
1621
1675
  return batchResult;
1622
1676
  }
1623
1677
  /**
@@ -1642,7 +1696,7 @@ var ReactorClient = class {
1642
1696
  this.logger.verbose("addRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @metadata, @branch)", sourceIdentifier, targetIdentifier, relationshipType, metadata, branch);
1643
1697
  const jobInfo = await this.reactor.addRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
1644
1698
  const completedJob = await this.waitForJob(jobInfo, signal);
1645
- if (completedJob.status === JobStatus.FAILED) throw new Error(completedJob.error?.message);
1699
+ if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
1646
1700
  const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
1647
1701
  return this.gateDocument(result, { branch }, signal);
1648
1702
  }
@@ -1654,7 +1708,7 @@ var ReactorClient = class {
1654
1708
  if (!await this.readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, { branch }, signal)) throw new RelationshipNotFoundError(sourceIdentifier, targetIdentifier, relationshipType);
1655
1709
  const jobInfo = await this.reactor.updateRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
1656
1710
  const completedJob = await this.waitForJob(jobInfo, signal);
1657
- if (completedJob.status === JobStatus.FAILED) throw new Error(completedJob.error?.message);
1711
+ if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
1658
1712
  const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
1659
1713
  return this.gateDocument(result, { branch }, signal);
1660
1714
  }
@@ -1665,7 +1719,7 @@ var ReactorClient = class {
1665
1719
  this.logger.verbose("removeRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @branch)", sourceIdentifier, targetIdentifier, relationshipType, branch);
1666
1720
  const jobInfo = await this.reactor.removeRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
1667
1721
  const completedJob = await this.waitForJob(jobInfo, signal);
1668
- if (completedJob.status === JobStatus.FAILED) throw new Error(completedJob.error?.message);
1722
+ if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
1669
1723
  const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
1670
1724
  return this.gateDocument(result, { branch }, signal);
1671
1725
  }
@@ -1677,10 +1731,10 @@ var ReactorClient = class {
1677
1731
  const metadata = (await this.readRelationshipEdge(sourceParentIdentifier, targetIdentifier, relationshipType, { branch }, signal))?.metadata;
1678
1732
  const removeJobInfo = await this.reactor.removeRelationship(sourceParentIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
1679
1733
  const removeCompletedJob = await this.waitForJob(removeJobInfo, signal);
1680
- if (removeCompletedJob.status === JobStatus.FAILED) throw new Error(removeCompletedJob.error?.message);
1734
+ if (removeCompletedJob.status === "FAILED") throw new Error(removeCompletedJob.error?.message);
1681
1735
  const addJobInfo = await this.reactor.addRelationship(targetParentIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
1682
1736
  const addCompletedJob = await this.waitForJob(addJobInfo, signal);
1683
- if (addCompletedJob.status === JobStatus.FAILED) throw new Error(addCompletedJob.error?.message);
1737
+ if (addCompletedJob.status === "FAILED") throw new Error(addCompletedJob.error?.message);
1684
1738
  const sourceResult = await this.reactor.getByIdOrSlug(sourceParentIdentifier, { branch }, removeCompletedJob.consistencyToken, signal);
1685
1739
  const targetResult = await this.reactor.getByIdOrSlug(targetParentIdentifier, { branch }, addCompletedJob.consistencyToken, signal);
1686
1740
  return {
@@ -1694,7 +1748,7 @@ var ReactorClient = class {
1694
1748
  const completedJobs = await Promise.all(Object.entries(result.jobs).map(async ([key, jobInfo]) => {
1695
1749
  return [key, await this.waitForJob(jobInfo, signal)];
1696
1750
  }));
1697
- for (const [, completedJob] of completedJobs) if (completedJob.status === JobStatus.FAILED) throw new Error(completedJob.error?.message);
1751
+ for (const [, completedJob] of completedJobs) if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
1698
1752
  return { jobs: Object.fromEntries(completedJobs) };
1699
1753
  }
1700
1754
  /**
@@ -1703,8 +1757,8 @@ var ReactorClient = class {
1703
1757
  async deleteDocument(identifier, propagate, signal) {
1704
1758
  this.logger.verbose("deleteDocument(@identifier, @propagate)", identifier, propagate);
1705
1759
  const jobs = [];
1706
- if (propagate === PropagationMode.Cascade) {
1707
- const toDelete = new Set([identifier]);
1760
+ if (propagate === "cascade") {
1761
+ const toDelete = /* @__PURE__ */ new Set([identifier]);
1708
1762
  let changed = true;
1709
1763
  while (changed) {
1710
1764
  if (signal?.aborted) throw new Error("Operation aborted");
@@ -1728,7 +1782,7 @@ var ReactorClient = class {
1728
1782
  const jobInfo = await this.reactor.deleteDocument(identifier, this.signer, signal);
1729
1783
  jobs.push(jobInfo);
1730
1784
  const completedJobs = await Promise.all(jobs.map((job) => this.waitForJob(job, signal)));
1731
- for (const completedJob of completedJobs) if (completedJob.status === JobStatus.FAILED) throw new Error(completedJob.error?.message);
1785
+ for (const completedJob of completedJobs) if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
1732
1786
  }
1733
1787
  /**
1734
1788
  * Deletes documents and waits for completion
@@ -1758,7 +1812,13 @@ var ReactorClient = class {
1758
1812
  */
1759
1813
  subscribe(search, callback, view) {
1760
1814
  this.logger.verbose("subscribe(@search, @view)", search, view);
1761
- const readable = async (document) => filterReadableScopes(document, await this.readableScopes(document, view));
1815
+ const served = async (type, documents, reads) => {
1816
+ const documentsServed = await this.gateServedAll(documents, view, void 0, reads);
1817
+ return documentsServed.length > 0 ? {
1818
+ type,
1819
+ documents: documentsServed
1820
+ } : void 0;
1821
+ };
1762
1822
  let disposed = false;
1763
1823
  let delivering = Promise.resolve();
1764
1824
  /**
@@ -1772,49 +1832,46 @@ var ReactorClient = class {
1772
1832
  * gated is withheld, because serving it unfiltered would leak the scopes
1773
1833
  * the gate did not clear -- but it is logged rather than swallowed, since
1774
1834
  * the gate rethrows a transient failure precisely so it is not read as a
1775
- * denial.
1835
+ * denial. An event that resolves to undefined was withheld.
1776
1836
  */
1777
1837
  const deliver = (event) => {
1778
1838
  Promise.resolve(event).catch(() => void 0);
1779
1839
  delivering = delivering.then(async () => {
1780
1840
  const built = await event;
1781
- if (disposed) return;
1841
+ if (disposed || !built) return;
1782
1842
  callback(built);
1783
1843
  }).catch((error) => {
1784
1844
  this.logger.error("Subscription delivery failed for @search: @Error", { search }, error);
1785
1845
  });
1786
1846
  };
1787
1847
  const unsubscribeCreated = this.subscriptionManager.onDocumentCreated((result) => {
1848
+ const reads = this.eventReads.forEvent();
1788
1849
  deliver((async () => {
1789
- const documents = await Promise.all(result.results.map((id) => this.reactor.get(id, withAuthScope(view), void 0, void 0)));
1790
- return {
1791
- type: DocumentChangeType.Created,
1792
- documents: await Promise.all(documents.map(readable))
1793
- };
1850
+ const documents = await Promise.all(result.results.map((id) => reads.get(id, withAllScopes(view))));
1851
+ return served("created", documents, reads);
1794
1852
  })());
1795
1853
  }, search);
1796
1854
  const unsubscribeDeleted = this.subscriptionManager.onDocumentDeleted((documentIds) => {
1797
- deliver({
1798
- type: DocumentChangeType.Deleted,
1855
+ const reads = this.eventReads.forEvent();
1856
+ for (const childId of documentIds) deliver((async () => await this.servesEvery([childId], view, reads) ? {
1857
+ type: "deleted",
1799
1858
  documents: [],
1800
- context: { childId: documentIds[0] }
1801
- });
1859
+ context: { childId }
1860
+ } : void 0)());
1802
1861
  }, search);
1803
1862
  const unsubscribeUpdated = this.subscriptionManager.onDocumentStateUpdated((result) => {
1804
- deliver((async () => ({
1805
- type: DocumentChangeType.Updated,
1806
- documents: await Promise.all(result.results.map(readable))
1807
- }))());
1863
+ deliver(served("updated", result.results, this.eventReads.forEvent()));
1808
1864
  }, search, view);
1809
1865
  const unsubscribeRelationship = this.subscriptionManager.onRelationshipChanged((parentId, childId, changeType) => {
1810
- deliver({
1811
- type: changeType === RelationshipChangeType.Added ? DocumentChangeType.ChildAdded : DocumentChangeType.ChildRemoved,
1866
+ const reads = this.eventReads.forEvent();
1867
+ deliver((async () => await this.servesEvery([parentId, childId], view, reads) ? {
1868
+ type: changeType === "added" ? "child_added" : "child_removed",
1812
1869
  documents: [],
1813
1870
  context: {
1814
1871
  parentId,
1815
1872
  childId
1816
1873
  }
1817
- });
1874
+ } : void 0)());
1818
1875
  }, search);
1819
1876
  return () => {
1820
1877
  disposed = true;
@@ -1824,6 +1881,225 @@ var ReactorClient = class {
1824
1881
  unsubscribeRelationship();
1825
1882
  };
1826
1883
  }
1884
+ readSubject(subject) {
1885
+ return subject ?? authSubjectFromSigner(this.signer);
1886
+ }
1887
+ /**
1888
+ * Which scopes of one document the subject may read. Resolved once per
1889
+ * document, so the gate builds its model once however many scopes are then
1890
+ * tested, and the filtering itself stays synchronous.
1891
+ */
1892
+ readableScopes(document, view, signal, reads) {
1893
+ const subject = this.readSubject(view?.subject);
1894
+ const branch = view?.branch ?? "main";
1895
+ return reads ? reads.scopePredicate(document, subject, branch) : this.readGate.scopePredicate(document, subject, branch, signal);
1896
+ }
1897
+ /**
1898
+ * One document, filtered to the scopes the subject may read. Every method
1899
+ * that hands a document back goes through here, including the ones that
1900
+ * follow a write: a document returned from a mutation is a read like any
1901
+ * other, and returning it whole served scopes the same subject would be
1902
+ * refused by `get`. Its author still sees what it wrote, because an allow on
1903
+ * execute confers read of that scope.
1904
+ */
1905
+ async gateDocument(document, view, signal) {
1906
+ return filterReadableScopes(document, await this.readableScopes(document, view, signal));
1907
+ }
1908
+ /**
1909
+ * One document as a listing or a feed serves it, or undefined when withheld.
1910
+ * The document must hold every scope, or withholding is decided on a subset;
1911
+ * a narrowing view is applied here instead.
1912
+ */
1913
+ async gateServed(document, view, signal, reads) {
1914
+ const readable = await this.readableScopes(document, view, signal, reads);
1915
+ if (refusesEveryDomainScope(document, readable)) return;
1916
+ const narrowed = narrowedScopes(view);
1917
+ return filterReadableScopes(document, narrowed ? (scope) => readable(scope) && narrowed.has(scope) : readable);
1918
+ }
1919
+ async gateServedAll(documents, view, signal, reads) {
1920
+ return (await Promise.all(documents.map((document) => this.gateServed(document, view, signal, reads)))).filter((document) => document !== void 0);
1921
+ }
1922
+ /**
1923
+ * Gates a page and every page after it. `nextCursor` is left as the stream
1924
+ * reported it.
1925
+ */
1926
+ async gateListing(page, view, signal) {
1927
+ const results = (await Promise.all(page.results.map((document) => this.gateListed(document, view, signal)))).filter((document) => document !== void 0);
1928
+ const next = page.next;
1929
+ return {
1930
+ ...page,
1931
+ results,
1932
+ next: next ? async () => this.gateListing(await next(), view, signal) : void 0
1933
+ };
1934
+ }
1935
+ /**
1936
+ * One document as a listing serves it, or undefined when withheld. It holds
1937
+ * only the view's scopes; a domain scope it lacks is decided on its name, or
1938
+ * on its state read now when a condition needs that.
1939
+ */
1940
+ async gateListed(document, view, signal) {
1941
+ const readable = await this.readableScopes(document, view, signal);
1942
+ const unheld = unheldScopesReadOnState(document);
1943
+ if (refusesEveryDomainScope(document, unheld.length === 0 ? readable : (scope) => !unheld.includes(scope) && readable(scope)) && !await this.servesUnheld(document, unheld, view, signal)) return;
1944
+ return filterReadableScopes(document, readable);
1945
+ }
1946
+ async servesUnheld(document, scopes, view, signal) {
1947
+ if (scopes.length === 0) return false;
1948
+ const id = document.header.id;
1949
+ let held;
1950
+ try {
1951
+ held = await this.reactor.get(id, withAuthScope({
1952
+ ...view,
1953
+ scopes
1954
+ }), void 0, signal);
1955
+ } catch (error) {
1956
+ await assertAbsent(this.documentView, id, error, signal);
1957
+ return false;
1958
+ }
1959
+ const readable = await this.readableScopes(held, view, signal);
1960
+ return scopes.some(readable);
1961
+ }
1962
+ /**
1963
+ * Whether an event naming only these ids may reach the subject. An id with no
1964
+ * live document behind it has no content left to protect, so it does not
1965
+ * withhold; a deleted document is read at its deletion boundary when
1966
+ * deletion is positional, and gated like any other.
1967
+ */
1968
+ async servesEvery(ids, view, reads) {
1969
+ return (await Promise.all(ids.map((id) => this.servesId(id, view, reads)))).every(Boolean);
1970
+ }
1971
+ async servesId(id, view, reads) {
1972
+ let document;
1973
+ try {
1974
+ document = await reads.get(id, withAllScopes(view));
1975
+ } catch (error) {
1976
+ await assertAbsent(reads, id, error);
1977
+ return true;
1978
+ }
1979
+ return !refusesEveryDomainScope(document, await this.readableScopes(document, view, void 0, reads));
1980
+ }
1981
+ async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
1982
+ const scopeCursors = decodeCompositeCursor(paging.cursor);
1983
+ const allOperations = [];
1984
+ const activeCursors = {};
1985
+ for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
1986
+ if (!canRead(scopeName)) continue;
1987
+ const scopeView = {
1988
+ ...view,
1989
+ scopes: [scopeName]
1990
+ };
1991
+ const scopePaging = {
1992
+ cursor,
1993
+ limit: paging.limit
1994
+ };
1995
+ const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
1996
+ allOperations.push(...scopeResult.results);
1997
+ if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
1998
+ }
1999
+ allOperations.sort((a, b) => a.index - b.index);
2000
+ return {
2001
+ results: allOperations,
2002
+ options: paging,
2003
+ nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
2004
+ };
2005
+ }
2006
+ /**
2007
+ * Drops the edges whose far-end document the subject may read no domain scope
2008
+ * of. An edge is withheld whole rather than stripped of its metadata: the
2009
+ * document-shaped relationship reads already answer with the far end stripped
2010
+ * to the scopes the gate allows, so the far end's existence is disclosed
2011
+ * either way, but an edge's metadata is content about the pair that the far
2012
+ * end's own reads would refuse. An edge to a far end stripped to nothing
2013
+ * therefore carries content past a refusal, and there is no useful shell to
2014
+ * hand back in its place.
2015
+ *
2016
+ * `nextCursor` and `options` are left as the underlying stream reported them,
2017
+ * because a caller must feed them back to resume from the right position. A
2018
+ * gated page can therefore be shorter than the limit it asked for.
2019
+ */
2020
+ async gateEdges(page, farEnd, view, signal) {
2021
+ const ids = [...new Set(page.results.map((edge) => edge[farEnd]))];
2022
+ if (ids.length === 0) return page;
2023
+ const farEndView = {
2024
+ subject: view?.subject,
2025
+ branch: view?.branch
2026
+ };
2027
+ const documents = await this.reactor.find({ ids }, farEndView, {
2028
+ cursor: "0",
2029
+ limit: ids.length
2030
+ }, void 0, signal);
2031
+ const readable = new Map(await Promise.all(documents.results.map(async (doc) => {
2032
+ const allows = await this.readableScopes(doc, view, signal);
2033
+ return [doc.header.id, !refusesEveryDomainScope(doc, allows)];
2034
+ })));
2035
+ const results = page.results.filter((edge) => readable.get(edge[farEnd]) !== false);
2036
+ if (results.length === page.results.length) return page;
2037
+ const nextPage = page.next;
2038
+ return {
2039
+ ...page,
2040
+ results,
2041
+ next: nextPage ? async () => this.gateEdges(await nextPage(), farEnd, view, signal) : void 0
2042
+ };
2043
+ }
2044
+ /**
2045
+ * One relationship edge, or undefined when it does not exist. A point lookup:
2046
+ * the pair is filtered in SQL rather than scanned out of the source's edge
2047
+ * list, which on a drive with thousands of children is the difference between
2048
+ * one query and dozens.
2049
+ */
2050
+ async readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, view, signal) {
2051
+ const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
2052
+ const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
2053
+ return (await this.documentIndexer.getDirectedRelationships(sourceId, targetId, [relationshipType], {
2054
+ cursor: "0",
2055
+ limit: 1
2056
+ }, void 0, signal)).results[0];
2057
+ }
2058
+ /**
2059
+ * The id a write on `identifier` is stored under, which its signatures bind.
2060
+ * A create names its own id, and an id no slug maps to is taken as given, so
2061
+ * a document still in flight resolves to itself.
2062
+ */
2063
+ async resolveWriteTarget(identifier, branch, actions, signal) {
2064
+ if (actions.some((action) => action.type === "CREATE_DOCUMENT")) return identifier;
2065
+ const view = { branch };
2066
+ const bySlug = await this.documentView.resolveSlug(identifier, view, void 0, signal);
2067
+ if (bySlug === void 0 || bySlug === identifier) return identifier;
2068
+ return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
2069
+ }
2070
+ /**
2071
+ * The decision model for one target document, built at its stream heads.
2072
+ *
2073
+ * The document is fetched unfiltered, because the policy is what decides:
2074
+ * reading it through the read gate would withhold the very scopes the
2075
+ * decision is about. A deleted document is served at its deletion boundary,
2076
+ * which is what lets the model refuse an execute against it -- authEnforcement
2077
+ * requires documentDecisions, so that read is available whenever this runs.
2078
+ *
2079
+ * Reading past the gate discloses nothing a submit does not. The `auth` and
2080
+ * `document` scopes are readable by every holder, so a verdict resting on the
2081
+ * policy alone is one the caller could compute unaided; and a verdict resting
2082
+ * on a conditional grant reads the executing scope's state exactly as
2083
+ * admission reads it, so the answer here is what submitting and being refused
2084
+ * would have revealed anyway.
2085
+ *
2086
+ * The append condition the build records is dropped. It guards a write, and
2087
+ * this makes none; reproducing it is also what the preflight cannot do, which
2088
+ * is why the answer is a prediction.
2089
+ */
2090
+ async buildEvaluationTarget(config, documentId, branch, signal) {
2091
+ const document = await this.documentView.get(documentId, { branch }, void 0, signal);
2092
+ const target = {
2093
+ documentId,
2094
+ branch
2095
+ };
2096
+ const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
2097
+ return {
2098
+ definition: config.model(target),
2099
+ model: built.model,
2100
+ scopeStates: document.state ?? {}
2101
+ };
2102
+ }
1827
2103
  async removeAllIncomingRelationships(documentId, signal) {
1828
2104
  const incoming = await this.documentIndexer.getIncoming(documentId, void 0, void 0, void 0, signal);
1829
2105
  const jobs = [];
@@ -1837,18 +2113,6 @@ var ReactorClient = class {
1837
2113
  //#endregion
1838
2114
  //#region src/queue/types.ts
1839
2115
  /**
1840
- * State of a job in the queue
1841
- */
1842
- let JobQueueState = /* @__PURE__ */ function(JobQueueState) {
1843
- JobQueueState[JobQueueState["UNKNOWN"] = -1] = "UNKNOWN";
1844
- JobQueueState[JobQueueState["PREPROCESSING"] = 0] = "PREPROCESSING";
1845
- JobQueueState[JobQueueState["PENDING"] = 1] = "PENDING";
1846
- JobQueueState[JobQueueState["READY"] = 2] = "READY";
1847
- JobQueueState[JobQueueState["RUNNING"] = 3] = "RUNNING";
1848
- JobQueueState[JobQueueState["RESOLVED"] = 4] = "RESOLVED";
1849
- return JobQueueState;
1850
- }({});
1851
- /**
1852
2116
  * How a retry is accounted against the job's retry limit.
1853
2117
  * - `CountAgainstLimit` (default): a fault; the job eventually exhausts its
1854
2118
  * retries and fails terminally.
@@ -1881,6 +2145,11 @@ function toErrorInfo(error) {
1881
2145
  };
1882
2146
  }
1883
2147
  var JobResultHandler = class {
2148
+ queue;
2149
+ jobTracker;
2150
+ eventBus;
2151
+ resolver;
2152
+ logger;
1884
2153
  constructor(queue, jobTracker, eventBus, resolver, logger) {
1885
2154
  this.queue = queue;
1886
2155
  this.jobTracker = jobTracker;
@@ -1911,7 +2180,7 @@ var JobResultHandler = class {
1911
2180
  if (result.error && AppendConditionFailedError.isError(result.error) && this.countConflicts(handle.job) < MAX_EXEMPT_CONFLICT_RETRIES) {
1912
2181
  const errorInfo = toErrorInfo(result.error);
1913
2182
  try {
1914
- await this.queue.retryJob(handle.job.id, errorInfo, RetryAccounting.ExemptFromLimit);
2183
+ await this.queue.retryJob(handle.job.id, errorInfo, "exempt-from-limit");
1915
2184
  return;
1916
2185
  } catch {}
1917
2186
  }
@@ -2009,6 +2278,12 @@ var JobResultHandler = class {
2009
2278
  * is stranded at PENDING for as long as the deferral lasts.
2010
2279
  */
2011
2280
  var DeferredJobs = class {
2281
+ queue;
2282
+ jobTracker;
2283
+ eventBus;
2284
+ logger;
2285
+ ttlMs;
2286
+ onReleased;
2012
2287
  #byDocumentId = /* @__PURE__ */ new Map();
2013
2288
  #timers = /* @__PURE__ */ new Map();
2014
2289
  constructor(queue, jobTracker, eventBus, logger, ttlMs = DEFAULT_DEFERRED_JOB_TTL_MS, onReleased = () => Promise.resolve()) {
@@ -2170,6 +2445,13 @@ function bucketFor(documentId, numWorkers) {
2170
2445
  * (Powerhouse board wiki id: d400d711-f07e-4389-a226-4e9fdd4fa8ba)
2171
2446
  */
2172
2447
  var WorkerPoolJobExecutorManager = class {
2448
+ workerFactory;
2449
+ eventBus;
2450
+ queue;
2451
+ jobTracker;
2452
+ logger;
2453
+ resolver;
2454
+ operationIndex;
2173
2455
  workers = [];
2174
2456
  isRunning = false;
2175
2457
  activeJobs = 0;
@@ -2434,6 +2716,12 @@ function fillMissingMemberships(documentIds, found) {
2434
2716
  * Listens for job available events and dispatches jobs to executors.
2435
2717
  */
2436
2718
  var SimpleJobExecutorManager = class {
2719
+ executorFactory;
2720
+ eventBus;
2721
+ queue;
2722
+ jobTracker;
2723
+ logger;
2724
+ resolver;
2437
2725
  executors = [];
2438
2726
  isRunning = false;
2439
2727
  activeJobs = 0;
@@ -2608,6 +2896,7 @@ var SimpleJobExecutorManager = class {
2608
2896
  * Subscribes to operation events to update job states.
2609
2897
  */
2610
2898
  var InMemoryJobTracker = class {
2899
+ eventBus;
2611
2900
  jobs = /* @__PURE__ */ new Map();
2612
2901
  unsubscribers = [];
2613
2902
  constructor(eventBus) {
@@ -2628,11 +2917,11 @@ var InMemoryJobTracker = class {
2628
2917
  handleWriteReady(event) {
2629
2918
  const jobId = event.jobId;
2630
2919
  const job = this.jobs.get(jobId);
2631
- if (job && job.status === JobStatus.RUNNING) {
2920
+ if (job && job.status === "RUNNING") {
2632
2921
  const consistencyToken = createConsistencyToken(event.operations);
2633
2922
  this.jobs.set(jobId, {
2634
2923
  ...job,
2635
- status: JobStatus.WRITE_READY,
2924
+ status: "WRITE_READY",
2636
2925
  consistencyToken,
2637
2926
  result: summarizeSubmittedActions(event.operations, event.submittedActionIds)
2638
2927
  });
@@ -2641,9 +2930,9 @@ var InMemoryJobTracker = class {
2641
2930
  handleReadReady(event) {
2642
2931
  const jobId = event.jobId;
2643
2932
  const job = this.jobs.get(jobId);
2644
- if (job && job.status === JobStatus.WRITE_READY) this.jobs.set(jobId, {
2933
+ if (job && job.status === "WRITE_READY") this.jobs.set(jobId, {
2645
2934
  ...job,
2646
- status: JobStatus.READ_READY
2935
+ status: "READ_READY"
2647
2936
  });
2648
2937
  }
2649
2938
  handleJobFailed(event) {
@@ -2666,7 +2955,7 @@ var InMemoryJobTracker = class {
2666
2955
  this.jobs.set(jobId, {
2667
2956
  id: jobId,
2668
2957
  documentId: "",
2669
- status: JobStatus.RUNNING,
2958
+ status: "RUNNING",
2670
2959
  createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
2671
2960
  consistencyToken: createEmptyConsistencyToken(),
2672
2961
  meta: {
@@ -2678,7 +2967,7 @@ var InMemoryJobTracker = class {
2678
2967
  }
2679
2968
  this.jobs.set(jobId, {
2680
2969
  ...job,
2681
- status: JobStatus.RUNNING
2970
+ status: "RUNNING"
2682
2971
  });
2683
2972
  }
2684
2973
  markFailed(jobId, error, job) {
@@ -2687,7 +2976,7 @@ var InMemoryJobTracker = class {
2687
2976
  this.jobs.set(jobId, {
2688
2977
  id: jobId,
2689
2978
  documentId: job?.documentId ?? "",
2690
- status: JobStatus.FAILED,
2979
+ status: "FAILED",
2691
2980
  createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
2692
2981
  completedAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
2693
2982
  error,
@@ -2702,7 +2991,7 @@ var InMemoryJobTracker = class {
2702
2991
  }
2703
2992
  this.jobs.set(jobId, {
2704
2993
  ...existing,
2705
- status: JobStatus.FAILED,
2994
+ status: "FAILED",
2706
2995
  completedAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
2707
2996
  error,
2708
2997
  job,
@@ -2796,6 +3085,7 @@ function highestOf(ops) {
2796
3085
  return highest;
2797
3086
  }
2798
3087
  var ProcessorQueue = class {
3088
+ options;
2799
3089
  tasks = [];
2800
3090
  running = false;
2801
3091
  closed = false;
@@ -3378,11 +3668,11 @@ var JobExecutionHandle = class {
3378
3668
  onDefer;
3379
3669
  getStateName(state) {
3380
3670
  switch (state) {
3381
- case JobQueueState.PREPROCESSING: return "PREPROCESSING";
3382
- case JobQueueState.PENDING: return "PENDING";
3383
- case JobQueueState.READY: return "READY";
3384
- case JobQueueState.RUNNING: return "RUNNING";
3385
- case JobQueueState.RESOLVED: return "RESOLVED";
3671
+ case 0: return "PREPROCESSING";
3672
+ case 1: return "PENDING";
3673
+ case 2: return "READY";
3674
+ case 3: return "RUNNING";
3675
+ case 4: return "RESOLVED";
3386
3676
  default: return `UNKNOWN`;
3387
3677
  }
3388
3678
  }
@@ -3401,23 +3691,23 @@ var JobExecutionHandle = class {
3401
3691
  return this._state;
3402
3692
  }
3403
3693
  start() {
3404
- if (this._state !== JobQueueState.READY) throw new Error(`Cannot start job in state ${this.getStateName(this._state)}`);
3405
- this._state = JobQueueState.RUNNING;
3694
+ if (this._state !== 2) throw new Error(`Cannot start job in state ${this.getStateName(this._state)}`);
3695
+ this._state = 3;
3406
3696
  this.onStart?.();
3407
3697
  }
3408
3698
  complete() {
3409
- if (this._state !== JobQueueState.RUNNING) throw new Error(`Cannot complete job in state ${this.getStateName(this._state)}`);
3410
- this._state = JobQueueState.RESOLVED;
3699
+ if (this._state !== 3) throw new Error(`Cannot complete job in state ${this.getStateName(this._state)}`);
3700
+ this._state = 4;
3411
3701
  this.onComplete?.();
3412
3702
  }
3413
3703
  fail(error) {
3414
- if (this._state !== JobQueueState.RUNNING) throw new Error(`Cannot fail job in state ${this.getStateName(this._state)}`);
3415
- this._state = JobQueueState.RESOLVED;
3704
+ if (this._state !== 3) throw new Error(`Cannot fail job in state ${this.getStateName(this._state)}`);
3705
+ this._state = 4;
3416
3706
  this.onFail?.(error);
3417
3707
  }
3418
3708
  defer() {
3419
- if (this._state !== JobQueueState.RUNNING) throw new Error(`Cannot defer job in state ${this.getStateName(this._state)}`);
3420
- this._state = JobQueueState.RESOLVED;
3709
+ if (this._state !== 3) throw new Error(`Cannot defer job in state ${this.getStateName(this._state)}`);
3710
+ this._state = 4;
3421
3711
  this.onDefer?.();
3422
3712
  }
3423
3713
  };
@@ -3430,6 +3720,8 @@ var JobExecutionHandle = class {
3430
3720
  * Implements dependency management through queue hints.
3431
3721
  */
3432
3722
  var InMemoryQueue = class {
3723
+ eventBus;
3724
+ resolver;
3433
3725
  queues = /* @__PURE__ */ new Map();
3434
3726
  jobIdToQueueKey = /* @__PURE__ */ new Map();
3435
3727
  docIdToJobId = /* @__PURE__ */ new Map();
@@ -3564,7 +3856,7 @@ var InMemoryQueue = class {
3564
3856
  this.jobIdToQueueKey.delete(job.id);
3565
3857
  this.markJobExecuting(job);
3566
3858
  if (queue.length === 0) this.queues.delete(queueKey);
3567
- const handle = new JobExecutionHandle(job, JobQueueState.READY, {
3859
+ const handle = new JobExecutionHandle(job, 2, {
3568
3860
  onStart: () => {},
3569
3861
  onComplete: () => {
3570
3862
  this.completeJob(job.id);
@@ -3590,7 +3882,7 @@ var InMemoryQueue = class {
3590
3882
  this.jobIdToQueueKey.delete(job.id);
3591
3883
  this.markJobExecuting(job);
3592
3884
  if (queue.length === 0) this.queues.delete(queueKey);
3593
- const handle = new JobExecutionHandle(job, JobQueueState.READY, {
3885
+ const handle = new JobExecutionHandle(job, 2, {
3594
3886
  onStart: () => {},
3595
3887
  onComplete: () => {
3596
3888
  this.completeJob(job.id);
@@ -3624,7 +3916,7 @@ var InMemoryQueue = class {
3624
3916
  this.jobIdToQueueKey.delete(job.id);
3625
3917
  this.markJobExecuting(job);
3626
3918
  if (queue.length === 0) this.queues.delete(queueKey);
3627
- const handle = new JobExecutionHandle(job, JobQueueState.READY, {
3919
+ const handle = new JobExecutionHandle(job, 2, {
3628
3920
  onStart: () => {},
3629
3921
  onComplete: () => {
3630
3922
  this.completeJob(job.id);
@@ -3726,7 +4018,7 @@ var InMemoryQueue = class {
3726
4018
  if (documentId) this.markJobComplete(jobId, documentId);
3727
4019
  this.jobIndex.delete(jobId);
3728
4020
  }
3729
- async retryJob(jobId, error, accounting = RetryAccounting.CountAgainstLimit) {
4021
+ async retryJob(jobId, error, accounting = "count-against-limit") {
3730
4022
  const job = this.jobIndex.get(jobId);
3731
4023
  if (!job) return;
3732
4024
  job.lastError = error;
@@ -3738,7 +4030,7 @@ var InMemoryQueue = class {
3738
4030
  const retryCount = job.retryCount || 0;
3739
4031
  const updatedJob = {
3740
4032
  ...job,
3741
- retryCount: accounting === RetryAccounting.CountAgainstLimit ? retryCount + 1 : retryCount,
4033
+ retryCount: accounting === "count-against-limit" ? retryCount + 1 : retryCount,
3742
4034
  lastError: error
3743
4035
  };
3744
4036
  await this.enqueue(updatedJob);
@@ -3939,6 +4231,8 @@ function describeSource(source) {
3939
4231
  * and failure-caching state.
3940
4232
  */
3941
4233
  var DocumentModelResolver = class {
4234
+ registry;
4235
+ loader;
3942
4236
  loadingModels = /* @__PURE__ */ new Map();
3943
4237
  failedModelTypes = /* @__PURE__ */ new Set();
3944
4238
  broadcastHook = null;
@@ -4009,6 +4303,7 @@ var DocumentModelResolver = class {
4009
4303
  * Since there is no loader, missing models cannot be recovered.
4010
4304
  */
4011
4305
  var NullDocumentModelResolver = class {
4306
+ registry;
4012
4307
  constructor(registry) {
4013
4308
  this.registry = registry;
4014
4309
  }
@@ -4232,6 +4527,8 @@ var ReactorSubscriptionManager = class {
4232
4527
  * is emitted, so that reactor.get() returns fresh data when callbacks fire.
4233
4528
  */
4234
4529
  var SubscriptionNotificationReadModel = class {
4530
+ subscriptionManager;
4531
+ documentView;
4235
4532
  name = "subscription-notification";
4236
4533
  constructor(subscriptionManager, documentView) {
4237
4534
  this.subscriptionManager = subscriptionManager;
@@ -4254,10 +4551,10 @@ var SubscriptionNotificationReadModel = class {
4254
4551
  deleted.push(deletedId);
4255
4552
  } else if (actionType === "ADD_RELATIONSHIP") {
4256
4553
  const input = operation.action.input;
4257
- this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, RelationshipChangeType.Added, input.childType);
4554
+ this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, "added", input.childType);
4258
4555
  } else if (actionType === "REMOVE_RELATIONSHIP") {
4259
4556
  const input = operation.action.input;
4260
- this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, RelationshipChangeType.Removed, input.childType);
4557
+ this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, "removed", input.childType);
4261
4558
  } else if (!created.includes(context.documentId)) updatedIds.add(context.documentId);
4262
4559
  }
4263
4560
  if (created.length > 0) this.subscriptionManager.notifyDocumentsCreated(created, documentTypes, parentIds);
@@ -4282,6 +4579,10 @@ var SubscriptionNotificationReadModel = class {
4282
4579
  * keeps the common case (a membership write later than all history) free.
4283
4580
  */
4284
4581
  var GroupReevaluationTrigger = class {
4582
+ logger;
4583
+ eventBus;
4584
+ queue;
4585
+ operationIndex;
4285
4586
  unsubscribe;
4286
4587
  constructor(logger, eventBus, queue, operationIndex) {
4287
4588
  this.logger = logger;
@@ -4433,7 +4734,7 @@ var Mailbox = class {
4433
4734
  this.itemsMap.set(item.id, item);
4434
4735
  for (const op of item.operations) this._latestOrdinal = Math.max(this._latestOrdinal, op.context.ordinal);
4435
4736
  item.on((syncOp, _, next) => {
4436
- if (next === SyncOperationStatus.Applied) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
4737
+ if (next === 2) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
4437
4738
  });
4438
4739
  }
4439
4740
  if (this.paused) {
@@ -4547,7 +4848,7 @@ var BufferedMailbox = class {
4547
4848
  this.itemsMap.set(item.id, item);
4548
4849
  for (const op of item.operations) this._latestOrdinal = Math.max(this._latestOrdinal, op.context.ordinal);
4549
4850
  item.on((syncOp, _, next) => {
4550
- if (next === SyncOperationStatus.Applied) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
4851
+ if (next === 2) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
4551
4852
  });
4552
4853
  }
4553
4854
  this.addedBuffer.push(...items);
@@ -4661,7 +4962,14 @@ var GraphQLRequestError = class extends Error {
4661
4962
  * difference between a channel that recovers and one that is dead for the
4662
4963
  * process lifetime.
4663
4964
  */
4664
- const RECOVERABLE_GRAPHQL_ERROR_CODES = { malformedStoredOperation: "MALFORMED_STORED_OPERATION" };
4965
+ const RECOVERABLE_GRAPHQL_ERROR_CODES = {
4966
+ /**
4967
+ * A stored operation cannot be represented in the schema - an action with no
4968
+ * id, say. The document holding it needs repairing, but the channel serves
4969
+ * every other document, and a peer that stopped polling would stop receiving
4970
+ * those too.
4971
+ */
4972
+ malformedStoredOperation: "MALFORMED_STORED_OPERATION" };
4665
4973
  const RECOVERABLE_CODES = new Set(Object.values(RECOVERABLE_GRAPHQL_ERROR_CODES));
4666
4974
  /**
4667
4975
  * True when every error the response carried named a recoverable code.
@@ -4770,23 +5078,23 @@ var SyncOperation = class {
4770
5078
  this.scopes = scopes;
4771
5079
  this.branch = branch;
4772
5080
  this.operations = operations;
4773
- this.status = SyncOperationStatus.Unknown;
5081
+ this.status = -1;
4774
5082
  }
4775
5083
  on(callback) {
4776
5084
  this.callbacks.push(callback);
4777
5085
  }
4778
5086
  started() {
4779
- this.transition(SyncOperationStatus.TransportPending);
5087
+ this.transition(0);
4780
5088
  }
4781
5089
  transported() {
4782
- this.transition(SyncOperationStatus.ExecutionPending);
5090
+ this.transition(1);
4783
5091
  }
4784
5092
  executed() {
4785
- this.transition(SyncOperationStatus.Applied);
5093
+ this.transition(2);
4786
5094
  }
4787
5095
  failed(error) {
4788
5096
  this.error = error;
4789
- this.transition(SyncOperationStatus.Error);
5097
+ this.transition(3);
4790
5098
  }
4791
5099
  transition(next) {
4792
5100
  const prev = this.status;
@@ -4996,9 +5304,11 @@ function consolidateSyncOperations(syncOps) {
4996
5304
  }
4997
5305
  const first = group.ops[0];
4998
5306
  const merged = new SyncOperation(first.id, first.jobId, remappedDeps, first.remoteName, first.documentId, first.scopes, first.branch, allOperations);
4999
- if (first.status > SyncOperationStatus.TransportPending) if (first.status >= SyncOperationStatus.Error) merged.executed();
5000
- else if (first.status >= SyncOperationStatus.Applied) merged.transported();
5001
- else merged.started();
5307
+ if (first.status > 0) {
5308
+ if (first.status >= 3) merged.executed();
5309
+ else if (first.status >= 2) merged.transported();
5310
+ else merged.started();
5311
+ }
5002
5312
  result.push(merged);
5003
5313
  }
5004
5314
  return result;
@@ -5129,7 +5439,7 @@ function syncOperationErrorType(error) {
5129
5439
  return error?.errorType ?? classifyJobFailure(error?.error?.name ?? "Error");
5130
5440
  }
5131
5441
  /** Dead-letter types that must not stop the document syncing. */
5132
- const NON_QUARANTINING_ERROR_TYPES = new Set(["AUTH_TIMESTAMP_NOT_MONOTONIC"]);
5442
+ const NON_QUARANTINING_ERROR_TYPES = /* @__PURE__ */ new Set(["AUTH_TIMESTAMP_NOT_MONOTONIC"]);
5133
5443
  /**
5134
5444
  * A held auth operation must not quarantine: reconciling the two policies needs
5135
5445
  * the traffic a quarantine would stop.
@@ -5355,7 +5665,7 @@ function envelopesToSyncOperations(envelope, remoteName) {
5355
5665
  }
5356
5666
  const getLatestAppliedOrdinal = (syncOps) => {
5357
5667
  let maxOrdinal = 0;
5358
- for (const syncOp of syncOps) if (syncOp.status === SyncOperationStatus.Applied) for (const op of syncOp.operations) maxOrdinal = Math.max(maxOrdinal, op.context.ordinal);
5668
+ for (const syncOp of syncOps) if (syncOp.status === 2) for (const op of syncOp.operations) maxOrdinal = Math.max(maxOrdinal, op.context.ordinal);
5359
5669
  return maxOrdinal;
5360
5670
  };
5361
5671
  //#endregion
@@ -5370,6 +5680,7 @@ const DECISION_FIELDS = ["deniedReason", "errorType"];
5370
5680
  * GraphQL-based synchronization channel for network communication between reactors.
5371
5681
  */
5372
5682
  var GqlRequestChannel = class {
5683
+ logger;
5373
5684
  inbox;
5374
5685
  outbox;
5375
5686
  deadLetter;
@@ -5577,7 +5888,7 @@ var GqlRequestChannel = class {
5577
5888
  const syncOps = [];
5578
5889
  for (const dl of deadLetters) {
5579
5890
  const syncOp = new SyncOperation(crypto.randomUUID(), dl.jobId, [], this.remoteName, dl.documentId, dl.scopes, dl.branch, []);
5580
- syncOp.failed(new ChannelError(ChannelErrorSource.Outbox, new Error(dl.error), dl.errorType ?? void 0));
5891
+ syncOp.failed(new ChannelError("outbox", new Error(dl.error), dl.errorType ?? void 0));
5581
5892
  syncOps.push(syncOp);
5582
5893
  }
5583
5894
  this.deadLetter.add(...syncOps);
@@ -5597,7 +5908,7 @@ var GqlRequestChannel = class {
5597
5908
  const classification = this.classifyError(err);
5598
5909
  this.failureCount++;
5599
5910
  this.lastFailureUtcMs = Date.now();
5600
- const channelError = new ChannelError(ChannelErrorSource.Inbox, err);
5911
+ const channelError = new ChannelError("inbox", err);
5601
5912
  this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
5602
5913
  if (classification === "unrecoverable") {
5603
5914
  this.pollTimer.stop();
@@ -5841,7 +6152,7 @@ var GqlRequestChannel = class {
5841
6152
  this.transitionConnectionState("reconnecting");
5842
6153
  this.schedulePushRetry();
5843
6154
  } else {
5844
- const channelError = new ChannelError(ChannelErrorSource.Outbox, err);
6155
+ const channelError = new ChannelError("outbox", err);
5845
6156
  for (const syncOp of syncOps) syncOp.failed(channelError);
5846
6157
  this.deadLetter.add(...syncOps);
5847
6158
  this.outbox.remove(...syncOps);
@@ -6063,7 +6374,7 @@ var GqlRequestChannelFactory = class {
6063
6374
  ...retryMaxDelayMs !== void 0 && { retryMaxDelayMs },
6064
6375
  ...maxQueueDepth !== void 0 && { maxQueueDepth },
6065
6376
  ...backpressureCheckIntervalMs !== void 0 && { backpressureCheckIntervalMs },
6066
- startPaused: options?.pollBehavior === PollBehavior.Manual
6377
+ startPaused: options?.pollBehavior === "manual"
6067
6378
  });
6068
6379
  return new GqlRequestChannel(this.logger, remoteId, remoteName, cursorStorage, gqlConfig, operationIndex, pollTimer);
6069
6380
  }
@@ -6080,6 +6391,7 @@ function firstOrdinalOf$1(syncOp) {
6080
6391
  * In general, the resolvers are responsible for updating mailboxes.
6081
6392
  */
6082
6393
  var GqlResponseChannel = class {
6394
+ logger;
6083
6395
  inbox;
6084
6396
  outbox;
6085
6397
  deadLetter;
@@ -6190,7 +6502,7 @@ var GqlResponseChannel = class {
6190
6502
  */
6191
6503
  rememberUnserved(syncOps) {
6192
6504
  for (const syncOp of syncOps) {
6193
- if (syncOp.status === SyncOperationStatus.Applied) continue;
6505
+ if (syncOp.status === 2) continue;
6194
6506
  const first = firstOrdinalOf$1(syncOp);
6195
6507
  if (first > 0 && first < this.evictedOutboxFloor) this.evictedOutboxFloor = first;
6196
6508
  }
@@ -6235,7 +6547,7 @@ var GqlResponseChannel = class {
6235
6547
  unservedFloor() {
6236
6548
  let floor = this.evictedOutboxFloor;
6237
6549
  for (const syncOp of this.outbox.items) {
6238
- if (syncOp.status === SyncOperationStatus.Applied) continue;
6550
+ if (syncOp.status === 2) continue;
6239
6551
  const first = firstOrdinalOf$1(syncOp);
6240
6552
  if (first > 0 && first < floor) floor = first;
6241
6553
  }
@@ -6275,6 +6587,7 @@ function remoteCursorToRow(cursor) {
6275
6587
  };
6276
6588
  }
6277
6589
  var KyselySyncCursorStorage = class {
6590
+ db;
6278
6591
  constructor(db) {
6279
6592
  this.db = db;
6280
6593
  }
@@ -6350,6 +6663,7 @@ function deadLetterRecordToRow(record) {
6350
6663
  * PGlite/Kysely-backed implementation of {@link ISyncDeadLetterStorage}.
6351
6664
  */
6352
6665
  var KyselySyncDeadLetterStorage = class {
6666
+ db;
6353
6667
  constructor(db) {
6354
6668
  this.db = db;
6355
6669
  }
@@ -6471,6 +6785,7 @@ function remoteRecordToRow(remote) {
6471
6785
  };
6472
6786
  }
6473
6787
  var KyselySyncRemoteStorage = class {
6788
+ db;
6474
6789
  constructor(db) {
6475
6790
  this.db = db;
6476
6791
  }
@@ -6621,6 +6936,7 @@ var BatchAggregator = class {
6621
6936
  * to provide a fast path for jobs that have already synced.
6622
6937
  */
6623
6938
  var SyncAwaiter = class {
6939
+ eventBus;
6624
6940
  completedResults = /* @__PURE__ */ new Map();
6625
6941
  pendingWaiters = /* @__PURE__ */ new Map();
6626
6942
  unsubscribers = [];
@@ -6820,24 +7136,19 @@ var SyncStatusTracker = class {
6820
7136
  }
6821
7137
  };
6822
7138
  function deriveStatus(inboxCount, outboxCount, errorCount) {
6823
- if (errorCount > 0) return SyncStatus.Error;
6824
- if (inboxCount > 0 && outboxCount > 0) return SyncStatus.OutgoingAndIncoming;
6825
- if (inboxCount > 0) return SyncStatus.Incoming;
6826
- if (outboxCount > 0) return SyncStatus.Outgoing;
6827
- return SyncStatus.Synced;
7139
+ if (errorCount > 0) return "ERROR";
7140
+ if (inboxCount > 0 && outboxCount > 0) return "OUTGOING_AND_INCOMING";
7141
+ if (inboxCount > 0) return "INCOMING";
7142
+ if (outboxCount > 0) return "OUTGOING";
7143
+ return "SYNCED";
6828
7144
  }
6829
7145
  //#endregion
6830
7146
  //#region src/sync/sync-manager.ts
6831
- var OutboxMode = /* @__PURE__ */ function(OutboxMode) {
6832
- OutboxMode["Backfill"] = "backfill";
6833
- OutboxMode["BatchTriggered"] = "batch-triggered";
6834
- return OutboxMode;
6835
- }(OutboxMode || {});
6836
7147
  const defaultSyncManagerConfig = {
6837
7148
  maxDeadLettersPerRemote: 100,
6838
7149
  maxInboxBatchSize: 32,
6839
7150
  maxHeldOperationsPerRemote: 1e4,
6840
- staleRemotePollWindowMs: 5 * 6e4
7151
+ staleRemotePollWindowMs: 3e5
6841
7152
  };
6842
7153
  const PLAN_KEY_TO_JOB_UUID_CAP = 1e4;
6843
7154
  /**
@@ -6940,7 +7251,7 @@ var SyncManager = class {
6940
7251
  if (outboxAckOrdinal > 0) {
6941
7252
  const backfillController = new AbortController();
6942
7253
  this.backfillAbortControllers.set(record.name, backfillController);
6943
- this.updateOutbox(remote, outboxAckOrdinal, OutboxMode.Backfill, backfillController.signal).catch((error) => {
7254
+ this.updateOutbox(remote, outboxAckOrdinal, "backfill", backfillController.signal).catch((error) => {
6944
7255
  if (backfillController.signal.aborted) return;
6945
7256
  this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
6946
7257
  }).finally(() => {
@@ -7059,7 +7370,7 @@ var SyncManager = class {
7059
7370
  }
7060
7371
  const backfillController = new AbortController();
7061
7372
  this.backfillAbortControllers.set(name, backfillController);
7062
- this.updateOutbox(remote, 0, OutboxMode.Backfill, backfillController.signal).catch((error) => {
7373
+ this.updateOutbox(remote, 0, "backfill", backfillController.signal).catch((error) => {
7063
7374
  if (backfillController.signal.aborted) return;
7064
7375
  this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
7065
7376
  }).finally(() => {
@@ -7179,7 +7490,7 @@ var SyncManager = class {
7179
7490
  scopes: syncOp.scopes,
7180
7491
  branch: syncOp.branch,
7181
7492
  operations: syncOp.operations,
7182
- errorSource: syncOp.error?.source ?? ChannelErrorSource.None,
7493
+ errorSource: syncOp.error?.source ?? "none",
7183
7494
  errorMessage: syncOp.error?.error.message ?? "unknown",
7184
7495
  errorType
7185
7496
  };
@@ -7239,7 +7550,7 @@ var SyncManager = class {
7239
7550
  for (const remote of affectedRemotes) trimMailboxFromBatch(remote.channel.inbox, batch);
7240
7551
  for (const remote of affectedRemotes) {
7241
7552
  if (!this.remotes.has(remote.meta.name) || this.removing.has(remote.meta.name)) continue;
7242
- await this.updateOutbox(remote, remote.channel.outbox.latestOrdinal, OutboxMode.BatchTriggered);
7553
+ await this.updateOutbox(remote, remote.channel.outbox.latestOrdinal, "batch-triggered");
7243
7554
  }
7244
7555
  await this.drainPrunes();
7245
7556
  }
@@ -7281,7 +7592,7 @@ var SyncManager = class {
7281
7592
  if (this.isShutdown) return;
7282
7593
  const err = error instanceof Error ? error : new Error(String(error));
7283
7594
  this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.meta.name, syncOp.documentId, err.message);
7284
- const channelError = new ChannelError(ChannelErrorSource.Inbox, err);
7595
+ const channelError = new ChannelError("inbox", err);
7285
7596
  syncOp.failed(channelError);
7286
7597
  remote.channel.deadLetter.add(syncOp);
7287
7598
  remote.channel.inbox.remove(syncOp);
@@ -7294,14 +7605,14 @@ var SyncManager = class {
7294
7605
  if (this.isShutdown) return;
7295
7606
  const err = error instanceof Error ? error : new Error(String(error));
7296
7607
  this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, jobInfo.id, err.message);
7297
- const channelError = new ChannelError(ChannelErrorSource.Inbox, err);
7608
+ const channelError = new ChannelError("inbox", err);
7298
7609
  syncOp.failed(channelError);
7299
7610
  remote.channel.deadLetter.add(syncOp);
7300
7611
  remote.channel.inbox.remove(syncOp);
7301
7612
  return;
7302
7613
  }
7303
7614
  if (this.isShutdown) return;
7304
- if (completedJobInfo.status === JobStatus.FAILED) {
7615
+ if (completedJobInfo.status === "FAILED") {
7305
7616
  const errorMessage = completedJobInfo.error?.message || "Unknown error";
7306
7617
  this.logger.error("Failed to apply operations from inbox (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, completedJobInfo.id, errorMessage);
7307
7618
  syncOp.failed(this.inboxFailure(completedJobInfo.error));
@@ -7344,7 +7655,7 @@ var SyncManager = class {
7344
7655
  if (this.isShutdown) return;
7345
7656
  for (const { remote, syncOp } of items) {
7346
7657
  const err = error instanceof Error ? error : new Error(String(error));
7347
- syncOp.failed(new ChannelError(ChannelErrorSource.Inbox, err));
7658
+ syncOp.failed(new ChannelError("inbox", err));
7348
7659
  remote.channel.deadLetter.add(syncOp);
7349
7660
  remote.channel.inbox.remove(syncOp);
7350
7661
  }
@@ -7361,7 +7672,7 @@ var SyncManager = class {
7361
7672
  for (const { remote, syncOp } of items) {
7362
7673
  if (!(syncOp.jobId in result.jobs)) {
7363
7674
  this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.meta.name, syncOp.documentId, syncOp.jobId);
7364
- const error = new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
7675
+ const error = new ChannelError("inbox", /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
7365
7676
  syncOp.failed(error);
7366
7677
  remote.channel.deadLetter.add(syncOp);
7367
7678
  remote.channel.inbox.remove(syncOp);
@@ -7374,13 +7685,13 @@ var SyncManager = class {
7374
7685
  } catch (error) {
7375
7686
  if (this.isShutdown) continue;
7376
7687
  const err = error instanceof Error ? error : new Error(String(error));
7377
- syncOp.failed(new ChannelError(ChannelErrorSource.Inbox, err));
7688
+ syncOp.failed(new ChannelError("inbox", err));
7378
7689
  remote.channel.deadLetter.add(syncOp);
7379
7690
  remote.channel.inbox.remove(syncOp);
7380
7691
  continue;
7381
7692
  }
7382
7693
  if (this.isShutdown) return;
7383
- if (completedJobInfo.status === JobStatus.FAILED) {
7694
+ if (completedJobInfo.status === "FAILED") {
7384
7695
  syncOp.failed(this.inboxFailure(completedJobInfo.error));
7385
7696
  remote.channel.deadLetter.add(syncOp);
7386
7697
  } else syncOp.executed();
@@ -7399,7 +7710,7 @@ var SyncManager = class {
7399
7710
  */
7400
7711
  inboxFailure(error) {
7401
7712
  const message = error?.message || "Unknown error";
7402
- return new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Failed to apply operations: ${message}`), classifyJobFailure(error?.name ?? "Error"));
7713
+ return new ChannelError("inbox", /* @__PURE__ */ new Error(`Failed to apply operations: ${message}`), classifyJobFailure(error?.name ?? "Error"));
7403
7714
  }
7404
7715
  /**
7405
7716
  * Where the next outbox derivation starts: at the evicted floor when there is
@@ -7426,7 +7737,7 @@ var SyncManager = class {
7426
7737
  */
7427
7738
  evictPastOutboxBound(remote) {
7428
7739
  const cap = this.config.maxHeldOperationsPerRemote;
7429
- const queued = remote.channel.outbox.items.filter((syncOp) => syncOp.status === SyncOperationStatus.Unknown).sort((a, b) => firstOrdinalOf(a) - firstOrdinalOf(b));
7740
+ const queued = remote.channel.outbox.items.filter((syncOp) => syncOp.status === -1).sort((a, b) => firstOrdinalOf(a) - firstOrdinalOf(b));
7430
7741
  let kept = 0;
7431
7742
  let cut = queued.length;
7432
7743
  for (let i = 0; i < queued.length; i++) {
@@ -7518,7 +7829,7 @@ var SyncManager = class {
7518
7829
  * it marked; the count is what keeps that drain from removing the remote this
7519
7830
  * call is still adding to.
7520
7831
  */
7521
- async updateOutbox(remote, ackOrdinal, mode = OutboxMode.Backfill, signal) {
7832
+ async updateOutbox(remote, ackOrdinal, mode = "backfill", signal) {
7522
7833
  this.derivingOutboxes++;
7523
7834
  try {
7524
7835
  await this.deriveOutbox(remote, ackOrdinal, mode, signal);
@@ -7546,11 +7857,11 @@ var SyncManager = class {
7546
7857
  const prevJobId = lastJobByDoc.get(batch.documentId);
7547
7858
  const deps = [];
7548
7859
  if (prevJobId) deps.push(prevJobId);
7549
- if (mode === OutboxMode.BatchTriggered && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
7860
+ if (mode === "batch-triggered" && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
7550
7861
  const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.meta.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
7551
7862
  syncOps.push(syncOp);
7552
7863
  lastJobByDoc.set(batch.documentId, jobId);
7553
- if (mode === OutboxMode.BatchTriggered) prevChainJobId = jobId;
7864
+ if (mode === "batch-triggered") prevChainJobId = jobId;
7554
7865
  }
7555
7866
  remote.channel.outbox.add(...syncOps);
7556
7867
  this.evictPastOutboxBound(remote);
@@ -7907,7 +8218,7 @@ var Reactor = class {
7907
8218
  const jobInfo = {
7908
8219
  id: jobId,
7909
8220
  documentId: job.documentId,
7910
- status: JobStatus.PENDING,
8221
+ status: "PENDING",
7911
8222
  createdAtUtcIso,
7912
8223
  consistencyToken: {
7913
8224
  version: 1,
@@ -7949,7 +8260,7 @@ var Reactor = class {
7949
8260
  const jobInfo = {
7950
8261
  id: jobId,
7951
8262
  documentId: job.documentId,
7952
- status: JobStatus.PENDING,
8263
+ status: "PENDING",
7953
8264
  createdAtUtcIso,
7954
8265
  consistencyToken: {
7955
8266
  version: 1,
@@ -7987,7 +8298,7 @@ var Reactor = class {
7987
8298
  const jobInfo = {
7988
8299
  id: jobId,
7989
8300
  documentId: job.documentId,
7990
- status: JobStatus.PENDING,
8301
+ status: "PENDING",
7991
8302
  createdAtUtcIso,
7992
8303
  consistencyToken: {
7993
8304
  version: 1,
@@ -8027,7 +8338,7 @@ var Reactor = class {
8027
8338
  const jobInfo = {
8028
8339
  id: jobId,
8029
8340
  documentId: job.documentId,
8030
- status: JobStatus.PENDING,
8341
+ status: "PENDING",
8031
8342
  createdAtUtcIso,
8032
8343
  consistencyToken: {
8033
8344
  version: 1,
@@ -8062,7 +8373,7 @@ var Reactor = class {
8062
8373
  const jobInfo = {
8063
8374
  id: planKeyToJobId.get(jobPlan.key),
8064
8375
  documentId: jobPlan.documentId,
8065
- status: JobStatus.PENDING,
8376
+ status: "PENDING",
8066
8377
  createdAtUtcIso,
8067
8378
  consistencyToken: {
8068
8379
  version: 1,
@@ -8132,7 +8443,7 @@ var Reactor = class {
8132
8443
  const jobInfo = {
8133
8444
  id: planKeyToJobId.get(jobPlan.key),
8134
8445
  documentId: jobPlan.documentId,
8135
- status: JobStatus.PENDING,
8446
+ status: "PENDING",
8136
8447
  createdAtUtcIso,
8137
8448
  consistencyToken: {
8138
8449
  version: 1,
@@ -8221,7 +8532,7 @@ var Reactor = class {
8221
8532
  return Promise.resolve({
8222
8533
  id: jobId,
8223
8534
  documentId: "",
8224
- status: JobStatus.FAILED,
8535
+ status: "FAILED",
8225
8536
  createdAtUtcIso: now,
8226
8537
  completedAtUtcIso: now,
8227
8538
  error: toErrorInfo$1("Job not found"),
@@ -8572,11 +8883,12 @@ var ReactorBuilder = class {
8572
8883
  if (resolver instanceof DocumentModelResolver) resolver.setModelLoadedHook((documentType) => eventBus.emit(ReactorEventTypes.MODEL_LOADED, { documentType }));
8573
8884
  const queue = this.queueInstance ?? new InMemoryQueue(eventBus, resolver);
8574
8885
  const jobTracker = new InMemoryJobTracker(eventBus);
8575
- const writeCache = new KyselyWriteCache(keyframeStore, operationStore, documentModelRegistry, {
8886
+ const cacheConfig = {
8576
8887
  maxDocuments: this.writeCacheConfig?.maxDocuments ?? 100,
8577
8888
  ringBufferSize: this.writeCacheConfig?.ringBufferSize ?? 10,
8578
8889
  keyframeInterval: this.writeCacheConfig?.keyframeInterval ?? 10
8579
- });
8890
+ };
8891
+ const writeCache = new KyselyWriteCache(keyframeStore, operationStore, documentModelRegistry, cacheConfig);
8580
8892
  await writeCache.startup();
8581
8893
  const operationIndex = new KyselyOperationIndex(database);
8582
8894
  const documentMetaCache = new DocumentMetaCache(operationStore, { maxDocuments: 1e3 });
@@ -8585,22 +8897,24 @@ var ReactorBuilder = class {
8585
8897
  const executionScope = new KyselyExecutionScope(database, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache);
8586
8898
  let executorManager = this.executorManager;
8587
8899
  let executorStartCount = this.executorConfig.maxConcurrency ?? 1;
8588
- if (!executorManager) if (this.workerPool) {
8589
- const pool = this.workerPool;
8590
- let factory = pool.factory;
8591
- if (!factory) {
8592
- if (pool.db === void 0) throw new Error("unreachable: worker pool configured without db or factory");
8593
- if (this.signer && !this.workerSigner) this.logger.warn("Worker pool has no signer spec; pooled workers store synthesized operations unsigned");
8594
- if (this.trustPolicy && !this.workerTrustPolicy) this.logger.warn("Worker pool has no trust policy spec; pooled workers apply the default trust policy");
8595
- factory = await this.createDefaultWorkerFactory(pool.numWorkers, pool.db);
8596
- }
8597
- const poolManager = new WorkerPoolJobExecutorManager(factory, eventBus, queue, jobTracker, this.logger, resolver, operationIndex, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
8598
- executorManager = poolManager;
8599
- executorStartCount = pool.numWorkers;
8600
- if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
8601
- } 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);
8900
+ if (!executorManager) {
8901
+ if (this.workerPool) {
8902
+ const pool = this.workerPool;
8903
+ let factory = pool.factory;
8904
+ if (!factory) {
8905
+ if (pool.db === void 0) throw new Error("unreachable: worker pool configured without db or factory");
8906
+ if (this.signer && !this.workerSigner) this.logger.warn("Worker pool has no signer spec; pooled workers store synthesized operations unsigned");
8907
+ if (this.trustPolicy && !this.workerTrustPolicy) this.logger.warn("Worker pool has no trust policy spec; pooled workers apply the default trust policy");
8908
+ factory = await this.createDefaultWorkerFactory(pool.numWorkers, pool.db);
8909
+ }
8910
+ const poolManager = new WorkerPoolJobExecutorManager(factory, eventBus, queue, jobTracker, this.logger, resolver, operationIndex, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
8911
+ executorManager = poolManager;
8912
+ executorStartCount = pool.numWorkers;
8913
+ if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
8914
+ } 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);
8915
+ }
8602
8916
  await executorManager.start(executorStartCount);
8603
- const callerReadModels = Array.from(new Set([...this.readModels]));
8917
+ const callerReadModels = Array.from(/* @__PURE__ */ new Set([...this.readModels]));
8604
8918
  const degradedComponents = [];
8605
8919
  const startDegraded = (component, error) => {
8606
8920
  degradedComponents.push({
@@ -8614,7 +8928,7 @@ var ReactorBuilder = class {
8614
8928
  yieldDeadlineMs: this.executorConfig.yieldDeadlineMs ?? 50
8615
8929
  };
8616
8930
  const documentViewConsistencyTracker = new ConsistencyTracker();
8617
- const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, featureFlags.documentDecisions ? DeletedDocumentRead.StateAtDeletion : DeletedDocumentRead.NotFound, readModelIndexing);
8931
+ const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, featureFlags.documentDecisions ? "StateAtDeletion" : "NotFound", readModelIndexing);
8618
8932
  try {
8619
8933
  await documentView.init();
8620
8934
  } catch (error) {
@@ -8670,7 +8984,7 @@ var ReactorBuilder = class {
8670
8984
  const reactor = new Reactor(this.logger, documentModelRegistry, queue, jobTracker, readModelCoordinator, this.features, documentView, documentIndexer, operationStore, eventBus, executorManager);
8671
8985
  let syncModule = void 0;
8672
8986
  if (this.channelScheme) {
8673
- const factory = this.channelScheme === ChannelScheme.CONNECT ? new GqlRequestChannelFactory(this.logger, this.jwtHandler, queue) : new GqlResponseChannelFactory(this.logger);
8987
+ const factory = this.channelScheme === "connect" ? new GqlRequestChannelFactory(this.logger, this.jwtHandler, queue) : new GqlResponseChannelFactory(this.logger);
8674
8988
  syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
8675
8989
  await syncModule.syncManager.startup();
8676
8990
  } else if (this.syncBuilder) {
@@ -8771,7 +9085,7 @@ var ReactorBuilder = class {
8771
9085
  poolInstrumentations.push(forwarder);
8772
9086
  this.instrumentedPools.push(forwarder);
8773
9087
  }
8774
- const { ProjectionShardManager } = await import("./projection-shard-manager-mQhfVipO.js");
9088
+ const { ProjectionShardManager } = await import("./projection-shard-manager-BpcqDtnQ.js");
8775
9089
  const manager = new ProjectionShardManager({
8776
9090
  shardCount: config.shardCount,
8777
9091
  db,
@@ -8805,7 +9119,7 @@ var ReactorBuilder = class {
8805
9119
  return manager;
8806
9120
  }
8807
9121
  async createDefaultProjectionWorkerFactory() {
8808
- const [{ createProjectionThreadTransport }, { projectionWorkerEntryPath }] = await Promise.all([import("./transport-CuogVKN_.js"), import("./projection-worker-wI4PwcV2.js")]);
9122
+ const [{ createProjectionThreadTransport }, { projectionWorkerEntryPath }] = await Promise.all([import("./transport-B3G50KVd.js"), import("./projection-worker-D8uNFdwX.js")]);
8809
9123
  return () => createProjectionThreadTransport(projectionWorkerEntryPath);
8810
9124
  }
8811
9125
  /**
@@ -8815,9 +9129,9 @@ var ReactorBuilder = class {
8815
9129
  */
8816
9130
  async createDefaultWorkerFactory(numWorkers, db) {
8817
9131
  const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
8818
- import("./worker-handle-B3p-nuqG.js"),
8819
- import("./transport-ByGviWdZ.js"),
8820
- import("./worker-BDVzCtuf.js").then((n) => n.n)
9132
+ import("./worker-handle-C2rLUHwv.js"),
9133
+ import("./transport-CZDHzfvy.js"),
9134
+ Promise.resolve().then(() => worker_exports)
8821
9135
  ]);
8822
9136
  const poolConfig = {
8823
9137
  enabled: true,
@@ -9291,6 +9605,23 @@ const SIGNATURE_REFUSAL_CODES = [
9291
9605
  "SIGNER_UNAUTHORIZED"
9292
9606
  ];
9293
9607
  //#endregion
9608
+ //#region src/executor/worker/index.ts
9609
+ var worker_exports = /* @__PURE__ */ __exportAll({
9610
+ createForwardingLogger: () => createForwardingLogger,
9611
+ errorToInfo: () => errorToInfo,
9612
+ sanitizeArg: () => sanitizeArg,
9613
+ workerEntryPath: () => workerEntryPath
9614
+ });
9615
+ function resolveWorkerEntryPath() {
9616
+ const url = new URL("./entry.js", import.meta.url);
9617
+ if (url.protocol !== "file:") return url.href;
9618
+ let pathname = decodeURIComponent(url.pathname);
9619
+ if (/^\/[A-Za-z]:[/\\]/.test(pathname)) pathname = pathname.slice(1);
9620
+ return pathname;
9621
+ }
9622
+ /** Absolute path to the worker entry script for use with `new Worker(workerEntryPath)`. */
9623
+ const workerEntryPath = resolveWorkerEntryPath();
9624
+ //#endregion
9294
9625
  //#region src/decision/sync-scope-gate.ts
9295
9626
  /** What a subject may read of a document that is not there to be read. */
9296
9627
  const META_ONLY = (scope) => ALWAYS_READABLE_SCOPES.has(scope);
@@ -9302,6 +9633,9 @@ const META_ONLY = (scope) => ALWAYS_READABLE_SCOPES.has(scope);
9302
9633
  * path's own job, and it is the only thing this adds to the gate it wraps.
9303
9634
  */
9304
9635
  var SyncScopeGate = class {
9636
+ gate;
9637
+ documentView;
9638
+ logger;
9305
9639
  constructor(gate, documentView, logger) {
9306
9640
  this.gate = gate;
9307
9641
  this.documentView = documentView;