space-data-module-sdk 0.5.18 → 0.5.19

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
@@ -189,6 +189,11 @@ the preferred server-side target when the guest needs network-oriented runtime
189
189
  features such as sockets or TLS. Plain `wasi` remains the strict portability
190
190
  baseline; `wasmedge` is the practical higher-capability target.
191
191
 
192
+ If a manifest declares `runtimeTargets: ["browser", "wasmedge"]`, this SDK
193
+ treats that as the explicit "one binary for both" profile. That pair now
194
+ defaults to a shared `single-thread` artifact so the compiled wasm can be loaded
195
+ unchanged by the browser harness and the WasmEdge harness.
196
+
192
197
  ## WasmEdge Pthreads
193
198
 
194
199
  `space-data-module-sdk` is also the source of truth for module thread-model
@@ -197,6 +202,7 @@ selection.
197
202
  - `compileModuleFromSource({ threadModel })` accepts an explicit thread model.
198
203
  - If `threadModel` is omitted, the SDK resolves it from `manifest.runtimeTargets`.
199
204
  - `runtimeTargets: ["wasmedge"]` defaults to `emscripten-pthreads`.
205
+ - `runtimeTargets: ["browser", "wasmedge"]` defaults to `single-thread`.
200
206
  - Other targets currently default to `single-thread`.
201
207
 
202
208
  WasmEdge-targeted pthread builds do not use the embedded `sdn-emception`
@@ -214,11 +220,32 @@ If a runtime cannot interoperate with the guest pthread contract directly,
214
220
  document that as a wrapper requirement instead of changing the guest artifact
215
221
  semantics.
216
222
 
223
+ ## Browser + WasmEdge Isomorphism
224
+
225
+ The supported isomorphic profile and browser edge shims are documented in
226
+ [`docs/browser-wasmedge-isomorphic.md`](./docs/browser-wasmedge-isomorphic.md).
227
+
228
+ The checked-in same-artifact demo lives in
229
+ [`examples/isomorphic-loader`](./examples/isomorphic-loader):
230
+
231
+ - [`build-demo.mjs`](./examples/isomorphic-loader/build-demo.mjs) compiles the
232
+ shared artifact
233
+ - [`browser-demo.html`](./examples/isomorphic-loader/browser-demo.html) and
234
+ [`browser-demo.mjs`](./examples/isomorphic-loader/browser-demo.mjs) load that
235
+ artifact in the browser harness with browser edge shims
236
+ - [`wasmedge-demo.mjs`](./examples/isomorphic-loader/wasmedge-demo.mjs) loads
237
+ that same artifact in WasmEdge
238
+
217
239
  ## Testing
218
240
 
219
241
  This repo now exposes a manifest-driven harness generator from
220
242
  `space-data-module-sdk/testing` and two complementary integration suites:
221
243
 
244
+ - browser/isomorphic helpers:
245
+ - `createBrowserModuleHarness(...)`
246
+ - `detectArtifactProfile(...)`
247
+ - `loadModule(...)`
248
+
222
249
  - shared process-level helpers for command-surface runtimes:
223
250
  - `createPluginInvokeProcessClient(...)`
