space-data-module-sdk 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,7 @@ A module built with this SDK is a `.wasm` artifact with:
34
34
  - an exported `_start` entry when the artifact supports WASI command mode
35
35
  - optional `sds.bundle` custom-section payloads for:
36
36
  - manifest bytes
37
+ - resolved deployment plans and input bindings
37
38
  - deployment authorization
38
39
  - detached signatures
39
40
  - encrypted transport envelopes
@@ -81,6 +82,14 @@ outputs. The reference invoke examples live in
81
82
  - `manifest.hybrid.json`
82
83
  - `module.c`
83
84
 
85
+ Input and output ports can independently declare regular `flatbuffer` payloads
86
+ or `aligned-binary` layouts. Mixed contracts are valid. When a port advertises
87
+ an `aligned-binary` layout, it must also advertise a regular `flatbuffer`
88
+ fallback for the same schema in the same accepted type set. A module can accept
89
+ a regular `OMM.fbs` request and emit an aligned-binary `StateVector.fbs`
90
+ response, provided the output port also declares the regular `StateVector.fbs`
91
+ fallback and the aligned type ref carries the correct layout metadata.
92
+
84
93
  ## Runtime Portability
85
94
 
86
95
  The module format is language-neutral. A host can load modules from this SDK
@@ -115,6 +124,25 @@ This repo currently includes:
115
124
  - a reference Node host and sync `sdn_host` bridge for the first hostcall
116
125
  surface
117
126
 
127
+ ## Testing
128
+
129
+ This repo now exposes a manifest-driven harness generator from
130
+ `space-data-module-sdk/testing` and two complementary integration suites:
131
+
132
+ - `npm run test:runtime-matrix`
133
+ - cross-language runtime smoke across the same WASM in Node.js, Go, Python,
134
+ Rust, Java, C#, and Swift
135
+ - covers method calling, aligned-binary envelope metadata preservation,
136
+ stdin/stdout/stderr, args, env, preopened filesystem access, and basic WASI
137
+ clock/time smoke
138
+ - `npm run test:host-surfaces`
139
+ - authoritative Node-host coverage for HTTP, TCP, UDP, TLS, WebSocket, MQTT,
140
+ process execution, timers, filesystem, and the sync `sdn_host` ABI
141
+
142
+ The detailed edge cases and the current WASI-vs-host portability boundary are
143
+ documented in
144
+ [`docs/testing-harness.md`](./docs/testing-harness.md).
145
+
118
146
  ## Install
119
147
 
120
148
  ```bash
@@ -161,8 +189,36 @@ import { createDeploymentAuthorization } from "space-data-module-sdk/auth";
161
189
  import { encryptJsonForRecipient } from "space-data-module-sdk/transport";
162
190
  import { compileModuleFromSource } from "space-data-module-sdk/compiler";
163
191
  import { createSingleFileBundle } from "space-data-module-sdk/bundle";
192
+ import { validateDeploymentPlan } from "space-data-module-sdk/deployment";
193
+ import { generateManifestHarnessPlan } from "space-data-module-sdk/testing";
164
194
  ```
165
195
 
196
+ ## Protocol Installation
197
+
198
+ Modules can declare hosted protocol contracts in `manifest.protocols`.
199
+
200
+ Those declarations are for stable artifact identity:
201
+
202
+ - `wireId`
203
+ - `transportKind`
204
+ - `role`
205
+ - `specUri`
206
+ - hosting hints like `defaultPort` and `requireSecureTransport`
207
+
208
+ Concrete multiaddrs, peer IDs, and producer routing do not belong in the
209
+ canonical manifest. They belong in deployment metadata attached to the final
210
+ package or bundle.
211
+
212
+ This repo exposes that deployment surface from
213
+ `space-data-module-sdk/deployment`. Use it to:
214
+
215
+ - validate resolved protocol installations
216
+ - describe input bindings from producers to module ports
217
+ - attach a deployment plan to `sds.bundle`
218
+
219
+ The full contract split is documented in
220
+ [`docs/protocol-installation.md`](./docs/protocol-installation.md).
221
+
166
222
  ## Single-File Bundles
167
223
 
168
224
  `sds.bundle` keeps module delivery to one file without changing WebAssembly
