@powerhousedao/reactor 6.2.3-dev.3 → 6.2.3-dev.30

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 (42) hide show
  1. package/dist/{build-worker-executor-DnU3ZU4H.js → build-worker-executor-Bvoba-AV.js} +24 -8
  2. package/dist/build-worker-executor-Bvoba-AV.js.map +1 -0
  3. package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-BVTGbEB0.js} +190 -40
  4. package/dist/document-indexer-BVTGbEB0.js.map +1 -0
  5. package/dist/entry.d.ts +2 -1
  6. package/dist/entry.js +10 -6
  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-yZrksiJR.js → forwarding-logger-CnuVDVze.js} +1297 -241
  11. package/dist/forwarding-logger-CnuVDVze.js.map +1 -0
  12. package/dist/index.d.ts +2707 -2040
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1708 -648
  15. package/dist/index.js.map +1 -1
  16. package/dist/projection-entry.d.ts +2 -1
  17. package/dist/projection-entry.js +11 -17
  18. package/dist/projection-entry.js.map +1 -1
  19. package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-BpcqDtnQ.js} +4 -4
  20. package/dist/projection-shard-manager-BpcqDtnQ.js.map +1 -0
  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-CrERzl8s.js → worker-handle-C2rLUHwv.js} +22 -16
  28. package/dist/worker-handle-C2rLUHwv.js.map +1 -0
  29. package/package.json +19 -10
  30. package/dist/build-worker-executor-DnU3ZU4H.js.map +0 -1
  31. package/dist/document-indexer-C5Gsa1B3.js.map +0 -1
  32. package/dist/drive-container-types-yZrksiJR.js.map +0 -1
  33. package/dist/error-info-Cpu4OY3o.js +0 -62
  34. package/dist/error-info-Cpu4OY3o.js.map +0 -1
  35. package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
  36. package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
  37. package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
  38. package/dist/types-DMKLa0Ok.js +0 -33
  39. package/dist/types-DMKLa0Ok.js.map +0 -1
  40. package/dist/worker-HPysBfhx.js +0 -22
  41. package/dist/worker-HPysBfhx.js.map +0 -1
  42. package/dist/worker-handle-CrERzl8s.js.map +0 -1
@@ -1,5 +1,4 @@
1
- import { _ as DocumentMetaCache, c as KyselyKeyframeStore, f as KyselyExecutionScope, g as KyselyOperationIndex, h as KyselyWriteCache, l as DocumentModelRegistry, m as EventBus, n as REACTOR_SCHEMA, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as CollectionMembershipCache } from "./drive-container-types-yZrksiJR.js";
2
- import { n as ReactorEventTypes } from "./types-DMKLa0Ok.js";
1
+ import { C as DocumentMetaCache, S as KyselyOperationIndex, a as DEFAULT_DRIVE_CONTAINER_TYPES, b as EventBus, d as KyselyOperationStore, f as KyselyKeyframeStore, m as SimpleJobExecutor, o as REACTOR_SCHEMA, ot as ReactorEventTypes, p as DocumentModelRegistry, v as KyselyExecutionScope, w as CollectionMembershipCache, x as KyselyWriteCache } from "./forwarding-logger-CnuVDVze.js";
3
2
  //#region src/executor/worker/build-worker-executor.ts
4
3
  async function defaultLoadFactory(spec) {
5
4
  const ref = spec.module;
@@ -38,11 +37,18 @@ async function buildWorkerExecutor(options) {
38
37
  const loadFactory = options.loadFactory ?? defaultLoadFactory;
39
38
  const registry = new DocumentModelRegistry();
40
39
  await loadModelManifest(init.models, loadFactory, registry, logger);
41
- let signatureVerifier;
42
- if (init.signatureVerifier) try {
43
- signatureVerifier = await loadFactory(init.signatureVerifier);
40
+ let signer;
41
+ if (init.signer) try {
42
+ signer = await loadFactory(init.signer);
44
43
  } catch (error) {
45
- logger.error("worker failed to load signature verifier: @spec @error", init.signatureVerifier, error);
44
+ logger.error("worker failed to load signer: @spec @error", init.signer.module, error);
45
+ throw error;
46
+ }
47
+ let trustPolicy;
48
+ if (init.trustPolicy) try {
49
+ trustPolicy = await loadFactory(init.trustPolicy);
50
+ } catch (error) {
51
+ logger.error("worker failed to load trust policy: @spec @error", init.trustPolicy.module, error);
46
52
  throw error;
47
53
  }
48
54
  const database = baseDatabase.withSchema(REACTOR_SCHEMA);
@@ -67,17 +73,27 @@ async function buildWorkerExecutor(options) {
67
73
  jobMeta: event.jobMeta
68
74
  };
69
75
  });
76
+ let signatureRefusals = [];
77
+ eventBus.subscribe(ReactorEventTypes.SIGNATURE_REFUSED, (_t, event) => {
78
+ signatureRefusals.push(event);
79
+ });
80
+ const executorConfig = options.executorConfig ?? {};
70
81
  return {
71
- executor: new SimpleJobExecutor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, options.executorConfig ?? {}, signatureVerifier, executionScope),
82
+ executor: new SimpleJobExecutor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, executorConfig, executionScope, signer, trustPolicy),
72
83
  registry,
73
84
  takeLastWriteReady() {
74
85
  const captured = lastWriteReady;
75
86
  lastWriteReady = null;
76
87
  return captured;
88
+ },
89
+ takeSignatureRefusals() {
90
+ const captured = signatureRefusals;
91
+ signatureRefusals = [];
92
+ return captured;
77
93
  }
