space-data-module-sdk 0.2.0 → 0.2.5

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 (104) hide show
  1. package/README.md +74 -2
  2. package/package.json +10 -3
  3. package/schemas/PluginInvokeRequest.fbs +18 -0
  4. package/schemas/PluginInvokeResponse.fbs +30 -0
  5. package/schemas/PluginManifest.fbs +7 -0
  6. package/schemas/TypedArenaBuffer.fbs +23 -2
  7. package/src/bundle/codec.js +24 -0
  8. package/src/compiler/compileModule.js +274 -106
  9. package/src/compiler/emceptionNode.js +217 -0
  10. package/src/compiler/flatcSupport.js +66 -0
  11. package/src/compiler/invokeGlue.js +884 -0
  12. package/src/compliance/pluginCompliance.js +241 -1
  13. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts +51 -0
  14. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts.map +1 -0
  15. package/src/generated/orbpro/invoke/plugin-invoke-request.js +131 -0
  16. package/src/generated/orbpro/invoke/plugin-invoke-request.js.map +1 -0
  17. package/src/generated/orbpro/invoke/plugin-invoke-request.ts +173 -0
  18. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts +76 -0
  19. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts.map +1 -0
  20. package/src/generated/orbpro/invoke/plugin-invoke-response.js +184 -0
  21. package/src/generated/orbpro/invoke/plugin-invoke-response.js.map +1 -0
  22. package/src/generated/orbpro/invoke/plugin-invoke-response.ts +243 -0
  23. package/src/generated/orbpro/invoke.d.ts +3 -0
  24. package/src/generated/orbpro/invoke.d.ts.map +1 -0
  25. package/src/generated/orbpro/invoke.js +5 -0
  26. package/src/generated/orbpro/invoke.js.map +1 -0
  27. package/src/generated/orbpro/invoke.ts +6 -0
  28. package/src/generated/orbpro/manifest/accepted-type-set.d.ts +4 -4
  29. package/src/generated/orbpro/manifest/accepted-type-set.d.ts.map +1 -1
  30. package/src/generated/orbpro/manifest/accepted-type-set.js +18 -11
  31. package/src/generated/orbpro/manifest/accepted-type-set.js.map +1 -1
  32. package/src/generated/orbpro/manifest/build-artifact.d.ts +1 -1
  33. package/src/generated/orbpro/manifest/build-artifact.d.ts.map +1 -1
  34. package/src/generated/orbpro/manifest/build-artifact.js +28 -15
  35. package/src/generated/orbpro/manifest/build-artifact.js.map +1 -1
  36. package/src/generated/orbpro/manifest/capability-kind.d.ts +1 -1
  37. package/src/generated/orbpro/manifest/capability-kind.d.ts.map +1 -1
  38. package/src/generated/orbpro/manifest/capability-kind.js +1 -1
  39. package/src/generated/orbpro/manifest/capability-kind.js.map +1 -1
  40. package/src/generated/orbpro/manifest/drain-policy.d.ts.map +1 -1
  41. package/src/generated/orbpro/manifest/drain-policy.js.map +1 -1
  42. package/src/generated/orbpro/manifest/host-capability.d.ts +2 -2
  43. package/src/generated/orbpro/manifest/host-capability.d.ts.map +1 -1
  44. package/src/generated/orbpro/manifest/host-capability.js +19 -11
  45. package/src/generated/orbpro/manifest/host-capability.js.map +1 -1
  46. package/src/generated/orbpro/manifest/invoke-surface.d.ts +8 -0
  47. package/src/generated/orbpro/manifest/invoke-surface.d.ts.map +1 -0
  48. package/src/generated/orbpro/manifest/invoke-surface.js +11 -0
  49. package/src/generated/orbpro/manifest/invoke-surface.js.map +1 -0
  50. package/src/generated/orbpro/manifest/invoke-surface.ts +11 -0
  51. package/src/generated/orbpro/manifest/method-manifest.d.ts +6 -6
  52. package/src/generated/orbpro/manifest/method-manifest.d.ts.map +1 -1
  53. package/src/generated/orbpro/manifest/method-manifest.js +33 -16
  54. package/src/generated/orbpro/manifest/method-manifest.js.map +1 -1
  55. package/src/generated/orbpro/manifest/plugin-family.d.ts.map +1 -1
  56. package/src/generated/orbpro/manifest/plugin-family.js.map +1 -1
  57. package/src/generated/orbpro/manifest/plugin-manifest.d.ts +10 -2
  58. package/src/generated/orbpro/manifest/plugin-manifest.d.ts.map +1 -1
  59. package/src/generated/orbpro/manifest/plugin-manifest.js +48 -9
  60. package/src/generated/orbpro/manifest/plugin-manifest.js.map +1 -1
  61. package/src/generated/orbpro/manifest/plugin-manifest.ts +322 -491
  62. package/src/generated/orbpro/manifest/port-manifest.d.ts +4 -4
  63. package/src/generated/orbpro/manifest/port-manifest.d.ts.map +1 -1
  64. package/src/generated/orbpro/manifest/port-manifest.js +26 -13
  65. package/src/generated/orbpro/manifest/port-manifest.js.map +1 -1
  66. package/src/generated/orbpro/manifest/protocol-spec.d.ts +1 -1
  67. package/src/generated/orbpro/manifest/protocol-spec.d.ts.map +1 -1
  68. package/src/generated/orbpro/manifest/protocol-spec.js +28 -15
  69. package/src/generated/orbpro/manifest/protocol-spec.js.map +1 -1
  70. package/src/generated/orbpro/manifest/timer-spec.d.ts +1 -1
  71. package/src/generated/orbpro/manifest/timer-spec.d.ts.map +1 -1
  72. package/src/generated/orbpro/manifest/timer-spec.js +27 -16
  73. package/src/generated/orbpro/manifest/timer-spec.js.map +1 -1
  74. package/src/generated/orbpro/manifest.d.ts +13 -0
  75. package/src/generated/orbpro/manifest.d.ts.map +1 -0
  76. package/src/generated/orbpro/manifest.js +1 -0
  77. package/src/generated/orbpro/manifest.js.map +1 -0
  78. package/src/generated/orbpro/manifest.ts +16 -0
  79. package/src/generated/orbpro/stream/buffer-mutability.d.ts.map +1 -1
  80. package/src/generated/orbpro/stream/buffer-mutability.js.map +1 -1
  81. package/src/generated/orbpro/stream/buffer-ownership.d.ts.map +1 -1
  82. package/src/generated/orbpro/stream/buffer-ownership.js.map +1 -1
  83. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts +22 -5
  84. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts.map +1 -1
  85. package/src/generated/orbpro/stream/flat-buffer-type-ref.js +107 -17
  86. package/src/generated/orbpro/stream/flat-buffer-type-ref.js.map +1 -1
  87. package/src/generated/orbpro/stream/flat-buffer-type-ref.ts +126 -2
  88. package/src/generated/orbpro/stream/payload-wire-format.d.ts +8 -0
  89. package/src/generated/orbpro/stream/payload-wire-format.d.ts.map +1 -0
  90. package/src/generated/orbpro/stream/payload-wire-format.js +11 -0
  91. package/src/generated/orbpro/stream/payload-wire-format.js.map +1 -0
  92. package/src/generated/orbpro/stream/payload-wire-format.ts +11 -0
  93. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts +4 -4
  94. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts.map +1 -1
  95. package/src/generated/orbpro/stream/typed-arena-buffer.js +42 -24
  96. package/src/generated/orbpro/stream/typed-arena-buffer.js.map +1 -1
  97. package/src/index.d.ts +83 -5
  98. package/src/index.js +3 -0
  99. package/src/invoke/codec.js +278 -0
  100. package/src/invoke/index.js +9 -0
  101. package/src/manifest/codec.js +10 -2
  102. package/src/manifest/index.js +5 -2
  103. package/src/manifest/normalize.js +58 -0
  104. package/src/runtime/constants.js +12 -0