224
251
  - `resolveWasmEdgePluginLaunchPlan(...)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.5.18",
3
+ "version": "0.5.19",
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",
@@ -35,6 +35,11 @@
35
35
  "types": "./src/index.d.ts",
36
36
  "default": "./src/runtime-host/index.js"
37
37
  },
38
+ "./host/browser": "./src/host/browserHost.js",
39
+ "./host/browser-edge-shims": "./src/host/browserEdgeShims.js",
40
+ "./host/wasi-shim": "./src/host/wasiShim.js",
41
+ "./host/isomorphic": "./src/host/isomorphicLoader.js",
42
+ "./testing/browser": "./src/testing/browserModuleHarness.js",
38
43
  "./testing": "./src/testing/index.js",
39
44
  "./standards": "./src/standards/index.js",
40
45
  "./schemas/*": "./schemas/*"
package/src/browser.js CHANGED
@@ -6,3 +6,9 @@ export * from "./capabilities.js";
6
6
  export * from "./deployment/index.js";
7
7
  export * from "./invoke/index.js";
8
8
  export * from "./runtime/index.js";
9
+ export * from "./host/browserHost.js";
10
+ export * from "./host/browserEdgeShims.js";
11
+ export * from "./host/wasiShim.js";
12
+ export * from "./host/abi.js";
13
+ export * from "./host/isomorphicLoader.js";
14
+ export { createBrowserModuleHarness, detectArtifactProfile } from "./testing/browserModuleHarness.js";
@@ -142,6 +142,9 @@ function resolveThreadModel({ manifest, threadModel } = {}) {
142
142
  .map((target) => String(target ?? "").trim().toLowerCase())
143
143
  .filter(Boolean)
144
144
  : [];
145
+ if (runtimeTargets.includes(RuntimeTarget.BROWSER)) {
146
+ return ModuleThreadModel.SINGLE_THREAD;
147
+ }
145
148
  if (runtimeTargets.includes(RuntimeTarget.WASMEDGE)) {
146
149
  return ModuleThreadModel.EMSCRIPTEN_PTHREADS;
147
150
  }
@@ -30,7 +30,6 @@ const ExternalInterfaceKindSet = new Set(Object.values(ExternalInterfaceKind));
30
30
  const ProtocolRoleSet = new Set(Object.values(ProtocolRole));
31
31
  const ProtocolTransportKindSet = new Set(Object.values(ProtocolTransportKind));
32
32
  const BrowserIncompatibleCapabilitySet = new Set([
33
- "filesystem",
34
33
  "pipe",
35
34
  "network",
36
35
  "tcp",
@@ -0,0 +1,355 @@
1
+ const textEncoder = new TextEncoder();
2
+
3
+ const FILE_ENTRY = "file";
4
+ const DIRECTORY_ENTRY = "directory";
5
+
6
+ export class BrowserFilesystemScopeError extends Error {
7
+ constructor(requestedPath, filesystemRoot) {
8
+ super(`Path "${requestedPath}" escapes the configured filesystem root.`);
9
+ this.name = "BrowserFilesystemScopeError";
10
+ this.code = "filesystem-scope-violation";
11
+ this.requestedPath = requestedPath;
12
+ this.filesystemRoot = filesystemRoot;
13
+ }
14
+ }
15
+
16
+ function normalizePath(path) {
17
+ const raw = String(path ?? "").trim();
18
+ const absolute = raw.startsWith("/") ? raw : `/${raw}`;
19
+ const segments = [];
20
+ for (const segment of absolute.split("/")) {
21
+ if (!segment || segment === ".") {
22
+ continue;
23
+ }
24
+ if (segment === "..") {
25
+ if (segments.length === 0) {
26
+ throw new BrowserFilesystemScopeError(path, "/");
27
+ }
28
+ segments.pop();
29
+ continue;
30
+ }
31
+ segments.push(segment);
32
+ }
33
+ return segments.length === 0 ? "/" : `/${segments.join("/")}`;
34
+ }
35
+
36
+ function joinPaths(basePath, requestedPath) {
37
+ const base = normalizePath(basePath ?? "/");
38
+ const requested = String(requestedPath ?? "").trim();
39
+ if (!requested || requested === ".") {
40
+ return base;
41
+ }
42
+ if (requested.startsWith("/")) {
43
+ return normalizePath(requested);
44
+ }
45
+ return normalizePath(`${base}/${requested}`);
46
+ }
47
+
48
+ function assertWithinRoot(resolvedPath, filesystemRoot, requestedPath) {
49
+ const root = normalizePath(filesystemRoot ?? "/");
50
+ if (
51
+ resolvedPath !== root &&
52
+ root !== "/" &&
53
+ !resolvedPath.startsWith(`${root}/`)
54
+ ) {
55
+ throw new BrowserFilesystemScopeError(requestedPath, root);
56
+ }
57
+ }
58
+
59
+ function getParentPath(path) {
60
+ if (path === "/") {
61
+ return null;
62
+ }
63
+ const segments = path.split("/").filter(Boolean);
64
+ if (segments.length <= 1) {
65
+ return "/";
66
+ }
67
+ return `/${segments.slice(0, -1).join("/")}`;
68
+ }
69
+
70
+ function getBaseName(path) {
71
+ if (path === "/") {
72
+ return "/";
73
+ }
74
+ const segments = path.split("/").filter(Boolean);
75
+ return segments[segments.length - 1];
76
+ }
77
+
78
+ function cloneEntry(entry) {
79
+ if (!entry) {
80
+ return null;
81
+ }
82
+ if (entry.kind === FILE_ENTRY) {
83
+ return {
84
+ ...entry,
85
+ bytes: new Uint8Array(entry.bytes),
86
+ };
87
+ }
88
+ return { ...entry };
89
+ }
90
+
91
+ function toUint8Array(value, encoding = null) {
92
+ if (typeof value === "string") {
93
+ return new TextEncoder().encode(value);
94
+ }
95
+ if (value instanceof Uint8Array) {
96
+ return new Uint8Array(value);
97
+ }
98
+ if (ArrayBuffer.isView(value)) {
99
+ return new Uint8Array(
100
+ value.buffer,
101
+ value.byteOffset,
102
+ value.byteLength,
103
+ ).slice();
104
+ }
105
+ if (value instanceof ArrayBuffer) {
106
+ return new Uint8Array(value.slice(0));
107
+ }
108
+ if (value == null) {
109
+ return new Uint8Array();
110
+ }
111
+ if (encoding) {
112
+ return textEncoder.encode(String(value));
113
+ }
114
+ return textEncoder.encode(String(value));
115
+ }
116
+
117
+ export function createMemoryFilesystemEdgeShim(options = {}) {
118
+ const filesystemRoot = normalizePath(options.filesystemRoot ?? "/");
119
+ const entries = new Map();
120
+ const now = Date.now();
121
+ entries.set(filesystemRoot, {
122
+ kind: DIRECTORY_ENTRY,
123
+ ctimeMs: now,
124
+ mtimeMs: now,
125
+ });
126
+
127
+ function resolvePath(requestedPath = ".") {
128
+ const resolvedPath = joinPaths(filesystemRoot, requestedPath);
129
+ assertWithinRoot(resolvedPath, filesystemRoot, requestedPath);
130
+ return resolvedPath;
131
+ }
132
+
133
+ function getEntry(path) {
134
+ return entries.get(path) ?? null;
135
+ }
136
+
137
+ function requireEntry(path) {
138
+ const entry = getEntry(path);
139
+ if (!entry) {
140
+ throw new Error(`Path "${path}" does not exist.`);
141
+ }
142
+ return entry;
143
+ }
144
+
145
+ function requireDirectory(path) {
146
+ const entry = requireEntry(path);
147
+ if (entry.kind !== DIRECTORY_ENTRY) {
148
+ throw new Error(`Path "${path}" is not a directory.`);
149
+ }
150
+ return entry;
151
+ }
152
+
153
+ function requireParentDirectory(path) {
154
+ const parentPath = getParentPath(path);
155
+ if (!parentPath) {
156
+ return null;
157
+ }
158
+ return requireDirectory(parentPath);
159
+ }
160
+
161
+ function touch(path) {
162
+ const entry = requireEntry(path);
163
+ entry.mtimeMs = Date.now();
164
+ }
165
+
166
+ function ensureDirectory(path, recursive = false) {
167
+ const resolvedPath = resolvePath(path);
168
+ const existing = getEntry(resolvedPath);
169
+ if (existing) {
170
+ if (existing.kind !== DIRECTORY_ENTRY) {
171
+ throw new Error(`Path "${resolvedPath}" already exists and is not a directory.`);
172
+ }
173
+ return resolvedPath;
174
+ }
175
+
176
+ const parentPath = getParentPath(resolvedPath);
177
+ if (parentPath && !getEntry(parentPath)) {
178
+ if (!recursive) {
179
+ throw new Error(`Parent directory "${parentPath}" does not exist.`);
180
+ }
181
+ ensureDirectory(parentPath, true);
182
+ }
183
+ requireParentDirectory(resolvedPath);
184
+ const nowMs = Date.now();
185
+ entries.set(resolvedPath, {
186
+ kind: DIRECTORY_ENTRY,
187
+ ctimeMs: nowMs,
188
+ mtimeMs: nowMs,
189
+ });
190
+ if (parentPath) {
191
+ touch(parentPath);
192
+ }
193
+ return resolvedPath;
194
+ }
195
+
196
+ function setFileBytes(resolvedPath, bytes) {
197
+ const parentPath = getParentPath(resolvedPath);
198
+ if (parentPath) {
199
+ requireDirectory(parentPath);
200
+ }
201
+ const existing = getEntry(resolvedPath);
202
+ const nowMs = Date.now();
203
+ entries.set(resolvedPath, {
204
+ kind: FILE_ENTRY,
205
+ bytes,
206
+ ctimeMs: existing?.ctimeMs ?? nowMs,
207
+ mtimeMs: nowMs,
208
+ });
209
+ if (parentPath) {
210
+ touch(parentPath);
211
+ }
212
+ }
213
+
214
+ return Object.freeze({
215
+ filesystemRoot,
216
+ resolvePath,
217
+ async readFile(path, options = {}) {
218
+ const resolvedPath = resolvePath(path);
219
+ const entry = requireEntry(resolvedPath);
220
+ if (entry.kind !== FILE_ENTRY) {
221
+ throw new Error(`Path "${resolvedPath}" is not a file.`);
222
+ }
223
+ if (options.encoding) {
224
+ return new TextDecoder(options.encoding).decode(entry.bytes);
225
+ }
226
+ return new Uint8Array(entry.bytes);
227
+ },
228
+ async writeFile(path, value, options = {}) {
229
+ const resolvedPath = resolvePath(path);
230
+ setFileBytes(resolvedPath, toUint8Array(value, options.encoding ?? null));
231
+ return { path: resolvedPath };
232
+ },
233
+ async appendFile(path, value, options = {}) {
234
+ const resolvedPath = resolvePath(path);
235
+ const existing = getEntry(resolvedPath);
236
+ const nextBytes = toUint8Array(value, options.encoding ?? null);
237
+ if (!existing) {
238
+ setFileBytes(resolvedPath, nextBytes);
239
+ return { path: resolvedPath };
240
+ }
241
+ if (existing.kind !== FILE_ENTRY) {
242
+ throw new Error(`Path "${resolvedPath}" is not a file.`);
243
+ }
244
+ const combined = new Uint8Array(existing.bytes.length + nextBytes.length);
245
+ combined.set(existing.bytes, 0);
246
+ combined.set(nextBytes, existing.bytes.length);
247
+ setFileBytes(resolvedPath, combined);
248
+ return { path: resolvedPath };
249
+ },
250
+ async deleteFile(path) {
251
+ const resolvedPath = resolvePath(path);
252
+ const entry = requireEntry(resolvedPath);
253
+ if (entry.kind !== FILE_ENTRY) {
254
+ throw new Error(`Path "${resolvedPath}" is not a file.`);
255
+ }
256
+ entries.delete(resolvedPath);
257
+ const parentPath = getParentPath(resolvedPath);
258
+ if (parentPath) {
259
+ touch(parentPath);
260
+ }
261
+ return { path: resolvedPath };
262
+ },
263
+ async mkdir(path, options = {}) {
264
+ const resolvedPath = ensureDirectory(path, options.recursive === true);
265
+ return { path: resolvedPath };
266
+ },
267
+ async readdir(path = ".") {
268
+ const resolvedPath = resolvePath(path);
269
+ requireDirectory(resolvedPath);
270
+ const children = [];
271
+ for (const [entryPath, entry] of entries.entries()) {
272
+ if (entryPath === resolvedPath) {
273
+ continue;
274
+ }
275
+ const parentPath = getParentPath(entryPath);
276
+ if (parentPath !== resolvedPath) {
277
+ continue;
278
+ }
279
+ children.push({
280
+ name: getBaseName(entryPath),
281
+ isFile: entry.kind === FILE_ENTRY,
282
+ isDirectory: entry.kind === DIRECTORY_ENTRY,
283
+ });
284
+ }
285
+ children.sort((left, right) => left.name.localeCompare(right.name));
286
+ return children;
287
+ },
288
+ async stat(path) {
289
+ const resolvedPath = resolvePath(path);
290
+ const entry = requireEntry(resolvedPath);
291
+ return {
292
+ path: resolvedPath,
293
+ size: entry.kind === FILE_ENTRY ? entry.bytes.length : 0,
294
+ isFile: entry.kind === FILE_ENTRY,
295
+ isDirectory: entry.kind === DIRECTORY_ENTRY,
296
+ ctimeMs: entry.ctimeMs,
297
+ mtimeMs: entry.mtimeMs,
298
+ };
299
+ },
300
+ async rename(fromPath, toPath) {
301
+ const resolvedFromPath = resolvePath(fromPath);
302
+ const resolvedToPath = resolvePath(toPath);
303
+ requireEntry(resolvedFromPath);
304
+ if (getEntry(resolvedToPath)) {
305
+ throw new Error(`Path "${resolvedToPath}" already exists.`);
306
+ }
307
+ const targetParentPath = getParentPath(resolvedToPath);
308
+ if (targetParentPath) {
309
+ requireDirectory(targetParentPath);
310
+ }
311
+
312
+ const moves = Array.from(entries.entries())
313
+ .filter(([entryPath]) =>
314
+ entryPath === resolvedFromPath ||
315
+ entryPath.startsWith(`${resolvedFromPath}/`),
316
+ )
317
+ .sort((left, right) => left[0].length - right[0].length);
318
+
319
+ for (const [entryPath, entry] of moves) {
320
+ const suffix = entryPath.slice(resolvedFromPath.length);
321
+ entries.set(`${resolvedToPath}${suffix}`, cloneEntry(entry));
322
+ }
323
+ for (const [entryPath] of moves) {
324
+ entries.delete(entryPath);
325
+ }
326
+
327
+ if (targetParentPath) {
328
+ touch(targetParentPath);
329
+ }
330
+ const fromParentPath = getParentPath(resolvedFromPath);
331
+ if (fromParentPath && fromParentPath !== targetParentPath) {
332
+ touch(fromParentPath);
333
+ }
334
+
335
+ return {
336
+ from: resolvedFromPath,
337
+ to: resolvedToPath,
338
+ };
339
+ },
340
+ });
341
+ }
342
+
343
+ export function createBrowserEdgeShims(options = {}) {
344
+ return Object.freeze({
345
+ fetch: options.fetch ?? globalThis.fetch?.bind(globalThis),
346
+ WebSocket: options.WebSocket ?? globalThis.WebSocket,
347
+ crypto: options.crypto ?? globalThis.crypto,
348
+ performance: options.performance ?? globalThis.performance,
349
+ filesystem:
350
+ options.filesystem ??
351
+ createMemoryFilesystemEdgeShim({
352
+ filesystemRoot: options.filesystemRoot ?? "/",
353
+ }),
354
+ });
355
+ }