@@ -179,6 +235,9 @@ The reference path lives in
179
235
  - the `go` and `python` directories show non-JS readers against the same
180
236
  bundle contract
181
237
 
238
+ Standard bundle payloads now include the optional `deployment-plan` JSON entry
239
+ for resolved protocol installations and producer input bindings.
240
+
182
241
  ## Module Publication
183
242
 
184
243
  Packages that publish SDN modules now use the canonical `sdn-module`
@@ -294,8 +353,11 @@ npm run check:compliance
294
353
  ```
295
354
 
296
355
  Node.js `>=20` is required. The compiler uses `sdn-emception` and `flatc-wasm`
297
- by default, with a system Emscripten fallback only if the embedded toolchain
298
- cannot load.
356
+ by default.
357
+
358
+ If another repo needs the same compiler runtime, the package also exposes a
359
+ shared emception session at `space-data-module-sdk/compiler/emception` with
360
+ helpers for serialized command execution and virtual filesystem access.
299
361
 
300
362
  ## License
301
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
5
5
  "type": "module",
6
6
  "types": "./src/index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/DigitalArsenal/space-data-module-sdk.git"
10
10
  },
11
11
  "bin": {
12
- "space-data-module": "./bin/space-data-module.js"
12
+ "space-data-module": "bin/space-data-module.js"
13
13
  },
14
14
  "exports": {
15
15
  ".": "./src/index.js",
@@ -18,8 +18,12 @@
18
18
  "./auth": "./src/auth/index.js",
19
19
  "./transport": "./src/transport/index.js",
20
20
  "./compiler": "./src/compiler/index.js",
21
+ "./compiler/emception": "./src/compiler/emception.js",
21
22
  "./bundle": "./src/bundle/index.js",
23
+ "./deployment": "./src/deployment/index.js",
22
24
  "./invoke": "./src/invoke/index.js",
25
+ "./runtime": "./src/runtime/index.js",
26
+ "./testing": "./src/testing/index.js",
23
27
  "./standards": "./src/standards/index.js",
24
28
  "./schemas/*": "./schemas/*"
25
29
  },
@@ -30,6 +34,8 @@
30
34
  ],
31
35
  "scripts": {
32
36
  "test": "node --test",
37
+ "test:host-surfaces": "node --test test/node-host.test.js test/host-abi.test.js",
38
+ "test:runtime-matrix": "SPACE_DATA_MODULE_SDK_ENABLE_RUNTIME_MATRIX=1 node --test test/runtime-matrix.test.js",
33
39
  "start:lab": "node ./lab/server.js",
34
40
  "check:compliance": "node ./bin/space-data-module.js check --repo-root .",
35
41
  "generate:vectors": "node ./examples/single-file-bundle/generate-vectors.mjs",
@@ -134,6 +134,15 @@ table ProtocolSpec {
134
134
  input_port_id: string;
135
135
  output_port_id: string;
136
136
  description: string;
137
+ wire_id: string;
138
+ transport_kind: string;
139
+ role: string;
140
+ spec_uri: string;
141
+ auto_install: bool = true;
142
+ advertise: bool = false;
143
+ discovery_key: string;
144
+ default_port: uint16;
145
+ require_secure_transport: bool = false;
137
146
  }
138
147
 
139
148
  /// Build artifact emitted by the plugin toolchain.
@@ -5,4 +5,7 @@ export const DEFAULT_HASH_ALGORITHM = "sha256";
5
5
  export const DEFAULT_MANIFEST_EXPORT_SYMBOL = "plugin_get_manifest_flatbuffer";
6
6
  export const DEFAULT_MANIFEST_SIZE_SYMBOL =
7
7
  "plugin_get_manifest_flatbuffer_size";
8
-
8
+ export const SDS_DEPLOYMENT_SECTION_NAME = "sds.deployment";
9
+ export const SDS_DEPLOYMENT_ENTRY_ID = "deployment-plan";
10
+ export const SDS_DEPLOYMENT_MEDIA_TYPE =
11
+ "application/vnd.space-data.module.deployment+json";
@@ -3,11 +3,14 @@ import { decodePluginManifest, encodePluginManifest } from "../manifest/index.js
3
3
  import { toUint8Array } from "../runtime/bufferLike.js";
4
4
  import { sha256Bytes } from "../utils/crypto.js";
5
5
  import { bytesToHex } from "../utils/encoding.js";
6
+ import { createDeploymentPlanBundleEntry } from "../deployment/index.js";
6
7
  import {
7
8
  DEFAULT_MANIFEST_EXPORT_SYMBOL,
8
9
  DEFAULT_MANIFEST_SIZE_SYMBOL,
9
10
  SDS_BUNDLE_SECTION_NAME,
10
11
  SDS_CUSTOM_SECTION_PREFIX,
12
+ SDS_DEPLOYMENT_ENTRY_ID,
13
+ SDS_DEPLOYMENT_SECTION_NAME,
11
14
  } from "./constants.js";
12
15
  import {
13
16
  decodeModuleBundle,
@@ -268,6 +271,9 @@ function normalizeStandardEntries(options = {}) {
268
271
  description: "Transport envelope metadata.",
269
272
  });
270
273
  }
274
+ if (options.deploymentPlan !== undefined) {
275
+ entries.push(createDeploymentPlanBundleEntry(options.deploymentPlan));
276
+ }
271
277
  return entries.filter((entry) => entry.payload !== null);
272
278
  }
273
279
 
@@ -282,7 +288,12 @@ function normalizeAdditionalEntries(entries = []) {
282
288
  }
283
289
 
284
290
  async function withSha256(entry) {
285
- const payloadBytes = normalizeBytes(entry.payload, `entry "${entry.entryId}" payload`);
291
+ const payloadBytes =
292
+ toUint8Array(entry.payload) ??
293
+ ((entry.payloadEncoding === "json-utf8" ||
294
+ moduleBundleEncodingToName(entry.payloadEncoding) === "json-utf8")
295
+ ? canonicalBytes(entry.payload)
296
+ : normalizeBytes(entry.payload, `entry "${entry.entryId}" payload`));
286
297
  return {
287
298
  ...entry,
288
299
  payload: payloadBytes,
@@ -315,6 +326,17 @@ function buildParsedEntries(bundle) {
315
326
  parsedEntry.decodedManifest = null;
316
327
  }
317
328
  }
329
+ if (
330
+ parsedEntry.entryId === SDS_DEPLOYMENT_ENTRY_ID ||
331
+ parsedEntry.sectionName === SDS_DEPLOYMENT_SECTION_NAME
332
+ ) {
333
+ parsedEntry.decodedDeploymentPlan =
334
+ parsedEntry.payloadEncodingName === "json-utf8" &&
335
+ parsedEntry.decodedPayload &&
336
+ typeof parsedEntry.decodedPayload === "object"
337
+ ? parsedEntry.decodedPayload
338
+ : null;
339
+ }
318
340
  return parsedEntry;
319
341
  });
320
342
  }
@@ -432,16 +454,22 @@ export async function parseSingleFileBundle(bytes, options = {}) {
432
454
  manifest = null;
433
455
  }
434
456
  }
457
+ const deploymentEntry =
458
+ parsedEntries.find(
459
+ (entry) =>
460
+ entry.entryId === SDS_DEPLOYMENT_ENTRY_ID ||
461
+ entry.sectionName === SDS_DEPLOYMENT_SECTION_NAME,
462
+ ) ?? null;
435
463
  return {
436
464
  wasmBytes,
437
465
  bundleBytes,
438
466
  bundle,
439
467
  entries: parsedEntries,
440
468
  manifest,
469
+ deploymentPlan: deploymentEntry?.decodedDeploymentPlan ?? null,
441
470
  customSections,
442
471
  canonicalWasmBytes: canonical.canonicalWasmBytes,
443
472
  canonicalModuleHash: canonical.hashBytes,
444
473
  canonicalModuleHashHex: canonical.hashHex,
445
474
  };
446
475
  }
447
-
@@ -1,14 +1,10 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
  import {
4
- mkdir,
5
4
  mkdtemp,
6
- readFile,
7
5
  rm,
8
6
  writeFile,
9
7
  } from "node:fs/promises";
10
- import { execFile as execFileCallback } from "node:child_process";
11
- import { promisify } from "node:util";
12
8
 
13
9
  import {
14
10
  createDeploymentAuthorization,
@@ -43,7 +39,6 @@ import {
43
39
  import { sha256Bytes } from "../utils/crypto.js";
44
40
  import { getWasmWallet } from "../utils/wasmCrypto.js";
45
41
 
46
- const execFile = promisify(execFileCallback);
47
42
  const C_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
48
43
 
49
44
  function selectCompiler(language) {
@@ -89,14 +84,6 @@ async function getInvokeCppSupportFiles() {
89
84
  return { runtimeHeaders, schemaHeaders };
90
85
  }
91
86
 
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
- }
99
-
100
87
  async function writeFilesToEmception(emception, rootDir, files) {
101
88
  for (const [relativePath, content] of Object.entries(files)) {
102
89
  const filePath = path.posix.join(rootDir, relativePath);
@@ -235,101 +222,6 @@ async function compileWithEmception(options = {}) {
235
222
  }
236
223
  }
237
224
 
238
- // ---------------------------------------------------------------------------
239
- // System Emscripten — fallback to emcc/em++ on PATH
240
- // ---------------------------------------------------------------------------
241
-
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;
255
- const tempDir = await mkdtemp(
256
- path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
257
- );
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");
265
- const resolvedOutputPath = path.resolve(
266
- outputPath ?? path.join(tempDir, "module.wasm"),
267
- );
268
- const runtimeIncludeDir = path.join(tempDir, "flatbuffers-runtime");
269
-
270
- const { runtimeHeaders, schemaHeaders } = await getInvokeCppSupportFiles();
271
-
272
- await writeFile(sourcePath, sourceCode, "utf8");
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);
278
-
279
- const args = buildCompilerArgs(exportedSymbols, compileOptions);
280
-
281
- try {
282
- await execFile(sourceCompilerCommand, [
283
- "-c",
284
- sourcePath,
285
- `-I${tempDir}`,
286
- "-o",
287
- sourceObjectPath,
288
- ], { timeout: 120_000 });
289
-
290
- await execFile(compilerCommand, [
291
- "-c",
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,
314
- ...args,
315
- "-o",
316
- resolvedOutputPath,
317
- ], { timeout: 120_000 });
318
- } catch (error) {
319
- error.message =
320
- `Compilation failed with ${compilerCommand}: ` +
321
- (error.stderr || error.message);
322
- throw error;
323
- }
324
-
325
- const wasmBytes = await readFile(resolvedOutputPath);
326
- return { wasmBytes, outputPath: resolvedOutputPath, tempDir };
327
- }
328
-
329
- // ---------------------------------------------------------------------------
330
- // Public API
331
- // ---------------------------------------------------------------------------
332
-
333
225
  export async function compileModuleFromSource(options = {}) {
334
226
  const manifest = options.manifest ?? {};
335
227
  const sourceCode = String(options.sourceCode ?? "");
@@ -382,38 +274,17 @@ export async function compileModuleFromSource(options = {}) {
382
274
  ...options,
383
275
  noEntry: includeCommandMain !== true,
384
276
  };
385
- let compilerBackend = "em++ (emception)";
386
- let result;
387
- try {
388
- result = await compileWithEmception({
389
- sourceCompilerCommand: compiler.command,
390
- sourceExtension: compiler.extension,
391
- sourceCode,
392
- manifestSource,
393
- invokeHeaderSource,
394
- invokeSource,
395
- exportedSymbols,
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,
407
- sourceCode,
408
- manifestSource,
409
- invokeHeaderSource,
410
- invokeSource,
411
- exportedSymbols,
412
- outputPath: options.outputPath,
413
- compileOptions,
414
- });
415
- compilerBackend = "em++ (system)";
416
- }
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
+ });
417
288
  wasmBytes = result.wasmBytes;
418
289
  resolvedOutputPath = result.outputPath;
419
290
  tempDir = result.tempDir;
@@ -425,7 +296,7 @@ export async function compileModuleFromSource(options = {}) {
425
296
  });
426
297
 
427
298
  return {
428
- compiler: compilerBackend,
299
+ compiler: "em++ (emception)",
429
300
  language: compiler.language,
430
301
  outputPath: resolvedOutputPath,
431
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
+ }