78
94
  };
79
95
  }
80
96
  //#endregion
81
97
  export { defaultLoadFactory as n, buildWorkerExecutor as t };
82
98
 
83
- //# sourceMappingURL=build-worker-executor-DnU3ZU4H.js.map
99
+ //# sourceMappingURL=build-worker-executor-Bvoba-AV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-worker-executor-Bvoba-AV.js","names":[],"sources":["../src/executor/worker/build-worker-executor.ts"],"sourcesContent":["import type {\n DocumentModelModule,\n ISigner,\n OperationWithContext,\n} from \"@powerhousedao/shared/document-model\";\nimport type { ILogger } from \"document-model\";\nimport type { Kysely } from \"kysely\";\nimport { CollectionMembershipCache } from \"../../cache/collection-membership-cache.js\";\nimport { DocumentMetaCache } from \"../../cache/document-meta-cache.js\";\nimport { KyselyOperationIndex } from \"../../cache/kysely-operation-index.js\";\nimport { KyselyWriteCache } from \"../../cache/kysely-write-cache.js\";\nimport type { WriteCacheConfig } from \"../../cache/write-cache-types.js\";\nimport { DEFAULT_DRIVE_CONTAINER_TYPES } from \"../../core/drive-container-types.js\";\nimport type { Database } from \"../../core/types.js\";\nimport { EventBus } from \"../../events/event-bus.js\";\nimport {\n ReactorEventTypes,\n type JobWriteReadyEvent,\n type SignatureRefusedEvent,\n} from \"../../events/types.js\";\nimport { DocumentModelRegistry } from \"../../registry/implementation.js\";\nimport type { JobMeta } from \"../../shared/types.js\";\nimport { KyselyKeyframeStore } from \"../../storage/kysely/keyframe-store.js\";\nimport { KyselyOperationStore } from \"../../storage/kysely/store.js\";\nimport type { Database as StorageDatabase } from \"../../storage/kysely/types.js\";\nimport { REACTOR_SCHEMA } from \"../../storage/migrations/migrator.js\";\nimport { KyselyExecutionScope } from \"../execution-scope.js\";\nimport { SimpleJobExecutor } from \"../simple-job-executor.js\";\nimport type { SignatureTrustPolicy } from \"../../signer/types.js\";\nimport type { JobExecutorConfig } from \"../types.js\";\nimport type {\n FactorySpec,\n InitMessage,\n ModelManifestEntry,\n} from \"./protocol.js\";\n\n/**\n * In-worker capture of the JOB_WRITE_READY event emitted by the executor.\n * The worker forwards `operations` and `jobMeta` back to the parent; the\n * parent re-enriches `collectionMemberships` at emit time.\n */\nexport type WorkerWriteReadyCapture = {\n operations: OperationWithContext[];\n jobMeta: JobMeta;\n};\n\nexport type WorkerExecutorStack = {\n executor: SimpleJobExecutor;\n registry: DocumentModelRegistry;\n /**\n * Synchronously pops the most-recent JOB_WRITE_READY captured on this\n * worker's local event bus and clears it. Returns null if the executor\n * did not produce one for this job.\n */\n takeLastWriteReady(): WorkerWriteReadyCapture | null;\n /** Drains the refusals this worker's executor emitted since the last call. */\n takeSignatureRefusals(): SignatureRefusedEvent[];\n};\n\nexport type BuildWorkerExecutorOptions = {\n init: InitMessage;\n database: Kysely<Database>;\n logger: ILogger;\n executorConfig?: JobExecutorConfig;\n driveContainerTypes?: ReadonlySet<string>;\n /**\n * Override the module loader used to materialize factory specs. Tests\n * can inject a deterministic resolver instead of touching the real\n * Node module loader.\n */\n loadFactory?: (spec: FactorySpec) => Promise<unknown>;\n};\n\nexport async function defaultLoadFactory(spec: FactorySpec): Promise<unknown> {\n const ref = spec.module;\n const specifier =\n \"filePath\" in ref\n ? new URL(`file://${ref.filePath}`).href\n : ref.packageName;\n const mod = (await import(specifier)) as Record<string, unknown>;\n const exported = mod[ref.exportName];\n if (typeof exported === \"function\") {\n return (exported as (args: unknown) => unknown)(spec.initArgs);\n }\n return exported;\n}\n\nasync function loadModelManifest(\n entries: ModelManifestEntry[],\n loadFactory: (spec: FactorySpec) => Promise<unknown>,\n registry: DocumentModelRegistry,\n logger: ILogger,\n): Promise<void> {\n for (const entry of entries) {\n let module: DocumentModelModule;\n try {\n module = (await loadFactory(entry.spec)) as DocumentModelModule;\n } catch (error) {\n logger.error(\n \"worker failed to load document model: @entry @error\",\n entry,\n error,\n );\n throw error;\n }\n const [result] = registry.registerModules(module);\n if (result.status === \"error\") {\n logger.error(\n \"worker failed to register document model: @entry @error\",\n entry,\n result.error,\n );\n throw result.error;\n }\n }\n}\n\n/**\n * Assembles the in-worker storage stack plus a {@link SimpleJobExecutor}\n * bound to a pre-built Kysely instance. The parent owns the wire protocol\n * and routing; the worker owns everything below `SimpleJobExecutor`.\n *\n * The local event bus exists only to satisfy the executor's contract: its\n * JOB_WRITE_READY emissions are captured here and shipped to the parent\n * via {@link WorkerExecutorStack.takeLastWriteReady}.\n */\nexport async function buildWorkerExecutor(\n options: BuildWorkerExecutorOptions,\n): Promise<WorkerExecutorStack> {\n const { init, database: baseDatabase, logger } = options;\n const driveContainerTypes =\n options.driveContainerTypes ?? DEFAULT_DRIVE_CONTAINER_TYPES;\n const loadFactory = options.loadFactory ?? defaultLoadFactory;\n\n const registry = new DocumentModelRegistry();\n await loadModelManifest(init.models, loadFactory, registry, logger);\n\n let signer: ISigner | undefined;\n if (init.signer) {\n try {\n signer = (await loadFactory(init.signer)) as ISigner;\n } catch (error) {\n logger.error(\n \"worker failed to load signer: @spec @error\",\n init.signer.module,\n error,\n );\n throw error;\n }\n }\n\n let trustPolicy: SignatureTrustPolicy | undefined;\n if (init.trustPolicy) {\n try {\n trustPolicy = (await loadFactory(\n init.trustPolicy,\n )) as SignatureTrustPolicy;\n } catch (error) {\n logger.error(\n \"worker failed to load trust policy: @spec @error\",\n init.trustPolicy.module,\n error,\n );\n throw error;\n }\n }\n\n const database = baseDatabase.withSchema(REACTOR_SCHEMA);\n const operationStore = new KyselyOperationStore(\n database as unknown as Kysely<StorageDatabase>,\n );\n const keyframeStore = new KyselyKeyframeStore(\n database as unknown as Kysely<StorageDatabase>,\n );\n\n const cacheConfig: WriteCacheConfig = {\n maxDocuments: 100,\n ringBufferSize: 10,\n keyframeInterval: 10,\n };\n const writeCache = new KyselyWriteCache(\n keyframeStore,\n operationStore,\n registry,\n cacheConfig,\n );\n await writeCache.startup();\n\n const operationIndex = new KyselyOperationIndex(\n database as unknown as Kysely<StorageDatabase>,\n );\n\n const documentMetaCache = new DocumentMetaCache(operationStore, {\n maxDocuments: 1000,\n });\n await documentMetaCache.startup();\n\n const collectionMembershipCache = new CollectionMembershipCache(\n operationIndex,\n );\n\n const executionScope = new KyselyExecutionScope(\n database as unknown as Kysely<StorageDatabase>,\n operationStore,\n operationIndex,\n keyframeStore,\n writeCache,\n documentMetaCache,\n collectionMembershipCache,\n );\n\n const eventBus = new EventBus();\n let lastWriteReady: WorkerWriteReadyCapture | null = null;\n eventBus.subscribe(\n ReactorEventTypes.JOB_WRITE_READY,\n (_t: number, event: JobWriteReadyEvent) => {\n lastWriteReady = {\n operations: event.operations,\n jobMeta: event.jobMeta,\n };\n },\n );\n\n let signatureRefusals: SignatureRefusedEvent[] = [];\n eventBus.subscribe(\n ReactorEventTypes.SIGNATURE_REFUSED,\n (_t: number, event: SignatureRefusedEvent) => {\n signatureRefusals.push(event);\n },\n );\n\n const executorConfig = options.executorConfig ?? {};\n const executor = new SimpleJobExecutor(\n logger,\n registry,\n operationStore,\n eventBus,\n writeCache,\n operationIndex,\n documentMetaCache,\n collectionMembershipCache,\n driveContainerTypes,\n executorConfig,\n executionScope,\n signer,\n trustPolicy,\n );\n\n return {\n executor,\n registry,\n takeLastWriteReady(): WorkerWriteReadyCapture | null {\n const captured = lastWriteReady;\n lastWriteReady = null;\n return captured;\n },\n takeSignatureRefusals(): SignatureRefusedEvent[] {\n const captured = signatureRefusals;\n signatureRefusals = [];\n return captured;\n },\n };\n}\n"],"mappings":";;AAyEA,eAAsB,mBAAmB,MAAqC;CAC5E,MAAM,MAAM,KAAK;CAMjB,MAAM,YAAW,OAJf,cAAc,MAAA,OACV,IAAI,IAAI,UAAU,IAAI,UAAU,CAAC,CAAC,QAAA,OAClC,IAAI,cAAA,CAEW,IAAI;CACzB,IAAI,OAAO,aAAa,YACtB,OAAQ,SAAwC,KAAK,QAAQ;CAE/D,OAAO;AACT;AAEA,eAAe,kBACb,SACA,aACA,UACA,QACe;CACf,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI;EACJ,IAAI;GACF,SAAU,MAAM,YAAY,MAAM,IAAI;EACxC,SAAS,OAAO;GACd,OAAO,MACL,uDACA,OACA,KACF;GACA,MAAM;EACR;EACA,MAAM,CAAC,UAAU,SAAS,gBAAgB,MAAM;EAChD,IAAI,OAAO,WAAW,SAAS;GAC7B,OAAO,MACL,2DACA,OACA,OAAO,KACT;GACA,MAAM,OAAO;EACf;CACF;AACF;;;;;;;;;;AAWA,eAAsB,oBACpB,SAC8B;CAC9B,MAAM,EAAE,MAAM,UAAU,cAAc,WAAW;CACjD,MAAM,sBACJ,QAAQ,uBAAuB;CACjC,MAAM,cAAc,QAAQ,eAAe;CAE3C,MAAM,WAAW,IAAI,sBAAsB;CAC3C,MAAM,kBAAkB,KAAK,QAAQ,aAAa,UAAU,MAAM;CAElE,IAAI;CACJ,IAAI,KAAK,QACP,IAAI;EACF,SAAU,MAAM,YAAY,KAAK,MAAM;CACzC,SAAS,OAAO;EACd,OAAO,MACL,8CACA,KAAK,OAAO,QACZ,KACF;EACA,MAAM;CACR;CAGF,IAAI;CACJ,IAAI,KAAK,aACP,IAAI;EACF,cAAe,MAAM,YACnB,KAAK,WACP;CACF,SAAS,OAAO;EACd,OAAO,MACL,oDACA,KAAK,YAAY,QACjB,KACF;EACA,MAAM;CACR;CAGF,MAAM,WAAW,aAAa,WAAW,cAAc;CACvD,MAAM,iBAAiB,IAAI,qBACzB,QACF;CACA,MAAM,gBAAgB,IAAI,oBACxB,QACF;CAOA,MAAM,aAAa,IAAI,iBACrB,eACA,gBACA,UACA;EARA,cAAc;EACd,gBAAgB;EAChB,kBAAkB;CAMR,CACZ;CACA,MAAM,WAAW,QAAQ;CAEzB,MAAM,iBAAiB,IAAI,qBACzB,QACF;CAEA,MAAM,oBAAoB,IAAI,kBAAkB,gBAAgB,EAC9D,cAAc,IAChB,CAAC;CACD,MAAM,kBAAkB,QAAQ;CAEhC,MAAM,4BAA4B,IAAI,0BACpC,cACF;CAEA,MAAM,iBAAiB,IAAI,qBACzB,UACA,gBACA,gBACA,eACA,YACA,mBACA,yBACF;CAEA,MAAM,WAAW,IAAI,SAAS;CAC9B,IAAI,iBAAiD;CACrD,SAAS,UACP,kBAAkB,kBACjB,IAAY,UAA8B;EACzC,iBAAiB;GACf,YAAY,MAAM;GAClB,SAAS,MAAM;EACjB;CACF,CACF;CAEA,IAAI,oBAA6C,CAAC;CAClD,SAAS,UACP,kBAAkB,oBACjB,IAAY,UAAiC;EAC5C,kBAAkB,KAAK,KAAK;CAC9B,CACF;CAEA,MAAM,iBAAiB,QAAQ,kBAAkB,CAAC;CAiBlD,OAAO;EACL,UAAA,IAjBmB,kBACnB,QACA,UACA,gBACA,UACA,YACA,gBACA,mBACA,2BACA,qBACA,gBACA,gBACA,QACA,WAIO;EACP;EACA,qBAAqD;GACnD,MAAM,WAAW;GACjB,iBAAiB;GACjB,OAAO;EACT;EACA,wBAAiD;GAC/C,MAAM,WAAW;GACjB,oBAAoB,CAAC;GACrB,OAAO;EACT;CACF;AACF"}
@@ -1,17 +1,55 @@
1
- import { L as DocumentExistence } from "./drive-container-types-yZrksiJR.js";
2
- import { n as ReactorEventTypes } from "./types-DMKLa0Ok.js";
1
+ import { ot as ReactorEventTypes, ut as yieldToMain } from "./forwarding-logger-CnuVDVze.js";
3
2
  import { createAuthState, isDenied } from "@powerhousedao/shared/document-model";