@@ -1,6 +1,12 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import {
4
+ mkdir,
5
+ mkdtemp,
6
+ readFile,
7
+ rm,
8
+ writeFile,
9
+ } from "node:fs/promises";
4
10
  import { execFile as execFileCallback } from "node:child_process";
5
11
  import { promisify } from "node:util";
6
12
 
@@ -11,7 +17,18 @@ import {
11
17
  } from "../auth/index.js";
12
18
  import { validateArtifactWithStandards } from "../compliance/index.js";
13
19
  import { generateEmbeddedManifestSource } from "../embeddedManifest.js";
20
+ import {
21
+ generateInvokeSupportHeader,
22
+ generateInvokeSupportSource,
23
+ resolveInvokeSurfaces,
24
+ } from "./invokeGlue.js";
25
+ import {
26
+ getFlatbuffersCppRuntimeHeaders,
27
+ getInvokeCppSchemaHeaders,
28
+ } from "./flatcSupport.js";
29
+ import { runWithEmceptionLock } from "./emceptionNode.js";
14
30
  import { encodePluginManifest, toEmbeddedPluginManifest } from "../manifest/index.js";
31
+ import { DefaultInvokeExports, InvokeSurface } from "../runtime/constants.js";
15
32
  import {
16
33
  encryptJsonForRecipient,
17
34
  generateX25519Keypair,
@@ -49,7 +66,7 @@ function ensureExportableMethodIds(manifest) {
49
66
  }
50
67
  }
