space-data-module-sdk 0.2.0 → 0.2.6

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 (110) hide show
  1. package/README.md +77 -2
  2. package/package.json +13 -4
  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 +182 -143
  9. package/src/compiler/emception.d.ts +60 -0
  10. package/src/compiler/emception.js +191 -0
  11. package/src/compiler/emceptionNode.js +234 -0
  12. package/src/compiler/flatcSupport.js +66 -0
  13. package/src/compiler/index.d.ts +24 -0
  14. package/src/compiler/index.js +5 -0
  15. package/src/compiler/invokeGlue.js +884 -0
  16. package/src/compliance/pluginCompliance.js +241 -1
  17. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts +51 -0
  18. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts.map +1 -0
  19. package/src/generated/orbpro/invoke/plugin-invoke-request.js +131 -0
  20. package/src/generated/orbpro/invoke/plugin-invoke-request.js.map +1 -0
  21. package/src/generated/orbpro/invoke/plugin-invoke-request.ts +173 -0
  22. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts +76 -0
  23. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts.map +1 -0
  24. package/src/generated/orbpro/invoke/plugin-invoke-response.js +184 -0
  25. package/src/generated/orbpro/invoke/plugin-invoke-response.js.map +1 -0
  26. package/src/generated/orbpro/invoke/plugin-invoke-response.ts +243 -0
  27. package/src/generated/orbpro/invoke.d.ts +3 -0
  28. package/src/generated/orbpro/invoke.d.ts.map +1 -0
  29. package/src/generated/orbpro/invoke.js +5 -0
  30. package/src/generated/orbpro/invoke.js.map +1 -0
  31. package/src/generated/orbpro/invoke.ts +6 -0
  32. package/src/generated/orbpro/manifest/accepted-type-set.d.ts +4 -4
  33. package/src/generated/orbpro/manifest/accepted-type-set.d.ts.map +1 -1
  34. package/src/generated/orbpro/manifest/accepted-type-set.js +18 -11
  35. package/src/generated/orbpro/manifest/accepted-type-set.js.map +1 -1
  36. package/src/generated/orbpro/manifest/build-artifact.d.ts +1 -1
  37. package/src/generated/orbpro/manifest/build-artifact.d.ts.map +1 -1
  38. package/src/generated/orbpro/manifest/build-artifact.js +28 -15
  39. package/src/generated/orbpro/manifest/build-artifact.js.map +1 -1
  40. package/src/generated/orbpro/manifest/capability-kind.d.ts +1 -1
  41. package/src/generated/orbpro/manifest/capability-kind.d.ts.map +1 -1
  42. package/src/generated/orbpro/manifest/capability-kind.js +1 -1
  43. package/src/generated/orbpro/manifest/capability-kind.js.map +1 -1
  44. package/src/generated/orbpro/manifest/drain-policy.d.ts.map +1 -1
  45. package/src/generated/orbpro/manifest/drain-policy.js.map +1 -1
  46. package/src/generated/orbpro/manifest/host-capability.d.ts +2 -2
  47. package/src/generated/orbpro/manifest/host-capability.d.ts.map +1 -1
  48. package/src/generated/orbpro/manifest/host-capability.js +19 -11
  49. package/src/generated/orbpro/manifest/host-capability.js.map +1 -1
  50. package/src/generated/orbpro/manifest/invoke-surface.d.ts +8 -0
  51. package/src/generated/orbpro/manifest/invoke-surface.d.ts.map +1 -0
  52. package/src/generated/orbpro/manifest/invoke-surface.js +11 -0
  53. package/src/generated/orbpro/manifest/invoke-surface.js.map +1 -0
  54. package/src/generated/orbpro/manifest/invoke-surface.ts +11 -0
  55. package/src/generated/orbpro/manifest/method-manifest.d.ts +6 -6
  56. package/src/generated/orbpro/manifest/method-manifest.d.ts.map +1 -1
  57. package/src/generated/orbpro/manifest/method-manifest.js +33 -16
  58. package/src/generated/orbpro/manifest/method-manifest.js.map +1 -1
  59. package/src/generated/orbpro/manifest/plugin-family.d.ts.map +1 -1
  60. package/src/generated/orbpro/manifest/plugin-family.js.map +1 -1
  61. package/src/generated/orbpro/manifest/plugin-manifest.d.ts +10 -2
  62. package/src/generated/orbpro/manifest/plugin-manifest.d.ts.map +1 -1
  63. package/src/generated/orbpro/manifest/plugin-manifest.js +48 -9
  64. package/src/generated/orbpro/manifest/plugin-manifest.js.map +1 -1
  65. package/src/generated/orbpro/manifest/plugin-manifest.ts +322 -491
  66. package/src/generated/orbpro/manifest/port-manifest.d.ts +4 -4
  67. package/src/generated/orbpro/manifest/port-manifest.d.ts.map +1 -1
  68. package/src/generated/orbpro/manifest/port-manifest.js +26 -13
  69. package/src/generated/orbpro/manifest/port-manifest.js.map +1 -1
  70. package/src/generated/orbpro/manifest/protocol-spec.d.ts +1 -1
  71. package/src/generated/orbpro/manifest/protocol-spec.d.ts.map +1 -1
  72. package/src/generated/orbpro/manifest/protocol-spec.js +28 -15
  73. package/src/generated/orbpro/manifest/protocol-spec.js.map +1 -1
  74. package/src/generated/orbpro/manifest/timer-spec.d.ts +1 -1
  75. package/src/generated/orbpro/manifest/timer-spec.d.ts.map +1 -1
  76. package/src/generated/orbpro/manifest/timer-spec.js +27 -16
  77. package/src/generated/orbpro/manifest/timer-spec.js.map +1 -1
  78. package/src/generated/orbpro/manifest.d.ts +13 -0
  79. package/src/generated/orbpro/manifest.d.ts.map +1 -0
  80. package/src/generated/orbpro/manifest.js +1 -0
  81. package/src/generated/orbpro/manifest.js.map +1 -0
  82. package/src/generated/orbpro/manifest.ts +16 -0
  83. package/src/generated/orbpro/stream/buffer-mutability.d.ts.map +1 -1
  84. package/src/generated/orbpro/stream/buffer-mutability.js.map +1 -1
  85. package/src/generated/orbpro/stream/buffer-ownership.d.ts.map +1 -1
  86. package/src/generated/orbpro/stream/buffer-ownership.js.map +1 -1
  87. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts +22 -5
  88. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts.map +1 -1
  89. package/src/generated/orbpro/stream/flat-buffer-type-ref.js +107 -17
  90. package/src/generated/orbpro/stream/flat-buffer-type-ref.js.map +1 -1
  91. package/src/generated/orbpro/stream/flat-buffer-type-ref.ts +126 -2
  92. package/src/generated/orbpro/stream/payload-wire-format.d.ts +8 -0
  93. package/src/generated/orbpro/stream/payload-wire-format.d.ts.map +1 -0
  94. package/src/generated/orbpro/stream/payload-wire-format.js +11 -0
  95. package/src/generated/orbpro/stream/payload-wire-format.js.map +1 -0
  96. package/src/generated/orbpro/stream/payload-wire-format.ts +11 -0
  97. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts +4 -4
  98. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts.map +1 -1
  99. package/src/generated/orbpro/stream/typed-arena-buffer.js +42 -24
  100. package/src/generated/orbpro/stream/typed-arena-buffer.js.map +1 -1
  101. package/src/index.d.ts +96 -5
  102. package/src/index.js +3 -0
  103. package/src/invoke/codec.js +278 -0
  104. package/src/invoke/index.js +9 -0
  105. package/src/manifest/codec.js +10 -2
  106. package/src/manifest/index.js +5 -2
  107. package/src/manifest/normalize.js +58 -0
  108. package/src/runtime/constants.js +12 -0
  109. package/src/runtime/index.d.ts +13 -0
  110. package/src/runtime/index.js +2 -0
