sdn-flow 0.2.0

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/.claude/SKILLS.md +7 -0
  2. package/.claude/skills/sdn-plugin-abi-compliance/SKILL.md +56 -0
  3. package/.claude/todo/001-js-host-startup-and-deno.md +85 -0
  4. package/LICENSE +21 -0
  5. package/README.md +223 -0
  6. package/bin/sdn-flow-host.js +169 -0
  7. package/docs/.nojekyll +0 -0
  8. package/docs/ARCHITECTURE.md +200 -0
  9. package/docs/HOST_CAPABILITY_MODEL.md +317 -0
  10. package/docs/PLUGIN_ARCHITECTURE.md +145 -0
  11. package/docs/PLUGIN_COMPATIBILITY.md +61 -0
  12. package/docs/PLUGIN_COMPLIANCE_CHECKS.md +82 -0
  13. package/docs/PLUGIN_MANIFEST.md +94 -0
  14. package/docs/css/style.css +465 -0
  15. package/docs/index.html +218 -0
  16. package/docs/js/app.mjs +751 -0
  17. package/docs/js/editor-panel.mjs +203 -0
  18. package/docs/js/flow-canvas.mjs +515 -0
  19. package/docs/js/flow-model.mjs +391 -0
  20. package/docs/js/workers/emception.worker.js +146 -0
  21. package/docs/js/workers/pyodide.worker.js +134 -0
  22. package/native/flow_source_generator.cpp +1958 -0
  23. package/package.json +67 -0
  24. package/schemas/FlowRuntimeAbi.fbs +91 -0
  25. package/src/auth/canonicalize.js +5 -0
  26. package/src/auth/index.js +11 -0
  27. package/src/auth/permissions.js +8 -0
  28. package/src/compiler/CppFlowSourceGenerator.js +475 -0
  29. package/src/compiler/EmceptionCompilerAdapter.js +244 -0
  30. package/src/compiler/SignedArtifactCatalog.js +152 -0
  31. package/src/compiler/index.js +8 -0
  32. package/src/compiler/nativeFlowSourceGeneratorTool.js +144 -0
  33. package/src/compliance/index.js +13 -0
  34. package/src/compliance/pluginCompliance.js +11 -0
  35. package/src/deploy/FlowDeploymentClient.js +532 -0
  36. package/src/deploy/index.js +8 -0
  37. package/src/designer/FlowDesignerSession.js +158 -0
  38. package/src/designer/index.js +2 -0
  39. package/src/designer/requirements.js +184 -0
  40. package/src/generated/runtimeAbiLayouts.js +544 -0
  41. package/src/host/appHost.js +105 -0
  42. package/src/host/autoHost.js +113 -0
  43. package/src/host/browserHostAdapters.js +108 -0
  44. package/src/host/compiledFlowRuntimeHost.js +703 -0
  45. package/src/host/constants.js +55 -0
  46. package/src/host/dependencyRuntime.js +227 -0
  47. package/src/host/descriptorAbi.js +351 -0
  48. package/src/host/fetchService.js +237 -0
  49. package/src/host/httpHostAdapters.js +280 -0
  50. package/src/host/index.js +91 -0
  51. package/src/host/installedFlowHost.js +885 -0
  52. package/src/host/invocationAbi.js +440 -0
  53. package/src/host/normalize.js +372 -0
  54. package/src/host/packageManagers.js +369 -0
  55. package/src/host/profile.js +134 -0
  56. package/src/host/runtimeAbi.js +106 -0
  57. package/src/host/workspace.js +895 -0
  58. package/src/index.js +8 -0
  59. package/src/runtime/FlowRuntime.js +273 -0
  60. package/src/runtime/MethodRegistry.js +295 -0
  61. package/src/runtime/constants.js +44 -0
  62. package/src/runtime/index.js +19 -0
  63. package/src/runtime/normalize.js +377 -0
  64. package/src/transport/index.js +7 -0
  65. package/src/transport/pki.js +7 -0
  66. package/src/utils/crypto.js +7 -0
  67. package/src/utils/encoding.js +65 -0
  68. package/src/utils/wasmCrypto.js +69 -0
  69. package/tools/run-plugin-compliance-check.mjs +153 -0
