@powerhousedao/reactor 6.2.3-dev.1 → 6.2.3-dev.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-worker-executor-DnU3ZU4H.js → build-worker-executor-DUjyF4NM.js} +2 -2
- package/dist/{build-worker-executor-DnU3ZU4H.js.map → build-worker-executor-DUjyF4NM.js.map} +1 -1
- package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-C1oY6CaR.js} +169 -21
- package/dist/document-indexer-C1oY6CaR.js.map +1 -0
- package/dist/{drive-container-types-yZrksiJR.js → drive-container-types-CS5IxDiA.js} +174 -67
- package/dist/drive-container-types-CS5IxDiA.js.map +1 -0
- package/dist/entry.js +2 -2
- package/dist/index.d.ts +419 -78
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +355 -43
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +8 -8
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-CUo4bF4O.js} +3 -2
- package/dist/projection-shard-manager-CUo4bF4O.js.map +1 -0
- package/dist/types-DMKLa0Ok.js.map +1 -1
- package/dist/{worker-HPysBfhx.js → worker-g03lNM42.js} +2 -2
- package/dist/{worker-HPysBfhx.js.map → worker-g03lNM42.js.map} +1 -1
- package/package.json +4 -4
- package/dist/document-indexer-C5Gsa1B3.js.map +0 -1
- package/dist/drive-container-types-yZrksiJR.js.map +0 -1
- package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
|
@@ -1,4 +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-
|
|
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-CS5IxDiA.js";
|
|
2
2
|
import { n as ReactorEventTypes } from "./types-DMKLa0Ok.js";
|
|
3
3
|
//#region src/executor/worker/build-worker-executor.ts
|
|
4
4
|
async function defaultLoadFactory(spec) {
|
|
@@ -80,4 +80,4 @@ async function buildWorkerExecutor(options) {
|
|
|
80
80
|
//#endregion
|
|
81
81
|
export { defaultLoadFactory as n, buildWorkerExecutor as t };
|
|
82
82
|
|
|
83
|
-
//# sourceMappingURL=build-worker-executor-
|
|
83
|
+
//# sourceMappingURL=build-worker-executor-DUjyF4NM.js.map
|
package/dist/{build-worker-executor-DnU3ZU4H.js.map → build-worker-executor-DUjyF4NM.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-worker-executor-
|
|
1
|
+
{"version":3,"file":"build-worker-executor-DUjyF4NM.js","names":[],"sources":["../src/executor/worker/build-worker-executor.ts"],"sourcesContent":["import type {\n DocumentModelModule,\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} from \"../../events/types.js\";\nimport { DocumentModelRegistry } from \"../../registry/implementation.js\";\nimport type { JobMeta } from \"../../shared/types.js\";\nimport type { SignatureVerificationHandler } from \"../../signer/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 { 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};\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 signatureVerifier: SignatureVerificationHandler | undefined;\n if (init.signatureVerifier) {\n try {\n signatureVerifier = (await loadFactory(\n init.signatureVerifier,\n )) as SignatureVerificationHandler;\n } catch (error) {\n logger.error(\n \"worker failed to load signature verifier: @spec @error\",\n init.signatureVerifier,\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 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 signatureVerifier,\n executionScope,\n );\n\n return {\n executor,\n registry,\n takeLastWriteReady(): WorkerWriteReadyCapture | null {\n const captured = lastWriteReady;\n lastWriteReady = null;\n return captured;\n },\n };\n}\n"],"mappings":";;;AAqEA,eAAsB,mBAAmB,MAAqC;CAC5E,MAAM,MAAM,KAAK;CAMjB,MAAM,YADO,OAHX,cAAc,MAAA,OACV,IAAI,IAAI,UAAU,IAAI,WAAW,CAAC,QAAA,OAClC,IAAI,eAEW,IAAI;AACzB,KAAI,OAAO,aAAa,WACtB,QAAQ,SAAwC,KAAK,SAAS;AAEhE,QAAO;;AAGT,eAAe,kBACb,SACA,aACA,UACA,QACe;AACf,MAAK,MAAM,SAAS,SAAS;EAC3B,IAAI;AACJ,MAAI;AACF,YAAU,MAAM,YAAY,MAAM,KAAK;WAChC,OAAO;AACd,UAAO,MACL,uDACA,OACA,MACD;AACD,SAAM;;EAER,MAAM,CAAC,UAAU,SAAS,gBAAgB,OAAO;AACjD,MAAI,OAAO,WAAW,SAAS;AAC7B,UAAO,MACL,2DACA,OACA,OAAO,MACR;AACD,SAAM,OAAO;;;;;;;;;;;;;AAcnB,eAAsB,oBACpB,SAC8B;CAC9B,MAAM,EAAE,MAAM,UAAU,cAAc,WAAW;CACjD,MAAM,sBACJ,QAAQ,uBAAuB;CACjC,MAAM,cAAc,QAAQ,eAAe;CAE3C,MAAM,WAAW,IAAI,uBAAuB;AAC5C,OAAM,kBAAkB,KAAK,QAAQ,aAAa,UAAU,OAAO;CAEnE,IAAI;AACJ,KAAI,KAAK,kBACP,KAAI;AACF,sBAAqB,MAAM,YACzB,KAAK,kBACN;UACM,OAAO;AACd,SAAO,MACL,0DACA,KAAK,mBACL,MACD;AACD,QAAM;;CAIV,MAAM,WAAW,aAAa,WAAW,eAAe;CACxD,MAAM,iBAAiB,IAAI,qBACzB,SACD;CACD,MAAM,gBAAgB,IAAI,oBACxB,SACD;CAOD,MAAM,aAAa,IAAI,iBACrB,eACA,gBACA,UARoC;EACpC,cAAc;EACd,gBAAgB;EAChB,kBAAkB;EACnB,CAMA;AACD,OAAM,WAAW,SAAS;CAE1B,MAAM,iBAAiB,IAAI,qBACzB,SACD;CAED,MAAM,oBAAoB,IAAI,kBAAkB,gBAAgB,EAC9D,cAAc,KACf,CAAC;AACF,OAAM,kBAAkB,SAAS;CAEjC,MAAM,4BAA4B,IAAI,0BACpC,eACD;CAED,MAAM,iBAAiB,IAAI,qBACzB,UACA,gBACA,gBACA,eACA,YACA,mBACA,0BACD;CAED,MAAM,WAAW,IAAI,UAAU;CAC/B,IAAI,iBAAiD;AACrD,UAAS,UACP,kBAAkB,kBACjB,IAAY,UAA8B;AACzC,mBAAiB;GACf,YAAY,MAAM;GAClB,SAAS,MAAM;GAChB;GAEJ;AAkBD,QAAO;EACL,UAhBe,IAAI,kBACnB,QACA,UACA,gBACA,UACA,YACA,gBACA,mBACA,2BACA,qBAVqB,QAAQ,kBAAkB,EAAE,EAYjD,mBACA,eACD;EAIC;EACA,qBAAqD;GACnD,MAAM,WAAW;AACjB,oBAAiB;AACjB,UAAO;;EAEV"}
|
|
@@ -1,9 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tt as yieldToMain, z as DocumentExistence } from "./drive-container-types-CS5IxDiA.js";
|
|
2
2
|
import { n as ReactorEventTypes } from "./types-DMKLa0Ok.js";
|
|
3
3
|
import { createAuthState, isDenied } from "@powerhousedao/shared/document-model";
|
|
4
4
|
import { v4 } from "uuid";
|
|
5
5
|
import { childLogger } from "document-model";
|
|
6
6
|
//#region src/read-models/base-read-model.ts
|
|
7
|
+
/** Small enough that a chunk's transaction rarely outlasts the yield deadline. */
|
|
8
|
+
const DEFAULT_COMMIT_CHUNK_SIZE = 50;
|
|
9
|
+
/** Matches the executor's own default, so both paths yield on the same cadence. */
|
|
10
|
+
const DEFAULT_READ_MODEL_YIELD_DEADLINE_MS = 50;
|
|
11
|
+
const defaultReadModelIndexingConfig = {
|
|
12
|
+
commitChunkSize: 50,
|
|
13
|
+
yieldDeadlineMs: 50
|
|
14
|
+
};
|
|
15
|
+
/** For read models whose callers can observe where a batch was split. */
|
|
16
|
+
const unchunkedReadModelIndexingConfig = {
|
|
17
|
+
commitChunkSize: Number.MAX_SAFE_INTEGER,
|
|
18
|
+
yieldDeadlineMs: 50
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Keeps the chunk size at one operation or more: a chunk of zero or less never
|
|
22
|
+
* advances the indexing loop, so the pass would spin without ever resolving.
|
|
23
|
+
*/
|
|
24
|
+
function normalizeIndexingConfig(config) {
|
|
25
|
+
if (Number.isNaN(config.commitChunkSize)) return {
|
|
26
|
+
...config,
|
|
27
|
+
commitChunkSize: 50
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
...config,
|
|
31
|
+
commitChunkSize: Math.max(1, Math.min(Math.floor(config.commitChunkSize), Number.MAX_SAFE_INTEGER))
|
|
32
|
+
};
|
|
33
|
+
}
|
|
7
34
|
/**
|
|
8
35
|
* Base class for read models that provides catch-up/rewind functionality.
|
|
9
36
|
* Handles initialization, state tracking via ViewState table, and consistency tracking.
|
|
@@ -12,6 +39,13 @@ import { childLogger } from "document-model";
|
|
|
12
39
|
var BaseReadModel = class {
|
|
13
40
|
lastOrdinal = 0;
|
|
14
41
|
name;
|
|
42
|
+
indexing;
|
|
43
|
+
/**
|
|
44
|
+
* Lowest ordinal this model failed to commit and has not committed since, or
|
|
45
|
+
* zero when there is none. The stored cursor is held below it so replay from
|
|
46
|
+
* the cursor still reaches every operation the failed pass left out.
|
|
47
|
+
*/
|
|
48
|
+
uncommittedOrdinal = 0;
|
|
15
49
|
constructor(db, operationIndex, writeCache, consistencyTracker, config) {
|
|
16
50
|
this.db = db;
|
|
17
51
|
this.operationIndex = operationIndex;
|
|
@@ -19,6 +53,7 @@ var BaseReadModel = class {
|
|
|
19
53
|
this.consistencyTracker = consistencyTracker;
|
|
20
54
|
this.config = config;
|
|
21
55
|
this.name = config.readModelId;
|
|
56
|
+
this.indexing = normalizeIndexingConfig(config.indexing ?? defaultReadModelIndexingConfig);
|
|
22
57
|
}
|
|
23
58
|
/**
|
|
24
59
|
* Initializes the read model by loading state and catching up on missed operations.
|
|
@@ -36,15 +71,33 @@ var BaseReadModel = class {
|
|
|
36
71
|
}
|
|
37
72
|
}
|
|
38
73
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
74
|
+
* Commits the batch in chunks, yielding between them with no transaction
|
|
75
|
+
* open. A chunk that throws leaves the earlier chunks committed, so the pass
|
|
76
|
+
* saves the cursor for that prefix and parks it below the operation it could
|
|
77
|
+
* not commit before rethrowing.
|
|
41
78
|
*/
|
|
42
79
|
async indexOperations(items) {
|
|
43
80
|
if (items.length === 0) return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
const { commitChunkSize, yieldDeadlineMs } = this.indexing;
|
|
82
|
+
let lastYield = performance.now();
|
|
83
|
+
let committed = 0;
|
|
84
|
+
for (let start = 0; start < items.length; start += commitChunkSize) {
|
|
85
|
+
if (start > 0 && performance.now() - lastYield > yieldDeadlineMs) {
|
|
86
|
+
await yieldToMain();
|
|
87
|
+
lastYield = performance.now();
|
|
88
|
+
}
|
|
89
|
+
const chunk = items.slice(start, start + commitChunkSize);
|
|
90
|
+
try {
|
|
91
|
+
await this.commitOperations(chunk);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.park(items, committed);
|
|
94
|
+
await this.recordCommittedPrefix(items.slice(0, committed));
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
committed += chunk.length;
|
|
98
|
+
}
|
|
99
|
+
this.liftParkIfCommitted(items);
|
|
100
|
+
await this.persistCursor(items);
|
|
48
101
|
this.updateConsistencyTracker(items);
|
|
49
102
|
}
|
|
50
103
|
/**
|
|
@@ -118,6 +171,59 @@ var BaseReadModel = class {
|
|
|
118
171
|
}
|
|
119
172
|
this.consistencyTracker.update(coordinates);
|
|
120
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Saves the cursor for the chunks that did commit before a later chunk threw.
|
|
176
|
+
* A failure here is swallowed: the cursor simply stays where the pass found
|
|
177
|
+
* it, which is equally safe, and the commit error is the one worth raising.
|
|
178
|
+
*/
|
|
179
|
+
async recordCommittedPrefix(prefix) {
|
|
180
|
+
if (prefix.length === 0) return;
|
|
181
|
+
try {
|
|
182
|
+
await this.persistCursor(prefix);
|
|
183
|
+
} catch {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.updateConsistencyTracker(prefix);
|
|
187
|
+
}
|
|
188
|
+
/** Writes the cursor for the given items, never past a parked ordinal. */
|
|
189
|
+
async persistCursor(items) {
|
|
190
|
+
await this.db.transaction().execute(async (trx) => {
|
|
191
|
+
await this.saveState(trx, items);
|
|
192
|
+
await this.clampCursorToPark(trx);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Holds the cursor written by {@link saveState}, which subclasses may
|
|
197
|
+
* override, below the lowest operation this model failed to commit.
|
|
198
|
+
*/
|
|
199
|
+
async clampCursorToPark(trx) {
|
|
200
|
+
if (this.uncommittedOrdinal === 0) return;
|
|
201
|
+
const ceiling = this.uncommittedOrdinal - 1;
|
|
202
|
+
if (this.lastOrdinal <= ceiling) return;
|
|
203
|
+
this.lastOrdinal = ceiling;
|
|
204
|
+
await trx.updateTable("ViewState").set({
|
|
205
|
+
lastOrdinal: ceiling,
|
|
206
|
+
lastOperationTimestamp: /* @__PURE__ */ new Date()
|
|
207
|
+
}).where("readModelId", "=", this.config.readModelId).execute();
|
|
208
|
+
}
|
|
209
|
+
/** Remembers the lowest ordinal the failed pass left uncommitted. */
|
|
210
|
+
park(items, committed) {
|
|
211
|
+
let lowest = 0;
|
|
212
|
+
for (let i = committed; i < items.length; i++) {
|
|
213
|
+
const ordinal = items[i].context.ordinal;
|
|
214
|
+
if (lowest === 0 || ordinal < lowest) lowest = ordinal;
|
|
215
|
+
}
|
|
216
|
+
if (lowest === 0) return;
|
|
217
|
+
if (this.uncommittedOrdinal === 0 || lowest < this.uncommittedOrdinal) this.uncommittedOrdinal = lowest;
|
|
218
|
+
}
|
|
219
|
+
/** The park lifts once a later pass commits the operation that failed. */
|
|
220
|
+
liftParkIfCommitted(items) {
|
|
221
|
+
if (this.uncommittedOrdinal === 0) return;
|
|
222
|
+
for (const item of items) if (item.context.ordinal === this.uncommittedOrdinal) {
|
|
223
|
+
this.uncommittedOrdinal = 0;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
121
227
|
};
|
|
122
228
|
//#endregion
|
|
123
229
|
//#region src/read-models/coordinator.ts
|
|
@@ -280,15 +386,34 @@ const DOCUMENT_VIEW_READ_MODEL = "document-view";
|
|
|
280
386
|
const DOCUMENT_INDEXER_READ_MODEL = "document-indexer";
|
|
281
387
|
//#endregion
|
|
282
388
|
//#region src/read-models/document-view.ts
|
|
389
|
+
/**
|
|
390
|
+
* What a single-document read of a deleted document returns. A listing omits a
|
|
391
|
+
* deleted document under either value.
|
|
392
|
+
*/
|
|
393
|
+
let DeletedDocumentRead = /* @__PURE__ */ function(DeletedDocumentRead) {
|
|
394
|
+
/**
|
|
395
|
+
* The document reads as missing: `get` throws and `resolveIdOrSlug` does not
|
|
396
|
+
* match its id.
|
|
397
|
+
*/
|
|
398
|
+
DeletedDocumentRead["NotFound"] = "NotFound";
|
|
399
|
+
/**
|
|
400
|
+
* The document's state as of the deletion, with `state.document.isDeleted`
|
|
401
|
+
* telling the caller what it holds. Only meaningful with `documentDecisions`,
|
|
402
|
+
* which is what makes deletion positional.
|
|
403
|
+
*/
|
|
404
|
+
DeletedDocumentRead["StateAtDeletion"] = "StateAtDeletion";
|
|
405
|
+
return DeletedDocumentRead;
|
|
406
|
+
}({});
|
|
283
407
|
var KyselyDocumentView = class extends BaseReadModel {
|
|
284
408
|
_db;
|
|
285
|
-
constructor(db, operationStore, operationIndex, writeCache, consistencyTracker,
|
|
409
|
+
constructor(db, operationStore, operationIndex, writeCache, consistencyTracker, deletedDocumentRead, indexing = defaultReadModelIndexingConfig) {
|
|
286
410
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
287
411
|
readModelId: DOCUMENT_VIEW_READ_MODEL,
|
|
288
|
-
rebuildStateOnInit: true
|
|
412
|
+
rebuildStateOnInit: true,
|
|
413
|
+
indexing
|
|
289
414
|
});
|
|
290
415
|
this.operationStore = operationStore;
|
|
291
|
-
this.
|
|
416
|
+
this.deletedDocumentRead = deletedDocumentRead;
|
|
292
417
|
this._db = db;
|
|
293
418
|
}
|
|
294
419
|
/**
|
|
@@ -300,12 +425,17 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
300
425
|
* without either fall back to header/document/auth, because their sibling
|
|
301
426
|
* echoes may be stale. All other action types index only header and their
|
|
302
427
|
* own scope.
|
|
428
|
+
*
|
|
429
|
+
* The header row is the one row every scope's chain writes, so it accepts a
|
|
430
|
+
* write only from an operation whose global ordinal is at least the one the
|
|
431
|
+
* row already carries. Without that, a chunked pass that started earlier
|
|
432
|
+
* reverts a concurrent rename with the stale echo its later chunks carry.
|
|
303
433
|
*/
|
|
304
434
|
async commitOperations(items) {
|
|
305
435
|
await this._db.transaction().execute(async (trx) => {
|
|
306
436
|
for (const item of items) {
|
|
307
437
|
const { operation, context } = item;
|
|
308
|
-
const { documentId, scope, branch, documentType, resultingState } = context;
|
|
438
|
+
const { documentId, scope, branch, documentType, resultingState, ordinal } = context;
|
|
309
439
|
const { index, hash } = operation;
|
|
310
440
|
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
441
|
let fullState;
|
|
@@ -324,6 +454,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
324
454
|
deletedAt: now,
|
|
325
455
|
lastOperationIndex: index,
|
|
326
456
|
lastOperationHash: hash,
|
|
457
|
+
lastOperationOrdinal: ordinal,
|
|
327
458
|
lastUpdatedAt: now
|
|
328
459
|
}).where("documentId", "=", documentId).where("branch", "=", branch).execute();
|
|
329
460
|
const deletedDocumentScope = fullState?.document;
|
|
@@ -345,7 +476,15 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
345
476
|
else scopesToIndex.push([scope, {}]);
|
|
346
477
|
}
|
|
347
478
|
for (const [scopeName, scopeState] of scopesToIndex) {
|
|
348
|
-
const
|
|
479
|
+
const needsExistingContent = scopeName === "header" && preserveHeaderMeta;
|
|
480
|
+
const existingSnapshot = await trx.selectFrom("DocumentSnapshot").select([
|
|
481
|
+
"slug",
|
|
482
|
+
"name",
|
|
483
|
+
"isDeleted",
|
|
484
|
+
"snapshotVersion",
|
|
485
|
+
"lastOperationOrdinal"
|
|
486
|
+
]).$if(needsExistingContent, (qb) => qb.select("content")).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst();
|
|
487
|
+
if (scopeName === "header" && existingSnapshot !== void 0 && existingSnapshot.lastOperationOrdinal > ordinal) continue;
|
|
349
488
|
const newState = typeof scopeState === "object" && scopeState !== null ? scopeState : {};
|
|
350
489
|
let slug = existingSnapshot?.slug ?? null;
|
|
351
490
|
let name = existingSnapshot?.name ?? null;
|
|
@@ -354,7 +493,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
354
493
|
const headerName = newState.name;
|
|
355
494
|
if (typeof headerSlug === "string") slug = headerSlug;
|
|
356
495
|
if (typeof headerName === "string") name = headerName;
|
|
357
|
-
if (
|
|
496
|
+
if (needsExistingContent && existingSnapshot) {
|
|
358
497
|
const existingHeader = existingSnapshot.content;
|
|
359
498
|
if (existingHeader && "meta" in existingHeader) newState.meta = existingHeader.meta;
|
|
360
499
|
}
|
|
@@ -372,12 +511,13 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
372
511
|
if (existingSnapshot) await trx.updateTable("DocumentSnapshot").set({
|
|
373
512
|
lastOperationIndex: index,
|
|
374
513
|
lastOperationHash: hash,
|
|
514
|
+
lastOperationOrdinal: ordinal,
|
|
375
515
|
lastUpdatedAt: /* @__PURE__ */ new Date(),
|
|
376
516
|
snapshotVersion: existingSnapshot.snapshotVersion + 1,
|
|
377
517
|
content: newState,
|
|
378
518
|
slug,
|
|
379
519
|
name
|
|
380
|
-
}).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).execute();
|
|
520
|
+
}).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).$if(scopeName === "header", (qb) => qb.where("lastOperationOrdinal", "<=", ordinal)).execute();
|
|
381
521
|
else {
|
|
382
522
|
const snapshot = {
|
|
383
523
|
id: v4(),
|
|
@@ -390,6 +530,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
390
530
|
documentType,
|
|
391
531
|
lastOperationIndex: index,
|
|
392
532
|
lastOperationHash: hash,
|
|
533
|
+
lastOperationOrdinal: ordinal,
|
|
393
534
|
identifiers: null,
|
|
394
535
|
metadata: null,
|
|
395
536
|
deletedAt: null
|
|
@@ -419,7 +560,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
419
560
|
])];
|
|
420
561
|
else scopesToQuery = [];
|
|
421
562
|
let query = this._db.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("branch", "=", branch);
|
|
422
|
-
if (
|
|
563
|
+
if (this.deletedDocumentRead === DeletedDocumentRead.NotFound) query = query.where("isDeleted", "=", false);
|
|
423
564
|
if (scopesToQuery.length > 0) query = query.where("scope", "in", scopesToQuery);
|
|
424
565
|
const snapshots = await query.execute();
|
|
425
566
|
if (snapshots.length === 0) throw new Error(`Document not found: ${documentId}`);
|
|
@@ -564,7 +705,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
564
705
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
565
706
|
const branch = view?.branch || "main";
|
|
566
707
|
let idCheckQuery = this._db.selectFrom("DocumentSnapshot").select("documentId").where("documentId", "=", identifier).where("branch", "=", branch);
|
|
567
|
-
if (
|
|
708
|
+
if (this.deletedDocumentRead === DeletedDocumentRead.NotFound) idCheckQuery = idCheckQuery.where("isDeleted", "=", false);
|
|
568
709
|
const idCheckPromise = idCheckQuery.executeTakeFirst();
|
|
569
710
|
const slugCheckPromise = this._db.selectFrom("SlugMapping").select("documentId").where("slug", "=", identifier).where("branch", "=", branch).executeTakeFirst();
|
|
570
711
|
const [idMatch, slugMatch] = await Promise.all([idCheckPromise, slugCheckPromise]);
|
|
@@ -713,18 +854,25 @@ async function collectAllPages(firstPage, signal) {
|
|
|
713
854
|
}
|
|
714
855
|
//#endregion
|
|
715
856
|
//#region src/storage/kysely/document-indexer.ts
|
|
857
|
+
function isRelationshipAction(actionType) {
|
|
858
|
+
return actionType === "ADD_RELATIONSHIP" || actionType === "REMOVE_RELATIONSHIP" || actionType === "UPDATE_RELATIONSHIP";
|
|
859
|
+
}
|
|
716
860
|
var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
717
861
|
_db;
|
|
718
|
-
constructor(db, operationIndex, writeCache, consistencyTracker) {
|
|
862
|
+
constructor(db, operationIndex, writeCache, consistencyTracker, indexing = defaultReadModelIndexingConfig) {
|
|
719
863
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
720
864
|
readModelId: DOCUMENT_INDEXER_READ_MODEL,
|
|
721
|
-
rebuildStateOnInit: false
|
|
865
|
+
rebuildStateOnInit: false,
|
|
866
|
+
indexing
|
|
722
867
|
});
|
|
723
868
|
this._db = db;
|
|
724
869
|
}
|
|
870
|
+
/** Opens no transaction for a batch carrying no relationship operation. */
|
|
725
871
|
async commitOperations(items) {
|
|
872
|
+
const relationshipOps = items.filter((item) => isRelationshipAction(item.operation.action.type));
|
|
873
|
+
if (relationshipOps.length === 0) return;
|
|
726
874
|
await this._db.transaction().execute(async (trx) => {
|
|
727
|
-
for (const item of
|
|
875
|
+
for (const item of relationshipOps) {
|
|
728
876
|
const { operation } = item;
|
|
729
877
|
const actionType = operation.action.type;
|
|
730
878
|
if (actionType === "ADD_RELATIONSHIP") await this.handleAddRelationship(trx, operation);
|
|
@@ -953,6 +1101,6 @@ var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
|
953
1101
|
}
|
|
954
1102
|
};
|
|
955
1103
|
//#endregion
|
|
956
|
-
export {
|
|
1104
|
+
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
1105
|
|
|
958
|
-
//# sourceMappingURL=document-indexer-
|
|
1106
|
+
//# sourceMappingURL=document-indexer-C1oY6CaR.js.map
|