@effectstream/orchestrator 0.100.9
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/package.json +37 -0
- package/scripts/launch-avail.ts +64 -0
- package/scripts/launch-bitcoin.ts +62 -0
- package/scripts/launch-cardano.ts +99 -0
- package/scripts/launch-evm.ts +72 -0
- package/scripts/launch-midnight.ts +108 -0
- package/scripts/launch-near.ts +41 -0
- package/scripts/launch-pglite.ts +30 -0
- package/scripts/resolve-package.ts +42 -0
- package/scripts/wait-http.ts +24 -0
- package/scripts/wait-tcp.ts +30 -0
- package/src/api-client.ts +92 -0
- package/src/api-server.ts +183 -0
- package/src/cli.ts +183 -0
- package/src/commands/list.ts +63 -0
- package/src/commands/logs.ts +142 -0
- package/src/commands/restart.ts +34 -0
- package/src/commands/silence.ts +49 -0
- package/src/commands/start.ts +304 -0
- package/src/commands/status.ts +111 -0
- package/src/commands/stop.ts +100 -0
- package/src/config.ts +65 -0
- package/src/display.ts +190 -0
- package/src/load-config.ts +64 -0
- package/src/port-check.ts +126 -0
- package/src/process-manager.ts +266 -0
- package/src/task-runner.ts +276 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { ProcessConfig } from "./config.ts";
|
|
2
|
+
import type { ProcessManager } from "./process-manager.ts";
|
|
3
|
+
|
|
4
|
+
export type RunOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* If set, only these processes (plus all their transitive dependencies)
|
|
7
|
+
* will be launched. All other configs are ignored.
|
|
8
|
+
*/
|
|
9
|
+
only?: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Names of processes to skip (even if they appear as dependencies of others).
|
|
12
|
+
* Use with care—skipping a dependency may break its dependents.
|
|
13
|
+
*/
|
|
14
|
+
except?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* If true, processes are launched one at a time in dependency order
|
|
17
|
+
* instead of in parallel waves. Useful when binaries conflict at the OS level.
|
|
18
|
+
*/
|
|
19
|
+
serial?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* If true, --only launches only the named processes without pulling in
|
|
22
|
+
* their transitive dependencies. Dependencies are assumed to be already running.
|
|
23
|
+
*/
|
|
24
|
+
noDeps?: boolean;
|
|
25
|
+
onTaskStart?: (name: string, logFile: string | null) => void;
|
|
26
|
+
/** Called for background processes (waitToExit: false) once launched. */
|
|
27
|
+
onTaskRunning?: (name: string) => void;
|
|
28
|
+
onTaskDone?: (name: string) => void;
|
|
29
|
+
onTaskFailed?: (name: string, code: number) => void;
|
|
30
|
+
/** Called when a critical process fails and the whole run should stop. */
|
|
31
|
+
onShutdown?: (reason: string) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns `configs` filtered to only include the named processes and all of
|
|
36
|
+
* their transitive dependencies (preserving original order).
|
|
37
|
+
*/
|
|
38
|
+
export function resolveWithDependencies(
|
|
39
|
+
all: ProcessConfig[],
|
|
40
|
+
names: string[],
|
|
41
|
+
): ProcessConfig[] {
|
|
42
|
+
const map = new Map(all.map((p) => [p.name, p]));
|
|
43
|
+
const resolved = new Set<string>();
|
|
44
|
+
|
|
45
|
+
function resolve(name: string, chain: string[] = []) {
|
|
46
|
+
if (resolved.has(name)) return;
|
|
47
|
+
if (chain.includes(name)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Circular dependency detected: ${[...chain, name].join(" → ")}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const p = map.get(name);
|
|
53
|
+
if (!p) throw new Error(`Process "${name}" not found in config`);
|
|
54
|
+
for (const dep of p.dependsOn ?? []) {
|
|
55
|
+
resolve(dep, [...chain, name]);
|
|
56
|
+
}
|
|
57
|
+
resolved.add(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const name of names) resolve(name);
|
|
61
|
+
return all.filter((p) => resolved.has(p.name));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Runs processes in dependency order using `manager`.
|
|
66
|
+
*
|
|
67
|
+
* - All processes whose dependencies are satisfied are launched IN PARALLEL
|
|
68
|
+
* as a "wave". This avoids slow sequential freePort/spawn calls blocking
|
|
69
|
+
* unrelated processes.
|
|
70
|
+
* - Processes with `waitToExit: true` must exit (successfully) before their
|
|
71
|
+
* dependents can start.
|
|
72
|
+
* - Processes with `waitToExit: false` (default) are considered "done" for
|
|
73
|
+
* dependency purposes as soon as they are launched.
|
|
74
|
+
* - The function resolves once every process has been launched (and all
|
|
75
|
+
* `waitToExit` ones have finished).
|
|
76
|
+
*/
|
|
77
|
+
export async function runProcesses(
|
|
78
|
+
allConfigs: ProcessConfig[],
|
|
79
|
+
manager: ProcessManager,
|
|
80
|
+
options: RunOptions = {},
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
// Apply filters
|
|
83
|
+
let configs = allConfigs;
|
|
84
|
+
|
|
85
|
+
if (options.only && options.only.length > 0) {
|
|
86
|
+
if (options.noDeps) {
|
|
87
|
+
const onlySet = new Set(options.only);
|
|
88
|
+
configs = allConfigs.filter((p) => onlySet.has(p.name));
|
|
89
|
+
} else {
|
|
90
|
+
configs = resolveWithDependencies(allConfigs, options.only);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// When no --only filter, exclude autoStart: false processes
|
|
94
|
+
configs = configs.filter((p) => p.autoStart !== false);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (options.except && options.except.length > 0) {
|
|
98
|
+
const skip = new Set(options.except);
|
|
99
|
+
configs = configs.filter((p) => !skip.has(p.name));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build task graph
|
|
103
|
+
const configMap = new Map(configs.map((c) => [c.name, c]));
|
|
104
|
+
|
|
105
|
+
// Pre-seed "done" with processes already managed (from earlier start calls).
|
|
106
|
+
// A process that is currently running or has exited successfully counts as done
|
|
107
|
+
// for dependency resolution, so new processes can depend on already-launched ones.
|
|
108
|
+
const done = new Set<string>();
|
|
109
|
+
for (const p of manager.getAll()) {
|
|
110
|
+
if (p.status === "running" || p.status === "done") {
|
|
111
|
+
done.add(p.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate that all declared dependencies are present in the run set or already done
|
|
116
|
+
if (!options.noDeps) {
|
|
117
|
+
for (const config of configs) {
|
|
118
|
+
for (const dep of config.dependsOn ?? []) {
|
|
119
|
+
if (!configMap.has(dep) && !done.has(dep)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Process "${config.name}" depends on "${dep}" which is not in the run set and not already running. ` +
|
|
122
|
+
`If you used --only, you may need to include "${dep}" too, or start it first.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// In no-deps mode, treat all dependencies as already satisfied
|
|
130
|
+
if (options.noDeps) {
|
|
131
|
+
for (const config of configs) {
|
|
132
|
+
for (const dep of config.dependsOn ?? []) {
|
|
133
|
+
done.add(dep);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const waitingToExit = new Set<string>();
|
|
139
|
+
const pending = new Set(configs.map((c) => c.name));
|
|
140
|
+
|
|
141
|
+
// Don't re-launch processes that are already running
|
|
142
|
+
for (const name of [...pending]) {
|
|
143
|
+
if (manager.isRunning(name)) {
|
|
144
|
+
pending.delete(name);
|
|
145
|
+
done.add(name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// wakePending absorbs a wake() that fires before waitForWake() is called,
|
|
150
|
+
// so the scheduler never gets stuck after a fast-exiting waitToExit process.
|
|
151
|
+
let wakeResolve: (() => void) | null = null;
|
|
152
|
+
let wakePending = false;
|
|
153
|
+
const wake = () => {
|
|
154
|
+
if (wakeResolve) {
|
|
155
|
+
const r = wakeResolve;
|
|
156
|
+
wakeResolve = null;
|
|
157
|
+
r();
|
|
158
|
+
} else {
|
|
159
|
+
wakePending = true;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const waitForWake = () => {
|
|
163
|
+
if (wakePending) {
|
|
164
|
+
wakePending = false;
|
|
165
|
+
return Promise.resolve();
|
|
166
|
+
}
|
|
167
|
+
return new Promise<void>((resolve) => {
|
|
168
|
+
wakeResolve = resolve;
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
let shutdownRequested = false;
|
|
173
|
+
|
|
174
|
+
while (pending.size > 0 || waitingToExit.size > 0) {
|
|
175
|
+
if (shutdownRequested) break;
|
|
176
|
+
|
|
177
|
+
// Collect every pending task whose dependencies are all satisfied
|
|
178
|
+
let wave = [...pending].filter((name) => {
|
|
179
|
+
const config = configMap.get(name)!;
|
|
180
|
+
return (config.dependsOn ?? []).every((d) => done.has(d));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// In serial mode, launch only one process at a time
|
|
184
|
+
if (options.serial && wave.length > 1) {
|
|
185
|
+
wave = [wave[0]];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (wave.length > 0) {
|
|
189
|
+
// Remove from pending before any async work to prevent double-launch
|
|
190
|
+
for (const name of wave) pending.delete(name);
|
|
191
|
+
|
|
192
|
+
// Launch all tasks in this wave in parallel.
|
|
193
|
+
// Each task is isolated in its own try/catch so one launch failure
|
|
194
|
+
// does not prevent other tasks in the same wave from starting.
|
|
195
|
+
await Promise.all(
|
|
196
|
+
wave.map(async (name) => {
|
|
197
|
+
if (shutdownRequested) return;
|
|
198
|
+
|
|
199
|
+
const config = configMap.get(name)!;
|
|
200
|
+
|
|
201
|
+
let waitForExit: Promise<number>;
|
|
202
|
+
let logFile: string | null;
|
|
203
|
+
try {
|
|
204
|
+
({ waitForExit, logFile } = await manager.launch(config));
|
|
205
|
+
} catch (err: any) {
|
|
206
|
+
options.onTaskFailed?.(name, -1);
|
|
207
|
+
if (config.critical !== false) {
|
|
208
|
+
shutdownRequested = true;
|
|
209
|
+
options.onShutdown?.(`Failed to launch "${name}": ${err?.message ?? err}`);
|
|
210
|
+
wake();
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// onTaskStart is called after launch so manager.get(name) is valid
|
|
216
|
+
options.onTaskStart?.(name, logFile);
|
|
217
|
+
|
|
218
|
+
if (config.waitToExit === true) {
|
|
219
|
+
waitingToExit.add(name);
|
|
220
|
+
|
|
221
|
+
waitForExit.then((code) => {
|
|
222
|
+
waitingToExit.delete(name);
|
|
223
|
+
|
|
224
|
+
if (code === 0) {
|
|
225
|
+
done.add(name);
|
|
226
|
+
options.onTaskDone?.(name);
|
|
227
|
+
} else {
|
|
228
|
+
options.onTaskFailed?.(name, code);
|
|
229
|
+
|
|
230
|
+
if (config.critical !== false) {
|
|
231
|
+
shutdownRequested = true;
|
|
232
|
+
options.onShutdown?.(
|
|
233
|
+
`Process "${name}" exited with code ${code}`,
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
done.add(name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
wake();
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
// Background — immediately "done" for dep resolution
|
|
244
|
+
done.add(name);
|
|
245
|
+
options.onTaskRunning?.(name);
|
|
246
|
+
|
|
247
|
+
waitForExit.then((code) => {
|
|
248
|
+
if (code !== 0 && config.critical !== false && !manager.isStopping(name)) {
|
|
249
|
+
options.onTaskFailed?.(name, code);
|
|
250
|
+
shutdownRequested = true;
|
|
251
|
+
options.onShutdown?.(
|
|
252
|
+
`Background process "${name}" exited unexpectedly with code ${code}`,
|
|
253
|
+
);
|
|
254
|
+
wake();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
} else if (waitingToExit.size > 0) {
|
|
261
|
+
// All ready tasks are launched; wait for a waitToExit one to finish
|
|
262
|
+
await waitForWake();
|
|
263
|
+
} else {
|
|
264
|
+
// pending > 0 but nothing is ready and nothing is waiting — circular dep
|
|
265
|
+
const blocked = [...pending].map((n) => {
|
|
266
|
+
const missing = (configMap.get(n)!.dependsOn ?? []).filter(
|
|
267
|
+
(d) => !done.has(d),
|
|
268
|
+
);
|
|
269
|
+
return ` "${n}" is waiting for: ${missing.join(", ")}`;
|
|
270
|
+
});
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Circular dependency or unresolvable dependency detected:\n${blocked.join("\n")}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|