@powerhousedao/reactor 6.2.3-dev.9 → 6.2.3-staging.0
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-BN30c2xw.js → build-worker-executor-BonPsKdx.js} +24 -8
- package/dist/build-worker-executor-BonPsKdx.js.map +1 -0
- package/dist/{document-indexer-CDX1D2RZ.js → document-indexer-I6i_YzsR.js} +866 -87
- package/dist/document-indexer-I6i_YzsR.js.map +1 -0
- package/dist/entry.d.ts +2 -1
- package/dist/entry.js +10 -6
- package/dist/entry.js.map +1 -1
- package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
- package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
- package/dist/{drive-container-types-aO-PASn7.js → forwarding-logger-BkwlvD9W.js} +1727 -306
- package/dist/forwarding-logger-BkwlvD9W.js.map +1 -0
- package/dist/index.d.ts +3946 -2817
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2557 -712
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +2 -1
- package/dist/projection-entry.js +62 -20
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-CJIdjsd8.js} +24 -4
- package/dist/projection-shard-manager-CJIdjsd8.js.map +1 -0
- package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
- package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
- package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
- package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
- package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
- package/dist/{worker-handle-CrERzl8s.js → worker-handle-DjbEQkq-.js} +22 -16
- package/dist/worker-handle-DjbEQkq-.js.map +1 -0
- package/package.json +20 -10
- package/dist/build-worker-executor-BN30c2xw.js.map +0 -1
- package/dist/document-indexer-CDX1D2RZ.js.map +0 -1
- package/dist/drive-container-types-aO-PASn7.js.map +0 -1
- package/dist/error-info-Cpu4OY3o.js +0 -62
- package/dist/error-info-Cpu4OY3o.js.map +0 -1
- package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
- package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
- package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
- package/dist/types-DMKLa0Ok.js +0 -33
- package/dist/types-DMKLa0Ok.js.map +0 -1
- package/dist/worker-Bb0u-X9m.js +0 -22
- package/dist/worker-Bb0u-X9m.js.map +0 -1
- package/dist/worker-handle-CrERzl8s.js.map +0 -1
|
@@ -1,17 +1,363 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as ReactorEventTypes } from "./types-DMKLa0Ok.js";
|
|
1
|
+
import { C as SettledWatermark, at as DocumentNotFoundError, mt as ReactorEventTypes, vt as yieldToMain, w as createKyselyWatermarkProbe } from "./forwarding-logger-BkwlvD9W.js";
|
|
3
2
|
import { createAuthState, isDenied } from "@powerhousedao/shared/document-model";
|
|
4
3
|
import { v4 } from "uuid";
|
|
5
4
|
import { childLogger } from "document-model";
|
|
5
|
+
//#region src/catch-up/scheduler.ts
|
|
6
|
+
const noHooks = {
|
|
7
|
+
onSwept: () => {},
|
|
8
|
+
describeSessions: () => Promise.resolve([])
|
|
9
|
+
};
|
|
10
|
+
function isCatchUpConsumer(value) {
|
|
11
|
+
if (typeof value !== "object" || value === null) return false;
|
|
12
|
+
const candidate = value;
|
|
13
|
+
return typeof candidate.sweep === "function" && typeof candidate.consumerId === "string" && typeof candidate.appliedThrough === "number";
|
|
14
|
+
}
|
|
15
|
+
/** Periodically sweeps every consumer up to the settled watermark. */
|
|
16
|
+
var CatchUpScheduler = class {
|
|
17
|
+
watermark;
|
|
18
|
+
operationIndex;
|
|
19
|
+
config;
|
|
20
|
+
logger;
|
|
21
|
+
hooks;
|
|
22
|
+
fixed = [];
|
|
23
|
+
sources = [];
|
|
24
|
+
sourced = /* @__PURE__ */ new Map();
|
|
25
|
+
timer;
|
|
26
|
+
running;
|
|
27
|
+
started = false;
|
|
28
|
+
stopped = false;
|
|
29
|
+
warnedStallSince;
|
|
30
|
+
statusSources = [];
|
|
31
|
+
constructor(watermark, operationIndex, config, logger, hooks = noHooks) {
|
|
32
|
+
this.watermark = watermark;
|
|
33
|
+
this.operationIndex = operationIndex;
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.logger = logger;
|
|
36
|
+
this.hooks = hooks;
|
|
37
|
+
}
|
|
38
|
+
get settledWatermark() {
|
|
39
|
+
return this.watermark;
|
|
40
|
+
}
|
|
41
|
+
addConsumer(consumer, thread) {
|
|
42
|
+
if (this.fixed.some((entry) => entry.consumer === consumer)) return;
|
|
43
|
+
this.fixed.push({
|
|
44
|
+
consumer,
|
|
45
|
+
thread,
|
|
46
|
+
lastAdvanceUtcMs: Date.now(),
|
|
47
|
+
warnedHeld: false
|
|
48
|
+
});
|
|
49
|
+
this.ensureTimer();
|
|
50
|
+
}
|
|
51
|
+
/** Consumers read on every tick, so models registered later are swept too. */
|
|
52
|
+
addSource(consumers, thread) {
|
|
53
|
+
this.sources.push({
|
|
54
|
+
consumers,
|
|
55
|
+
thread
|
|
56
|
+
});
|
|
57
|
+
this.ensureTimer();
|
|
58
|
+
}
|
|
59
|
+
/** Consumers another thread sweeps, reported in status(). */
|
|
60
|
+
addStatusSource(source) {
|
|
61
|
+
this.statusSources.push(source);
|
|
62
|
+
}
|
|
63
|
+
start() {
|
|
64
|
+
this.started = true;
|
|
65
|
+
this.ensureTimer();
|
|
66
|
+
}
|
|
67
|
+
async stop() {
|
|
68
|
+
this.stopped = true;
|
|
69
|
+
if (this.timer !== void 0) {
|
|
70
|
+
clearInterval(this.timer);
|
|
71
|
+
this.timer = void 0;
|
|
72
|
+
}
|
|
73
|
+
if (this.running !== void 0) await this.running.catch(() => []);
|
|
74
|
+
}
|
|
75
|
+
async sweepNow() {
|
|
76
|
+
if (this.running !== void 0) await this.running.catch(() => []);
|
|
77
|
+
return this.run();
|
|
78
|
+
}
|
|
79
|
+
status() {
|
|
80
|
+
return {
|
|
81
|
+
watermark: this.watermark.status(),
|
|
82
|
+
consumers: this.entries().map((entry) => ({
|
|
83
|
+
consumerId: entry.consumer.consumerId,
|
|
84
|
+
thread: entry.thread,
|
|
85
|
+
appliedThrough: entry.consumer.appliedThrough,
|
|
86
|
+
trackedAbove: entry.consumer.trackedAbove,
|
|
87
|
+
...entry.blockedAt !== void 0 ? { blockedAt: entry.blockedAt } : {},
|
|
88
|
+
lastAdvanceUtcMs: entry.lastAdvanceUtcMs
|
|
89
|
+
})).concat(this.statusSources.flatMap((source) => source()))
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
ensureTimer() {
|
|
93
|
+
if (!this.started || this.stopped || this.timer !== void 0) return;
|
|
94
|
+
if (this.fixed.length === 0 && this.sources.length === 0) return;
|
|
95
|
+
const timer = setInterval(() => {
|
|
96
|
+
if (this.running !== void 0) return;
|
|
97
|
+
this.run().catch((error) => {
|
|
98
|
+
this.logger.error("catch-up tick failed: @error", error);
|
|
99
|
+
});
|
|
100
|
+
}, this.config.intervalMs);
|
|
101
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
102
|
+
this.timer = timer;
|
|
103
|
+
}
|
|
104
|
+
run() {
|
|
105
|
+
const run = this.tick().finally(() => {
|
|
106
|
+
if (this.running === run) this.running = void 0;
|
|
107
|
+
});
|
|
108
|
+
this.running = run;
|
|
109
|
+
return run;
|
|
110
|
+
}
|
|
111
|
+
async tick() {
|
|
112
|
+
const settled = await this.watermark.refresh();
|
|
113
|
+
await this.warnIfWatermarkHeld();
|
|
114
|
+
const entries = this.entries();
|
|
115
|
+
if (entries.length === 0) return [];
|
|
116
|
+
let lowest = Number.POSITIVE_INFINITY;
|
|
117
|
+
for (const entry of entries) lowest = Math.min(lowest, entry.consumer.appliedThrough);
|
|
118
|
+
const shared = await this.readPage(lowest, settled);
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (this.stopped) break;
|
|
122
|
+
const result = await this.sweepOne(entry, shared, settled);
|
|
123
|
+
if (result !== void 0) results.push(result);
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
entries() {
|
|
128
|
+
const entries = [...this.fixed];
|
|
129
|
+
const seen = new Set(entries.map((entry) => entry.consumer));
|
|
130
|
+
for (const source of this.sources) for (const consumer of source.consumers()) {
|
|
131
|
+
if (seen.has(consumer)) continue;
|
|
132
|
+
seen.add(consumer);
|
|
133
|
+
let entry = this.sourced.get(consumer);
|
|
134
|
+
if (entry === void 0) {
|
|
135
|
+
entry = {
|
|
136
|
+
consumer,
|
|
137
|
+
thread: source.thread,
|
|
138
|
+
lastAdvanceUtcMs: Date.now(),
|
|
139
|
+
warnedHeld: false
|
|
140
|
+
};
|
|
141
|
+
this.sourced.set(consumer, entry);
|
|
142
|
+
}
|
|
143
|
+
entries.push(entry);
|
|
144
|
+
}
|
|
145
|
+
return entries;
|
|
146
|
+
}
|
|
147
|
+
async sweepOne(entry, shared, settled) {
|
|
148
|
+
const applied = entry.consumer.appliedThrough;
|
|
149
|
+
let page = shared;
|
|
150
|
+
if (applied >= shared.bound && shared.bound < settled) try {
|
|
151
|
+
page = await this.readPage(applied, settled);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.logger.error("@consumer catch-up could not read present ordinals: @error", entry.consumer.consumerId, error);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let result;
|
|
157
|
+
try {
|
|
158
|
+
result = await entry.consumer.sweep(page.bound, page.present);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
this.logger.error("@consumer catch-up sweep failed: @error", entry.consumer.consumerId, error);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (result.to > result.from) {
|
|
164
|
+
entry.lastAdvanceUtcMs = Date.now();
|
|
165
|
+
entry.warnedHeld = false;
|
|
166
|
+
}
|
|
167
|
+
entry.blockedAt = result.blockedAt;
|
|
168
|
+
this.warnIfConsumerHeld(entry, settled);
|
|
169
|
+
if (result.to > result.from || result.replayed > 0 || result.blockedAt !== void 0) this.hooks.onSwept(result, entry.thread);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
async warnIfWatermarkHeld() {
|
|
173
|
+
const status = this.watermark.status();
|
|
174
|
+
const since = status.stalledSinceUtcMs;
|
|
175
|
+
if (since === void 0) {
|
|
176
|
+
this.warnedStallSince = void 0;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const heldMs = Date.now() - since;
|
|
180
|
+
if (heldMs < this.config.stuckWarnMs || this.warnedStallSince === since) return;
|
|
181
|
+
this.warnedStallSince = since;
|
|
182
|
+
const sessions = await this.hooks.describeSessions(status.waitingOn).catch(() => []);
|
|
183
|
+
this.logger.warn("settled watermark held at @settled (head @head) for @ms ms, waiting on xid @xids: @sessions", status.settledThrough, status.head, heldMs, status.waitingOn.join(", "), sessions.map((session) => `pid ${session.pid} ${session.applicationName} ${session.state} since ${session.xactStart ?? "?"}`).join("; "));
|
|
184
|
+
}
|
|
185
|
+
warnIfConsumerHeld(entry, settled) {
|
|
186
|
+
const applied = entry.consumer.appliedThrough;
|
|
187
|
+
if (applied >= settled || entry.warnedHeld) return;
|
|
188
|
+
const heldMs = Date.now() - entry.lastAdvanceUtcMs;
|
|
189
|
+
if (heldMs < this.config.stuckWarnMs) return;
|
|
190
|
+
entry.warnedHeld = true;
|
|
191
|
+
this.logger.warn("@consumer cursor held at @applied for @ms ms (settled @settled)", entry.consumer.consumerId, applied, heldMs, settled);
|
|
192
|
+
}
|
|
193
|
+
async readPage(after, settled) {
|
|
194
|
+
const limit = this.config.sweepPageSize;
|
|
195
|
+
const present = await this.operationIndex.getOrdinalsInRange(after, settled, limit);
|
|
196
|
+
return {
|
|
197
|
+
bound: present.length >= limit ? present[present.length - 1] : settled,
|
|
198
|
+
present
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/catch-up/types.ts
|
|
204
|
+
const defaultCatchUpConfig = {
|
|
205
|
+
intervalMs: 2e3,
|
|
206
|
+
stuckWarnMs: 6e4,
|
|
207
|
+
maxTrackedAboveCursor: 1e5,
|
|
208
|
+
sweepPageSize: 1e4
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/catch-up/contiguous-cursor.ts
|
|
212
|
+
/** In memory, per consumer; tracks ordinals above appliedThrough only. */
|
|
213
|
+
var ContiguousCursor = class {
|
|
214
|
+
through;
|
|
215
|
+
limit;
|
|
216
|
+
claimed = /* @__PURE__ */ new Set();
|
|
217
|
+
applied = /* @__PURE__ */ new Set();
|
|
218
|
+
constructor(through, limit) {
|
|
219
|
+
this.through = through;
|
|
220
|
+
this.limit = limit;
|
|
221
|
+
}
|
|
222
|
+
setLimit(limit) {
|
|
223
|
+
this.limit = limit;
|
|
224
|
+
}
|
|
225
|
+
/** Every present ordinal at or below this has been applied. */
|
|
226
|
+
get appliedThrough() {
|
|
227
|
+
return this.through;
|
|
228
|
+
}
|
|
229
|
+
get trackedAbove() {
|
|
230
|
+
return this.claimed.size + this.applied.size;
|
|
231
|
+
}
|
|
232
|
+
/** Ordinals the caller must apply; the rest are already covered. */
|
|
233
|
+
claim(ordinals) {
|
|
234
|
+
const mine = /* @__PURE__ */ new Set();
|
|
235
|
+
for (const ordinal of ordinals) {
|
|
236
|
+
if (ordinal <= this.through) continue;
|
|
237
|
+
if (this.claimed.has(ordinal) || this.applied.has(ordinal)) continue;
|
|
238
|
+
this.claimed.add(ordinal);
|
|
239
|
+
mine.add(ordinal);
|
|
240
|
+
}
|
|
241
|
+
return mine;
|
|
242
|
+
}
|
|
243
|
+
/** Releases claims; committed ones join the applied set. */
|
|
244
|
+
settle(ordinals, committed) {
|
|
245
|
+
for (const ordinal of ordinals) {
|
|
246
|
+
if (!this.claimed.delete(ordinal)) continue;
|
|
247
|
+
if (committed && ordinal > this.through) this.applied.add(ordinal);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Past the limit, clears the applied set; the next sweep re-applies it. */
|
|
251
|
+
enforceLimit() {
|
|
252
|
+
if (this.claimed.size + this.applied.size > this.limit) this.applied.clear();
|
|
253
|
+
}
|
|
254
|
+
/** Present ordinals above the cursor that no path has claimed or applied. */
|
|
255
|
+
missing(present) {
|
|
256
|
+
return present.filter((ordinal) => ordinal > this.through && !this.claimed.has(ordinal) && !this.applied.has(ordinal));
|
|
257
|
+
}
|
|
258
|
+
/** `settled`, held below the lowest claimed or unapplied ordinal. */
|
|
259
|
+
target(settled, present) {
|
|
260
|
+
let to = settled;
|
|
261
|
+
for (const ordinal of this.claimed) if (ordinal <= to) to = ordinal - 1;
|
|
262
|
+
for (const ordinal of present) {
|
|
263
|
+
if (ordinal <= this.through || ordinal > to) continue;
|
|
264
|
+
if (!this.applied.has(ordinal)) to = ordinal - 1;
|
|
265
|
+
}
|
|
266
|
+
return Math.max(to, this.through);
|
|
267
|
+
}
|
|
268
|
+
advance(to) {
|
|
269
|
+
if (to <= this.through) return;
|
|
270
|
+
this.through = to;
|
|
271
|
+
for (const ordinal of this.applied) if (ordinal <= to) this.applied.delete(ordinal);
|
|
272
|
+
for (const ordinal of this.claimed) if (ordinal <= to) this.claimed.delete(ordinal);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
6
276
|
//#region src/read-models/base-read-model.ts
|
|
277
|
+
/** Small enough that a chunk's transaction rarely outlasts the yield deadline. */
|
|
278
|
+
const DEFAULT_COMMIT_CHUNK_SIZE = 50;
|
|
279
|
+
/** Matches the executor's own default, so both paths yield on the same cadence. */
|
|
280
|
+
const DEFAULT_READ_MODEL_YIELD_DEADLINE_MS = 50;
|
|
281
|
+
const defaultReadModelIndexingConfig = {
|
|
282
|
+
commitChunkSize: 50,
|
|
283
|
+
yieldDeadlineMs: 50
|
|
284
|
+
};
|
|
285
|
+
/** For read models whose callers can observe where a batch was split. */
|
|
286
|
+
const unchunkedReadModelIndexingConfig = {
|
|
287
|
+
commitChunkSize: Number.MAX_SAFE_INTEGER,
|
|
288
|
+
yieldDeadlineMs: 50
|
|
289
|
+
};
|
|
7
290
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Subclasses override commitOperations() with their specific domain logic.
|
|
291
|
+
* Keeps the chunk size at one operation or more: a chunk of zero or less never
|
|
292
|
+
* advances the indexing loop, so the pass would spin without ever resolving.
|
|
11
293
|
*/
|
|
294
|
+
function normalizeIndexingConfig(config) {
|
|
295
|
+
if (Number.isNaN(config.commitChunkSize)) return {
|
|
296
|
+
...config,
|
|
297
|
+
commitChunkSize: 50
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
...config,
|
|
301
|
+
commitChunkSize: Math.max(1, Math.min(Math.floor(config.commitChunkSize), Number.MAX_SAFE_INTEGER))
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function ordinalOf(item) {
|
|
305
|
+
return item.context.ordinal;
|
|
306
|
+
}
|
|
307
|
+
function isTracked(ordinal) {
|
|
308
|
+
return Number.isFinite(ordinal) && ordinal > 0;
|
|
309
|
+
}
|
|
310
|
+
function streamKeyOf(stream) {
|
|
311
|
+
return `${stream.documentId}\u0000${stream.scope}\u0000${stream.branch}`;
|
|
312
|
+
}
|
|
313
|
+
function groupByStream(items) {
|
|
314
|
+
const groups = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const item of items) {
|
|
316
|
+
const { documentId, scope, branch } = item.context;
|
|
317
|
+
const key = streamKeyOf(item.context);
|
|
318
|
+
let group = groups.get(key);
|
|
319
|
+
if (group === void 0) {
|
|
320
|
+
group = {
|
|
321
|
+
documentId,
|
|
322
|
+
scope,
|
|
323
|
+
branch,
|
|
324
|
+
lowest: ordinalOf(item),
|
|
325
|
+
late: []
|
|
326
|
+
};
|
|
327
|
+
groups.set(key, group);
|
|
328
|
+
}
|
|
329
|
+
group.lowest = Math.min(group.lowest, ordinalOf(item));
|
|
330
|
+
group.late.push(item);
|
|
331
|
+
}
|
|
332
|
+
return [...groups.values()].sort((a, b) => a.lowest - b.lowest);
|
|
333
|
+
}
|
|
334
|
+
function mergeByOrdinal(...lists) {
|
|
335
|
+
const byOrdinal = /* @__PURE__ */ new Map();
|
|
336
|
+
for (const list of lists) for (const item of list) byOrdinal.set(ordinalOf(item), item);
|
|
337
|
+
return [...byOrdinal.values()].sort((a, b) => ordinalOf(a) - ordinalOf(b));
|
|
338
|
+
}
|
|
339
|
+
function errorMessage(error) {
|
|
340
|
+
return error instanceof Error ? error.message : String(error);
|
|
341
|
+
}
|
|
342
|
+
/** A read model whose cursor only sweeps and boot replay advance. */
|
|
12
343
|
var BaseReadModel = class {
|
|
13
|
-
|
|
344
|
+
db;
|
|
345
|
+
operationIndex;
|
|
346
|
+
writeCache;
|
|
347
|
+
consistencyTracker;
|
|
348
|
+
config;
|
|
14
349
|
name;
|
|
350
|
+
indexing;
|
|
351
|
+
catchUpLogger;
|
|
352
|
+
cursor;
|
|
353
|
+
persisted = 0;
|
|
354
|
+
watermark;
|
|
355
|
+
maxTrackedAboveCursor = defaultCatchUpConfig.maxTrackedAboveCursor;
|
|
356
|
+
loggedFailure;
|
|
357
|
+
failedItem;
|
|
358
|
+
initialized = false;
|
|
359
|
+
liveInFlight = /* @__PURE__ */ new Map();
|
|
360
|
+
sweptListeners = /* @__PURE__ */ new Set();
|
|
15
361
|
constructor(db, operationIndex, writeCache, consistencyTracker, config) {
|
|
16
362
|
this.db = db;
|
|
17
363
|
this.operationIndex = operationIndex;
|
|
@@ -19,33 +365,131 @@ var BaseReadModel = class {
|
|
|
19
365
|
this.consistencyTracker = consistencyTracker;
|
|
20
366
|
this.config = config;
|
|
21
367
|
this.name = config.readModelId;
|
|
368
|
+
this.catchUpLogger = childLogger([
|
|
369
|
+
"reactor",
|
|
370
|
+
"read-model",
|
|
371
|
+
config.readModelId
|
|
372
|
+
]);
|
|
373
|
+
this.indexing = normalizeIndexingConfig(config.indexing ?? defaultReadModelIndexingConfig);
|
|
374
|
+
this.cursor = new ContiguousCursor(0, this.maxTrackedAboveCursor);
|
|
22
375
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
376
|
+
get consumerId() {
|
|
377
|
+
return this.config.readModelId;
|
|
378
|
+
}
|
|
379
|
+
get appliedThrough() {
|
|
380
|
+
return this.cursor.appliedThrough;
|
|
381
|
+
}
|
|
382
|
+
get trackedAbove() {
|
|
383
|
+
return this.cursor.trackedAbove;
|
|
384
|
+
}
|
|
385
|
+
/** Shares a thread's watermark; otherwise boot probes through this db. */
|
|
386
|
+
attachCatchUp(watermark, maxTrackedAboveCursor) {
|
|
387
|
+
this.watermark = watermark;
|
|
388
|
+
this.maxTrackedAboveCursor = maxTrackedAboveCursor;
|
|
389
|
+
this.cursor.setLimit(maxTrackedAboveCursor);
|
|
390
|
+
}
|
|
391
|
+
/** Notified with the coordinates each sweep applied. */
|
|
392
|
+
onSwept(listener) {
|
|
393
|
+
this.sweptListeners.add(listener);
|
|
394
|
+
return () => {
|
|
395
|
+
this.sweptListeners.delete(listener);
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/** A chunk that throws ends the replay; sweeps continue from below it. */
|
|
26
399
|
async init() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
400
|
+
if (this.initialized) {
|
|
401
|
+
await this.replayFromCursor();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
let stored = await this.loadState();
|
|
405
|
+
if (stored === void 0) {
|
|
406
|
+
if (this.config.startFrom === "head") {
|
|
407
|
+
const watermark = this.settledWatermark();
|
|
408
|
+
const settled = await watermark.refresh();
|
|
409
|
+
const head = Math.max(settled, watermark.status().head);
|
|
410
|
+
await this.initializeState(head);
|
|
411
|
+
this.resetCursor(head);
|
|
412
|
+
this.initialized = true;
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
await this.initializeState(0);
|
|
416
|
+
stored = 0;
|
|
36
417
|
}
|
|
418
|
+
this.resetCursor(stored);
|
|
419
|
+
this.initialized = true;
|
|
420
|
+
await this.replayFromCursor();
|
|
37
421
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
422
|
+
/** The live path: never moves the cursor. */
|
|
423
|
+
indexOperations(items) {
|
|
424
|
+
if (items.length === 0) return Promise.resolve();
|
|
425
|
+
return this.reserveOperations(items).apply();
|
|
426
|
+
}
|
|
427
|
+
/** Holds a queued batch as live, so a sweep neither takes nor passes it. */
|
|
428
|
+
reserveOperations(items) {
|
|
429
|
+
const cursor = this.cursor;
|
|
430
|
+
const owned = this.claimLive(items);
|
|
431
|
+
const unmark = this.markLive(owned);
|
|
432
|
+
let done = false;
|
|
433
|
+
return {
|
|
434
|
+
apply: () => {
|
|
435
|
+
if (done) return Promise.resolve();
|
|
436
|
+
done = true;
|
|
437
|
+
if (cursor !== this.cursor) {
|
|
438
|
+
unmark();
|
|
439
|
+
return this.indexOperations(items);
|
|
440
|
+
}
|
|
441
|
+
if (owned.length === 0) {
|
|
442
|
+
unmark();
|
|
443
|
+
return Promise.resolve();
|
|
444
|
+
}
|
|
445
|
+
return this.applyChunked(owned, unmark);
|
|
446
|
+
},
|
|
447
|
+
release: () => {
|
|
448
|
+
if (done) return;
|
|
449
|
+
done = true;
|
|
450
|
+
cursor.settle(owned.map(ordinalOf), false);
|
|
451
|
+
unmark();
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async sweep(settledThrough, present, signal) {
|
|
456
|
+
const startedAt = performance.now();
|
|
457
|
+
const from = this.cursor.appliedThrough;
|
|
458
|
+
const range = present.filter((ordinal) => ordinal > from && ordinal <= settledThrough);
|
|
459
|
+
const mine = this.cursor.claim(this.cursor.missing(range));
|
|
460
|
+
let replayed = 0;
|
|
461
|
+
let reapplied = 0;
|
|
462
|
+
let blockedAt;
|
|
463
|
+
if (mine.size > 0) {
|
|
464
|
+
let late;
|
|
465
|
+
try {
|
|
466
|
+
late = await this.operationIndex.getByOrdinals([...mine], signal);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
this.cursor.settle(mine, false);
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
const found = new Set(late.map(ordinalOf));
|
|
472
|
+
this.cursor.settle([...mine].filter((ordinal) => !found.has(ordinal)), true);
|
|
473
|
+
for (const group of groupByStream(late)) {
|
|
474
|
+
const outcome = await this.sweepStream(group, signal);
|
|
475
|
+
replayed += outcome.replayed;
|
|
476
|
+
reapplied += outcome.reapplied;
|
|
477
|
+
blockedAt ??= outcome.blockedAt;
|
|
478
|
+
}
|
|
479
|
+
if (replayed > 0) this.catchUpLogger.warn("@consumer applied @n operations its live path never received: ordinals @first..@last", this.consumerId, replayed, Math.min(...found), Math.max(...found));
|
|
480
|
+
}
|
|
481
|
+
const to = this.cursor.target(settledThrough, range);
|
|
482
|
+
if (to > from) await this.moveCursor(to);
|
|
483
|
+
this.cursor.enforceLimit();
|
|
484
|
+
return {
|
|
485
|
+
consumerId: this.consumerId,
|
|
486
|
+
from,
|
|
487
|
+
to: this.cursor.appliedThrough,
|
|
488
|
+
durationMs: performance.now() - startedAt,
|
|
489
|
+
replayed,
|
|
490
|
+
reapplied,
|
|
491
|
+
...blockedAt !== void 0 ? { blockedAt } : {}
|
|
492
|
+
};
|
|
49
493
|
}
|
|
50
494
|
/**
|
|
51
495
|
* Waits for the read model to reach the specified consistency level.
|
|
@@ -54,10 +498,14 @@ var BaseReadModel = class {
|
|
|
54
498
|
if (token.coordinates.length === 0) return;
|
|
55
499
|
await this.consistencyTracker.waitFor(token.coordinates, timeoutMs, signal);
|
|
56
500
|
}
|
|
501
|
+
/** The cursor, for subclasses: every present ordinal at or below it is applied. */
|
|
502
|
+
get lastOrdinal() {
|
|
503
|
+
return this.cursor.appliedThrough;
|
|
504
|
+
}
|
|
505
|
+
/** Called after the cursor advances. */
|
|
506
|
+
onCursorAdvanced(appliedThrough) {}
|
|
57
507
|
async commitOperations(items) {}
|
|
58
|
-
/**
|
|
59
|
-
* Rebuilds document state for each operation using the write cache.
|
|
60
|
-
*/
|
|
508
|
+
/** Rebuilds resultingState as the executor writes it: scopes plus header. */
|
|
61
509
|
async rebuildStateForOperations(operations) {
|
|
62
510
|
const result = [];
|
|
63
511
|
for (const op of operations) {
|
|
@@ -68,7 +516,10 @@ var BaseReadModel = class {
|
|
|
68
516
|
operation: op.operation,
|
|
69
517
|
context: {
|
|
70
518
|
...op.context,
|
|
71
|
-
resultingState: JSON.stringify(
|
|
519
|
+
resultingState: JSON.stringify({
|
|
520
|
+
...document.state,
|
|
521
|
+
header: document.header
|
|
522
|
+
})
|
|
72
523
|
}
|
|
73
524
|
});
|
|
74
525
|
}
|
|
@@ -84,28 +535,19 @@ var BaseReadModel = class {
|
|
|
84
535
|
/**
|
|
85
536
|
* Initializes the ViewState row for this read model.
|
|
86
537
|
*/
|
|
87
|
-
async initializeState() {
|
|
538
|
+
async initializeState(lastOrdinal = 0) {
|
|
88
539
|
await this.db.insertInto("ViewState").values({
|
|
89
540
|
readModelId: this.config.readModelId,
|
|
90
|
-
lastOrdinal
|
|
91
|
-
}).execute();
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Saves the last processed ordinal to the ViewState table.
|
|
95
|
-
*/
|
|
96
|
-
async saveState(trx, items) {
|
|
97
|
-
let maxOrdinal = 0;
|
|
98
|
-
for (const item of items) maxOrdinal = Math.max(maxOrdinal, item.context.ordinal);
|
|
99
|
-
this.lastOrdinal = maxOrdinal;
|
|
100
|
-
await trx.updateTable("ViewState").set({
|
|
101
|
-
lastOrdinal: maxOrdinal,
|
|
102
|
-
lastOperationTimestamp: /* @__PURE__ */ new Date()
|
|
103
|
-
}).where("readModelId", "=", this.config.readModelId).execute();
|
|
541
|
+
lastOrdinal
|
|
542
|
+
}).onConflict((oc) => oc.column("readModelId").doNothing()).execute();
|
|
104
543
|
}
|
|
105
544
|
/**
|
|
106
545
|
* Updates the consistency tracker with the processed operations.
|
|
107
546
|
*/
|
|
108
547
|
updateConsistencyTracker(items) {
|
|
548
|
+
this.consistencyTracker.update(this.coordinatesOf(items));
|
|
549
|
+
}
|
|
550
|
+
coordinatesOf(items) {
|
|
109
551
|
const coordinates = [];
|
|
110
552
|
for (let i = 0; i < items.length; i++) {
|
|
111
553
|
const item = items[i];
|
|
@@ -116,11 +558,297 @@ var BaseReadModel = class {
|
|
|
116
558
|
operationIndex: item.operation.index
|
|
117
559
|
});
|
|
118
560
|
}
|
|
119
|
-
|
|
561
|
+
return coordinates;
|
|
562
|
+
}
|
|
563
|
+
settledWatermark() {
|
|
564
|
+
this.watermark ??= new SettledWatermark(createKyselyWatermarkProbe(this.db), this.catchUpLogger);
|
|
565
|
+
return this.watermark;
|
|
566
|
+
}
|
|
567
|
+
resetCursor(appliedThrough) {
|
|
568
|
+
this.cursor = new ContiguousCursor(appliedThrough, this.maxTrackedAboveCursor);
|
|
569
|
+
this.persisted = appliedThrough;
|
|
570
|
+
}
|
|
571
|
+
/** Items this pass must apply; untracked ordinals always apply. */
|
|
572
|
+
claimLive(items) {
|
|
573
|
+
const mine = this.cursor.claim(items.map(ordinalOf));
|
|
574
|
+
return items.filter((item) => {
|
|
575
|
+
const ordinal = ordinalOf(item);
|
|
576
|
+
return !isTracked(ordinal) || mine.has(ordinal);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** Marks items in flight on their streams; returns the release. */
|
|
580
|
+
markLive(items) {
|
|
581
|
+
const keys = /* @__PURE__ */ new Set();
|
|
582
|
+
for (const item of items) {
|
|
583
|
+
if (!isTracked(ordinalOf(item))) continue;
|
|
584
|
+
const key = streamKeyOf(item.context);
|
|
585
|
+
let ordinals = this.liveInFlight.get(key);
|
|
586
|
+
if (ordinals === void 0) {
|
|
587
|
+
ordinals = /* @__PURE__ */ new Set();
|
|
588
|
+
this.liveInFlight.set(key, ordinals);
|
|
589
|
+
}
|
|
590
|
+
ordinals.add(ordinalOf(item));
|
|
591
|
+
keys.add(key);
|
|
592
|
+
}
|
|
593
|
+
return () => {
|
|
594
|
+
for (const item of items) this.liveInFlight.get(streamKeyOf(item.context))?.delete(ordinalOf(item));
|
|
595
|
+
for (const key of keys) if (this.liveInFlight.get(key)?.size === 0) this.liveInFlight.delete(key);
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/** True while a live pass applies an earlier operation of the stream. */
|
|
599
|
+
liveBelow(group) {
|
|
600
|
+
const ordinals = this.liveInFlight.get(streamKeyOf(group));
|
|
601
|
+
if (ordinals === void 0) return false;
|
|
602
|
+
for (const ordinal of ordinals) if (ordinal < group.lowest) return true;
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
/** Commits in chunks, yielding with no transaction open. */
|
|
606
|
+
async applyChunked(items, release = this.markLive(items)) {
|
|
607
|
+
try {
|
|
608
|
+
const { commitChunkSize, yieldDeadlineMs } = this.indexing;
|
|
609
|
+
let lastYield = performance.now();
|
|
610
|
+
let committed = 0;
|
|
611
|
+
for (let start = 0; start < items.length; start += commitChunkSize) {
|
|
612
|
+
if (start > 0 && performance.now() - lastYield > yieldDeadlineMs) {
|
|
613
|
+
await yieldToMain();
|
|
614
|
+
lastYield = performance.now();
|
|
615
|
+
}
|
|
616
|
+
const chunk = items.slice(start, start + commitChunkSize);
|
|
617
|
+
try {
|
|
618
|
+
await this.commitOperations(chunk);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
this.failedItem = chunk[0];
|
|
621
|
+
const prefix = items.slice(0, committed);
|
|
622
|
+
this.cursor.settle(prefix.map(ordinalOf), true);
|
|
623
|
+
this.cursor.settle(items.slice(committed).map(ordinalOf), false);
|
|
624
|
+
if (prefix.length > 0) this.updateConsistencyTracker(prefix);
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
committed += chunk.length;
|
|
628
|
+
}
|
|
629
|
+
this.cursor.settle(items.map(ordinalOf), true);
|
|
630
|
+
this.cursor.enforceLimit();
|
|
631
|
+
this.updateConsistencyTracker(items);
|
|
632
|
+
} finally {
|
|
633
|
+
release();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async replayFromCursor() {
|
|
637
|
+
const settledAtBoot = await this.settledAtBoot();
|
|
638
|
+
let page = await this.operationIndex.getSinceOrdinal(this.cursor.appliedThrough);
|
|
639
|
+
while (page.results.length > 0) {
|
|
640
|
+
const ordinals = page.results.map(ordinalOf);
|
|
641
|
+
const failed = await this.replayPage(page.results);
|
|
642
|
+
const pageMax = Math.max(...ordinals);
|
|
643
|
+
const to = this.cursor.target(Math.min(settledAtBoot, pageMax), ordinals);
|
|
644
|
+
if (to > this.cursor.appliedThrough) await this.moveCursor(to);
|
|
645
|
+
if (failed || !page.next) break;
|
|
646
|
+
page = await page.next();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/** Without a probe, boot replay applies but leaves the cursor to sweeps. */
|
|
650
|
+
async settledAtBoot() {
|
|
651
|
+
try {
|
|
652
|
+
return await this.settledWatermark().refresh();
|
|
653
|
+
} catch (error) {
|
|
654
|
+
this.catchUpLogger.warn("@consumer could not probe the settled watermark at boot; the cursor waits for a sweep: @error", this.consumerId, error);
|
|
655
|
+
return this.cursor.appliedThrough;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/** Returns true when a chunk failed, which ends the replay. */
|
|
659
|
+
async replayPage(results) {
|
|
660
|
+
const owned = this.claimLive(results);
|
|
661
|
+
if (owned.length === 0) return false;
|
|
662
|
+
const release = this.markLive(owned);
|
|
663
|
+
try {
|
|
664
|
+
return await this.replayOwned(owned);
|
|
665
|
+
} finally {
|
|
666
|
+
release();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async replayOwned(owned) {
|
|
670
|
+
let rebuilt;
|
|
671
|
+
try {
|
|
672
|
+
rebuilt = await this.rebuildIfConfigured(owned);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
this.cursor.settle(owned.map(ordinalOf), false);
|
|
675
|
+
this.block(owned[0], error);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
this.cursor.settle(rebuilt.absent, true);
|
|
679
|
+
if (rebuilt.items.length === 0) return false;
|
|
680
|
+
try {
|
|
681
|
+
await this.applyChunked(rebuilt.items);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
this.block(this.failedItem ?? rebuilt.items[0], error);
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
async sweepStream(group, signal) {
|
|
689
|
+
const owned = group.late.map(ordinalOf);
|
|
690
|
+
if (this.liveBelow(group)) {
|
|
691
|
+
this.cursor.settle(owned, false);
|
|
692
|
+
return {
|
|
693
|
+
replayed: 0,
|
|
694
|
+
reapplied: 0
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const ownedSet = new Set(owned);
|
|
698
|
+
let items = group.late;
|
|
699
|
+
if (this.config.replayStreamSuffix ?? true) {
|
|
700
|
+
let suffix;
|
|
701
|
+
try {
|
|
702
|
+
suffix = await this.operationIndex.getStreamAfter(group, group.lowest, signal);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
this.cursor.settle(owned, false);
|
|
705
|
+
return {
|
|
706
|
+
replayed: 0,
|
|
707
|
+
reapplied: 0,
|
|
708
|
+
blockedAt: this.block(group.late[0], error)
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
items = mergeByOrdinal(group.late, suffix);
|
|
712
|
+
}
|
|
713
|
+
let rebuilt;
|
|
714
|
+
try {
|
|
715
|
+
rebuilt = await this.rebuildIfConfigured(items);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
this.cursor.settle(owned, false);
|
|
718
|
+
return {
|
|
719
|
+
replayed: 0,
|
|
720
|
+
reapplied: 0,
|
|
721
|
+
blockedAt: this.block(items[0], error)
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
await this.commitOperations(rebuilt.items);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
this.cursor.settle(owned, false);
|
|
728
|
+
return {
|
|
729
|
+
replayed: 0,
|
|
730
|
+
reapplied: 0,
|
|
731
|
+
blockedAt: this.block(rebuilt.items[0] ?? items[0], error)
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
this.cursor.settle(owned, true);
|
|
735
|
+
this.updateConsistencyTracker(rebuilt.items);
|
|
736
|
+
this.notifySwept(rebuilt.items);
|
|
737
|
+
let replayed = 0;
|
|
738
|
+
for (const item of rebuilt.items) if (ownedSet.has(ordinalOf(item))) replayed++;
|
|
739
|
+
return {
|
|
740
|
+
replayed,
|
|
741
|
+
reapplied: rebuilt.items.length - replayed
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/** A document gone from the write cache is absent, not a failure. */
|
|
745
|
+
async rebuildIfConfigured(items) {
|
|
746
|
+
if (!this.config.rebuildStateOnInit) return {
|
|
747
|
+
items,
|
|
748
|
+
absent: []
|
|
749
|
+
};
|
|
750
|
+
const rebuilt = [];
|
|
751
|
+
const absent = [];
|
|
752
|
+
for (const item of items) {
|
|
753
|
+
let result;
|
|
754
|
+
try {
|
|
755
|
+
result = await this.rebuildStateForOperations([item]);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (!(error instanceof DocumentNotFoundError)) throw error;
|
|
758
|
+
this.catchUpLogger.warn("@consumer dropped ordinal @ordinal: document @documentId is gone", this.consumerId, ordinalOf(item), item.context.documentId);
|
|
759
|
+
absent.push(ordinalOf(item));
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
rebuilt.push(...result);
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
items: rebuilt,
|
|
766
|
+
absent
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
block(item, error) {
|
|
770
|
+
const blockedAt = {
|
|
771
|
+
ordinal: ordinalOf(item),
|
|
772
|
+
documentId: item.context.documentId,
|
|
773
|
+
scope: item.context.scope,
|
|
774
|
+
branch: item.context.branch,
|
|
775
|
+
type: item.operation.action.type,
|
|
776
|
+
error: errorMessage(error)
|
|
777
|
+
};
|
|
778
|
+
if (this.loggedFailure !== blockedAt.ordinal) {
|
|
779
|
+
this.loggedFailure = blockedAt.ordinal;
|
|
780
|
+
this.catchUpLogger.error("@consumer cursor held at @applied: ordinal @ordinal (@documentId/@scope/@branch, @type) failed: @error", this.consumerId, this.cursor.appliedThrough, blockedAt.ordinal, blockedAt.documentId, blockedAt.scope, blockedAt.branch, blockedAt.type, blockedAt.error);
|
|
781
|
+
}
|
|
782
|
+
return blockedAt;
|
|
783
|
+
}
|
|
784
|
+
notifySwept(items) {
|
|
785
|
+
if (this.sweptListeners.size === 0 || items.length === 0) return;
|
|
786
|
+
const coordinates = this.coordinatesOf(items);
|
|
787
|
+
for (const listener of this.sweptListeners) listener(coordinates);
|
|
788
|
+
}
|
|
789
|
+
/** Compare-and-set against the value this process last wrote. */
|
|
790
|
+
async moveCursor(to) {
|
|
791
|
+
if (to <= this.persisted) {
|
|
792
|
+
this.advanceCursor(to);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const expected = this.persisted;
|
|
796
|
+
const result = await this.db.updateTable("ViewState").set({
|
|
797
|
+
lastOrdinal: to,
|
|
798
|
+
lastOperationTimestamp: /* @__PURE__ */ new Date()
|
|
799
|
+
}).where("readModelId", "=", this.config.readModelId).where("lastOrdinal", "=", expected).executeTakeFirst();
|
|
800
|
+
if (Number(result.numUpdatedRows) > 0) {
|
|
801
|
+
this.persisted = to;
|
|
802
|
+
this.advanceCursor(to);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const stored = await this.loadState();
|
|
806
|
+
if (stored === void 0) {
|
|
807
|
+
await this.initializeState(0);
|
|
808
|
+
this.lowered(0);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (stored < this.cursor.appliedThrough) {
|
|
812
|
+
this.lowered(stored);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
this.persisted = stored;
|
|
816
|
+
this.advanceCursor(Math.min(to, stored));
|
|
817
|
+
}
|
|
818
|
+
advanceCursor(to) {
|
|
819
|
+
const before = this.cursor.appliedThrough;
|
|
820
|
+
this.cursor.advance(to);
|
|
821
|
+
if (this.cursor.appliedThrough > before) this.onCursorAdvanced(this.cursor.appliedThrough);
|
|
822
|
+
}
|
|
823
|
+
lowered(to) {
|
|
824
|
+
this.catchUpLogger.info("@consumer cursor lowered externally from @old to @new; replaying", this.consumerId, this.cursor.appliedThrough, to);
|
|
825
|
+
this.resetCursor(to);
|
|
120
826
|
}
|
|
121
827
|
};
|
|
122
828
|
//#endregion
|
|
123
829
|
//#region src/read-models/coordinator.ts
|
|
830
|
+
/** Claims a batch for every model that supports it, as the batch queues. */
|
|
831
|
+
function reserveBatch(readModels, operations, logger) {
|
|
832
|
+
const reservations = /* @__PURE__ */ new Map();
|
|
833
|
+
for (const readModel of readModels) {
|
|
834
|
+
if (readModel.reserveOperations === void 0) continue;
|
|
835
|
+
try {
|
|
836
|
+
reservations.set(readModel, readModel.reserveOperations(operations));
|
|
837
|
+
} catch (error) {
|
|
838
|
+
logger.error("Read model @name could not reserve a batch: @Error", readModel.name, error);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return reservations;
|
|
842
|
+
}
|
|
843
|
+
/** Applies the model's reservation, or indexes a model registered since. */
|
|
844
|
+
function indexReserved(readModel, operations, reservations) {
|
|
845
|
+
const reservation = reservations.get(readModel);
|
|
846
|
+
return reservation ? reservation.apply() : readModel.indexOperations(operations);
|
|
847
|
+
}
|
|
848
|
+
/** Frees every claim a run did not apply, so a sweep can take it. */
|
|
849
|
+
function releaseBatch(reservations) {
|
|
850
|
+
for (const reservation of reservations.values()) reservation.release();
|
|
851
|
+
}
|
|
124
852
|
/**
|
|
125
853
|
* Coordinates read model synchronization by listening to operation write events
|
|
126
854
|
* and updating all registered read models on per-`documentId:scope:branch`
|
|
@@ -129,6 +857,9 @@ var BaseReadModel = class {
|
|
|
129
857
|
* implicitly.
|
|
130
858
|
*/
|
|
131
859
|
var ReadModelCoordinator = class {
|
|
860
|
+
eventBus;
|
|
861
|
+
preReady;
|
|
862
|
+
postReady;
|
|
132
863
|
unsubscribe;
|
|
133
864
|
isRunning = false;
|
|
134
865
|
chains = /* @__PURE__ */ new Map();
|
|
@@ -170,6 +901,9 @@ var ReadModelCoordinator = class {
|
|
|
170
901
|
getChainDepth() {
|
|
171
902
|
return this.chains.size;
|
|
172
903
|
}
|
|
904
|
+
indexedReadModels() {
|
|
905
|
+
return [...this.preReady, ...this.postReady];
|
|
906
|
+
}
|
|
173
907
|
addReadModel(readModel, stage) {
|
|
174
908
|
if (this.readModels.some(({ name }) => name === readModel.name)) throw new Error(`Read model "${readModel.name}" is already registered`);
|
|
175
909
|
if (stage === "pre_ready") this.preReady.push(readModel);
|
|
@@ -183,7 +917,9 @@ var ReadModelCoordinator = class {
|
|
|
183
917
|
}
|
|
184
918
|
const enqueuedAt = performance.now();
|
|
185
919
|
const key = this.queueKeyFor(event);
|
|
186
|
-
const
|
|
920
|
+
const reservations = reserveBatch(this.readModels, event.operations, this.logger);
|
|
921
|
+
const run = () => this.runChain(event, enqueuedAt, reservations);
|
|
922
|
+
const current = (this.chains.get(key) ?? Promise.resolve()).then(run, run);
|
|
187
923
|
this.chains.set(key, current);
|
|
188
924
|
current.finally(() => {
|
|
189
925
|
if (this.chains.get(key) === current) this.chains.delete(key);
|
|
@@ -208,11 +944,18 @@ var ReadModelCoordinator = class {
|
|
|
208
944
|
postReadyDurationMs: 0
|
|
209
945
|
});
|
|
210
946
|
}
|
|
211
|
-
async runChain(event, enqueuedAt) {
|
|
947
|
+
async runChain(event, enqueuedAt, reservations) {
|
|
948
|
+
try {
|
|
949
|
+
await this.runBatch(event, enqueuedAt, reservations);
|
|
950
|
+
} finally {
|
|
951
|
+
releaseBatch(reservations);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async runBatch(event, enqueuedAt, reservations) {
|
|
212
955
|
const chainWaitDurationMs = performance.now() - enqueuedAt;
|
|
213
956
|
const preReadyStart = performance.now();
|
|
214
957
|
try {
|
|
215
|
-
await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event)));
|
|
958
|
+
await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event, reservations)));
|
|
216
959
|
} catch (error) {
|
|
217
960
|
this.logger.error("Pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
218
961
|
}
|
|
@@ -230,7 +973,7 @@ var ReadModelCoordinator = class {
|
|
|
230
973
|
const emitDurationMs = performance.now() - emitStart;
|
|
231
974
|
const postReadyStart = performance.now();
|
|
232
975
|
try {
|
|
233
|
-
await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event)));
|
|
976
|
+
await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event, reservations)));
|
|
234
977
|
} catch (error) {
|
|
235
978
|
this.logger.error("Post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
236
979
|
}
|
|
@@ -244,11 +987,11 @@ var ReadModelCoordinator = class {
|
|
|
244
987
|
postReadyDurationMs
|
|
245
988
|
});
|
|
246
989
|
}
|
|
247
|
-
async indexWithTiming(readModel, stage, event) {
|
|
990
|
+
async indexWithTiming(readModel, stage, event, reservations) {
|
|
248
991
|
const start = performance.now();
|
|
249
992
|
let success = false;
|
|
250
993
|
try {
|
|
251
|
-
await readModel
|
|
994
|
+
await indexReserved(readModel, event.operations, reservations);
|
|
252
995
|
success = true;
|
|
253
996
|
} finally {
|
|
254
997
|
this.emitReadModelIndexed({
|
|
@@ -280,15 +1023,37 @@ const DOCUMENT_VIEW_READ_MODEL = "document-view";
|
|
|
280
1023
|
const DOCUMENT_INDEXER_READ_MODEL = "document-indexer";
|
|
281
1024
|
//#endregion
|
|
282
1025
|
//#region src/read-models/document-view.ts
|
|
1026
|
+
/**
|
|
1027
|
+
* What a single-document read of a deleted document returns. A listing omits a
|
|
1028
|
+
* deleted document under either value.
|
|
1029
|
+
*/
|
|
1030
|
+
let DeletedDocumentRead = /* @__PURE__ */ function(DeletedDocumentRead) {
|
|
1031
|
+
/**
|
|
1032
|
+
* The document reads as missing: `get` throws and `resolveIdOrSlug` does not
|
|
1033
|
+
* match its id.
|
|
1034
|
+
*/
|
|
1035
|
+
DeletedDocumentRead["NotFound"] = "NotFound";
|
|
1036
|
+
/**
|
|
1037
|
+
* The document's state as of the deletion, with `state.document.isDeleted`
|
|
1038
|
+
* telling the caller what it holds. Only meaningful with `documentDecisions`,
|
|
1039
|
+
* which is what makes deletion positional.
|
|
1040
|
+
*/
|
|
1041
|
+
DeletedDocumentRead["StateAtDeletion"] = "StateAtDeletion";
|
|
1042
|
+
return DeletedDocumentRead;
|
|
1043
|
+
}({});
|
|
283
1044
|
var KyselyDocumentView = class extends BaseReadModel {
|
|
1045
|
+
operationStore;
|
|
1046
|
+
deletedDocumentRead;
|
|
284
1047
|
_db;
|
|
285
|
-
constructor(db, operationStore, operationIndex, writeCache, consistencyTracker,
|
|
1048
|
+
constructor(db, operationStore, operationIndex, writeCache, consistencyTracker, deletedDocumentRead, indexing = defaultReadModelIndexingConfig) {
|
|
286
1049
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
287
1050
|
readModelId: DOCUMENT_VIEW_READ_MODEL,
|
|
288
|
-
rebuildStateOnInit: true
|
|
1051
|
+
rebuildStateOnInit: true,
|
|
1052
|
+
indexing,
|
|
1053
|
+
replayStreamSuffix: false
|
|
289
1054
|
});
|
|
290
1055
|
this.operationStore = operationStore;
|
|
291
|
-
this.
|
|
1056
|
+
this.deletedDocumentRead = deletedDocumentRead;
|
|
292
1057
|
this._db = db;
|
|
293
1058
|
}
|
|
294
1059
|
/**
|
|
@@ -300,12 +1065,16 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
300
1065
|
* without either fall back to header/document/auth, because their sibling
|
|
301
1066
|
* echoes may be stale. All other action types index only header and their
|
|
302
1067
|
* own scope.
|
|
1068
|
+
*
|
|
1069
|
+
* Every row accepts a write only from an operation whose global ordinal is
|
|
1070
|
+
* at least the one the row already carries, so an older duplicate from a
|
|
1071
|
+
* sweep, boot replay or chunk interleave never rolls a scope back.
|
|
303
1072
|
*/
|
|
304
1073
|
async commitOperations(items) {
|
|
305
1074
|
await this._db.transaction().execute(async (trx) => {
|
|
306
1075
|
for (const item of items) {
|
|
307
1076
|
const { operation, context } = item;
|
|
308
|
-
const { documentId, scope, branch, documentType, resultingState } = context;
|
|
1077
|
+
const { documentId, scope, branch, documentType, resultingState, ordinal } = context;
|
|
309
1078
|
const { index, hash } = operation;
|
|
310
1079
|
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
1080
|
let fullState;
|
|
@@ -324,10 +1093,11 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
324
1093
|
deletedAt: now,
|
|
325
1094
|
lastOperationIndex: index,
|
|
326
1095
|
lastOperationHash: hash,
|
|
1096
|
+
lastOperationOrdinal: ordinal,
|
|
327
1097
|
lastUpdatedAt: now
|
|
328
|
-
}).where("documentId", "=", documentId).where("branch", "=", branch).execute();
|
|
1098
|
+
}).where("documentId", "=", documentId).where("branch", "=", branch).where("lastOperationOrdinal", "<=", ordinal).execute();
|
|
329
1099
|
const deletedDocumentScope = fullState?.document;
|
|
330
|
-
if (deletedDocumentScope !== void 0) await trx.updateTable("DocumentSnapshot").set({ content: deletedDocumentScope }).where("documentId", "=", documentId).where("branch", "=", branch).where("scope", "=", "document").execute();
|
|
1100
|
+
if (deletedDocumentScope !== void 0) await trx.updateTable("DocumentSnapshot").set({ content: deletedDocumentScope }).where("documentId", "=", documentId).where("branch", "=", branch).where("scope", "=", "document").where("lastOperationOrdinal", "<=", ordinal).execute();
|
|
331
1101
|
await trx.deleteFrom("SlugMapping").where("documentId", "=", documentId).where("branch", "=", branch).execute();
|
|
332
1102
|
continue;
|
|
333
1103
|
}
|
|
@@ -345,7 +1115,15 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
345
1115
|
else scopesToIndex.push([scope, {}]);
|
|
346
1116
|
}
|
|
347
1117
|
for (const [scopeName, scopeState] of scopesToIndex) {
|
|
348
|
-
const
|
|
1118
|
+
const needsExistingContent = scopeName === "header" && preserveHeaderMeta;
|
|
1119
|
+
const existingSnapshot = await trx.selectFrom("DocumentSnapshot").select([
|
|
1120
|
+
"slug",
|
|
1121
|
+
"name",
|
|
1122
|
+
"isDeleted",
|
|
1123
|
+
"snapshotVersion",
|
|
1124
|
+
"lastOperationOrdinal"
|
|
1125
|
+
]).$if(needsExistingContent, (qb) => qb.select("content")).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst();
|
|
1126
|
+
if (existingSnapshot !== void 0 && existingSnapshot.lastOperationOrdinal > ordinal) continue;
|
|
349
1127
|
const newState = typeof scopeState === "object" && scopeState !== null ? scopeState : {};
|
|
350
1128
|
let slug = existingSnapshot?.slug ?? null;
|
|
351
1129
|
let name = existingSnapshot?.name ?? null;
|
|
@@ -354,7 +1132,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
354
1132
|
const headerName = newState.name;
|
|
355
1133
|
if (typeof headerSlug === "string") slug = headerSlug;
|
|
356
1134
|
if (typeof headerName === "string") name = headerName;
|
|
357
|
-
if (
|
|
1135
|
+
if (needsExistingContent && existingSnapshot) {
|
|
358
1136
|
const existingHeader = existingSnapshot.content;
|
|
359
1137
|
if (existingHeader && "meta" in existingHeader) newState.meta = existingHeader.meta;
|
|
360
1138
|
}
|
|
@@ -372,12 +1150,13 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
372
1150
|
if (existingSnapshot) await trx.updateTable("DocumentSnapshot").set({
|
|
373
1151
|
lastOperationIndex: index,
|
|
374
1152
|
lastOperationHash: hash,
|
|
1153
|
+
lastOperationOrdinal: ordinal,
|
|
375
1154
|
lastUpdatedAt: /* @__PURE__ */ new Date(),
|
|
376
1155
|
snapshotVersion: existingSnapshot.snapshotVersion + 1,
|
|
377
1156
|
content: newState,
|
|
378
1157
|
slug,
|
|
379
1158
|
name
|
|
380
|
-
}).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).execute();
|
|
1159
|
+
}).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).where("lastOperationOrdinal", "<=", ordinal).execute();
|
|
381
1160
|
else {
|
|
382
1161
|
const snapshot = {
|
|
383
1162
|
id: v4(),
|
|
@@ -390,6 +1169,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
390
1169
|
documentType,
|
|
391
1170
|
lastOperationIndex: index,
|
|
392
1171
|
lastOperationHash: hash,
|
|
1172
|
+
lastOperationOrdinal: ordinal,
|
|
393
1173
|
identifiers: null,
|
|
394
1174
|
metadata: null,
|
|
395
1175
|
deletedAt: null
|
|
@@ -404,7 +1184,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
404
1184
|
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
405
1185
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
406
1186
|
if (documentIds.length === 0) return [];
|
|
407
|
-
const existingIds = existence ===
|
|
1187
|
+
const existingIds = existence === "IncludingDeleted" ? await this.idsWithStreams(documentIds, signal) : await this.liveIds(documentIds);
|
|
408
1188
|
return documentIds.map((id) => existingIds.has(id));
|
|
409
1189
|
}
|
|
410
1190
|
async get(documentId, view, consistencyToken, signal) {
|
|
@@ -412,14 +1192,14 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
412
1192
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
413
1193
|
const branch = view?.branch || "main";
|
|
414
1194
|
let scopesToQuery;
|
|
415
|
-
if (view?.scopes && view.scopes.length > 0) scopesToQuery = [
|
|
1195
|
+
if (view?.scopes && view.scopes.length > 0) scopesToQuery = [.../* @__PURE__ */ new Set([
|
|
416
1196
|
"header",
|
|
417
1197
|
"document",
|
|
418
1198
|
...view.scopes
|
|
419
1199
|
])];
|
|
420
1200
|
else scopesToQuery = [];
|
|
421
1201
|
let query = this._db.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("branch", "=", branch);
|
|
422
|
-
if (
|
|
1202
|
+
if (this.deletedDocumentRead === "NotFound") query = query.where("isDeleted", "=", false);
|
|
423
1203
|
if (scopesToQuery.length > 0) query = query.where("scope", "in", scopesToQuery);
|
|
424
1204
|
const snapshots = await query.execute();
|
|
425
1205
|
if (snapshots.length === 0) throw new Error(`Document not found: ${documentId}`);
|
|
@@ -449,7 +1229,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
449
1229
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
450
1230
|
const branch = view?.branch || "main";
|
|
451
1231
|
let scopesToQuery;
|
|
452
|
-
if (view?.scopes && view.scopes.length > 0) scopesToQuery = [
|
|
1232
|
+
if (view?.scopes && view.scopes.length > 0) scopesToQuery = [.../* @__PURE__ */ new Set([
|
|
453
1233
|
"header",
|
|
454
1234
|
"document",
|
|
455
1235
|
...view.scopes
|
|
@@ -508,17 +1288,11 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
508
1288
|
const branch = view?.branch || "main";
|
|
509
1289
|
const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
|
|
510
1290
|
const limit = paging?.limit || 100;
|
|
511
|
-
const
|
|
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();
|
|
1291
|
+
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
1292
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
allDocumentIds.push(snapshot.documentId);
|
|
520
|
-
}
|
|
521
|
-
const docsToFetch = allDocumentIds.slice(startIndex, startIndex + limit);
|
|
1293
|
+
const hasMore = rows.length > limit;
|
|
1294
|
+
const docsToFetch = rows.slice(0, limit).map((row) => row.documentId);
|
|
1295
|
+
const documents = [];
|
|
522
1296
|
for (const documentId of docsToFetch) {
|
|
523
1297
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
524
1298
|
try {
|
|
@@ -528,7 +1302,6 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
528
1302
|
continue;
|
|
529
1303
|
}
|
|
530
1304
|
}
|
|
531
|
-
const hasMore = allDocumentIds.length > startIndex + limit;
|
|
532
1305
|
const nextCursor = hasMore ? String(startIndex + limit) : void 0;
|
|
533
1306
|
return {
|
|
534
1307
|
results: documents,
|
|
@@ -537,7 +1310,6 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
537
1310
|
limit: 100
|
|
538
1311
|
},
|
|
539
1312
|
nextCursor,
|
|
540
|
-
totalCount: allDocumentIds.length,
|
|
541
1313
|
next: hasMore ? () => this.findByType(type, view, {
|
|
542
1314
|
cursor: nextCursor,
|
|
543
1315
|
limit
|
|
@@ -564,7 +1336,7 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
564
1336
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
565
1337
|
const branch = view?.branch || "main";
|
|
566
1338
|
let idCheckQuery = this._db.selectFrom("DocumentSnapshot").select("documentId").where("documentId", "=", identifier).where("branch", "=", branch);
|
|
567
|
-
if (
|
|
1339
|
+
if (this.deletedDocumentRead === "NotFound") idCheckQuery = idCheckQuery.where("isDeleted", "=", false);
|
|
568
1340
|
const idCheckPromise = idCheckQuery.executeTakeFirst();
|
|
569
1341
|
const slugCheckPromise = this._db.selectFrom("SlugMapping").select("documentId").where("slug", "=", identifier).where("branch", "=", branch).executeTakeFirst();
|
|
570
1342
|
const [idMatch, slugMatch] = await Promise.all([idCheckPromise, slugCheckPromise]);
|
|
@@ -713,18 +1485,25 @@ async function collectAllPages(firstPage, signal) {
|
|
|
713
1485
|
}
|
|
714
1486
|
//#endregion
|
|
715
1487
|
//#region src/storage/kysely/document-indexer.ts
|
|
1488
|
+
function isRelationshipAction(actionType) {
|
|
1489
|
+
return actionType === "ADD_RELATIONSHIP" || actionType === "REMOVE_RELATIONSHIP" || actionType === "UPDATE_RELATIONSHIP";
|
|
1490
|
+
}
|
|
716
1491
|
var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
717
1492
|
_db;
|
|
718
|
-
constructor(db, operationIndex, writeCache, consistencyTracker) {
|
|
1493
|
+
constructor(db, operationIndex, writeCache, consistencyTracker, indexing = defaultReadModelIndexingConfig) {
|
|
719
1494
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
720
1495
|
readModelId: DOCUMENT_INDEXER_READ_MODEL,
|
|
721
|
-
rebuildStateOnInit: false
|
|
1496
|
+
rebuildStateOnInit: false,
|
|
1497
|
+
indexing
|
|
722
1498
|
});
|
|
723
1499
|
this._db = db;
|
|
724
1500
|
}
|
|
1501
|
+
/** Opens no transaction for a batch carrying no relationship operation. */
|
|
725
1502
|
async commitOperations(items) {
|
|
1503
|
+
const relationshipOps = items.filter((item) => isRelationshipAction(item.operation.action.type));
|
|
1504
|
+
if (relationshipOps.length === 0) return;
|
|
726
1505
|
await this._db.transaction().execute(async (trx) => {
|
|
727
|
-
for (const item of
|
|
1506
|
+
for (const item of relationshipOps) {
|
|
728
1507
|
const { operation } = item;
|
|
729
1508
|
const actionType = operation.action.type;
|
|
730
1509
|
if (actionType === "ADD_RELATIONSHIP") await this.handleAddRelationship(trx, operation);
|
|
@@ -896,7 +1675,7 @@ var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
|
896
1675
|
async findAncestors(documentId, types, consistencyToken, signal) {
|
|
897
1676
|
if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
|
|
898
1677
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
899
|
-
const nodes = new Set([documentId]);
|
|
1678
|
+
const nodes = /* @__PURE__ */ new Set([documentId]);
|
|
900
1679
|
const edges = [];
|
|
901
1680
|
const queue = [documentId];
|
|
902
1681
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -953,6 +1732,6 @@ var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
|
953
1732
|
}
|
|
954
1733
|
};
|
|
955
1734
|
//#endregion
|
|
956
|
-
export {
|
|
1735
|
+
export { ContiguousCursor as _, KyselyDocumentView as a, isCatchUpConsumer as b, ReadModelCoordinator as c, reserveBatch as d, BaseReadModel as f, unchunkedReadModelIndexingConfig as g, defaultReadModelIndexingConfig as h, DeletedDocumentRead as i, indexReserved as l, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS as m, ConsistencyTracker as n, DOCUMENT_INDEXER_READ_MODEL as o, DEFAULT_COMMIT_CHUNK_SIZE as p, makeConsistencyKey as r, DOCUMENT_VIEW_READ_MODEL as s, KyselyDocumentIndexer as t, releaseBatch as u, defaultCatchUpConfig as v, CatchUpScheduler as y };
|
|
957
1736
|
|
|
958
|
-
//# sourceMappingURL=document-indexer-
|
|
1737
|
+
//# sourceMappingURL=document-indexer-I6i_YzsR.js.map
|