51
68
 
52
- function buildCompilerArgs(compiler, exportedSymbols, options = {}) {
69
+ function buildCompilerArgs(exportedSymbols, options = {}) {
53
70
  const linkerExports = exportedSymbols.map(
54
71
  (symbol) => "-Wl,--export=" + symbol,
55
72
  );
@@ -57,113 +74,250 @@ function buildCompilerArgs(compiler, exportedSymbols, options = {}) {
57
74
  if (options.allowUndefinedImports === true) {
58
75
  extraArgs.push("-s", "ERROR_ON_UNDEFINED_SYMBOLS=0", "-Wl,--allow-undefined");
59
76
  }
60
- return [
61
- "-O2",
62
- "--no-entry",
63
- "-s",
64
- "STANDALONE_WASM=1",
65
- ...extraArgs,
66
- ...linkerExports,
67
- ];
77
+ const args = ["-O2", "-s", "STANDALONE_WASM=1", ...extraArgs, ...linkerExports];
78
+ if (options.noEntry === true) {
79
+ args.splice(1, 0, "--no-entry");
80
+ }
81
+ return args;
68
82
  }
69
83
 
70
- // ---------------------------------------------------------------------------
71
- // Emception in-process WASM-based Emscripten (preferred)
72
- // ---------------------------------------------------------------------------
84
+ async function getInvokeCppSupportFiles() {
85
+ const [runtimeHeaders, schemaHeaders] = await Promise.all([
86
+ getFlatbuffersCppRuntimeHeaders(),
87
+ getInvokeCppSchemaHeaders(),
88
+ ]);
89
+ return { runtimeHeaders, schemaHeaders };
90
+ }
73
91
 
74
- let emceptionInstance = null;
75
- let emceptionLoadAttempted = false;
92
+ async function writeFilesToDirectory(rootDir, files) {
93
+ for (const [relativePath, content] of Object.entries(files)) {
94
+ const filePath = path.join(rootDir, relativePath);
95
+ await mkdir(path.dirname(filePath), { recursive: true });
96
+ await writeFile(filePath, content, "utf8");
97
+ }
98
+ }
76
99
 
77
- async function loadEmception() {
78
- if (emceptionLoadAttempted) return emceptionInstance;
79
- emceptionLoadAttempted = true;
80
- try {
81
- const { default: Emception } = await import(
82
- "sdn-emception"
83
- );
84
- emceptionInstance = new Emception();
85
- await emceptionInstance.init();
86
- return emceptionInstance;
87
- } catch {
88
- // emception not available or init failed — fall back to system emcc
100
+ async function writeFilesToEmception(emception, rootDir, files) {
101
+ for (const [relativePath, content] of Object.entries(files)) {
102
+ const filePath = path.posix.join(rootDir, relativePath);
103
+ emception.FS.mkdirTree(path.posix.dirname(filePath));
104
+ emception.writeFile(filePath, content);
89
105
  }
90
- return null;
91
106
  }
92
107
 
93
- async function compileWithEmception(
94
- emception,
95
- compiler,
96
- sourceCode,
97
- manifestSource,
98
- exportedSymbols,
99
- compileOptions,
100
- ) {
101
- const ext = compiler.extension;
102
- const inputPath = `/working/module.${ext}`;
103
- const manifestPath = "/working/plugin-manifest-exports.c";
104
- const outputPath = "/working/module.wasm";
105
-
106
- emception.writeFile(inputPath, sourceCode);
107
- emception.writeFile(manifestPath, manifestSource);
108
-
109
- const args = buildCompilerArgs(compiler, exportedSymbols, compileOptions);
110
- const cmd = [
111
- compiler.command,
112
- inputPath,
113
- manifestPath,
114
- ...args,
115
- "-o",
108
+ function removeEmceptionDirectory(emception, directoryPath) {
109
+ if (!emception.FS.analyzePath(directoryPath).exists) {
110
+ return;
111
+ }
112
+ const entries = emception.FS.readdir(directoryPath).filter(
113
+ (entry) => entry !== "." && entry !== "..",
114
+ );
115
+ for (const entry of entries) {
116
+ const entryPath = path.posix.join(directoryPath, entry);
117
+ const stat = emception.FS.stat(entryPath);
118
+ if (emception.FS.isDir(stat.mode)) {
119
+ removeEmceptionDirectory(emception, entryPath);
120
+ emception.FS.rmdir(entryPath);
121
+ } else {
122
+ emception.FS.unlink(entryPath);
123
+ }
124
+ }
125
+ emception.FS.rmdir(directoryPath);
126
+ }
127
+
128
+ async function compileWithEmception(options = {}) {
129
+ const {
130
+ sourceCompilerCommand,
131
+ sourceExtension,
132
+ sourceCode,
133
+ manifestSource,
134
+ invokeHeaderSource,
135
+ invokeSource,
136
+ exportedSymbols,
116
137
  outputPath,
117
- ].join(" ");
138
+ compileOptions,
139
+ } = options;
140
+ const tempDir = await mkdtemp(
141
+ path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
142
+ );
143
+ const resolvedOutputPath = path.resolve(
144
+ outputPath ?? path.join(tempDir, "module.wasm"),
145
+ );
118
146
 
119
- const result = emception.run(cmd);
120
- if (result.returncode !== 0) {
121
- throw new Error(
122
- `Compilation failed with ${compiler.command} (emception): ${result.stderr || result.stdout}`,
123
- );
147
+ try {
148
+ return await runWithEmceptionLock(async (emception) => {
149
+ const workDir = "/working/space-data-module-sdk-compile";
150
+ const runtimeIncludeDir = path.posix.join(workDir, "flatbuffers-runtime");
151
+ const sourcePath = path.posix.join(workDir, `module.${sourceExtension}`);
152
+ const manifestSourcePath = path.posix.join(workDir, "plugin-manifest-exports.cpp");
153
+ const invokeHeaderPath = path.posix.join(workDir, "space_data_module_invoke.h");
154
+ const invokeSourcePath = path.posix.join(workDir, "plugin-invoke-bridge.cpp");
155
+ const sourceObjectPath = path.posix.join(workDir, "module.o");
156
+ const manifestObjectPath = path.posix.join(workDir, "plugin-manifest-exports.o");
157
+ const invokeObjectPath = path.posix.join(workDir, "plugin-invoke-bridge.o");
158
+ const wasmOutputPath = path.posix.join(workDir, "module.wasm");
159
+
160
+ const { runtimeHeaders, schemaHeaders } = await getInvokeCppSupportFiles();
161
+ const args = buildCompilerArgs(exportedSymbols, compileOptions);
162
+
163
+ try {
164
+ emception.FS.mkdirTree(workDir);
165
+ await writeFilesToEmception(emception, runtimeIncludeDir, runtimeHeaders);
166
+ await writeFilesToEmception(emception, workDir, schemaHeaders);
167
+ emception.writeFile(sourcePath, sourceCode);
168
+ emception.writeFile(manifestSourcePath, manifestSource);
169
+ emception.writeFile(invokeHeaderPath, invokeHeaderSource);
170
+ emception.writeFile(invokeSourcePath, invokeSource);
171
+
172
+ const commands = [
173
+ [
174
+ sourceCompilerCommand,
175
+ "-c",
176
+ sourcePath,
177
+ `-I${workDir}`,
178
+ "-o",
179
+ sourceObjectPath,
180
+ ],
181
+ [
182
+ "em++",
183
+ "-c",
184
+ manifestSourcePath,
185
+ "-std=c++17",
186
+ `-I${workDir}`,
187
+ `-I${runtimeIncludeDir}`,
188
+ "-o",
189
+ manifestObjectPath,
190
+ ],
191
+ [
192
+ "em++",
193
+ "-c",
194
+ invokeSourcePath,
195
+ "-std=c++17",
196
+ `-I${workDir}`,
197
+ `-I${runtimeIncludeDir}`,
198
+ "-o",
199
+ invokeObjectPath,
200
+ ],
201
+ [
202
+ "em++",
203
+ sourceObjectPath,
204
+ manifestObjectPath,
205
+ invokeObjectPath,
206
+ ...args,
207
+ "-o",
208
+ wasmOutputPath,
209
+ ],
210
+ ];
211
+
212
+ for (const command of commands) {
213
+ const result = emception.run(command.join(" "));
214
+ if (result.returncode !== 0) {
215
+ throw new Error(
216
+ `Compilation failed with ${command[0]} (emception): ${result.stderr || result.stdout}`,
217
+ );
218
+ }
219
+ }
220
+
221
+ const wasmBytes = new Uint8Array(emception.readFile(wasmOutputPath));
222
+ await writeFile(resolvedOutputPath, wasmBytes);
223
+ return { wasmBytes, outputPath: resolvedOutputPath, tempDir };
224
+ } finally {
225
+ try {
226
+ removeEmceptionDirectory(emception, workDir);
227
+ } catch {
228
+ // Best-effort cleanup only; the shared emception instance remains usable.
229
+ }
230
+ }
231
+ });
232
+ } catch (error) {
233
+ await rm(tempDir, { recursive: true, force: true });
234
+ throw error;
124
235
  }
125
-
126
- const wasmBytes = emception.readFile(outputPath);
127
- return new Uint8Array(wasmBytes);
128
236
  }
129
237
 
130
238
  // ---------------------------------------------------------------------------
131
239
  // System Emscripten — fallback to emcc/em++ on PATH
132
240
  // ---------------------------------------------------------------------------
133
241
 
134
- async function compileWithSystemEmcc(
135
- compiler,
136
- sourceCode,
137
- manifestSource,
138
- exportedSymbols,
139
- outputPath,
140
- compileOptions,
141
- ) {
242
+ async function compileWithSystemToolchain(options = {}) {
243
+ const {
244
+ compilerCommand,
245
+ sourceCompilerCommand,
246
+ sourceExtension,
247
+ sourceCode,
248
+ manifestSource,
249
+ invokeHeaderSource,
250
+ invokeSource,
251
+ exportedSymbols,
252
+ outputPath,
253
+ compileOptions,
254
+ } = options;
142
255
  const tempDir = await mkdtemp(
143
256
  path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
144
257
  );
145
- const sourcePath = path.join(tempDir, `module.${compiler.extension}`);
146
- const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.c");
258
+ const sourcePath = path.join(tempDir, `module.${sourceExtension}`);
259
+ const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.cpp");
260
+ const invokeHeaderPath = path.join(tempDir, "space_data_module_invoke.h");
261
+ const invokeSourcePath = path.join(tempDir, "plugin-invoke-bridge.cpp");
262
+ const sourceObjectPath = path.join(tempDir, "module.o");
263
+ const manifestObjectPath = path.join(tempDir, "plugin-manifest-exports.o");
264
+ const invokeObjectPath = path.join(tempDir, "plugin-invoke-bridge.o");
147
265
  const resolvedOutputPath = path.resolve(
148
266
  outputPath ?? path.join(tempDir, "module.wasm"),
149
267
  );
268
+ const runtimeIncludeDir = path.join(tempDir, "flatbuffers-runtime");
269
+
270
+ const { runtimeHeaders, schemaHeaders } = await getInvokeCppSupportFiles();
150
271
 
151
272
  await writeFile(sourcePath, sourceCode, "utf8");
152
273
  await writeFile(manifestSourcePath, manifestSource, "utf8");
274
+ await writeFile(invokeHeaderPath, invokeHeaderSource, "utf8");
275
+ await writeFile(invokeSourcePath, invokeSource, "utf8");
276
+ await writeFilesToDirectory(tempDir, schemaHeaders);
277
+ await writeFilesToDirectory(runtimeIncludeDir, runtimeHeaders);
153
278
 
154
- const args = buildCompilerArgs(compiler, exportedSymbols, compileOptions);
279
+ const args = buildCompilerArgs(exportedSymbols, compileOptions);
155
280
 
156
281
  try {
157
- await execFile(compiler.command, [
282
+ await execFile(sourceCompilerCommand, [
283
+ "-c",
158
284
  sourcePath,
285
+ `-I${tempDir}`,
286
+ "-o",
287
+ sourceObjectPath,
288
+ ], { timeout: 120_000 });
289
+
290
+ await execFile(compilerCommand, [
291
+ "-c",
159
292
  manifestSourcePath,
293
+ "-std=c++17",
294
+ `-I${tempDir}`,
295
+ `-I${runtimeIncludeDir}`,
296
+ "-o",
297
+ manifestObjectPath,
298
+ ], { timeout: 120_000 });
299
+
300
+ await execFile(compilerCommand, [
301
+ "-c",
302
+ invokeSourcePath,
303
+ "-std=c++17",
304
+ `-I${tempDir}`,
305
+ `-I${runtimeIncludeDir}`,
306
+ "-o",
307
+ invokeObjectPath,
308
+ ], { timeout: 120_000 });
309
+
310
+ await execFile(compilerCommand, [
311
+ sourceObjectPath,
312
+ manifestObjectPath,
313
+ invokeObjectPath,
160
314
  ...args,
161
315
  "-o",
162
316
  resolvedOutputPath,
163
317
  ], { timeout: 120_000 });
164
318
  } catch (error) {
165
319
  error.message =
166
- `Compilation failed with ${compiler.command}: ` +
320
+ `Compilation failed with ${compilerCommand}: ` +
167
321
  (error.stderr || error.message);
168
322
  throw error;
169
323
  }
@@ -193,16 +347,27 @@ export async function compileModuleFromSource(options = {}) {
193
347
  }
194
348
 
195
349
  const compiler = selectCompiler(options.language);
350
+ const invokeSurfaces = resolveInvokeSurfaces(manifest);
351
+ const includeCommandMain = invokeSurfaces.includes(InvokeSurface.COMMAND);
196
352
  const { manifest: embeddedManifest, warnings } = toEmbeddedPluginManifest(
197
353
  manifest,
198
354
  );
199
355
  const manifestSource = generateEmbeddedManifestSource({
200
356
  manifest: embeddedManifest,
201
357
  });
358
+ const invokeHeaderSource = generateInvokeSupportHeader();
359
+ const invokeSource = generateInvokeSupportSource({
360
+ manifest,
361
+ includeCommandMain,
362
+ });
202
363
 
203
364
  const exportedSymbols = [
204
365
  "plugin_get_manifest_flatbuffer",
205
366
  "plugin_get_manifest_flatbuffer_size",
367
+ DefaultInvokeExports.invokeSymbol,
368
+ DefaultInvokeExports.allocSymbol,
369
+ DefaultInvokeExports.freeSymbol,
370
+ ...(includeCommandMain ? [DefaultInvokeExports.commandSymbol] : []),
206
371
  ...new Set(
207
372
  (Array.isArray(manifest.methods) ? manifest.methods : [])
208
373
  .map((method) => String(method?.methodId ?? "").trim())
@@ -210,51 +375,54 @@ export async function compileModuleFromSource(options = {}) {
210
375
  ),
211
376
  ];
212
377
 
213
- // Try emception first, fall back to system emcc/em++
214
- const emception = await loadEmception();
215
-
216
378
  let wasmBytes;
217
379
  let resolvedOutputPath = null;
218
380
  let tempDir = null;
219
- let compilerBackend;
220
-
221
- if (emception) {
222
- wasmBytes = await compileWithEmception(
223
- emception,
224
- compiler,
381
+ const compileOptions = {
382
+ ...options,
383
+ noEntry: includeCommandMain !== true,
384
+ };
385
+ let compilerBackend = "em++ (emception)";
386
+ let result;
387
+ try {
388
+ result = await compileWithEmception({
389
+ sourceCompilerCommand: compiler.command,
390
+ sourceExtension: compiler.extension,
225
391
  sourceCode,
226
392
  manifestSource,
393
+ invokeHeaderSource,
394
+ invokeSource,
227
395
  exportedSymbols,
228
- options,
229
- );
230
- compilerBackend = `${compiler.command} (emception)`;
231
- } else {
232
- const result = await compileWithSystemEmcc(
233
- compiler,
396
+ outputPath: options.outputPath,
397
+ compileOptions,
398
+ });
399
+ } catch (error) {
400
+ if (error?.code !== "EMCEPTION_LOAD_FAILED") {
401
+ throw error;
402
+ }
403
+ result = await compileWithSystemToolchain({
404
+ compilerCommand: "em++",
405
+ sourceCompilerCommand: compiler.command,
406
+ sourceExtension: compiler.extension,
234
407
  sourceCode,
235
408
  manifestSource,
409
+ invokeHeaderSource,
410
+ invokeSource,
236
411
  exportedSymbols,
237
- options.outputPath,
238
- options,
239
- );
240
- wasmBytes = result.wasmBytes;
241
- resolvedOutputPath = result.outputPath;
242
- tempDir = result.tempDir;
243
- compilerBackend = `${compiler.command} (system)`;
412
+ outputPath: options.outputPath,
413
+ compileOptions,
414
+ });
415
+ compilerBackend = "em++ (system)";
244
416
  }
417
+ wasmBytes = result.wasmBytes;
418
+ resolvedOutputPath = result.outputPath;
419
+ tempDir = result.tempDir;
245
420
 
246
421
  // Validate the compiled artifact
247
- const report = emception
248
- ? await validateArtifactWithStandards({
249
- manifest,
250
- exportNames: WebAssembly.Module.exports(
251
- new WebAssembly.Module(wasmBytes),
252
- ).map((e) => e.name),
253
- })
254
- : await validateArtifactWithStandards({
255
- manifest,
256
- wasmPath: resolvedOutputPath,
257
- });
422
+ const report = await validateArtifactWithStandards({
423
+ manifest,
424
+ wasmPath: resolvedOutputPath,
425
+ });
258
426
 
259
427
  return {
260
428
  compiler: compilerBackend,
@@ -0,0 +1,217 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { cp, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
+ import { readFileSync } from "node:fs";
5
+ import { createRequire } from "node:module";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const PATCH_VERSION = "space-data-module-sdk-emception-node-v1";
10
+ const PATCH_MARKER_FILENAME = ".space-data-module-sdk-emception-patch";
11
+ const EMCEPTION_PATCH_ROOT = path.join(
12
+ os.tmpdir(),
13
+ `space-data-module-sdk-emception-node-${process.pid}`,
14
+ );
15
+ const FILE_URL_FETCH_PATCH_FLAG =
16
+ "__spaceDataModuleSdkFileUrlFetchPatched";
17
+
18
+ let patchedEmceptionRootPromise = null;
19
+ let emceptionInstancePromise = null;
20
+ let emceptionExecutionQueue = Promise.resolve();
21
+
22
+ function installNodeRuntimeShims() {
23
+ if (typeof globalThis.require !== "function") {
24
+ globalThis.require = require;
25
+ }
26
+
27
+ if (!globalThis.XMLHttpRequest) {
28
+ class FileUrlXMLHttpRequest {
29
+ open(method, url, async = true) {
30
+ this.method = method;
31
+ this.url = url;
32
+ this.async = async;
33
+ this.status = 0;
34
+ this.response = null;
35
+ }
36
+
37
+ overrideMimeType() {}
38
+
39
+ send() {
40
+ if (this.async) {
41
+ throw new Error("Async file XMLHttpRequest is not supported.");
42
+ }
43
+ const target =
44
+ typeof this.url === "string" && this.url.startsWith("file:")
45
+ ? new URL(this.url)
46
+ : this.url;
47
+ const data = readFileSync(target);
48
+ this.status = 200;
49
+ this.response = data.buffer.slice(
50
+ data.byteOffset,
51
+ data.byteOffset + data.byteLength,
52
+ );
53
+ }
54
+ }
55
+
56
+ globalThis.XMLHttpRequest = FileUrlXMLHttpRequest;
57
+ }
58
+
59
+ if (!globalThis[FILE_URL_FETCH_PATCH_FLAG]) {
60
+ const originalFetch = globalThis.fetch;
61
+ globalThis.fetch = async (input, init) => {
62
+ const url =
63
+ typeof input === "string"
64
+ ? input
65
+ : input?.url ?? String(input);
66
+ if (!url.startsWith("file:")) {
67
+ if (typeof originalFetch !== "function") {
68
+ throw new Error("fetch is not available for non-file URLs.");
69
+ }
70
+ return originalFetch(input, init);
71
+ }
72
+ const bytes = await readFile(new URL(url));
73
+ return {
74
+ ok: true,
75
+ status: 200,
76
+ url,
77
+ async arrayBuffer() {
78
+ return bytes.buffer.slice(
79
+ bytes.byteOffset,
80
+ bytes.byteOffset + bytes.byteLength,
81
+ );
82
+ },
83
+ };
84
+ };
85
+ globalThis[FILE_URL_FETCH_PATCH_FLAG] = true;
86
+ }
87
+ }
88
+
89
+ async function patchEmceptionModuleTree(rootDir) {
90
+ const entries = await readdir(rootDir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ const fullPath = path.join(rootDir, entry.name);
93
+ if (entry.isDirectory()) {
94
+ await patchEmceptionModuleTree(fullPath);
95
+ continue;
96
+ }
97
+ if (!entry.name.endsWith(".mjs")) {
98
+ continue;
99
+ }
100
+
101
+ let source = await readFile(fullPath, "utf8");
102
+ source = source.replaceAll(
103
+ 'scriptDirectory=__dirname+"/"',
104
+ 'scriptDirectory=(new URL(".", import.meta.url)).pathname',
105
+ );
106
+
107
+ if (entry.name === "emception.mjs") {
108
+ source = source.replace(
109
+ "this.#fs = await new FileSystem();",
110
+ [
111
+ "this.#fs = await new FileSystem({",
112
+ " locateFile: (file, scriptDirectory) => scriptDirectory + file,",
113
+ ' cache: "/tmp/emception-cache",',
114
+ " });",
115
+ ].join("\n"),
116
+ );
117
+ source = source.replace(
118
+ "const config = {",
119
+ [
120
+ "const config = {",
121
+ " locateFile: (file, scriptDirectory) => scriptDirectory + file,",
122
+ ].join("\n"),
123
+ );
124
+ }
125
+
126
+ if (entry.name === "FileSystem.mjs") {
127
+ source = source.replace(
128
+ [
129
+ " if (!this.exists(cache)) {",
130
+ " this.persist(cache);",
131
+ " }",
132
+ " await this.pull();",
133
+ ].join("\n"),
134
+ [
135
+ ' if (typeof indexedDB !== "undefined") {',
136
+ " if (!this.exists(cache)) {",
137
+ " this.persist(cache);",
138
+ " }",
139
+ " await this.pull();",
140
+ " }",
141
+ ].join("\n"),
142
+ );
143
+ }
144
+
145
+ await writeFile(fullPath, source, "utf8");
146
+ }
147
+ }
148
+
149
+ async function preparePatchedEmceptionRoot() {
150
+ if (!patchedEmceptionRootPromise) {
151
+ patchedEmceptionRootPromise = (async () => {
152
+ const sourceRoot = path.dirname(require.resolve("sdn-emception"));
153
+ const markerPath = path.join(EMCEPTION_PATCH_ROOT, PATCH_MARKER_FILENAME);
154
+ let marker = null;
155
+ try {
156
+ marker = await readFile(markerPath, "utf8");
157
+ } catch {
158
+ marker = null;
159
+ }
160
+
161
+ if (marker?.trim() !== PATCH_VERSION) {
162
+ await rm(EMCEPTION_PATCH_ROOT, { recursive: true, force: true });
163
+ await cp(sourceRoot, EMCEPTION_PATCH_ROOT, { recursive: true });
164
+ await patchEmceptionModuleTree(EMCEPTION_PATCH_ROOT);
165
+ await writeFile(markerPath, `${PATCH_VERSION}\n`, "utf8");
166
+ }
167
+
168
+ return EMCEPTION_PATCH_ROOT;
169
+ })().catch((error) => {
170
+ patchedEmceptionRootPromise = null;
171
+ throw error;
172
+ });
173
+ }
174
+
175
+ return patchedEmceptionRootPromise;
176
+ }
177
+
178
+ export async function loadEmception() {
179
+ if (!emceptionInstancePromise) {
180
+ emceptionInstancePromise = (async () => {
181
+ installNodeRuntimeShims();
182
+ const patchedRoot = await preparePatchedEmceptionRoot();
183
+ const moduleUrl = pathToFileURL(path.join(patchedRoot, "emception.mjs")).href;
184
+ const { default: Emception } = await import(moduleUrl);
185
+ const emception = new Emception({
186
+ baseUrl: pathToFileURL(`${patchedRoot}${path.sep}`).href,
187
+ });
188
+ await emception.init();
189
+ return emception;
190
+ })().catch((error) => {
191
+ emceptionInstancePromise = null;
192
+ throw error;
193
+ });
194
+ }
195
+
196
+ return emceptionInstancePromise;
197
+ }
198
+
199
+ export async function runWithEmceptionLock(task) {
200
+ const previous = emceptionExecutionQueue;
201
+ let release = () => {};
202
+ emceptionExecutionQueue = new Promise((resolve) => {
203
+ release = resolve;
204
+ });
205
+ await previous.catch(() => {});
206
+ try {
207
+ const emception = await loadEmception().catch((error) => {
208
+ if (!error.code) {
209
+ error.code = "EMCEPTION_LOAD_FAILED";
210
+ }
211
+ throw error;
212
+ });
213
+ return await task(emception);
214
+ } finally {
215
+ release();
216
+ }
217
+ }