libfx 0.0.7-dev.632.g9fe8a30c19a7 → 0.0.8-dev.820.g1d9d3b63d6ea
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 +130 -5
- package/core-output.js +58 -0
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +234 -92
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/mcp.js +47 -18
- package/node.cjs +2542 -0
- package/node.js +302 -61
- package/package.json +9 -3
- package/wasm-module.js +50 -0
package/node.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { closeSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { Socket } from "node:net";
|
|
3
5
|
import { homedir } from "node:os";
|
|
4
6
|
import { isAbsolute, resolve } from "node:path";
|
|
5
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { CoreOutput } from "./core-output.js";
|
|
9
|
+
import { loadModule, withModuleFailure } from "./wasm-module.js";
|
|
6
10
|
import {
|
|
7
11
|
createFxAgent as createWasmAgent,
|
|
8
12
|
createFxTerminal as createWasmTerminal,
|
|
@@ -15,21 +19,29 @@ import {
|
|
|
15
19
|
|
|
16
20
|
export { encodeXtermKeyEvent, fxSdkApiVersion, listModels, supportsJspi, xtermAdapter };
|
|
17
21
|
export const libfxApiVersion = 2;
|
|
22
|
+
const nativeCoreApiVersion = 3;
|
|
18
23
|
|
|
19
24
|
const fetchOperationStale = 0;
|
|
20
25
|
const fetchOperationApplied = 1;
|
|
21
26
|
const fetchOperationBackpressure = 2;
|
|
22
27
|
|
|
23
|
-
const
|
|
28
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
24
29
|
const defaultCoreWasm = new URL("./fx-core.wasm", import.meta.url);
|
|
25
30
|
const defaultTermWasm = new URL("./fx-term.wasm", import.meta.url);
|
|
26
|
-
const defaultNativeCandidates = [
|
|
27
|
-
"./libfx.node",
|
|
28
|
-
`./libfx.${process.platform}-${process.arch}.node`,
|
|
29
|
-
];
|
|
30
31
|
let nativeBackendPromise;
|
|
31
32
|
const wasmFilePromises = new Map();
|
|
32
33
|
|
|
34
|
+
const backendReasonCodes = {
|
|
35
|
+
unsupportedPlatform: "LIBFX_UNSUPPORTED_PLATFORM",
|
|
36
|
+
missingArtifact: "LIBFX_NATIVE_ARTIFACT_MISSING",
|
|
37
|
+
nativeLoad: "LIBFX_NATIVE_LOAD_FAILED",
|
|
38
|
+
nativeApi: "LIBFX_NATIVE_API_MISMATCH",
|
|
39
|
+
missingSurface: "LIBFX_NATIVE_SURFACE_MISSING",
|
|
40
|
+
disabledNative: "LIBFX_NATIVE_DISABLED",
|
|
41
|
+
jspiUnavailable: "LIBFX_JSPI_UNAVAILABLE",
|
|
42
|
+
wasmLoad: "LIBFX_WASM_LOAD_FAILED",
|
|
43
|
+
};
|
|
44
|
+
|
|
33
45
|
function jspiFallbackError(surface, nativeError) {
|
|
34
46
|
const nativeDetail = nativeError ? ` Native loading failed: ${nativeError.message}.` : " No compatible native addon was found.";
|
|
35
47
|
const error = new Error(
|
|
@@ -46,7 +58,8 @@ async function loadNativeCandidate(candidate) {
|
|
|
46
58
|
if (candidate == null) return null;
|
|
47
59
|
if (candidate instanceof URL) {
|
|
48
60
|
if (candidate.protocol === "file:" && candidate.pathname.endsWith(".node")) {
|
|
49
|
-
|
|
61
|
+
// Bundlers trace the asset URL; Node must load the native file at runtime.
|
|
62
|
+
return Reflect.apply(nodeRequire, undefined, [fileURLToPath(candidate)]);
|
|
50
63
|
}
|
|
51
64
|
const imported = await import(candidate.href);
|
|
52
65
|
return imported.default ?? imported;
|
|
@@ -55,18 +68,36 @@ async function loadNativeCandidate(candidate) {
|
|
|
55
68
|
if (typeof candidate !== "string") {
|
|
56
69
|
throw new TypeError("nativeAddon must be a module, path, URL, false, or undefined");
|
|
57
70
|
}
|
|
58
|
-
if (candidate.endsWith(".node"))
|
|
71
|
+
if (candidate.endsWith(".node")) {
|
|
72
|
+
return Reflect.apply(nodeRequire, undefined, [isAbsolute(candidate) ? candidate : resolve(candidate)]);
|
|
73
|
+
}
|
|
59
74
|
const imported = await import(candidate.startsWith("file:") ? candidate : pathToFileURL(candidate).href);
|
|
60
75
|
return imported.default ?? imported;
|
|
61
76
|
}
|
|
62
77
|
|
|
78
|
+
function defaultNativeCandidate() {
|
|
79
|
+
if (process.platform === "linux" && process.arch === "x64") {
|
|
80
|
+
return new URL("./libfx.linux-x64.node", import.meta.url);
|
|
81
|
+
}
|
|
82
|
+
if (process.platform === "linux" && process.arch === "arm64") {
|
|
83
|
+
return new URL("./libfx.linux-arm64.node", import.meta.url);
|
|
84
|
+
}
|
|
85
|
+
if (process.platform === "darwin" && process.arch === "x64") {
|
|
86
|
+
return new URL("./libfx.darwin-x64.node", import.meta.url);
|
|
87
|
+
}
|
|
88
|
+
if (process.platform === "darwin" && process.arch === "arm64") {
|
|
89
|
+
return new URL("./libfx.darwin-arm64.node", import.meta.url);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
function validateNativeBackend(backend) {
|
|
64
95
|
if (!backend) return null;
|
|
65
96
|
const hasLowLevelCore = typeof backend.createCore === "function";
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
const expectedVersion = hasLowLevelCore ? nativeCoreApiVersion : libfxApiVersion;
|
|
98
|
+
if ((hasLowLevelCore || backend.libfxApiVersion !== undefined) && backend.libfxApiVersion !== expectedVersion) {
|
|
68
99
|
const actualVersion = backend.libfxApiVersion ?? "missing";
|
|
69
|
-
throw new Error(`native addon API version ${actualVersion} is incompatible with
|
|
100
|
+
throw new Error(`native addon API version ${actualVersion} is incompatible with expected API version ${expectedVersion}`);
|
|
70
101
|
}
|
|
71
102
|
if (typeof backend.createCore !== "function" && typeof backend.createFxTerminal !== "function") {
|
|
72
103
|
throw new Error("native addon must export createCore() or createFxTerminal()");
|
|
@@ -74,52 +105,217 @@ function validateNativeBackend(backend) {
|
|
|
74
105
|
return backend;
|
|
75
106
|
}
|
|
76
107
|
|
|
108
|
+
function missingArtifact(error) {
|
|
109
|
+
return error?.code === "ENOENT" || error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function nativeCandidateFilePath(candidate) {
|
|
113
|
+
if (candidate instanceof URL) return candidate.protocol === "file:" ? fileURLToPath(candidate) : null;
|
|
114
|
+
if (typeof candidate !== "string") return null;
|
|
115
|
+
if (candidate.startsWith("file:")) return fileURLToPath(new URL(candidate));
|
|
116
|
+
return URL.canParse(candidate) ? null : resolve(candidate);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function nativeArtifactMissing(candidate) {
|
|
120
|
+
try {
|
|
121
|
+
const path = nativeCandidateFilePath(candidate);
|
|
122
|
+
if (path === null) return false;
|
|
123
|
+
await access(path);
|
|
124
|
+
return false;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return missingArtifact(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validationFailure(error) {
|
|
131
|
+
return error?.message?.startsWith("native addon API version ") ? "api" : "surface";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function loadAndValidateNativeCandidate(candidate, artifactMissing = false) {
|
|
135
|
+
let backend;
|
|
136
|
+
try {
|
|
137
|
+
backend = await loadNativeCandidate(candidate);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { backend: null, error, failure: artifactMissing ? "missing" : "load" };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return { backend: validateNativeBackend(backend), error: null, failure: null };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { backend: null, error, failure: validationFailure(error) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
77
148
|
async function discoverNativeBackend() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return { backend: validateNativeBackend(await loadNativeCandidate(url)), error: null };
|
|
88
|
-
} catch (error) {
|
|
89
|
-
return { backend: null, error };
|
|
149
|
+
const candidate = defaultNativeCandidate();
|
|
150
|
+
if (!candidate) {
|
|
151
|
+
return { backend: null, error: null, failure: "unsupported" };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await access(fileURLToPath(candidate));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (missingArtifact(error)) {
|
|
157
|
+
return { backend: null, error: null, probeError: error, failure: "missing" };
|
|
90
158
|
}
|
|
159
|
+
return { backend: null, error, failure: "load" };
|
|
91
160
|
}
|
|
92
|
-
return
|
|
161
|
+
return loadAndValidateNativeCandidate(candidate);
|
|
93
162
|
}
|
|
94
163
|
|
|
95
164
|
async function resolveNativeBackend(nativeAddon) {
|
|
96
|
-
if (nativeAddon === false) return { backend: null, error: null };
|
|
165
|
+
if (nativeAddon === false) return { backend: null, error: null, failure: "disabled" };
|
|
97
166
|
if (nativeAddon !== undefined) {
|
|
98
|
-
|
|
99
|
-
return { backend: validateNativeBackend(await loadNativeCandidate(nativeAddon)), error: null };
|
|
100
|
-
} catch (error) {
|
|
101
|
-
return { backend: null, error };
|
|
102
|
-
}
|
|
167
|
+
return loadAndValidateNativeCandidate(nativeAddon, await nativeArtifactMissing(nativeAddon));
|
|
103
168
|
}
|
|
104
169
|
nativeBackendPromise ??= discoverNativeBackend();
|
|
105
170
|
return nativeBackendPromise;
|
|
106
171
|
}
|
|
107
172
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
173
|
+
function wasmInput(input) {
|
|
174
|
+
const path = wasmFilePath(input);
|
|
175
|
+
if (path === null) {
|
|
176
|
+
if (input instanceof URL) return input.href;
|
|
177
|
+
return input;
|
|
178
|
+
}
|
|
113
179
|
const cached = wasmFilePromises.get(path);
|
|
114
180
|
if (cached) return cached;
|
|
115
|
-
const
|
|
181
|
+
const pendingRead = readFile(path);
|
|
182
|
+
let pending;
|
|
183
|
+
pending = withModuleFailure(pendingRead, () => {
|
|
184
|
+
if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
|
|
185
|
+
});
|
|
116
186
|
wasmFilePromises.set(path, pending);
|
|
117
|
-
|
|
187
|
+
pendingRead.catch(() => {
|
|
118
188
|
if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
|
|
119
189
|
});
|
|
120
190
|
return pending;
|
|
121
191
|
}
|
|
122
192
|
|
|
193
|
+
function wasmFilePath(input) {
|
|
194
|
+
if (input instanceof URL && input.protocol === "file:") return fileURLToPath(input);
|
|
195
|
+
if (typeof input === "string" && !URL.canParse(input)) return resolve(input);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function reason(code, message, error) {
|
|
200
|
+
const causeCode = error?.code;
|
|
201
|
+
return {
|
|
202
|
+
code,
|
|
203
|
+
message,
|
|
204
|
+
...(typeof causeCode === "string" || typeof causeCode === "number" ? { causeCode } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function nativeFailureReason(result) {
|
|
209
|
+
const detailError = result.probeError ?? result.error;
|
|
210
|
+
switch (result.failure) {
|
|
211
|
+
case "unsupported":
|
|
212
|
+
return reason(
|
|
213
|
+
backendReasonCodes.unsupportedPlatform,
|
|
214
|
+
`native addon is not available for ${process.platform}-${process.arch}`,
|
|
215
|
+
);
|
|
216
|
+
case "missing":
|
|
217
|
+
return reason(
|
|
218
|
+
backendReasonCodes.missingArtifact,
|
|
219
|
+
`native addon artifact was not found${detailError?.message ? `: ${detailError.message}` : ""}`,
|
|
220
|
+
detailError,
|
|
221
|
+
);
|
|
222
|
+
case "load":
|
|
223
|
+
return reason(
|
|
224
|
+
backendReasonCodes.nativeLoad,
|
|
225
|
+
`native addon failed to load${result.error?.message ? `: ${result.error.message}` : ""}`,
|
|
226
|
+
result.error,
|
|
227
|
+
);
|
|
228
|
+
case "api":
|
|
229
|
+
return reason(backendReasonCodes.nativeApi, result.error.message, result.error);
|
|
230
|
+
case "surface":
|
|
231
|
+
return reason(backendReasonCodes.missingSurface, result.error.message, result.error);
|
|
232
|
+
case "disabled":
|
|
233
|
+
return reason(backendReasonCodes.disabledNative, "native addon loading is disabled");
|
|
234
|
+
default:
|
|
235
|
+
return reason(backendReasonCodes.nativeLoad, "native addon is unavailable", result.error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function validateBackendInfoOptions(value) {
|
|
240
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
241
|
+
throw new TypeError("getBackendInfo() options must be an object");
|
|
242
|
+
}
|
|
243
|
+
const options = { ...value };
|
|
244
|
+
for (const key of Object.keys(options)) {
|
|
245
|
+
if (!new Set(["surface", "backend", "nativeAddon", "wasm"]).has(key)) {
|
|
246
|
+
throw new TypeError(`getBackendInfo() does not accept ${key}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const surface = options.surface ?? "agent";
|
|
250
|
+
if (!new Set(["agent", "terminal"]).has(surface)) {
|
|
251
|
+
throw new TypeError('surface must be "agent" or "terminal"');
|
|
252
|
+
}
|
|
253
|
+
const backend = options.backend ?? "auto";
|
|
254
|
+
if (!new Set(["auto", "native", "wasm"]).has(backend)) {
|
|
255
|
+
throw new TypeError('backend must be "auto", "native", or "wasm"');
|
|
256
|
+
}
|
|
257
|
+
if (Object.hasOwn(options, "nativeAddon") && options.nativeAddon !== undefined && options.nativeAddon !== false &&
|
|
258
|
+
typeof options.nativeAddon !== "string" && !(options.nativeAddon instanceof URL) &&
|
|
259
|
+
(typeof options.nativeAddon !== "object" || options.nativeAddon === null)) {
|
|
260
|
+
throw new TypeError("nativeAddon must be a module, path, URL, false, or undefined");
|
|
261
|
+
}
|
|
262
|
+
const validWasm = options.wasm === undefined || typeof options.wasm === "string" || options.wasm instanceof URL ||
|
|
263
|
+
options.wasm instanceof Promise || options.wasm instanceof WebAssembly.Module || options.wasm instanceof Response ||
|
|
264
|
+
options.wasm instanceof ArrayBuffer || ArrayBuffer.isView(options.wasm);
|
|
265
|
+
if (Object.hasOwn(options, "wasm") && !validWasm) {
|
|
266
|
+
throw new TypeError("wasm must be a URL, Response, ArrayBuffer, typed array, or WebAssembly.Module");
|
|
267
|
+
}
|
|
268
|
+
return { ...options, surface, backend };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function getBackendInfo(value = {}) {
|
|
272
|
+
const { surface, backend, nativeAddon, wasm } = validateBackendInfoOptions(value);
|
|
273
|
+
const attempts = [];
|
|
274
|
+
if (backend !== "wasm") {
|
|
275
|
+
const native = await resolveNativeBackend(nativeAddon);
|
|
276
|
+
const nativeMethod = surface === "agent" ? "createCore" : "createFxTerminal";
|
|
277
|
+
if (typeof native.backend?.[nativeMethod] === "function") {
|
|
278
|
+
attempts.push({ backend: "native", available: true, reason: null });
|
|
279
|
+
return { surface, backend: "native", attempts };
|
|
280
|
+
}
|
|
281
|
+
const failureReason = native.backend
|
|
282
|
+
? reason(backendReasonCodes.missingSurface, `native addon does not provide ${nativeMethod}()`)
|
|
283
|
+
: nativeFailureReason(native);
|
|
284
|
+
attempts.push({ backend: "native", available: false, reason: failureReason });
|
|
285
|
+
if (backend === "native") return { surface, backend: "unavailable", attempts };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!supportsJspi()) {
|
|
289
|
+
attempts.push({
|
|
290
|
+
backend: "wasm-jspi",
|
|
291
|
+
available: false,
|
|
292
|
+
reason: reason(
|
|
293
|
+
backendReasonCodes.jspiUnavailable,
|
|
294
|
+
"WebAssembly backend requires JavaScript Promise Integration (JSPI)",
|
|
295
|
+
),
|
|
296
|
+
});
|
|
297
|
+
return { surface, backend: "unavailable", attempts };
|
|
298
|
+
}
|
|
299
|
+
const defaultWasm = surface === "agent" ? defaultCoreWasm : defaultTermWasm;
|
|
300
|
+
const wasmSource = wasm ?? defaultWasm;
|
|
301
|
+
try {
|
|
302
|
+
await loadModule(wasmInput(wasmSource));
|
|
303
|
+
attempts.push({ backend: "wasm-jspi", available: true, reason: null });
|
|
304
|
+
return { surface, backend: "wasm-jspi", attempts };
|
|
305
|
+
} catch (error) {
|
|
306
|
+
attempts.push({
|
|
307
|
+
backend: "wasm-jspi",
|
|
308
|
+
available: false,
|
|
309
|
+
reason: reason(
|
|
310
|
+
backendReasonCodes.wasmLoad,
|
|
311
|
+
`WebAssembly asset failed to load or compile: ${error?.message ?? String(error)}`,
|
|
312
|
+
error,
|
|
313
|
+
),
|
|
314
|
+
});
|
|
315
|
+
return { surface, backend: "unavailable", attempts };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
123
319
|
function createNativeCoreRuntime(addon, options) {
|
|
124
320
|
const { apiKey, model, gatewayChatUrl } = options;
|
|
125
321
|
const core = addon.createCore({
|
|
@@ -129,9 +325,24 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
129
325
|
...(model === undefined ? {} : { model }),
|
|
130
326
|
...(gatewayChatUrl === undefined ? {} : { gatewayChatUrl }),
|
|
131
327
|
});
|
|
328
|
+
let readyFd;
|
|
329
|
+
let readySocket;
|
|
330
|
+
try {
|
|
331
|
+
readyFd = addon.takeCoreReadyFd(core);
|
|
332
|
+
readySocket = new Socket({ fd: readyFd, readable: true, writable: false });
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (readyFd !== undefined) {
|
|
335
|
+
try { closeSync(readyFd); } catch {}
|
|
336
|
+
}
|
|
337
|
+
addon.destroyCore(core);
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
const readyClosed = new Promise((resolve) => readySocket.once("close", resolve));
|
|
132
341
|
let exitedResolve;
|
|
133
342
|
let lineHandler = null;
|
|
134
|
-
|
|
343
|
+
const output = new CoreOutput((message, size) => lineHandler(message, size));
|
|
344
|
+
let draining = false;
|
|
345
|
+
let outputError;
|
|
135
346
|
let settled = false;
|
|
136
347
|
let fetchState = null;
|
|
137
348
|
const exited = new Promise((resolve) => { exitedResolve = resolve; });
|
|
@@ -139,13 +350,15 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
139
350
|
fetchState?.controller.abort();
|
|
140
351
|
try { addon.abortCoreFetch(core); } catch {}
|
|
141
352
|
};
|
|
142
|
-
const finish = (code) => {
|
|
353
|
+
const finish = (code, error) => {
|
|
143
354
|
if (settled) return;
|
|
144
355
|
settled = true;
|
|
145
|
-
|
|
356
|
+
outputError = error;
|
|
357
|
+
output.close();
|
|
146
358
|
abortHostEffects();
|
|
147
359
|
try { addon.destroyCore(core); } catch {}
|
|
148
|
-
|
|
360
|
+
readySocket.destroy();
|
|
361
|
+
void readyClosed.then(() => exitedResolve(code));
|
|
149
362
|
};
|
|
150
363
|
const pumpFetch = async (request) => {
|
|
151
364
|
const controller = new AbortController();
|
|
@@ -189,10 +402,14 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
189
402
|
} catch {}
|
|
190
403
|
}
|
|
191
404
|
} finally {
|
|
192
|
-
if (fetchState === state)
|
|
405
|
+
if (fetchState === state) {
|
|
406
|
+
fetchState = null;
|
|
407
|
+
queueMicrotask(drainReady);
|
|
408
|
+
}
|
|
193
409
|
}
|
|
194
410
|
};
|
|
195
|
-
|
|
411
|
+
function drainReady() {
|
|
412
|
+
if (settled) return;
|
|
196
413
|
try {
|
|
197
414
|
if (fetchState) {
|
|
198
415
|
if (!fetchState.controller.signal.aborted && !addon.coreFetchActive(core, fetchState.handle)) {
|
|
@@ -202,29 +419,55 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
202
419
|
const fetchRequest = addon.takeCoreFetch(core);
|
|
203
420
|
if (fetchRequest) void pumpFetch(JSON.parse(fetchRequest.toString("utf8")));
|
|
204
421
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
422
|
+
if (addon.coreExitCode(core) !== 0) {
|
|
423
|
+
finish(1, new Error("native output delivery failed"));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
void drainOutput();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
finish(1, error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function drainOutput() {
|
|
432
|
+
if (draining || settled) return;
|
|
433
|
+
draining = true;
|
|
434
|
+
try {
|
|
435
|
+
while (!settled) {
|
|
436
|
+
const chunk = addon.drainCore(core);
|
|
437
|
+
if (!chunk.length) break;
|
|
438
|
+
const pending = output.write(chunk);
|
|
439
|
+
if (pending) await pending;
|
|
440
|
+
}
|
|
441
|
+
if (!settled && addon.coreExited(core)) {
|
|
442
|
+
output.finish();
|
|
443
|
+
finish(addon.coreExitCode(core));
|
|
215
444
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
445
|
+
} catch (error) {
|
|
446
|
+
finish(1, error);
|
|
447
|
+
} finally {
|
|
448
|
+
draining = false;
|
|
219
449
|
}
|
|
220
|
-
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
readySocket.on("data", drainReady);
|
|
453
|
+
readySocket.on("end", () => { drainReady(); if (!settled) finish(1); });
|
|
454
|
+
readySocket.on("error", () => finish(1));
|
|
455
|
+
readySocket.on("close", () => { if (!settled) finish(1); });
|
|
456
|
+
// Some runtimes defer descriptor adoption until connect().
|
|
457
|
+
if (readySocket.pending) {
|
|
458
|
+
try { readySocket.connect({ fd: readyFd }); } catch (error) { finish(1); throw error; }
|
|
459
|
+
}
|
|
221
460
|
|
|
222
461
|
return {
|
|
223
462
|
exited,
|
|
463
|
+
get error() { return outputError; },
|
|
224
464
|
write(data) { addon.writeCore(core, Buffer.from(data)); },
|
|
225
465
|
closeStdin() { addon.closeCore(core); },
|
|
226
466
|
abortHostEffects,
|
|
227
|
-
abort() {
|
|
467
|
+
abort(error) {
|
|
468
|
+
if (error) finish(1, error);
|
|
469
|
+
else { abortHostEffects(); addon.closeCore(core); }
|
|
470
|
+
},
|
|
228
471
|
setLineHandler(handler) { lineHandler = handler; },
|
|
229
472
|
};
|
|
230
473
|
}
|
|
@@ -270,10 +513,8 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
270
513
|
if (nativeAttempted) throw nativeError;
|
|
271
514
|
throw jspiFallbackError(surface, nativeError);
|
|
272
515
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
wasm: await wasmBytes(runtimeOptions.wasm ?? defaultWasm),
|
|
276
|
-
});
|
|
516
|
+
const wasmSource = runtimeOptions.wasm ?? defaultWasm;
|
|
517
|
+
return wasmFactory({ ...runtimeOptions, wasm: wasmInput(wasmSource) });
|
|
277
518
|
}
|
|
278
519
|
|
|
279
520
|
export async function createFxAgent(options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libfx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8-dev.820.g1d9d3b63d6ea",
|
|
4
4
|
"description": "Embed fx agents and terminals in JavaScript hosts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -17,11 +17,17 @@
|
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
|
-
"node":
|
|
20
|
+
"node": {
|
|
21
|
+
"import": "./node.js",
|
|
22
|
+
"require": "./node.cjs"
|
|
23
|
+
},
|
|
21
24
|
"browser": "./browser.js",
|
|
22
25
|
"default": "./browser.js"
|
|
23
26
|
},
|
|
24
|
-
"./node":
|
|
27
|
+
"./node": {
|
|
28
|
+
"import": "./node.js",
|
|
29
|
+
"require": "./node.cjs"
|
|
30
|
+
},
|
|
25
31
|
"./browser": "./browser.js",
|
|
26
32
|
"./wasm": "./fx-sdk.js",
|
|
27
33
|
"./mcp": "./mcp.js",
|
package/wasm-module.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const modulePromisesBySource = new Map();
|
|
2
|
+
const modulePromisesByObject = new WeakMap();
|
|
3
|
+
const moduleFailureSource = Symbol("libfx.moduleFailureSource");
|
|
4
|
+
|
|
5
|
+
export function withModuleFailure(input, onFailure) {
|
|
6
|
+
return { [moduleFailureSource]: { input, onFailure } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function compileModule(input) {
|
|
10
|
+
const failureSource = input?.[moduleFailureSource];
|
|
11
|
+
if (failureSource) {
|
|
12
|
+
try {
|
|
13
|
+
return await compileModule(failureSource.input);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
failureSource.onFailure();
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (input instanceof WebAssembly.Module) return input;
|
|
20
|
+
if (typeof input === "string") input = fetch(input);
|
|
21
|
+
if (input instanceof Promise) input = await input;
|
|
22
|
+
if (input instanceof WebAssembly.Module) return input;
|
|
23
|
+
if (input instanceof Response) {
|
|
24
|
+
const contentType = input.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
|
|
25
|
+
if (contentType === "application/wasm" && typeof WebAssembly.compileStreaming === "function") {
|
|
26
|
+
return WebAssembly.compileStreaming(input);
|
|
27
|
+
}
|
|
28
|
+
const bytes = await input.arrayBuffer();
|
|
29
|
+
return WebAssembly.compile(bytes);
|
|
30
|
+
}
|
|
31
|
+
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
|
|
32
|
+
return WebAssembly.compile(input);
|
|
33
|
+
}
|
|
34
|
+
throw new TypeError("wasm must be a URL, Response, ArrayBuffer, typed array, or WebAssembly.Module");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadModule(input) {
|
|
38
|
+
if (input instanceof WebAssembly.Module) return Promise.resolve(input);
|
|
39
|
+
const isString = typeof input === "string";
|
|
40
|
+
if (!isString && (typeof input !== "object" || input === null)) return compileModule(input);
|
|
41
|
+
const cache = isString ? modulePromisesBySource : modulePromisesByObject;
|
|
42
|
+
const cached = cache.get(input);
|
|
43
|
+
if (cached) return cached;
|
|
44
|
+
const pending = compileModule(input);
|
|
45
|
+
cache.set(input, pending);
|
|
46
|
+
pending.catch(() => {
|
|
47
|
+
if (cache.get(input) === pending) cache.delete(input);
|
|
48
|
+
});
|
|
49
|
+
return pending;
|
|
50
|
+
}
|