4
3
  import { v4 } from "uuid";
5
4
  import { childLogger } from "document-model";
6
5
  //#region src/read-models/base-read-model.ts
6
+ /** Small enough that a chunk's transaction rarely outlasts the yield deadline. */
7
+ const DEFAULT_COMMIT_CHUNK_SIZE = 50;
8
+ /** Matches the executor's own default, so both paths yield on the same cadence. */
9
+ const DEFAULT_READ_MODEL_YIELD_DEADLINE_MS = 50;
10
+ const defaultReadModelIndexingConfig = {
11
+ commitChunkSize: 50,
12
+ yieldDeadlineMs: 50
13
+ };
14
+ /** For read models whose callers can observe where a batch was split. */
15
+ const unchunkedReadModelIndexingConfig = {
16
+ commitChunkSize: Number.MAX_SAFE_INTEGER,
17
+ yieldDeadlineMs: 50
18
+ };
19
+ /**
20
+ * Keeps the chunk size at one operation or more: a chunk of zero or less never
21
+ * advances the indexing loop, so the pass would spin without ever resolving.
22
+ */
23
+ function normalizeIndexingConfig(config) {
24
+ if (Number.isNaN(config.commitChunkSize)) return {
25
+ ...config,
26
+ commitChunkSize: 50
27
+ };
28
+ return {
29
+ ...config,
30
+ commitChunkSize: Math.max(1, Math.min(Math.floor(config.commitChunkSize), Number.MAX_SAFE_INTEGER))
31
+ };
32
+ }
7
33
  /**
8
34
  * Base class for read models that provides catch-up/rewind functionality.
9
35
  * Handles initialization, state tracking via ViewState table, and consistency tracking.
10
36
  * Subclasses override commitOperations() with their specific domain logic.
11
37
  */
