@voyantjs/workflows-orchestrator-node 0.107.5 → 0.107.7

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.
Files changed (69) hide show
  1. package/dist/dashboard-chunks.d.ts +17 -0
  2. package/dist/dashboard-chunks.d.ts.map +1 -0
  3. package/dist/dashboard-chunks.js +19 -0
  4. package/dist/dashboard-http-server.d.ts +6 -0
  5. package/dist/dashboard-http-server.d.ts.map +1 -0
  6. package/dist/dashboard-http-server.js +99 -0
  7. package/dist/dashboard-metrics.d.ts +3 -0
  8. package/dist/dashboard-metrics.d.ts.map +1 -0
  9. package/dist/dashboard-metrics.js +26 -0
  10. package/dist/dashboard-request.d.ts +7 -0
  11. package/dist/dashboard-request.d.ts.map +1 -0
  12. package/dist/dashboard-request.js +436 -0
  13. package/dist/dashboard-server.d.ts +9 -171
  14. package/dist/dashboard-server.d.ts.map +1 -1
  15. package/dist/dashboard-server.js +7 -1229
  16. package/dist/dashboard-sse.d.ts +7 -0
  17. package/dist/dashboard-sse.d.ts.map +1 -0
  18. package/dist/dashboard-sse.js +134 -0
  19. package/dist/dashboard-static.d.ts +7 -0
  20. package/dist/dashboard-static.d.ts.map +1 -0
  21. package/dist/dashboard-static.js +89 -0
  22. package/dist/dashboard-types.d.ts +134 -0
  23. package/dist/dashboard-types.d.ts.map +1 -0
  24. package/dist/dashboard-types.js +1 -0
  25. package/dist/node-selfhost-defaults.d.ts +7 -0
  26. package/dist/node-selfhost-defaults.d.ts.map +1 -0
  27. package/dist/node-selfhost-defaults.js +8 -0
  28. package/dist/node-selfhost-deps.d.ts +4 -0
  29. package/dist/node-selfhost-deps.d.ts.map +1 -0
  30. package/dist/node-selfhost-deps.js +403 -0
  31. package/dist/node-selfhost-resume-input.d.ts +4 -0
  32. package/dist/node-selfhost-resume-input.d.ts.map +1 -0
  33. package/dist/node-selfhost-resume-input.js +20 -0
  34. package/dist/node-standalone-driver.d.ts.map +1 -1
  35. package/dist/node-standalone-driver.js +40 -3
  36. package/dist/node-step-runner.d.ts +3 -0
  37. package/dist/node-step-runner.d.ts.map +1 -0
  38. package/dist/node-step-runner.js +26 -0
  39. package/dist/postgres-manifest-store.d.ts.map +1 -1
  40. package/dist/postgres-manifest-store.js +6 -2
  41. package/dist/postgres-run-record-store.js +1 -1
  42. package/dist/postgres-schema.d.ts.map +1 -1
  43. package/dist/postgres-schema.js +2 -0
  44. package/dist/sleep-alarm-manager.d.ts.map +1 -1
  45. package/dist/sleep-alarm-manager.js +9 -1
  46. package/dist/store-stream.d.ts.map +1 -1
  47. package/dist/store-stream.js +9 -1
  48. package/dist/wakeup-poller.d.ts.map +1 -1
  49. package/dist/wakeup-poller.js +9 -1
  50. package/package.json +3 -3
  51. package/src/dashboard-chunks.ts +35 -0
  52. package/src/dashboard-http-server.ts +118 -0
  53. package/src/dashboard-metrics.ts +39 -0
  54. package/src/dashboard-request.ts +488 -0
  55. package/src/dashboard-server.ts +17 -1535
  56. package/src/dashboard-sse.ts +150 -0
  57. package/src/dashboard-static.ts +88 -0
  58. package/src/dashboard-types.ts +106 -0
  59. package/src/node-selfhost-defaults.ts +9 -0
  60. package/src/node-selfhost-deps.ts +495 -0
  61. package/src/node-selfhost-resume-input.ts +27 -0
  62. package/src/node-standalone-driver.ts +59 -3
  63. package/src/node-step-runner.ts +28 -0
  64. package/src/postgres-manifest-store.ts +2 -0
  65. package/src/postgres-run-record-store.ts +1 -1
  66. package/src/postgres-schema.ts +2 -0
  67. package/src/sleep-alarm-manager.ts +12 -1
  68. package/src/store-stream.ts +12 -1
  69. package/src/wakeup-poller.ts +12 -1
