@powerhousedao/powerhouse-vetra-packages 6.1.0-dev.3 → 6.1.0-dev.5
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/browser/assets/entry-Bzani6_n.js +313 -0
- package/dist/browser/assets/projection-entry-Bpu-8SnI.js +406 -0
- package/dist/browser/{connect-CKdlDSUw.js → connect-B-YCtXM-.js} +3 -3
- package/dist/browser/connect-B-YCtXM-.js.map +1 -0
- package/dist/browser/{dist-CC1E3l2O.js → dist-Cjx0f38R.js} +6 -4
- package/dist/browser/dist-Cjx0f38R.js.map +1 -0
- package/dist/browser/{dist-DQgJ8n4d.js → document-drive-BNjl7aW1.js} +232 -218
- package/dist/browser/document-drive-BNjl7aW1.js.map +1 -0
- package/dist/browser/document-models/index.js +1 -1
- package/dist/browser/{editor-Dm_73jiz.js → editor-_cvNKjkT.js} +4 -4
- package/dist/browser/{editor-Dm_73jiz.js.map → editor-_cvNKjkT.js.map} +1 -1
- package/dist/browser/{editor-B3yz7YdR.js → editor-qD_ZliXV.js} +3 -3
- package/dist/browser/{editor-B3yz7YdR.js.map → editor-qD_ZliXV.js.map} +1 -1
- package/dist/browser/editors/document-model-editor/module.js +1 -1
- package/dist/browser/editors/generic-drive-explorer/index.js +3 -3
- package/dist/browser/editors/generic-drive-explorer/module.js +1 -1
- package/dist/browser/{folder-view-H2ov-zId.js → folder-view-C_TsWjGR.js} +2 -2
- package/dist/browser/{folder-view-H2ov-zId.js.map → folder-view-C_TsWjGR.js.map} +1 -1
- package/dist/browser/index.js +2 -2
- package/dist/node/{connect-SGvLzr5K.mjs → connect-BvAuTe1i.mjs} +3 -3
- package/dist/node/connect-BvAuTe1i.mjs.map +1 -0
- package/dist/node/{dist-Cay1iRRr.mjs → dist-CYnOnkdQ.mjs} +6 -4
- package/dist/node/dist-CYnOnkdQ.mjs.map +1 -0
- package/dist/node/{dist-Bz4SgEHs.mjs → document-drive-nsHdysTA.mjs} +232 -218
- package/dist/node/document-drive-nsHdysTA.mjs.map +1 -0
- package/dist/node/document-models/index.mjs +1 -1
- package/dist/node/{editor-W8QOlGXD.mjs → editor-CALj_VW9.mjs} +3 -3
- package/dist/node/{editor-W8QOlGXD.mjs.map → editor-CALj_VW9.mjs.map} +1 -1
- package/dist/node/{editor-BaXuDsby.mjs → editor-C_RSXRUo.mjs} +4 -4
- package/dist/node/{editor-BaXuDsby.mjs.map → editor-C_RSXRUo.mjs.map} +1 -1
- package/dist/node/editors/document-model-editor/module.mjs +1 -1
- package/dist/node/editors/generic-drive-explorer/index.mjs +3 -3
- package/dist/node/editors/generic-drive-explorer/module.mjs +1 -1
- package/dist/node/{folder-view-B0FNXbc0.mjs → folder-view-C20JG1ws.mjs} +2 -2
- package/dist/node/{folder-view-B0FNXbc0.mjs.map → folder-view-C20JG1ws.mjs.map} +1 -1
- package/dist/node/index.mjs +2 -2
- package/package.json +6 -6
- package/dist/browser/connect-CKdlDSUw.js.map +0 -1
- package/dist/browser/dist-CC1E3l2O.js.map +0 -1
- package/dist/browser/dist-DQgJ8n4d.js.map +0 -1
- package/dist/node/connect-SGvLzr5K.mjs.map +0 -1
- package/dist/node/dist-Bz4SgEHs.mjs.map +0 -1
- package/dist/node/dist-Cay1iRRr.mjs.map +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { o as instrumentPgPool } from "./drive-container-types-BNpMlgT_.js";
|
|
2
|
+
import { n as errorToInfo, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
|
|
3
|
+
import { n as defaultLoadFactory, t as buildWorkerExecutor } from "./build-worker-executor-DDVXB921.js";
|
|
4
|
+
import { ConsoleLogger } from "document-model";
|
|
5
|
+
import { isMainThread, parentPort } from "node:worker_threads";
|
|
6
|
+
//#region src/executor/worker/run-worker.ts
|
|
7
|
+
const POOL_SAMPLE_INTERVAL_MS = 1e3;
|
|
8
|
+
async function defaultCreateDatabase(config, workerId) {
|
|
9
|
+
const { Kysely, PostgresDialect } = await import("kysely");
|
|
10
|
+
const Pool = (await import("pg")).default.Pool;
|
|
11
|
+
const pool = new Pool({
|
|
12
|
+
host: config.host,
|
|
13
|
+
port: config.port,
|
|
14
|
+
database: config.database,
|
|
15
|
+
user: config.user,
|
|
16
|
+
password: config.password,
|
|
17
|
+
ssl: config.ssl ? { rejectUnauthorized: false } : void 0,
|
|
18
|
+
application_name: config.applicationName ?? workerId,
|
|
19
|
+
max: config.poolSize,
|
|
20
|
+
connectionTimeoutMillis: config.connectionTimeoutMillis,
|
|
21
|
+
idleTimeoutMillis: config.idleTimeoutMillis
|
|
22
|
+
});
|
|
23
|
+
const poolInstrumentation = instrumentPgPool(pool, workerId);
|
|
24
|
+
const kysely = new Kysely({ dialect: new PostgresDialect({ pool }) });
|
|
25
|
+
return {
|
|
26
|
+
kysely,
|
|
27
|
+
poolInstrumentation,
|
|
28
|
+
async shutdown() {
|
|
29
|
+
try {
|
|
30
|
+
await kysely.destroy();
|
|
31
|
+
} catch {}
|
|
32
|
+
try {
|
|
33
|
+
await pool.end();
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Drives the worker's message loop. Owns lifecycle of the database handle
|
|
40
|
+
* and executor stack. The default factories build a real Postgres pool and
|
|
41
|
+
* use dynamic `import()` for model/verifier specs; tests inject overrides
|
|
42
|
+
* for an in-process PGlite path.
|
|
43
|
+
*/
|
|
44
|
+
function runWorker(parentPort, overrides = {}) {
|
|
45
|
+
let workerId = "";
|
|
46
|
+
let initCompleted = false;
|
|
47
|
+
let initConfig = null;
|
|
48
|
+
let executorStack = null;
|
|
49
|
+
let database = null;
|
|
50
|
+
let poolSampleTimer = null;
|
|
51
|
+
let pendingPoolSamples = [];
|
|
52
|
+
let detachPoolListener = null;
|
|
53
|
+
const activeLoadFactory = overrides.loadFactory ?? defaultLoadFactory;
|
|
54
|
+
function post(msg) {
|
|
55
|
+
parentPort.postMessage(msg);
|
|
56
|
+
}
|
|
57
|
+
const logger = createForwardingLogger(post);
|
|
58
|
+
function startPoolReporter(instrumentation) {
|
|
59
|
+
detachPoolListener = instrumentation.onAcquire((durationMs) => {
|
|
60
|
+
pendingPoolSamples.push(durationMs);
|
|
61
|
+
});
|
|
62
|
+
poolSampleTimer = setInterval(() => {
|
|
63
|
+
if (pendingPoolSamples.length === 0) return;
|
|
64
|
+
const durations = pendingPoolSamples;
|
|
65
|
+
pendingPoolSamples = [];
|
|
66
|
+
const stats = instrumentation.getStats();
|
|
67
|
+
post({
|
|
68
|
+
type: "pool-acquire-samples",
|
|
69
|
+
workerId,
|
|
70
|
+
poolName: instrumentation.name,
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
durations,
|
|
73
|
+
size: stats.size,
|
|
74
|
+
idle: stats.idle,
|
|
75
|
+
waiting: stats.waiting
|
|
76
|
+
});
|
|
77
|
+
}, POOL_SAMPLE_INTERVAL_MS);
|
|
78
|
+
poolSampleTimer.unref();
|
|
79
|
+
}
|
|
80
|
+
function stopPoolReporter() {
|
|
81
|
+
if (detachPoolListener) {
|
|
82
|
+
detachPoolListener();
|
|
83
|
+
detachPoolListener = null;
|
|
84
|
+
}
|
|
85
|
+
if (poolSampleTimer) {
|
|
86
|
+
clearInterval(poolSampleTimer);
|
|
87
|
+
poolSampleTimer = null;
|
|
88
|
+
}
|
|
89
|
+
pendingPoolSamples = [];
|
|
90
|
+
}
|
|
91
|
+
process.on("uncaughtException", (err) => {
|
|
92
|
+
try {
|
|
93
|
+
post({
|
|
94
|
+
type: "log",
|
|
95
|
+
level: "error",
|
|
96
|
+
message: "worker uncaughtException",
|
|
97
|
+
args: [errorToInfo(err)],
|
|
98
|
+
timestamp: Date.now()
|
|
99
|
+
});
|
|
100
|
+
} catch {}
|
|
101
|
+
throw err;
|
|
102
|
+
});
|
|
103
|
+
process.on("unhandledRejection", (reason) => {
|
|
104
|
+
try {
|
|
105
|
+
post({
|
|
106
|
+
type: "log",
|
|
107
|
+
level: "error",
|
|
108
|
+
message: "worker unhandledRejection",
|
|
109
|
+
args: [errorToInfo(reason)],
|
|
110
|
+
timestamp: Date.now()
|
|
111
|
+
});
|
|
112
|
+
} catch {}
|
|
113
|
+
});
|
|
114
|
+
async function handleInit(msg) {
|
|
115
|
+
workerId = msg.workerId;
|
|
116
|
+
initConfig = msg;
|
|
117
|
+
database = await (overrides.createDatabase ?? defaultCreateDatabase)(msg.db, msg.workerId);
|
|
118
|
+
if (overrides.beforeBuildExecutor) await overrides.beforeBuildExecutor(database.kysely);
|
|
119
|
+
executorStack = await buildWorkerExecutor({
|
|
120
|
+
init: msg,
|
|
121
|
+
database: database.kysely,
|
|
122
|
+
logger: new ConsoleLogger([`reactor-worker:${msg.workerId}`]),
|
|
123
|
+
loadFactory: activeLoadFactory
|
|
124
|
+
});
|
|
125
|
+
if (database.poolInstrumentation) startPoolReporter(database.poolInstrumentation);
|
|
126
|
+
initCompleted = true;
|
|
127
|
+
logger.info("worker initialized: @workerId", msg.workerId);
|
|
128
|
+
post({
|
|
129
|
+
type: "ready",
|
|
130
|
+
correlationId: msg.correlationId,
|
|
131
|
+
workerId: msg.workerId
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function handleExecute(correlationId, job) {
|
|
135
|
+
if (!executorStack) {
|
|
136
|
+
post({
|
|
137
|
+
type: "result",
|
|
138
|
+
correlationId,
|
|
139
|
+
result: {
|
|
140
|
+
job,
|
|
141
|
+
success: false
|
|
142
|
+
},
|
|
143
|
+
error: errorToInfo(/* @__PURE__ */ new Error("execute received before init"))
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
post({
|
|
149
|
+
type: "result",
|
|
150
|
+
correlationId,
|
|
151
|
+
result: await executorStack.executor.executeJob(job),
|
|
152
|
+
writeReady: executorStack.takeLastWriteReady() ?? void 0
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
post({
|
|
156
|
+
type: "result",
|
|
157
|
+
correlationId,
|
|
158
|
+
result: {
|
|
159
|
+
job,
|
|
160
|
+
success: false
|
|
161
|
+
},
|
|
162
|
+
error: errorToInfo(error)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function handleLoadModel(msg) {
|
|
167
|
+
const stack = executorStack;
|
|
168
|
+
if (!stack) {
|
|
169
|
+
post({
|
|
170
|
+
type: "model-load-failed",
|
|
171
|
+
correlationId: msg.correlationId,
|
|
172
|
+
documentType: msg.model.documentType,
|
|
173
|
+
version: msg.model.version,
|
|
174
|
+
error: errorToInfo(/* @__PURE__ */ new Error("load-model received before init"))
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let module;
|
|
179
|
+
try {
|
|
180
|
+
module = await activeLoadFactory(msg.model.spec);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
post({
|
|
183
|
+
type: "model-load-failed",
|
|
184
|
+
correlationId: msg.correlationId,
|
|
185
|
+
documentType: msg.model.documentType,
|
|
186
|
+
version: msg.model.version,
|
|
187
|
+
error: errorToInfo(error)
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const [result] = stack.registry.registerModules(module);
|
|
192
|
+
if (result.status === "error") {
|
|
193
|
+
post({
|
|
194
|
+
type: "model-load-failed",
|
|
195
|
+
correlationId: msg.correlationId,
|
|
196
|
+
documentType: msg.model.documentType,
|
|
197
|
+
version: msg.model.version,
|
|
198
|
+
error: errorToInfo(result.error)
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
post({
|
|
203
|
+
type: "model-loaded",
|
|
204
|
+
correlationId: msg.correlationId,
|
|
205
|
+
documentType: msg.model.documentType,
|
|
206
|
+
version: msg.model.version
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async function shutdownDatabase() {
|
|
210
|
+
stopPoolReporter();
|
|
211
|
+
if (database) await database.shutdown();
|
|
212
|
+
}
|
|
213
|
+
function handleParentMessage(msg) {
|
|
214
|
+
switch (msg.type) {
|
|
215
|
+
case "init":
|
|
216
|
+
handleInit(msg).catch((err) => {
|
|
217
|
+
post({
|
|
218
|
+
type: "log",
|
|
219
|
+
level: "error",
|
|
220
|
+
message: "worker init failed",
|
|
221
|
+
args: [errorToInfo(err)],
|
|
222
|
+
timestamp: Date.now()
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
case "execute":
|
|
227
|
+
if (!initCompleted) {
|
|
228
|
+
logger.warn("received execute before init");
|
|
229
|
+
post({
|
|
230
|
+
type: "result",
|
|
231
|
+
correlationId: msg.correlationId,
|
|
232
|
+
result: {
|
|
233
|
+
job: msg.job,
|
|
234
|
+
success: false
|
|
235
|
+
},
|
|
236
|
+
error: errorToInfo(/* @__PURE__ */ new Error("execute received before init"))
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
handleExecute(msg.correlationId, msg.job).catch((err) => {
|
|
241
|
+
post({
|
|
242
|
+
type: "result",
|
|
243
|
+
correlationId: msg.correlationId,
|
|
244
|
+
result: {
|
|
245
|
+
job: msg.job,
|
|
246
|
+
success: false
|
|
247
|
+
},
|
|
248
|
+
error: errorToInfo(err)
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
break;
|
|
252
|
+
case "shutdown":
|
|
253
|
+
logger.info("worker shutting down: @workerId", workerId);
|
|
254
|
+
shutdownDatabase().finally(() => {
|
|
255
|
+
post({
|
|
256
|
+
type: "log",
|
|
257
|
+
level: "info",
|
|
258
|
+
message: "worker shutdown",
|
|
259
|
+
args: [],
|
|
260
|
+
timestamp: Date.now()
|
|
261
|
+
});
|
|
262
|
+
process.exit(0);
|
|
263
|
+
});
|
|
264
|
+
break;
|
|
265
|
+
case "abort":
|
|
266
|
+
logger.warn("abort received (no-op stub): @correlationId", msg.correlationId);
|
|
267
|
+
break;
|
|
268
|
+
case "load-model":
|
|
269
|
+
handleLoadModel(msg).catch((err) => {
|
|
270
|
+
post({
|
|
271
|
+
type: "model-load-failed",
|
|
272
|
+
correlationId: msg.correlationId,
|
|
273
|
+
documentType: msg.model.documentType,
|
|
274
|
+
version: msg.model.version,
|
|
275
|
+
error: errorToInfo(err)
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
default: {
|
|
280
|
+
const raw = msg;
|
|
281
|
+
if (raw["type"] === "__test_throw") {
|
|
282
|
+
const rawReason = raw["reason"];
|
|
283
|
+
const reason = typeof rawReason === "string" ? rawReason : "synthetic uncaughtException";
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
throw new Error(reason);
|
|
286
|
+
}, 0);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
parentPort.on("message", handleParentMessage);
|
|
294
|
+
parentPort.__reactorWorkerHarness = {
|
|
295
|
+
handleParentMessage,
|
|
296
|
+
get initCompleted() {
|
|
297
|
+
return initCompleted;
|
|
298
|
+
},
|
|
299
|
+
get initConfig() {
|
|
300
|
+
return initConfig;
|
|
301
|
+
},
|
|
302
|
+
get workerId() {
|
|
303
|
+
return workerId;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/executor/worker/entry.ts
|
|
309
|
+
if (isMainThread || parentPort === null) throw new Error("entry.ts must be run as a worker thread");
|
|
310
|
+
runWorker(parentPort);
|
|
311
|
+
//#endregion
|
|
312
|
+
|
|
313
|
+
//# sourceMappingURL=entry.js.map
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { S as CollectionMembershipCache, _ as KyselyWriteCache, d as KyselyKeyframeStore, f as DocumentModelRegistry, g as EventBus, n as REACTOR_SCHEMA, o as instrumentPgPool, s as KyselyOperationStore, v as KyselyOperationIndex, y as DocumentMetaCache } from "./drive-container-types-BNpMlgT_.js";
|
|
2
|
+
import { n as ReactorEventTypes } from "./types-CxSpmNGK.js";
|
|
3
|
+
import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, t as KyselyDocumentIndexer } from "./document-indexer-B2iLRB0o.js";
|
|
4
|
+
import { n as errorToInfo, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
|
|
5
|
+
import { n as defaultLoadFactory } from "./build-worker-executor-DDVXB921.js";
|
|
6
|
+
import { ConsoleLogger } from "document-model";
|
|
7
|
+
import { isMainThread, parentPort } from "node:worker_threads";
|
|
8
|
+
//#region src/projection/projection-worker/build-projection-stack.ts
|
|
9
|
+
async function loadModelManifest(entries, loadFactory, registry, logger) {
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
let module;
|
|
12
|
+
try {
|
|
13
|
+
module = await loadFactory(entry.spec);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
logger.error("projection worker failed to load document model: @entry @error", entry, error);
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const [result] = registry.registerModules(module);
|
|
19
|
+
if (result.status === "error") {
|
|
20
|
+
logger.error("projection worker failed to register document model: @entry @error", entry, result.error);
|
|
21
|
+
throw result.error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function instantiateReadModel(kind, database, operationStore, operationIndex, writeCache) {
|
|
26
|
+
switch (kind) {
|
|
27
|
+
case "document-view": return new KyselyDocumentView(database, operationStore, operationIndex, writeCache, new ConsistencyTracker());
|
|
28
|
+
case "document-indexer": return new KyselyDocumentIndexer(database, operationIndex, writeCache, new ConsistencyTracker());
|
|
29
|
+
default: {
|
|
30
|
+
const exhaustive = kind;
|
|
31
|
+
throw new Error(`unknown built-in read model kind: ${String(exhaustive)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function initReadModels(models, logger) {
|
|
36
|
+
for (const model of models) {
|
|
37
|
+
const maybeInit = model;
|
|
38
|
+
if (typeof maybeInit.init !== "function") continue;
|
|
39
|
+
try {
|
|
40
|
+
await maybeInit.init();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.error("projection worker read model init failed: @name @error", model.name, error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function buildProjectionStack(options) {
|
|
48
|
+
const { init, database: baseDatabase, logger, events } = options;
|
|
49
|
+
const loadFactory = options.loadFactory ?? defaultLoadFactory;
|
|
50
|
+
const registry = new DocumentModelRegistry();
|
|
51
|
+
await loadModelManifest(init.models, loadFactory, registry, logger);
|
|
52
|
+
const database = baseDatabase.withSchema(REACTOR_SCHEMA);
|
|
53
|
+
const operationStore = new KyselyOperationStore(database);
|
|
54
|
+
const writeCache = new KyselyWriteCache(new KyselyKeyframeStore(database), operationStore, registry, {
|
|
55
|
+
maxDocuments: 100,
|
|
56
|
+
ringBufferSize: 10,
|
|
57
|
+
keyframeInterval: 10
|
|
58
|
+
});
|
|
59
|
+
await writeCache.startup();
|
|
60
|
+
const operationIndex = new KyselyOperationIndex(database);
|
|
61
|
+
await new DocumentMetaCache(operationStore, { maxDocuments: 1e3 }).startup();
|
|
62
|
+
new CollectionMembershipCache(operationIndex);
|
|
63
|
+
const preReady = init.preReadyKinds.map((kind) => instantiateReadModel(kind, database, operationStore, operationIndex, writeCache));
|
|
64
|
+
const postReady = init.postReadyKinds.map((kind) => instantiateReadModel(kind, database, operationStore, operationIndex, writeCache));
|
|
65
|
+
await initReadModels([...preReady, ...postReady], logger);
|
|
66
|
+
const eventBus = new EventBus();
|
|
67
|
+
const subscriptions = [];
|
|
68
|
+
subscriptions.push(eventBus.subscribe(ReactorEventTypes.JOB_READ_READY, (_t, event) => {
|
|
69
|
+
events.onReadReady(event);
|
|
70
|
+
}));
|
|
71
|
+
subscriptions.push(eventBus.subscribe(ReactorEventTypes.READMODEL_INDEXED, (_t, event) => {
|
|
72
|
+
events.onReadModelIndexed(event);
|
|
73
|
+
}));
|
|
74
|
+
subscriptions.push(eventBus.subscribe(ReactorEventTypes.READMODEL_BATCH_COMPLETED, (_t, event) => {
|
|
75
|
+
events.onBatchCompleted(event);
|
|
76
|
+
}));
|
|
77
|
+
const coordinator = new ReadModelCoordinator(eventBus, preReady, postReady);
|
|
78
|
+
coordinator.start();
|
|
79
|
+
return {
|
|
80
|
+
registry,
|
|
81
|
+
coordinator,
|
|
82
|
+
eventBus,
|
|
83
|
+
async relayWriteReady(event) {
|
|
84
|
+
await eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, event);
|
|
85
|
+
},
|
|
86
|
+
getChainDepth() {
|
|
87
|
+
return coordinator.getChainDepth();
|
|
88
|
+
},
|
|
89
|
+
async drain() {
|
|
90
|
+
await coordinator.drain();
|
|
91
|
+
},
|
|
92
|
+
shutdown() {
|
|
93
|
+
coordinator.stop();
|
|
94
|
+
for (const unsub of subscriptions) unsub();
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/projection/projection-worker/run-projection-worker.ts
|
|
101
|
+
const POOL_SAMPLE_INTERVAL_MS = 1e3;
|
|
102
|
+
async function defaultCreateDatabase(config, shardId) {
|
|
103
|
+
const { Kysely, PostgresDialect } = await import("kysely");
|
|
104
|
+
const Pool = (await import("pg")).default.Pool;
|
|
105
|
+
const pool = new Pool({
|
|
106
|
+
host: config.host,
|
|
107
|
+
port: config.port,
|
|
108
|
+
database: config.database,
|
|
109
|
+
user: config.user,
|
|
110
|
+
password: config.password,
|
|
111
|
+
ssl: config.ssl ? { rejectUnauthorized: false } : void 0,
|
|
112
|
+
application_name: config.applicationName ?? shardId,
|
|
113
|
+
max: config.poolSize,
|
|
114
|
+
connectionTimeoutMillis: config.connectionTimeoutMillis,
|
|
115
|
+
idleTimeoutMillis: config.idleTimeoutMillis
|
|
116
|
+
});
|
|
117
|
+
const poolInstrumentation = instrumentPgPool(pool, shardId);
|
|
118
|
+
const kysely = new Kysely({ dialect: new PostgresDialect({ pool }) });
|
|
119
|
+
return {
|
|
120
|
+
kysely,
|
|
121
|
+
poolInstrumentation,
|
|
122
|
+
async shutdown() {
|
|
123
|
+
try {
|
|
124
|
+
await kysely.destroy();
|
|
125
|
+
} catch {}
|
|
126
|
+
try {
|
|
127
|
+
await pool.end();
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Drives the projection worker's message loop. Owns lifecycle of the
|
|
134
|
+
* database handle and the projection stack. The default factory builds a
|
|
135
|
+
* real Postgres pool; tests inject overrides for an in-process PGlite path.
|
|
136
|
+
*/
|
|
137
|
+
function runProjectionWorker(parentPort, overrides = {}) {
|
|
138
|
+
let shardId = "";
|
|
139
|
+
let initCompleted = false;
|
|
140
|
+
let stack = null;
|
|
141
|
+
let database = null;
|
|
142
|
+
let depthTimer = null;
|
|
143
|
+
let lastReportedDepth = -1;
|
|
144
|
+
let poolSampleTimer = null;
|
|
145
|
+
let pendingPoolSamples = [];
|
|
146
|
+
let detachPoolListener = null;
|
|
147
|
+
function post(msg) {
|
|
148
|
+
parentPort.postMessage(msg);
|
|
149
|
+
}
|
|
150
|
+
function startPoolReporter(instrumentation) {
|
|
151
|
+
detachPoolListener = instrumentation.onAcquire((durationMs) => {
|
|
152
|
+
pendingPoolSamples.push(durationMs);
|
|
153
|
+
});
|
|
154
|
+
poolSampleTimer = setInterval(() => {
|
|
155
|
+
if (pendingPoolSamples.length === 0) return;
|
|
156
|
+
const durations = pendingPoolSamples;
|
|
157
|
+
pendingPoolSamples = [];
|
|
158
|
+
const stats = instrumentation.getStats();
|
|
159
|
+
post({
|
|
160
|
+
type: "pool-acquire-samples",
|
|
161
|
+
shardId,
|
|
162
|
+
poolName: instrumentation.name,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
durations,
|
|
165
|
+
size: stats.size,
|
|
166
|
+
idle: stats.idle,
|
|
167
|
+
waiting: stats.waiting
|
|
168
|
+
});
|
|
169
|
+
}, POOL_SAMPLE_INTERVAL_MS);
|
|
170
|
+
poolSampleTimer.unref();
|
|
171
|
+
}
|
|
172
|
+
function stopPoolReporter() {
|
|
173
|
+
if (detachPoolListener) {
|
|
174
|
+
detachPoolListener();
|
|
175
|
+
detachPoolListener = null;
|
|
176
|
+
}
|
|
177
|
+
if (poolSampleTimer) {
|
|
178
|
+
clearInterval(poolSampleTimer);
|
|
179
|
+
poolSampleTimer = null;
|
|
180
|
+
}
|
|
181
|
+
pendingPoolSamples = [];
|
|
182
|
+
}
|
|
183
|
+
const logger = createForwardingLogger((msg) => post({
|
|
184
|
+
...msg,
|
|
185
|
+
shardId
|
|
186
|
+
}));
|
|
187
|
+
process.on("uncaughtException", (err) => {
|
|
188
|
+
try {
|
|
189
|
+
post({
|
|
190
|
+
type: "log",
|
|
191
|
+
shardId,
|
|
192
|
+
level: "error",
|
|
193
|
+
message: "projection worker uncaughtException",
|
|
194
|
+
args: [errorToInfo(err)],
|
|
195
|
+
timestamp: Date.now()
|
|
196
|
+
});
|
|
197
|
+
} catch {}
|
|
198
|
+
throw err;
|
|
199
|
+
});
|
|
200
|
+
process.on("unhandledRejection", (reason) => {
|
|
201
|
+
try {
|
|
202
|
+
post({
|
|
203
|
+
type: "log",
|
|
204
|
+
shardId,
|
|
205
|
+
level: "error",
|
|
206
|
+
message: "projection worker unhandledRejection",
|
|
207
|
+
args: [errorToInfo(reason)],
|
|
208
|
+
timestamp: Date.now()
|
|
209
|
+
});
|
|
210
|
+
} catch {}
|
|
211
|
+
});
|
|
212
|
+
function startDepthReporter(intervalMs) {
|
|
213
|
+
if (intervalMs <= 0) return;
|
|
214
|
+
depthTimer = setInterval(() => {
|
|
215
|
+
if (!stack) return;
|
|
216
|
+
const depth = stack.getChainDepth();
|
|
217
|
+
if (depth === lastReportedDepth) return;
|
|
218
|
+
lastReportedDepth = depth;
|
|
219
|
+
post({
|
|
220
|
+
type: "chain-depth",
|
|
221
|
+
shardId,
|
|
222
|
+
depth,
|
|
223
|
+
timestamp: Date.now()
|
|
224
|
+
});
|
|
225
|
+
}, intervalMs);
|
|
226
|
+
depthTimer.unref();
|
|
227
|
+
}
|
|
228
|
+
function stopDepthReporter() {
|
|
229
|
+
if (depthTimer) {
|
|
230
|
+
clearInterval(depthTimer);
|
|
231
|
+
depthTimer = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function handleInit(msg) {
|
|
235
|
+
shardId = msg.shardId;
|
|
236
|
+
database = await (overrides.createDatabase ?? defaultCreateDatabase)(msg.db, msg.shardId);
|
|
237
|
+
if (overrides.beforeBuildStack) await overrides.beforeBuildStack(database.kysely);
|
|
238
|
+
stack = await buildProjectionStack({
|
|
239
|
+
init: msg,
|
|
240
|
+
database: database.kysely,
|
|
241
|
+
logger: new ConsoleLogger([`projection-shard:${msg.shardId}`]),
|
|
242
|
+
loadFactory: overrides.loadFactory,
|
|
243
|
+
events: {
|
|
244
|
+
onReadReady: (event) => {
|
|
245
|
+
post({
|
|
246
|
+
type: "read-ready",
|
|
247
|
+
shardId,
|
|
248
|
+
jobId: event.jobId,
|
|
249
|
+
operations: event.operations
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
onReadModelIndexed: (event) => {
|
|
253
|
+
post({
|
|
254
|
+
type: "readmodel-indexed",
|
|
255
|
+
shardId,
|
|
256
|
+
jobId: event.jobId,
|
|
257
|
+
readModelName: event.readModelName,
|
|
258
|
+
stage: event.stage,
|
|
259
|
+
durationMs: event.durationMs,
|
|
260
|
+
operationCount: event.operationCount,
|
|
261
|
+
success: event.success
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
onBatchCompleted: (event) => {
|
|
265
|
+
post({
|
|
266
|
+
type: "readmodel-batch-completed",
|
|
267
|
+
shardId,
|
|
268
|
+
jobId: event.jobId,
|
|
269
|
+
batchSize: event.batchSize,
|
|
270
|
+
chainWaitDurationMs: event.chainWaitDurationMs,
|
|
271
|
+
preReadyDurationMs: event.preReadyDurationMs,
|
|
272
|
+
emitDurationMs: event.emitDurationMs,
|
|
273
|
+
postReadyDurationMs: event.postReadyDurationMs
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
initCompleted = true;
|
|
279
|
+
startDepthReporter(msg.chainDepthReportIntervalMs);
|
|
280
|
+
if (database.poolInstrumentation) startPoolReporter(database.poolInstrumentation);
|
|
281
|
+
logger.info("projection worker initialized: @shardId", msg.shardId);
|
|
282
|
+
post({
|
|
283
|
+
type: "ready",
|
|
284
|
+
correlationId: msg.correlationId,
|
|
285
|
+
shardId
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async function handleWriteReady(msg) {
|
|
289
|
+
if (!stack) {
|
|
290
|
+
logger.warn("write-ready received before init on shard @shardId", shardId);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const event = {
|
|
294
|
+
jobId: msg.jobId,
|
|
295
|
+
operations: msg.operations,
|
|
296
|
+
jobMeta: msg.jobMeta,
|
|
297
|
+
collectionMemberships: msg.collectionMemberships
|
|
298
|
+
};
|
|
299
|
+
await stack.relayWriteReady(event);
|
|
300
|
+
}
|
|
301
|
+
async function handleDrain(correlationId) {
|
|
302
|
+
if (stack) await stack.drain();
|
|
303
|
+
post({
|
|
304
|
+
type: "drained",
|
|
305
|
+
correlationId,
|
|
306
|
+
shardId
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function shutdownStack() {
|
|
310
|
+
stopDepthReporter();
|
|
311
|
+
stopPoolReporter();
|
|
312
|
+
if (stack) {
|
|
313
|
+
try {
|
|
314
|
+
await stack.drain();
|
|
315
|
+
} catch (error) {
|
|
316
|
+
logger.warn("projection worker drain failed during shutdown: @error", error);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await stack.shutdown();
|
|
320
|
+
} catch (error) {
|
|
321
|
+
logger.warn("projection worker stack shutdown failed: @error", error);
|
|
322
|
+
}
|
|
323
|
+
stack = null;
|
|
324
|
+
}
|
|
325
|
+
if (database) {
|
|
326
|
+
await database.shutdown();
|
|
327
|
+
database = null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function handleParentMessage(msg) {
|
|
331
|
+
switch (msg.type) {
|
|
332
|
+
case "init":
|
|
333
|
+
handleInit(msg).catch((err) => {
|
|
334
|
+
post({
|
|
335
|
+
type: "log",
|
|
336
|
+
shardId,
|
|
337
|
+
level: "error",
|
|
338
|
+
message: "projection worker init failed",
|
|
339
|
+
args: [errorToInfo(err)],
|
|
340
|
+
timestamp: Date.now()
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
break;
|
|
344
|
+
case "write-ready":
|
|
345
|
+
if (!initCompleted) {
|
|
346
|
+
logger.warn("write-ready received before init on shard @shardId", shardId);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
handleWriteReady(msg).catch((err) => {
|
|
350
|
+
post({
|
|
351
|
+
type: "log",
|
|
352
|
+
shardId,
|
|
353
|
+
level: "error",
|
|
354
|
+
message: "projection worker write-ready failed",
|
|
355
|
+
args: [errorToInfo(err)],
|
|
356
|
+
timestamp: Date.now()
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
break;
|
|
360
|
+
case "drain":
|
|
361
|
+
handleDrain(msg.correlationId).catch((err) => {
|
|
362
|
+
post({
|
|
363
|
+
type: "log",
|
|
364
|
+
shardId,
|
|
365
|
+
level: "error",
|
|
366
|
+
message: "projection worker drain failed",
|
|
367
|
+
args: [errorToInfo(err)],
|
|
368
|
+
timestamp: Date.now()
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
case "shutdown":
|
|
373
|
+
logger.info("projection worker shutting down: @shardId", shardId);
|
|
374
|
+
shutdownStack().finally(() => {
|
|
375
|
+
post({
|
|
376
|
+
type: "log",
|
|
377
|
+
shardId,
|
|
378
|
+
level: "info",
|
|
379
|
+
message: "projection worker shutdown",
|
|
380
|
+
args: [],
|
|
381
|
+
timestamp: Date.now()
|
|
382
|
+
});
|
|
383
|
+
process.exit(0);
|
|
384
|
+
});
|
|
385
|
+
break;
|
|
386
|
+
default: break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
parentPort.on("message", handleParentMessage);
|
|
390
|
+
parentPort.__reactorProjectionWorkerHarness = {
|
|
391
|
+
handleParentMessage,
|
|
392
|
+
get initCompleted() {
|
|
393
|
+
return initCompleted;
|
|
394
|
+
},
|
|
395
|
+
get shardId() {
|
|
396
|
+
return shardId;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/projection/projection-worker/projection-entry.ts
|
|
402
|
+
if (isMainThread || parentPort === null) throw new Error("projection-worker entry.ts must be run as a worker thread");
|
|
403
|
+
runProjectionWorker(parentPort);
|
|
404
|
+
//#endregion
|
|
405
|
+
|
|
406
|
+
//# sourceMappingURL=projection-entry.js.map
|