12
38
  var BaseReadModel = class {
39
+ db;
40
+ operationIndex;
41
+ writeCache;
42
+ consistencyTracker;
43
+ config;
13
44
  lastOrdinal = 0;
14
45
  name;
46
+ indexing;
47
+ /**
48
+ * Lowest ordinal this model failed to commit and has not committed since, or
49
+ * zero when there is none. The stored cursor is held below it so replay from
50
+ * the cursor still reaches every operation the failed pass left out.
51
+ */
52
+ uncommittedOrdinal = 0;
15
53
  constructor(db, operationIndex, writeCache, consistencyTracker, config) {
16
54
  this.db = db;
17
55
  this.operationIndex = operationIndex;
@@ -19,6 +57,7 @@ var BaseReadModel = class {
19
57
  this.consistencyTracker = consistencyTracker;
20
58
  this.config = config;
21
59
  this.name = config.readModelId;
60
+ this.indexing = normalizeIndexingConfig(config.indexing ?? defaultReadModelIndexingConfig);
22
61
  }
23
62
  /**
24
63
  * Initializes the read model by loading state and catching up on missed operations.
@@ -36,15 +75,33 @@ var BaseReadModel = class {
36
75
  }
37
76
  }
38
77
  /**
39
- * Template method: runs domain-specific commitOperations, then persists
40
- * state and updates consistency tracking.
78
+ * Commits the batch in chunks, yielding between them with no transaction
79
+ * open. A chunk that throws leaves the earlier chunks committed, so the pass
80
+ * saves the cursor for that prefix and parks it below the operation it could
81
+ * not commit before rethrowing.
41
82
  */
42
83
  async indexOperations(items) {
43
84
  if (items.length === 0) return;
44
- await this.commitOperations(items);
45
- await this.db.transaction().execute(async (trx) => {
46
- await this.saveState(trx, items);
47
- });
85
+ const { commitChunkSize, yieldDeadlineMs } = this.indexing;
86
+ let lastYield = performance.now();
87
+ let committed = 0;
88
+ for (let start = 0; start < items.length; start += commitChunkSize) {
89
+ if (start > 0 && performance.now() - lastYield > yieldDeadlineMs) {
90
+ await yieldToMain();
91
+ lastYield = performance.now();
92
+ }
93
+ const chunk = items.slice(start, start + commitChunkSize);
94
+ try {
95
+ await this.commitOperations(chunk);
96
+ } catch (error) {
97
+ this.park(items, committed);
98
+ await this.recordCommittedPrefix(items.slice(0, committed));
99
+ throw error;
100
+ }
101
+ committed += chunk.length;
102
+ }
103
+ this.liftParkIfCommitted(items);
104
+ await this.persistCursor(items);
48
105
  this.updateConsistencyTracker(items);
49
106
  }
50
107
  /**
@@ -96,9 +153,10 @@ var BaseReadModel = class {
96
153
  async saveState(trx, items) {
97
154
  let maxOrdinal = 0;
98
155
  for (const item of items) maxOrdinal = Math.max(maxOrdinal, item.context.ordinal);
99
- this.lastOrdinal = maxOrdinal;
156
+ const next = Math.max(this.lastOrdinal, maxOrdinal);
157
+ this.lastOrdinal = next;
100
158
  await trx.updateTable("ViewState").set({
101
- lastOrdinal: maxOrdinal,
159
+ lastOrdinal: next,
102
160
  lastOperationTimestamp: /* @__PURE__ */ new Date()
103
161
  }).where("readModelId", "=", this.config.readModelId).execute();
