@powerhousedao/reactor 6.1.0-dev.0 → 6.1.0-dev.10
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,313 @@
|
|
|
1
|
+
import { n as ReactorEventTypes } from "./types-CxSpmNGK.js";
|
|
2
|
+
import { childLogger } from "document-model";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/projection/projection-shard-manager.ts
|
|
5
|
+
const DEFAULT_INIT_TIMEOUT_MS = 3e4;
|
|
6
|
+
const DEFAULT_SHUTDOWN_GRACE_MS = 5e3;
|
|
7
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 3e4;
|
|
8
|
+
const DEFAULT_CHAIN_DEPTH_REPORT_INTERVAL_MS = 250;
|
|
9
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
10
|
+
const FNV_PRIME = 16777619;
|
|
11
|
+
function bucketFor(documentId, numWorkers) {
|
|
12
|
+
if (numWorkers < 1) throw new Error(`bucketFor: numWorkers must be >= 1 (got ${numWorkers})`);
|
|
13
|
+
let hash = FNV_OFFSET_BASIS;
|
|
14
|
+
for (let i = 0; i < documentId.length; i++) {
|
|
15
|
+
hash ^= documentId.charCodeAt(i);
|
|
16
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
17
|
+
}
|
|
18
|
+
return (hash >>> 0) % numWorkers;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Host-side coordinator for N sharded projection workers.
|
|
22
|
+
*
|
|
23
|
+
* Implements {@link IReadModelCoordinator} so it slots into the same
|
|
24
|
+
* `readModelCoordinator` field on the reactor module as the in-process
|
|
25
|
+
* {@link ReadModelCoordinator}. The host subscribes to JOB_WRITE_READY
|
|
26
|
+
* exactly once; events are routed to a single shard by
|
|
27
|
+
* `bucketFor(documentId, shardCount)`. Each worker maintains the
|
|
28
|
+
* per-queueKey serial chain locally and forwards JOB_READ_READY and
|
|
29
|
+
* READMODEL_* events back to the host for the rest of the reactor (sync
|
|
30
|
+
* manager, awaiters, observers) to consume on the host bus.
|
|
31
|
+
*
|
|
32
|
+
* @see Sharded projection workers sub-feature brief
|
|
33
|
+
* (Powerhouse board wiki id: eb26f01f-8f68-4918-a6f6-ac7a4679b533)
|
|
34
|
+
*/
|
|
35
|
+
var ProjectionShardManager = class {
|
|
36
|
+
readModels = [];
|
|
37
|
+
config;
|
|
38
|
+
logger;
|
|
39
|
+
hostBus;
|
|
40
|
+
shards = [];
|
|
41
|
+
initPromises = /* @__PURE__ */ new Map();
|
|
42
|
+
pendingDrains = /* @__PURE__ */ new Map();
|
|
43
|
+
hostSubscription;
|
|
44
|
+
isRunning = false;
|
|
45
|
+
started = false;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
if (config.shardCount < 1) throw new Error(`ProjectionShardManager: shardCount must be >= 1 (got ${config.shardCount})`);
|
|
48
|
+
this.config = config;
|
|
49
|
+
this.logger = childLogger(["reactor", "projection-shard-manager"]);
|
|
50
|
+
this.hostBus = config.hostBus;
|
|
51
|
+
}
|
|
52
|
+
async startup() {
|
|
53
|
+
if (this.started) return;
|
|
54
|
+
this.started = true;
|
|
55
|
+
const initTimeoutMs = this.config.initTimeoutMs ?? DEFAULT_INIT_TIMEOUT_MS;
|
|
56
|
+
const reportIntervalMs = this.config.chainDepthReportIntervalMs ?? DEFAULT_CHAIN_DEPTH_REPORT_INTERVAL_MS;
|
|
57
|
+
const initPromises = [];
|
|
58
|
+
for (let i = 0; i < this.config.shardCount; i++) {
|
|
59
|
+
const shardId = `projection-shard-${i}`;
|
|
60
|
+
const transport = this.config.factory(i, shardId);
|
|
61
|
+
const state = {
|
|
62
|
+
shardIndex: i,
|
|
63
|
+
shardId,
|
|
64
|
+
transport,
|
|
65
|
+
ready: false,
|
|
66
|
+
lastDepth: 0,
|
|
67
|
+
lastDepthAt: 0,
|
|
68
|
+
poolInstrumentation: this.config.poolInstrumentations?.[i],
|
|
69
|
+
onMessage: (msg) => this.handleWorkerMessage(state, msg),
|
|
70
|
+
onError: (err) => this.handleTransportError(state, err),
|
|
71
|
+
onExit: (code) => this.handleTransportExit(state, code)
|
|
72
|
+
};
|
|
73
|
+
transport.on("message", state.onMessage);
|
|
74
|
+
transport.on("error", state.onError);
|
|
75
|
+
transport.on("exit", state.onExit);
|
|
76
|
+
this.shards.push(state);
|
|
77
|
+
const correlationId = randomUUID();
|
|
78
|
+
const initPromise = new Promise((resolve, reject) => {
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
this.initPromises.delete(correlationId);
|
|
81
|
+
reject(/* @__PURE__ */ new Error(`projection shard ${shardId} did not become ready within ${initTimeoutMs}ms`));
|
|
82
|
+
}, initTimeoutMs);
|
|
83
|
+
this.initPromises.set(correlationId, {
|
|
84
|
+
resolve,
|
|
85
|
+
reject,
|
|
86
|
+
timer
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
const init = {
|
|
90
|
+
type: "init",
|
|
91
|
+
correlationId,
|
|
92
|
+
shardId,
|
|
93
|
+
shardIndex: i,
|
|
94
|
+
shardCount: this.config.shardCount,
|
|
95
|
+
db: this.config.db,
|
|
96
|
+
models: this.config.models,
|
|
97
|
+
preReadyKinds: this.config.preReadyKinds,
|
|
98
|
+
postReadyKinds: this.config.postReadyKinds,
|
|
99
|
+
chainDepthReportIntervalMs: reportIntervalMs
|
|
100
|
+
};
|
|
101
|
+
transport.postMessage(init);
|
|
102
|
+
initPromises.push(initPromise);
|
|
103
|
+
}
|
|
104
|
+
await Promise.all(initPromises);
|
|
105
|
+
this.logger.info("projection shard manager ready: @count shards", this.shards.length);
|
|
106
|
+
}
|
|
107
|
+
start() {
|
|
108
|
+
if (this.isRunning) return;
|
|
109
|
+
this.hostSubscription = this.hostBus.subscribe(ReactorEventTypes.JOB_WRITE_READY, (_t, event) => {
|
|
110
|
+
this.routeWriteReady(event);
|
|
111
|
+
});
|
|
112
|
+
this.isRunning = true;
|
|
113
|
+
}
|
|
114
|
+
stop() {
|
|
115
|
+
if (!this.isRunning) return;
|
|
116
|
+
if (this.hostSubscription) {
|
|
117
|
+
this.hostSubscription();
|
|
118
|
+
this.hostSubscription = void 0;
|
|
119
|
+
}
|
|
120
|
+
this.isRunning = false;
|
|
121
|
+
}
|
|
122
|
+
async drain() {
|
|
123
|
+
if (this.shards.length === 0) return;
|
|
124
|
+
const drainTimeoutMs = this.config.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
125
|
+
const correlationId = randomUUID();
|
|
126
|
+
const remaining = new Set(this.shards.map((s) => s.shardId));
|
|
127
|
+
const promise = new Promise((resolve, reject) => {
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
this.pendingDrains.delete(correlationId);
|
|
130
|
+
reject(/* @__PURE__ */ new Error(`projection shards did not drain within ${drainTimeoutMs}ms (remaining: ${[...remaining].join(", ")})`));
|
|
131
|
+
}, drainTimeoutMs);
|
|
132
|
+
this.pendingDrains.set(correlationId, {
|
|
133
|
+
resolve,
|
|
134
|
+
reject,
|
|
135
|
+
remaining,
|
|
136
|
+
timer
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
for (const shard of this.shards) shard.transport.postMessage({
|
|
140
|
+
type: "drain",
|
|
141
|
+
correlationId
|
|
142
|
+
});
|
|
143
|
+
await promise;
|
|
144
|
+
}
|
|
145
|
+
getChainDepth() {
|
|
146
|
+
let total = 0;
|
|
147
|
+
for (const shard of this.shards) total += shard.lastDepth;
|
|
148
|
+
return total;
|
|
149
|
+
}
|
|
150
|
+
getShardDepths() {
|
|
151
|
+
return this.shards.map((shard) => ({
|
|
152
|
+
shardId: shard.shardId,
|
|
153
|
+
depth: shard.lastDepth,
|
|
154
|
+
timestamp: shard.lastDepthAt
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
async shutdown() {
|
|
158
|
+
this.stop();
|
|
159
|
+
const graceMs = this.config.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;
|
|
160
|
+
const correlationId = randomUUID();
|
|
161
|
+
for (const shard of this.shards) try {
|
|
162
|
+
const msg = {
|
|
163
|
+
type: "shutdown",
|
|
164
|
+
correlationId,
|
|
165
|
+
graceMs
|
|
166
|
+
};
|
|
167
|
+
shard.transport.postMessage(msg);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.logger.warn("projection shard postMessage(shutdown) failed for @shardId: @error", shard.shardId, err);
|
|
170
|
+
}
|
|
171
|
+
const terminationDeadline = Date.now() + graceMs;
|
|
172
|
+
while (this.shards.some((s) => s.ready) && Date.now() < terminationDeadline) await new Promise((resolve) => setTimeout(resolve, 25));
|
|
173
|
+
for (const shard of this.shards) {
|
|
174
|
+
try {
|
|
175
|
+
await shard.transport.terminate();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
this.logger.warn("projection shard terminate failed for @shardId: @error", shard.shardId, err);
|
|
178
|
+
}
|
|
179
|
+
shard.transport.off("message", shard.onMessage);
|
|
180
|
+
shard.transport.off("error", shard.onError);
|
|
181
|
+
shard.transport.off("exit", shard.onExit);
|
|
182
|
+
}
|
|
183
|
+
this.shards.length = 0;
|
|
184
|
+
}
|
|
185
|
+
routeWriteReady(event) {
|
|
186
|
+
if (event.operations.length === 0) return;
|
|
187
|
+
const documentId = event.operations[0].context.documentId;
|
|
188
|
+
const index = bucketFor(documentId, this.shards.length);
|
|
189
|
+
const shard = this.shards[index];
|
|
190
|
+
if (!shard.ready) {
|
|
191
|
+
this.logger.warn("dropping JOB_WRITE_READY: shard @index not ready", index);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
shard.transport.postMessage({
|
|
195
|
+
type: "write-ready",
|
|
196
|
+
jobId: event.jobId,
|
|
197
|
+
operations: event.operations,
|
|
198
|
+
jobMeta: event.jobMeta,
|
|
199
|
+
collectionMemberships: event.collectionMemberships
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
handleWorkerMessage(shard, msg) {
|
|
203
|
+
switch (msg.type) {
|
|
204
|
+
case "ready":
|
|
205
|
+
this.handleReady(shard, msg.correlationId);
|
|
206
|
+
return;
|
|
207
|
+
case "read-ready":
|
|
208
|
+
this.relayReadReady({
|
|
209
|
+
jobId: msg.jobId,
|
|
210
|
+
operations: msg.operations
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
case "readmodel-indexed":
|
|
214
|
+
this.relayReadModelIndexed({
|
|
215
|
+
jobId: msg.jobId,
|
|
216
|
+
readModelName: msg.readModelName,
|
|
217
|
+
stage: msg.stage,
|
|
218
|
+
durationMs: msg.durationMs,
|
|
219
|
+
operationCount: msg.operationCount,
|
|
220
|
+
success: msg.success
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
case "readmodel-batch-completed":
|
|
224
|
+
this.relayBatchCompleted({
|
|
225
|
+
jobId: msg.jobId,
|
|
226
|
+
batchSize: msg.batchSize,
|
|
227
|
+
chainWaitDurationMs: msg.chainWaitDurationMs,
|
|
228
|
+
preReadyDurationMs: msg.preReadyDurationMs,
|
|
229
|
+
emitDurationMs: msg.emitDurationMs,
|
|
230
|
+
postReadyDurationMs: msg.postReadyDurationMs
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
case "chain-depth":
|
|
234
|
+
shard.lastDepth = msg.depth;
|
|
235
|
+
shard.lastDepthAt = msg.timestamp;
|
|
236
|
+
return;
|
|
237
|
+
case "pool-acquire-samples":
|
|
238
|
+
this.handlePoolAcquireSamples(shard, msg);
|
|
239
|
+
return;
|
|
240
|
+
case "drained":
|
|
241
|
+
this.handleDrained(msg);
|
|
242
|
+
return;
|
|
243
|
+
case "log":
|
|
244
|
+
this.handleLog(shard, msg);
|
|
245
|
+
return;
|
|
246
|
+
default: return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
handlePoolAcquireSamples(shard, msg) {
|
|
250
|
+
if (!shard.poolInstrumentation) return;
|
|
251
|
+
shard.poolInstrumentation.updateStats({
|
|
252
|
+
size: msg.size,
|
|
253
|
+
idle: msg.idle,
|
|
254
|
+
waiting: msg.waiting
|
|
255
|
+
});
|
|
256
|
+
shard.poolInstrumentation.pushSamples(msg.durations);
|
|
257
|
+
}
|
|
258
|
+
handleReady(shard, correlationId) {
|
|
259
|
+
shard.ready = true;
|
|
260
|
+
const pending = this.initPromises.get(correlationId);
|
|
261
|
+
if (!pending) return;
|
|
262
|
+
this.initPromises.delete(correlationId);
|
|
263
|
+
clearTimeout(pending.timer);
|
|
264
|
+
pending.resolve();
|
|
265
|
+
}
|
|
266
|
+
handleDrained(msg) {
|
|
267
|
+
const pending = this.pendingDrains.get(msg.correlationId);
|
|
268
|
+
if (!pending) return;
|
|
269
|
+
pending.remaining.delete(msg.shardId);
|
|
270
|
+
if (pending.remaining.size === 0) {
|
|
271
|
+
this.pendingDrains.delete(msg.correlationId);
|
|
272
|
+
clearTimeout(pending.timer);
|
|
273
|
+
pending.resolve();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
handleLog(shard, msg) {
|
|
277
|
+
switch (msg.level) {
|
|
278
|
+
case "debug":
|
|
279
|
+
this.logger.debug(msg.message, ...msg.args);
|
|
280
|
+
return;
|
|
281
|
+
case "info":
|
|
282
|
+
this.logger.info(msg.message, ...msg.args);
|
|
283
|
+
return;
|
|
284
|
+
case "warn":
|
|
285
|
+
this.logger.warn(msg.message, ...msg.args);
|
|
286
|
+
return;
|
|
287
|
+
case "error":
|
|
288
|
+
this.logger.error(msg.message, ...msg.args);
|
|
289
|
+
return;
|
|
290
|
+
default: msg.level;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
handleTransportError(shard, err) {
|
|
294
|
+
this.logger.error("projection shard transport error @shardId: @error", shard.shardId, err);
|
|
295
|
+
}
|
|
296
|
+
handleTransportExit(shard, code) {
|
|
297
|
+
if (shard.ready) this.logger.warn("projection shard exited unexpectedly @shardId code=@code", shard.shardId, code);
|
|
298
|
+
shard.ready = false;
|
|
299
|
+
}
|
|
300
|
+
relayReadReady(event) {
|
|
301
|
+
this.hostBus.emit(ReactorEventTypes.JOB_READ_READY, event).catch((err) => this.logger.error("host JOB_READ_READY emit failed for job @jobId: @error", event.jobId, err));
|
|
302
|
+
}
|
|
303
|
+
relayReadModelIndexed(event) {
|
|
304
|
+
this.hostBus.emit(ReactorEventTypes.READMODEL_INDEXED, event).catch((err) => this.logger.error("host READMODEL_INDEXED emit failed for job @jobId: @error", event.jobId, err));
|
|
305
|
+
}
|
|
306
|
+
relayBatchCompleted(event) {
|
|
307
|
+
this.hostBus.emit(ReactorEventTypes.READMODEL_BATCH_COMPLETED, event).catch((err) => this.logger.error("host READMODEL_BATCH_COMPLETED emit failed for job @jobId: @error", event.jobId, err));
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
//#endregion
|
|
311
|
+
export { ProjectionShardManager };
|
|
312
|
+
|
|
313
|
+
//# sourceMappingURL=projection-shard-manager-_c7orNo5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection-shard-manager-_c7orNo5.js","names":[],"sources":["../src/projection/projection-shard-manager.ts"],"sourcesContent":["import { childLogger, type ILogger } from \"document-model\";\nimport { randomUUID } from \"node:crypto\";\nimport type { IEventBus } from \"../events/interfaces.js\";\nimport {\n ReactorEventTypes,\n type JobReadReadyEvent,\n type JobWriteReadyEvent,\n type ReadModelBatchCompletedEvent,\n type ReadModelIndexedEvent,\n type Unsubscribe,\n} from \"../events/types.js\";\nimport type {\n IReadModel,\n IReadModelCoordinator,\n} from \"../read-models/interfaces.js\";\nimport type { ForwardingPoolInstrumentation } from \"../storage/pool-instrumentation.js\";\nimport type {\n BuiltInReadModelKind,\n ChainDepthReport,\n DbConfig,\n ModelManifestEntry,\n ProjectionDrainedMessage,\n ProjectionInitMessage,\n ProjectionParentMessage,\n ProjectionPoolAcquireSamplesMessage,\n ProjectionWorkerMessage,\n} from \"./protocol.js\";\nimport type { IProjectionTransport } from \"./transport.js\";\n\nconst DEFAULT_INIT_TIMEOUT_MS = 30_000;\nconst DEFAULT_SHUTDOWN_GRACE_MS = 5_000;\nconst DEFAULT_DRAIN_TIMEOUT_MS = 30_000;\nconst DEFAULT_CHAIN_DEPTH_REPORT_INTERVAL_MS = 250;\n\nconst FNV_OFFSET_BASIS = 0x811c9dc5;\nconst FNV_PRIME = 0x01000193;\n\nfunction bucketFor(documentId: string, numWorkers: number): number {\n if (numWorkers < 1) {\n throw new Error(`bucketFor: numWorkers must be >= 1 (got ${numWorkers})`);\n }\n let hash = FNV_OFFSET_BASIS;\n for (let i = 0; i < documentId.length; i++) {\n hash ^= documentId.charCodeAt(i);\n hash = Math.imul(hash, FNV_PRIME);\n }\n return (hash >>> 0) % numWorkers;\n}\n\n/**\n * Factory that builds one projection-worker transport. Mirrors\n * `WorkerFactory` from the executor pool: lets tests inject fake\n * transports without spawning real worker threads.\n */\nexport type ProjectionWorkerFactory = (\n shardIndex: number,\n shardId: string,\n) => IProjectionTransport;\n\nexport type ProjectionShardManagerConfig = {\n shardCount: number;\n db: DbConfig;\n models: ModelManifestEntry[];\n preReadyKinds: BuiltInReadModelKind[];\n postReadyKinds: BuiltInReadModelKind[];\n factory: ProjectionWorkerFactory;\n logger: ILogger;\n hostBus: IEventBus;\n initTimeoutMs?: number;\n shutdownGraceMs?: number;\n drainTimeoutMs?: number;\n chainDepthReportIntervalMs?: number;\n /**\n * Host-side forwarding instrumentations indexed by shard index. The\n * manager routes each shard's `pool-acquire-samples` message to the\n * matching forwarder so the host's OpenTelemetry instrumentation records\n * acquire-wait latencies as if each shard's pg.Pool were local.\n */\n poolInstrumentations?: ForwardingPoolInstrumentation[];\n};\n\ntype ShardState = {\n shardIndex: number;\n shardId: string;\n transport: IProjectionTransport;\n ready: boolean;\n lastDepth: number;\n lastDepthAt: number;\n poolInstrumentation?: ForwardingPoolInstrumentation;\n onMessage: (msg: ProjectionWorkerMessage) => void;\n onError: (err: Error) => void;\n onExit: (code: number) => void;\n};\n\ntype PendingDrain = {\n resolve: () => void;\n reject: (err: Error) => void;\n remaining: Set<string>;\n timer: NodeJS.Timeout;\n};\n\n/**\n * Host-side coordinator for N sharded projection workers.\n *\n * Implements {@link IReadModelCoordinator} so it slots into the same\n * `readModelCoordinator` field on the reactor module as the in-process\n * {@link ReadModelCoordinator}. The host subscribes to JOB_WRITE_READY\n * exactly once; events are routed to a single shard by\n * `bucketFor(documentId, shardCount)`. Each worker maintains the\n * per-queueKey serial chain locally and forwards JOB_READ_READY and\n * READMODEL_* events back to the host for the rest of the reactor (sync\n * manager, awaiters, observers) to consume on the host bus.\n *\n * @see Sharded projection workers sub-feature brief\n * (Powerhouse board wiki id: eb26f01f-8f68-4918-a6f6-ac7a4679b533)\n */\nexport class ProjectionShardManager implements IReadModelCoordinator {\n readonly readModels: IReadModel[] = [];\n\n private readonly config: ProjectionShardManagerConfig;\n private readonly logger: ILogger;\n private readonly hostBus: IEventBus;\n private readonly shards: ShardState[] = [];\n private readonly initPromises = new Map<\n string,\n { resolve: () => void; reject: (err: Error) => void; timer: NodeJS.Timeout }\n >();\n private readonly pendingDrains = new Map<string, PendingDrain>();\n private hostSubscription?: Unsubscribe;\n private isRunning = false;\n private started = false;\n\n constructor(config: ProjectionShardManagerConfig) {\n if (config.shardCount < 1) {\n throw new Error(\n `ProjectionShardManager: shardCount must be >= 1 (got ${config.shardCount})`,\n );\n }\n this.config = config;\n this.logger = childLogger([\"reactor\", \"projection-shard-manager\"]);\n this.hostBus = config.hostBus;\n }\n\n async startup(): Promise<void> {\n if (this.started) {\n return;\n }\n this.started = true;\n const initTimeoutMs = this.config.initTimeoutMs ?? DEFAULT_INIT_TIMEOUT_MS;\n const reportIntervalMs =\n this.config.chainDepthReportIntervalMs ??\n DEFAULT_CHAIN_DEPTH_REPORT_INTERVAL_MS;\n\n const initPromises: Promise<void>[] = [];\n for (let i = 0; i < this.config.shardCount; i++) {\n const shardId = `projection-shard-${i}`;\n const transport = this.config.factory(i, shardId);\n const state: ShardState = {\n shardIndex: i,\n shardId,\n transport,\n ready: false,\n lastDepth: 0,\n lastDepthAt: 0,\n poolInstrumentation: this.config.poolInstrumentations?.[i],\n onMessage: (msg) => this.handleWorkerMessage(state, msg),\n onError: (err) => this.handleTransportError(state, err),\n onExit: (code) => this.handleTransportExit(state, code),\n };\n transport.on(\"message\", state.onMessage);\n transport.on(\"error\", state.onError);\n transport.on(\"exit\", state.onExit);\n this.shards.push(state);\n\n const correlationId = randomUUID();\n const initPromise = new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.initPromises.delete(correlationId);\n reject(\n new Error(\n `projection shard ${shardId} did not become ready within ${initTimeoutMs}ms`,\n ),\n );\n }, initTimeoutMs);\n this.initPromises.set(correlationId, { resolve, reject, timer });\n });\n\n const init: ProjectionInitMessage = {\n type: \"init\",\n correlationId,\n shardId,\n shardIndex: i,\n shardCount: this.config.shardCount,\n db: this.config.db,\n models: this.config.models,\n preReadyKinds: this.config.preReadyKinds,\n postReadyKinds: this.config.postReadyKinds,\n chainDepthReportIntervalMs: reportIntervalMs,\n };\n transport.postMessage(init);\n initPromises.push(initPromise);\n }\n\n await Promise.all(initPromises);\n this.logger.info(\n \"projection shard manager ready: @count shards\",\n this.shards.length,\n );\n }\n\n start(): void {\n if (this.isRunning) {\n return;\n }\n this.hostSubscription = this.hostBus.subscribe(\n ReactorEventTypes.JOB_WRITE_READY,\n (_t: number, event: JobWriteReadyEvent) => {\n this.routeWriteReady(event);\n },\n );\n this.isRunning = true;\n }\n\n stop(): void {\n if (!this.isRunning) {\n return;\n }\n if (this.hostSubscription) {\n this.hostSubscription();\n this.hostSubscription = undefined;\n }\n this.isRunning = false;\n }\n\n async drain(): Promise<void> {\n if (this.shards.length === 0) {\n return;\n }\n const drainTimeoutMs =\n this.config.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;\n const correlationId = randomUUID();\n const remaining = new Set(this.shards.map((s) => s.shardId));\n const promise = new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pendingDrains.delete(correlationId);\n reject(\n new Error(\n `projection shards did not drain within ${drainTimeoutMs}ms (remaining: ${[\n ...remaining,\n ].join(\", \")})`,\n ),\n );\n }, drainTimeoutMs);\n this.pendingDrains.set(correlationId, {\n resolve,\n reject,\n remaining,\n timer,\n });\n });\n for (const shard of this.shards) {\n shard.transport.postMessage({ type: \"drain\", correlationId });\n }\n await promise;\n }\n\n getChainDepth(): number {\n let total = 0;\n for (const shard of this.shards) {\n total += shard.lastDepth;\n }\n return total;\n }\n\n getShardDepths(): ChainDepthReport[] {\n return this.shards.map((shard) => ({\n shardId: shard.shardId,\n depth: shard.lastDepth,\n timestamp: shard.lastDepthAt,\n }));\n }\n\n async shutdown(): Promise<void> {\n this.stop();\n const graceMs = this.config.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\n const correlationId = randomUUID();\n for (const shard of this.shards) {\n try {\n const msg: ProjectionParentMessage = {\n type: \"shutdown\",\n correlationId,\n graceMs,\n };\n shard.transport.postMessage(msg);\n } catch (err) {\n this.logger.warn(\n \"projection shard postMessage(shutdown) failed for @shardId: @error\",\n shard.shardId,\n err,\n );\n }\n }\n const terminationDeadline = Date.now() + graceMs;\n while (\n this.shards.some((s) => s.ready) &&\n Date.now() < terminationDeadline\n ) {\n await new Promise<void>((resolve) => setTimeout(resolve, 25));\n }\n for (const shard of this.shards) {\n try {\n await shard.transport.terminate();\n } catch (err) {\n this.logger.warn(\n \"projection shard terminate failed for @shardId: @error\",\n shard.shardId,\n err,\n );\n }\n shard.transport.off(\"message\", shard.onMessage);\n shard.transport.off(\"error\", shard.onError);\n shard.transport.off(\"exit\", shard.onExit);\n }\n this.shards.length = 0;\n }\n\n private routeWriteReady(event: JobWriteReadyEvent): void {\n if (event.operations.length === 0) {\n return;\n }\n const documentId = event.operations[0]!.context.documentId;\n const index = bucketFor(documentId, this.shards.length);\n const shard = this.shards[index]!;\n if (!shard.ready) {\n this.logger.warn(\n \"dropping JOB_WRITE_READY: shard @index not ready\",\n index,\n );\n return;\n }\n shard.transport.postMessage({\n type: \"write-ready\",\n jobId: event.jobId,\n operations: event.operations,\n jobMeta: event.jobMeta,\n collectionMemberships: event.collectionMemberships,\n });\n }\n\n private handleWorkerMessage(\n shard: ShardState,\n msg: ProjectionWorkerMessage,\n ): void {\n switch (msg.type) {\n case \"ready\":\n this.handleReady(shard, msg.correlationId);\n return;\n case \"read-ready\":\n this.relayReadReady({\n jobId: msg.jobId,\n operations: msg.operations,\n });\n return;\n case \"readmodel-indexed\":\n this.relayReadModelIndexed({\n jobId: msg.jobId,\n readModelName: msg.readModelName,\n stage: msg.stage,\n durationMs: msg.durationMs,\n operationCount: msg.operationCount,\n success: msg.success,\n });\n return;\n case \"readmodel-batch-completed\":\n this.relayBatchCompleted({\n jobId: msg.jobId,\n batchSize: msg.batchSize,\n chainWaitDurationMs: msg.chainWaitDurationMs,\n preReadyDurationMs: msg.preReadyDurationMs,\n emitDurationMs: msg.emitDurationMs,\n postReadyDurationMs: msg.postReadyDurationMs,\n });\n return;\n case \"chain-depth\":\n shard.lastDepth = msg.depth;\n shard.lastDepthAt = msg.timestamp;\n return;\n case \"pool-acquire-samples\":\n this.handlePoolAcquireSamples(shard, msg);\n return;\n case \"drained\":\n this.handleDrained(msg);\n return;\n case \"log\":\n this.handleLog(shard, msg);\n return;\n default: {\n const exhaustive: never = msg;\n void exhaustive;\n return;\n }\n }\n }\n\n private handlePoolAcquireSamples(\n shard: ShardState,\n msg: ProjectionPoolAcquireSamplesMessage,\n ): void {\n if (!shard.poolInstrumentation) {\n return;\n }\n shard.poolInstrumentation.updateStats({\n size: msg.size,\n idle: msg.idle,\n waiting: msg.waiting,\n });\n shard.poolInstrumentation.pushSamples(msg.durations);\n }\n\n private handleReady(shard: ShardState, correlationId: string): void {\n shard.ready = true;\n const pending = this.initPromises.get(correlationId);\n if (!pending) {\n return;\n }\n this.initPromises.delete(correlationId);\n clearTimeout(pending.timer);\n pending.resolve();\n }\n\n private handleDrained(msg: ProjectionDrainedMessage): void {\n const pending = this.pendingDrains.get(msg.correlationId);\n if (!pending) {\n return;\n }\n pending.remaining.delete(msg.shardId);\n if (pending.remaining.size === 0) {\n this.pendingDrains.delete(msg.correlationId);\n clearTimeout(pending.timer);\n pending.resolve();\n }\n }\n\n private handleLog(\n shard: ShardState,\n msg: Extract<ProjectionWorkerMessage, { type: \"log\" }>,\n ): void {\n switch (msg.level) {\n case \"debug\":\n this.logger.debug(msg.message, ...msg.args);\n return;\n case \"info\":\n this.logger.info(msg.message, ...msg.args);\n return;\n case \"warn\":\n this.logger.warn(msg.message, ...msg.args);\n return;\n case \"error\":\n this.logger.error(msg.message, ...msg.args);\n return;\n default: {\n const exhaustive: never = msg.level;\n void exhaustive;\n }\n }\n void shard;\n }\n\n private handleTransportError(shard: ShardState, err: Error): void {\n this.logger.error(\n \"projection shard transport error @shardId: @error\",\n shard.shardId,\n err,\n );\n }\n\n private handleTransportExit(shard: ShardState, code: number): void {\n if (shard.ready) {\n this.logger.warn(\n \"projection shard exited unexpectedly @shardId code=@code\",\n shard.shardId,\n code,\n );\n }\n shard.ready = false;\n }\n\n private relayReadReady(event: JobReadReadyEvent): void {\n void this.hostBus\n .emit(ReactorEventTypes.JOB_READ_READY, event)\n .catch((err: unknown) =>\n this.logger.error(\n \"host JOB_READ_READY emit failed for job @jobId: @error\",\n event.jobId,\n err,\n ),\n );\n }\n\n private relayReadModelIndexed(event: ReadModelIndexedEvent): void {\n void this.hostBus\n .emit(ReactorEventTypes.READMODEL_INDEXED, event)\n .catch((err: unknown) =>\n this.logger.error(\n \"host READMODEL_INDEXED emit failed for job @jobId: @error\",\n event.jobId,\n err,\n ),\n );\n }\n\n private relayBatchCompleted(event: ReadModelBatchCompletedEvent): void {\n void this.hostBus\n .emit(ReactorEventTypes.READMODEL_BATCH_COMPLETED, event)\n .catch((err: unknown) =>\n this.logger.error(\n \"host READMODEL_BATCH_COMPLETED emit failed for job @jobId: @error\",\n event.jobId,\n err,\n ),\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/projection/projection-worker/index.ts
|
|
2
|
+
function resolveProjectionWorkerEntryPath() {
|
|
3
|
+
const url = new URL("./projection-entry.js", import.meta.url);
|
|
4
|
+
if (url.protocol !== "file:") return url.href;
|
|
5
|
+
let pathname = decodeURIComponent(url.pathname);
|
|
6
|
+
if (/^\/[A-Za-z]:[/\\]/.test(pathname)) pathname = pathname.slice(1);
|
|
7
|
+
return pathname;
|
|
8
|
+
}
|
|
9
|
+
const projectionWorkerEntryPath = resolveProjectionWorkerEntryPath();
|
|
10
|
+
//#endregion
|
|
11
|
+
export { projectionWorkerEntryPath };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=projection-worker-wI4PwcV2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection-worker-wI4PwcV2.js","names":[],"sources":["../src/projection/projection-worker/index.ts"],"sourcesContent":["function resolveProjectionWorkerEntryPath(): string {\n const url = new URL(\"./projection-entry.js\", import.meta.url);\n if (url.protocol !== \"file:\") {\n return url.href;\n }\n let pathname = decodeURIComponent(url.pathname);\n if (/^\\/[A-Za-z]:[/\\\\]/.test(pathname)) {\n pathname = pathname.slice(1);\n }\n return pathname;\n}\n\nexport const projectionWorkerEntryPath = resolveProjectionWorkerEntryPath();\n"],"mappings":";AAAA,SAAS,mCAA2C;CAClD,MAAM,MAAM,IAAI,IAAI,yBAAyB,OAAO,KAAK,IAAI;AAC7D,KAAI,IAAI,aAAa,QACnB,QAAO,IAAI;CAEb,IAAI,WAAW,mBAAmB,IAAI,SAAS;AAC/C,KAAI,oBAAoB,KAAK,SAAS,CACpC,YAAW,SAAS,MAAM,EAAE;AAE9B,QAAO;;AAGT,MAAa,4BAA4B,kCAAkC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
//#region src/executor/worker/transport.ts
|
|
3
|
+
/**
|
|
4
|
+
* Transport-agnostic surface that {@link WorkerHandle} consumes.
|
|
5
|
+
*
|
|
6
|
+
* The handle is written against this interface — not a `worker_threads.Worker`
|
|
7
|
+
* directly — so unit tests can swap in a fake transport and future cards can
|
|
8
|
+
* add a `child_process` adapter without touching the handle.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a `node:worker_threads` Worker so it satisfies {@link IWorkerTransport}.
|
|
12
|
+
*/
|
|
13
|
+
function createThreadTransport(scriptPath, options) {
|
|
14
|
+
const worker = new Worker(scriptPath, options);
|
|
15
|
+
return {
|
|
16
|
+
postMessage(message) {
|
|
17
|
+
worker.postMessage(message);
|
|
18
|
+
},
|
|
19
|
+
on(event, listener) {
|
|
20
|
+
worker.on(event, listener);
|
|
21
|
+
},
|
|
22
|
+
off(event, listener) {
|
|
23
|
+
worker.off(event, listener);
|
|
24
|
+
},
|
|
25
|
+
async terminate() {
|
|
26
|
+
return await worker.terminate();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { createThreadTransport };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=transport-ByGviWdZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-ByGviWdZ.js","names":[],"sources":["../src/executor/worker/transport.ts"],"sourcesContent":["/**\n * Transport-agnostic surface that {@link WorkerHandle} consumes.\n *\n * The handle is written against this interface — not a `worker_threads.Worker`\n * directly — so unit tests can swap in a fake transport and future cards can\n * add a `child_process` adapter without touching the handle.\n */\n\nimport { Worker, type WorkerOptions } from \"node:worker_threads\";\nimport type { ParentMessage, WorkerMessage } from \"./protocol.js\";\n\nexport type WorkerTransportEventMap = {\n message: WorkerMessage;\n error: Error;\n exit: number;\n};\n\nexport type WorkerTransportEvent = keyof WorkerTransportEventMap;\n\nexport type WorkerTransportListener<E extends WorkerTransportEvent> = (\n payload: WorkerTransportEventMap[E],\n) => void;\n\n/**\n * Minimal subset of `worker_threads.Worker` that {@link WorkerHandle} relies\n * on. Implementations must guarantee that `postMessage` payloads are\n * structured-cloned across the boundary.\n */\nexport interface IWorkerTransport {\n postMessage(message: ParentMessage): void;\n on<E extends WorkerTransportEvent>(\n event: E,\n listener: WorkerTransportListener<E>,\n ): void;\n off<E extends WorkerTransportEvent>(\n event: E,\n listener: WorkerTransportListener<E>,\n ): void;\n terminate(): Promise<number>;\n}\n\n/**\n * Wraps a `node:worker_threads` Worker so it satisfies {@link IWorkerTransport}.\n */\nexport function createThreadTransport(\n scriptPath: string | URL,\n options?: WorkerOptions,\n): IWorkerTransport {\n const worker = new Worker(scriptPath, options);\n return {\n postMessage(message) {\n worker.postMessage(message);\n },\n on(event, listener) {\n worker.on(event, listener as (...args: unknown[]) => void);\n },\n off(event, listener) {\n worker.off(event, listener as (...args: unknown[]) => void);\n },\n async terminate() {\n return await worker.terminate();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;AA4CA,SAAgB,sBACd,YACA,SACkB;CAClB,MAAM,SAAS,IAAI,OAAO,YAAY,QAAQ;AAC9C,QAAO;EACL,YAAY,SAAS;AACnB,UAAO,YAAY,QAAQ;;EAE7B,GAAG,OAAO,UAAU;AAClB,UAAO,GAAG,OAAO,SAAyC;;EAE5D,IAAI,OAAO,UAAU;AACnB,UAAO,IAAI,OAAO,SAAyC;;EAE7D,MAAM,YAAY;AAChB,UAAO,MAAM,OAAO,WAAW;;EAElC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
//#region src/projection/transport.ts
|
|
3
|
+
function createProjectionThreadTransport(scriptPath, options) {
|
|
4
|
+
const worker = new Worker(scriptPath, options);
|
|
5
|
+
return {
|
|
6
|
+
postMessage(message) {
|
|
7
|
+
worker.postMessage(message);
|
|
8
|
+
},
|
|
9
|
+
on(event, listener) {
|
|
10
|
+
worker.on(event, listener);
|
|
11
|
+
},
|
|
12
|
+
off(event, listener) {
|
|
13
|
+
worker.off(event, listener);
|
|
14
|
+
},
|
|
15
|
+
async terminate() {
|
|
16
|
+
return await worker.terminate();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { createProjectionThreadTransport };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=transport-CuogVKN_.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-CuogVKN_.js","names":[],"sources":["../src/projection/transport.ts"],"sourcesContent":["import { Worker, type WorkerOptions } from \"node:worker_threads\";\nimport type {\n ProjectionParentMessage,\n ProjectionWorkerMessage,\n} from \"./protocol.js\";\n\nexport type ProjectionTransportEventMap = {\n message: ProjectionWorkerMessage;\n error: Error;\n exit: number;\n};\n\nexport type ProjectionTransportEvent = keyof ProjectionTransportEventMap;\n\nexport type ProjectionTransportListener<E extends ProjectionTransportEvent> = (\n payload: ProjectionTransportEventMap[E],\n) => void;\n\n/**\n * Minimal subset of `worker_threads.Worker` the {@link ProjectionShardManager}\n * relies on. The host writes against this interface so tests can swap in\n * a fake transport without spawning real worker threads.\n */\nexport interface IProjectionTransport {\n postMessage(message: ProjectionParentMessage): void;\n on<E extends ProjectionTransportEvent>(\n event: E,\n listener: ProjectionTransportListener<E>,\n ): void;\n off<E extends ProjectionTransportEvent>(\n event: E,\n listener: ProjectionTransportListener<E>,\n ): void;\n terminate(): Promise<number>;\n}\n\nexport function createProjectionThreadTransport(\n scriptPath: string | URL,\n options?: WorkerOptions,\n): IProjectionTransport {\n const worker = new Worker(scriptPath, options);\n return {\n postMessage(message) {\n worker.postMessage(message);\n },\n on(event, listener) {\n worker.on(event, listener as (...args: unknown[]) => void);\n },\n off(event, listener) {\n worker.off(event, listener as (...args: unknown[]) => void);\n },\n async terminate() {\n return await worker.terminate();\n },\n };\n}\n"],"mappings":";;AAoCA,SAAgB,gCACd,YACA,SACsB;CACtB,MAAM,SAAS,IAAI,OAAO,YAAY,QAAQ;AAC9C,QAAO;EACL,YAAY,SAAS;AACnB,UAAO,YAAY,QAAQ;;EAE7B,GAAG,OAAO,UAAU;AAClB,UAAO,GAAG,OAAO,SAAyC;;EAE5D,IAAI,OAAO,UAAU;AACnB,UAAO,IAAI,OAAO,SAAyC;;EAE7D,MAAM,YAAY;AAChB,UAAO,MAAM,OAAO,WAAW;;EAElC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/events/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Custom error class that aggregates multiple errors from event subscribers.
|
|
4
|
+
*/
|
|
5
|
+
var EventBusAggregateError = class extends Error {
|
|
6
|
+
errors;
|
|
7
|
+
constructor(errors) {
|
|
8
|
+
const message = `EventBus emit failed with ${errors.length} error(s): ${errors.map((e) => {
|
|
9
|
+
if (e && typeof e === "object" && "message" in e) return e.message;
|
|
10
|
+
return String(e);
|
|
11
|
+
}).join("; ")}`;
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "EventBusAggregateError";
|
|
14
|
+
this.errors = errors;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Event types for reactor lifecycle events.
|
|
19
|
+
*/
|
|
20
|
+
const ReactorEventTypes = {
|
|
21
|
+
JOB_PENDING: 10001,
|
|
22
|
+
JOB_RUNNING: 10002,
|
|
23
|
+
JOB_WRITE_READY: 10003,
|
|
24
|
+
JOB_READ_READY: 10004,
|
|
25
|
+
JOB_FAILED: 10005,
|
|
26
|
+
READMODEL_BATCH_COMPLETED: 10006,
|
|
27
|
+
READMODEL_INDEXED: 10007
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ReactorEventTypes as n, EventBusAggregateError as t };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=types-CxSpmNGK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-CxSpmNGK.js","names":[],"sources":["../src/events/types.ts"],"sourcesContent":["import type { OperationWithContext } from \"@powerhousedao/shared/document-model\";\nimport type { Job } from \"../queue/types.js\";\nimport type { JobMeta } from \"../shared/types.js\";\n\n/**\n * Describes a function to unsubscribe from an event.\n */\nexport type Unsubscribe = () => void;\n\n/**\n * A subscriber is a function that is called when an event is emitted.\n *\n * It is passed the event type and the data.\n * It can return a promise or a value.\n * If it returns a promise, the event bus will wait for the promise to resolve before calling the next subscriber.\n * If it throws an error, the event bus will reject with an aggregate error of all errors.\n *\n * @param type - The type of event to emit.\n * @param data - The data to pass to the subscriber.\n */\nexport type Subscriber = (type: number, data: any) => void | Promise<void>;\n\n/**\n * Custom error class that aggregates multiple errors from event subscribers.\n */\nexport class EventBusAggregateError extends Error {\n public readonly errors: any[];\n\n constructor(errors: unknown[]) {\n const message = `EventBus emit failed with ${errors.length} error(s): ${errors\n .map((e) => {\n if (e && typeof e === \"object\" && \"message\" in e) {\n return (e as Error).message;\n }\n return String(e);\n })\n .join(\"; \")}`;\n super(message);\n\n this.name = \"EventBusAggregateError\";\n this.errors = errors;\n }\n}\n\n/**\n * Event types for reactor lifecycle events.\n */\nexport const ReactorEventTypes = {\n JOB_PENDING: 10001,\n JOB_RUNNING: 10002,\n JOB_WRITE_READY: 10003,\n JOB_READ_READY: 10004,\n JOB_FAILED: 10005,\n READMODEL_BATCH_COMPLETED: 10006,\n READMODEL_INDEXED: 10007,\n} as const;\n\n/**\n * Stage within ReadModelCoordinator.runChain. Used as a dimension on\n * stage-attribution events and histograms.\n */\nexport type ReadModelStage = \"pre_ready\" | \"emit\" | \"post_ready\";\n\n/**\n * Stage in which an individual read model ran. The emit stage does not\n * involve a read model so it is excluded from this discriminant.\n */\nexport type ReadModelIndexingStage = \"pre_ready\" | \"post_ready\";\n\n/**\n * Event emitted when a job is registered and waiting to be executed.\n */\nexport type JobPendingEvent = {\n jobId: string;\n jobMeta: JobMeta;\n};\n\n/**\n * Event emitted when a job starts executing.\n */\nexport type JobRunningEvent = {\n jobId: string;\n jobMeta: JobMeta;\n};\n\n/**\n * Event emitted when operations are written to IOperationStore.\n * Contains the operations directly to avoid round-trip queries.\n */\nexport type JobWriteReadyEvent = {\n jobId: string;\n operations: OperationWithContext[];\n jobMeta: JobMeta;\n /**\n * Maps documentId to the collection IDs it belongs to.\n * Used by SyncManager to route operations only to remotes\n * whose collection contains the document.\n */\n collectionMemberships?: Record<string, string[]>;\n};\n\n/**\n * Event emitted after all read models have finished processing operations.\n * This event fires after JOB_WRITE_READY and guarantees that:\n * - All read models (DocumentView, DocumentIndexer, etc.) have indexed the operations\n * - All consistency trackers have been updated with the new operation indices\n * - Queries without consistency tokens will now see the updated data\n *\n * This event is useful for:\n * - Test synchronization (knowing when read models are ready)\n * - Observability (measuring read model latency)\n * - Event-driven workflows (triggering downstream processes)\n */\nexport type JobReadReadyEvent = {\n jobId: string;\n operations: OperationWithContext[];\n};\n\n/**\n * Event emitted when a job fails with an unrecoverable error.\n * This event allows the JobAwaiter and other subscribers to react to job failures\n * without polling.\n */\nexport type JobFailedEvent = {\n jobId: string;\n error: Error;\n job?: Job;\n};\n\n/**\n * Event emitted once per batch processed by ReadModelCoordinator.runChain,\n * after all three projection stages complete. Carries per-stage wall times,\n * the chain wait time, and the batch size so projection cost can be\n * attributed by an observer.\n */\nexport type ReadModelBatchCompletedEvent = {\n jobId: string;\n batchSize: number;\n chainWaitDurationMs: number;\n preReadyDurationMs: number;\n emitDurationMs: number;\n postReadyDurationMs: number;\n};\n\n/**\n * Event emitted once per individual read model per batch and stage, after\n * that read model's indexOperations call resolves (or rejects). Lets\n * observers attribute projection cost to a specific read model.\n */\nexport type ReadModelIndexedEvent = {\n jobId: string;\n readModelName: string;\n stage: ReadModelIndexingStage;\n durationMs: number;\n operationCount: number;\n success: boolean;\n};\n"],"mappings":";;;;AAyBA,IAAa,yBAAb,cAA4C,MAAM;CAChD;CAEA,YAAY,QAAmB;EAC7B,MAAM,UAAU,6BAA6B,OAAO,OAAO,aAAa,OACrE,KAAK,MAAM;AACV,OAAI,KAAK,OAAO,MAAM,YAAY,aAAa,EAC7C,QAAQ,EAAY;AAEtB,UAAO,OAAO,EAAE;IAChB,CACD,KAAK,KAAK;AACb,QAAM,QAAQ;AAEd,OAAK,OAAO;AACZ,OAAK,SAAS;;;;;;AAOlB,MAAa,oBAAoB;CAC/B,aAAa;CACb,aAAa;CACb,iBAAiB;CACjB,gBAAgB;CAChB,YAAY;CACZ,2BAA2B;CAC3B,mBAAmB;CACpB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { M as __exportAll } from "./drive-container-types-BNpMlgT_.js";
|
|
2
|
+
import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
|
|
3
|
+
//#region src/executor/worker/index.ts
|
|
4
|
+
var worker_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
createForwardingLogger: () => createForwardingLogger,
|
|
6
|
+
errorToInfo: () => errorToInfo,
|
|
7
|
+
sanitizeArg: () => sanitizeArg,
|
|
8
|
+
workerEntryPath: () => workerEntryPath
|
|
9
|
+
});
|
|
10
|
+
function resolveWorkerEntryPath() {
|
|
11
|
+
const url = new URL("./entry.js", import.meta.url);
|
|
12
|
+
if (url.protocol !== "file:") return url.href;
|
|
13
|
+
let pathname = decodeURIComponent(url.pathname);
|
|
14
|
+
if (/^\/[A-Za-z]:[/\\]/.test(pathname)) pathname = pathname.slice(1);
|
|
15
|
+
return pathname;
|
|
16
|
+
}
|
|
17
|
+
/** Absolute path to the worker entry script for use with `new Worker(workerEntryPath)`. */
|
|
18
|
+
const workerEntryPath = resolveWorkerEntryPath();
|
|
19
|
+
//#endregion
|
|
20
|
+
export { worker_exports as n, workerEntryPath as t };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=worker-SUoDhurA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-SUoDhurA.js","names":[],"sources":["../src/executor/worker/index.ts"],"sourcesContent":["export { createForwardingLogger } from \"./forwarding-logger.js\";\nexport { errorToInfo, sanitizeArg } from \"./sanitize.js\";\n\nfunction resolveWorkerEntryPath(): string {\n const url = new URL(\"./entry.js\", import.meta.url);\n if (url.protocol !== \"file:\") {\n return url.href;\n }\n let pathname = decodeURIComponent(url.pathname);\n if (/^\\/[A-Za-z]:[/\\\\]/.test(pathname)) {\n pathname = pathname.slice(1);\n }\n return pathname;\n}\n\n/** Absolute path to the worker entry script for use with `new Worker(workerEntryPath)`. */\nexport const workerEntryPath = resolveWorkerEntryPath();\n"],"mappings":";;;;;;;;;AAGA,SAAS,yBAAiC;CACxC,MAAM,MAAM,IAAI,IAAI,cAAc,OAAO,KAAK,IAAI;AAClD,KAAI,IAAI,aAAa,QACnB,QAAO,IAAI;CAEb,IAAI,WAAW,mBAAmB,IAAI,SAAS;AAC/C,KAAI,oBAAoB,KAAK,SAAS,CACpC,YAAW,SAAS,MAAM,EAAE;AAE9B,QAAO;;;AAIT,MAAa,kBAAkB,wBAAwB"}
|