@@ -0,0 +1,391 @@
1
+ /**
2
+ * flow-model.mjs — Flow data model with CRC-32 and SHA-256 integrity.
3
+ *
4
+ * Mirrors the sdn-flow JSON schema: nodes, edges, triggers, triggerBindings,
5
+ * externalInterfaces, artifactDependencies, and editor layout metadata.
6
+ */
7
+
8
+ let walletPromise = null;
9
+
10
+ async function getWallet() {
11
+ if (!walletPromise) {
12
+ walletPromise = import("hd-wallet-wasm").then(async (module) => {
13
+ const init = module.default ?? module.createHDWallet;
14
+ return init();
15
+ });
16
+ }
17
+ return walletPromise;
18
+ }
19
+
20
+ // ── CRC-32 (ISO 3309 / ITU-T V.42) ──
21
+
22
+ const CRC_TABLE = new Uint32Array(256);
23
+ for (let i = 0; i < 256; i++) {
24
+ let c = i;
25
+ for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
26
+ CRC_TABLE[i] = c;
27
+ }
28
+
29
+ export function crc32(bytes) {
30
+ if (typeof bytes === "string") bytes = new TextEncoder().encode(bytes);
31
+ let crc = 0xFFFFFFFF;
32
+ for (let i = 0; i < bytes.length; i++) crc = CRC_TABLE[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
33
+ return (crc ^ 0xFFFFFFFF) >>> 0;
34
+ }
35
+
36
+ export function crc32Hex(bytes) {
37
+ return crc32(bytes).toString(16).padStart(8, "0");
38
+ }
39
+
40
+ // ── SHA-256 ──
41
+
42
+ export async function sha256(bytes) {
43
+ if (typeof bytes === "string") bytes = new TextEncoder().encode(bytes);
44
+ try {
45
+ const wallet = await getWallet();
46
+ const digest = wallet.utils.sha256(bytes);
47
+ return Array.from(digest).map(b => b.toString(16).padStart(2, "0")).join("");
48
+ } catch {
49
+ // Fallback to Web Crypto if hd-wallet-wasm is unavailable
50
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
51
+ return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, "0")).join("");
52
+ }
53
+ }
54
+
55
+ // ── Canonical JSON ──
56
+
57
+ export function canonicalize(value) {
58
+ if (value === null || value === undefined) return "null";
59
+ if (typeof value === "boolean" || typeof value === "number") return JSON.stringify(value);
60
+ if (typeof value === "string") return JSON.stringify(value);
61
+ if (value instanceof Uint8Array) {
62
+ return JSON.stringify({ __type: "bytes", base64: btoa(String.fromCharCode(...value)) });
63
+ }
64
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
65
+ const keys = Object.keys(value).filter(k => value[k] !== undefined).sort();
66
+ return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalize(value[k])}`).join(",")}}`;
67
+ }
68
+
69
+ export function canonicalBytes(value) {
70
+ return new TextEncoder().encode(canonicalize(value));
71
+ }
72
+
73
+ // ── Kind colors ──
74
+
75
+ export const KIND_COLORS = {
76
+ trigger: "#c586c0",
77
+ transform: "#569cd6",
78
+ analyzer: "#4ec9b0",
79
+ publisher: "#ce9178",
80
+ responder: "#dcdcaa",
81
+ renderer: "#d16969",
82
+ sink: "#d7ba7d",
83
+ };
84
+
85
+ // ── Default ports per kind ──
86
+
87
+ const DEFAULT_PORTS = {
88
+ trigger: { inputs: [], outputs: [{ id: "out", label: "out" }] },
89
+ transform: { inputs: [{ id: "in", label: "in" }], outputs: [{ id: "out", label: "out" }] },
90
+ analyzer: { inputs: [{ id: "in", label: "in" }], outputs: [{ id: "out", label: "out" }, { id: "metrics", label: "metrics" }] },
91
+ publisher: { inputs: [{ id: "in", label: "in" }], outputs: [] },
92
+ responder: { inputs: [{ id: "req", label: "req" }], outputs: [{ id: "res", label: "res" }] },
93
+ renderer: { inputs: [{ id: "in", label: "in" }], outputs: [] },
94
+ sink: { inputs: [{ id: "in", label: "in" }], outputs: [] },
95
+ };
96
+
97
+ // ── ID generation ──
98
+
99
+ let _idCounter = 0;
100
+ function genId(prefix = "n") {
101
+ return `${prefix}-${Date.now().toString(36)}-${(++_idCounter).toString(36)}`;
102
+ }
103
+
104
+ // ── FlowModel ──
105
+
106
+ export class FlowModel extends EventTarget {
107
+ constructor() {
108
+ super();
109
+ this.programId = "";
110
+ this.name = "Untitled Flow";
111
+ this.version = "0.1.0";
112
+ this.description = "";
113
+ this.nodes = new Map();
114
+ this.edges = new Map();
115
+ this.triggers = new Map();
116
+ this.triggerBindings = [];
117
+ this.externalInterfaces = [];
118
+ this.artifactDependencies = [];
119
+ this.requiredPlugins = [];
120
+ this.editorMeta = { viewport: { x: 0, y: 0, zoom: 1 }, nodes: {} };
121
+ }
122
+
123
+ // ── Nodes ──
124
+
125
+ addNode({ kind, pluginId, methodId, label, lang, x, y, source, ports }) {
126
+ const nodeId = genId("node");
127
+ const defaultPorts = DEFAULT_PORTS[kind] || DEFAULT_PORTS.transform;
128
+ const node = {
129
+ nodeId,
130
+ pluginId: pluginId || "",
131
+ methodId: methodId || "",
132
+ kind,
133
+ label: label || `${kind}-${nodeId.slice(-4)}`,
134
+ drainPolicy: "drain-until-yield",
135
+ lang: lang || null,
136
+ source: source || this._defaultSource(kind, lang),
137
+ ports: ports || {
138
+ inputs: defaultPorts.inputs.map(p => ({ ...p })),
139
+ outputs: defaultPorts.outputs.map(p => ({ ...p })),
140
+ },
141
+ };
142
+ this.nodes.set(nodeId, node);
143
+ this.editorMeta.nodes[nodeId] = { x: x || 200, y: y || 200 };
144
+ this._emit("node-add", { node });
145
+ return node;
146
+ }
147
+
148
+ updateNode(nodeId, changes) {
149
+ const node = this.nodes.get(nodeId);
150
+ if (!node) return;
151
+ Object.assign(node, changes);
152
+ this._emit("node-update", { node });
153
+ }
154
+
155
+ removeNode(nodeId) {
156
+ const node = this.nodes.get(nodeId);
157
+ if (!node) return;
158
+ // Remove connected edges
159
+ for (const [edgeId, edge] of this.edges) {
160
+ if (edge.fromNodeId === nodeId || edge.toNodeId === nodeId) {
161
+ this.edges.delete(edgeId);
162
+ this._emit("edge-remove", { edgeId });
163
+ }
164
+ }
165
+ // Remove trigger bindings
166
+ this.triggerBindings = this.triggerBindings.filter(b => b.targetNodeId !== nodeId);
167
+ this.nodes.delete(nodeId);
168
+ delete this.editorMeta.nodes[nodeId];
169
+ this._emit("node-remove", { nodeId });
170
+ }
171
+
172
+ moveNode(nodeId, x, y) {
173
+ if (this.editorMeta.nodes[nodeId]) {
174
+ this.editorMeta.nodes[nodeId].x = x;
175
+ this.editorMeta.nodes[nodeId].y = y;
176
+ this._emit("node-move", { nodeId, x, y });
177
+ }
178
+ }
179
+
180
+ addPort(nodeId, direction, portId, label) {
181
+ const node = this.nodes.get(nodeId);
182
+ if (!node) return;
183
+ const list = direction === "input" ? node.ports.inputs : node.ports.outputs;
184
+ if (!list.find(p => p.id === portId)) {
185
+ list.push({ id: portId, label: label || portId });
186
+ this._emit("node-update", { node });
187
+ }
188
+ }
189
+
190
+ removePort(nodeId, direction, portId) {
191
+ const node = this.nodes.get(nodeId);
192
+ if (!node) return;
193
+ const list = direction === "input" ? node.ports.inputs : node.ports.outputs;
194
+ const idx = list.findIndex(p => p.id === portId);
195
+ if (idx >= 0) {
196
+ list.splice(idx, 1);
197
+ // Remove connected edges
198
+ for (const [edgeId, edge] of this.edges) {
199
+ if ((direction === "input" && edge.toNodeId === nodeId && edge.toPortId === portId) ||
200
+ (direction === "output" && edge.fromNodeId === nodeId && edge.fromPortId === portId)) {
201
+ this.edges.delete(edgeId);
202
+ this._emit("edge-remove", { edgeId });
203
+ }
204
+ }
205
+ this._emit("node-update", { node });
206
+ }
207
+ }
208
+
209
+ // ── Edges ──
210
+
211
+ addEdge({ fromNodeId, fromPortId, toNodeId, toPortId, backpressurePolicy, queueDepth }) {
212
+ // Prevent duplicate edges
213
+ for (const edge of this.edges.values()) {
214
+ if (edge.fromNodeId === fromNodeId && edge.fromPortId === fromPortId &&
215
+ edge.toNodeId === toNodeId && edge.toPortId === toPortId) return edge;
216
+ }
217
+ const edgeId = genId("edge");
218
+ const edge = {
219
+ edgeId, fromNodeId, fromPortId, toNodeId, toPortId,
220
+ backpressurePolicy: backpressurePolicy || "queue",
221
+ queueDepth: queueDepth || 32,
222
+ };
223
+ this.edges.set(edgeId, edge);
224
+ this._emit("edge-add", { edge });
225
+ return edge;
226
+ }
227
+
228
+ removeEdge(edgeId) {
229
+ if (this.edges.delete(edgeId)) {
230
+ this._emit("edge-remove", { edgeId });
231
+ }
232
+ }
233
+
234
+ // ── Serialization ──
235
+
236
+ toJSON() {
237
+ return {
238
+ programId: this.programId,
239
+ name: this.name,
240
+ version: this.version,
241
+ description: this.description,
242
+ nodes: [...this.nodes.values()].map(n => ({
243
+ nodeId: n.nodeId,
244
+ pluginId: n.pluginId,
245
+ methodId: n.methodId,
246
+ kind: n.kind,
247
+ drainPolicy: n.drainPolicy,
248
+ label: n.label,
249
+ lang: n.lang,
250
+ source: n.source,
251
+ ports: n.ports,
252
+ })),
253
+ edges: [...this.edges.values()].map(e => ({
254
+ edgeId: e.edgeId,
255
+ fromNodeId: e.fromNodeId,
256
+ fromPortId: e.fromPortId,
257
+ toNodeId: e.toNodeId,
258
+ toPortId: e.toPortId,
259
+ backpressurePolicy: e.backpressurePolicy,
260
+ queueDepth: e.queueDepth,
261
+ })),
262
+ triggers: [...this.triggers.values()],
263
+ triggerBindings: this.triggerBindings,
264
+ externalInterfaces: this.externalInterfaces,
265
+ artifactDependencies: this.artifactDependencies,
266
+ requiredPlugins: this.requiredPlugins,
267
+ editor: this.editorMeta,
268
+ };
269
+ }
270
+
271
+ fromJSON(json) {
272
+ this.clear();
273
+ this.programId = json.programId || "";
274
+ this.name = json.name || "Untitled";
275
+ this.version = json.version || "0.1.0";
276
+ this.description = json.description || "";
277
+ if (json.editor) {
278
+ this.editorMeta = JSON.parse(JSON.stringify(json.editor));
279
+ }
280
+ for (const n of (json.nodes || [])) {
281
+ const node = {
282
+ nodeId: n.nodeId,
283
+ pluginId: n.pluginId || "",
284
+ methodId: n.methodId || "",
285
+ kind: n.kind || "transform",
286
+ label: n.label || n.nodeId,
287
+ drainPolicy: n.drainPolicy || "drain-until-yield",
288
+ lang: n.lang || null,
289
+ source: n.source || "",
290
+ ports: n.ports || (DEFAULT_PORTS[n.kind] || DEFAULT_PORTS.transform),
291
+ };
292
+ this.nodes.set(node.nodeId, node);
293
+ if (!this.editorMeta.nodes[node.nodeId]) {
294
+ this.editorMeta.nodes[node.nodeId] = { x: 200, y: 200 };
295
+ }
296
+ }
297
+ for (const e of (json.edges || [])) {
298
+ this.edges.set(e.edgeId, { ...e });
299
+ }
300
+ for (const t of (json.triggers || [])) {
301
+ this.triggers.set(t.triggerId, { ...t });
302
+ }
303
+ this.triggerBindings = (json.triggerBindings || []).map(b => ({ ...b }));
304
+ this.externalInterfaces = (json.externalInterfaces || []).map(i => ({ ...i }));
305
+ this.artifactDependencies = (json.artifactDependencies || []).map(d => ({ ...d }));
306
+ this.requiredPlugins = [...(json.requiredPlugins || [])];
307
+ this._emit("load");
308
+ }
309
+
310
+ clear() {
311
+ this.nodes.clear();
312
+ this.edges.clear();
313
+ this.triggers.clear();
314
+ this.triggerBindings = [];
315
+ this.externalInterfaces = [];
316
+ this.artifactDependencies = [];
317
+ this.requiredPlugins = [];
318
+ this.editorMeta = { viewport: { x: 0, y: 0, zoom: 1 }, nodes: {} };
319
+ this._emit("clear");
320
+ }
321
+
322
+ // ── Integrity ──
323
+
324
+ computeCRC() {
325
+ const json = this.toJSON();
326
+ delete json.editor; // editor layout doesn't affect integrity
327
+ return crc32Hex(canonicalBytes(json));
328
+ }
329
+
330
+ async computeSHA256() {
331
+ const json = this.toJSON();
332
+ delete json.editor;
333
+ return sha256(canonicalBytes(json));
334
+ }
335
+
336
+ // ── Default source templates ──
337
+
338
+ _defaultSource(kind, lang) {
339
+ if (!lang) return "";
340
+ const templates = {
341
+ cpp: `#include <cstdint>
342
+ #include <cstring>
343
+
344
+ // sdn-flow plugin method
345
+ // Input frames arrive as FlatBuffer byte spans.
346
+ // Return 0 on success.
347
+
348
+ extern "C" int process(const uint8_t* input, uint32_t input_len,
349
+ uint8_t* output, uint32_t* output_len) {
350
+ // TODO: implement
351
+ *output_len = 0;
352
+ return 0;
353
+ }
354
+ `,
355
+ python: `"""sdn-flow plugin method (Pyodide/pybind11)"""
356
+
357
+ def process(input_bytes: bytes) -> bytes:
358
+ """Process a FlatBuffer frame and return the output frame."""
359
+ # TODO: implement
360
+ return b""
361
+ `,
362
+ rust: `//! sdn-flow plugin method
363
+
364
+ #[no_mangle]
365
+ pub extern "C" fn process(
366
+ input: *const u8, input_len: u32,
367
+ output: *mut u8, output_len: *mut u32,
368
+ ) -> i32 {
369
+ // TODO: implement
370
+ unsafe { *output_len = 0; }
371
+ 0
372
+ }
373
+ `,
374
+ typescript: `// sdn-flow plugin method
375
+
376
+ export function process(input: Uint8Array): Uint8Array {
377
+ // TODO: implement
378
+ return new Uint8Array(0);
379
+ }
380
+ `,
381
+ };
382
+ return templates[lang] || "";
383
+ }
384
+
385
+ // ── Events ──
386
+
387
+ _emit(type, detail = {}) {
388
+ this.dispatchEvent(new CustomEvent(type, { detail }));
389
+ this.dispatchEvent(new CustomEvent("change", { detail: { type, ...detail } }));
390
+ }
391
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * emception.worker.js — Web Worker that loads emception (in-browser C++ → WASM
3
+ * compiler) and exposes it via a simple message-passing API.
4
+ *
5
+ * Protocol:
6
+ * postMessage({ id, method, args })
7
+ * → onmessage({ id, result?, error?, log? })
8
+ *
9
+ * Methods:
10
+ * init({ baseUrl }) — download and initialize emception
11
+ * writeFile(path, data) — write to VFS
12
+ * readFile(path, opts) — read from VFS
13
+ * run(cmd) — run an em++ / emcc command
14
+ * compile({ source, lang, flags, outputName }) — high-level compile
15
+ */
16
+
17
+ let emception = null;
18
+
19
+ // Dynamic import of emception — baseUrl determines where .pack files live
20
+ async function loadEmception(baseUrl) {
21
+ // Try the new ESM API first (src/emception.mjs)
22
+ try {
23
+ const mod = await import(new URL("src/emception.mjs", baseUrl).href);
24
+ const Emception = mod.default || mod.Emception;
25
+ return new Emception({ baseUrl });
26
+ } catch (e) {
27
+ // Fallback: try the demo-style class
28
+ try {
29
+ const mod = await import(new URL("demo/emception.js", baseUrl).href);
30
+ const Emception = mod.default || mod.Emception;
31
+ const instance = new Emception();
32
+ return instance;
33
+ } catch (e2) {
34
+ throw new Error(`Failed to load emception from ${baseUrl}: ${e.message}; fallback: ${e2.message}`);
35
+ }
36
+ }
37
+ }
38
+
39
+ function reply(id, result) { postMessage({ id, result }); }
40
+ function replyError(id, error) { postMessage({ id, error: String(error) }); }
41
+ function log(text, level = "info") { postMessage({ log: text, level }); }
42
+
43
+ self.onmessage = async ({ data }) => {
44
+ const { id, method, args } = data;
45
+ try {
46
+ switch (method) {
47
+ case "init": {
48
+ const baseUrl = args?.baseUrl || "../emception/";
49
+ log(`Loading emception from ${baseUrl}...`);
50
+ emception = await loadEmception(baseUrl);
51
+
52
+ // Wire up output callbacks
53
+ if (emception.onstdout !== undefined) {
54
+ emception.onstdout = (str) => log(str, "info");
55
+ }
56
+ if (emception.onstderr !== undefined) {
57
+ emception.onstderr = (str) => log(str, "warn");
58
+ }
59
+ if (emception.onprogress !== undefined) {
60
+ emception.onprogress = (stage, detail) => log(`[${stage}] ${detail}`, "info");
61
+ }
62
+
63
+ log("Initializing emception tools (this may take a moment)...");
64
+ await emception.init();
65
+ log("Emception ready.", "success");
66
+ reply(id, { ready: true });
67
+ break;
68
+ }
69
+
70
+ case "writeFile": {
71
+ if (!emception) throw new Error("Not initialized");
72
+ const [path, data] = args;
73
+ if (emception.writeFile) {
74
+ emception.writeFile(path, data);
75
+ } else if (emception.fileSystem) {
76
+ emception.fileSystem.writeFile(path, data);
77
+ }
78
+ reply(id, { ok: true });
79
+ break;
80
+ }
81
+
82
+ case "readFile": {
83
+ if (!emception) throw new Error("Not initialized");
84
+ const [path, opts] = args;
85
+ let content;
86
+ if (emception.readFile) {
87
+ content = emception.readFile(path, opts);
88
+ } else if (emception.fileSystem) {
89
+ content = emception.fileSystem.readFile(path, opts);
90
+ }
91
+ reply(id, { content });
92
+ break;
93
+ }
94
+
95
+ case "run": {
96
+ if (!emception) throw new Error("Not initialized");
97
+ log(`$ ${args.cmd}`, "cmd");
98
+ const result = emception.run(args.cmd);
99
+ reply(id, result);
100
+ break;
101
+ }
102
+
103
+ case "compile": {
104
+ if (!emception) throw new Error("Not initialized");
105
+ const { source, lang, flags, outputName } = args;
106
+ // Use high-level compile if available
107
+ if (emception.compile) {
108
+ const result = emception.compile(source, { lang, flags, outputName });
109
+ reply(id, result);
110
+ } else {
111
+ // Manual: write source, run em++, read output
112
+ const ext = lang === "c" ? ".c" : ".cpp";
113
+ const compiler = lang === "c" ? "emcc" : "em++";
114
+ const outName = outputName || "output";
115
+ const flagStr = (flags || ["-O2"]).join(" ");
116
+
117
+ if (emception.writeFile) {
118
+ emception.writeFile(`/working/input${ext}`, source);
119
+ } else if (emception.fileSystem) {
120
+ emception.fileSystem.writeFile(`/working/input${ext}`, source);
121
+ }
122
+
123
+ const cmd = `${compiler} ${flagStr} -sWASM=1 -sEXPORT_ES6=1 -sSINGLE_FILE=1 input${ext} -o ${outName}.mjs`;
124
+ log(`$ ${cmd}`, "cmd");
125
+ const result = emception.run(cmd);
126
+
127
+ let wasmModule = null;
128
+ let loaderModule = null;
129
+ if (result.returncode === 0) {
130
+ try {
131
+ const readFn = emception.readFile?.bind(emception) || emception.fileSystem?.readFile?.bind(emception.fileSystem);
132
+ loaderModule = readFn(`/working/${outName}.mjs`, { encoding: "utf8" });
133
+ } catch (e) {}
134
+ }
135
+ reply(id, { ...result, loaderModule });
136
+ }
137
+ break;
138
+ }
139
+
140
+ default:
141
+ replyError(id, `Unknown method: ${method}`);
142
+ }
143
+ } catch (err) {
144
+ replyError(id, err.message || String(err));
145
+ }
146
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * pyodide.worker.js — Web Worker that loads Pyodide (Python → WASM runtime)
3
+ * and exposes Python execution and pybind11 compilation capabilities.
4
+ *
5
+ * Protocol:
6
+ * postMessage({ id, method, args })
7
+ * → onmessage({ id, result?, error?, log? })
8
+ *
9
+ * Methods:
10
+ * init() — download and initialize Pyodide
11
+ * run({ code }) — execute Python code, return stdout + result
12
+ * installPackages({ packages })— install Python packages via micropip
13
+ * compilePybind({ source, moduleName }) — compile pybind11 C++ (requires emception)
14
+ */
15
+
16
+ let pyodide = null;
17
+
18
+ const PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.27.0/full/";
19
+
20
+ function reply(id, result) { postMessage({ id, result }); }
21
+ function replyError(id, error) { postMessage({ id, error: String(error) }); }
22
+ function log(text, level = "info") { postMessage({ log: text, level }); }
23
+
24
+ self.onmessage = async ({ data }) => {
25
+ const { id, method, args } = data;
26
+ try {
27
+ switch (method) {
28
+ case "init": {
29
+ log("Loading Pyodide...");
30
+ importScripts(`${PYODIDE_CDN}pyodide.js`);
31
+ pyodide = await loadPyodide({
32
+ indexURL: PYODIDE_CDN,
33
+ stdout: (text) => log(text, "info"),
34
+ stderr: (text) => log(text, "warn"),
35
+ });
36
+ // Pre-install micropip for package management
37
+ await pyodide.loadPackage("micropip");
38
+ log("Pyodide ready (Python " + pyodide.version + ")", "success");
39
+ reply(id, { ready: true, version: pyodide.version });
40
+ break;
41
+ }
42
+
43
+ case "run": {
44
+ if (!pyodide) throw new Error("Not initialized");
45
+ const { code } = args;
46
+
47
+ // Capture stdout
48
+ let stdout = "";
49
+ pyodide.setStdout({ batched: (text) => { stdout += text + "\n"; log(text, "info"); } });
50
+ pyodide.setStderr({ batched: (text) => { log(text, "warn"); } });
51
+
52
+ const result = await pyodide.runPythonAsync(code);
53
+
54
+ // Convert Python result to JS
55
+ let jsResult = null;
56
+ if (result !== undefined && result !== null) {
57
+ try { jsResult = result.toJs ? result.toJs() : result; } catch (e) { jsResult = String(result); }
58
+ }
59
+
60
+ reply(id, { stdout: stdout.trim(), result: jsResult });
61
+ break;
62
+ }
63
+
64
+ case "installPackages": {
65
+ if (!pyodide) throw new Error("Not initialized");
66
+ const { packages } = args;
67
+ log(`Installing packages: ${packages.join(", ")}...`);
68
+ const micropip = pyodide.pyimport("micropip");
69
+ await micropip.install(packages);
70
+ log(`Packages installed: ${packages.join(", ")}`, "success");
71
+ reply(id, { ok: true });
72
+ break;
73
+ }
74
+
75
+ case "compilePybind": {
76
+ if (!pyodide) throw new Error("Not initialized");
77
+ const { source, moduleName } = args;
78
+ // pybind11 compilation requires:
79
+ // 1. Python headers (available in Pyodide's sysconfig)
80
+ // 2. pybind11 headers (install via micropip)
81
+ // 3. Compilation via emception (delegated back to main thread)
82
+ //
83
+ // For now, we return the compile plan — the main thread orchestrates
84
+ // the actual emception compilation with the right include paths.
85
+
86
+ // Get Python include path from Pyodide
87
+ const includePath = await pyodide.runPythonAsync(`
88
+ import sysconfig
89
+ sysconfig.get_path('include')
90
+ `);
91
+
92
+ // Get pybind11 include path
93
+ try {
94
+ const micropip = pyodide.pyimport("micropip");
95
+ await micropip.install(["pybind11"]);
96
+ } catch (e) {
97
+ log("pybind11 not available via micropip, using fallback headers", "warn");
98
+ }
99
+
100
+ let pybindInclude = null;
101
+ try {
102
+ pybindInclude = await pyodide.runPythonAsync(`
103
+ import pybind11
104
+ pybind11.get_include()
105
+ `);
106
+ } catch (e) {}
107
+
108
+ const flags = [
109
+ "-shared", "-fPIC", "-O2", "-std=c++17",
110
+ `-I${includePath}`,
111
+ ...(pybindInclude ? [`-I${pybindInclude}`] : []),
112
+ `-DMODULE_NAME=${moduleName || "sdn_module"}`,
113
+ ];
114
+
115
+ reply(id, {
116
+ compilePlan: {
117
+ source,
118
+ moduleName: moduleName || "sdn_module",
119
+ lang: "c++",
120
+ flags,
121
+ pythonInclude: includePath,
122
+ pybindInclude,
123
+ },
124
+ });
125
+ break;
126
+ }
127
+
128
+ default:
129
+ replyError(id, `Unknown method: ${method}`);
130
+ }
131
+ } catch (err) {
132
+ replyError(id, err.message || String(err));
133
+ }
134
+ };