nvent 0.5.15 → 1.0.0-alpha.2

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 (149) hide show
  1. package/dist/module.d.mts +181 -8
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1257 -1196
  4. package/dist/runtime/app/composables/useFunctionCall.d.ts +17 -0
  5. package/dist/runtime/app/composables/useFunctionCall.js +25 -0
  6. package/dist/runtime/app/composables/useNventStream.d.ts +23 -0
  7. package/dist/runtime/app/composables/useNventStream.js +58 -0
  8. package/dist/runtime/nitro/plugins/00.iii-lifecycle.d.ts +19 -0
  9. package/dist/runtime/nitro/plugins/00.iii-lifecycle.js +62 -0
  10. package/dist/runtime/nitro/plugins/01.iii-worker.d.ts +15 -0
  11. package/dist/runtime/nitro/plugins/01.iii-worker.js +91 -0
  12. package/dist/runtime/nitro/routes/stream-proxy.d.ts +17 -0
  13. package/dist/runtime/nitro/routes/stream-proxy.js +45 -0
  14. package/dist/runtime/nitro/utils/console.d.ts +32 -0
  15. package/dist/runtime/nitro/utils/console.js +95 -0
  16. package/dist/runtime/nitro/utils/defineFunction.d.ts +378 -3
  17. package/dist/runtime/nitro/utils/defineFunction.js +224 -4
  18. package/dist/runtime/nitro/utils/engine.d.ts +25 -0
  19. package/dist/runtime/nitro/utils/engine.js +130 -0
  20. package/dist/runtime/nitro/utils/nventDir.d.ts +1 -0
  21. package/dist/runtime/nitro/utils/nventDir.js +20 -0
  22. package/dist/runtime/nitro/utils/useIii.d.ts +23 -0
  23. package/dist/runtime/nitro/utils/useIii.js +14 -0
  24. package/dist/runtime/nitro/utils/workers/node.d.ts +24 -0
  25. package/dist/runtime/nitro/utils/workers/node.js +57 -0
  26. package/dist/runtime/nitro/utils/workers/python.d.ts +67 -0
  27. package/dist/runtime/nitro/utils/workers/python.js +220 -0
  28. package/dist/runtime/python/nvent.py +208 -0
  29. package/dist/runtime/python/worker_runtime.py +659 -0
  30. package/dist/runtime/virtual.d.ts +17 -0
  31. package/dist/types.d.mts +5 -1
  32. package/package.json +11 -14
  33. package/dist/runtime/adapters/base/index.d.ts +0 -6
  34. package/dist/runtime/adapters/base/index.js +0 -1
  35. package/dist/runtime/adapters/base/store-validator.d.ts +0 -48
  36. package/dist/runtime/adapters/base/store-validator.js +0 -147
  37. package/dist/runtime/adapters/builtin/file-queue.d.ts +0 -67
  38. package/dist/runtime/adapters/builtin/file-queue.js +0 -499
  39. package/dist/runtime/adapters/builtin/file-store.d.ts +0 -32
  40. package/dist/runtime/adapters/builtin/file-store.js +0 -206
  41. package/dist/runtime/adapters/builtin/file-stream.d.ts +0 -39
  42. package/dist/runtime/adapters/builtin/file-stream.js +0 -56
  43. package/dist/runtime/adapters/builtin/index.d.ts +0 -10
  44. package/dist/runtime/adapters/builtin/index.js +0 -5
  45. package/dist/runtime/adapters/builtin/memory-queue.d.ts +0 -52
  46. package/dist/runtime/adapters/builtin/memory-queue.js +0 -243
  47. package/dist/runtime/adapters/builtin/memory-store.d.ts +0 -69
  48. package/dist/runtime/adapters/builtin/memory-store.js +0 -357
  49. package/dist/runtime/adapters/builtin/memory-stream.d.ts +0 -21
  50. package/dist/runtime/adapters/builtin/memory-stream.js +0 -56
  51. package/dist/runtime/adapters/factory.d.ts +0 -31
  52. package/dist/runtime/adapters/factory.js +0 -135
  53. package/dist/runtime/adapters/index.d.ts +0 -8
  54. package/dist/runtime/adapters/index.js +0 -3
  55. package/dist/runtime/adapters/interfaces/index.d.ts +0 -11
  56. package/dist/runtime/adapters/interfaces/index.js +0 -3
  57. package/dist/runtime/adapters/interfaces/queue.d.ts +0 -155
  58. package/dist/runtime/adapters/interfaces/queue.js +0 -0
  59. package/dist/runtime/adapters/interfaces/store.d.ts +0 -299
  60. package/dist/runtime/adapters/interfaces/store.js +0 -0
  61. package/dist/runtime/adapters/interfaces/stream.d.ts +0 -62
  62. package/dist/runtime/adapters/interfaces/stream.js +0 -0
  63. package/dist/runtime/adapters/registry.d.ts +0 -85
  64. package/dist/runtime/adapters/registry.js +0 -161
  65. package/dist/runtime/config/index.d.ts +0 -23
  66. package/dist/runtime/config/index.js +0 -200
  67. package/dist/runtime/config/types.d.ts +0 -439
  68. package/dist/runtime/config/types.js +0 -0
  69. package/dist/runtime/events/eventBus.d.ts +0 -20
  70. package/dist/runtime/events/eventBus.js +0 -35
  71. package/dist/runtime/events/types.d.ts +0 -144
  72. package/dist/runtime/events/types.js +0 -0
  73. package/dist/runtime/events/utils/scheduleTrigger.d.ts +0 -8
  74. package/dist/runtime/events/utils/scheduleTrigger.js +0 -71
  75. package/dist/runtime/events/utils/stallDetector.d.ts +0 -83
  76. package/dist/runtime/events/utils/stallDetector.js +0 -318
  77. package/dist/runtime/events/utils/triggerRuntime.d.ts +0 -58
  78. package/dist/runtime/events/utils/triggerRuntime.js +0 -212
  79. package/dist/runtime/events/wiring/flowWiring.d.ts +0 -34
  80. package/dist/runtime/events/wiring/flowWiring.js +0 -1258
  81. package/dist/runtime/events/wiring/registry.d.ts +0 -19
  82. package/dist/runtime/events/wiring/registry.js +0 -43
  83. package/dist/runtime/events/wiring/stateWiring.d.ts +0 -37
  84. package/dist/runtime/events/wiring/stateWiring.js +0 -92
  85. package/dist/runtime/events/wiring/streamWiring.d.ts +0 -36
  86. package/dist/runtime/events/wiring/streamWiring.js +0 -156
  87. package/dist/runtime/events/wiring/triggerWiring.d.ts +0 -21
  88. package/dist/runtime/events/wiring/triggerWiring.js +0 -422
  89. package/dist/runtime/nitro/plugins/00.adapters.d.ts +0 -14
  90. package/dist/runtime/nitro/plugins/00.adapters.js +0 -73
  91. package/dist/runtime/nitro/plugins/01.ws-lifecycle.d.ts +0 -5
  92. package/dist/runtime/nitro/plugins/01.ws-lifecycle.js +0 -69
  93. package/dist/runtime/nitro/plugins/02.workers.d.ts +0 -2
  94. package/dist/runtime/nitro/plugins/02.workers.js +0 -92
  95. package/dist/runtime/nitro/plugins/03.triggers.d.ts +0 -12
  96. package/dist/runtime/nitro/plugins/03.triggers.js +0 -55
  97. package/dist/runtime/nitro/routes/webhook.await.d.ts +0 -23
  98. package/dist/runtime/nitro/routes/webhook.await.js +0 -112
  99. package/dist/runtime/nitro/routes/webhook.trigger.d.ts +0 -86
  100. package/dist/runtime/nitro/routes/webhook.trigger.js +0 -73
  101. package/dist/runtime/nitro/utils/adapters.d.ts +0 -66
  102. package/dist/runtime/nitro/utils/adapters.js +0 -51
  103. package/dist/runtime/nitro/utils/awaitPatterns/event.d.ts +0 -15
  104. package/dist/runtime/nitro/utils/awaitPatterns/event.js +0 -128
  105. package/dist/runtime/nitro/utils/awaitPatterns/index.d.ts +0 -28
  106. package/dist/runtime/nitro/utils/awaitPatterns/index.js +0 -55
  107. package/dist/runtime/nitro/utils/awaitPatterns/schedule.d.ts +0 -16
  108. package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +0 -83
  109. package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +0 -15
  110. package/dist/runtime/nitro/utils/awaitPatterns/time.js +0 -71
  111. package/dist/runtime/nitro/utils/awaitPatterns/webhook.d.ts +0 -15
  112. package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +0 -128
  113. package/dist/runtime/nitro/utils/defineFunctionConfig.d.ts +0 -378
  114. package/dist/runtime/nitro/utils/defineFunctionConfig.js +0 -3
  115. package/dist/runtime/nitro/utils/defineHooks.d.ts +0 -95
  116. package/dist/runtime/nitro/utils/defineHooks.js +0 -9
  117. package/dist/runtime/nitro/utils/registerAdapter.d.ts +0 -59
  118. package/dist/runtime/nitro/utils/registerAdapter.js +0 -13
  119. package/dist/runtime/nitro/utils/useAwait.d.ts +0 -83
  120. package/dist/runtime/nitro/utils/useAwait.js +0 -169
  121. package/dist/runtime/nitro/utils/useEventManager.d.ts +0 -15
  122. package/dist/runtime/nitro/utils/useEventManager.js +0 -26
  123. package/dist/runtime/nitro/utils/useFlow.d.ts +0 -66
  124. package/dist/runtime/nitro/utils/useFlow.js +0 -310
  125. package/dist/runtime/nitro/utils/useHookRegistry.d.ts +0 -40
  126. package/dist/runtime/nitro/utils/useHookRegistry.js +0 -25
  127. package/dist/runtime/nitro/utils/useNventLogger.d.ts +0 -42
  128. package/dist/runtime/nitro/utils/useNventLogger.js +0 -54
  129. package/dist/runtime/nitro/utils/useRunContext.d.ts +0 -6
  130. package/dist/runtime/nitro/utils/useRunContext.js +0 -102
  131. package/dist/runtime/nitro/utils/useStreamTopics.d.ts +0 -83
  132. package/dist/runtime/nitro/utils/useStreamTopics.js +0 -94
  133. package/dist/runtime/nitro/utils/useTrigger.d.ts +0 -150
  134. package/dist/runtime/nitro/utils/useTrigger.js +0 -311
  135. package/dist/runtime/nitro/utils/wsPeerManager.d.ts +0 -44
  136. package/dist/runtime/nitro/utils/wsPeerManager.js +0 -32
  137. package/dist/runtime/scheduler/index.d.ts +0 -33
  138. package/dist/runtime/scheduler/index.js +0 -42
  139. package/dist/runtime/scheduler/scheduler.d.ts +0 -132
  140. package/dist/runtime/scheduler/scheduler.js +0 -799
  141. package/dist/runtime/scheduler/types.d.ts +0 -122
  142. package/dist/runtime/scheduler/types.js +0 -0
  143. package/dist/runtime/worker/node/runner.d.ts +0 -95
  144. package/dist/runtime/worker/node/runner.js +0 -272
  145. package/dist/runtime/worker/python/get_config.py +0 -64
  146. package/dist/runtime/worker/system/awaitHandlers.d.ts +0 -27
  147. package/dist/runtime/worker/system/awaitHandlers.js +0 -230
  148. package/dist/runtime/worker/system/index.d.ts +0 -24
  149. package/dist/runtime/worker/system/index.js +0 -39
