space-data-module-sdk 0.5.15 → 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 +51 -0
- package/package.json +13 -4
- package/schemas/HostStorageAbi.fbs +53 -0
- package/src/browser.js +6 -0
- package/src/bundle/codec.js +1 -1
- package/src/compiler/compileModule.js +3 -0
- package/src/compliance/pluginCompliance.js +1 -2
- package/src/generated/orbpro/invoke/plugin-invoke-request.js +1 -1
- package/src/generated/orbpro/invoke/plugin-invoke-response.js +1 -1
- package/src/generated/orbpro/manifest/accepted-type-set.js +1 -1
- package/src/generated/orbpro/manifest/build-artifact.js +1 -1
- package/src/generated/orbpro/manifest/host-capability.js +1 -1
- package/src/generated/orbpro/manifest/method-manifest.js +1 -1
- package/src/generated/orbpro/manifest/plugin-manifest.js +1 -1
- package/src/generated/orbpro/manifest/port-manifest.js +1 -1
- package/src/generated/orbpro/manifest/protocol-spec.js +1 -1
- package/src/generated/orbpro/manifest/timer-spec.js +1 -1
- package/src/generated/orbpro/module/canonicalization-rule.js +1 -1
- package/src/generated/orbpro/module/module-bundle-entry.js +1 -1
- package/src/generated/orbpro/module/module-bundle.js +1 -1
- package/src/generated/orbpro/stream/flat-buffer-type-ref.js +1 -1
- package/src/generated/orbpro/stream/typed-arena-buffer.js +1 -1
- package/src/host/browserEdgeShims.js +355 -0
- package/src/host/browserHost.js +399 -0
- package/src/host/index.js +4 -0
- package/src/host/isomorphicLoader.js +101 -0
- package/src/host/wasiShim.js +275 -0
- package/src/index.d.ts +364 -0
- package/src/index.js +1 -0
- package/src/invoke/codec.js +1 -1
- package/src/manifest/codec.js +1 -1
- package/src/manifest/typeRefs.js +49 -12
- package/src/runtime-host/flatsqlRuntimeStore.js +217 -0
- package/src/runtime-host/index.js +21 -0
- package/src/runtime-host/moduleRegistry.js +92 -0
- package/src/runtime-host/runtimeRegionStore.js +245 -0
- package/src/testing/browserModuleHarness.js +275 -0
- package/src/testing/buildWasmEdgeRunner.js +41 -2
- package/src/testing/index.d.ts +173 -2
- package/src/testing/index.js +5 -0
- package/src/testing/moduleHarness.js +102 -1
- package/src/testing/native/wasmedge_emscripten_pthread_runner.c +2319 -209
- package/src/testing/processInvoke.js +250 -9
- package/src/testing/streamInvokeCodec.js +175 -0
- package/src/transport/records.js +19 -6
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser host adapter for the SDN host contract.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the NodeHost public interface using browser-native Web APIs.
|
|
5
|
+
* Plugs into createJsonHostcallBridge() exactly like NodeHost does.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RuntimeTarget } from "../runtime/constants.js";
|
|
9
|
+
import {
|
|
10
|
+
parseCronExpression,
|
|
11
|
+
matchesCronExpression,
|
|
12
|
+
nextCronOccurrence,
|
|
13
|
+
} from "./cron.js";
|
|
14
|
+
import { createBrowserEdgeShims } from "./browserEdgeShims.js";
|
|
15
|
+
|
|
16
|
+
export const BrowserHostSupportedCapabilities = Object.freeze([
|
|
17
|
+
"clock",
|
|
18
|
+
"random",
|
|
19
|
+
"timers",
|
|
20
|
+
"schedule_cron",
|
|
21
|
+
"http",
|
|
22
|
+
"websocket",
|
|
23
|
+
"filesystem",
|
|
24
|
+
"context_read",
|
|
25
|
+
"context_write",
|
|
26
|
+
"crypto_hash",
|
|
27
|
+
"crypto_encrypt",
|
|
28
|
+
"crypto_decrypt",
|
|
29
|
+
"logging",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export const BrowserHostSupportedOperations = Object.freeze([
|
|
33
|
+
"host.runtimeTarget",
|
|
34
|
+
"host.listCapabilities",
|
|
35
|
+
"host.listSupportedCapabilities",
|
|
36
|
+
"host.listOperations",
|
|
37
|
+
"host.hasCapability",
|
|
38
|
+
"clock.now",
|
|
39
|
+
"clock.monotonicNow",
|
|
40
|
+
"clock.nowIso",
|
|
41
|
+
"random.bytes",
|
|
42
|
+
"schedule.parse",
|
|
43
|
+
"schedule.matches",
|
|
44
|
+
"schedule.next",
|
|
45
|
+
"http.request",
|
|
46
|
+
"websocket.exchange",
|
|
47
|
+
"filesystem.resolvePath",
|
|
48
|
+
"filesystem.readFile",
|
|
49
|
+
"filesystem.writeFile",
|
|
50
|
+
"filesystem.appendFile",
|
|
51
|
+
"filesystem.deleteFile",
|
|
52
|
+
"filesystem.mkdir",
|
|
53
|
+
"filesystem.readdir",
|
|
54
|
+
"filesystem.stat",
|
|
55
|
+
"filesystem.rename",
|
|
56
|
+
"context.get",
|
|
57
|
+
"context.set",
|
|
58
|
+
"context.delete",
|
|
59
|
+
"context.listKeys",
|
|
60
|
+
"context.listScopes",
|
|
61
|
+
"crypto.sha256",
|
|
62
|
+
"crypto.sha512",
|
|
63
|
+
"crypto.aesGcmEncrypt",
|
|
64
|
+
"crypto.aesGcmDecrypt",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export class BrowserHostCapabilityError extends Error {
|
|
68
|
+
constructor(capability, operation, message) {
|
|
69
|
+
super(message ?? `Capability "${capability}" is not available in this host.`);
|
|
70
|
+
this.name = "BrowserHostCapabilityError";
|
|
71
|
+
this.capability = capability;
|
|
72
|
+
this.operation = operation;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class BrowserHost {
|
|
77
|
+
constructor(options = {}) {
|
|
78
|
+
this.runtimeTarget = RuntimeTarget.BROWSER;
|
|
79
|
+
|
|
80
|
+
const granted = options.capabilities
|
|
81
|
+
? new Set(options.capabilities)
|
|
82
|
+
: new Set(BrowserHostSupportedCapabilities);
|
|
83
|
+
const edgeShims = createBrowserEdgeShims({
|
|
84
|
+
...options.edgeShims,
|
|
85
|
+
fetch: options.fetch ?? options.edgeShims?.fetch,
|
|
86
|
+
WebSocket: options.WebSocket ?? options.edgeShims?.WebSocket,
|
|
87
|
+
crypto: options.crypto ?? options.edgeShims?.crypto,
|
|
88
|
+
performance: options.performance ?? options.edgeShims?.performance,
|
|
89
|
+
filesystem: options.filesystem ?? options.edgeShims?.filesystem,
|
|
90
|
+
filesystemRoot: options.filesystemRoot ?? options.edgeShims?.filesystemRoot,
|
|
91
|
+
});
|
|
92
|
+
const performanceApi = edgeShims.performance ?? {
|
|
93
|
+
now: () => Date.now(),
|
|
94
|
+
timeOrigin: 0,
|
|
95
|
+
};
|
|
96
|
+
const cryptoApi = edgeShims.crypto;
|
|
97
|
+
const fetchImpl = edgeShims.fetch;
|
|
98
|
+
const WebSocketImpl = edgeShims.WebSocket;
|
|
99
|
+
const filesystem = edgeShims.filesystem;
|
|
100
|
+
|
|
101
|
+
this._grantedCapabilities = granted;
|
|
102
|
+
this._contextStore = options.contextStore ?? new Map();
|
|
103
|
+
this.filesystemRoot = filesystem?.filesystemRoot ?? "/";
|
|
104
|
+
|
|
105
|
+
// --- Capability objects (frozen, browser-native) ---
|
|
106
|
+
|
|
107
|
+
this.clock = Object.freeze({
|
|
108
|
+
now: () => {
|
|
109
|
+
this.#assertCapability("clock", "clock.now");
|
|
110
|
+
return Date.now();
|
|
111
|
+
},
|
|
112
|
+
monotonicNow: () => {
|
|
113
|
+
this.#assertCapability("clock", "clock.monotonicNow");
|
|
114
|
+
return performanceApi.now();
|
|
115
|
+
},
|
|
116
|
+
nowIso: () => {
|
|
117
|
+
this.#assertCapability("clock", "clock.nowIso");
|
|
118
|
+
return new Date().toISOString();
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.random = Object.freeze({
|
|
123
|
+
bytes: (length) => {
|
|
124
|
+
this.#assertCapability("random", "random.bytes");
|
|
125
|
+
if (!cryptoApi?.getRandomValues) {
|
|
126
|
+
throw new Error("No crypto.getRandomValues implementation is available.");
|
|
127
|
+
}
|
|
128
|
+
const len = Number(length) || 32;
|
|
129
|
+
const buf = new Uint8Array(len);
|
|
130
|
+
cryptoApi.getRandomValues(buf);
|
|
131
|
+
return buf;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.timers = Object.freeze({
|
|
136
|
+
delay: (ms) => {
|
|
137
|
+
this.#assertCapability("timers", "timers.delay");
|
|
138
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this.schedule = Object.freeze({
|
|
143
|
+
parse: (expression) => {
|
|
144
|
+
this.#assertCapability("schedule_cron", "schedule.parse");
|
|
145
|
+
return parseCronExpression(expression);
|
|
146
|
+
},
|
|
147
|
+
matches: (expression, date) => {
|
|
148
|
+
this.#assertCapability("schedule_cron", "schedule.matches");
|
|
149
|
+
return matchesCronExpression(expression, date ? new Date(date) : new Date());
|
|
150
|
+
},
|
|
151
|
+
next: (expression, from) => {
|
|
152
|
+
this.#assertCapability("schedule_cron", "schedule.next");
|
|
153
|
+
return nextCronOccurrence(expression, from ? new Date(from) : new Date());
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.http = Object.freeze({
|
|
158
|
+
request: async (params) => {
|
|
159
|
+
this.#assertCapability("http", "http.request");
|
|
160
|
+
if (typeof fetchImpl !== "function") {
|
|
161
|
+
throw new Error("No fetch implementation is available for the browser host.");
|
|
162
|
+
}
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeout = params.timeoutMs
|
|
165
|
+
? setTimeout(() => controller.abort(), params.timeoutMs)
|
|
166
|
+
: null;
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetchImpl(params.url, {
|
|
169
|
+
method: params.method ?? "GET",
|
|
170
|
+
headers: params.headers ?? undefined,
|
|
171
|
+
body: params.body ?? undefined,
|
|
172
|
+
signal: controller.signal,
|
|
173
|
+
});
|
|
174
|
+
const responseType = params.responseType ?? "utf8";
|
|
175
|
+
let body;
|
|
176
|
+
if (responseType === "json") body = await response.json();
|
|
177
|
+
else if (responseType === "bytes") body = new Uint8Array(await response.arrayBuffer());
|
|
178
|
+
else body = await response.text();
|
|
179
|
+
return {
|
|
180
|
+
status: response.status,
|
|
181
|
+
statusText: response.statusText,
|
|
182
|
+
ok: response.ok,
|
|
183
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
184
|
+
body,
|
|
185
|
+
};
|
|
186
|
+
} finally {
|
|
187
|
+
if (timeout) clearTimeout(timeout);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
this.websocket = Object.freeze({
|
|
193
|
+
exchange: async (params) => {
|
|
194
|
+
this.#assertCapability("websocket", "websocket.exchange");
|
|
195
|
+
if (!WebSocketImpl) {
|
|
196
|
+
throw new Error("No WebSocket implementation is available for the browser host.");
|
|
197
|
+
}
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const ws = new WebSocketImpl(params.url, params.protocols ?? undefined);
|
|
200
|
+
const timeout = params.timeoutMs
|
|
201
|
+
? setTimeout(() => {
|
|
202
|
+
ws.close();
|
|
203
|
+
reject(new Error("WebSocket exchange timed out."));
|
|
204
|
+
}, params.timeoutMs)
|
|
205
|
+
: null;
|
|
206
|
+
|
|
207
|
+
ws.onopen = () => {
|
|
208
|
+
if (params.message != null) ws.send(params.message);
|
|
209
|
+
if (!params.expectResponse) {
|
|
210
|
+
if (timeout) clearTimeout(timeout);
|
|
211
|
+
ws.close();
|
|
212
|
+
resolve({
|
|
213
|
+
url: params.url,
|
|
214
|
+
protocol: ws.protocol ?? "",
|
|
215
|
+
extensions: ws.extensions ?? "",
|
|
216
|
+
closeCode: null,
|
|
217
|
+
closeReason: "",
|
|
218
|
+
body: null,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
ws.onmessage = (event) => {
|
|
223
|
+
if (timeout) clearTimeout(timeout);
|
|
224
|
+
ws.close();
|
|
225
|
+
resolve({
|
|
226
|
+
url: params.url,
|
|
227
|
+
protocol: ws.protocol ?? "",
|
|
228
|
+
extensions: ws.extensions ?? "",
|
|
229
|
+
closeCode: null,
|
|
230
|
+
closeReason: "",
|
|
231
|
+
body: event.data,
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
ws.onerror = (event) => {
|
|
235
|
+
if (timeout) clearTimeout(timeout);
|
|
236
|
+
reject(new Error(`WebSocket error: ${event.type}`));
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.context = Object.freeze({
|
|
243
|
+
get: (scope, key) => {
|
|
244
|
+
this.#assertCapability("context_read", "context.get");
|
|
245
|
+
const scopeMap = this._contextStore.get(scope);
|
|
246
|
+
return scopeMap ? (scopeMap.get(key) ?? null) : null;
|
|
247
|
+
},
|
|
248
|
+
set: (scope, key, value) => {
|
|
249
|
+
this.#assertCapability("context_write", "context.set");
|
|
250
|
+
if (!this._contextStore.has(scope)) this._contextStore.set(scope, new Map());
|
|
251
|
+
this._contextStore.get(scope).set(key, value);
|
|
252
|
+
},
|
|
253
|
+
delete: (scope, key) => {
|
|
254
|
+
this.#assertCapability("context_write", "context.delete");
|
|
255
|
+
const scopeMap = this._contextStore.get(scope);
|
|
256
|
+
if (scopeMap) scopeMap.delete(key);
|
|
257
|
+
},
|
|
258
|
+
listKeys: (scope) => {
|
|
259
|
+
this.#assertCapability("context_read", "context.listKeys");
|
|
260
|
+
const scopeMap = this._contextStore.get(scope);
|
|
261
|
+
return scopeMap ? [...scopeMap.keys()] : [];
|
|
262
|
+
},
|
|
263
|
+
listScopes: () => {
|
|
264
|
+
this.#assertCapability("context_read", "context.listScopes");
|
|
265
|
+
return [...this._contextStore.keys()];
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.crypto = Object.freeze({
|
|
270
|
+
sha256: async (data) => {
|
|
271
|
+
this.#assertCapability("crypto_hash", "crypto.sha256");
|
|
272
|
+
if (!cryptoApi?.subtle) {
|
|
273
|
+
throw new Error("No Web Crypto subtle implementation is available.");
|
|
274
|
+
}
|
|
275
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
276
|
+
return new Uint8Array(await cryptoApi.subtle.digest("SHA-256", bytes));
|
|
277
|
+
},
|
|
278
|
+
sha512: async (data) => {
|
|
279
|
+
this.#assertCapability("crypto_hash", "crypto.sha512");
|
|
280
|
+
if (!cryptoApi?.subtle) {
|
|
281
|
+
throw new Error("No Web Crypto subtle implementation is available.");
|
|
282
|
+
}
|
|
283
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
284
|
+
return new Uint8Array(await cryptoApi.subtle.digest("SHA-512", bytes));
|
|
285
|
+
},
|
|
286
|
+
aesGcmEncrypt: async (params) => {
|
|
287
|
+
this.#assertCapability("crypto_encrypt", "crypto.aesGcmEncrypt");
|
|
288
|
+
if (!cryptoApi?.subtle || !cryptoApi?.getRandomValues) {
|
|
289
|
+
throw new Error("No Web Crypto implementation is available.");
|
|
290
|
+
}
|
|
291
|
+
const key = await cryptoApi.subtle.importKey(
|
|
292
|
+
"raw",
|
|
293
|
+
params.key,
|
|
294
|
+
{ name: "AES-GCM" },
|
|
295
|
+
false,
|
|
296
|
+
["encrypt"],
|
|
297
|
+
);
|
|
298
|
+
const iv = params.iv ?? cryptoApi.getRandomValues(new Uint8Array(12));
|
|
299
|
+
const ciphertext = new Uint8Array(
|
|
300
|
+
await cryptoApi.subtle.encrypt({ name: "AES-GCM", iv }, key, params.plaintext),
|
|
301
|
+
);
|
|
302
|
+
return { ciphertext, iv };
|
|
303
|
+
},
|
|
304
|
+
aesGcmDecrypt: async (params) => {
|
|
305
|
+
this.#assertCapability("crypto_decrypt", "crypto.aesGcmDecrypt");
|
|
306
|
+
if (!cryptoApi?.subtle) {
|
|
307
|
+
throw new Error("No Web Crypto subtle implementation is available.");
|
|
308
|
+
}
|
|
309
|
+
const key = await cryptoApi.subtle.importKey(
|
|
310
|
+
"raw",
|
|
311
|
+
params.key,
|
|
312
|
+
{ name: "AES-GCM" },
|
|
313
|
+
false,
|
|
314
|
+
["decrypt"],
|
|
315
|
+
);
|
|
316
|
+
return new Uint8Array(
|
|
317
|
+
await cryptoApi.subtle.decrypt(
|
|
318
|
+
{ name: "AES-GCM", iv: params.iv },
|
|
319
|
+
key,
|
|
320
|
+
params.ciphertext,
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this.filesystem = Object.freeze({
|
|
327
|
+
resolvePath: (path) => {
|
|
328
|
+
this.#assertCapability("filesystem", "filesystem.resolvePath");
|
|
329
|
+
return filesystem.resolvePath(path);
|
|
330
|
+
},
|
|
331
|
+
readFile: async (path, options) => {
|
|
332
|
+
this.#assertCapability("filesystem", "filesystem.readFile");
|
|
333
|
+
return filesystem.readFile(path, options);
|
|
334
|
+
},
|
|
335
|
+
writeFile: async (path, value, options) => {
|
|
336
|
+
this.#assertCapability("filesystem", "filesystem.writeFile");
|
|
337
|
+
return filesystem.writeFile(path, value, options);
|
|
338
|
+
},
|
|
339
|
+
appendFile: async (path, value, options) => {
|
|
340
|
+
this.#assertCapability("filesystem", "filesystem.appendFile");
|
|
341
|
+
return filesystem.appendFile(path, value, options);
|
|
342
|
+
},
|
|
343
|
+
deleteFile: async (path) => {
|
|
344
|
+
this.#assertCapability("filesystem", "filesystem.deleteFile");
|
|
345
|
+
return filesystem.deleteFile(path);
|
|
346
|
+
},
|
|
347
|
+
mkdir: async (path, options) => {
|
|
348
|
+
this.#assertCapability("filesystem", "filesystem.mkdir");
|
|
349
|
+
return filesystem.mkdir(path, options);
|
|
350
|
+
},
|
|
351
|
+
readdir: async (path = ".") => {
|
|
352
|
+
this.#assertCapability("filesystem", "filesystem.readdir");
|
|
353
|
+
return filesystem.readdir(path);
|
|
354
|
+
},
|
|
355
|
+
stat: async (path) => {
|
|
356
|
+
this.#assertCapability("filesystem", "filesystem.stat");
|
|
357
|
+
return filesystem.stat(path);
|
|
358
|
+
},
|
|
359
|
+
rename: async (fromPath, toPath) => {
|
|
360
|
+
this.#assertCapability("filesystem", "filesystem.rename");
|
|
361
|
+
return filesystem.rename(fromPath, toPath);
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --- Public interface (mirrors NodeHost) ---
|
|
367
|
+
|
|
368
|
+
listCapabilities() {
|
|
369
|
+
return [...this._grantedCapabilities];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
listSupportedCapabilities() {
|
|
373
|
+
return [...BrowserHostSupportedCapabilities];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
listOperations() {
|
|
377
|
+
return [...BrowserHostSupportedOperations];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
hasCapability(capability) {
|
|
381
|
+
return this._grantedCapabilities.has(capability);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
assertCapability(capability, operation) {
|
|
385
|
+
this.#assertCapability(capability, operation);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// --- Private ---
|
|
389
|
+
|
|
390
|
+
#assertCapability(capability, operation) {
|
|
391
|
+
if (!this._grantedCapabilities.has(capability)) {
|
|
392
|
+
throw new BrowserHostCapabilityError(capability, operation);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function createBrowserHost(options = {}) {
|
|
398
|
+
return new BrowserHost(options);
|
|
399
|
+
}
|
package/src/host/index.js
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isomorphic module loader.
|
|
3
|
+
*
|
|
4
|
+
* Unified entry point that detects the runtime environment and artifact
|
|
5
|
+
* profile, then loads the module through the appropriate path:
|
|
6
|
+
* - Browser: createBrowserModuleHarness (WASI shim + optional sdn_host)
|
|
7
|
+
* - Node/WasmEdge: createModuleHarness (subprocess)
|
|
8
|
+
*
|
|
9
|
+
* The same compiled .wasm artifact works in both environments.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
createBrowserModuleHarness,
|
|
14
|
+
detectArtifactProfile,
|
|
15
|
+
} from "../testing/browserModuleHarness.js";
|
|
16
|
+
|
|
17
|
+
const isBrowser =
|
|
18
|
+
typeof globalThis.window !== "undefined" &&
|
|
19
|
+
typeof globalThis.document !== "undefined";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load a WASM module isomorphically.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {Uint8Array|ArrayBuffer|Response|string|WebAssembly.Module} options.wasmSource
|
|
26
|
+
* The WASM artifact — same binary for all runtimes.
|
|
27
|
+
* @param {Object} [options.host] - Host instance (BrowserHost or NodeHost).
|
|
28
|
+
* @param {string[]} [options.args] - WASI args.
|
|
29
|
+
* @param {Object} [options.env] - WASI environment variables.
|
|
30
|
+
* @param {string} [options.surface] - "direct" or "command".
|
|
31
|
+
* @param {Object} [options.runtimeHost] - Runtime host for row/region ops.
|
|
32
|
+
* @returns {Promise<Object>} Harness with invoke(), readManifest(), destroy().
|
|
33
|
+
*/
|
|
34
|
+
export async function loadModule(options = {}) {
|
|
35
|
+
if (isBrowser) {
|
|
36
|
+
return createBrowserModuleHarness(options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { createModuleHarness } = await import("../testing/moduleHarness.js");
|
|
40
|
+
const source = options.wasmSource;
|
|
41
|
+
if (typeof source !== "string") {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
"Server-side isomorphic loader expects a file path string for wasmSource.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const runtimeKind = options.runtimeKind ?? "wasmedge";
|
|
48
|
+
if (runtimeKind === "wasmedge") {
|
|
49
|
+
return createModuleHarness({
|
|
50
|
+
runtime: {
|
|
51
|
+
kind: "wasmedge",
|
|
52
|
+
wasmPath: source,
|
|
53
|
+
wasmEdgeBinary: options.wasmEdgeBinary,
|
|
54
|
+
wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
|
|
55
|
+
enableThreads: options.enableThreads,
|
|
56
|
+
env: options.env,
|
|
57
|
+
cwd: options.cwd,
|
|
58
|
+
hostProfile: options.hostProfile,
|
|
59
|
+
modules: options.modules,
|
|
60
|
+
defaultModuleId: options.defaultModuleId,
|
|
61
|
+
metadata: options.metadata,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return createModuleHarness({
|
|
67
|
+
runtime: {
|
|
68
|
+
kind: runtimeKind,
|
|
69
|
+
command: options.command ?? runtimeKind,
|
|
70
|
+
args: options.args ?? [source],
|
|
71
|
+
env: options.env,
|
|
72
|
+
cwd: options.cwd,
|
|
73
|
+
hostProfile: options.hostProfile,
|
|
74
|
+
modules: options.modules,
|
|
75
|
+
defaultModuleId: options.defaultModuleId,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Inspect a WASM module's artifact profile without instantiating it.
|
|
82
|
+
*
|
|
83
|
+
* @param {Uint8Array|ArrayBuffer|WebAssembly.Module} source
|
|
84
|
+
* @returns {Promise<{profile: string, exports: string[], imports: Array}>}
|
|
85
|
+
*/
|
|
86
|
+
export async function inspectModule(source) {
|
|
87
|
+
let wasmModule;
|
|
88
|
+
if (source instanceof WebAssembly.Module) {
|
|
89
|
+
wasmModule = source;
|
|
90
|
+
} else {
|
|
91
|
+
const bytes =
|
|
92
|
+
source instanceof ArrayBuffer ? new Uint8Array(source) : source;
|
|
93
|
+
wasmModule = await WebAssembly.compile(bytes);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const profile = detectArtifactProfile(wasmModule);
|
|
97
|
+
const exports = WebAssembly.Module.exports(wasmModule).map((e) => e.name);
|
|
98
|
+
const imports = WebAssembly.Module.imports(wasmModule);
|
|
99
|
+
|
|
100
|
+
return { profile, exports, imports };
|
|
101
|
+
}
|