@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.
- package/dist/dashboard-chunks.d.ts +17 -0
- package/dist/dashboard-chunks.d.ts.map +1 -0
- package/dist/dashboard-chunks.js +19 -0
- package/dist/dashboard-http-server.d.ts +6 -0
- package/dist/dashboard-http-server.d.ts.map +1 -0
- package/dist/dashboard-http-server.js +99 -0
- package/dist/dashboard-metrics.d.ts +3 -0
- package/dist/dashboard-metrics.d.ts.map +1 -0
- package/dist/dashboard-metrics.js +26 -0
- package/dist/dashboard-request.d.ts +7 -0
- package/dist/dashboard-request.d.ts.map +1 -0
- package/dist/dashboard-request.js +436 -0
- package/dist/dashboard-server.d.ts +9 -171
- package/dist/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard-server.js +7 -1229
- package/dist/dashboard-sse.d.ts +7 -0
- package/dist/dashboard-sse.d.ts.map +1 -0
- package/dist/dashboard-sse.js +134 -0
- package/dist/dashboard-static.d.ts +7 -0
- package/dist/dashboard-static.d.ts.map +1 -0
- package/dist/dashboard-static.js +89 -0
- package/dist/dashboard-types.d.ts +134 -0
- package/dist/dashboard-types.d.ts.map +1 -0
- package/dist/dashboard-types.js +1 -0
- package/dist/node-selfhost-defaults.d.ts +7 -0
- package/dist/node-selfhost-defaults.d.ts.map +1 -0
- package/dist/node-selfhost-defaults.js +8 -0
- package/dist/node-selfhost-deps.d.ts +4 -0
- package/dist/node-selfhost-deps.d.ts.map +1 -0
- package/dist/node-selfhost-deps.js +403 -0
- package/dist/node-selfhost-resume-input.d.ts +4 -0
- package/dist/node-selfhost-resume-input.d.ts.map +1 -0
- package/dist/node-selfhost-resume-input.js +20 -0
- package/dist/node-standalone-driver.d.ts.map +1 -1
- package/dist/node-standalone-driver.js +40 -3
- package/dist/node-step-runner.d.ts +3 -0
- package/dist/node-step-runner.d.ts.map +1 -0
- package/dist/node-step-runner.js +26 -0
- package/dist/postgres-manifest-store.d.ts.map +1 -1
- package/dist/postgres-manifest-store.js +6 -2
- package/dist/postgres-run-record-store.js +1 -1
- package/dist/postgres-schema.d.ts.map +1 -1
- package/dist/postgres-schema.js +2 -0
- package/dist/sleep-alarm-manager.d.ts.map +1 -1
- package/dist/sleep-alarm-manager.js +9 -1
- package/dist/store-stream.d.ts.map +1 -1
- package/dist/store-stream.js +9 -1
- package/dist/wakeup-poller.d.ts.map +1 -1
- package/dist/wakeup-poller.js +9 -1
- package/package.json +3 -3
- package/src/dashboard-chunks.ts +35 -0
- package/src/dashboard-http-server.ts +118 -0
- package/src/dashboard-metrics.ts +39 -0
- package/src/dashboard-request.ts +488 -0
- package/src/dashboard-server.ts +17 -1535
- package/src/dashboard-sse.ts +150 -0
- package/src/dashboard-static.ts +88 -0
- package/src/dashboard-types.ts +106 -0
- package/src/node-selfhost-defaults.ts +9 -0
- package/src/node-selfhost-deps.ts +495 -0
- package/src/node-selfhost-resume-input.ts +27 -0
- package/src/node-standalone-driver.ts +59 -3
- package/src/node-step-runner.ts +28 -0
- package/src/postgres-manifest-store.ts +2 -0
- package/src/postgres-run-record-store.ts +1 -1
- package/src/postgres-schema.ts +2 -0
- package/src/sleep-alarm-manager.ts +12 -1
- package/src/store-stream.ts +12 -1
- package/src/wakeup-poller.ts +12 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http"
|
|
2
|
+
import type { ChunkBus, ChunkEvent } from "./dashboard-chunks.js"
|
|
3
|
+
import type { SnapshotRunStore } from "./snapshot-run-store.js"
|
|
4
|
+
import type { StoreEvent, StoreStream } from "./store-stream.js"
|
|
5
|
+
|
|
6
|
+
function unrefTimer(timer: ReturnType<typeof setInterval>): void {
|
|
7
|
+
if (
|
|
8
|
+
typeof timer === "object" &&
|
|
9
|
+
timer !== null &&
|
|
10
|
+
"unref" in timer &&
|
|
11
|
+
typeof timer.unref === "function"
|
|
12
|
+
) {
|
|
13
|
+
timer.unref()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function handleSseStream(
|
|
18
|
+
res: ServerResponse,
|
|
19
|
+
stream: StoreStream,
|
|
20
|
+
chunkBus?: ChunkBus,
|
|
21
|
+
): void {
|
|
22
|
+
res.writeHead(200, {
|
|
23
|
+
"content-type": "text/event-stream",
|
|
24
|
+
"cache-control": "no-store",
|
|
25
|
+
connection: "keep-alive",
|
|
26
|
+
"access-control-allow-origin": "*",
|
|
27
|
+
})
|
|
28
|
+
res.write("retry: 3000\n\n")
|
|
29
|
+
|
|
30
|
+
const writeEvent = (event: StoreEvent): void => {
|
|
31
|
+
try {
|
|
32
|
+
res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`)
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore write errors on closed sockets.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const writeChunk = (event: ChunkEvent): void => {
|
|
39
|
+
try {
|
|
40
|
+
res.write(`event: stream.chunk\ndata: ${JSON.stringify(event)}\n\n`)
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore write errors on closed sockets.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const unsubscribeStore = stream.subscribe(writeEvent)
|
|
47
|
+
const unsubscribeChunk = chunkBus ? chunkBus.subscribe(writeChunk) : () => {}
|
|
48
|
+
const ping = setInterval(() => {
|
|
49
|
+
try {
|
|
50
|
+
res.write(`: ping ${Date.now()}\n\n`)
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore write errors on closed sockets.
|
|
53
|
+
}
|
|
54
|
+
}, 25_000)
|
|
55
|
+
unrefTimer(ping)
|
|
56
|
+
|
|
57
|
+
res.on("close", () => {
|
|
58
|
+
clearInterval(ping)
|
|
59
|
+
unsubscribeStore()
|
|
60
|
+
unsubscribeChunk()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const TERMINAL_STATUSES = new Set([
|
|
65
|
+
"completed",
|
|
66
|
+
"failed",
|
|
67
|
+
"cancelled",
|
|
68
|
+
"compensated",
|
|
69
|
+
"compensation_failed",
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
export function handleRunSseStream(
|
|
73
|
+
res: ServerResponse,
|
|
74
|
+
runId: string,
|
|
75
|
+
stream: StoreStream,
|
|
76
|
+
chunkBus: ChunkBus | undefined,
|
|
77
|
+
store: SnapshotRunStore,
|
|
78
|
+
): void {
|
|
79
|
+
res.writeHead(200, {
|
|
80
|
+
"content-type": "text/event-stream",
|
|
81
|
+
"cache-control": "no-store",
|
|
82
|
+
connection: "keep-alive",
|
|
83
|
+
"access-control-allow-origin": "*",
|
|
84
|
+
})
|
|
85
|
+
res.write("retry: 3000\n\n")
|
|
86
|
+
|
|
87
|
+
let closed = false
|
|
88
|
+
const close = (): void => {
|
|
89
|
+
if (closed) return
|
|
90
|
+
closed = true
|
|
91
|
+
try {
|
|
92
|
+
res.end()
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore close failures.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const writeEvent = (kind: string, data: unknown): void => {
|
|
99
|
+
if (closed) return
|
|
100
|
+
try {
|
|
101
|
+
res.write(`event: ${kind}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore write errors on closed sockets.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
void store.get(runId).then((run) => {
|
|
108
|
+
if (run) {
|
|
109
|
+
writeEvent("hello", { run })
|
|
110
|
+
if (TERMINAL_STATUSES.has(run.status)) close()
|
|
111
|
+
} else {
|
|
112
|
+
writeEvent("hello", { run: null })
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const unsubscribeStore = stream.subscribe((event) => {
|
|
117
|
+
if (event.kind === "added" || event.kind === "updated") {
|
|
118
|
+
if (event.run.id !== runId) return
|
|
119
|
+
writeEvent(event.kind, event)
|
|
120
|
+
if (TERMINAL_STATUSES.has(event.run.status)) close()
|
|
121
|
+
} else if (event.kind === "removed") {
|
|
122
|
+
if (event.runId !== runId) return
|
|
123
|
+
writeEvent(event.kind, event)
|
|
124
|
+
close()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const unsubscribeChunk = chunkBus
|
|
129
|
+
? chunkBus.subscribe((event) => {
|
|
130
|
+
if (event.runId !== runId) return
|
|
131
|
+
writeEvent("stream.chunk", event)
|
|
132
|
+
})
|
|
133
|
+
: () => {}
|
|
134
|
+
|
|
135
|
+
const ping = setInterval(() => {
|
|
136
|
+
try {
|
|
137
|
+
res.write(`: ping ${Date.now()}\n\n`)
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignore write errors on closed sockets.
|
|
140
|
+
}
|
|
141
|
+
}, 25_000)
|
|
142
|
+
unrefTimer(ping)
|
|
143
|
+
|
|
144
|
+
res.on("close", () => {
|
|
145
|
+
closed = true
|
|
146
|
+
clearInterval(ping)
|
|
147
|
+
unsubscribeStore()
|
|
148
|
+
unsubscribeChunk()
|
|
149
|
+
})
|
|
150
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises"
|
|
2
|
+
import { extname, join, resolve as resolvePath } from "node:path"
|
|
3
|
+
|
|
4
|
+
export function createStaticReader(rootDir: string): (path: string) => Promise<Uint8Array | null> {
|
|
5
|
+
const root = resolvePath(rootDir)
|
|
6
|
+
return async (path: string) => {
|
|
7
|
+
const absolute = resolvePath(root, path)
|
|
8
|
+
if (!absolute.startsWith(`${root}/`) && absolute !== root) return null
|
|
9
|
+
try {
|
|
10
|
+
return await readFile(absolute)
|
|
11
|
+
} catch {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function findDashboardDir(startFrom: string): Promise<string | undefined> {
|
|
18
|
+
const candidates = [
|
|
19
|
+
join(startFrom, "apps/workflows-local-dashboard/dist"),
|
|
20
|
+
join(startFrom, "../local-dashboard/dist"),
|
|
21
|
+
join(startFrom, "../../apps/workflows-local-dashboard/dist"),
|
|
22
|
+
join(startFrom, "../../../apps/workflows-local-dashboard/dist"),
|
|
23
|
+
]
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
try {
|
|
26
|
+
const entry = await stat(join(candidate, "index.html"))
|
|
27
|
+
if (entry.isFile()) return candidate
|
|
28
|
+
} catch {
|
|
29
|
+
// Continue scanning candidate locations.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function assertReadableFile(path: string, label: string): Promise<void> {
|
|
36
|
+
let info: Awaited<ReturnType<typeof stat>>
|
|
37
|
+
try {
|
|
38
|
+
info = await stat(path)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new Error(`voyant workflows selfhost: ${label} not found at "${path}"`, { cause: err })
|
|
41
|
+
}
|
|
42
|
+
if (!info.isFile()) {
|
|
43
|
+
throw new Error(`voyant workflows selfhost: ${label} must be a file (got "${path}")`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function assertReadableDirectory(path: string, label: string): Promise<void> {
|
|
48
|
+
let info: Awaited<ReturnType<typeof stat>>
|
|
49
|
+
try {
|
|
50
|
+
info = await stat(path)
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(`voyant workflows selfhost: ${label} not found at "${path}"`, { cause: err })
|
|
53
|
+
}
|
|
54
|
+
if (!info.isDirectory()) {
|
|
55
|
+
throw new Error(`voyant workflows selfhost: ${label} must be a directory (got "${path}")`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mimeFor(path: string): string {
|
|
60
|
+
const ext = extname(path).toLowerCase()
|
|
61
|
+
switch (ext) {
|
|
62
|
+
case ".html":
|
|
63
|
+
return "text/html; charset=utf-8"
|
|
64
|
+
case ".js":
|
|
65
|
+
case ".mjs":
|
|
66
|
+
return "application/javascript; charset=utf-8"
|
|
67
|
+
case ".css":
|
|
68
|
+
return "text/css; charset=utf-8"
|
|
69
|
+
case ".json":
|
|
70
|
+
return "application/json; charset=utf-8"
|
|
71
|
+
case ".svg":
|
|
72
|
+
return "image/svg+xml"
|
|
73
|
+
case ".png":
|
|
74
|
+
return "image/png"
|
|
75
|
+
case ".map":
|
|
76
|
+
return "application/json"
|
|
77
|
+
default:
|
|
78
|
+
return "application/octet-stream"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function urlPath(raw: string): string {
|
|
83
|
+
try {
|
|
84
|
+
return new URL(raw, "http://local").pathname
|
|
85
|
+
} catch {
|
|
86
|
+
return raw
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { createServer } from "node:http"
|
|
2
|
+
import type { ServiceResolver } from "@voyantjs/workflows/driver"
|
|
3
|
+
import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
|
|
4
|
+
import type { ChunkBus } from "./dashboard-chunks.js"
|
|
5
|
+
import type { SchedulerHandle } from "./scheduler.js"
|
|
6
|
+
import type { ListFilter, SnapshotRunStore, StoredRun } from "./snapshot-run-store.js"
|
|
7
|
+
|
|
8
|
+
export interface ServeDeps {
|
|
9
|
+
store: SnapshotRunStore
|
|
10
|
+
createServer: typeof createServer
|
|
11
|
+
shutdown?: () => void | Promise<void>
|
|
12
|
+
healthCheck?: () => Promise<HealthReport> | HealthReport
|
|
13
|
+
readinessCheck?: () => Promise<HealthReport> | HealthReport
|
|
14
|
+
collectMetrics?: () => Promise<string> | string
|
|
15
|
+
staticDir?: string
|
|
16
|
+
readStatic?: (path: string) => Promise<Uint8Array | null>
|
|
17
|
+
triggerRun?: (args: {
|
|
18
|
+
workflowId: string
|
|
19
|
+
input: unknown
|
|
20
|
+
runId?: string
|
|
21
|
+
tags?: string[]
|
|
22
|
+
triggeredByUserId?: string | null
|
|
23
|
+
}) => Promise<{ ok: true; saved: StoredRun } | { ok: false; message: string; exitCode: number }>
|
|
24
|
+
resumeRun?: (args: {
|
|
25
|
+
parentRunId: string
|
|
26
|
+
workflowId?: string
|
|
27
|
+
input?: unknown
|
|
28
|
+
resumeFromStep?: string
|
|
29
|
+
seedResults?: Record<string, unknown>
|
|
30
|
+
runId?: string
|
|
31
|
+
tags?: string[]
|
|
32
|
+
triggeredByUserId?: string | null
|
|
33
|
+
}) => Promise<
|
|
34
|
+
| { ok: true; saved: StoredRun; parentRunId: string; resumeFromStep: string }
|
|
35
|
+
| { ok: false; message: string; exitCode: number }
|
|
36
|
+
>
|
|
37
|
+
listWorkflows?: () => { id: string; description?: string }[]
|
|
38
|
+
injectWaitpoint?: (args: {
|
|
39
|
+
runId: string
|
|
40
|
+
injection: WaitpointInjection
|
|
41
|
+
}) => Promise<{ ok: true; saved: StoredRun } | { ok: false; message: string; exitCode: number }>
|
|
42
|
+
scheduler?: SchedulerHandle
|
|
43
|
+
listSchedules?: () => { workflowId: string; name?: string; nextAt: number; done: boolean }[]
|
|
44
|
+
cancelRun?: (args: {
|
|
45
|
+
runId: string
|
|
46
|
+
}) => Promise<{ ok: true; saved: StoredRun } | { ok: false; message: string; exitCode: number }>
|
|
47
|
+
chunkBus?: ChunkBus
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RequestHandlerDeps {
|
|
51
|
+
store: SnapshotRunStore
|
|
52
|
+
healthCheck?: ServeDeps["healthCheck"]
|
|
53
|
+
readinessCheck?: ServeDeps["readinessCheck"]
|
|
54
|
+
collectMetrics?: ServeDeps["collectMetrics"]
|
|
55
|
+
readStatic?: (path: string) => Promise<Uint8Array | null>
|
|
56
|
+
hasStaticDashboard?: boolean
|
|
57
|
+
triggerRun?: ServeDeps["triggerRun"]
|
|
58
|
+
resumeRun?: ServeDeps["resumeRun"]
|
|
59
|
+
listWorkflows?: ServeDeps["listWorkflows"]
|
|
60
|
+
injectWaitpoint?: ServeDeps["injectWaitpoint"]
|
|
61
|
+
listSchedules?: ServeDeps["listSchedules"]
|
|
62
|
+
cancelRun?: ServeDeps["cancelRun"]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface HandlerResponse {
|
|
66
|
+
status: number
|
|
67
|
+
headers: Record<string, string>
|
|
68
|
+
body: string | Uint8Array
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface HealthReport {
|
|
72
|
+
ok: boolean
|
|
73
|
+
service?: string
|
|
74
|
+
checks?: Record<string, "ok" | "error">
|
|
75
|
+
details?: Record<string, unknown>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MetricsSnapshot {
|
|
79
|
+
workflowsRegistered: number
|
|
80
|
+
schedulesRegistered: number
|
|
81
|
+
runsTotal: number
|
|
82
|
+
wakeupsTotal: number
|
|
83
|
+
runsByStatus: Record<string, number>
|
|
84
|
+
generatedAtMs: number
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ServeHandle {
|
|
88
|
+
close: () => Promise<void>
|
|
89
|
+
url: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface NodeSelfHostServerOptions {
|
|
93
|
+
entryFile: string
|
|
94
|
+
port?: number
|
|
95
|
+
host?: string
|
|
96
|
+
staticDir?: string
|
|
97
|
+
cacheBustEntry?: boolean
|
|
98
|
+
services?: ServiceResolver
|
|
99
|
+
store?: SnapshotRunStore
|
|
100
|
+
databaseUrl?: string
|
|
101
|
+
wakeupPollIntervalMs?: number
|
|
102
|
+
wakeupLeaseMs?: number
|
|
103
|
+
wakeupLeaseOwner?: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type DashboardListFilter = ListFilter
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const localTenantMeta = {
|
|
2
|
+
tenantId: "tnt_local",
|
|
3
|
+
projectId: "prj_local",
|
|
4
|
+
organizationId: "org_local",
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createDefaultWakeupLeaseOwner(): string {
|
|
8
|
+
return `node-selfhost-${process.pid}-${Math.random().toString(36).slice(2, 8)}`
|
|
9
|
+
}
|