@powerhousedao/reactor 6.1.0-dev.1 → 6.1.0-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-DDVXB921.js +83 -0
- package/dist/build-worker-executor-DDVXB921.js.map +1 -0
- package/dist/document-indexer-B2iLRB0o.js +917 -0
- package/dist/document-indexer-B2iLRB0o.js.map +1 -0
- package/dist/drive-container-types-BNpMlgT_.js +2964 -0
- package/dist/drive-container-types-BNpMlgT_.js.map +1 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.js +313 -0
- package/dist/entry.js.map +1 -0
- package/dist/error-info-Cpu4OY3o.js +62 -0
- package/dist/error-info-Cpu4OY3o.js.map +1 -0
- package/dist/errors-D3S6Eysd.js +56 -0
- package/dist/errors-D3S6Eysd.js.map +1 -0
- package/dist/forwarding-logger-BBkMSxuJ.js +85 -0
- package/dist/forwarding-logger-BBkMSxuJ.js.map +1 -0
- package/dist/index.d.ts +991 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +900 -3889
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +1 -0
- package/dist/projection-entry.js +406 -0
- package/dist/projection-entry.js.map +1 -0
- package/dist/projection-shard-manager-_c7orNo5.js +313 -0
- package/dist/projection-shard-manager-_c7orNo5.js.map +1 -0
- package/dist/projection-worker-wI4PwcV2.js +13 -0
- package/dist/projection-worker-wI4PwcV2.js.map +1 -0
- package/dist/transport-ByGviWdZ.js +33 -0
- package/dist/transport-ByGviWdZ.js.map +1 -0
- package/dist/transport-CuogVKN_.js +23 -0
- package/dist/transport-CuogVKN_.js.map +1 -0
- package/dist/types-CxSpmNGK.js +32 -0
- package/dist/types-CxSpmNGK.js.map +1 -0
- package/dist/worker-SUoDhurA.js +22 -0
- package/dist/worker-SUoDhurA.js.map +1 -0
- package/dist/worker-handle-B1w03nRA.js +383 -0
- package/dist/worker-handle-B1w03nRA.js.map +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
import { n as ReactorEventTypes } from "./types-CxSpmNGK.js";
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
import { childLogger } from "document-model";
|
|
4
|
+
//#region src/read-models/base-read-model.ts
|
|
5
|
+
/**
|
|
6
|
+
* Base class for read models that provides catch-up/rewind functionality.
|
|
7
|
+
* Handles initialization, state tracking via ViewState table, and consistency tracking.
|
|
8
|
+
* Subclasses override commitOperations() with their specific domain logic.
|
|
9
|
+
*/
|
|
10
|
+
var BaseReadModel = class {
|
|
11
|
+
lastOrdinal = 0;
|
|
12
|
+
name;
|
|
13
|
+
constructor(db, operationIndex, writeCache, consistencyTracker, config) {
|
|
14
|
+
this.db = db;
|
|
15
|
+
this.operationIndex = operationIndex;
|
|
16
|
+
this.writeCache = writeCache;
|
|
17
|
+
this.consistencyTracker = consistencyTracker;
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.name = config.readModelId;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Initializes the read model by loading state and catching up on missed operations.
|
|
23
|
+
*/
|
|
24
|
+
async init() {
|
|
25
|
+
const viewState = await this.loadState();
|
|
26
|
+
if (viewState !== void 0) {
|
|
27
|
+
this.lastOrdinal = viewState;
|
|
28
|
+
const missedOperations = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
|
|
29
|
+
if (missedOperations.results.length > 0) {
|
|
30
|
+
const ops = this.config.rebuildStateOnInit ? await this.rebuildStateForOperations(missedOperations.results) : missedOperations.results;
|
|
31
|
+
await this.indexOperations(ops);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
await this.initializeState();
|
|
35
|
+
const allOperations = await this.operationIndex.getSinceOrdinal(0);
|
|
36
|
+
if (allOperations.results.length > 0) {
|
|
37
|
+
const ops = this.config.rebuildStateOnInit ? await this.rebuildStateForOperations(allOperations.results) : allOperations.results;
|
|
38
|
+
await this.indexOperations(ops);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Template method: runs domain-specific commitOperations, then persists
|
|
44
|
+
* state and updates consistency tracking.
|
|
45
|
+
*/
|
|
46
|
+
async indexOperations(items) {
|
|
47
|
+
if (items.length === 0) return;
|
|
48
|
+
await this.commitOperations(items);
|
|
49
|
+
await this.db.transaction().execute(async (trx) => {
|
|
50
|
+
await this.saveState(trx, items);
|
|
51
|
+
});
|
|
52
|
+
this.updateConsistencyTracker(items);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Waits for the read model to reach the specified consistency level.
|
|
56
|
+
*/
|
|
57
|
+
async waitForConsistency(token, timeoutMs, signal) {
|
|
58
|
+
if (token.coordinates.length === 0) return;
|
|
59
|
+
await this.consistencyTracker.waitFor(token.coordinates, timeoutMs, signal);
|
|
60
|
+
}
|
|
61
|
+
async commitOperations(items) {}
|
|
62
|
+
/**
|
|
63
|
+
* Rebuilds document state for each operation using the write cache.
|
|
64
|
+
*/
|
|
65
|
+
async rebuildStateForOperations(operations) {
|
|
66
|
+
const result = [];
|
|
67
|
+
for (const op of operations) {
|
|
68
|
+
const { documentId, scope, branch } = op.context;
|
|
69
|
+
const targetRevision = op.operation.index;
|
|
70
|
+
const document = await this.writeCache.getState(documentId, scope, branch, targetRevision);
|
|
71
|
+
result.push({
|
|
72
|
+
operation: op.operation,
|
|
73
|
+
context: {
|
|
74
|
+
...op.context,
|
|
75
|
+
resultingState: JSON.stringify(document)
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Loads the last processed ordinal from the ViewState table.
|
|
83
|
+
* Returns undefined if no state exists for this read model.
|
|
84
|
+
*/
|
|
85
|
+
async loadState() {
|
|
86
|
+
return (await this.db.selectFrom("ViewState").select("lastOrdinal").where("readModelId", "=", this.config.readModelId).executeTakeFirst())?.lastOrdinal;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Initializes the ViewState row for this read model.
|
|
90
|
+
*/
|
|
91
|
+
async initializeState() {
|
|
92
|
+
await this.db.insertInto("ViewState").values({
|
|
93
|
+
readModelId: this.config.readModelId,
|
|
94
|
+
lastOrdinal: 0
|
|
95
|
+
}).execute();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Saves the last processed ordinal to the ViewState table.
|
|
99
|
+
*/
|
|
100
|
+
async saveState(trx, items) {
|
|
101
|
+
const maxOrdinal = Math.max(...items.map((item) => item.context.ordinal));
|
|
102
|
+
this.lastOrdinal = maxOrdinal;
|
|
103
|
+
await trx.updateTable("ViewState").set({
|
|
104
|
+
lastOrdinal: maxOrdinal,
|
|
105
|
+
lastOperationTimestamp: /* @__PURE__ */ new Date()
|
|
106
|
+
}).where("readModelId", "=", this.config.readModelId).execute();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Updates the consistency tracker with the processed operations.
|
|
110
|
+
*/
|
|
111
|
+
updateConsistencyTracker(items) {
|
|
112
|
+
const coordinates = [];
|
|
113
|
+
for (let i = 0; i < items.length; i++) {
|
|
114
|
+
const item = items[i];
|
|
115
|
+
coordinates.push({
|
|
116
|
+
documentId: item.context.documentId,
|
|
117
|
+
scope: item.context.scope,
|
|
118
|
+
branch: item.context.branch,
|
|
119
|
+
operationIndex: item.operation.index
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
this.consistencyTracker.update(coordinates);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/read-models/coordinator.ts
|
|
127
|
+
/**
|
|
128
|
+
* Coordinates read model synchronization by listening to operation write events
|
|
129
|
+
* and updating all registered read models on per-`documentId:scope:branch`
|
|
130
|
+
* serial chains. Cross-key projection runs in parallel; same-key projection is
|
|
131
|
+
* serialized so the executor can return to dispatch without holding ordering
|
|
132
|
+
* implicitly.
|
|
133
|
+
*/
|
|
134
|
+
var ReadModelCoordinator = class {
|
|
135
|
+
unsubscribe;
|
|
136
|
+
isRunning = false;
|
|
137
|
+
chains = /* @__PURE__ */ new Map();
|
|
138
|
+
logger;
|
|
139
|
+
readModels;
|
|
140
|
+
constructor(eventBus, preReady, postReady) {
|
|
141
|
+
this.eventBus = eventBus;
|
|
142
|
+
this.preReady = preReady;
|
|
143
|
+
this.postReady = postReady;
|
|
144
|
+
this.readModels = [...preReady, ...postReady];
|
|
145
|
+
this.logger = childLogger(["reactor", "read-model-coordinator"]);
|
|
146
|
+
}
|
|
147
|
+
start() {
|
|
148
|
+
if (this.isRunning) return;
|
|
149
|
+
this.unsubscribe = this.eventBus.subscribe(ReactorEventTypes.JOB_WRITE_READY, (type, event) => {
|
|
150
|
+
return this.handleWriteReady(event);
|
|
151
|
+
});
|
|
152
|
+
this.isRunning = true;
|
|
153
|
+
}
|
|
154
|
+
stop() {
|
|
155
|
+
if (!this.isRunning) return;
|
|
156
|
+
if (this.unsubscribe) {
|
|
157
|
+
this.unsubscribe();
|
|
158
|
+
this.unsubscribe = void 0;
|
|
159
|
+
}
|
|
160
|
+
this.isRunning = false;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolves when every per-queueKey projection chain has flushed. Intended
|
|
164
|
+
* for test fixtures and explicit shutdown; production callers use
|
|
165
|
+
* consistency tokens instead.
|
|
166
|
+
*/
|
|
167
|
+
async drain() {
|
|
168
|
+
while (this.chains.size > 0) {
|
|
169
|
+
const pending = Array.from(this.chains.values());
|
|
170
|
+
await Promise.allSettled(pending);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
getChainDepth() {
|
|
174
|
+
return this.chains.size;
|
|
175
|
+
}
|
|
176
|
+
handleWriteReady(event) {
|
|
177
|
+
if (event.operations.length === 0) {
|
|
178
|
+
this.emitEmptyReadReady(event);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const enqueuedAt = performance.now();
|
|
182
|
+
const key = this.queueKeyFor(event);
|
|
183
|
+
const current = (this.chains.get(key) ?? Promise.resolve()).then(() => this.runChain(event, enqueuedAt));
|
|
184
|
+
this.chains.set(key, current);
|
|
185
|
+
current.finally(() => {
|
|
186
|
+
if (this.chains.get(key) === current) this.chains.delete(key);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async emitEmptyReadReady(event) {
|
|
190
|
+
const readyEvent = {
|
|
191
|
+
jobId: event.jobId,
|
|
192
|
+
operations: event.operations
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
await this.eventBus.emit(ReactorEventTypes.JOB_READ_READY, readyEvent);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.logger.error("JOB_READ_READY emit failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
198
|
+
}
|
|
199
|
+
this.emitBatchCompleted({
|
|
200
|
+
jobId: event.jobId,
|
|
201
|
+
batchSize: 0,
|
|
202
|
+
chainWaitDurationMs: 0,
|
|
203
|
+
preReadyDurationMs: 0,
|
|
204
|
+
emitDurationMs: 0,
|
|
205
|
+
postReadyDurationMs: 0
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async runChain(event, enqueuedAt) {
|
|
209
|
+
const chainWaitDurationMs = performance.now() - enqueuedAt;
|
|
210
|
+
const preReadyStart = performance.now();
|
|
211
|
+
try {
|
|
212
|
+
await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event)));
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.logger.error("Pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
215
|
+
}
|
|
216
|
+
const preReadyDurationMs = performance.now() - preReadyStart;
|
|
217
|
+
const readyEvent = {
|
|
218
|
+
jobId: event.jobId,
|
|
219
|
+
operations: event.operations
|
|
220
|
+
};
|
|
221
|
+
const emitStart = performance.now();
|
|
222
|
+
try {
|
|
223
|
+
await this.eventBus.emit(ReactorEventTypes.JOB_READ_READY, readyEvent);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.logger.error("JOB_READ_READY emit failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
226
|
+
}
|
|
227
|
+
const emitDurationMs = performance.now() - emitStart;
|
|
228
|
+
const postReadyStart = performance.now();
|
|
229
|
+
try {
|
|
230
|
+
await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event)));
|
|
231
|
+
} catch (error) {
|
|
232
|
+
this.logger.error("Post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
233
|
+
}
|
|
234
|
+
const postReadyDurationMs = performance.now() - postReadyStart;
|
|
235
|
+
this.emitBatchCompleted({
|
|
236
|
+
jobId: event.jobId,
|
|
237
|
+
batchSize: event.operations.length,
|
|
238
|
+
chainWaitDurationMs,
|
|
239
|
+
preReadyDurationMs,
|
|
240
|
+
emitDurationMs,
|
|
241
|
+
postReadyDurationMs
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async indexWithTiming(readModel, stage, event) {
|
|
245
|
+
const start = performance.now();
|
|
246
|
+
let success = false;
|
|
247
|
+
try {
|
|
248
|
+
await readModel.indexOperations(event.operations);
|
|
249
|
+
success = true;
|
|
250
|
+
} finally {
|
|
251
|
+
this.emitReadModelIndexed({
|
|
252
|
+
jobId: event.jobId,
|
|
253
|
+
readModelName: readModel.name,
|
|
254
|
+
stage,
|
|
255
|
+
durationMs: performance.now() - start,
|
|
256
|
+
operationCount: event.operations.length,
|
|
257
|
+
success
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
emitReadModelIndexed(payload) {
|
|
262
|
+
this.eventBus.emit(ReactorEventTypes.READMODEL_INDEXED, payload).catch((err) => this.logger.error("READMODEL_INDEXED emit failed for job @jobId: @Error", { jobId: payload.jobId }, err));
|
|
263
|
+
}
|
|
264
|
+
emitBatchCompleted(payload) {
|
|
265
|
+
this.eventBus.emit(ReactorEventTypes.READMODEL_BATCH_COMPLETED, payload).catch((err) => this.logger.error("READMODEL_BATCH_COMPLETED emit failed for job @jobId: @Error", { jobId: payload.jobId }, err));
|
|
266
|
+
}
|
|
267
|
+
queueKeyFor(event) {
|
|
268
|
+
const ctx = event.operations[0].context;
|
|
269
|
+
return `${ctx.documentId}:${ctx.scope}:${ctx.branch}`;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/read-models/document-view.ts
|
|
274
|
+
var KyselyDocumentView = class extends BaseReadModel {
|
|
275
|
+
_db;
|
|
276
|
+
constructor(db, operationStore, operationIndex, writeCache, consistencyTracker) {
|
|
277
|
+
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
278
|
+
readModelId: "document-view",
|
|
279
|
+
rebuildStateOnInit: true
|
|
280
|
+
});
|
|
281
|
+
this.operationStore = operationStore;
|
|
282
|
+
this._db = db;
|
|
283
|
+
}
|
|
284
|
+
async commitOperations(items) {
|
|
285
|
+
await this._db.transaction().execute(async (trx) => {
|
|
286
|
+
for (const item of items) {
|
|
287
|
+
const { operation, context } = item;
|
|
288
|
+
const { documentId, scope, branch, documentType, resultingState } = context;
|
|
289
|
+
const { index, hash } = operation;
|
|
290
|
+
if (!resultingState) throw new Error(`Missing resultingState in context for operation ${operation.id || "unknown"}. IDocumentView requires resultingState from upstream - it does not rebuild documents.`);
|
|
291
|
+
let fullState;
|
|
292
|
+
try {
|
|
293
|
+
fullState = JSON.parse(resultingState);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw new Error(`Failed to parse resultingState for operation ${operation.id || "unknown"}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
296
|
+
}
|
|
297
|
+
const operationType = operation.action.type;
|
|
298
|
+
if (operationType === "DELETE_DOCUMENT") {
|
|
299
|
+
const now = /* @__PURE__ */ new Date();
|
|
300
|
+
await trx.updateTable("DocumentSnapshot").set({
|
|
301
|
+
isDeleted: true,
|
|
302
|
+
deletedAt: now,
|
|
303
|
+
lastOperationIndex: index,
|
|
304
|
+
lastOperationHash: hash,
|
|
305
|
+
lastUpdatedAt: now
|
|
306
|
+
}).where("documentId", "=", documentId).where("branch", "=", branch).execute();
|
|
307
|
+
await trx.deleteFrom("SlugMapping").where("documentId", "=", documentId).where("branch", "=", branch).execute();
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
let scopesToIndex;
|
|
311
|
+
if (operationType === "CREATE_DOCUMENT") scopesToIndex = Object.entries(fullState).filter(([key]) => key === "header" || key === "document" || key === "auth");
|
|
312
|
+
else if (operationType === "UPGRADE_DOCUMENT") {
|
|
313
|
+
const scopeStatesToIndex = [];
|
|
314
|
+
for (const [scopeName, scopeState] of Object.entries(fullState)) {
|
|
315
|
+
if (scopeName === "header") {
|
|
316
|
+
scopeStatesToIndex.push([scopeName, scopeState]);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (scopeName === scope) {
|
|
320
|
+
scopeStatesToIndex.push([scopeName, scopeState]);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!await trx.selectFrom("DocumentSnapshot").select("scope").where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst()) scopeStatesToIndex.push([scopeName, scopeState]);
|
|
324
|
+
}
|
|
325
|
+
scopesToIndex = scopeStatesToIndex;
|
|
326
|
+
} else {
|
|
327
|
+
scopesToIndex = [];
|
|
328
|
+
if (fullState.header !== void 0) scopesToIndex.push(["header", fullState.header]);
|
|
329
|
+
if (fullState[scope] !== void 0) scopesToIndex.push([scope, fullState[scope]]);
|
|
330
|
+
else scopesToIndex.push([scope, {}]);
|
|
331
|
+
}
|
|
332
|
+
for (const [scopeName, scopeState] of scopesToIndex) {
|
|
333
|
+
const existingSnapshot = await trx.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst();
|
|
334
|
+
const newState = typeof scopeState === "object" && scopeState !== null ? scopeState : {};
|
|
335
|
+
let slug = existingSnapshot?.slug ?? null;
|
|
336
|
+
let name = existingSnapshot?.name ?? null;
|
|
337
|
+
if (scopeName === "header") {
|
|
338
|
+
const headerSlug = newState.slug;
|
|
339
|
+
const headerName = newState.name;
|
|
340
|
+
if (typeof headerSlug === "string") slug = headerSlug;
|
|
341
|
+
if (typeof headerName === "string") name = headerName;
|
|
342
|
+
if (slug && slug !== documentId) await trx.insertInto("SlugMapping").values({
|
|
343
|
+
slug,
|
|
344
|
+
documentId,
|
|
345
|
+
scope: scopeName,
|
|
346
|
+
branch
|
|
347
|
+
}).onConflict((oc) => oc.column("slug").doUpdateSet({
|
|
348
|
+
documentId,
|
|
349
|
+
scope: scopeName,
|
|
350
|
+
branch
|
|
351
|
+
})).execute();
|
|
352
|
+
}
|
|
353
|
+
if (existingSnapshot) await trx.updateTable("DocumentSnapshot").set({
|
|
354
|
+
lastOperationIndex: index,
|
|
355
|
+
lastOperationHash: hash,
|
|
356
|
+
lastUpdatedAt: /* @__PURE__ */ new Date(),
|
|
357
|
+
snapshotVersion: existingSnapshot.snapshotVersion + 1,
|
|
358
|
+
content: newState,
|
|
359
|
+
slug,
|
|
360
|
+
name
|
|
361
|
+
}).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).execute();
|
|
362
|
+
else {
|
|
363
|
+
const snapshot = {
|
|
364
|
+
id: v4(),
|
|
365
|
+
documentId,
|
|
366
|
+
slug,
|
|
367
|
+
name,
|
|
368
|
+
scope: scopeName,
|
|
369
|
+
branch,
|
|
370
|
+
content: newState,
|
|
371
|
+
documentType,
|
|
372
|
+
lastOperationIndex: index,
|
|
373
|
+
lastOperationHash: hash,
|
|
374
|
+
identifiers: null,
|
|
375
|
+
metadata: null,
|
|
376
|
+
deletedAt: null
|
|
377
|
+
};
|
|
378
|
+
await trx.insertInto("DocumentSnapshot").values(snapshot).execute();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async exists(documentIds, consistencyToken, signal) {
|
|
385
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
386
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
387
|
+
if (documentIds.length === 0) return [];
|
|
388
|
+
const snapshots = await this._db.selectFrom("DocumentSnapshot").select(["documentId"]).where("documentId", "in", documentIds).where("isDeleted", "=", false).distinct().execute();
|
|
389
|
+
const existingIds = new Set(snapshots.map((s) => s.documentId));
|
|
390
|
+
return documentIds.map((id) => existingIds.has(id));
|
|
391
|
+
}
|
|
392
|
+
async get(documentId, view, consistencyToken, signal) {
|
|
393
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
394
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
395
|
+
const branch = view?.branch || "main";
|
|
396
|
+
let scopesToQuery;
|
|
397
|
+
if (view?.scopes && view.scopes.length > 0) scopesToQuery = [...new Set([
|
|
398
|
+
"header",
|
|
399
|
+
"document",
|
|
400
|
+
...view.scopes
|
|
401
|
+
])];
|
|
402
|
+
else scopesToQuery = [];
|
|
403
|
+
let query = this._db.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("branch", "=", branch).where("isDeleted", "=", false);
|
|
404
|
+
if (scopesToQuery.length > 0) query = query.where("scope", "in", scopesToQuery);
|
|
405
|
+
const snapshots = await query.execute();
|
|
406
|
+
if (snapshots.length === 0) throw new Error(`Document not found: ${documentId}`);
|
|
407
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
408
|
+
const headerSnapshot = snapshots.find((s) => s.scope === "header");
|
|
409
|
+
if (!headerSnapshot) throw new Error(`Document header not found: ${documentId}`);
|
|
410
|
+
const header = headerSnapshot.content;
|
|
411
|
+
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
412
|
+
header.revision = revisions.revision;
|
|
413
|
+
header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
414
|
+
const state = {};
|
|
415
|
+
for (const snapshot of snapshots) {
|
|
416
|
+
if (snapshot.scope === "header") continue;
|
|
417
|
+
state[snapshot.scope] = snapshot.content;
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
header,
|
|
421
|
+
state,
|
|
422
|
+
operations: {},
|
|
423
|
+
initialState: state,
|
|
424
|
+
clipboard: []
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async getMany(documentIds, view, consistencyToken, signal) {
|
|
428
|
+
if (documentIds.length === 0) return [];
|
|
429
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
430
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
431
|
+
const branch = view?.branch || "main";
|
|
432
|
+
let scopesToQuery;
|
|
433
|
+
if (view?.scopes && view.scopes.length > 0) scopesToQuery = [...new Set([
|
|
434
|
+
"header",
|
|
435
|
+
"document",
|
|
436
|
+
...view.scopes
|
|
437
|
+
])];
|
|
438
|
+
else scopesToQuery = [];
|
|
439
|
+
let query = this._db.selectFrom("DocumentSnapshot").selectAll().where("documentId", "in", documentIds).where("branch", "=", branch).where("isDeleted", "=", false);
|
|
440
|
+
if (scopesToQuery.length > 0) query = query.where("scope", "in", scopesToQuery);
|
|
441
|
+
const allSnapshots = await query.execute();
|
|
442
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
443
|
+
const snapshotsByDocId = /* @__PURE__ */ new Map();
|
|
444
|
+
for (const snapshot of allSnapshots) {
|
|
445
|
+
const existing = snapshotsByDocId.get(snapshot.documentId) || [];
|
|
446
|
+
existing.push(snapshot);
|
|
447
|
+
snapshotsByDocId.set(snapshot.documentId, existing);
|
|
448
|
+
}
|
|
449
|
+
const foundDocumentIds = [...snapshotsByDocId.keys()];
|
|
450
|
+
const revisionResults = await Promise.all(foundDocumentIds.map((docId) => this.operationStore.getRevisions(docId, branch, signal)));
|
|
451
|
+
const revisionsByDocId = /* @__PURE__ */ new Map();
|
|
452
|
+
for (let i = 0; i < foundDocumentIds.length; i++) revisionsByDocId.set(foundDocumentIds[i], revisionResults[i]);
|
|
453
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
454
|
+
const documents = [];
|
|
455
|
+
for (const documentId of documentIds) {
|
|
456
|
+
const snapshots = snapshotsByDocId.get(documentId);
|
|
457
|
+
if (!snapshots || snapshots.length === 0) continue;
|
|
458
|
+
const headerSnapshot = snapshots.find((s) => s.scope === "header");
|
|
459
|
+
if (!headerSnapshot) continue;
|
|
460
|
+
const header = headerSnapshot.content;
|
|
461
|
+
const revisions = revisionsByDocId.get(documentId);
|
|
462
|
+
if (revisions) {
|
|
463
|
+
header.revision = revisions.revision;
|
|
464
|
+
header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
465
|
+
}
|
|
466
|
+
const state = {};
|
|
467
|
+
for (const snapshot of snapshots) {
|
|
468
|
+
if (snapshot.scope === "header") continue;
|
|
469
|
+
state[snapshot.scope] = snapshot.content;
|
|
470
|
+
}
|
|
471
|
+
const document = {
|
|
472
|
+
header,
|
|
473
|
+
state,
|
|
474
|
+
operations: {},
|
|
475
|
+
initialState: state,
|
|
476
|
+
clipboard: []
|
|
477
|
+
};
|
|
478
|
+
documents.push(document);
|
|
479
|
+
}
|
|
480
|
+
return documents;
|
|
481
|
+
}
|
|
482
|
+
async getByIdOrSlug(identifier, view, consistencyToken, signal) {
|
|
483
|
+
const documentId = await this.resolveIdOrSlug(identifier, view, consistencyToken, signal);
|
|
484
|
+
return this.get(documentId, view, void 0, signal);
|
|
485
|
+
}
|
|
486
|
+
async findByType(type, view, paging, consistencyToken, signal) {
|
|
487
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
488
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
489
|
+
const branch = view?.branch || "main";
|
|
490
|
+
const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
|
|
491
|
+
const limit = paging?.limit || 100;
|
|
492
|
+
const documents = [];
|
|
493
|
+
const processedDocumentIds = /* @__PURE__ */ new Set();
|
|
494
|
+
const allDocumentIds = [];
|
|
495
|
+
const snapshots = await this._db.selectFrom("DocumentSnapshot").selectAll().where("documentType", "=", type).where("branch", "=", branch).where("isDeleted", "=", false).orderBy("lastUpdatedAt", "desc").execute();
|
|
496
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
497
|
+
for (const snapshot of snapshots) {
|
|
498
|
+
if (processedDocumentIds.has(snapshot.documentId)) continue;
|
|
499
|
+
processedDocumentIds.add(snapshot.documentId);
|
|
500
|
+
allDocumentIds.push(snapshot.documentId);
|
|
501
|
+
}
|
|
502
|
+
const docsToFetch = allDocumentIds.slice(startIndex, startIndex + limit);
|
|
503
|
+
for (const documentId of docsToFetch) {
|
|
504
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
505
|
+
try {
|
|
506
|
+
const document = await this.get(documentId, view, void 0, signal);
|
|
507
|
+
documents.push(document);
|
|
508
|
+
} catch {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const hasMore = allDocumentIds.length > startIndex + limit;
|
|
513
|
+
const nextCursor = hasMore ? String(startIndex + limit) : void 0;
|
|
514
|
+
return {
|
|
515
|
+
results: documents,
|
|
516
|
+
options: paging || {
|
|
517
|
+
cursor: "0",
|
|
518
|
+
limit: 100
|
|
519
|
+
},
|
|
520
|
+
nextCursor,
|
|
521
|
+
totalCount: allDocumentIds.length,
|
|
522
|
+
next: hasMore ? () => this.findByType(type, view, {
|
|
523
|
+
cursor: nextCursor,
|
|
524
|
+
limit
|
|
525
|
+
}, consistencyToken, signal) : void 0
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
async resolveSlug(slug, view, consistencyToken, signal) {
|
|
529
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
530
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
531
|
+
const branch = view?.branch || "main";
|
|
532
|
+
const mapping = await this._db.selectFrom("SlugMapping").select("documentId").where("slug", "=", slug).where("branch", "=", branch).executeTakeFirst();
|
|
533
|
+
if (!mapping) return;
|
|
534
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
535
|
+
if (view?.scopes && view.scopes.length > 0) {
|
|
536
|
+
if (!await this._db.selectFrom("DocumentSnapshot").select("scope").where("documentId", "=", mapping.documentId).where("branch", "=", branch).where("scope", "in", view.scopes).where("isDeleted", "=", false).executeTakeFirst()) return;
|
|
537
|
+
}
|
|
538
|
+
return mapping.documentId;
|
|
539
|
+
}
|
|
540
|
+
async resolveSlugs(slugs, view, consistencyToken, signal) {
|
|
541
|
+
return (await Promise.all(slugs.map((slug) => this.resolveSlug(slug, view, consistencyToken, signal)))).filter((id) => id !== void 0);
|
|
542
|
+
}
|
|
543
|
+
async resolveIdOrSlug(identifier, view, consistencyToken, signal) {
|
|
544
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
545
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
546
|
+
const branch = view?.branch || "main";
|
|
547
|
+
const idCheckPromise = this._db.selectFrom("DocumentSnapshot").select("documentId").where("documentId", "=", identifier).where("branch", "=", branch).where("isDeleted", "=", false).executeTakeFirst();
|
|
548
|
+
const slugCheckPromise = this._db.selectFrom("SlugMapping").select("documentId").where("slug", "=", identifier).where("branch", "=", branch).executeTakeFirst();
|
|
549
|
+
const [idMatch, slugMatch] = await Promise.all([idCheckPromise, slugCheckPromise]);
|
|
550
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
551
|
+
const idMatchDocId = idMatch?.documentId;
|
|
552
|
+
const slugMatchDocId = slugMatch?.documentId;
|
|
553
|
+
if (idMatchDocId && slugMatchDocId && idMatchDocId !== slugMatchDocId) throw new Error(`Ambiguous identifier "${identifier}": matches both document ID "${idMatchDocId}" and slug for document ID "${slugMatchDocId}". Please use get() for ID or resolveSlug() + get() for slug to be explicit.`);
|
|
554
|
+
const resolvedDocumentId = idMatchDocId || slugMatchDocId;
|
|
555
|
+
if (!resolvedDocumentId) throw new Error(`Document not found: ${identifier}`);
|
|
556
|
+
return resolvedDocumentId;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/shared/consistency-tracker.ts
|
|
561
|
+
/**
|
|
562
|
+
* Creates a consistency key from documentId, scope, and branch.
|
|
563
|
+
*/
|
|
564
|
+
function makeConsistencyKey(documentId, scope, branch) {
|
|
565
|
+
return `${documentId}:${scope}:${branch}`;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Tracks operation indexes for documents and provides read-after-write consistency guarantees.
|
|
569
|
+
* Maintains an in-memory map of the latest operation index for each (documentId, scope, branch) tuple.
|
|
570
|
+
*/
|
|
571
|
+
var ConsistencyTracker = class {
|
|
572
|
+
state = /* @__PURE__ */ new Map();
|
|
573
|
+
waiters = [];
|
|
574
|
+
update(coordinates) {
|
|
575
|
+
const deduplicated = this.deduplicateCoordinates(coordinates);
|
|
576
|
+
for (let i = 0; i < deduplicated.length; i++) {
|
|
577
|
+
const coord = deduplicated[i];
|
|
578
|
+
const key = makeConsistencyKey(coord.documentId, coord.scope, coord.branch);
|
|
579
|
+
const current = this.state.get(key);
|
|
580
|
+
if (current === void 0 || coord.operationIndex > current) this.state.set(key, coord.operationIndex);
|
|
581
|
+
}
|
|
582
|
+
this.checkWaiters();
|
|
583
|
+
}
|
|
584
|
+
getLatest(key) {
|
|
585
|
+
return this.state.get(key);
|
|
586
|
+
}
|
|
587
|
+
waitFor(coordinates, timeoutMs, signal) {
|
|
588
|
+
if (signal?.aborted) return Promise.reject(/* @__PURE__ */ new Error("Operation aborted"));
|
|
589
|
+
if (this.areCoordinatesSatisfied(coordinates)) return Promise.resolve();
|
|
590
|
+
return new Promise((resolve, reject) => {
|
|
591
|
+
const waiter = {
|
|
592
|
+
coordinates,
|
|
593
|
+
resolve,
|
|
594
|
+
reject,
|
|
595
|
+
signal
|
|
596
|
+
};
|
|
597
|
+
if (timeoutMs !== void 0) waiter.timeoutId = setTimeout(() => {
|
|
598
|
+
this.removeWaiter(waiter);
|
|
599
|
+
reject(/* @__PURE__ */ new Error(`Consistency wait timed out after ${timeoutMs}ms`));
|
|
600
|
+
}, timeoutMs);
|
|
601
|
+
if (signal) {
|
|
602
|
+
const abortHandler = () => {
|
|
603
|
+
this.removeWaiter(waiter);
|
|
604
|
+
reject(/* @__PURE__ */ new Error("Operation aborted"));
|
|
605
|
+
};
|
|
606
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
607
|
+
}
|
|
608
|
+
this.waiters.push(waiter);
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
serialize() {
|
|
612
|
+
return Array.from(this.state.entries());
|
|
613
|
+
}
|
|
614
|
+
hydrate(entries) {
|
|
615
|
+
this.state.clear();
|
|
616
|
+
for (const [key, index] of entries) this.state.set(key, index);
|
|
617
|
+
}
|
|
618
|
+
deduplicateCoordinates(coordinates) {
|
|
619
|
+
const map = /* @__PURE__ */ new Map();
|
|
620
|
+
for (let i = 0; i < coordinates.length; i++) {
|
|
621
|
+
const coord = coordinates[i];
|
|
622
|
+
const key = makeConsistencyKey(coord.documentId, coord.scope, coord.branch);
|
|
623
|
+
const existing = map.get(key);
|
|
624
|
+
if (!existing || coord.operationIndex > existing.operationIndex) map.set(key, coord);
|
|
625
|
+
}
|
|
626
|
+
return Array.from(map.values());
|
|
627
|
+
}
|
|
628
|
+
areCoordinatesSatisfied(coordinates) {
|
|
629
|
+
for (let i = 0; i < coordinates.length; i++) {
|
|
630
|
+
const coord = coordinates[i];
|
|
631
|
+
const key = makeConsistencyKey(coord.documentId, coord.scope, coord.branch);
|
|
632
|
+
const latest = this.state.get(key);
|
|
633
|
+
if (latest === void 0 || latest < coord.operationIndex) return false;
|
|
634
|
+
}
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
checkWaiters() {
|
|
638
|
+
const satisfiedWaiters = [];
|
|
639
|
+
const unsatisfiedWaiters = [];
|
|
640
|
+
for (const waiter of this.waiters) {
|
|
641
|
+
if (waiter.signal?.aborted) continue;
|
|
642
|
+
if (this.areCoordinatesSatisfied(waiter.coordinates)) satisfiedWaiters.push(waiter);
|
|
643
|
+
else unsatisfiedWaiters.push(waiter);
|
|
644
|
+
}
|
|
645
|
+
this.waiters = unsatisfiedWaiters;
|
|
646
|
+
for (const waiter of satisfiedWaiters) {
|
|
647
|
+
if (waiter.timeoutId !== void 0) clearTimeout(waiter.timeoutId);
|
|
648
|
+
waiter.resolve();
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
removeWaiter(waiter) {
|
|
652
|
+
const index = this.waiters.indexOf(waiter);
|
|
653
|
+
if (index !== -1) this.waiters.splice(index, 1);
|
|
654
|
+
if (waiter.timeoutId !== void 0) clearTimeout(waiter.timeoutId);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/shared/collect-all-pages.ts
|
|
659
|
+
/**
|
|
660
|
+
* Collects all results from a paged result set by following the next() function
|
|
661
|
+
* until all pages have been fetched.
|
|
662
|
+
*/
|
|
663
|
+
async function collectAllPages(firstPage, signal) {
|
|
664
|
+
const allResults = [...firstPage.results];
|
|
665
|
+
let currentPage = firstPage;
|
|
666
|
+
while (currentPage.next) {
|
|
667
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
668
|
+
currentPage = await currentPage.next();
|
|
669
|
+
allResults.push(...currentPage.results);
|
|
670
|
+
}
|
|
671
|
+
return allResults;
|
|
672
|
+
}
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region src/storage/kysely/document-indexer.ts
|
|
675
|
+
var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
676
|
+
_db;
|
|
677
|
+
constructor(db, operationIndex, writeCache, consistencyTracker) {
|
|
678
|
+
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
679
|
+
readModelId: "document-indexer",
|
|
680
|
+
rebuildStateOnInit: false
|
|
681
|
+
});
|
|
682
|
+
this._db = db;
|
|
683
|
+
}
|
|
684
|
+
async commitOperations(items) {
|
|
685
|
+
await this._db.transaction().execute(async (trx) => {
|
|
686
|
+
for (const item of items) {
|
|
687
|
+
const { operation } = item;
|
|
688
|
+
const actionType = operation.action.type;
|
|
689
|
+
if (actionType === "ADD_RELATIONSHIP") await this.handleAddRelationship(trx, operation);
|
|
690
|
+
else if (actionType === "REMOVE_RELATIONSHIP") await this.handleRemoveRelationship(trx, operation);
|
|
691
|
+
else if (actionType === "UPDATE_RELATIONSHIP") await this.handleUpdateRelationship(trx, operation);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async getOutgoing(documentId, types, paging, consistencyToken, signal) {
|
|
696
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
697
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
698
|
+
const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
|
|
699
|
+
const limit = paging?.limit || 100;
|
|
700
|
+
let query = this._db.selectFrom("DocumentRelationship").selectAll().where("sourceId", "=", documentId);
|
|
701
|
+
if (types && types.length > 0) query = query.where("relationshipType", "in", types);
|
|
702
|
+
const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
|
|
703
|
+
const hasMore = rows.length > limit;
|
|
704
|
+
const results = hasMore ? rows.slice(0, limit) : rows;
|
|
705
|
+
const nextCursor = hasMore ? String(startIndex + limit) : void 0;
|
|
706
|
+
return {
|
|
707
|
+
results: results.map((row) => ({
|
|
708
|
+
sourceId: row.sourceId,
|
|
709
|
+
targetId: row.targetId,
|
|
710
|
+
relationshipType: row.relationshipType,
|
|
711
|
+
metadata: row.metadata ? row.metadata : void 0,
|
|
712
|
+
createdAt: row.createdAt,
|
|
713
|
+
updatedAt: row.updatedAt
|
|
714
|
+
})),
|
|
715
|
+
options: paging || {
|
|
716
|
+
cursor: "0",
|
|
717
|
+
limit: 100
|
|
718
|
+
},
|
|
719
|
+
nextCursor,
|
|
720
|
+
next: hasMore ? () => this.getOutgoing(documentId, types, {
|
|
721
|
+
cursor: nextCursor,
|
|
722
|
+
limit
|
|
723
|
+
}, consistencyToken, signal) : void 0
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
async getIncoming(documentId, types, paging, consistencyToken, signal) {
|
|
727
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
728
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
729
|
+
const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
|
|
730
|
+
const limit = paging?.limit || 100;
|
|
731
|
+
let query = this._db.selectFrom("DocumentRelationship").selectAll().where("targetId", "=", documentId);
|
|
732
|
+
if (types && types.length > 0) query = query.where("relationshipType", "in", types);
|
|
733
|
+
const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
|
|
734
|
+
const hasMore = rows.length > limit;
|
|
735
|
+
const results = hasMore ? rows.slice(0, limit) : rows;
|
|
736
|
+
const nextCursor = hasMore ? String(startIndex + limit) : void 0;
|
|
737
|
+
return {
|
|
738
|
+
results: results.map((row) => ({
|
|
739
|
+
sourceId: row.sourceId,
|
|
740
|
+
targetId: row.targetId,
|
|
741
|
+
relationshipType: row.relationshipType,
|
|
742
|
+
metadata: row.metadata ? row.metadata : void 0,
|
|
743
|
+
createdAt: row.createdAt,
|
|
744
|
+
updatedAt: row.updatedAt
|
|
745
|
+
})),
|
|
746
|
+
options: paging || {
|
|
747
|
+
cursor: "0",
|
|
748
|
+
limit: 100
|
|
749
|
+
},
|
|
750
|
+
nextCursor,
|
|
751
|
+
next: hasMore ? () => this.getIncoming(documentId, types, {
|
|
752
|
+
cursor: nextCursor,
|
|
753
|
+
limit
|
|
754
|
+
}, consistencyToken, signal) : void 0
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async getOrphanedChildren(parentIds, types, signal) {
|
|
758
|
+
if (parentIds.length === 0) return [];
|
|
759
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
760
|
+
let query = this._db.selectFrom("DocumentRelationship as r").select("r.targetId").distinct().where("r.sourceId", "in", parentIds).where((eb) => eb.not(eb.exists(eb.selectFrom("DocumentRelationship as other").select("other.id").whereRef("other.targetId", "=", "r.targetId").where("other.sourceId", "not in", parentIds))));
|
|
761
|
+
if (types && types.length > 0) query = query.where("r.relationshipType", "in", types);
|
|
762
|
+
return (await query.execute()).map((row) => row.targetId);
|
|
763
|
+
}
|
|
764
|
+
async hasRelationship(sourceId, targetId, types, consistencyToken, signal) {
|
|
765
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
766
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
767
|
+
let query = this._db.selectFrom("DocumentRelationship").select("id").where("sourceId", "=", sourceId).where("targetId", "=", targetId);
|
|
768
|
+
if (types && types.length > 0) query = query.where("relationshipType", "in", types);
|
|
769
|
+
return await query.executeTakeFirst() !== void 0;
|
|
770
|
+
}
|
|
771
|
+
async getUndirectedRelationships(a, b, types, paging, consistencyToken, signal) {
|
|
772
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
773
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
774
|
+
const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
|
|
775
|
+
const limit = paging?.limit || 100;
|
|
776
|
+
let query = this._db.selectFrom("DocumentRelationship").selectAll().where((eb) => eb.or([eb.and([eb("sourceId", "=", a), eb("targetId", "=", b)]), eb.and([eb("sourceId", "=", b), eb("targetId", "=", a)])]));
|
|
777
|
+
if (types && types.length > 0) query = query.where("relationshipType", "in", types);
|
|
778
|
+
const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
|
|
779
|
+
const hasMore = rows.length > limit;
|
|
780
|
+
const results = hasMore ? rows.slice(0, limit) : rows;
|
|
781
|
+
const nextCursor = hasMore ? String(startIndex + limit) : void 0;
|
|
782
|
+
return {
|
|
783
|
+
results: results.map((row) => ({
|
|
784
|
+
sourceId: row.sourceId,
|
|
785
|
+
targetId: row.targetId,
|
|
786
|
+
relationshipType: row.relationshipType,
|
|
787
|
+
metadata: row.metadata ? row.metadata : void 0,
|
|
788
|
+
createdAt: row.createdAt,
|
|
789
|
+
updatedAt: row.updatedAt
|
|
790
|
+
})),
|
|
791
|
+
options: paging || {
|
|
792
|
+
cursor: "0",
|
|
793
|
+
limit: 100
|
|
794
|
+
},
|
|
795
|
+
nextCursor,
|
|
796
|
+
next: hasMore ? () => this.getUndirectedRelationships(a, b, types, {
|
|
797
|
+
cursor: nextCursor,
|
|
798
|
+
limit
|
|
799
|
+
}, consistencyToken, signal) : void 0
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async getDirectedRelationships(sourceId, targetId, types, paging, consistencyToken, signal) {
|
|
803
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
804
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
805
|
+
const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
|
|
806
|
+
const limit = paging?.limit || 100;
|
|
807
|
+
let query = this._db.selectFrom("DocumentRelationship").selectAll().where("sourceId", "=", sourceId).where("targetId", "=", targetId);
|
|
808
|
+
if (types && types.length > 0) query = query.where("relationshipType", "in", types);
|
|
809
|
+
const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
|
|
810
|
+
const hasMore = rows.length > limit;
|
|
811
|
+
const results = hasMore ? rows.slice(0, limit) : rows;
|
|
812
|
+
const nextCursor = hasMore ? String(startIndex + limit) : void 0;
|
|
813
|
+
return {
|
|
814
|
+
results: results.map((row) => ({
|
|
815
|
+
sourceId: row.sourceId,
|
|
816
|
+
targetId: row.targetId,
|
|
817
|
+
relationshipType: row.relationshipType,
|
|
818
|
+
metadata: row.metadata ? row.metadata : void 0,
|
|
819
|
+
createdAt: row.createdAt,
|
|
820
|
+
updatedAt: row.updatedAt
|
|
821
|
+
})),
|
|
822
|
+
options: paging || {
|
|
823
|
+
cursor: "0",
|
|
824
|
+
limit: 100
|
|
825
|
+
},
|
|
826
|
+
nextCursor,
|
|
827
|
+
next: hasMore ? () => this.getDirectedRelationships(sourceId, targetId, types, {
|
|
828
|
+
cursor: nextCursor,
|
|
829
|
+
limit
|
|
830
|
+
}, consistencyToken, signal) : void 0
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async findPath(sourceId, targetId, types, consistencyToken, signal) {
|
|
834
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
835
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
836
|
+
if (sourceId === targetId) return [sourceId];
|
|
837
|
+
const visited = /* @__PURE__ */ new Set();
|
|
838
|
+
const queue = [{
|
|
839
|
+
id: sourceId,
|
|
840
|
+
path: [sourceId]
|
|
841
|
+
}];
|
|
842
|
+
while (queue.length > 0) {
|
|
843
|
+
const current = queue.shift();
|
|
844
|
+
if (current.id === targetId) return current.path;
|
|
845
|
+
if (visited.has(current.id)) continue;
|
|
846
|
+
visited.add(current.id);
|
|
847
|
+
const outgoingRelationships = await collectAllPages(await this.getOutgoing(current.id, types, void 0, consistencyToken, signal), signal);
|
|
848
|
+
for (const rel of outgoingRelationships) if (!visited.has(rel.targetId)) queue.push({
|
|
849
|
+
id: rel.targetId,
|
|
850
|
+
path: [...current.path, rel.targetId]
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
async findAncestors(documentId, types, consistencyToken, signal) {
|
|
856
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
857
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
858
|
+
const nodes = new Set([documentId]);
|
|
859
|
+
const edges = [];
|
|
860
|
+
const queue = [documentId];
|
|
861
|
+
const visited = /* @__PURE__ */ new Set();
|
|
862
|
+
while (queue.length > 0) {
|
|
863
|
+
const currentId = queue.shift();
|
|
864
|
+
if (visited.has(currentId)) continue;
|
|
865
|
+
visited.add(currentId);
|
|
866
|
+
const incomingRelationships = await collectAllPages(await this.getIncoming(currentId, types, void 0, consistencyToken, signal), signal);
|
|
867
|
+
for (const rel of incomingRelationships) {
|
|
868
|
+
nodes.add(rel.sourceId);
|
|
869
|
+
edges.push({
|
|
870
|
+
from: rel.sourceId,
|
|
871
|
+
to: rel.targetId,
|
|
872
|
+
type: rel.relationshipType
|
|
873
|
+
});
|
|
874
|
+
if (!visited.has(rel.sourceId)) queue.push(rel.sourceId);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
nodes: Array.from(nodes),
|
|
879
|
+
edges
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
async getRelationshipTypes(consistencyToken, signal) {
|
|
883
|
+
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
884
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
885
|
+
return (await this._db.selectFrom("DocumentRelationship").select("relationshipType").distinct().execute()).map((row) => row.relationshipType);
|
|
886
|
+
}
|
|
887
|
+
async handleAddRelationship(trx, operation) {
|
|
888
|
+
const input = operation.action.input;
|
|
889
|
+
if (!await trx.selectFrom("Document").select("id").where("id", "=", input.sourceId).executeTakeFirst()) await trx.insertInto("Document").values({ id: input.sourceId }).execute();
|
|
890
|
+
if (!await trx.selectFrom("Document").select("id").where("id", "=", input.targetId).executeTakeFirst()) await trx.insertInto("Document").values({ id: input.targetId }).execute();
|
|
891
|
+
if (!await trx.selectFrom("DocumentRelationship").select("id").where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).executeTakeFirst()) {
|
|
892
|
+
const relationship = {
|
|
893
|
+
id: v4(),
|
|
894
|
+
sourceId: input.sourceId,
|
|
895
|
+
targetId: input.targetId,
|
|
896
|
+
relationshipType: input.relationshipType,
|
|
897
|
+
metadata: input.metadata || null
|
|
898
|
+
};
|
|
899
|
+
await trx.insertInto("DocumentRelationship").values(relationship).execute();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
async handleRemoveRelationship(trx, operation) {
|
|
903
|
+
const input = operation.action.input;
|
|
904
|
+
await trx.deleteFrom("DocumentRelationship").where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).execute();
|
|
905
|
+
}
|
|
906
|
+
async handleUpdateRelationship(trx, operation) {
|
|
907
|
+
const input = operation.action.input;
|
|
908
|
+
await trx.updateTable("DocumentRelationship").set({
|
|
909
|
+
metadata: input.metadata,
|
|
910
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
911
|
+
}).where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).execute();
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
//#endregion
|
|
915
|
+
export { ReadModelCoordinator as a, KyselyDocumentView as i, ConsistencyTracker as n, BaseReadModel as o, makeConsistencyKey as r, KyselyDocumentIndexer as t };
|
|
916
|
+
|
|
917
|
+
//# sourceMappingURL=document-indexer-B2iLRB0o.js.map
|