space-data-module-sdk 0.5.8 → 0.5.9
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 +86 -47
- package/package.json +1 -1
- package/src/index.d.ts +13 -0
- package/src/invoke/codec.js +9 -7
- package/src/testing/buildWasmEdgeRunner.js +214 -0
- package/src/testing/index.d.ts +109 -0
- package/src/testing/index.js +16 -0
- package/src/testing/moduleHarness.js +62 -0
- package/src/testing/native/wasmedge_emscripten_pthread_runner.c +1001 -0
- package/src/testing/processInvoke.js +226 -0
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
function formatProcessFailure(message, stderrChunks = [], cause = null) {
|
|
14
|
+
const stderrText = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
15
|
+
const details = stderrText ? `${message}\n${stderrText}` : message;
|
|
16
|
+
return cause ? new Error(details, { cause }) : new Error(details);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createLengthPrefixedRequest(bytes) {
|
|
20
|
+
const payload = Buffer.from(bytes);
|
|
21
|
+
const prefix = Buffer.allocUnsafe(4);
|
|
22
|
+
prefix.writeUInt32LE(payload.length, 0);
|
|
23
|
+
return Buffer.concat([prefix, payload]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeLaunchPlan(options = {}) {
|
|
27
|
+
if (options.launchPlan) {
|
|
28
|
+
return {
|
|
29
|
+
...options.launchPlan,
|
|
30
|
+
args: Array.isArray(options.launchPlan.args) ? options.launchPlan.args : [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
command: options.command ?? null,
|
|
35
|
+
args: Array.isArray(options.args) ? options.args : [],
|
|
36
|
+
env: options.env ?? process.env,
|
|
37
|
+
cwd: options.cwd ?? process.cwd(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildWasmEdgeSpawnEnv(baseEnv = process.env) {
|
|
42
|
+
const env = { ...baseEnv };
|
|
43
|
+
delete env.DYLD_LIBRARY_PATH;
|
|
44
|
+
delete env.DYLD_FALLBACK_LIBRARY_PATH;
|
|
45
|
+
delete env.DYLD_FRAMEWORK_PATH;
|
|
46
|
+
delete env.DYLD_FALLBACK_FRAMEWORK_PATH;
|
|
47
|
+
delete env.LIBRARY_PATH;
|
|
48
|
+
return env;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveWasmEdgePluginLaunchPlan(options = {}) {
|
|
52
|
+
const wasmPath =
|
|
53
|
+
typeof options.wasmPath === "string" && options.wasmPath.trim().length > 0
|
|
54
|
+
? path.resolve(options.wasmPath)
|
|
55
|
+
: null;
|
|
56
|
+
if (!wasmPath) {
|
|
57
|
+
throw new Error("resolveWasmEdgePluginLaunchPlan requires a wasmPath.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const invokeArgs =
|
|
61
|
+
Array.isArray(options.invokeArgs) && options.invokeArgs.length > 0
|
|
62
|
+
? [...options.invokeArgs]
|
|
63
|
+
: ["--serve-plugin-invoke"];
|
|
64
|
+
|
|
65
|
+
if (options.wasmEdgeRunnerBinary) {
|
|
66
|
+
return {
|
|
67
|
+
command: options.wasmEdgeRunnerBinary,
|
|
68
|
+
args: [wasmPath, ...invokeArgs],
|
|
69
|
+
env: buildWasmEdgeSpawnEnv(options.env),
|
|
70
|
+
wasmPath,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
command: options.wasmEdgeBinary ?? "wasmedge",
|
|
76
|
+
args: [
|
|
77
|
+
...(options.enableThreads === false ? [] : ["--enable-threads"]),
|
|
78
|
+
wasmPath,
|
|
79
|
+
...invokeArgs,
|
|
80
|
+
],
|
|
81
|
+
env: buildWasmEdgeSpawnEnv(options.env),
|
|
82
|
+
wasmPath,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function createPluginInvokeProcessClient(options = {}) {
|
|
87
|
+
const launchPlan = normalizeLaunchPlan(options);
|
|
88
|
+
if (
|
|
89
|
+
typeof launchPlan.command !== "string" ||
|
|
90
|
+
launchPlan.command.trim().length === 0
|
|
91
|
+
) {
|
|
92
|
+
throw new Error("createPluginInvokeProcessClient requires a command.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const child = spawn(launchPlan.command, launchPlan.args, {
|
|
96
|
+
cwd: launchPlan.cwd ?? process.cwd(),
|
|
97
|
+
env: launchPlan.env ?? process.env,
|
|
98
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
102
|
+
const stderrChunks = [];
|
|
103
|
+
const pending = [];
|
|
104
|
+
let closed = false;
|
|
105
|
+
let closeError = null;
|
|
106
|
+
let expectedShutdown = false;
|
|
107
|
+
|
|
108
|
+
function rejectPending(error) {
|
|
109
|
+
while (pending.length > 0) {
|
|
110
|
+
pending.shift().reject(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function drainResponses() {
|
|
115
|
+
while (pending.length > 0 && stdoutBuffer.length >= 4) {
|
|
116
|
+
const responseLength = stdoutBuffer.readUInt32LE(0);
|
|
117
|
+
if (stdoutBuffer.length < 4 + responseLength) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const responseBytes = stdoutBuffer.subarray(4, 4 + responseLength);
|
|
121
|
+
stdoutBuffer = stdoutBuffer.subarray(4 + responseLength);
|
|
122
|
+
pending.shift().resolve(new Uint8Array(responseBytes));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
child.stdout.on("data", (chunk) => {
|
|
127
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, Buffer.from(chunk)]);
|
|
128
|
+
drainResponses();
|
|
129
|
+
});
|
|
130
|
+
child.stderr.on("data", (chunk) => {
|
|
131
|
+
stderrChunks.push(Buffer.from(chunk));
|
|
132
|
+
});
|
|
133
|
+
child.on("error", (error) => {
|
|
134
|
+
closeError = formatProcessFailure(
|
|
135
|
+
"Failed to launch plugin invoke process.",
|
|
136
|
+
stderrChunks,
|
|
137
|
+
error,
|
|
138
|
+
);
|
|
139
|
+
rejectPending(closeError);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const closePromise = once(child, "close").then(([code, signal]) => {
|
|
143
|
+
closed = true;
|
|
144
|
+
if (!expectedShutdown && (code !== 0 || signal !== null)) {
|
|
145
|
+
closeError = formatProcessFailure(
|
|
146
|
+
`Plugin invoke process exited unexpectedly with ${
|
|
147
|
+
signal ? `signal ${signal}` : `code ${code}`
|
|
148
|
+
}.`,
|
|
149
|
+
stderrChunks,
|
|
150
|
+
);
|
|
151
|
+
rejectPending(closeError);
|
|
152
|
+
throw closeError;
|
|
153
|
+
}
|
|
154
|
+
if (!expectedShutdown && code !== 0) {
|
|
155
|
+
closeError = formatProcessFailure(
|
|
156
|
+
`Plugin invoke process exited with code ${code}.`,
|
|
157
|
+
stderrChunks,
|
|
158
|
+
);
|
|
159
|
+
rejectPending(closeError);
|
|
160
|
+
throw closeError;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
async function invokeRaw(requestBytes) {
|
|
165
|
+
if (closeError) {
|
|
166
|
+
throw closeError;
|
|
167
|
+
}
|
|
168
|
+
if (closed) {
|
|
169
|
+
throw formatProcessFailure(
|
|
170
|
+
"Plugin invoke process is already closed.",
|
|
171
|
+
stderrChunks,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const normalizedRequest = toUint8Array(requestBytes);
|
|
176
|
+
if (!normalizedRequest) {
|
|
177
|
+
throw new TypeError(
|
|
178
|
+
"Expected Uint8Array, ArrayBufferView, or ArrayBuffer request bytes.",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
pending.push({ resolve, reject });
|
|
184
|
+
child.stdin.write(createLengthPrefixedRequest(normalizedRequest), (error) => {
|
|
185
|
+
if (!error) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const pendingIndex = pending.findIndex((entry) => entry.resolve === resolve);
|
|
189
|
+
if (pendingIndex >= 0) {
|
|
190
|
+
pending.splice(pendingIndex, 1);
|
|
191
|
+
}
|
|
192
|
+
reject(
|
|
193
|
+
formatProcessFailure(
|
|
194
|
+
"Failed to send PluginInvokeRequest to child process.",
|
|
195
|
+
stderrChunks,
|
|
196
|
+
error,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
launchPlan,
|
|
205
|
+
|
|
206
|
+
async invoke(request = {}) {
|
|
207
|
+
const requestBytes = encodePluginInvokeRequest(request);
|
|
208
|
+
const responseBytes = await invokeRaw(requestBytes);
|
|
209
|
+
return decodePluginInvokeResponse(responseBytes);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
invokeRaw,
|
|
213
|
+
|
|
214
|
+
async destroy() {
|
|
215
|
+
expectedShutdown = true;
|
|
216
|
+
if (!closed) {
|
|
217
|
+
child.kill();
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await closePromise;
|
|
221
|
+
} catch {
|
|
222
|
+
// Best-effort shutdown: callers only need pending requests cleared.
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|