@gcoredev/fastedge-test 0.0.1-beta.4 → 0.1.0-beta.3
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 +6 -6
- package/dist/fastedge-cli/METADATA.json +1 -3
- package/dist/fastedge-cli/{fastedge-run-linux-x64-unkown → fastedge-run-darwin-arm64} +0 -0
- package/dist/fastedge-cli/fastedge-run-linux-x64 +0 -0
- package/dist/fastedge-cli/fastedge-run.exe +0 -0
- package/dist/frontend/assets/index-CEFjsU8e.js +35 -0
- package/dist/frontend/assets/index-DdlINQc_.css +1 -0
- package/dist/frontend/index.html +2 -2
- package/dist/lib/index.cjs +299 -107
- package/dist/lib/index.js +301 -110
- package/dist/lib/runner/HostFunctions.d.ts +8 -0
- package/dist/lib/runner/HttpWasmRunner.d.ts +34 -14
- package/dist/lib/runner/IStateManager.d.ts +3 -2
- package/dist/lib/runner/IWasmRunner.d.ts +16 -1
- package/dist/lib/runner/NullStateManager.d.ts +1 -0
- package/dist/lib/runner/PortManager.d.ts +17 -19
- package/dist/lib/runner/ProxyWasmRunner.d.ts +7 -0
- package/dist/lib/schemas/api.d.ts +8 -2
- package/dist/lib/schemas/config.d.ts +4 -1
- package/dist/lib/test-framework/index.cjs +301 -108
- package/dist/lib/test-framework/index.js +303 -111
- package/dist/lib/test-framework/suite-runner.d.ts +1 -1
- package/dist/server.js +30 -29
- package/docs/API.md +758 -360
- package/docs/DEBUGGER.md +151 -0
- package/docs/INDEX.md +111 -0
- package/docs/RUNNER.md +582 -0
- package/docs/TEST_CONFIG.md +242 -0
- package/docs/TEST_FRAMEWORK.md +384 -284
- package/docs/WEBSOCKET.md +499 -0
- package/docs/quickstart.md +171 -0
- package/llms.txt +72 -14
- package/package.json +15 -5
- package/schemas/api-config.schema.json +12 -5
- package/schemas/api-load.schema.json +11 -6
- package/schemas/{test-config.schema.json → fastedge-config.test.schema.json} +12 -5
- package/dist/fastedge-cli/.gitkeep +0 -0
- package/dist/frontend/assets/index-CnXStFTd.css +0 -1
- package/dist/frontend/assets/index-FR9Oqsow.js +0 -37
- package/docs/HYBRID_LOADING.md +0 -546
- package/docs/LOCAL_SERVER.md +0 -153
package/dist/lib/index.js
CHANGED
|
@@ -730,6 +730,8 @@ var HostFunctions = class {
|
|
|
730
730
|
this.pendingHttpCall = null;
|
|
731
731
|
this.httpCallResponse = null;
|
|
732
732
|
this.streamClosed = false;
|
|
733
|
+
// Local response state (from proxy_send_local_response / send_http_response)
|
|
734
|
+
this.localResponse = null;
|
|
733
735
|
this.memory = memory;
|
|
734
736
|
this.propertyResolver = propertyResolver;
|
|
735
737
|
this.propertyAccessControl = propertyAccessControl;
|
|
@@ -784,6 +786,16 @@ var HostFunctions = class {
|
|
|
784
786
|
resetStreamClosed() {
|
|
785
787
|
this.streamClosed = false;
|
|
786
788
|
}
|
|
789
|
+
// Local response accessors (called by ProxyWasmRunner after callHook)
|
|
790
|
+
hasLocalResponse() {
|
|
791
|
+
return this.localResponse !== null;
|
|
792
|
+
}
|
|
793
|
+
getLocalResponse() {
|
|
794
|
+
return this.localResponse;
|
|
795
|
+
}
|
|
796
|
+
resetLocalResponse() {
|
|
797
|
+
this.localResponse = null;
|
|
798
|
+
}
|
|
787
799
|
getRequestHeaders() {
|
|
788
800
|
return this.requestHeaders;
|
|
789
801
|
}
|
|
@@ -1044,15 +1056,21 @@ var HostFunctions = class {
|
|
|
1044
1056
|
this.currentContextId = contextId || this.currentContextId;
|
|
1045
1057
|
return 0 /* Ok */;
|
|
1046
1058
|
},
|
|
1047
|
-
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, grpcStatus) => {
|
|
1059
|
+
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, headerPairsPtr, headerPairsLen, grpcStatus) => {
|
|
1048
1060
|
this.setLastHostCall(
|
|
1049
1061
|
`proxy_send_local_response status=${statusCode} bodyLen=${bodyLen}`
|
|
1050
1062
|
);
|
|
1051
1063
|
const statusText = this.memory.readString(statusCodePtr, statusCodeLen);
|
|
1052
|
-
const body = this.memory.
|
|
1064
|
+
const body = this.memory.readBytes(bodyPtr, bodyLen);
|
|
1065
|
+
if (headerPairsLen > 0) {
|
|
1066
|
+
const headerBytes = this.memory.readBytes(headerPairsPtr, headerPairsLen);
|
|
1067
|
+
const headers = HeaderManager.deserializeBinary(headerBytes);
|
|
1068
|
+
this.logDebug(`send_local_response headers (not merged): ${JSON.stringify(headers)}`);
|
|
1069
|
+
}
|
|
1070
|
+
this.localResponse = { statusCode, statusText, body };
|
|
1053
1071
|
this.logs.push({
|
|
1054
1072
|
level: 1,
|
|
1055
|
-
message: `local_response status=${statusCode} ${statusText}
|
|
1073
|
+
message: `local_response status=${statusCode} ${statusText} bodyLen=${body.byteLength} grpc=${grpcStatus}`
|
|
1056
1074
|
});
|
|
1057
1075
|
return 0 /* Ok */;
|
|
1058
1076
|
},
|
|
@@ -1632,7 +1650,6 @@ async function loadDotenvFiles(dotenvPath = ".") {
|
|
|
1632
1650
|
// server/runner/ProxyWasmRunner.ts
|
|
1633
1651
|
var textEncoder4 = new TextEncoder();
|
|
1634
1652
|
var ProxyWasmRunner = class {
|
|
1635
|
-
// Default to enabled
|
|
1636
1653
|
constructor(fastEdgeConfig, dotenvEnabled = true) {
|
|
1637
1654
|
this.module = null;
|
|
1638
1655
|
// Compiled module (reused)
|
|
@@ -1646,6 +1663,8 @@ var ProxyWasmRunner = class {
|
|
|
1646
1663
|
this.debug = process.env.PROXY_RUNNER_DEBUG === "1";
|
|
1647
1664
|
this.stateManager = null;
|
|
1648
1665
|
this.dotenvEnabled = true;
|
|
1666
|
+
// Default to enabled
|
|
1667
|
+
this.dotenvPath = ".";
|
|
1649
1668
|
this.memory = new MemoryManager();
|
|
1650
1669
|
this.propertyResolver = new PropertyResolver();
|
|
1651
1670
|
this.propertyAccessControl = new PropertyAccessControl();
|
|
@@ -1680,7 +1699,7 @@ var ProxyWasmRunner = class {
|
|
|
1680
1699
|
return;
|
|
1681
1700
|
}
|
|
1682
1701
|
try {
|
|
1683
|
-
const dotenvConfig = await loadDotenvFiles(
|
|
1702
|
+
const dotenvConfig = await loadDotenvFiles(this.dotenvPath);
|
|
1684
1703
|
if (dotenvConfig.secrets && Object.keys(dotenvConfig.secrets).length > 0) {
|
|
1685
1704
|
const existingSecrets = this.secretStore.getAll();
|
|
1686
1705
|
this.secretStore = new SecretStore({
|
|
@@ -1712,9 +1731,37 @@ var ProxyWasmRunner = class {
|
|
|
1712
1731
|
console.error("Failed to load dotenv files:", error);
|
|
1713
1732
|
}
|
|
1714
1733
|
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Apply dotenv settings to the running module without recompiling.
|
|
1736
|
+
* Resets SecretStore/Dictionary to empty, then re-loads dotenv files
|
|
1737
|
+
* using the new settings. The compiled WASM module is untouched.
|
|
1738
|
+
*/
|
|
1739
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
1740
|
+
this.dotenvEnabled = enabled;
|
|
1741
|
+
if (dotenvPath !== void 0) {
|
|
1742
|
+
this.dotenvPath = dotenvPath;
|
|
1743
|
+
}
|
|
1744
|
+
this.secretStore = new SecretStore();
|
|
1745
|
+
this.dictionary = new Dictionary();
|
|
1746
|
+
await this.loadDotenvIfEnabled();
|
|
1747
|
+
if (!enabled) {
|
|
1748
|
+
this.hostFunctions = new HostFunctions(
|
|
1749
|
+
this.memory,
|
|
1750
|
+
this.propertyResolver,
|
|
1751
|
+
this.propertyAccessControl,
|
|
1752
|
+
() => this.currentHook,
|
|
1753
|
+
this.debug,
|
|
1754
|
+
this.secretStore,
|
|
1755
|
+
this.dictionary
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1715
1759
|
async load(bufferOrPath, config) {
|
|
1716
|
-
if (config?.
|
|
1717
|
-
this.dotenvEnabled = config.
|
|
1760
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
1761
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
1762
|
+
}
|
|
1763
|
+
if (config?.dotenv?.path !== void 0) {
|
|
1764
|
+
this.dotenvPath = config.dotenv.path;
|
|
1718
1765
|
}
|
|
1719
1766
|
this.resetState();
|
|
1720
1767
|
let buffer;
|
|
@@ -1785,6 +1832,25 @@ var ProxyWasmRunner = class {
|
|
|
1785
1832
|
"system"
|
|
1786
1833
|
);
|
|
1787
1834
|
}
|
|
1835
|
+
if (results.onRequestHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1836
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1837
|
+
const responseHeaders = results.onRequestHeaders.output.response.headers;
|
|
1838
|
+
this.hostFunctions.resetLocalResponse();
|
|
1839
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1840
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1841
|
+
return {
|
|
1842
|
+
hookResults: results,
|
|
1843
|
+
finalResponse: {
|
|
1844
|
+
status: local.statusCode,
|
|
1845
|
+
statusText: local.statusText,
|
|
1846
|
+
headers: responseHeaders,
|
|
1847
|
+
body,
|
|
1848
|
+
contentType,
|
|
1849
|
+
isBase64
|
|
1850
|
+
},
|
|
1851
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1788
1854
|
const headersAfterRequestHeaders = results.onRequestHeaders.output.request.headers;
|
|
1789
1855
|
const propertiesAfterRequestHeaders = results.onRequestHeaders.properties;
|
|
1790
1856
|
this.logDebug(
|
|
@@ -1809,6 +1875,25 @@ var ProxyWasmRunner = class {
|
|
|
1809
1875
|
"system"
|
|
1810
1876
|
);
|
|
1811
1877
|
}
|
|
1878
|
+
if (results.onRequestBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1879
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1880
|
+
const responseHeaders = results.onRequestBody.output.response.headers;
|
|
1881
|
+
this.hostFunctions.resetLocalResponse();
|
|
1882
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1883
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1884
|
+
return {
|
|
1885
|
+
hookResults: results,
|
|
1886
|
+
finalResponse: {
|
|
1887
|
+
status: local.statusCode,
|
|
1888
|
+
statusText: local.statusText,
|
|
1889
|
+
headers: responseHeaders,
|
|
1890
|
+
body,
|
|
1891
|
+
contentType,
|
|
1892
|
+
isBase64
|
|
1893
|
+
},
|
|
1894
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1812
1897
|
const modifiedRequestHeaders = results.onRequestBody.output.request.headers;
|
|
1813
1898
|
const modifiedRequestBody = results.onRequestBody.output.request.body;
|
|
1814
1899
|
const propertiesAfterRequestBody = results.onRequestBody.properties;
|
|
@@ -1997,6 +2082,7 @@ var ProxyWasmRunner = class {
|
|
|
1997
2082
|
throw new Error("WASM module not loaded");
|
|
1998
2083
|
}
|
|
1999
2084
|
this.currentHook = this.getHookContext(call.hook);
|
|
2085
|
+
this.hostFunctions.resetLocalResponse();
|
|
2000
2086
|
const imports = this.createImports();
|
|
2001
2087
|
this.instance = await WebAssembly.instantiate(this.module, imports);
|
|
2002
2088
|
const memory = this.instance.exports.memory;
|
|
@@ -2395,7 +2481,7 @@ var ProxyWasmRunner = class {
|
|
|
2395
2481
|
/**
|
|
2396
2482
|
* Not supported for Proxy-WASM (HTTP WASM only)
|
|
2397
2483
|
*/
|
|
2398
|
-
async execute(
|
|
2484
|
+
async execute(request) {
|
|
2399
2485
|
throw new Error(
|
|
2400
2486
|
"execute() is not supported for Proxy-WASM. Use callHook() or callFullFlow() instead."
|
|
2401
2487
|
);
|
|
@@ -2430,16 +2516,24 @@ function ensureNullTerminated(value) {
|
|
|
2430
2516
|
}
|
|
2431
2517
|
return value.endsWith("\0") ? value : `${value}\0`;
|
|
2432
2518
|
}
|
|
2519
|
+
function encodeLocalResponseBody(body, contentType) {
|
|
2520
|
+
const isBinary = contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/") || contentType.includes("application/octet-stream") || contentType.includes("application/pdf") || contentType.includes("application/zip");
|
|
2521
|
+
if (isBinary) {
|
|
2522
|
+
return { body: Buffer.from(body).toString("base64"), isBase64: true };
|
|
2523
|
+
}
|
|
2524
|
+
return { body: new TextDecoder().decode(body), isBase64: false };
|
|
2525
|
+
}
|
|
2433
2526
|
|
|
2434
2527
|
// server/runner/HttpWasmRunner.ts
|
|
2435
|
-
import { spawn } from "child_process";
|
|
2436
|
-
import * as http from "http";
|
|
2528
|
+
import { spawn, execSync as execSync2 } from "child_process";
|
|
2437
2529
|
|
|
2438
2530
|
// server/utils/fastedge-cli.ts
|
|
2439
2531
|
import { execSync } from "child_process";
|
|
2440
|
-
import { existsSync } from "fs";
|
|
2441
|
-
import { join } from "path";
|
|
2532
|
+
import { existsSync, chmodSync } from "fs";
|
|
2533
|
+
import { join, dirname } from "path";
|
|
2534
|
+
import { fileURLToPath } from "url";
|
|
2442
2535
|
import os from "os";
|
|
2536
|
+
var _currentDir = dirname(fileURLToPath(import.meta.url));
|
|
2443
2537
|
function getCliBinaryName() {
|
|
2444
2538
|
switch (os.platform()) {
|
|
2445
2539
|
case "win32":
|
|
@@ -2455,19 +2549,30 @@ function getCliBinaryName() {
|
|
|
2455
2549
|
function getBundledCliPaths() {
|
|
2456
2550
|
const binaryName = getCliBinaryName();
|
|
2457
2551
|
return [
|
|
2458
|
-
//
|
|
2459
|
-
join(
|
|
2552
|
+
// Installed npm package: dist/lib/index.js → dist/fastedge-cli/
|
|
2553
|
+
join(_currentDir, "..", "fastedge-cli", binaryName),
|
|
2554
|
+
// Production: bundled server at dist/server.js → dist/fastedge-cli/
|
|
2555
|
+
join(_currentDir, "fastedge-cli", binaryName),
|
|
2460
2556
|
// Development/Tests: running from source
|
|
2461
|
-
//
|
|
2462
|
-
join(
|
|
2463
|
-
// Alternative: if
|
|
2464
|
-
join(
|
|
2557
|
+
// _currentDir might be server/utils/, so go up to project root
|
|
2558
|
+
join(_currentDir, "..", "..", "fastedge-run", binaryName),
|
|
2559
|
+
// Alternative: if _currentDir is already at project root
|
|
2560
|
+
join(_currentDir, "fastedge-run", binaryName)
|
|
2465
2561
|
];
|
|
2466
2562
|
}
|
|
2563
|
+
function ensureExecutable(binaryPath) {
|
|
2564
|
+
if (process.platform !== "win32") {
|
|
2565
|
+
try {
|
|
2566
|
+
chmodSync(binaryPath, 493);
|
|
2567
|
+
} catch {
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2467
2571
|
async function findFastEdgeRunCli() {
|
|
2468
2572
|
const envPath = process.env.FASTEDGE_RUN_PATH;
|
|
2469
2573
|
if (envPath) {
|
|
2470
2574
|
if (existsSync(envPath)) {
|
|
2575
|
+
ensureExecutable(envPath);
|
|
2471
2576
|
return envPath;
|
|
2472
2577
|
} else {
|
|
2473
2578
|
throw new Error(
|
|
@@ -2477,6 +2582,7 @@ async function findFastEdgeRunCli() {
|
|
|
2477
2582
|
}
|
|
2478
2583
|
for (const bundledPath of getBundledCliPaths()) {
|
|
2479
2584
|
if (existsSync(bundledPath)) {
|
|
2585
|
+
ensureExecutable(bundledPath);
|
|
2480
2586
|
return bundledPath;
|
|
2481
2587
|
}
|
|
2482
2588
|
}
|
|
@@ -2524,9 +2630,12 @@ var HttpWasmRunner = class {
|
|
|
2524
2630
|
this.port = null;
|
|
2525
2631
|
this.cliPath = null;
|
|
2526
2632
|
this.tempWasmPath = null;
|
|
2633
|
+
this.currentWasmPath = null;
|
|
2634
|
+
// resolved path used when spawning
|
|
2527
2635
|
this.logs = [];
|
|
2528
2636
|
this.stateManager = null;
|
|
2529
2637
|
this.dotenvEnabled = true;
|
|
2638
|
+
this.dotenvPath = null;
|
|
2530
2639
|
this.portManager = portManager;
|
|
2531
2640
|
this.dotenvEnabled = dotenvEnabled;
|
|
2532
2641
|
}
|
|
@@ -2534,8 +2643,11 @@ var HttpWasmRunner = class {
|
|
|
2534
2643
|
* Load WASM binary and spawn fastedge-run process
|
|
2535
2644
|
*/
|
|
2536
2645
|
async load(bufferOrPath, config) {
|
|
2537
|
-
if (config?.
|
|
2538
|
-
this.dotenvEnabled = config.
|
|
2646
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
2647
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
2648
|
+
}
|
|
2649
|
+
if (config?.dotenv?.path !== void 0) {
|
|
2650
|
+
this.dotenvPath = config.dotenv.path;
|
|
2539
2651
|
}
|
|
2540
2652
|
await this.cleanup();
|
|
2541
2653
|
this.cliPath = await findFastEdgeRunCli();
|
|
@@ -2547,7 +2659,7 @@ var HttpWasmRunner = class {
|
|
|
2547
2659
|
wasmPath = await writeTempWasmFile(bufferOrPath);
|
|
2548
2660
|
this.tempWasmPath = wasmPath;
|
|
2549
2661
|
}
|
|
2550
|
-
this.port = this.portManager.allocate();
|
|
2662
|
+
this.port = await this.portManager.allocate();
|
|
2551
2663
|
const args = [
|
|
2552
2664
|
"http",
|
|
2553
2665
|
"-p",
|
|
@@ -2558,8 +2670,13 @@ var HttpWasmRunner = class {
|
|
|
2558
2670
|
"true"
|
|
2559
2671
|
];
|
|
2560
2672
|
if (this.dotenvEnabled) {
|
|
2561
|
-
|
|
2673
|
+
if (this.dotenvPath) {
|
|
2674
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2675
|
+
} else {
|
|
2676
|
+
args.push("--dotenv");
|
|
2677
|
+
}
|
|
2562
2678
|
}
|
|
2679
|
+
this.currentWasmPath = wasmPath;
|
|
2563
2680
|
this.process = spawn(this.cliPath, args, {
|
|
2564
2681
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2565
2682
|
env: {
|
|
@@ -2575,17 +2692,17 @@ var HttpWasmRunner = class {
|
|
|
2575
2692
|
/**
|
|
2576
2693
|
* Execute an HTTP request through the WASM module
|
|
2577
2694
|
*/
|
|
2578
|
-
async execute(
|
|
2695
|
+
async execute(request) {
|
|
2579
2696
|
if (!this.port || !this.process) {
|
|
2580
2697
|
throw new Error("HttpWasmRunner not loaded. Call load() first.");
|
|
2581
2698
|
}
|
|
2582
2699
|
this.logs = [];
|
|
2583
2700
|
try {
|
|
2584
|
-
const url = `http://localhost:${this.port}${
|
|
2701
|
+
const url = `http://localhost:${this.port}${request.path}`;
|
|
2585
2702
|
const response = await fetch(url, {
|
|
2586
|
-
method:
|
|
2587
|
-
headers:
|
|
2588
|
-
body:
|
|
2703
|
+
method: request.method,
|
|
2704
|
+
headers: request.headers,
|
|
2705
|
+
body: request.body || void 0,
|
|
2589
2706
|
signal: AbortSignal.timeout(3e4)
|
|
2590
2707
|
// 30 second timeout
|
|
2591
2708
|
});
|
|
@@ -2613,7 +2730,7 @@ var HttpWasmRunner = class {
|
|
|
2613
2730
|
/**
|
|
2614
2731
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2615
2732
|
*/
|
|
2616
|
-
async callHook(
|
|
2733
|
+
async callHook(_hookCall) {
|
|
2617
2734
|
throw new Error(
|
|
2618
2735
|
"callHook() is not supported for HTTP WASM. Use execute() instead."
|
|
2619
2736
|
);
|
|
@@ -2621,11 +2738,53 @@ var HttpWasmRunner = class {
|
|
|
2621
2738
|
/**
|
|
2622
2739
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2623
2740
|
*/
|
|
2624
|
-
async callFullFlow(
|
|
2741
|
+
async callFullFlow(_url, _method, _headers, _body, _responseHeaders, _responseBody, _responseStatus, _responseStatusText, _properties, _enforceProductionPropertyRules) {
|
|
2625
2742
|
throw new Error(
|
|
2626
2743
|
"callFullFlow() is not supported for HTTP WASM. Use execute() instead."
|
|
2627
2744
|
);
|
|
2628
2745
|
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Apply dotenv settings by restarting the fastedge-run process.
|
|
2748
|
+
* The WASM file is not re-read; only the --dotenv flag changes.
|
|
2749
|
+
*/
|
|
2750
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
2751
|
+
this.dotenvEnabled = enabled;
|
|
2752
|
+
if (dotenvPath !== void 0) {
|
|
2753
|
+
this.dotenvPath = dotenvPath;
|
|
2754
|
+
}
|
|
2755
|
+
if (!this.process || !this.currentWasmPath || !this.cliPath || this.port === null) {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
await this.killProcess();
|
|
2759
|
+
this.process = null;
|
|
2760
|
+
const args = [
|
|
2761
|
+
"http",
|
|
2762
|
+
"-p",
|
|
2763
|
+
this.port.toString(),
|
|
2764
|
+
"-w",
|
|
2765
|
+
this.currentWasmPath,
|
|
2766
|
+
"--wasi-http",
|
|
2767
|
+
"true"
|
|
2768
|
+
];
|
|
2769
|
+
if (this.dotenvEnabled) {
|
|
2770
|
+
if (this.dotenvPath) {
|
|
2771
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2772
|
+
} else {
|
|
2773
|
+
args.push("--dotenv");
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
this.process = spawn(this.cliPath, args, {
|
|
2777
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2778
|
+
env: {
|
|
2779
|
+
RUST_LOG: "info",
|
|
2780
|
+
...process.env
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
this.setupLogCapture();
|
|
2784
|
+
this.setupErrorHandlers();
|
|
2785
|
+
const timeout = process.env.NODE_ENV === "test" || process.env.VITEST ? 2e4 : 1e4;
|
|
2786
|
+
await this.waitForServerReady(this.port, timeout);
|
|
2787
|
+
}
|
|
2629
2788
|
/**
|
|
2630
2789
|
* Clean up resources
|
|
2631
2790
|
*/
|
|
@@ -2650,12 +2809,41 @@ var HttpWasmRunner = class {
|
|
|
2650
2809
|
getType() {
|
|
2651
2810
|
return "http-wasm";
|
|
2652
2811
|
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Get the port the fastedge-run HTTP server is listening on
|
|
2814
|
+
*/
|
|
2815
|
+
getPort() {
|
|
2816
|
+
return this.port;
|
|
2817
|
+
}
|
|
2653
2818
|
/**
|
|
2654
2819
|
* Set state manager
|
|
2655
2820
|
*/
|
|
2656
2821
|
setStateManager(stateManager) {
|
|
2657
2822
|
this.stateManager = stateManager;
|
|
2658
2823
|
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Parse log level from a process output line.
|
|
2826
|
+
* Matches bare prefixes (e.g. "INFO target > msg") and bracketed prefixes (e.g. "[INFO] msg").
|
|
2827
|
+
* Falls back to the provided default if no known level prefix is found.
|
|
2828
|
+
*/
|
|
2829
|
+
parseLogLevel(message, fallback) {
|
|
2830
|
+
const match = message.trimStart().match(/^\[?(\w+)\]?/);
|
|
2831
|
+
const prefix = match?.[1]?.toUpperCase();
|
|
2832
|
+
switch (prefix) {
|
|
2833
|
+
case "TRACE":
|
|
2834
|
+
return 0;
|
|
2835
|
+
case "DEBUG":
|
|
2836
|
+
return 1;
|
|
2837
|
+
case "INFO":
|
|
2838
|
+
return 2;
|
|
2839
|
+
case "WARN":
|
|
2840
|
+
return 3;
|
|
2841
|
+
case "ERROR":
|
|
2842
|
+
return 4;
|
|
2843
|
+
default:
|
|
2844
|
+
return fallback;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2659
2847
|
/**
|
|
2660
2848
|
* Setup log capture from process stdout/stderr
|
|
2661
2849
|
*/
|
|
@@ -2664,13 +2852,17 @@ var HttpWasmRunner = class {
|
|
|
2664
2852
|
this.process.stdout?.on("data", (data) => {
|
|
2665
2853
|
const message = data.toString().trim();
|
|
2666
2854
|
if (message) {
|
|
2667
|
-
|
|
2855
|
+
const log = { level: this.parseLogLevel(message, 2), message };
|
|
2856
|
+
this.logs.push(log);
|
|
2857
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2668
2858
|
}
|
|
2669
2859
|
});
|
|
2670
2860
|
this.process.stderr?.on("data", (data) => {
|
|
2671
2861
|
const message = data.toString().trim();
|
|
2672
2862
|
if (message) {
|
|
2673
|
-
|
|
2863
|
+
const log = { level: this.parseLogLevel(message, 4), message };
|
|
2864
|
+
this.logs.push(log);
|
|
2865
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2674
2866
|
}
|
|
2675
2867
|
});
|
|
2676
2868
|
}
|
|
@@ -2700,66 +2892,54 @@ var HttpWasmRunner = class {
|
|
|
2700
2892
|
});
|
|
2701
2893
|
}
|
|
2702
2894
|
/**
|
|
2703
|
-
*
|
|
2704
|
-
*
|
|
2705
|
-
*
|
|
2706
|
-
*
|
|
2707
|
-
*
|
|
2708
|
-
*
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
{
|
|
2714
|
-
hostname: "localhost",
|
|
2715
|
-
port,
|
|
2716
|
-
path: "/",
|
|
2717
|
-
method: "GET",
|
|
2718
|
-
headers: {
|
|
2719
|
-
"content-length": "0",
|
|
2720
|
-
connection: "close"
|
|
2721
|
-
}
|
|
2722
|
-
},
|
|
2723
|
-
(res) => {
|
|
2724
|
-
res.resume();
|
|
2725
|
-
resolve(true);
|
|
2726
|
-
}
|
|
2727
|
-
);
|
|
2728
|
-
req.setTimeout(timeoutMs, () => {
|
|
2729
|
-
req.destroy();
|
|
2730
|
-
resolve(false);
|
|
2731
|
-
});
|
|
2732
|
-
req.on("error", () => resolve(false));
|
|
2733
|
-
req.end();
|
|
2734
|
-
});
|
|
2735
|
-
}
|
|
2736
|
-
/**
|
|
2737
|
-
* Wait for server to be ready by polling
|
|
2895
|
+
* Wait for the fastedge-run HTTP server to be ready by watching process logs
|
|
2896
|
+
* for the "Listening on" message.
|
|
2897
|
+
*
|
|
2898
|
+
* We intentionally avoid HTTP probing here. HTTP probes trigger WASM execution
|
|
2899
|
+
* (the app calls event.request.text() to read the body), which can hang for
|
|
2900
|
+
* many seconds in CI due to WASM JIT compilation on the first request, or
|
|
2901
|
+
* because newer fastedge-run builds hold the body stream open regardless of
|
|
2902
|
+
* content-length. Watching logs avoids all of this: fastedge-run emits
|
|
2903
|
+
* "Listening on http://127.0.0.1:<port>" as soon as the HTTP listener is bound,
|
|
2904
|
+
* before any WASM execution occurs.
|
|
2738
2905
|
*/
|
|
2739
|
-
|
|
2906
|
+
waitForServerReady(port, timeoutMs) {
|
|
2740
2907
|
const startTime = Date.now();
|
|
2741
|
-
|
|
2742
|
-
const
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2908
|
+
return new Promise((resolve, reject) => {
|
|
2909
|
+
const check = () => {
|
|
2910
|
+
if (this.process && this.process.exitCode !== null) {
|
|
2911
|
+
return reject(
|
|
2912
|
+
new Error(
|
|
2913
|
+
`FastEdge-run process exited with code ${this.process.exitCode} before server started`
|
|
2914
|
+
)
|
|
2915
|
+
);
|
|
2916
|
+
}
|
|
2917
|
+
if (this.logs.some((l) => l.message.includes("Listening on"))) {
|
|
2918
|
+
return resolve();
|
|
2919
|
+
}
|
|
2920
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
2921
|
+
const processInfo = this.process ? `Process state: exitCode=${this.process.exitCode}, killed=${this.process.killed}, pid=${this.process.pid}` : "Process is null";
|
|
2922
|
+
const recentLogs = this.logs.slice(-5).map((l) => `[${l.level}] ${l.message}`).join("\n");
|
|
2923
|
+
return reject(
|
|
2924
|
+
new Error(
|
|
2925
|
+
`FastEdge-run server did not start within ${timeoutMs}ms on port ${port}
|
|
2755
2926
|
${processInfo}
|
|
2756
2927
|
Recent logs:
|
|
2757
2928
|
${recentLogs || "(no logs)"}`
|
|
2758
|
-
|
|
2929
|
+
)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
setTimeout(check, 50);
|
|
2933
|
+
};
|
|
2934
|
+
check();
|
|
2935
|
+
});
|
|
2759
2936
|
}
|
|
2760
2937
|
/**
|
|
2761
|
-
* Kill the process gracefully (SIGINT) with
|
|
2762
|
-
*
|
|
2938
|
+
* Kill the process gracefully (SIGINT) with platform-specific force-kill fallback.
|
|
2939
|
+
* SIGINT is sent first on all platforms — Node.js translates it for Windows.
|
|
2940
|
+
* If the process does not exit within 2 seconds:
|
|
2941
|
+
* - Windows: taskkill /F /T to terminate the process tree
|
|
2942
|
+
* - Unix: SIGKILL
|
|
2763
2943
|
*/
|
|
2764
2944
|
async killProcess() {
|
|
2765
2945
|
if (!this.process) return;
|
|
@@ -2770,8 +2950,18 @@ ${recentLogs || "(no logs)"}`
|
|
|
2770
2950
|
}
|
|
2771
2951
|
this.process.kill("SIGINT");
|
|
2772
2952
|
const timeout = setTimeout(() => {
|
|
2773
|
-
if (this.process &&
|
|
2774
|
-
|
|
2953
|
+
if (this.process && this.process.exitCode === null && this.process.signalCode === null) {
|
|
2954
|
+
if (process.platform === "win32") {
|
|
2955
|
+
const pid = this.process.pid;
|
|
2956
|
+
if (pid) {
|
|
2957
|
+
try {
|
|
2958
|
+
execSync2(`taskkill /F /T /PID ${pid}`);
|
|
2959
|
+
} catch {
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
} else {
|
|
2963
|
+
this.process.kill("SIGKILL");
|
|
2964
|
+
}
|
|
2775
2965
|
}
|
|
2776
2966
|
resolve();
|
|
2777
2967
|
}, 2e3);
|
|
@@ -2794,9 +2984,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2794
2984
|
"application/zip",
|
|
2795
2985
|
"application/gzip"
|
|
2796
2986
|
];
|
|
2797
|
-
return binaryTypes.some(
|
|
2798
|
-
(type) => contentType.toLowerCase().includes(type)
|
|
2799
|
-
);
|
|
2987
|
+
return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
|
|
2800
2988
|
}
|
|
2801
2989
|
/**
|
|
2802
2990
|
* Parse headers from fetch Headers object
|
|
@@ -2811,6 +2999,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2811
2999
|
};
|
|
2812
3000
|
|
|
2813
3001
|
// server/runner/PortManager.ts
|
|
3002
|
+
import { createServer } from "net";
|
|
2814
3003
|
var PortManager = class {
|
|
2815
3004
|
constructor() {
|
|
2816
3005
|
this.minPort = 8100;
|
|
@@ -2818,18 +3007,31 @@ var PortManager = class {
|
|
|
2818
3007
|
this.allocatedPorts = /* @__PURE__ */ new Set();
|
|
2819
3008
|
this.lastAllocatedPort = this.minPort - 1;
|
|
2820
3009
|
}
|
|
2821
|
-
// Track last allocated port for sequential allocation
|
|
2822
3010
|
/**
|
|
2823
|
-
*
|
|
2824
|
-
*
|
|
2825
|
-
*
|
|
3011
|
+
* Check whether a port is actually free at the OS level.
|
|
3012
|
+
* This is necessary when multiple server processes run simultaneously —
|
|
3013
|
+
* each has its own PortManager with independent in-memory state, so
|
|
3014
|
+
* in-memory tracking alone is not enough to prevent cross-process conflicts.
|
|
3015
|
+
*/
|
|
3016
|
+
isPortFree(port) {
|
|
3017
|
+
return new Promise((resolve) => {
|
|
3018
|
+
const server = createServer();
|
|
3019
|
+
server.once("error", () => resolve(false));
|
|
3020
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
3021
|
+
server.listen(port, "127.0.0.1");
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Allocate an available port from the pool.
|
|
3026
|
+
* Combines in-memory tracking (avoids TCP TIME_WAIT reuse within this process)
|
|
3027
|
+
* with an OS-level check (avoids cross-process collisions).
|
|
2826
3028
|
* @returns The allocated port number
|
|
2827
3029
|
* @throws Error if no ports are available
|
|
2828
3030
|
*/
|
|
2829
|
-
allocate() {
|
|
3031
|
+
async allocate() {
|
|
2830
3032
|
for (let offset = 1; offset <= this.maxPort - this.minPort + 1; offset++) {
|
|
2831
3033
|
const port = this.minPort + (this.lastAllocatedPort - this.minPort + offset) % (this.maxPort - this.minPort + 1);
|
|
2832
|
-
if (!this.allocatedPorts.has(port)) {
|
|
3034
|
+
if (!this.allocatedPorts.has(port) && await this.isPortFree(port)) {
|
|
2833
3035
|
this.allocatedPorts.add(port);
|
|
2834
3036
|
this.lastAllocatedPort = port;
|
|
2835
3037
|
return port;
|
|
@@ -2841,32 +3043,19 @@ var PortManager = class {
|
|
|
2841
3043
|
}
|
|
2842
3044
|
/**
|
|
2843
3045
|
* Release a previously allocated port back to the pool
|
|
2844
|
-
* @param port The port number to release
|
|
2845
3046
|
*/
|
|
2846
3047
|
release(port) {
|
|
2847
3048
|
this.allocatedPorts.delete(port);
|
|
2848
3049
|
}
|
|
2849
|
-
/**
|
|
2850
|
-
* Get the number of currently allocated ports
|
|
2851
|
-
*/
|
|
2852
3050
|
getAllocatedCount() {
|
|
2853
3051
|
return this.allocatedPorts.size;
|
|
2854
3052
|
}
|
|
2855
|
-
/**
|
|
2856
|
-
* Get the number of available ports
|
|
2857
|
-
*/
|
|
2858
3053
|
getAvailableCount() {
|
|
2859
3054
|
return this.maxPort - this.minPort + 1 - this.allocatedPorts.size;
|
|
2860
3055
|
}
|
|
2861
|
-
/**
|
|
2862
|
-
* Check if a specific port is allocated
|
|
2863
|
-
*/
|
|
2864
3056
|
isAllocated(port) {
|
|
2865
3057
|
return this.allocatedPorts.has(port);
|
|
2866
3058
|
}
|
|
2867
|
-
/**
|
|
2868
|
-
* Reset all allocations (useful for testing)
|
|
2869
|
-
*/
|
|
2870
3059
|
reset() {
|
|
2871
3060
|
this.allocatedPorts.clear();
|
|
2872
3061
|
}
|
|
@@ -2916,6 +3105,8 @@ var NullStateManager = class {
|
|
|
2916
3105
|
}
|
|
2917
3106
|
emitHttpWasmRequestCompleted() {
|
|
2918
3107
|
}
|
|
3108
|
+
emitHttpWasmLog() {
|
|
3109
|
+
}
|
|
2919
3110
|
emitReloadWorkspaceWasm() {
|
|
2920
3111
|
}
|
|
2921
3112
|
};
|
|
@@ -2962,9 +3153,9 @@ async function createRunnerFromBuffer(buffer, config) {
|
|
|
2962
3153
|
const wasmType = config?.runnerType ?? await detectWasmType(buffer);
|
|
2963
3154
|
let runner;
|
|
2964
3155
|
if (wasmType === "http-wasm") {
|
|
2965
|
-
runner = new HttpWasmRunner(new PortManager(), config?.
|
|
3156
|
+
runner = new HttpWasmRunner(new PortManager(), config?.dotenv?.enabled ?? false);
|
|
2966
3157
|
} else {
|
|
2967
|
-
runner = new ProxyWasmRunner(void 0, config?.
|
|
3158
|
+
runner = new ProxyWasmRunner(void 0, config?.dotenv?.enabled ?? false);
|
|
2968
3159
|
}
|
|
2969
3160
|
await runner.load(buffer, config);
|
|
2970
3161
|
return runner;
|