104
162
  }
@@ -118,6 +176,59 @@ var BaseReadModel = class {
118
176
  }
119
177
  this.consistencyTracker.update(coordinates);
120
178
  }
179
+ /**
180
+ * Saves the cursor for the chunks that did commit before a later chunk threw.
181
+ * A failure here is swallowed: the cursor simply stays where the pass found
182
+ * it, which is equally safe, and the commit error is the one worth raising.
183
+ */
184
+ async recordCommittedPrefix(prefix) {
185
+ if (prefix.length === 0) return;
186
+ try {
187
+ await this.persistCursor(prefix);
188
+ } catch {
189
+ return;
190
+ }
191
+ this.updateConsistencyTracker(prefix);
192
+ }
193
+ /** Writes the cursor for the given items, never past a parked ordinal. */
194
+ async persistCursor(items) {
195
+ await this.db.transaction().execute(async (trx) => {
196
+ await this.saveState(trx, items);
197
+ await this.clampCursorToPark(trx);
198
+ });
199
+ }
200
+ /**
201
+ * Holds the cursor written by {@link saveState}, which subclasses may
202
+ * override, below the lowest operation this model failed to commit.
203
+ */
204
+ async clampCursorToPark(trx) {
205
+ if (this.uncommittedOrdinal === 0) return;
206
+ const ceiling = this.uncommittedOrdinal - 1;
207
+ if (this.lastOrdinal <= ceiling) return;
208
+ this.lastOrdinal = ceiling;
209
+ await trx.updateTable("ViewState").set({
210
+ lastOrdinal: ceiling,
211
+ lastOperationTimestamp: /* @__PURE__ */ new Date()
212
+ }).where("readModelId", "=", this.config.readModelId).execute();
213
+ }
214
+ /** Remembers the lowest ordinal the failed pass left uncommitted. */
215
+ park(items, committed) {
216
+ let lowest = 0;
217
+ for (let i = committed; i < items.length; i++) {
218
+ const ordinal = items[i].context.ordinal;
219
+ if (lowest === 0 || ordinal < lowest) lowest = ordinal;
220
+ }
221
+ if (lowest === 0) return;
222
+ if (this.uncommittedOrdinal === 0 || lowest < this.uncommittedOrdinal) this.uncommittedOrdinal = lowest;
223
+ }
224
+ /** The park lifts once a later pass commits the operation that failed. */
225
+ liftParkIfCommitted(items) {
226
+ if (this.uncommittedOrdinal === 0) return;
227
+ for (const item of items) if (item.context.ordinal === this.uncommittedOrdinal) {
228
+ this.uncommittedOrdinal = 0;
229
+ return;
230
+ }
231
+ }
121
232
  };