@@ -0,0 +1,130 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createConnection } from "node:net";
3
+ import { consola } from "consola";
4
+ const logger = consola.withTag("nvent:iii-engine");
5
+ const READY_POLL_INTERVAL_MS = 200;
6
+ const READY_TIMEOUT_MS = 15e3;
7
+ async function pollReady(httpPort) {
8
+ const url = `http://localhost:${httpPort}`;
9
+ const deadline = Date.now() + READY_TIMEOUT_MS;
10
+ while (Date.now() < deadline) {
11
+ try {
12
+ const res = await fetch(url, { signal: AbortSignal.timeout(500) });
13
+ if (res.status < 600) return;
14
+ } catch {
15
+ }
16
+ await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
17
+ }
18
+ throw new Error(`iii engine did not become ready within ${READY_TIMEOUT_MS}ms (HTTP port ${httpPort})`);
19
+ }
20
+ function isTcpPortOpen(port, host = "127.0.0.1") {
21
+ return new Promise((resolve) => {
22
+ const socket = createConnection({ port, host }, () => {
23
+ socket.destroy();
24
+ resolve(true);
25
+ });
26
+ socket.once("error", () => resolve(false));
27
+ setTimeout(() => {
28
+ socket.destroy();
29
+ resolve(false);
30
+ }, 1e3);
31
+ });
32
+ }
33
+ function parseEngineLineLevel(line) {
34
+ if (line.includes("[ERROR]")) return "error";
35
+ if (line.includes("[WARN]")) return "warn";
36
+ if (line.includes("[INFO]")) return "info";
37
+ return null;
38
+ }
39
+ const LOG_LEVEL_RANK = { none: 0, error: 1, warn: 2, info: 3 };
40
+ function shouldShow(lineLevel, minLevel) {
41
+ return LOG_LEVEL_RANK[lineLevel] <= LOG_LEVEL_RANK[minLevel];
42
+ }
43
+ export class EngineManager {
44
+ process = null;
45
+ opts;
46
+ constructor(opts) {
47
+ this.opts = { httpPort: 3111, wsPort: 49134, logLevel: "warn", ...opts };
48
+ }
49
+ isRunning() {
50
+ return this.process !== null && !this.process.killed && this.process.exitCode === null;
51
+ }
52
+ async start() {
53
+ if (this.isRunning()) {
54
+ logger.warn("iii engine is already running");
55
+ return;
56
+ }
57
+ const { binaryPath, configPath, httpPort, wsPort, logLevel } = this.opts;
58
+ if (logLevel === "info") logger.info(`Starting iii engine \u2014 config: ${configPath}`);
59
+ this.process = spawn(binaryPath, ["--config", configPath], {
60
+ stdio: ["ignore", "pipe", "pipe"],
61
+ detached: false
62
+ });
63
+ let lastLevel = "info";
64
+ this.process.stdout?.on("data", (chunk) => {
65
+ if (logLevel === "none") return;
66
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
67
+ const parsed = parseEngineLineLevel(line);
68
+ if (parsed) lastLevel = parsed;
69
+ const effectiveLevel = parsed ?? lastLevel;
70
+ if (!shouldShow(effectiveLevel, logLevel)) continue;
71
+ if (effectiveLevel === "error") logger.error(`[iii] ${line}`);
72
+ else if (effectiveLevel === "warn") logger.warn(`[iii] ${line}`);
73
+ else logger.info(`[iii] ${line}`);
74
+ }
75
+ });
76
+ this.process.stderr?.on("data", (chunk) => {
77
+ if (logLevel === "none") return;
78
+ for (const line of chunk.toString().split("\n").filter(Boolean))
79
+ logger.warn(`[iii] ${line}`);
80
+ });
81
+ let earlyExitCode = null;
82
+ const earlyExitPromise = new Promise((resolve) => {
83
+ this.process.once("exit", (code, signal) => {
84
+ earlyExitCode = code;
85
+ if (!earlyExitCode && logLevel === "info") logger.info(`iii engine exited (code=${code}, signal=${signal})`);
86
+ this.process = null;
87
+ resolve();
88
+ });
89
+ });
90
+ this.process.on("error", (err) => {
91
+ logger.error(`iii engine process error: ${err.message}`);
92
+ this.process = null;
93
+ });
94
+ await Promise.race([pollReady(httpPort), earlyExitPromise]);
95
+ if (earlyExitCode !== null && earlyExitCode !== 0) {
96
+ const [httpOk, wsOk] = await Promise.all([isTcpPortOpen(httpPort), isTcpPortOpen(wsPort)]);
97
+ if (httpOk && wsOk) {
98
+ if (logLevel === "info") logger.info(`iii engine already running on HTTP :${httpPort} / WS :${wsPort} \u2014 reusing`);
99
+ return;
100
+ }
101
+ throw new Error(
102
+ `iii engine failed to start (exit code ${earlyExitCode}). Ports ${httpPort} (HTTP) or ${wsPort} (WS) may be in use.`
103
+ );
104
+ }
105
+ if (logLevel === "info") logger.info(`iii engine ready (WS :${wsPort}, HTTP :${httpPort})`);
106
+ }
107
+ async stop() {
108
+ if (!this.isRunning() || !this.process) return;
109
+ const { logLevel } = this.opts;
110
+ if (logLevel === "info") logger.info("Stopping iii engine...");
111
+ return new Promise((resolve) => {
112
+ this.process.once("exit", () => {
113
+ this.process = null;
114
+ if (logLevel === "info") logger.info("iii engine stopped");
115
+ resolve();
116
+ });
117
+ this.process.kill("SIGTERM");
118
+ setTimeout(() => {
119
+ if (this.process) this.process.kill("SIGKILL");
120
+ }, 5e3);
121
+ });
122
+ }
123
+ async restart() {
124
+ await this.stop();
125
+ await this.start();
126
+ }
127
+ }
128
+ export function createEngineManager(opts) {
129
+ return new EngineManager(opts);
130
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveNventDir(importMetaUrl: string): string;
@@ -0,0 +1,20 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export function resolveNventDir(importMetaUrl) {
4
+ if (process.env.NVENT_DIR) return process.env.NVENT_DIR;
5
+ if (importMetaUrl && !importMetaUrl.endsWith("/_entry.js")) {
6
+ try {
7
+ const serverDir = dirname(fileURLToPath(importMetaUrl));
8
+ const candidate = join(serverDir, "..", "nvent");
9
+ if (candidate !== "/nvent") {
10
+ console.log(`[nvent] resolveNventDir via importMetaUrl: ${candidate}`);
11
+ return candidate;
12
+ }
13
+ } catch (e) {
14
+ console.warn(`[nvent] resolveNventDir: failed to parse importMetaUrl=${importMetaUrl}`, e);
15
+ }
16
+ }
17
+ const fromCwd = resolve(process.cwd(), "nvent");
18
+ console.log(`[nvent] resolveNventDir via CWD fallback: ${fromCwd} (importMetaUrl=${importMetaUrl})`);
19
+ return fromCwd;
20
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * useIii — returns the iii SDK instance stored by the worker plugin.
3
+ *
4
+ * Available in any Nitro event handler or iii function:
5
+ * ```ts
6
+ * const { iii } = useIii()
7
+ * const result = await iii.trigger('orders::process', data)
8
+ *
9
+ * // Inside a registered function handler, get the execution context separately:
10
+ * const { iii } = useIii()
11
+ * const { logger } = getContext() // auto-imported from iii-sdk via #imports
12
+ * ```
13
+ */
14
+ import type { registerWorker } from 'iii-sdk';
15
+ type IiiInstance = ReturnType<typeof registerWorker>;
16
+ export declare function useIii(): {
17
+ iii: IiiInstance;
18
+ };
19
+ /**
20
+ * useIiiHealth — returns the current engine health status.
21
+ */
22
+ export declare function useIiiHealth(): Promise<Record<string, unknown>>;
23
+ export { getContext } from 'iii-sdk';
@@ -0,0 +1,14 @@
1
+ import { useNitroApp } from "#imports";
2
+ export function useIii() {
3
+ const nitroApp = useNitroApp();
4
+ const iii = nitroApp.$iii;
5
+ if (!iii) {
6
+ throw new Error("[nvent] iii SDK not initialized. Is the iii-worker Nitro plugin loaded?");
7
+ }
8
+ return { iii };
9
+ }
10
+ export async function useIiiHealth() {
11
+ const { iii } = useIii();
12
+ return iii.trigger({ function_id: "engine::health::check", payload: {} });
13
+ }
14
+ export { getContext } from "iii-sdk";
@@ -0,0 +1,24 @@
1
+ import type { registerWorker } from 'iii-sdk';
2
+ import { FunctionContext } from '../defineFunction.js';
3
+ type IiiClient = ReturnType<typeof registerWorker>;
4
+ export interface NodeFnInfo {
5
+ name: string;
6
+ description?: string;
7
+ handler: (input: unknown, ctx: FunctionContext) => Promise<unknown>;
8
+ triggers: Array<{
9
+ type: string;
10
+ function_id: string;
11
+ config?: Record<string, unknown>;
12
+ }>;
13
+ enqueues: string[];
14
+ flows: string[];
15
+ /** Explicit stream name for implicit ctx.stream ops. Falls back to flows[0] then name prefix. */
16
+ stream?: string;
17
+ filePath?: string;
18
+ }
19
+ /**
20
+ * Registers all Node.js (TypeScript) functions and their triggers with the iii client.
21
+ * Mirrors the Python side: one registration per trigger, with a validated function ID.
22
+ */
23
+ export declare function registerNodeFunctions(iii: IiiClient, fns: NodeFnInfo[]): void;
24
+ export {};
@@ -0,0 +1,57 @@
1
+ import { FunctionContext, NVENT_STREAM_KEY } from "../defineFunction.js";
2
+ function extractStreamContext(input) {
3
+ if (typeof input === "object" && input !== null && NVENT_STREAM_KEY in input) {
4
+ return input[NVENT_STREAM_KEY];
5
+ }
6
+ }
7
+ function stripStreamContext(input) {
8
+ if (typeof input === "object" && input !== null && NVENT_STREAM_KEY in input) {
9
+ const { [NVENT_STREAM_KEY]: _, ...rest } = input;
10
+ return rest;
11
+ }
12
+ return input;
13
+ }
14
+ function triggerSuffix(type, cfg) {
15
+ if (type === "http") return `http(${cfg.http_method ?? "GET"}_${cfg.api_path ?? "/"})`;
16
+ if (type === "queue") return `queue(${cfg.topic ?? ""})`;
17
+ if (type === "cron") return `cron(${cfg.expression ?? ""})`;
18
+ return type;
19
+ }
20
+ export function registerNodeFunctions(iii, fns) {
21
+ for (const fn of fns) {
22
+ const metadata = {
23
+ name: fn.name,
24
+ description: fn.description,
25
+ filePath: fn.filePath,
26
+ triggers: (fn.triggers ?? []).map((t) => {
27
+ const cfg = t.config ?? {};
28
+ if (t.type === "http") return { type: "http", path: cfg.api_path, method: cfg.http_method };
29
+ if (t.type === "queue") return { type: "queue", topic: cfg.topic };
30
+ if (t.type === "cron") return { type: "cron", expression: cfg.expression };
31
+ return { type: t.type, ...cfg };
32
+ }),
33
+ flows: fn.flows ?? [],
34
+ enqueues: fn.enqueues ?? []
35
+ };
36
+ const seenSuffixes = /* @__PURE__ */ new Set();
37
+ for (const [index, trigger] of (fn.triggers ?? []).entries()) {
38
+ const cfg = trigger.config ?? {};
39
+ let suffix = triggerSuffix(trigger.type, cfg);
40
+ if (seenSuffixes.has(suffix)) suffix = `${suffix}::${index}`;
41
+ seenSuffixes.add(suffix);
42
+ const function_id = `steps::${fn.name}::trigger::${suffix}`;
43
+ const triggerType = trigger.type;
44
+ const streamName = fn.stream ?? fn.flows?.[0] ?? fn.name.split("::")[0];
45
+ iii.registerFunction(
46
+ { id: function_id, metadata },
47
+ (input) => {
48
+ const inherited = extractStreamContext(input);
49
+ const cleanInput = stripStreamContext(input);
50
+ const effectiveStream = inherited?.name ?? streamName;
51
+ return fn.handler(cleanInput, new FunctionContext(triggerType, fn.name, effectiveStream, inherited?.groupId));
52
+ }
53
+ );
54
+ iii.registerTrigger({ type: trigger.type, function_id, config: { ...cfg, metadata } });
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Python Worker Manager
3
+ *
4
+ * Manages spawning and lifecycle of Python worker processes that connect
5
+ * Python functions to the iii engine.
6
+ *
7
+ * Lives in src/iii/ so it can be used by both:
8
+ * - module.ts (dev) — starts workers in the Nuxt process for HMR
9
+ * - the Nitro plugin (prod) — starts workers at server startup
10
+ */
11
+ /** Minimal function descriptor — matches PythonFunctionMeta from registry.ts */
12
+ export interface PyFnInfo {
13
+ id: string;
14
+ absPath: string;
15
+ standalone: boolean;
16
+ }
17
+ export declare class PythonWorkerManager {
18
+ /** Absolute path to _runtime.py */
19
+ private readonly runtimeScript;
20
+ private readonly wsUrl;
21
+ private readonly workerName;
22
+ private readonly fns;
23
+ private readonly python;
24
+ private readonly logLevel;
25
+ private process;
26
+ private stopped;
27
+ private restartAttempts;
28
+ constructor(
29
+ /** Absolute path to _runtime.py */
30
+ runtimeScript: string, wsUrl: string, workerName: string, fns: PyFnInfo[], python?: string, logLevel?: string);
31
+ isRunning(): boolean;
32
+ start(): Promise<void>;
33
+ restart(): Promise<void>;
34
+ stop(): Promise<void>;
35
+ private _spawn;
36
+ private _scheduleRestart;
37
+ private _kill;
38
+ }
39
+ /**
40
+ * Orchestrates all Python workers:
41
+ * - Shared worker: one process for all non-standalone functions
42
+ * - Standalone workers: one dedicated process per function with `standalone: true`
43
+ */
44
+ export declare class PythonWorkersOrchestrator {
45
+ private readonly workersDir;
46
+ /** Content of worker_runtime.py, written to workersDir/_runtime.py */
47
+ private readonly runtimeContent;
48
+ /** Content of nvent.py, written to workersDir/nvent.py */
49
+ private readonly nventHelperContent;
50
+ private readonly wsUrl;
51
+ private readonly pythonBin;
52
+ private readonly logLevel;
53
+ private workers;
54
+ private lastFns;
55
+ private runtimeScript;
56
+ constructor(workersDir: string,
57
+ /** Content of worker_runtime.py, written to workersDir/_runtime.py */
58
+ runtimeContent: string,
59
+ /** Content of nvent.py, written to workersDir/nvent.py */
60
+ nventHelperContent: string, wsUrl: string, pythonBin: string, logLevel?: string);
61
+ start(fns: PyFnInfo[]): Promise<void>;
62
+ /** Called when a Python source file changes. Pass updated fns to handle additions/removals. */
63
+ onFileChanged(changedPath: string, fns?: PyFnInfo[]): Promise<void>;
64
+ stop(): Promise<void>;
65
+ private _prepareDir;
66
+ private _makeWorker;
67
+ }
@@ -0,0 +1,220 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { consola } from "consola";
5
+ const logger = consola.withTag("nvent:python");
6
+ function parsePythonLineLevel(line) {
7
+ if (line.startsWith("[ERROR]") || line.startsWith("[CRITICAL]")) return "error";
8
+ if (line.startsWith("[WARNING]") || line.startsWith("[WARN]")) return "warn";
9
+ if (line.startsWith("[INFO]")) return "info";
10
+ if (line.startsWith("[DEBUG]")) return "info";
11
+ return null;
12
+ }
13
+ const LOG_LEVEL_RANK = { none: 0, error: 1, warn: 2, info: 3 };
14
+ function shouldShow(lineLevel, minLevel) {
15
+ return (LOG_LEVEL_RANK[lineLevel] ?? 0) <= (LOG_LEVEL_RANK[minLevel] ?? 0);
16
+ }
17
+ const MAX_BACKOFF_MS = 3e4;
18
+ export class PythonWorkerManager {
19
+ constructor(runtimeScript, wsUrl, workerName, fns, python = "python3", logLevel = "warn") {
20
+ this.runtimeScript = runtimeScript;
21
+ this.wsUrl = wsUrl;
22
+ this.workerName = workerName;
23
+ this.fns = fns;
24
+ this.python = python;
25
+ this.logLevel = logLevel;
26
+ }
27
+ process = null;
28
+ stopped = false;
29
+ restartAttempts = 0;
30
+ isRunning() {
31
+ return this.process !== null && !this.process.killed && this.process.exitCode === null;
32
+ }
33
+ async start() {
34
+ if (this.isRunning()) return;
35
+ this.stopped = false;
36
+ this.restartAttempts = 0;
37
+ this._spawn();
38
+ }
39
+ async restart() {
40
+ this.stopped = true;
41
+ await this._kill();
42
+ this.stopped = false;
43
+ this.restartAttempts = 0;
44
+ this._spawn();
45
+ }
46
+ async stop() {
47
+ this.stopped = true;
48
+ await this._kill();
49
+ }
50
+ _spawn() {
51
+ if (this.logLevel === "info") logger.info(`Starting Python worker \u2014 ${this.workerName}`);
52
+ const fnArgs = this.fns.flatMap((fn) => [fn.absPath, fn.id]);
53
+ this.process = spawn(this.python, [this.runtimeScript, this.wsUrl, this.workerName, ...fnArgs], {
54
+ stdio: ["ignore", "pipe", "pipe"],
55
+ detached: false
56
+ });
57
+ let stdoutBuffer = "";
58
+ this.process.stdout?.on("data", (chunk) => {
59
+ const text = chunk.toString();
60
+ stdoutBuffer += text;
61
+ if (this.logLevel === "none") return;
62
+ for (const line of text.split("\n").filter(Boolean)) {
63
+ const level = parsePythonLineLevel(line) ?? "info";
64
+ if (!shouldShow(level, this.logLevel)) continue;
65
+ if (level === "error") logger.error(`[python] ${line}`);
66
+ else if (level === "warn") logger.warn(`[python] ${line}`);
67
+ else logger.info(`[python] ${line}`);
68
+ }
69
+ });
70
+ this.process.stderr?.on("data", (chunk) => {
71
+ if (this.logLevel === "none") return;
72
+ for (const line of chunk.toString().split("\n").filter(Boolean))
73
+ logger.error(`[python] ${line}`);
74
+ });
75
+ this.process.on("exit", (code, signal) => {
76
+ if (this.logLevel === "info") logger.info(`Python worker exited (code=${code}, signal=${signal})`);
77
+ this.process = null;
78
+ if (this.stopped) return;
79
+ if (code === 1 && stdoutBuffer.includes("iii package not found")) {
80
+ logger.error("Python worker stopped: iii SDK not installed. Run: pip install iii-sdk");
81
+ this.stopped = true;
82
+ return;
83
+ }
84
+ this._scheduleRestart();
85
+ });
86
+ this.process.on("error", (err) => {
87
+ logger.error(`Python worker process error: ${err.message}`);
88
+ this.process = null;
89
+ if (err.code === "ENOENT") {
90
+ logger.error(`Python binary not found: ${this.python}. Check functions.python.path in nvent config.`);
91
+ this.stopped = true;
92
+ return;
93
+ }
94
+ if (!this.stopped) this._scheduleRestart();
95
+ });
96
+ }
97
+ _scheduleRestart() {
98
+ this.restartAttempts++;
99
+ const delay = Math.min(500 * 2 ** this.restartAttempts, MAX_BACKOFF_MS);
100
+ logger.warn(`Python worker will restart in ${delay}ms (attempt ${this.restartAttempts})`);
101
+ setTimeout(() => {
102
+ if (!this.stopped) this._spawn();
103
+ }, delay);
104
+ }
105
+ _kill() {
106
+ if (!this.isRunning() || !this.process) return Promise.resolve();
107
+ return new Promise((resolve) => {
108
+ this.process.once("exit", () => {
109
+ this.process = null;
110
+ resolve();
111
+ });
112
+ this.process.kill("SIGTERM");
113
+ setTimeout(() => {
114
+ if (this.process) this.process.kill("SIGKILL");
115
+ }, 5e3);
116
+ });
117
+ }
118
+ }
119
+ export class PythonWorkersOrchestrator {
120
+ constructor(workersDir, runtimeContent, nventHelperContent, wsUrl, pythonBin, logLevel = "warn") {
121
+ this.workersDir = workersDir;
122
+ this.runtimeContent = runtimeContent;
123
+ this.nventHelperContent = nventHelperContent;
124
+ this.wsUrl = wsUrl;
125
+ this.pythonBin = pythonBin;
126
+ this.logLevel = logLevel;
127
+ this.runtimeScript = join(workersDir, "_runtime.py");
128
+ }
129
+ workers = /* @__PURE__ */ new Map();
130
+ lastFns = [];
131
+ runtimeScript;
132
+ async start(fns) {
133
+ this.lastFns = fns;
134
+ if (fns.length === 0) return;
135
+ this._prepareDir();
136
+ const sharedFns = fns.filter((fn) => !fn.standalone);
137
+ const standaloneFns = fns.filter((fn) => fn.standalone);
138
+ if (sharedFns.length > 0) {
139
+ const worker = this._makeWorker("__shared__", sharedFns);
140
+ await worker.start();
141
+ this.workers.set("__shared__", worker);
142
+ }
143
+ for (const fn of standaloneFns) {
144
+ const worker = this._makeWorker(fn.id, [fn]);
145
+ await worker.start();
146
+ this.workers.set(fn.id, worker);
147
+ }
148
+ }
149
+ /** Called when a Python source file changes. Pass updated fns to handle additions/removals. */
150
+ async onFileChanged(changedPath, fns) {
151
+ const effectiveFns = fns ?? this.lastFns;
152
+ this.lastFns = effectiveFns;
153
+ this._prepareDir();
154
+ const sharedFns = effectiveFns.filter((fn) => !fn.standalone);
155
+ const standaloneFns = effectiveFns.filter((fn) => fn.standalone);
156
+ const changedFn = effectiveFns.find((fn) => fn.absPath === changedPath);
157
+ const affectsShared = !changedFn?.standalone;
158
+ if (affectsShared) {
159
+ if (sharedFns.length > 0) {
160
+ const existing = this.workers.get("__shared__");
161
+ if (existing) {
162
+ await existing.restart();
163
+ } else {
164
+ const worker = this._makeWorker("__shared__", sharedFns);
165
+ await worker.start();
166
+ this.workers.set("__shared__", worker);
167
+ }
168
+ } else {
169
+ const existing = this.workers.get("__shared__");
170
+ if (existing) {
171
+ await existing.stop();
172
+ this.workers.delete("__shared__");
173
+ }
174
+ }
175
+ }
176
+ if (changedFn?.standalone) {
177
+ const existing = this.workers.get(changedFn.id);
178
+ if (existing) {
179
+ await existing.restart();
180
+ } else {
181
+ const worker = this._makeWorker(changedFn.id, [changedFn]);
182
+ await worker.start();
183
+ this.workers.set(changedFn.id, worker);
184
+ }
185
+ }
186
+ for (const [id, worker] of this.workers) {
187
+ if (id === "__shared__") continue;
188
+ if (!effectiveFns.find((fn) => fn.id === id)) {
189
+ await worker.stop();
190
+ this.workers.delete(id);
191
+ }
192
+ }
193
+ for (const fn of standaloneFns) {
194
+ if (!this.workers.has(fn.id)) {
195
+ const worker = this._makeWorker(fn.id, [fn]);
196
+ await worker.start();
197
+ this.workers.set(fn.id, worker);
198
+ }
199
+ }
200
+ }
201
+ async stop() {
202
+ for (const worker of this.workers.values()) await worker.stop();
203
+ this.workers.clear();
204
+ }
205
+ _prepareDir() {
206
+ mkdirSync(this.workersDir, { recursive: true });
207
+ writeFileSync(this.runtimeScript, this.runtimeContent, "utf-8");
208
+ writeFileSync(join(this.workersDir, "nvent.py"), this.nventHelperContent, "utf-8");
209
+ }
210
+ _makeWorker(name, fns) {
211
+ return new PythonWorkerManager(
212
+ this.runtimeScript,
213
+ this.wsUrl,
214
+ `nvent-python-${name}`,
215
+ fns,
216
+ this.pythonBin,
217
+ this.logLevel
218
+ );
219
+ }
220
+ }