space-data-module-sdk 0.5.14 → 0.5.18

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 (37) hide show
  1. package/README.md +91 -1
  2. package/package.json +8 -4
  3. package/schemas/HostStorageAbi.fbs +53 -0
  4. package/src/bundle/codec.js +1 -1
  5. package/src/compliance/pluginCompliance.js +0 -130
  6. package/src/generated/orbpro/invoke/plugin-invoke-request.js +1 -1
  7. package/src/generated/orbpro/invoke/plugin-invoke-response.js +1 -1
  8. package/src/generated/orbpro/manifest/accepted-type-set.js +1 -1
  9. package/src/generated/orbpro/manifest/build-artifact.js +1 -1
  10. package/src/generated/orbpro/manifest/host-capability.js +1 -1
  11. package/src/generated/orbpro/manifest/method-manifest.js +1 -1
  12. package/src/generated/orbpro/manifest/plugin-manifest.js +1 -1
  13. package/src/generated/orbpro/manifest/port-manifest.js +1 -1
  14. package/src/generated/orbpro/manifest/protocol-spec.js +1 -1
  15. package/src/generated/orbpro/manifest/timer-spec.js +1 -1
  16. package/src/generated/orbpro/module/canonicalization-rule.js +1 -1
  17. package/src/generated/orbpro/module/module-bundle-entry.js +1 -1
  18. package/src/generated/orbpro/module/module-bundle.js +1 -1
  19. package/src/generated/orbpro/stream/flat-buffer-type-ref.js +1 -1
  20. package/src/generated/orbpro/stream/typed-arena-buffer.js +1 -1
  21. package/src/index.d.ts +157 -1
  22. package/src/index.js +1 -0
  23. package/src/invoke/codec.js +10 -8
  24. package/src/manifest/codec.js +1 -1
  25. package/src/runtime-host/flatsqlRuntimeStore.js +217 -0
  26. package/src/runtime-host/index.js +21 -0
  27. package/src/runtime-host/moduleRegistry.js +92 -0
  28. package/src/runtime-host/runtimeRegionStore.js +245 -0
  29. package/src/testing/buildWasmEdgeRunner.js +253 -0
  30. package/src/testing/index.d.ts +311 -0
  31. package/src/testing/index.js +21 -0
  32. package/src/testing/moduleHarness.js +163 -0
  33. package/src/testing/native/wasmedge_emscripten_pthread_runner.c +3111 -0
  34. package/src/testing/processInvoke.js +467 -0
  35. package/src/testing/publicationProtectionDemo.js +271 -0
  36. package/src/testing/streamInvokeCodec.js +175 -0
  37. package/src/transport/records.js +56 -55