@@ -1,8 +1,10 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
- import { execFile as execFileCallback } from "node:child_process";
5
- import { promisify } from "node:util";
3
+ import {
4
+ mkdtemp,
5
+ rm,
6
+ writeFile,
7
+ } from "node:fs/promises";
6
8
 
7
9
  import {
8
10
  createDeploymentAuthorization,
@@ -11,7 +13,18 @@ import {
11
13
  } from "../auth/index.js";
12
14
  import { validateArtifactWithStandards } from "../compliance/index.js";
13
15
  import { generateEmbeddedManifestSource } from "../embeddedManifest.js";
16
+ import {
17
+ generateInvokeSupportHeader,
18
+ generateInvokeSupportSource,
19
+ resolveInvokeSurfaces,
20
+ } from "./invokeGlue.js";
21
+ import {
22
+ getFlatbuffersCppRuntimeHeaders,
23
+ getInvokeCppSchemaHeaders,
24
+ } from "./flatcSupport.js";
25
+ import { runWithEmceptionLock } from "./emceptionNode.js";
14
26
  import { encodePluginManifest, toEmbeddedPluginManifest } from "../manifest/index.js";
27
+ import { DefaultInvokeExports, InvokeSurface } from "../runtime/constants.js";
15
28
  import {
16
29
  encryptJsonForRecipient,
17
30
  generateX25519Keypair,
@@ -26,7 +39,6 @@ import {
26
39
  import { sha256Bytes } from "../utils/crypto.js";
27
40
  import { getWasmWallet } from "../utils/wasmCrypto.js";
28
41
 
29
- const execFile = promisify(execFileCallback);
30
42
  const C_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
31
43
 
32
44
  function selectCompiler(language) {
@@ -49,7 +61,7 @@ function ensureExportableMethodIds(manifest) {
49
61
  }
50
62
  }
51
63
 
52
- function buildCompilerArgs(compiler, exportedSymbols, options = {}) {
64
+ function buildCompilerArgs(exportedSymbols, options = {}) {
53
65
  const linkerExports = exportedSymbols.map(
54
66
  (symbol) => "-Wl,--export=" + symbol,
55
67
  );
@@ -57,125 +69,159 @@ function buildCompilerArgs(compiler, exportedSymbols, options = {}) {
57
69
  if (options.allowUndefinedImports === true) {
58
70
  extraArgs.push("-s", "ERROR_ON_UNDEFINED_SYMBOLS=0", "-Wl,--allow-undefined");
59
71
  }
60
- return [
61
- "-O2",
62
- "--no-entry",
63
- "-s",
64
- "STANDALONE_WASM=1",
65
- ...extraArgs,
66
- ...linkerExports,
67
- ];
72
+ const args = ["-O2", "-s", "STANDALONE_WASM=1", ...extraArgs, ...linkerExports];
73
+ if (options.noEntry === true) {
74
+ args.splice(1, 0, "--no-entry");
75
+ }
76
+ return args;
68
77
  }
69
78
 
70
- // ---------------------------------------------------------------------------
71
- // Emception in-process WASM-based Emscripten (preferred)
72
- // ---------------------------------------------------------------------------
73
-
74
- let emceptionInstance = null;
75
- let emceptionLoadAttempted = false;
79
+ async function getInvokeCppSupportFiles() {
80
+ const [runtimeHeaders, schemaHeaders] = await Promise.all([
81
+ getFlatbuffersCppRuntimeHeaders(),
82
+ getInvokeCppSchemaHeaders(),
83
+ ]);
84
+ return { runtimeHeaders, schemaHeaders };
85
+ }
76
86
 
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
87
+ async function writeFilesToEmception(emception, rootDir, files) {
88
+ for (const [relativePath, content] of Object.entries(files)) {
89
+ const filePath = path.posix.join(rootDir, relativePath);
90
+ emception.FS.mkdirTree(path.posix.dirname(filePath));
91
+ emception.writeFile(filePath, content);
89
92
  }
90
- return null;
91
93
  }
92
94
 
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",
116
- outputPath,
117
- ].join(" ");
118
-
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
- );
95
+ function removeEmceptionDirectory(emception, directoryPath) {
96
+ if (!emception.FS.analyzePath(directoryPath).exists) {
97
+ return;
124
98
  }
125
-
126
- const wasmBytes = emception.readFile(outputPath);
127
- return new Uint8Array(wasmBytes);
99
+ const entries = emception.FS.readdir(directoryPath).filter(
100
+ (entry) => entry !== "." && entry !== "..",
101
+ );
102
+ for (const entry of entries) {
103
+ const entryPath = path.posix.join(directoryPath, entry);
104
+ const stat = emception.FS.stat(entryPath);
105
+ if (emception.FS.isDir(stat.mode)) {
106
+ removeEmceptionDirectory(emception, entryPath);
107
+ emception.FS.rmdir(entryPath);
108
+ } else {
109
+ emception.FS.unlink(entryPath);
110
+ }
111
+ }
112
+ emception.FS.rmdir(directoryPath);
128
113
  }
129
114
 
130
- // ---------------------------------------------------------------------------
131
- // System Emscripten — fallback to emcc/em++ on PATH
132
- // ---------------------------------------------------------------------------
133
-
134
- async function compileWithSystemEmcc(
135
- compiler,
136
- sourceCode,
137
- manifestSource,
138
- exportedSymbols,
139
- outputPath,
140
- compileOptions,
141
- ) {
115
+ async function compileWithEmception(options = {}) {
116
+ const {
117
+ sourceCompilerCommand,
118
+ sourceExtension,
119
+ sourceCode,
120
+ manifestSource,
121
+ invokeHeaderSource,
122
+ invokeSource,
123
+ exportedSymbols,
124
+ outputPath,
125
+ compileOptions,
126
+ } = options;
142
127
  const tempDir = await mkdtemp(
143
128
  path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
144
129
  );
145
- const sourcePath = path.join(tempDir, `module.${compiler.extension}`);
146
- const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.c");
147
130
  const resolvedOutputPath = path.resolve(
148
131
  outputPath ?? path.join(tempDir, "module.wasm"),
149
132
  );
150
133
 
151
- await writeFile(sourcePath, sourceCode, "utf8");
152
- await writeFile(manifestSourcePath, manifestSource, "utf8");
153
-
154
- const args = buildCompilerArgs(compiler, exportedSymbols, compileOptions);
155
-
156
134
  try {
157
- await execFile(compiler.command, [
158
- sourcePath,
159
- manifestSourcePath,
160
- ...args,
161
- "-o",
162
- resolvedOutputPath,
163
- ], { timeout: 120_000 });
135
+ return await runWithEmceptionLock(async (emception) => {
136
+ const workDir = "/working/space-data-module-sdk-compile";
137
+ const runtimeIncludeDir = path.posix.join(workDir, "flatbuffers-runtime");
138
+ const sourcePath = path.posix.join(workDir, `module.${sourceExtension}`);
139
+ const manifestSourcePath = path.posix.join(workDir, "plugin-manifest-exports.cpp");
140
+ const invokeHeaderPath = path.posix.join(workDir, "space_data_module_invoke.h");
141
+ const invokeSourcePath = path.posix.join(workDir, "plugin-invoke-bridge.cpp");
142
+ const sourceObjectPath = path.posix.join(workDir, "module.o");
143
+ const manifestObjectPath = path.posix.join(workDir, "plugin-manifest-exports.o");
144
+ const invokeObjectPath = path.posix.join(workDir, "plugin-invoke-bridge.o");
145
+ const wasmOutputPath = path.posix.join(workDir, "module.wasm");
146
+
147
+ const { runtimeHeaders, schemaHeaders } = await getInvokeCppSupportFiles();
148
+ const args = buildCompilerArgs(exportedSymbols, compileOptions);
149
+
150
+ try {
151
+ emception.FS.mkdirTree(workDir);
152
+ await writeFilesToEmception(emception, runtimeIncludeDir, runtimeHeaders);
153
+ await writeFilesToEmception(emception, workDir, schemaHeaders);
154
+ emception.writeFile(sourcePath, sourceCode);
155
+ emception.writeFile(manifestSourcePath, manifestSource);
156
+ emception.writeFile(invokeHeaderPath, invokeHeaderSource);
157
+ emception.writeFile(invokeSourcePath, invokeSource);
158
+
159
+ const commands = [
160
+ [
161
+ sourceCompilerCommand,
162
+ "-c",
163
+ sourcePath,
164
+ `-I${workDir}`,
165
+ "-o",
166
+ sourceObjectPath,
167
+ ],
168
+ [
169
+ "em++",
170
+ "-c",
171
+ manifestSourcePath,
172
+ "-std=c++17",
173
+ `-I${workDir}`,
174
+ `-I${runtimeIncludeDir}`,
175
+ "-o",
176
+ manifestObjectPath,
177
+ ],
178
+ [
179
+ "em++",
180
+ "-c",
181
+ invokeSourcePath,
182
+ "-std=c++17",
183
+ `-I${workDir}`,
184
+ `-I${runtimeIncludeDir}`,
185
+ "-o",
186
+ invokeObjectPath,
187
+ ],
188
+ [
189
+ "em++",
190
+ sourceObjectPath,
191
+ manifestObjectPath,
192
+ invokeObjectPath,
193
+ ...args,
194
+ "-o",
195
+ wasmOutputPath,
196
+ ],
197
+ ];
198
+
199
+ for (const command of commands) {
200
+ const result = emception.run(command.join(" "));
201
+ if (result.returncode !== 0) {
202
+ throw new Error(
203
+ `Compilation failed with ${command[0]} (emception): ${result.stderr || result.stdout}`,
204
+ );
205
+ }
206
+ }
207
+
208
+ const wasmBytes = new Uint8Array(emception.readFile(wasmOutputPath));
209
+ await writeFile(resolvedOutputPath, wasmBytes);
210
+ return { wasmBytes, outputPath: resolvedOutputPath, tempDir };
211
+ } finally {
212
+ try {
213
+ removeEmceptionDirectory(emception, workDir);
214
+ } catch {
215
+ // Best-effort cleanup only; the shared emception instance remains usable.
216
+ }
217
+ }
218
+ });
164
219
  } catch (error) {
165
- error.message =
166
- `Compilation failed with ${compiler.command}: ` +
167
- (error.stderr || error.message);
220
+ await rm(tempDir, { recursive: true, force: true });
168
221
  throw error;
169
222
  }
170
-
171
- const wasmBytes = await readFile(resolvedOutputPath);
172
- return { wasmBytes, outputPath: resolvedOutputPath, tempDir };
173
223
  }
174
224
 
175
- // ---------------------------------------------------------------------------
176
- // Public API
177
- // ---------------------------------------------------------------------------
178
-
179
225
  export async function compileModuleFromSource(options = {}) {
180
226
  const manifest = options.manifest ?? {};
181
227
  const sourceCode = String(options.sourceCode ?? "");
@@ -193,16 +239,27 @@ export async function compileModuleFromSource(options = {}) {
193
239
  }
194
240
 
195
241
  const compiler = selectCompiler(options.language);
242
+ const invokeSurfaces = resolveInvokeSurfaces(manifest);
243
+ const includeCommandMain = invokeSurfaces.includes(InvokeSurface.COMMAND);
196
244
  const { manifest: embeddedManifest, warnings } = toEmbeddedPluginManifest(
197
245
  manifest,
198
246
  );
199
247
  const manifestSource = generateEmbeddedManifestSource({
200
248
  manifest: embeddedManifest,
201
249
  });
250
+ const invokeHeaderSource = generateInvokeSupportHeader();
251
+ const invokeSource = generateInvokeSupportSource({
252
+ manifest,
253
+ includeCommandMain,
254
+ });
202
255
 
203
256
  const exportedSymbols = [
204
257
  "plugin_get_manifest_flatbuffer",
205
258
  "plugin_get_manifest_flatbuffer_size",
259
+ DefaultInvokeExports.invokeSymbol,
260
+ DefaultInvokeExports.allocSymbol,
261
+ DefaultInvokeExports.freeSymbol,
262
+ ...(includeCommandMain ? [DefaultInvokeExports.commandSymbol] : []),
206
263
  ...new Set(
207
264
  (Array.isArray(manifest.methods) ? manifest.methods : [])
208
265
  .map((method) => String(method?.methodId ?? "").trim())
@@ -210,54 +267,36 @@ export async function compileModuleFromSource(options = {}) {
210
267
  ),
211
268
  ];
212
269
 
213
- // Try emception first, fall back to system emcc/em++
214
- const emception = await loadEmception();
215
-
216
270
  let wasmBytes;
217
271
  let resolvedOutputPath = null;
218
272
  let tempDir = null;
219
- let compilerBackend;
220
-
221
- if (emception) {
222
- wasmBytes = await compileWithEmception(
223
- emception,
224
- compiler,
225
- sourceCode,
226
- manifestSource,
227
- exportedSymbols,
228
- options,
229
- );
230
- compilerBackend = `${compiler.command} (emception)`;
231
- } else {
232
- const result = await compileWithSystemEmcc(
233
- compiler,
234
- sourceCode,
235
- manifestSource,
236
- exportedSymbols,
237
- options.outputPath,
238
- options,
239
- );
240
- wasmBytes = result.wasmBytes;
241
- resolvedOutputPath = result.outputPath;
242
- tempDir = result.tempDir;
243
- compilerBackend = `${compiler.command} (system)`;
244
- }
273
+ const compileOptions = {
274
+ ...options,
275
+ noEntry: includeCommandMain !== true,
276
+ };
277
+ const result = await compileWithEmception({
278
+ sourceCompilerCommand: compiler.command,
279
+ sourceExtension: compiler.extension,
280
+ sourceCode,
281
+ manifestSource,
282
+ invokeHeaderSource,
283
+ invokeSource,
284
+ exportedSymbols,
285
+ outputPath: options.outputPath,
286
+ compileOptions,
287
+ });
288
+ wasmBytes = result.wasmBytes;
289
+ resolvedOutputPath = result.outputPath;
290
+ tempDir = result.tempDir;
245
291
 
246
292
  // 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
- });
293
+ const report = await validateArtifactWithStandards({
294
+ manifest,
295
+ wasmPath: resolvedOutputPath,
296
+ });
258
297
 
259
298
  return {
260
- compiler: compilerBackend,
299
+ compiler: "em++ (emception)",
261
300
  language: compiler.language,
262
301
  outputPath: resolvedOutputPath,
263
302
  tempDir,
@@ -0,0 +1,60 @@
1
+ export interface EmceptionCommandResult {
2
+ command: string;
3
+ exitCode: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ }
7
+
8
+ export type SharedEmceptionFileContent =
9
+ | string
10
+ | Uint8Array
11
+ | ArrayBuffer
12
+ | ArrayBufferView;
13
+
14
+ export interface SharedEmceptionHandle {
15
+ getRaw(): unknown;
16
+ exists(targetPath: string): boolean;
17
+ mkdirTree(directoryPath: string): void;
18
+ writeFile(filePath: string, content: SharedEmceptionFileContent): void;
19
+ writeFiles(
20
+ rootDir: string,
21
+ files: Record<string, SharedEmceptionFileContent>,
22
+ ): void;
23
+ readFile(filePath: string): Uint8Array;
24
+ readFile(filePath: string, options: { encoding: "utf8" }): string;
25
+ removeTree(targetPath: string): void;
26
+ run(
27
+ command: string,
28
+ options?: { throwOnNonZero?: boolean },
29
+ ): EmceptionCommandResult;
30
+ }
31
+
32
+ export interface SharedEmceptionSession {
33
+ load(): Promise<unknown>;
34
+ withLock<T>(
35
+ task: (handle: SharedEmceptionHandle) => T | Promise<T>,
36
+ ): Promise<T>;
37
+ exists(targetPath: string): Promise<boolean>;
38
+ mkdirTree(directoryPath: string): Promise<void>;
39
+ writeFile(
40
+ filePath: string,
41
+ content: SharedEmceptionFileContent,
42
+ ): Promise<void>;
43
+ writeFiles(
44
+ rootDir: string,
45
+ files: Record<string, SharedEmceptionFileContent>,
46
+ ): Promise<void>;
47
+ readFile(filePath: string): Promise<Uint8Array>;
48
+ readFile(filePath: string, options: { encoding: "utf8" }): Promise<string>;
49
+ removeTree(targetPath: string): Promise<void>;
50
+ run(
51
+ command: string,
52
+ options?: { throwOnNonZero?: boolean },
53
+ ): Promise<EmceptionCommandResult>;
54
+ }
55
+
56
+ export function createSharedEmceptionSession(): SharedEmceptionSession;
57
+ export function loadSharedEmception(): Promise<unknown>;
58
+ export function withSharedEmception<T>(
59
+ task: (handle: SharedEmceptionHandle) => T | Promise<T>,
60
+ ): Promise<T>;
@@ -0,0 +1,191 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ getSharedEmceptionController,
5
+ loadEmception,
6
+ runWithEmceptionLock,
7
+ } from "./emceptionNode.js";
8
+
9
+ const TEXT_DECODER = new TextDecoder();
10
+ const TEXT_ENCODER = new TextEncoder();
11
+
12
+ function normalizeFileContent(content) {
13
+ if (typeof content === "string") {
14
+ return content;
15
+ }
16
+ if (content instanceof Uint8Array) {
17
+ return content;
18
+ }
19
+ if (content instanceof ArrayBuffer) {
20
+ return new Uint8Array(content);
21
+ }
22
+ if (ArrayBuffer.isView(content)) {
23
+ return new Uint8Array(
24
+ content.buffer,
25
+ content.byteOffset,
26
+ content.byteLength,
27
+ );
28
+ }
29
+ throw new TypeError(
30
+ "Emception file content must be a string, Uint8Array, ArrayBuffer, or ArrayBufferView.",
31
+ );
32
+ }
33
+
34
+ function cloneReadBytes(value) {
35
+ if (typeof value === "string") {
36
+ return TEXT_ENCODER.encode(value);
37
+ }
38
+ const bytes = normalizeFileContent(value);
39
+ return new Uint8Array(bytes);
40
+ }
41
+
42
+ function removeTree(emception, targetPath) {
43
+ const analysis = emception.FS.analyzePath(targetPath);
44
+ if (!analysis.exists) {
45
+ return;
46
+ }
47
+ const stat = emception.FS.stat(targetPath);
48
+ if (!emception.FS.isDir(stat.mode)) {
49
+ emception.FS.unlink(targetPath);
50
+ return;
51
+ }
52
+ const entries = emception.FS.readdir(targetPath).filter(
53
+ (entry) => entry !== "." && entry !== "..",
54
+ );
55
+ for (const entry of entries) {
56
+ removeTree(emception, path.posix.join(targetPath, entry));
57
+ }
58
+ emception.FS.rmdir(targetPath);
59
+ }
60
+
61
+ function normalizeRunResult(command, result) {
62
+ const normalized = {
63
+ command,
64
+ exitCode: Number(result?.returncode ?? 0) >>> 0,
65
+ stdout: String(result?.stdout ?? ""),
66
+ stderr: String(result?.stderr ?? ""),
67
+ };
68
+ return normalized;
69
+ }
70
+
71
+ function maybeThrowRunFailure(result, options = {}) {
72
+ if (options.throwOnNonZero === false || result.exitCode === 0) {
73
+ return result;
74
+ }
75
+ const detail = result.stderr || result.stdout || "unknown emception failure";
76
+ throw new Error(
77
+ `Emception command failed with exit code ${result.exitCode}: ${result.command}\n${detail}`,
78
+ );
79
+ }
80
+
81
+ class SharedEmceptionHandle {
82
+ constructor(emception) {
83
+ this.emception = emception;
84
+ }
85
+
86
+ getRaw() {
87
+ return this.emception;
88
+ }
89
+
90
+ exists(targetPath) {
91
+ return this.emception.FS.analyzePath(targetPath).exists;
92
+ }
93
+
94
+ mkdirTree(directoryPath) {
95
+ this.emception.FS.mkdirTree(directoryPath);
96
+ }
97
+
98
+ writeFile(filePath, content) {
99
+ this.emception.FS.mkdirTree(path.posix.dirname(filePath));
100
+ this.emception.writeFile(filePath, normalizeFileContent(content));
101
+ }
102
+
103
+ writeFiles(rootDir, files) {
104
+ for (const [relativePath, content] of Object.entries(files ?? {})) {
105
+ this.writeFile(path.posix.join(rootDir, relativePath), content);
106
+ }
107
+ }
108
+
109
+ readFile(filePath, options = {}) {
110
+ const bytes = cloneReadBytes(this.emception.readFile(filePath));
111
+ if (options.encoding === "utf8") {
112
+ return TEXT_DECODER.decode(bytes);
113
+ }
114
+ return bytes;
115
+ }
116
+
117
+ removeTree(targetPath) {
118
+ removeTree(this.emception, targetPath);
119
+ }
120
+
121
+ run(command, options = {}) {
122
+ const result = normalizeRunResult(command, this.emception.run(command));
123
+ return maybeThrowRunFailure(result, options);
124
+ }
125
+ }
126
+
127
+ class SharedEmceptionSession {
128
+ constructor(controller = getSharedEmceptionController()) {
129
+ this.controller = controller;
130
+ }
131
+
132
+ async load() {
133
+ return this.controller.load();
134
+ }
135
+
136
+ async withLock(task) {
137
+ return this.controller.withLock(
138
+ (emception) => task(new SharedEmceptionHandle(emception)),
139
+ );
140
+ }
141
+
142
+ async exists(targetPath) {
143
+ return this.withLock((handle) => handle.exists(targetPath));
144
+ }
145
+
146
+ async mkdirTree(directoryPath) {
147
+ await this.withLock((handle) => {
148
+ handle.mkdirTree(directoryPath);
149
+ });
150
+ }
151
+
152
+ async writeFile(filePath, content) {
153
+ await this.withLock((handle) => {
154
+ handle.writeFile(filePath, content);
155
+ });
156
+ }
157
+
158
+ async writeFiles(rootDir, files) {
159
+ await this.withLock((handle) => {
160
+ handle.writeFiles(rootDir, files);
161
+ });
162
+ }
163
+
164
+ async readFile(filePath, options = {}) {
165
+ return this.withLock((handle) => handle.readFile(filePath, options));
166
+ }
167
+
168
+ async removeTree(targetPath) {
169
+ await this.withLock((handle) => {
170
+ handle.removeTree(targetPath);
171
+ });
172
+ }
173
+
174
+ async run(command, options = {}) {
175
+ return this.withLock((handle) => handle.run(command, options));
176
+ }
177
+ }
178
+
179
+ export function createSharedEmceptionSession() {
180
+ return new SharedEmceptionSession();
181
+ }
182
+
183
+ export async function loadSharedEmception() {
184
+ return loadEmception();
185
+ }
186
+
187
+ export async function withSharedEmception(task) {
188
+ return runWithEmceptionLock(
189
+ (emception) => task(new SharedEmceptionHandle(emception)),
190
+ );
191
+ }