122
233
  //#endregion
123
234
  //#region src/read-models/coordinator.ts
@@ -129,6 +240,9 @@ var BaseReadModel = class {
129
240
  * implicitly.
130
241
  */
131
242
  var ReadModelCoordinator = class {
243
+ eventBus;
244
+ preReady;
245
+ postReady;
132
246
  unsubscribe;
133
247
  isRunning = false;
134
248
  chains = /* @__PURE__ */ new Map();
@@ -280,15 +394,36 @@ const DOCUMENT_VIEW_READ_MODEL = "document-view";
280
394
  const DOCUMENT_INDEXER_READ_MODEL = "document-indexer";
281
395
  //#endregion
282
396
  //#region src/read-models/document-view.ts
397
+ /**
398
+ * What a single-document read of a deleted document returns. A listing omits a
399
+ * deleted document under either value.
400
+ */
401
+ let DeletedDocumentRead = /* @__PURE__ */ function(DeletedDocumentRead) {
402
+ /**
403
+ * The document reads as missing: `get` throws and `resolveIdOrSlug` does not
404
+ * match its id.
405
+ */
406
+ DeletedDocumentRead["NotFound"] = "NotFound";
407
+ /**
408
+ * The document's state as of the deletion, with `state.document.isDeleted`
409
+ * telling the caller what it holds. Only meaningful with `documentDecisions`,
410
+ * which is what makes deletion positional.
411
+ */
412
+ DeletedDocumentRead["StateAtDeletion"] = "StateAtDeletion";
413
+ return DeletedDocumentRead;
414
+ }({});
283
415
  var KyselyDocumentView = class extends BaseReadModel {
416
+ operationStore;
417
+ deletedDocumentRead;
284
418
  _db;
285
- constructor(db, operationStore, operationIndex, writeCache, consistencyTracker, servesDeletionBoundary) {
419
+ constructor(db, operationStore, operationIndex, writeCache, consistencyTracker, deletedDocumentRead, indexing = defaultReadModelIndexingConfig) {
286
420
  super(db, operationIndex, writeCache, consistencyTracker, {
287
421
  readModelId: DOCUMENT_VIEW_READ_MODEL,
288
- rebuildStateOnInit: true
422
+ rebuildStateOnInit: true,
423
+ indexing
289
424
  });
290
425
  this.operationStore = operationStore;
291
- this.servesDeletionBoundary = servesDeletionBoundary;
426
+ this.deletedDocumentRead = deletedDocumentRead;
292
427
  this._db = db;
293
428
  }
294
429
  /**
@@ -300,12 +435,17 @@ var KyselyDocumentView = class extends BaseReadModel {
300
435
  * without either fall back to header/document/auth, because their sibling
301
436
  * echoes may be stale. All other action types index only header and their
302
437
  * own scope.
438
+ *
439
+ * The header row is the one row every scope's chain writes, so it accepts a
440
+ * write only from an operation whose global ordinal is at least the one the
441
+ * row already carries. Without that, a chunked pass that started earlier
442
+ * reverts a concurrent rename with the stale echo its later chunks carry.
303
443
  */
304
444
  async commitOperations(items) {
305
445
  await this._db.transaction().execute(async (trx) => {
306
446
  for (const item of items) {
307
447
  const { operation, context } = item;
308
- const { documentId, scope, branch, documentType, resultingState } = context;
448
+ const { documentId, scope, branch, documentType, resultingState, ordinal } = context;
309
449
  const { index, hash } = operation;
310
450
  if (!resultingState) throw new Error(`Missing resultingState in context for operation ${operation.id || "unknown"}. IDocumentView requires resultingState from upstream - it does not rebuild documents.`);
311
451
  let fullState;
@@ -324,6 +464,7 @@ var KyselyDocumentView = class extends BaseReadModel {
324
464
  deletedAt: now,
325
465
  lastOperationIndex: index,
326
466
  lastOperationHash: hash,
467
+ lastOperationOrdinal: ordinal,
327
468
  lastUpdatedAt: now
328
469
  }).where("documentId", "=", documentId).where("branch", "=", branch).execute();
329
470
  const deletedDocumentScope = fullState?.document;
@@ -345,7 +486,15 @@ var KyselyDocumentView = class extends BaseReadModel {
345
486
  else scopesToIndex.push([scope, {}]);
346
487
  }
347
488
  for (const [scopeName, scopeState] of scopesToIndex) {
348
- const existingSnapshot = await trx.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst();
489
+ const needsExistingContent = scopeName === "header" && preserveHeaderMeta;
490
+ const existingSnapshot = await trx.selectFrom("DocumentSnapshot").select([
491
+ "slug",
492
+ "name",
493
+ "isDeleted",
494
+ "snapshotVersion",
495
+ "lastOperationOrdinal"
496
+ ]).$if(needsExistingContent, (qb) => qb.select("content")).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst();
497
+ if (scopeName === "header" && existingSnapshot !== void 0 && existingSnapshot.lastOperationOrdinal > ordinal) continue;
349
498
  const newState = typeof scopeState === "object" && scopeState !== null ? scopeState : {};
350
499
  let slug = existingSnapshot?.slug ?? null;
351
500
  let name = existingSnapshot?.name ?? null;
@@ -354,7 +503,7 @@ var KyselyDocumentView = class extends BaseReadModel {
354
503
  const headerName = newState.name;
355
504
  if (typeof headerSlug === "string") slug = headerSlug;
356
505
  if (typeof headerName === "string") name = headerName;
357
- if (preserveHeaderMeta && existingSnapshot) {
506
+ if (needsExistingContent && existingSnapshot) {
358
507
  const existingHeader = existingSnapshot.content;
359
508
  if (existingHeader && "meta" in existingHeader) newState.meta = existingHeader.meta;
360
509
  }
@@ -372,12 +521,13 @@ var KyselyDocumentView = class extends BaseReadModel {
372
521
  if (existingSnapshot) await trx.updateTable("DocumentSnapshot").set({
373
522
  lastOperationIndex: index,
374
523
  lastOperationHash: hash,
524
+ lastOperationOrdinal: ordinal,
375
525
  lastUpdatedAt: /* @__PURE__ */ new Date(),
376
526
  snapshotVersion: existingSnapshot.snapshotVersion + 1,
377
527
  content: newState,
378
528
  slug,
379
529
  name
380
- }).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).execute();
530
+ }).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).$if(scopeName === "header", (qb) => qb.where("lastOperationOrdinal", "<=", ordinal)).execute();
381
531
  else {
382
532
  const snapshot = {
383
533
  id: v4(),
@@ -390,6 +540,7 @@ var KyselyDocumentView = class extends BaseReadModel {
390
540
  documentType,
391
541
  lastOperationIndex: index,
392
542
  lastOperationHash: hash,
543
+ lastOperationOrdinal: ordinal,
393
544
  identifiers: null,
394
545
  metadata: null,
395
546
  deletedAt: null
@@ -404,7 +555,7 @@ var KyselyDocumentView = class extends BaseReadModel {
404
555
  if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
405
556
  if (signal?.aborted) throw new Error("Operation aborted");
406
557
  if (documentIds.length === 0) return [];
407
- const existingIds = existence === DocumentExistence.IncludingDeleted ? await this.idsWithStreams(documentIds, signal) : await this.liveIds(documentIds);
558
+ const existingIds = existence === "IncludingDeleted" ? await this.idsWithStreams(documentIds, signal) : await this.liveIds(documentIds);
408
559
  return documentIds.map((id) => existingIds.has(id));
409
560
  }
410
561
  async get(documentId, view, consistencyToken, signal) {
@@ -412,14 +563,14 @@ var KyselyDocumentView = class extends BaseReadModel {
412
563
  if (signal?.aborted) throw new Error("Operation aborted");
413
564
  const branch = view?.branch || "main";
414
565
  let scopesToQuery;
415
- if (view?.scopes && view.scopes.length > 0) scopesToQuery = [...new Set([
566
+ if (view?.scopes && view.scopes.length > 0) scopesToQuery = [.../* @__PURE__ */ new Set([
416
567
  "header",
417
568
  "document",
418
569
  ...view.scopes
419
570
  ])];
420
571
  else scopesToQuery = [];
421
572
  let query = this._db.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("branch", "=", branch);
422
- if (!this.servesDeletionBoundary) query = query.where("isDeleted", "=", false);
573
+ if (this.deletedDocumentRead === "NotFound") query = query.where("isDeleted", "=", false);
423
574
  if (scopesToQuery.length > 0) query = query.where("scope", "in", scopesToQuery);
424
575
  const snapshots = await query.execute();
425
576
  if (snapshots.length === 0) throw new Error(`Document not found: ${documentId}`);
@@ -449,7 +600,7 @@ var KyselyDocumentView = class extends BaseReadModel {
449
600
  if (signal?.aborted) throw new Error("Operation aborted");
450
601
  const branch = view?.branch || "main";
451
602
  let scopesToQuery;
452
- if (view?.scopes && view.scopes.length > 0) scopesToQuery = [...new Set([
603
+ if (view?.scopes && view.scopes.length > 0) scopesToQuery = [.../* @__PURE__ */ new Set([
453
604
  "header",
454
605
  "document",
455
606
  ...view.scopes
@@ -508,17 +659,11 @@ var KyselyDocumentView = class extends BaseReadModel {
508
659
  const branch = view?.branch || "main";
509
660
  const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
510
661
  const limit = paging?.limit || 100;
511
- const documents = [];
512
- const processedDocumentIds = /* @__PURE__ */ new Set();
513
- const allDocumentIds = [];
514
- const snapshots = await this._db.selectFrom("DocumentSnapshot").selectAll().where("documentType", "=", type).where("branch", "=", branch).where("isDeleted", "=", false).orderBy("lastUpdatedAt", "desc").execute();
662
+ const rows = await this._db.selectFrom("DocumentSnapshot").select("documentId").select((eb) => eb.fn.max("lastUpdatedAt").as("lastUpdatedAt")).where("documentType", "=", type).where("branch", "=", branch).where("isDeleted", "=", false).groupBy("documentId").orderBy("lastUpdatedAt", "desc").orderBy("documentId", "asc").offset(startIndex).limit(limit + 1).execute();
515
663
  if (signal?.aborted) throw new Error("Operation aborted");
516
- for (const snapshot of snapshots) {
517
- if (processedDocumentIds.has(snapshot.documentId)) continue;
518
- processedDocumentIds.add(snapshot.documentId);
519
- allDocumentIds.push(snapshot.documentId);
520
- }
521
- const docsToFetch = allDocumentIds.slice(startIndex, startIndex + limit);
664
+ const hasMore = rows.length > limit;
665
+ const docsToFetch = rows.slice(0, limit).map((row) => row.documentId);
666
+ const documents = [];
522
667
  for (const documentId of docsToFetch) {
523
668
  if (signal?.aborted) throw new Error("Operation aborted");
524
669
  try {
@@ -528,7 +673,6 @@ var KyselyDocumentView = class extends BaseReadModel {
528
673
  continue;
529
674
  }
530
675
  }
531
- const hasMore = allDocumentIds.length > startIndex + limit;
532
676
  const nextCursor = hasMore ? String(startIndex + limit) : void 0;
533
677
  return {
534
678
  results: documents,
@@ -537,7 +681,6 @@ var KyselyDocumentView = class extends BaseReadModel {
537
681
  limit: 100
538
682
  },
539
683
  nextCursor,
540
- totalCount: allDocumentIds.length,
541
684
  next: hasMore ? () => this.findByType(type, view, {
542
685
  cursor: nextCursor,
543
686
  limit
@@ -564,7 +707,7 @@ var KyselyDocumentView = class extends BaseReadModel {
564
707
  if (signal?.aborted) throw new Error("Operation aborted");
565
708
  const branch = view?.branch || "main";
566
709
  let idCheckQuery = this._db.selectFrom("DocumentSnapshot").select("documentId").where("documentId", "=", identifier).where("branch", "=", branch);
567
- if (!this.servesDeletionBoundary) idCheckQuery = idCheckQuery.where("isDeleted", "=", false);
710
+ if (this.deletedDocumentRead === "NotFound") idCheckQuery = idCheckQuery.where("isDeleted", "=", false);
568
711
  const idCheckPromise = idCheckQuery.executeTakeFirst();
569
712
  const slugCheckPromise = this._db.selectFrom("SlugMapping").select("documentId").where("slug", "=", identifier).where("branch", "=", branch).executeTakeFirst();
570
713
  const [idMatch, slugMatch] = await Promise.all([idCheckPromise, slugCheckPromise]);
@@ -713,18 +856,25 @@ async function collectAllPages(firstPage, signal) {
713
856
  }
714
857
  //#endregion
715
858
  //#region src/storage/kysely/document-indexer.ts
859
+ function isRelationshipAction(actionType) {
860
+ return actionType === "ADD_RELATIONSHIP" || actionType === "REMOVE_RELATIONSHIP" || actionType === "UPDATE_RELATIONSHIP";
861
+ }
716
862
  var KyselyDocumentIndexer = class extends BaseReadModel {
717
863
  _db;
718
- constructor(db, operationIndex, writeCache, consistencyTracker) {
864
+ constructor(db, operationIndex, writeCache, consistencyTracker, indexing = defaultReadModelIndexingConfig) {
719
865
  super(db, operationIndex, writeCache, consistencyTracker, {
720
866
  readModelId: DOCUMENT_INDEXER_READ_MODEL,
721
- rebuildStateOnInit: false
867
+ rebuildStateOnInit: false,
868
+ indexing
722
869
  });
723
870
  this._db = db;
724
871
  }
872
+ /** Opens no transaction for a batch carrying no relationship operation. */
725
873
  async commitOperations(items) {
874
+ const relationshipOps = items.filter((item) => isRelationshipAction(item.operation.action.type));
875
+ if (relationshipOps.length === 0) return;
726
876
  await this._db.transaction().execute(async (trx) => {
727
- for (const item of items) {
877
+ for (const item of relationshipOps) {
728
878
  const { operation } = item;
729
879
  const actionType = operation.action.type;
730
880
  if (actionType === "ADD_RELATIONSHIP") await this.handleAddRelationship(trx, operation);
@@ -896,7 +1046,7 @@ var KyselyDocumentIndexer = class extends BaseReadModel {
896
1046
  async findAncestors(documentId, types, consistencyToken, signal) {
897
1047
  if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
898
1048
  if (signal?.aborted) throw new Error("Operation aborted");
899
- const nodes = new Set([documentId]);
1049
+ const nodes = /* @__PURE__ */ new Set([documentId]);
900
1050
  const edges = [];
901
1051
  const queue = [documentId];
902
1052
  const visited = /* @__PURE__ */ new Set();
@@ -953,6 +1103,6 @@ var KyselyDocumentIndexer = class extends BaseReadModel {
953
1103
  }
954
1104
  };
955
1105
  //#endregion
956
- export { DOCUMENT_INDEXER_READ_MODEL as a, BaseReadModel as c, KyselyDocumentView as i, ConsistencyTracker as n, DOCUMENT_VIEW_READ_MODEL as o, makeConsistencyKey as r, ReadModelCoordinator as s, KyselyDocumentIndexer as t };
1106
+ export { KyselyDocumentView as a, ReadModelCoordinator as c, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS as d, defaultReadModelIndexingConfig as f, DeletedDocumentRead as i, BaseReadModel as l, ConsistencyTracker as n, DOCUMENT_INDEXER_READ_MODEL as o, unchunkedReadModelIndexingConfig as p, makeConsistencyKey as r, DOCUMENT_VIEW_READ_MODEL as s, KyselyDocumentIndexer as t, DEFAULT_COMMIT_CHUNK_SIZE as u };
957
1107
 
958
- //# sourceMappingURL=document-indexer-C5Gsa1B3.js.map
1108
+ //# sourceMappingURL=document-indexer-BVTGbEB0.js.map