@@ -0,0 +1,467 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Buffer } from "node:buffer";
3
+ import { once } from "node:events";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+
7
+ import {
8
+ decodePluginInvokeResponse,
9
+ encodePluginInvokeRequest,
10
+ } from "../invoke/index.js";
11
+ import { toUint8Array } from "../runtime/bufferLike.js";
12
+
13
+ const WASMEDGE_HOST_MAGIC = Uint8Array.from([0x4f, 0x52, 0x50, 0x57]); // ORPW
14
+ const textEncoder = new TextEncoder();
15
+ const textDecoder = new TextDecoder();
16
+ const HOST_CONTROL_OPCODE = Object.freeze({
17
+ INSTALL_MODULE: 16,
18
+ LIST_MODULES: 17,
19
+ UNLOAD_MODULE: 18,
20
+ INVOKE_MODULE: 19,
21
+ APPEND_ROW: 20,
22
+ LIST_ROWS: 21,
23
+ RESOLVE_ROW: 22,
24
+ ALLOCATE_REGION: 23,
25
+ DESCRIBE_REGION: 24,
26
+ RESOLVE_RECORD: 25,
27
+ QUERY_ROWS: 26,
28
+ });
29
+
30
+ function formatProcessFailure(message, stderrChunks = [], cause = null) {
31
+ const stderrText = Buffer.concat(stderrChunks).toString("utf8").trim();
32
+ const details = stderrText ? `${message}\n${stderrText}` : message;
33
+ return cause ? new Error(details, { cause }) : new Error(details);
34
+ }
35
+
36
+ function createLengthPrefixedRequest(bytes) {
37
+ const payload = Buffer.from(bytes);
38
+ const prefix = Buffer.allocUnsafe(4);
39
+ prefix.writeUInt32LE(payload.length, 0);
40
+ return Buffer.concat([prefix, payload]);
41
+ }
42
+
43
+ function normalizeLaunchPlan(options = {}) {
44
+ if (options.launchPlan) {
45
+ return {
46
+ ...options.launchPlan,
47
+ args: Array.isArray(options.launchPlan.args) ? options.launchPlan.args : [],
48
+ };
49
+ }
50
+ return {
51
+ command: options.command ?? null,
52
+ args: Array.isArray(options.args) ? options.args : [],
53
+ env: options.env ?? process.env,
54
+ cwd: options.cwd ?? process.cwd(),
55
+ };
56
+ }
57
+
58
+ export function buildWasmEdgeSpawnEnv(baseEnv = process.env) {
59
+ const env = { ...baseEnv };
60
+ delete env.DYLD_LIBRARY_PATH;
61
+ delete env.DYLD_FALLBACK_LIBRARY_PATH;
62
+ delete env.DYLD_FRAMEWORK_PATH;
63
+ delete env.DYLD_FALLBACK_FRAMEWORK_PATH;
64
+ delete env.LIBRARY_PATH;
65
+ return env;
66
+ }
67
+
68
+ export function resolveWasmEdgePluginLaunchPlan(options = {}) {
69
+ const hostProfile = String(options.hostProfile ?? "").trim().toLowerCase();
70
+ if (hostProfile === "runtime-host") {
71
+ if (
72
+ typeof options.wasmEdgeRunnerBinary !== "string" ||
73
+ options.wasmEdgeRunnerBinary.trim().length === 0
74
+ ) {
75
+ throw new Error(
76
+ "resolveWasmEdgePluginLaunchPlan requires wasmEdgeRunnerBinary for runtime-host mode.",
77
+ );
78
+ }
79
+ return {
80
+ command: options.wasmEdgeRunnerBinary,
81
+ args: ["--serve-runtime-host"],
82
+ env: buildWasmEdgeSpawnEnv(options.env),
83
+ wasmPath: null,
84
+ hostProfile: "runtime-host",
85
+ };
86
+ }
87
+
88
+ const wasmPath =
89
+ typeof options.wasmPath === "string" && options.wasmPath.trim().length > 0
90
+ ? path.resolve(options.wasmPath)
91
+ : null;
92
+ if (!wasmPath) {
93
+ throw new Error("resolveWasmEdgePluginLaunchPlan requires a wasmPath.");
94
+ }
95
+
96
+ const invokeArgs =
97
+ Array.isArray(options.invokeArgs) && options.invokeArgs.length > 0
98
+ ? [...options.invokeArgs]
99
+ : ["--serve-plugin-invoke"];
100
+
101
+ if (options.wasmEdgeRunnerBinary) {
102
+ return {
103
+ command: options.wasmEdgeRunnerBinary,
104
+ args: [wasmPath, ...invokeArgs],
105
+ env: buildWasmEdgeSpawnEnv(options.env),
106
+ wasmPath,
107
+ };
108
+ }
109
+
110
+ return {
111
+ command: options.wasmEdgeBinary ?? "wasmedge",
112
+ args: [
113
+ ...(options.enableThreads === false ? [] : ["--enable-threads"]),
114
+ wasmPath,
115
+ ...invokeArgs,
116
+ ],
117
+ env: buildWasmEdgeSpawnEnv(options.env),
118
+ wasmPath,
119
+ };
120
+ }
121
+
122
+ async function createLengthPrefixedProcessClient(options = {}) {
123
+ const launchPlan = normalizeLaunchPlan(options);
124
+ if (
125
+ typeof launchPlan.command !== "string" ||
126
+ launchPlan.command.trim().length === 0
127
+ ) {
128
+ throw new Error("createPluginInvokeProcessClient requires a command.");
129
+ }
130
+
131
+ const child = spawn(launchPlan.command, launchPlan.args, {
132
+ cwd: launchPlan.cwd ?? process.cwd(),
133
+ env: launchPlan.env ?? process.env,
134
+ stdio: ["pipe", "pipe", "pipe"],
135
+ });
136
+
137
+ let stdoutBuffer = Buffer.alloc(0);
138
+ const stderrChunks = [];
139
+ const pending = [];
140
+ let closed = false;
141
+ let closeError = null;
142
+ let expectedShutdown = false;
143
+
144
+ function rejectPending(error) {
145
+ while (pending.length > 0) {
146
+ pending.shift().reject(error);
147
+ }
148
+ }
149
+
150
+ function drainResponses() {
151
+ while (pending.length > 0 && stdoutBuffer.length >= 4) {
152
+ const responseLength = stdoutBuffer.readUInt32LE(0);
153
+ if (stdoutBuffer.length < 4 + responseLength) {
154
+ return;
155
+ }
156
+ const responseBytes = stdoutBuffer.subarray(4, 4 + responseLength);
157
+ stdoutBuffer = stdoutBuffer.subarray(4 + responseLength);
158
+ pending.shift().resolve(new Uint8Array(responseBytes));
159
+ }
160
+ }
161
+
162
+ child.stdout.on("data", (chunk) => {
163
+ stdoutBuffer = Buffer.concat([stdoutBuffer, Buffer.from(chunk)]);
164
+ drainResponses();
165
+ });
166
+ child.stderr.on("data", (chunk) => {
167
+ stderrChunks.push(Buffer.from(chunk));
168
+ });
169
+ child.on("error", (error) => {
170
+ closeError = formatProcessFailure(
171
+ "Failed to launch plugin invoke process.",
172
+ stderrChunks,
173
+ error,
174
+ );
175
+ rejectPending(closeError);
176
+ });
177
+
178
+ const closePromise = once(child, "close").then(([code, signal]) => {
179
+ closed = true;
180
+ if (!expectedShutdown && (code !== 0 || signal !== null)) {
181
+ closeError = formatProcessFailure(
182
+ `Plugin invoke process exited unexpectedly with ${
183
+ signal ? `signal ${signal}` : `code ${code}`
184
+ }.`,
185
+ stderrChunks,
186
+ );
187
+ rejectPending(closeError);
188
+ throw closeError;
189
+ }
190
+ if (!expectedShutdown && code !== 0) {
191
+ closeError = formatProcessFailure(
192
+ `Plugin invoke process exited with code ${code}.`,
193
+ stderrChunks,
194
+ );
195
+ rejectPending(closeError);
196
+ throw closeError;
197
+ }
198
+ });
199
+
200
+ async function invokeRaw(requestBytes) {
201
+ if (closeError) {
202
+ throw closeError;
203
+ }
204
+ if (closed) {
205
+ throw formatProcessFailure(
206
+ "Plugin invoke process is already closed.",
207
+ stderrChunks,
208
+ );
209
+ }
210
+
211
+ const normalizedRequest = toUint8Array(requestBytes);
212
+ if (!normalizedRequest) {
213
+ throw new TypeError(
214
+ "Expected Uint8Array, ArrayBufferView, or ArrayBuffer request bytes.",
215
+ );
216
+ }
217
+
218
+ return new Promise((resolve, reject) => {
219
+ pending.push({ resolve, reject });
220
+ child.stdin.write(createLengthPrefixedRequest(normalizedRequest), (error) => {
221
+ if (!error) {
222
+ return;
223
+ }
224
+ const pendingIndex = pending.findIndex((entry) => entry.resolve === resolve);
225
+ if (pendingIndex >= 0) {
226
+ pending.splice(pendingIndex, 1);
227
+ }
228
+ reject(
229
+ formatProcessFailure(
230
+ "Failed to send PluginInvokeRequest to child process.",
231
+ stderrChunks,
232
+ error,
233
+ ),
234
+ );
235
+ });
236
+ });
237
+ }
238
+
239
+ return {
240
+ launchPlan,
241
+ invokeRaw,
242
+ async destroy() {
243
+ expectedShutdown = true;
244
+ if (!closed) {
245
+ child.kill();
246
+ }
247
+ try {
248
+ await closePromise;
249
+ } catch {
250
+ // Best-effort shutdown: callers only need pending requests cleared.
251
+ }
252
+ },
253
+ };
254
+ }
255
+
256
+ function encodeHostControl(opcode, payload = new Uint8Array()) {
257
+ const message = new Uint8Array(5 + payload.length);
258
+ message.set(WASMEDGE_HOST_MAGIC, 0);
259
+ message[4] = opcode;
260
+ message.set(payload, 5);
261
+ return message;
262
+ }
263
+
264
+ function encodeJsonPayload(value) {
265
+ return textEncoder.encode(JSON.stringify(value));
266
+ }
267
+
268
+ function decodeJsonPayload(bytes) {
269
+ if (!bytes || bytes.length === 0) {
270
+ return null;
271
+ }
272
+ return JSON.parse(textDecoder.decode(bytes));
273
+ }
274
+
275
+ function encodeU32(value) {
276
+ const bytes = new Uint8Array(4);
277
+ new DataView(bytes.buffer).setUint32(0, value >>> 0, true);
278
+ return bytes;
279
+ }
280
+
281
+ function decodeU32(bytes) {
282
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(
283
+ 0,
284
+ true,
285
+ );
286
+ }
287
+
288
+ function encodeModuleInvokePayload(moduleId, requestBytes) {
289
+ if (typeof moduleId !== "string" || moduleId.trim().length === 0) {
290
+ throw new TypeError("moduleId must be a non-empty string");
291
+ }
292
+ const moduleIdBytes = textEncoder.encode(moduleId.trim());
293
+ const requestPayload = toUint8Array(requestBytes);
294
+ if (!requestPayload) {
295
+ throw new TypeError("module invoke request bytes are required");
296
+ }
297
+ const payload = new Uint8Array(4 + moduleIdBytes.length + requestPayload.length);
298
+ payload.set(encodeU32(moduleIdBytes.length), 0);
299
+ payload.set(moduleIdBytes, 4);
300
+ payload.set(requestPayload, 4 + moduleIdBytes.length);
301
+ return payload;
302
+ }
303
+
304
+ function serializeRegionOptions(options = {}) {
305
+ return {
306
+ ...options,
307
+ initialRecords: Array.isArray(options.initialRecords)
308
+ ? options.initialRecords.map((record) =>
309
+ record === null || record === undefined
310
+ ? null
311
+ : Array.from(
312
+ toUint8Array(record) ??
313
+ (() => {
314
+ throw new TypeError(
315
+ "runtime region initialRecords must be byte-oriented",
316
+ );
317
+ })(),
318
+ ),
319
+ )
320
+ : [],
321
+ };
322
+ }
323
+
324
+ function normalizeRecordResponse(record) {
325
+ if (!record) {
326
+ return null;
327
+ }
328
+ return {
329
+ ...record,
330
+ bytes: Uint8Array.from(record.bytes ?? []),
331
+ };
332
+ }
333
+
334
+ async function invokeJsonHostControl(rawClient, opcode, payload) {
335
+ const responseBytes = await rawClient.invokeRaw(
336
+ encodeHostControl(opcode, encodeJsonPayload(payload)),
337
+ );
338
+ return decodeJsonPayload(responseBytes);
339
+ }
340
+
341
+ function attachRuntimeHostControls(client, rawClient, options = {}) {
342
+ const encodeRequest = options.encodeRequest ?? ((request) => request);
343
+ const decodeResponse = options.decodeResponse ?? ((response) => response);
344
+
345
+ return Object.assign(client, {
346
+ installModule(definition = {}) {
347
+ return invokeJsonHostControl(
348
+ rawClient,
349
+ HOST_CONTROL_OPCODE.INSTALL_MODULE,
350
+ definition,
351
+ );
352
+ },
353
+ listModules() {
354
+ return invokeJsonHostControl(rawClient, HOST_CONTROL_OPCODE.LIST_MODULES, {});
355
+ },
356
+ unloadModule(moduleId) {
357
+ return invokeJsonHostControl(rawClient, HOST_CONTROL_OPCODE.UNLOAD_MODULE, {
358
+ moduleId,
359
+ });
360
+ },
361
+ async invokeModule(moduleId, request = {}) {
362
+ const responseBytes = await rawClient.invokeRaw(
363
+ encodeHostControl(
364
+ HOST_CONTROL_OPCODE.INVOKE_MODULE,
365
+ encodeModuleInvokePayload(moduleId, encodeRequest(request)),
366
+ ),
367
+ );
368
+ return decodeResponse(responseBytes);
369
+ },
370
+ appendRow(options = {}) {
371
+ return invokeJsonHostControl(rawClient, HOST_CONTROL_OPCODE.APPEND_ROW, options);
372
+ },
373
+ listRows(schemaFileId = null) {
374
+ return invokeJsonHostControl(rawClient, HOST_CONTROL_OPCODE.LIST_ROWS, {
375
+ schemaFileId,
376
+ });
377
+ },
378
+ resolveRow(handle) {
379
+ return invokeJsonHostControl(rawClient, HOST_CONTROL_OPCODE.RESOLVE_ROW, handle);
380
+ },
381
+ queryRows(sql) {
382
+ return invokeJsonHostControl(rawClient, HOST_CONTROL_OPCODE.QUERY_ROWS, {
383
+ sql,
384
+ });
385
+ },
386
+ allocateRegion(options = {}) {
387
+ return invokeJsonHostControl(
388
+ rawClient,
389
+ HOST_CONTROL_OPCODE.ALLOCATE_REGION,
390
+ serializeRegionOptions(options),
391
+ );
392
+ },
393
+ describeRegion(regionId) {
394
+ return invokeJsonHostControl(rawClient, HOST_CONTROL_OPCODE.DESCRIBE_REGION, {
395
+ regionId,
396
+ });
397
+ },
398
+ async resolveRecord(query = {}) {
399
+ const record = await invokeJsonHostControl(
400
+ rawClient,
401
+ HOST_CONTROL_OPCODE.RESOLVE_RECORD,
402
+ query,
403
+ );
404
+ return normalizeRecordResponse(record);
405
+ },
406
+ });
407
+ }
408
+
409
+ export async function createWasmEdgeStreamProcessClient(options = {}) {
410
+ const rawClient = await createLengthPrefixedProcessClient(options);
411
+
412
+ const client = {
413
+ launchPlan: rawClient.launchPlan,
414
+
415
+ invokeRaw(requestBytes) {
416
+ return rawClient.invokeRaw(requestBytes);
417
+ },
418
+
419
+ async invoke(request = {}) {
420
+ const requestBytes = encodePluginInvokeRequest(request);
421
+ const responseBytes = await rawClient.invokeRaw(requestBytes);
422
+ return decodePluginInvokeResponse(responseBytes);
423
+ },
424
+
425
+ destroy() {
426
+ return rawClient.destroy();
427
+ },
428
+ };
429
+
430
+ return attachRuntimeHostControls(client, rawClient, {
431
+ encodeRequest(request) {
432
+ return encodePluginInvokeRequest(request);
433
+ },
434
+ decodeResponse(responseBytes) {
435
+ return decodePluginInvokeResponse(responseBytes);
436
+ },
437
+ });
438
+ }
439
+
440
+ export async function createPluginInvokeProcessClient(options = {}) {
441
+ const rawClient = await createLengthPrefixedProcessClient(options);
442
+
443
+ const client = {
444
+ launchPlan: rawClient.launchPlan,
445
+
446
+ async invoke(request = {}) {
447
+ const requestBytes = encodePluginInvokeRequest(request);
448
+ const responseBytes = await rawClient.invokeRaw(requestBytes);
449
+ return decodePluginInvokeResponse(responseBytes);
450
+ },
451
+
452
+ invokeRaw: rawClient.invokeRaw,
453
+
454
+ destroy() {
455
+ return rawClient.destroy();
456
+ },
457
+ };
458
+
459
+ return attachRuntimeHostControls(client, rawClient, {
460
+ encodeRequest(request) {
461
+ return encodePluginInvokeRequest(request);
462
+ },
463
+ decodeResponse(responseBytes) {
464
+ return decodePluginInvokeResponse(responseBytes);
465
+ },
466
+ });
467
+ }