space-data-module-sdk 0.2.5 → 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.
package/README.md CHANGED
@@ -294,8 +294,11 @@ npm run check:compliance
294
294
  ```
295
295
 
296
296
  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.
297
+ by default.
298
+
299
+ If another repo needs the same compiler runtime, the package also exposes a
300
+ shared emception session at `space-data-module-sdk/compiler/emception` with
301
+ helpers for serialized command execution and virtual filesystem access.
299
302
 
300
303
  ## License
301
304
 
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.6",
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,10 @@
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",
22
23
  "./invoke": "./src/invoke/index.js",
24
+ "./runtime": "./src/runtime/index.js",
23
25
  "./standards": "./src/standards/index.js",
24
26
  "./schemas/*": "./schemas/*"
25
27
  },
@@ -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
+ }
@@ -16,8 +16,6 @@ const FILE_URL_FETCH_PATCH_FLAG =
16
16
  "__spaceDataModuleSdkFileUrlFetchPatched";
17
17
 
18
18
  let patchedEmceptionRootPromise = null;
19
- let emceptionInstancePromise = null;
20
- let emceptionExecutionQueue = Promise.resolve();
21
19
 
22
20
  function installNodeRuntimeShims() {
23
21
  if (typeof globalThis.require !== "function") {
@@ -175,43 +173,62 @@ async function preparePatchedEmceptionRoot() {
175
173
  return patchedEmceptionRootPromise;
176
174
  }
177
175
 
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,
176
+ class EmceptionController {
177
+ #instancePromise = null;
178
+ #executionQueue = Promise.resolve();
179
+
180
+ async load() {
181
+ if (!this.#instancePromise) {
182
+ this.#instancePromise = (async () => {
183
+ installNodeRuntimeShims();
184
+ const patchedRoot = await preparePatchedEmceptionRoot();
185
+ const moduleUrl = pathToFileURL(path.join(patchedRoot, "emception.mjs")).href;
186
+ const { default: Emception } = await import(moduleUrl);
187
+ const emception = new Emception({
188
+ baseUrl: pathToFileURL(`${patchedRoot}${path.sep}`).href,
189
+ });
190
+ await emception.init();
191
+ return emception;
192
+ })().catch((error) => {
193
+ this.#instancePromise = null;
194
+ throw error;
187
195
  });
188
- await emception.init();
189
- return emception;
190
- })().catch((error) => {
191
- emceptionInstancePromise = null;
192
- throw error;
196
+ }
197
+
198
+ return this.#instancePromise;
199
+ }
200
+
201
+ async withLock(task) {
202
+ const previous = this.#executionQueue;
203
+ let release = () => {};
204
+ this.#executionQueue = new Promise((resolve) => {
205
+ release = resolve;
193
206
  });
207
+ await previous.catch(() => {});
208
+ try {
209
+ const emception = await this.load().catch((error) => {
210
+ if (!error.code) {
211
+ error.code = "EMCEPTION_LOAD_FAILED";
212
+ }
213
+ throw error;
214
+ });
215
+ return await task(emception);
216
+ } finally {
217
+ release();
218
+ }
194
219
  }
220
+ }
221
+
222
+ const sharedEmceptionController = new EmceptionController();
195
223
 
196
- return emceptionInstancePromise;
224
+ export function getSharedEmceptionController() {
225
+ return sharedEmceptionController;
226
+ }
227
+
228
+ export async function loadEmception() {
229
+ return sharedEmceptionController.load();
197
230
  }
198
231
 
199
232
  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
- }
233
+ return sharedEmceptionController.withLock(task);
217
234
  }
@@ -0,0 +1,24 @@
1
+ export type {
2
+ CompilationResult,
3
+ ProtectedArtifact,
4
+ } from "../index.js";
5
+
6
+ export {
7
+ cleanupCompilation,
8
+ compileModuleFromSource,
9
+ createRecipientKeypairHex,
10
+ protectModuleArtifact,
11
+ } from "../index.js";
12
+
13
+ export type {
14
+ EmceptionCommandResult,
15
+ SharedEmceptionFileContent,
16
+ SharedEmceptionHandle,
17
+ SharedEmceptionSession,
18
+ } from "./emception.js";
19
+
20
+ export {
21
+ createSharedEmceptionSession,
22
+ loadSharedEmception,
23
+ withSharedEmception,
24
+ } from "./emception.js";
@@ -5,3 +5,8 @@ export {
5
5
  protectModuleArtifact,
6
6
  } from "./compileModule.js";
7
7
 
8
+ export {
9
+ createSharedEmceptionSession,
10
+ loadSharedEmception,
11
+ withSharedEmception,
12
+ } from "./emception.js";
package/src/index.d.ts CHANGED
@@ -408,6 +408,19 @@ export function createRecipientKeypairHex(): Promise<{
408
408
  privateKeyHex: string;
409
409
  }>;
410
410
 
411
+ export type {
412
+ EmceptionCommandResult,
413
+ SharedEmceptionFileContent,
414
+ SharedEmceptionHandle,
415
+ SharedEmceptionSession,
416
+ } from "./compiler/emception.js";
417
+
418
+ export {
419
+ createSharedEmceptionSession,
420
+ loadSharedEmception,
421
+ withSharedEmception,
422
+ } from "./compiler/emception.js";
423
+
411
424
  // --- Standards ---
412
425
 
413
426
  export interface StandardsEntry {
@@ -0,0 +1,13 @@
1
+ export {
2
+ DefaultInvokeExports,
3
+ DefaultManifestExports,
4
+ DrainPolicy,
5
+ ExternalInterfaceDirection,
6
+ ExternalInterfaceKind,
7
+ InvokeSurface,
8
+ RuntimeTarget,
9
+ } from "../index.js";
10
+
11
+ export function isArrayBufferLike(value: unknown): boolean;
12
+ export function hasByteAddressableBuffer(value: unknown): boolean;
13
+ export function toUint8Array(value: unknown): Uint8Array | null;
@@ -0,0 +1,2 @@
1
+ export * from "./bufferLike.js";
2
+ export * from "./constants.js";