@@ -1,1229 +1,7 @@
1
- import { readFile, stat } from "node:fs/promises";
2
- import { createServer } from "node:http";
3
- import { extname, join, resolve as resolvePath } from "node:path";
4
- import { URL } from "node:url";
5
- import { handleStepRequest } from "@voyantjs/workflows/handler";
6
- import { createInMemoryRateLimiter } from "@voyantjs/workflows/rate-limit";
7
- import { createInMemoryRunStore, resume, resumeDueAlarms, trigger, } from "@voyantjs/workflows-orchestrator";
8
- import { loadEntryFile } from "./entry-loader.js";
9
- import { durationToMs, generateLocalRunId } from "./local-runtime.js";
10
- import { createPersistentWakeupManager } from "./persistent-wakeup-manager.js";
11
- import { createPostgresConnection } from "./postgres.js";
12
- import { createPostgresSnapshotRunStore } from "./postgres-snapshot-run-store.js";
13
- import { createPostgresWakeupStore } from "./postgres-wakeup-store.js";
14
- import { buildResumeJournal, buildSeededResumeJournal } from "./resume-run.js";
15
- import { recordToSnapshot, snapshotToRecord } from "./run-record-snapshot.js";
16
- import { createScheduler } from "./scheduler.js";
17
- import { createFsSnapshotRunStore, } from "./snapshot-run-store.js";
18
- import { createStoreStream } from "./store-stream.js";
19
- import { createFsWakeupStore } from "./wakeup-store.js";
20
- export function createChunkBus() {
21
- const subs = new Set();
22
- return {
23
- publish(event) {
24
- for (const fn of subs) {
25
- try {
26
- fn(event);
27
- }
28
- catch {
29
- // Ignore subscriber errors so streaming keeps going.
30
- }
31
- }
32
- },
33
- subscribe(fn) {
34
- subs.add(fn);
35
- return () => subs.delete(fn);
36
- },
37
- };
38
- }
39
- export async function handleRequest(req, deps) {
40
- const method = (req.method ?? "GET").toUpperCase();
41
- const url = new URL(req.url, "http://local");
42
- if (method === "OPTIONS") {
43
- return {
44
- status: 204,
45
- headers: {
46
- "access-control-allow-origin": "*",
47
- "access-control-allow-methods": "GET, OPTIONS, POST",
48
- "access-control-allow-headers": "content-type",
49
- },
50
- body: "",
51
- };
52
- }
53
- if (method === "POST") {
54
- const cancelMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/cancel$/);
55
- if (cancelMatch) {
56
- if (!deps.cancelRun) {
57
- return json(501, {
58
- error: "cancel_not_supported",
59
- message: "This self-host server was started without a workflow entry. " +
60
- "Restart with `--file <path>` to enable cancellation.",
61
- });
62
- }
63
- const runId = decodeURIComponent(cancelMatch[1]);
64
- const result = await deps.cancelRun({ runId });
65
- if (!result.ok) {
66
- return json(result.exitCode === 2 ? 400 : 404, {
67
- error: "cancel_failed",
68
- message: result.message,
69
- });
70
- }
71
- return json(200, { saved: result.saved });
72
- }
73
- const resumeMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/resume$/);
74
- if (resumeMatch) {
75
- if (!deps.resumeRun) {
76
- return json(501, {
77
- error: "resume_not_supported",
78
- message: "This self-host server was started without a workflow entry. " +
79
- "Restart with `--file <path>` to enable failed-step resume.",
80
- });
81
- }
82
- const parsed = parseResumeRequestBody(req.body);
83
- if (!parsed.ok) {
84
- return json(parsed.status, { error: parsed.error, message: parsed.message });
85
- }
86
- const parentRunId = decodeURIComponent(resumeMatch[1]);
87
- const result = await deps.resumeRun({ parentRunId, ...parsed.body });
88
- if (!result.ok) {
89
- return json(result.exitCode === 2 ? 400 : 404, {
90
- error: "resume_failed",
91
- message: result.message,
92
- });
93
- }
94
- return json(200, {
95
- saved: result.saved,
96
- parentRunId: result.parentRunId,
97
- resumeFromStep: result.resumeFromStep,
98
- });
99
- }
100
- const eventsMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/events$/);
101
- const signalsMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/signals$/);
102
- const tokenMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/tokens\/([^/]+)$/);
103
- if (eventsMatch || signalsMatch || tokenMatch) {
104
- if (!deps.injectWaitpoint) {
105
- return json(501, {
106
- error: "inject_not_supported",
107
- message: "This self-host server was started without a workflow entry. " +
108
- "Restart with `--file <path>` to enable event / signal / token injection.",
109
- });
110
- }
111
- let parsed;
112
- try {
113
- parsed = req.body ? JSON.parse(req.body) : {};
114
- }
115
- catch (err) {
116
- return json(400, {
117
- error: "invalid_json",
118
- message: err instanceof Error ? err.message : String(err),
119
- });
120
- }
121
- let injection;
122
- if (eventsMatch) {
123
- if (typeof parsed.eventType !== "string" || parsed.eventType.length === 0) {
124
- return json(400, { error: "invalid_body", message: "`eventType` (string) is required" });
125
- }
126
- injection = { kind: "EVENT", eventType: parsed.eventType, payload: parsed.payload };
127
- }
128
- else if (signalsMatch) {
129
- if (typeof parsed.name !== "string" || parsed.name.length === 0) {
130
- return json(400, { error: "invalid_body", message: "`name` (string) is required" });
131
- }
132
- injection = { kind: "SIGNAL", name: parsed.name, payload: parsed.payload };
133
- }
134
- else {
135
- injection = {
136
- kind: "MANUAL",
137
- tokenId: decodeURIComponent(tokenMatch[2]),
138
- payload: parsed.payload,
139
- };
140
- }
141
- const runId = decodeURIComponent((eventsMatch?.[1] ?? signalsMatch?.[1] ?? tokenMatch?.[1]));
142
- const result = await deps.injectWaitpoint({ runId, injection });
143
- if (!result.ok) {
144
- return json(result.exitCode === 2 ? 400 : 404, {
145
- error: "inject_failed",
146
- message: result.message,
147
- });
148
- }
149
- return json(200, { saved: result.saved });
150
- }
151
- if (url.pathname === "/api/runs") {
152
- if (!deps.triggerRun) {
153
- return json(501, {
154
- error: "trigger_not_supported",
155
- message: "This self-host server was started without a workflow entry. " +
156
- "Restart with `--file <path>` to enable triggering.",
157
- });
158
- }
159
- let parsed;
160
- try {
161
- parsed = req.body ? JSON.parse(req.body) : {};
162
- }
163
- catch (err) {
164
- return json(400, {
165
- error: "invalid_json",
166
- message: err instanceof Error ? err.message : String(err),
167
- });
168
- }
169
- if (typeof parsed.workflowId !== "string" || parsed.workflowId.length === 0) {
170
- return json(400, {
171
- error: "invalid_body",
172
- message: "`workflowId` (string) is required",
173
- });
174
- }
175
- if (parsed.runId !== undefined && typeof parsed.runId !== "string") {
176
- return json(400, {
177
- error: "invalid_body",
178
- message: "`runId` must be a string when provided",
179
- });
180
- }
181
- if (parsed.tags !== undefined && !isStringArray(parsed.tags)) {
182
- return json(400, {
183
- error: "invalid_body",
184
- message: "`tags` must be an array of strings when provided",
185
- });
186
- }
187
- if (parsed.triggeredByUserId !== undefined &&
188
- parsed.triggeredByUserId !== null &&
189
- typeof parsed.triggeredByUserId !== "string") {
190
- return json(400, {
191
- error: "invalid_body",
192
- message: "`triggeredByUserId` must be a string or null when provided",
193
- });
194
- }
195
- const result = await deps.triggerRun({
196
- workflowId: parsed.workflowId,
197
- input: parsed.input,
198
- runId: parsed.runId,
199
- tags: parsed.tags,
200
- triggeredByUserId: parsed.triggeredByUserId,
201
- });
202
- if (!result.ok) {
203
- return json(result.exitCode === 2 ? 400 : 404, {
204
- error: "trigger_failed",
205
- message: result.message,
206
- });
207
- }
208
- return json(200, { saved: result.saved });
209
- }
210
- return json(404, { error: "route_not_found", path: url.pathname });
211
- }
212
- if (method !== "GET" && method !== "HEAD") {
213
- return json(405, { error: "method_not_allowed", allowed: ["GET", "HEAD", "OPTIONS", "POST"] });
214
- }
215
- if (url.pathname === "/healthz") {
216
- const report = await resolveHealthReport(deps.healthCheck, {
217
- ok: true,
218
- service: "voyant-workflows-selfhost",
219
- });
220
- return json(report.ok ? 200 : 503, report);
221
- }
222
- if (url.pathname === "/readyz") {
223
- const report = await resolveHealthReport(deps.readinessCheck, {
224
- ok: Boolean(deps.triggerRun),
225
- service: "voyant-workflows-selfhost",
226
- checks: {
227
- workflowEntry: deps.triggerRun ? "ok" : "error",
228
- },
229
- details: deps.triggerRun
230
- ? undefined
231
- : {
232
- workflowEntry: "This self-host server was started without a workflow entry.",
233
- },
234
- });
235
- return json(report.ok ? 200 : 503, report);
236
- }
237
- if (url.pathname === "/metrics") {
238
- const body = await resolveMetricsBody(deps.collectMetrics);
239
- return {
240
- status: 200,
241
- headers: {
242
- "content-type": "text/plain; version=0.0.4; charset=utf-8",
243
- "cache-control": "no-store",
244
- },
245
- body,
246
- };
247
- }
248
- if (url.pathname === "/" || url.pathname === "") {
249
- if (deps.hasStaticDashboard && deps.readStatic) {
250
- const bytes = await deps.readStatic("index.html");
251
- if (bytes) {
252
- return {
253
- status: 200,
254
- headers: {
255
- "content-type": "text/html; charset=utf-8",
256
- "cache-control": "no-store",
257
- },
258
- body: bytes,
259
- };
260
- }
261
- }
262
- return json(200, {
263
- service: "voyant workflows selfhost",
264
- endpoints: ["/api/runs", "/api/runs/:id"],
265
- });
266
- }
267
- if (deps.hasStaticDashboard && deps.readStatic && !url.pathname.startsWith("/api/")) {
268
- const clean = url.pathname.replace(/^\/+/, "");
269
- if (clean && !clean.includes("..")) {
270
- const bytes = await deps.readStatic(clean);
271
- if (bytes) {
272
- return {
273
- status: 200,
274
- headers: {
275
- "content-type": mimeFor(clean),
276
- "cache-control": "no-store",
277
- },
278
- body: bytes,
279
- };
280
- }
281
- }
282
- }
283
- if (url.pathname === "/api/workflows") {
284
- const workflows = deps.listWorkflows ? deps.listWorkflows() : [];
285
- return json(200, { workflows });
286
- }
287
- if (url.pathname === "/api/schedules") {
288
- const schedules = deps.listSchedules ? deps.listSchedules() : [];
289
- return json(200, { schedules });
290
- }
291
- if (url.pathname === "/api/runs") {
292
- const filter = {};
293
- const workflowId = url.searchParams.get("workflow") ?? url.searchParams.get("workflowId");
294
- if (workflowId)
295
- filter.workflowId = workflowId;
296
- const status = url.searchParams.get("status");
297
- if (status)
298
- filter.status = status;
299
- const limitRaw = url.searchParams.get("limit");
300
- if (limitRaw !== null) {
301
- const limit = Number.parseInt(limitRaw, 10);
302
- if (Number.isNaN(limit) || limit < 0) {
303
- return json(400, {
304
- error: "invalid_limit",
305
- message: `limit must be a non-negative integer (got "${limitRaw}")`,
306
- });
307
- }
308
- filter.limit = limit;
309
- }
310
- const runs = await deps.store.list(filter);
311
- return json(200, { runs });
312
- }
313
- const runMatch = url.pathname.match(/^\/api\/runs\/([^/]+)$/);
314
- if (runMatch) {
315
- const runId = decodeURIComponent(runMatch[1]);
316
- const run = await deps.store.get(runId);
317
- if (!run)
318
- return json(404, { error: "not_found", runId });
319
- return json(200, { run });
320
- }
321
- return json(404, { error: "route_not_found", path: url.pathname });
322
- }
323
- function parseResumeRequestBody(body) {
324
- let parsed;
325
- try {
326
- parsed = body ? JSON.parse(body) : {};
327
- }
328
- catch (err) {
329
- return {
330
- ok: false,
331
- status: 400,
332
- error: "invalid_json",
333
- message: err instanceof Error ? err.message : String(err),
334
- };
335
- }
336
- if (!isPlainObject(parsed)) {
337
- return {
338
- ok: false,
339
- status: 400,
340
- error: "invalid_body",
341
- message: "request body must be an object",
342
- };
343
- }
344
- if (parsed.resumeFromStep !== undefined && typeof parsed.resumeFromStep !== "string") {
345
- return {
346
- ok: false,
347
- status: 400,
348
- error: "invalid_body",
349
- message: "`resumeFromStep` must be a string when provided",
350
- };
351
- }
352
- if (parsed.workflowId !== undefined && typeof parsed.workflowId !== "string") {
353
- return {
354
- ok: false,
355
- status: 400,
356
- error: "invalid_body",
357
- message: "`workflowId` must be a string when provided",
358
- };
359
- }
360
- if (parsed.runId !== undefined && typeof parsed.runId !== "string") {
361
- return {
362
- ok: false,
363
- status: 400,
364
- error: "invalid_body",
365
- message: "`runId` must be a string when provided",
366
- };
367
- }
368
- if (parsed.triggeredByUserId !== undefined &&
369
- parsed.triggeredByUserId !== null &&
370
- typeof parsed.triggeredByUserId !== "string") {
371
- return {
372
- ok: false,
373
- status: 400,
374
- error: "invalid_body",
375
- message: "`triggeredByUserId` must be a string or null when provided",
376
- };
377
- }
378
- if (parsed.tags !== undefined && !isStringArray(parsed.tags)) {
379
- return {
380
- ok: false,
381
- status: 400,
382
- error: "invalid_body",
383
- message: "`tags` must be an array of strings when provided",
384
- };
385
- }
386
- if (parsed.seedResults !== undefined && !isPlainObject(parsed.seedResults)) {
387
- return {
388
- ok: false,
389
- status: 400,
390
- error: "invalid_body",
391
- message: "`seedResults` must be an object when provided",
392
- };
393
- }
394
- return {
395
- ok: true,
396
- body: {
397
- input: parsed.input,
398
- workflowId: parsed.workflowId,
399
- resumeFromStep: parsed.resumeFromStep,
400
- seedResults: parsed.seedResults,
401
- runId: parsed.runId,
402
- tags: parsed.tags,
403
- triggeredByUserId: parsed.triggeredByUserId,
404
- },
405
- };
406
- }
407
- export function createStaticReader(rootDir) {
408
- const root = resolvePath(rootDir);
409
- return async (path) => {
410
- const absolute = resolvePath(root, path);
411
- if (!absolute.startsWith(`${root}/`) && absolute !== root)
412
- return null;
413
- try {
414
- return await readFile(absolute);
415
- }
416
- catch {
417
- return null;
418
- }
419
- };
420
- }
421
- export async function findDashboardDir(startFrom) {
422
- const candidates = [
423
- join(startFrom, "apps/workflows-local-dashboard/dist"),
424
- join(startFrom, "../local-dashboard/dist"),
425
- join(startFrom, "../../apps/workflows-local-dashboard/dist"),
426
- join(startFrom, "../../../apps/workflows-local-dashboard/dist"),
427
- ];
428
- for (const candidate of candidates) {
429
- try {
430
- const entry = await stat(join(candidate, "index.html"));
431
- if (entry.isFile())
432
- return candidate;
433
- }
434
- catch {
435
- // Continue scanning candidate locations.
436
- }
437
- }
438
- return undefined;
439
- }
440
- export async function startServer(options, deps) {
441
- const readStatic = deps.readStatic ?? (deps.staticDir ? createStaticReader(deps.staticDir) : undefined);
442
- const hasStaticDashboard = Boolean(readStatic);
443
- let storeStream;
444
- const getStoreStream = () => {
445
- if (!storeStream)
446
- storeStream = createStoreStream(deps.store);
447
- return storeStream;
448
- };
449
- const server = deps.createServer(async (req, res) => {
450
- const method = (req.method ?? "GET").toUpperCase();
451
- const url = req.url ?? "/";
452
- if ((method === "GET" || method === "HEAD") && urlPath(url) === "/api/runs/stream") {
453
- handleSseStream(res, getStoreStream(), deps.chunkBus);
454
- return;
455
- }
456
- const perRunMatch = urlPath(url).match(/^\/api\/runs\/([^/]+)\/stream$/);
457
- if ((method === "GET" || method === "HEAD") && perRunMatch) {
458
- const runId = decodeURIComponent(perRunMatch[1]);
459
- handleRunSseStream(res, runId, getStoreStream(), deps.chunkBus, deps.store);
460
- return;
461
- }
462
- try {
463
- const body = method === "POST" ? await readRequestBody(req) : undefined;
464
- const response = await handleRequest({ method, url, body }, {
465
- store: deps.store,
466
- healthCheck: deps.healthCheck,
467
- readinessCheck: deps.readinessCheck,
468
- collectMetrics: deps.collectMetrics,
469
- readStatic,
470
- hasStaticDashboard,
471
- triggerRun: deps.triggerRun,
472
- resumeRun: deps.resumeRun,
473
- listWorkflows: deps.listWorkflows,
474
- injectWaitpoint: deps.injectWaitpoint,
475
- listSchedules: deps.listSchedules,
476
- cancelRun: deps.cancelRun,
477
- });
478
- res.writeHead(response.status, response.headers);
479
- res.end(response.body);
480
- }
481
- catch (err) {
482
- const message = err instanceof Error ? err.message : String(err);
483
- res.writeHead(500, { "content-type": "application/json" });
484
- res.end(JSON.stringify({ error: "internal_error", message }));
485
- }
486
- });
487
- await new Promise((resolve, reject) => {
488
- server.once("error", reject);
489
- server.listen(options.port, options.host, () => {
490
- server.off("error", reject);
491
- resolve();
492
- });
493
- });
494
- deps.scheduler?.start();
495
- return {
496
- url: `http://${options.host}:${options.port}`,
497
- close: () => new Promise((resolve, reject) => {
498
- deps.scheduler?.stop();
499
- storeStream?.stop();
500
- server.closeAllConnections?.();
501
- server.close((err) => {
502
- if (err) {
503
- reject(err);
504
- return;
505
- }
506
- Promise.resolve(deps.shutdown?.()).then(() => resolve(), reject);
507
- });
508
- }),
509
- };
510
- }
511
- export function handleSseStream(res, stream, chunkBus) {
512
- res.writeHead(200, {
513
- "content-type": "text/event-stream",
514
- "cache-control": "no-store",
515
- connection: "keep-alive",
516
- "access-control-allow-origin": "*",
517
- });
518
- res.write("retry: 3000\n\n");
519
- const writeEvent = (event) => {
520
- try {
521
- res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`);
522
- }
523
- catch {
524
- // Ignore write errors on closed sockets.
525
- }
526
- };
527
- const writeChunk = (event) => {
528
- try {
529
- res.write(`event: stream.chunk\ndata: ${JSON.stringify(event)}\n\n`);
530
- }
531
- catch {
532
- // Ignore write errors on closed sockets.
533
- }
534
- };
535
- const unsubscribeStore = stream.subscribe(writeEvent);
536
- const unsubscribeChunk = chunkBus ? chunkBus.subscribe(writeChunk) : () => { };
537
- const ping = setInterval(() => {
538
- try {
539
- res.write(`: ping ${Date.now()}\n\n`);
540
- }
541
- catch {
542
- // Ignore write errors on closed sockets.
543
- }
544
- }, 25_000);
545
- ping.unref?.();
546
- res.on("close", () => {
547
- clearInterval(ping);
548
- unsubscribeStore();
549
- unsubscribeChunk();
550
- });
551
- }
552
- const TERMINAL_STATUSES = new Set([
553
- "completed",
554
- "failed",
555
- "cancelled",
556
- "compensated",
557
- "compensation_failed",
558
- ]);
559
- export function handleRunSseStream(res, runId, stream, chunkBus, store) {
560
- res.writeHead(200, {
561
- "content-type": "text/event-stream",
562
- "cache-control": "no-store",
563
- connection: "keep-alive",
564
- "access-control-allow-origin": "*",
565
- });
566
- res.write("retry: 3000\n\n");
567
- let closed = false;
568
- const close = () => {
569
- if (closed)
570
- return;
571
- closed = true;
572
- try {
573
- res.end();
574
- }
575
- catch {
576
- // Ignore close failures.
577
- }
578
- };
579
- const writeEvent = (kind, data) => {
580
- if (closed)
581
- return;
582
- try {
583
- res.write(`event: ${kind}\ndata: ${JSON.stringify(data)}\n\n`);
584
- }
585
- catch {
586
- // Ignore write errors on closed sockets.
587
- }
588
- };
589
- void store.get(runId).then((run) => {
590
- if (run) {
591
- writeEvent("hello", { run });
592
- if (TERMINAL_STATUSES.has(run.status))
593
- close();
594
- }
595
- else {
596
- writeEvent("hello", { run: null });
597
- }
598
- });
599
- const unsubscribeStore = stream.subscribe((event) => {
600
- if (event.kind === "added" || event.kind === "updated") {
601
- if (event.run.id !== runId)
602
- return;
603
- writeEvent(event.kind, event);
604
- if (TERMINAL_STATUSES.has(event.run.status))
605
- close();
606
- }
607
- else if (event.kind === "removed") {
608
- if (event.runId !== runId)
609
- return;
610
- writeEvent(event.kind, event);
611
- close();
612
- }
613
- });
614
- const unsubscribeChunk = chunkBus
615
- ? chunkBus.subscribe((event) => {
616
- if (event.runId !== runId)
617
- return;
618
- writeEvent("stream.chunk", event);
619
- })
620
- : () => { };
621
- const ping = setInterval(() => {
622
- try {
623
- res.write(`: ping ${Date.now()}\n\n`);
624
- }
625
- catch {
626
- // Ignore write errors on closed sockets.
627
- }
628
- }, 25_000);
629
- ping.unref?.();
630
- res.on("close", () => {
631
- closed = true;
632
- clearInterval(ping);
633
- unsubscribeStore();
634
- unsubscribeChunk();
635
- });
636
- }
637
- export async function startNodeSelfHostServer(opts) {
638
- const deps = await createNodeSelfHostDeps(opts);
639
- return startServer({
640
- port: opts.port ?? 3232,
641
- host: opts.host ?? "127.0.0.1",
642
- }, deps);
643
- }
644
- export async function createNodeSelfHostDeps(opts) {
645
- let staticDir = opts.staticDir;
646
- if (!staticDir)
647
- staticDir = await findDashboardDir(process.cwd());
648
- if (!staticDir && typeof import.meta.url === "string") {
649
- const here = resolvePath(new URL(".", import.meta.url).pathname);
650
- staticDir = await findDashboardDir(here);
651
- }
652
- if (staticDir) {
653
- await assertReadableDirectory(staticDir, "dashboard static dir");
654
- }
655
- const databaseUrl = opts.databaseUrl ?? process.env.DATABASE_URL;
656
- const pg = databaseUrl ? createPostgresConnection({ databaseUrl }) : undefined;
657
- const store = opts.store ?? (pg ? createPostgresSnapshotRunStore({ db: pg.db }) : createFsSnapshotRunStore());
658
- const wfMod = (await import("@voyantjs/workflows"));
659
- wfMod.__resetRegistry();
660
- const entryAbs = resolvePath(process.cwd(), opts.entryFile);
661
- await assertReadableFile(entryAbs, "workflow entry");
662
- await loadEntryFile(entryAbs, { cacheBust: opts.cacheBustEntry });
663
- const _handlerMod = await import("@voyantjs/workflows/handler");
664
- const rateLimiter = createInMemoryRateLimiter();
665
- const chunkBus = createChunkBus();
666
- const nodeStepRunner = async ({ attempt, fn, stepCtx }) => {
667
- const startedAt = Date.now();
668
- try {
669
- const output = await fn(stepCtx);
670
- return { attempt, status: "ok", output, startedAt, finishedAt: Date.now() };
671
- }
672
- catch (err) {
673
- const error = err;
674
- const code = typeof err.code === "string"
675
- ? err.code
676
- : "UNKNOWN";
677
- return {
678
- attempt,
679
- status: "err",
680
- error: {
681
- category: "USER_ERROR",
682
- code,
683
- message: error?.message ?? String(err),
684
- name: error?.name,
685
- stack: error?.stack,
686
- },
687
- startedAt,
688
- finishedAt: Date.now(),
689
- };
690
- }
691
- };
692
- const stepHandler = async (req, stepOpts) => handleStepRequest(req, { rateLimiter, nodeStepRunner, services: opts.services }, stepOpts);
693
- const tenantMeta = {
694
- tenantId: "tnt_local",
695
- projectId: "prj_local",
696
- organizationId: "org_local",
697
- };
698
- const wakeupStore = pg ? createPostgresWakeupStore({ db: pg.db }) : createFsWakeupStore();
699
- const leaseOwner = opts.wakeupLeaseOwner ??
700
- `node-selfhost-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
701
- const listWorkflows = () => wfMod.__listRegisteredWorkflows().map((workflow) => ({
702
- id: workflow.id,
703
- description: workflow.config.description,
704
- }));
705
- const registeredWorkflows = listWorkflows();
706
- if (registeredWorkflows.length === 0) {
707
- throw new Error("voyant workflows selfhost: workflow entry registered no workflows. " +
708
- `Check "${entryAbs}" and ensure it calls workflow(...).`);
709
- }
710
- const healthCheck = () => ({
711
- ok: true,
712
- service: "voyant-workflows-selfhost",
713
- });
714
- const readinessCheck = async () => {
715
- const checks = {
716
- workflowEntry: "ok",
717
- };
718
- const details = {};
719
- if (pg) {
720
- try {
721
- await pg.pool.query("select 1");
722
- checks.database = "ok";
723
- }
724
- catch (err) {
725
- checks.database = "error";
726
- details.database = err instanceof Error ? err.message : String(err);
727
- }
728
- }
729
- return {
730
- ok: Object.values(checks).every((status) => status === "ok"),
731
- service: "voyant-workflows-selfhost",
732
- checks,
733
- details: Object.keys(details).length > 0 ? details : undefined,
734
- };
735
- };
736
- const collectMetrics = async () => {
737
- const runs = await store.list();
738
- const wakeups = await wakeupStore.list();
739
- const runsByStatus = runs.reduce((acc, run) => {
740
- acc[run.status] = (acc[run.status] ?? 0) + 1;
741
- return acc;
742
- }, {});
743
- return renderMetrics({
744
- workflowsRegistered: listWorkflows().length,
745
- schedulesRegistered: listSchedules ? listSchedules().length : 0,
746
- runsTotal: runs.length,
747
- wakeupsTotal: wakeups.length,
748
- runsByStatus,
749
- generatedAtMs: Date.now(),
750
- });
751
- };
752
- const wakeupManager = createPersistentWakeupManager({
753
- wakeupStore,
754
- listRuns: () => store.list(),
755
- getRun: (runId) => store.get(runId),
756
- saveRun: async (stored) => {
757
- if (!store.update) {
758
- throw new Error("snapshot run store does not support update");
759
- }
760
- return store.update(stored);
761
- },
762
- toRecord: (stored) => snapshotToRecord(stored),
763
- fromRecord: (record, base) => recordToSnapshot(record, base),
764
- handler: stepHandler,
765
- onStreamChunk: ({ runId, chunk }) => chunkBus.publish({ runId, chunk }),
766
- logger: (level, message, data) => {
767
- const error = typeof data === "object" && data !== null && "error" in data ? data.error : undefined;
768
- const details = error ? `: ${String(error)}` : "";
769
- if (level === "error")
770
- console.error(`[voyant] ${message}${details}`);
771
- else
772
- console.warn(`[voyant] ${message}${details}`);
773
- },
774
- createRunStore: createInMemoryRunStore,
775
- resumeDueAlarmsImpl: resumeDueAlarms,
776
- leaseOwner,
777
- intervalMs: opts.wakeupPollIntervalMs,
778
- leaseMs: opts.wakeupLeaseMs,
779
- });
780
- const cancelRun = async ({ runId }) => {
781
- const existing = await store.get(runId);
782
- if (!existing)
783
- return { ok: false, message: `run "${runId}" not found`, exitCode: 1 };
784
- if (existing.status !== "waiting") {
785
- return {
786
- ok: false,
787
- message: `run "${runId}" is not parked (status: ${existing.status})`,
788
- exitCode: 2,
789
- };
790
- }
791
- if (!store.update) {
792
- return { ok: false, message: "snapshot run store does not support update", exitCode: 1 };
793
- }
794
- const now = Date.now();
795
- const updated = {
796
- ...existing,
797
- status: "cancelled",
798
- completedAt: now,
799
- durationMs: now - existing.startedAt,
800
- result: {
801
- ...existing.result,
802
- status: "cancelled",
803
- cancelledAt: now,
804
- },
805
- };
806
- const saved = await store.update(updated);
807
- await wakeupManager.clear(runId);
808
- return { ok: true, saved };
809
- };
810
- const triggerRun = async ({ workflowId, input, runId, tags, triggeredByUserId, }) => {
811
- const workflow = wfMod.__listRegisteredWorkflows().find((entry) => entry.id === workflowId);
812
- if (!workflow) {
813
- return {
814
- ok: false,
815
- message: `workflow "${workflowId}" is not registered in ${entryAbs}.`,
816
- exitCode: 2,
817
- };
818
- }
819
- const nextRunId = runId ?? generateLocalRunId();
820
- const memStore = createInMemoryRunStore();
821
- let record;
822
- try {
823
- record = await trigger({
824
- runId: nextRunId,
825
- workflowId,
826
- workflowVersion: "local",
827
- input,
828
- tenantMeta,
829
- tags,
830
- triggeredBy: triggeredByUserId === undefined || triggeredByUserId === null
831
- ? { kind: "api" }
832
- : { kind: "api", actor: triggeredByUserId },
833
- timeoutMs: durationToMs(workflow.config.timeout),
834
- }, {
835
- store: memStore,
836
- handler: stepHandler,
837
- onStreamChunk: (chunk) => chunkBus.publish({ runId: nextRunId, chunk }),
838
- });
839
- }
840
- catch (err) {
841
- return {
842
- ok: false,
843
- message: err instanceof Error ? err.message : String(err),
844
- exitCode: 1,
845
- };
846
- }
847
- if (!store.update) {
848
- return { ok: false, message: "snapshot run store does not support update", exitCode: 1 };
849
- }
850
- const stored = recordToSnapshot(record);
851
- stored.entryFile = entryAbs;
852
- const saved = await store.update(stored);
853
- await wakeupManager.syncStoredRun(saved);
854
- return { ok: true, saved };
855
- };
856
- const resumeRun = async ({ parentRunId, workflowId: requestedWorkflowId, input, resumeFromStep, seedResults, runId, tags, triggeredByUserId, }) => {
857
- const existing = await store.get(parentRunId);
858
- let parent;
859
- if (existing) {
860
- try {
861
- parent = snapshotToRecord(existing);
862
- }
863
- catch (err) {
864
- return {
865
- ok: false,
866
- message: err instanceof Error ? err.message : String(err),
867
- exitCode: 1,
868
- };
869
- }
870
- }
871
- else if (!requestedWorkflowId) {
872
- return {
873
- ok: false,
874
- message: `parent run "${parentRunId}" not found; pass workflowId, resumeFromStep, ` +
875
- "and seedResults to resume from an external workflow-runs parent",
876
- exitCode: 1,
877
- };
878
- }
879
- const workflowId = parent?.workflowId ?? requestedWorkflowId;
880
- const workflow = wfMod.__listRegisteredWorkflows().find((entry) => entry.id === workflowId);
881
- if (!workflow) {
882
- return {
883
- ok: false,
884
- message: `workflow "${workflowId}" is not registered in ${entryAbs}.`,
885
- exitCode: 2,
886
- };
887
- }
888
- let resumeSeed;
889
- try {
890
- resumeSeed = parent
891
- ? buildResumeJournal({
892
- parent,
893
- resumeFromStep,
894
- seedResults,
895
- })
896
- : buildSeededResumeJournal({
897
- parentRunId,
898
- resumeFromStep: requireExternalResumeFromStep(resumeFromStep),
899
- seedResults: requireExternalSeedResults(seedResults),
900
- });
901
- }
902
- catch (err) {
903
- return {
904
- ok: false,
905
- message: err instanceof Error ? err.message : String(err),
906
- exitCode: 2,
907
- };
908
- }
909
- const memStore = createInMemoryRunStore();
910
- const nextRunId = runId ?? generateLocalRunId();
911
- let record;
912
- try {
913
- record = await trigger({
914
- runId: nextRunId,
915
- workflowId,
916
- workflowVersion: parent?.workflowVersion ?? "local",
917
- input: input === undefined ? parent?.input : input,
918
- tenantMeta: parent?.tenantMeta ?? tenantMeta,
919
- environment: parent?.environment,
920
- triggeredBy: triggeredByUserId === undefined || triggeredByUserId === null
921
- ? { kind: "api" }
922
- : { kind: "api", actor: triggeredByUserId },
923
- tags: mergeTags(parent?.tags, [
924
- "resume:true",
925
- `parentRunId:${parent?.id ?? parentRunId}`,
926
- ...(tags ?? []),
927
- ]),
928
- timeoutMs: durationToMs(workflow.config.timeout),
929
- initialJournal: resumeSeed.journal,
930
- initialMetadataAppliedCount: resumeSeed.metadataAppliedCount,
931
- }, {
932
- store: memStore,
933
- handler: stepHandler,
934
- onStreamChunk: (chunk) => chunkBus.publish({ runId: nextRunId, chunk }),
935
- });
936
- }
937
- catch (err) {
938
- return {
939
- ok: false,
940
- message: err instanceof Error ? err.message : String(err),
941
- exitCode: 1,
942
- };
943
- }
944
- if (!store.update) {
945
- return { ok: false, message: "snapshot run store does not support update", exitCode: 1 };
946
- }
947
- const stored = recordToSnapshot(record, {
948
- entryFile: entryAbs,
949
- replayOf: parent?.id ?? parentRunId,
950
- });
951
- const saved = await store.update(stored);
952
- await wakeupManager.syncStoredRun(saved);
953
- return {
954
- ok: true,
955
- saved,
956
- parentRunId: parent?.id ?? parentRunId,
957
- resumeFromStep: resumeSeed.resumeFromStep,
958
- };
959
- };
960
- const injectWaitpoint = async ({ runId, injection }) => {
961
- const existing = await store.get(runId);
962
- if (!existing) {
963
- return { ok: false, message: `run "${runId}" not found`, exitCode: 1 };
964
- }
965
- if (existing.status !== "waiting") {
966
- return {
967
- ok: false,
968
- message: `run "${runId}" is not parked (status: ${existing.status})`,
969
- exitCode: 2,
970
- };
971
- }
972
- const record = snapshotToRecord(existing);
973
- if (!record) {
974
- return { ok: false, message: `run "${runId}" has no resumable snapshot`, exitCode: 1 };
975
- }
976
- const memStore = createInMemoryRunStore();
977
- await memStore.save(record);
978
- const out = await resume({ runId, injection }, {
979
- store: memStore,
980
- handler: stepHandler,
981
- onStreamChunk: (chunk) => chunkBus.publish({ runId, chunk }),
982
- });
983
- if (!out.ok) {
984
- const exitCode = out.status === "no_match" || out.status === "not_parked" ? 2 : 1;
985
- return { ok: false, message: out.message, exitCode };
986
- }
987
- if (!store.update) {
988
- return { ok: false, message: "snapshot run store does not support update", exitCode: 1 };
989
- }
990
- const saved = await store.update(recordToSnapshot(out.record, existing));
991
- await wakeupManager.syncStoredRun(saved);
992
- return { ok: true, saved };
993
- };
994
- try {
995
- await wakeupManager.bootstrap();
996
- }
997
- catch (err) {
998
- console.warn(`[voyant] failed to bootstrap wakeup leases from run store: ${err instanceof Error ? err.message : String(err)}`);
999
- }
1000
- wakeupManager.start();
1001
- let scheduler;
1002
- let listSchedules;
1003
- const sources = [];
1004
- for (const workflow of wfMod.__listRegisteredWorkflows()) {
1005
- const decl = workflow.config.schedule;
1006
- if (!decl)
1007
- continue;
1008
- const decls = Array.isArray(decl) ? decl : [decl];
1009
- for (const source of decls) {
1010
- sources.push({ workflowId: workflow.id, decl: source });
1011
- }
1012
- }
1013
- if (sources.length > 0) {
1014
- scheduler = createScheduler({
1015
- sources,
1016
- onFire: async ({ workflowId, input }) => {
1017
- await triggerRun({ workflowId, input });
1018
- },
1019
- logger: (level, message) => {
1020
- if (level === "error")
1021
- console.error(`[scheduler] ${message}`);
1022
- else if (level === "warn")
1023
- console.warn(`[scheduler] ${message}`);
1024
- },
1025
- });
1026
- listSchedules = () => scheduler.nextFirings();
1027
- }
1028
- return {
1029
- store,
1030
- createServer,
1031
- healthCheck,
1032
- readinessCheck,
1033
- collectMetrics,
1034
- shutdown: async () => {
1035
- wakeupManager.stop();
1036
- await pg?.close();
1037
- },
1038
- staticDir,
1039
- triggerRun,
1040
- resumeRun,
1041
- listWorkflows,
1042
- injectWaitpoint,
1043
- scheduler,
1044
- listSchedules,
1045
- cancelRun,
1046
- chunkBus,
1047
- };
1048
- }
1049
- async function assertReadableFile(path, label) {
1050
- let info;
1051
- try {
1052
- info = await stat(path);
1053
- }
1054
- catch (err) {
1055
- throw new Error(`voyant workflows selfhost: ${label} not found at "${path}"`, { cause: err });
1056
- }
1057
- if (!info.isFile()) {
1058
- throw new Error(`voyant workflows selfhost: ${label} must be a file (got "${path}")`);
1059
- }
1060
- }
1061
- async function assertReadableDirectory(path, label) {
1062
- let info;
1063
- try {
1064
- info = await stat(path);
1065
- }
1066
- catch (err) {
1067
- throw new Error(`voyant workflows selfhost: ${label} not found at "${path}"`, { cause: err });
1068
- }
1069
- if (!info.isDirectory()) {
1070
- throw new Error(`voyant workflows selfhost: ${label} must be a directory (got "${path}")`);
1071
- }
1072
- }
1073
- function mergeTags(...groups) {
1074
- const tags = new Set();
1075
- for (const group of groups) {
1076
- for (const tag of group ?? [])
1077
- tags.add(tag);
1078
- }
1079
- return Array.from(tags);
1080
- }
1081
- function requireExternalResumeFromStep(resumeFromStep) {
1082
- if (!resumeFromStep) {
1083
- throw new Error("resumeFromStep is required when the parent run is not stored by this self-host server");
1084
- }
1085
- return resumeFromStep;
1086
- }
1087
- function requireExternalSeedResults(seedResults) {
1088
- if (!seedResults) {
1089
- throw new Error("seedResults is required when the parent run is not stored by this self-host server");
1090
- }
1091
- return seedResults;
1092
- }
1093
- function isPlainObject(value) {
1094
- return typeof value === "object" && value !== null && !Array.isArray(value);
1095
- }
1096
- function isStringArray(value) {
1097
- return Array.isArray(value) && value.every((item) => typeof item === "string");
1098
- }
1099
- function json(status, body) {
1100
- return {
1101
- status,
1102
- headers: {
1103
- "content-type": "application/json; charset=utf-8",
1104
- "access-control-allow-origin": "*",
1105
- "cache-control": "no-store",
1106
- },
1107
- body: JSON.stringify(body, null, 2),
1108
- };
1109
- }
1110
- async function resolveHealthReport(check, fallback) {
1111
- if (!check)
1112
- return fallback;
1113
- try {
1114
- return await check();
1115
- }
1116
- catch (err) {
1117
- return {
1118
- ok: false,
1119
- service: fallback.service,
1120
- checks: {
1121
- ...(fallback.checks ?? {}),
1122
- self: "error",
1123
- },
1124
- details: {
1125
- ...(fallback.details ?? {}),
1126
- error: err instanceof Error ? err.message : String(err),
1127
- },
1128
- };
1129
- }
1130
- }
1131
- async function resolveMetricsBody(collectMetrics) {
1132
- if (!collectMetrics) {
1133
- return renderMetrics({
1134
- workflowsRegistered: 0,
1135
- schedulesRegistered: 0,
1136
- runsTotal: 0,
1137
- wakeupsTotal: 0,
1138
- runsByStatus: {},
1139
- generatedAtMs: Date.now(),
1140
- });
1141
- }
1142
- try {
1143
- return await collectMetrics();
1144
- }
1145
- catch (err) {
1146
- return [
1147
- "# HELP voyant_selfhost_metrics_error Metrics collection failure state.",
1148
- "# TYPE voyant_selfhost_metrics_error gauge",
1149
- "voyant_selfhost_metrics_error 1",
1150
- `# metrics_error ${escapeMetricLabelValue(err instanceof Error ? err.message : String(err))}`,
1151
- "",
1152
- ].join("\n");
1153
- }
1154
- }
1155
- export function renderMetrics(snapshot) {
1156
- const lines = [
1157
- "# HELP voyant_selfhost_up Self-host server availability.",
1158
- "# TYPE voyant_selfhost_up gauge",
1159
- "voyant_selfhost_up 1",
1160
- "# HELP voyant_selfhost_workflows_registered Registered workflow count.",
1161
- "# TYPE voyant_selfhost_workflows_registered gauge",
1162
- `voyant_selfhost_workflows_registered ${snapshot.workflowsRegistered}`,
1163
- "# HELP voyant_selfhost_schedules_registered Registered schedule count.",
1164
- "# TYPE voyant_selfhost_schedules_registered gauge",
1165
- `voyant_selfhost_schedules_registered ${snapshot.schedulesRegistered}`,
1166
- "# HELP voyant_selfhost_runs_total Persisted run count.",
1167
- "# TYPE voyant_selfhost_runs_total gauge",
1168
- `voyant_selfhost_runs_total ${snapshot.runsTotal}`,
1169
- "# HELP voyant_selfhost_runs_status Run count by status.",
1170
- "# TYPE voyant_selfhost_runs_status gauge",
1171
- ];
1172
- for (const [status, count] of Object.entries(snapshot.runsByStatus).sort(([a], [b]) => a.localeCompare(b))) {
1173
- lines.push(`voyant_selfhost_runs_status{status="${escapeMetricLabelValue(status)}"} ${count}`);
1174
- }
1175
- lines.push("# HELP voyant_selfhost_wakeups_total Persisted wakeup count.", "# TYPE voyant_selfhost_wakeups_total gauge", `voyant_selfhost_wakeups_total ${snapshot.wakeupsTotal}`, "# HELP voyant_selfhost_metrics_generated_at_seconds Metrics generation timestamp.", "# TYPE voyant_selfhost_metrics_generated_at_seconds gauge", `voyant_selfhost_metrics_generated_at_seconds ${Math.floor(snapshot.generatedAtMs / 1000)}`, "");
1176
- return lines.join("\n");
1177
- }
1178
- function escapeMetricLabelValue(value) {
1179
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
1180
- }
1181
- function mimeFor(path) {
1182
- const ext = extname(path).toLowerCase();
1183
- switch (ext) {
1184
- case ".html":
1185
- return "text/html; charset=utf-8";
1186
- case ".js":
1187
- case ".mjs":
1188
- return "application/javascript; charset=utf-8";
1189
- case ".css":
1190
- return "text/css; charset=utf-8";
1191
- case ".json":
1192
- return "application/json; charset=utf-8";
1193
- case ".svg":
1194
- return "image/svg+xml";
1195
- case ".png":
1196
- return "image/png";
1197
- case ".map":
1198
- return "application/json";
1199
- default:
1200
- return "application/octet-stream";
1201
- }
1202
- }
1203
- function urlPath(raw) {
1204
- try {
1205
- return new URL(raw, "http://local").pathname;
1206
- }
1207
- catch {
1208
- return raw;
1209
- }
1210
- }
1211
- async function readRequestBody(req) {
1212
- const maxBytes = 1_000_000;
1213
- return new Promise((resolve, reject) => {
1214
- let total = 0;
1215
- const chunks = [];
1216
- req.on("data", (chunk) => {
1217
- total += chunk.length;
1218
- if (total > maxBytes) {
1219
- req.destroy(new Error("request body exceeds 1MB"));
1220
- return;
1221
- }
1222
- chunks.push(chunk);
1223
- });
1224
- req.on("end", () => {
1225
- resolve(Buffer.concat(chunks).toString("utf8"));
1226
- });
1227
- req.on("error", reject);
1228
- });
1229
- }
1
+ export { createChunkBus } from "./dashboard-chunks.js";
2
+ export { startServer } from "./dashboard-http-server.js";
3
+ export { renderMetrics } from "./dashboard-metrics.js";
4
+ export { handleRequest } from "./dashboard-request.js";
5
+ export { handleRunSseStream, handleSseStream } from "./dashboard-sse.js";
6
+ export { createStaticReader, findDashboardDir } from "./dashboard-static.js";
7
+ export { createNodeSelfHostDeps, startNodeSelfHostServer } from "./node-selfhost-deps.js";