@bytecodealliance/preview3-shim 0.2.1 → 0.3.0
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/dist/browser/cli/environment.d.ts +10 -0
- package/dist/browser/cli/environment.js +15 -0
- package/dist/browser/cli/exit.d.ts +9 -0
- package/dist/browser/cli/exit.js +11 -0
- package/dist/browser/cli/run.d.ts +6 -0
- package/dist/browser/cli/run.js +7 -0
- package/dist/browser/cli/stderr.d.ts +7 -0
- package/dist/browser/cli/stderr.js +7 -0
- package/dist/browser/cli/stdin.d.ts +7 -0
- package/dist/browser/cli/stdin.js +7 -0
- package/dist/browser/cli/stdout.d.ts +7 -0
- package/dist/browser/cli/stdout.js +7 -0
- package/dist/browser/cli/terminal-input.d.ts +8 -0
- package/dist/browser/cli/terminal-input.js +5 -0
- package/dist/browser/cli/terminal-output.d.ts +8 -0
- package/dist/browser/cli/terminal-output.js +5 -0
- package/dist/browser/cli/terminal-stderr.d.ts +7 -0
- package/dist/browser/cli/terminal-stderr.js +7 -0
- package/dist/browser/cli/terminal-stdin.d.ts +7 -0
- package/dist/browser/cli/terminal-stdin.js +7 -0
- package/dist/browser/cli/terminal-stdout.d.ts +7 -0
- package/dist/browser/cli/terminal-stdout.js +7 -0
- package/dist/browser/cli.d.ts +11 -0
- package/dist/browser/cli.js +11 -0
- package/dist/browser/clocks/monotonic-clock.d.ts +13 -0
- package/dist/browser/clocks/monotonic-clock.js +19 -0
- package/dist/browser/clocks/system-clock.d.ts +9 -0
- package/dist/browser/clocks/system-clock.js +11 -0
- package/dist/browser/clocks/types.d.ts +3 -0
- package/dist/browser/clocks/types.js +1 -0
- package/dist/browser/clocks.d.ts +3 -0
- package/dist/browser/clocks.js +3 -0
- package/dist/browser/filesystem/preopens.d.ts +7 -0
- package/dist/browser/filesystem/preopens.js +7 -0
- package/dist/browser/filesystem/types.d.ts +33 -0
- package/dist/browser/filesystem/types.js +81 -0
- package/dist/browser/filesystem.d.ts +2 -0
- package/dist/browser/filesystem.js +2 -0
- package/dist/browser/http/client.d.ts +7 -0
- package/dist/browser/http/client.js +7 -0
- package/dist/browser/http/handler.d.ts +7 -0
- package/dist/browser/http/handler.js +7 -0
- package/dist/browser/http/types.d.ts +50 -0
- package/dist/browser/http/types.js +114 -0
- package/dist/browser/http.d.ts +3 -0
- package/dist/browser/http.js +3 -0
- package/dist/browser/index.d.ts +6 -0
- package/dist/browser/index.js +6 -0
- package/dist/browser/random/insecure-seed.d.ts +6 -0
- package/dist/browser/random/insecure-seed.js +7 -0
- package/dist/browser/random/insecure.d.ts +8 -0
- package/dist/browser/random/insecure.js +11 -0
- package/dist/browser/random/random.d.ts +8 -0
- package/dist/browser/random/random.js +11 -0
- package/dist/browser/random.d.ts +3 -0
- package/dist/browser/random.js +3 -0
- package/dist/browser/sockets/ip-name-lookup.d.ts +7 -0
- package/dist/browser/sockets/ip-name-lookup.js +7 -0
- package/dist/browser/sockets/types.d.ts +51 -0
- package/dist/browser/sockets/types.js +129 -0
- package/dist/browser/sockets.d.ts +2 -0
- package/dist/browser/sockets.js +2 -0
- package/dist/common/errors.d.ts +3 -0
- package/dist/common/errors.js +5 -0
- package/dist/nodejs/cli.js +2 -1
- package/dist/nodejs/filesystem/descriptor.js +2 -1
- package/dist/nodejs/http/client.js +2 -1
- package/dist/nodejs/http/server.js +2 -1
- package/dist/nodejs/sockets/tcp.js +2 -1
- package/dist/nodejs/sockets/udp.js +2 -1
- package/dist/nodejs/workers/cli-worker.bundle.js +88 -0
- package/dist/nodejs/workers/filesystem-worker.bundle.js +236 -0
- package/dist/nodejs/workers/http-worker.bundle.js +403 -0
- package/dist/nodejs/workers/resource-worker.d.ts +1 -1
- package/dist/nodejs/workers/resource-worker.js +5 -5
- package/dist/nodejs/workers/tcp-worker.bundle.js +496 -0
- package/dist/nodejs/workers/udp-worker.bundle.js +270 -0
- package/package.json +15 -6
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { promisify } from "util";
|
|
2
|
+
import { isMainThread, parentPort } from "worker_threads";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
//#region dist/nodejs/workers/resource-worker.js
|
|
5
|
+
function Router() {
|
|
6
|
+
const _hooks = {
|
|
7
|
+
beforeAll: null,
|
|
8
|
+
ops: /* @__PURE__ */ new Map()
|
|
9
|
+
};
|
|
10
|
+
if (isMainThread || !parentPort) throw new Error("Router can only be used in worker threads");
|
|
11
|
+
parentPort.on("message", async (msg) => {
|
|
12
|
+
const { _reply, _condvar, ...rest } = msg;
|
|
13
|
+
let result, error, transfer = [];
|
|
14
|
+
try {
|
|
15
|
+
if (_hooks.beforeAll) _hooks.beforeAll(rest);
|
|
16
|
+
const handler = _hooks.ops.get(rest.op);
|
|
17
|
+
if (!handler) throw new Error(`Unknown op ${rest.op}`);
|
|
18
|
+
const outcome = await handler(rest);
|
|
19
|
+
if (outcome && Array.isArray(outcome.transferable)) {
|
|
20
|
+
result = outcome.result;
|
|
21
|
+
transfer = outcome.transferable;
|
|
22
|
+
} else result = outcome;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
error = serializeError(err);
|
|
25
|
+
}
|
|
26
|
+
_reply.postMessage({
|
|
27
|
+
result,
|
|
28
|
+
error
|
|
29
|
+
}, transfer);
|
|
30
|
+
_reply.close();
|
|
31
|
+
if (_condvar) notify(_condvar);
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
beforeAll(fn) {
|
|
35
|
+
_hooks.beforeAll = fn;
|
|
36
|
+
return this;
|
|
37
|
+
},
|
|
38
|
+
op(name, fn) {
|
|
39
|
+
_hooks.ops.set(name, fn);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function serializeError(err) {
|
|
45
|
+
if (!(err instanceof Error)) return err;
|
|
46
|
+
const serialized = {
|
|
47
|
+
__resourceWorkerError: true,
|
|
48
|
+
name: err.name,
|
|
49
|
+
message: err.message,
|
|
50
|
+
stack: err.stack
|
|
51
|
+
};
|
|
52
|
+
for (const key of Reflect.ownKeys(err)) serialized[key] = err[key];
|
|
53
|
+
if (err.cause !== void 0) serialized.cause = serializeError(err.cause);
|
|
54
|
+
return serialized;
|
|
55
|
+
}
|
|
56
|
+
function notify(condvar) {
|
|
57
|
+
const lock = new Int32Array(condvar);
|
|
58
|
+
Atomics.store(lock, 0, 1);
|
|
59
|
+
Atomics.notify(lock, 0, 1);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region dist/nodejs/filesystem/error.js
|
|
63
|
+
const ERROR_MAP = {
|
|
64
|
+
EACCES: "access",
|
|
65
|
+
EALREADY: "already",
|
|
66
|
+
EBADF: "bad-descriptor",
|
|
67
|
+
EBUSY: "busy",
|
|
68
|
+
EDEADLK: "deadlock",
|
|
69
|
+
EDQUOT: "quota",
|
|
70
|
+
EEXIST: "exist",
|
|
71
|
+
EFBIG: "file-too-large",
|
|
72
|
+
EILSEQ: "illegal-byte-sequence",
|
|
73
|
+
EINPROGRESS: "in-progress",
|
|
74
|
+
EINTR: "interrupted",
|
|
75
|
+
EINVAL: "invalid",
|
|
76
|
+
EIO: "io",
|
|
77
|
+
EISDIR: "is-directory",
|
|
78
|
+
ELOOP: "loop",
|
|
79
|
+
EMLINK: "too-many-links",
|
|
80
|
+
EMSGSIZE: "message-size",
|
|
81
|
+
ENAMETOOLONG: "name-too-long",
|
|
82
|
+
ENODEV: "no-device",
|
|
83
|
+
ENOENT: "no-entry",
|
|
84
|
+
ENOLCK: "no-lock",
|
|
85
|
+
ENOMEM: "insufficient-memory",
|
|
86
|
+
ENOSPC: "insufficient-space",
|
|
87
|
+
ENOTDIR: "not-directory",
|
|
88
|
+
ERR_FS_EISDIR: "not-directory",
|
|
89
|
+
ENOTEMPTY: "not-empty",
|
|
90
|
+
ENOTRECOVERABLE: "not-recoverable",
|
|
91
|
+
ENOTSUP: "unsupported",
|
|
92
|
+
ENOTTY: "no-tty",
|
|
93
|
+
ENXIO: "no-such-device",
|
|
94
|
+
EOVERFLOW: "overflow",
|
|
95
|
+
EPERM: "not-permitted",
|
|
96
|
+
EPIPE: "pipe",
|
|
97
|
+
EROFS: "read-only",
|
|
98
|
+
ESPIPE: "invalid-seek",
|
|
99
|
+
ETXTBSY: "text-file-busy",
|
|
100
|
+
EXDEV: "cross-device"
|
|
101
|
+
};
|
|
102
|
+
function getErrorTag(e) {
|
|
103
|
+
if (e.code in ERROR_MAP) return ERROR_MAP[e.code];
|
|
104
|
+
if (e.code === -4094 || e.code === "UNKNOWN" && e.errno === -4094) return "no-such-device";
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Custom error for File operations with JCO compatible payload.
|
|
109
|
+
*/
|
|
110
|
+
var FSError = class FSError extends Error {
|
|
111
|
+
payload;
|
|
112
|
+
/**
|
|
113
|
+
* @param {string} tag – machine‐readable error tag
|
|
114
|
+
* @param {string} [message] – human‐readable message
|
|
115
|
+
* @param {any} [val] – optional extra data
|
|
116
|
+
* @param {object} [opts]
|
|
117
|
+
* @param {Error} [opts.cause]
|
|
118
|
+
*/
|
|
119
|
+
constructor(tag, message = void 0, val = void 0, opts = {}) {
|
|
120
|
+
super(message ?? `Error: ${tag}`, { cause: opts.cause });
|
|
121
|
+
this.name = "FSError";
|
|
122
|
+
this.payload = val !== void 0 ? {
|
|
123
|
+
tag,
|
|
124
|
+
val
|
|
125
|
+
} : { tag };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create or rewrap an error into FSError
|
|
129
|
+
* @param {any} err – number, string, Error, or FSError
|
|
130
|
+
*/
|
|
131
|
+
static from(err) {
|
|
132
|
+
if (err instanceof FSError) return err;
|
|
133
|
+
const tag = getErrorTag(err);
|
|
134
|
+
const message = err?.message;
|
|
135
|
+
return new FSError(tag, message, void 0, { cause: err });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region dist/nodejs/filesystem/utils.js
|
|
140
|
+
/** Map fs.Dirent -> WASI type */
|
|
141
|
+
function wasiTypeFromDirent(obj) {
|
|
142
|
+
if (obj.isFile()) return { tag: "regular-file" };
|
|
143
|
+
else if (obj.isSocket()) return { tag: "socket" };
|
|
144
|
+
else if (obj.isSymbolicLink()) return { tag: "symbolic-link" };
|
|
145
|
+
else if (obj.isFIFO()) return { tag: "fifo" };
|
|
146
|
+
else if (obj.isDirectory()) return { tag: "directory" };
|
|
147
|
+
else if (obj.isCharacterDevice()) return { tag: "character-device" };
|
|
148
|
+
else if (obj.isBlockDevice()) return { tag: "block-device" };
|
|
149
|
+
return {
|
|
150
|
+
tag: "other",
|
|
151
|
+
val: void 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region dist/nodejs/workers/filesystem-worker.js
|
|
156
|
+
const readAsync = promisify(fs.read);
|
|
157
|
+
const writeAsync = promisify(fs.write);
|
|
158
|
+
const { opendir } = fs.promises;
|
|
159
|
+
const CHUNK_BYTES = 64 * 1024;
|
|
160
|
+
const BUFFER = Buffer.alloc(CHUNK_BYTES);
|
|
161
|
+
const MAX_SAFE_FILE_POSITION = BigInt(Number.MAX_SAFE_INTEGER);
|
|
162
|
+
/** Auto‐dispatch all ops */
|
|
163
|
+
Router().op("read", handleRead).op("write", handleWrite).op("readDir", handleReadDir);
|
|
164
|
+
async function handleRead({ fd, offset, stream }) {
|
|
165
|
+
if (typeof fd !== "number" || fd < 0) throw new TypeError(`Invalid file descriptor: ${fd}`);
|
|
166
|
+
if (!stream || typeof stream.getWriter !== "function") throw new TypeError("stream must have a getWriter() method");
|
|
167
|
+
const writer = stream.getWriter();
|
|
168
|
+
let pos = BigInt(offset);
|
|
169
|
+
try {
|
|
170
|
+
filePosition(pos);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
await writer.close();
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const start = pos;
|
|
177
|
+
while (true) {
|
|
178
|
+
const { bytesRead } = await readAsync(fd, BUFFER, 0, CHUNK_BYTES, filePosition(pos));
|
|
179
|
+
if (bytesRead === 0) break;
|
|
180
|
+
await writer.write(BUFFER.subarray(0, bytesRead));
|
|
181
|
+
pos += BigInt(bytesRead);
|
|
182
|
+
}
|
|
183
|
+
await writer.close();
|
|
184
|
+
return { bytesRead: pos - start };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
await writer.abort(err);
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function handleWrite({ fd, offset, stream }) {
|
|
191
|
+
if (typeof fd !== "number" || fd < 0) throw new TypeError(`Invalid file descriptor: ${fd}`);
|
|
192
|
+
if (!stream || typeof stream.getReader !== "function") throw new TypeError("stream must have a getReader() method");
|
|
193
|
+
const reader = stream.getReader();
|
|
194
|
+
try {
|
|
195
|
+
let pos = BigInt(offset);
|
|
196
|
+
const start = pos;
|
|
197
|
+
while (true) {
|
|
198
|
+
const { done, value } = await reader.read();
|
|
199
|
+
if (done) break;
|
|
200
|
+
const buf = Buffer.from(value);
|
|
201
|
+
const { bytesWritten } = await writeAsync(fd, buf, 0, buf.length, filePosition(pos));
|
|
202
|
+
pos += BigInt(bytesWritten);
|
|
203
|
+
}
|
|
204
|
+
return { bytesWritten: pos - start };
|
|
205
|
+
} catch (err) {
|
|
206
|
+
await reader.cancel(err);
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function filePosition(position) {
|
|
211
|
+
if (position < 0n || position > MAX_SAFE_FILE_POSITION) throw new FSError("invalid");
|
|
212
|
+
return Number(position);
|
|
213
|
+
}
|
|
214
|
+
async function handleReadDir({ fullPath, stream, preopens }) {
|
|
215
|
+
if (!stream || typeof stream.getWriter !== "function") throw new TypeError("stream must have a getWriter() method");
|
|
216
|
+
const writer = stream.getWriter();
|
|
217
|
+
if (!preopens.some((p) => fullPath === p || fullPath.startsWith(p + "/"))) {
|
|
218
|
+
const err = new FSError("not-permitted");
|
|
219
|
+
await writer.abort(err);
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const walker = await opendir(fullPath);
|
|
224
|
+
for await (const dirent of walker) await writer.write({
|
|
225
|
+
name: dirent.name,
|
|
226
|
+
type: wasiTypeFromDirent(dirent)
|
|
227
|
+
});
|
|
228
|
+
await writer.close();
|
|
229
|
+
return { ok: true };
|
|
230
|
+
} catch (err) {
|
|
231
|
+
await writer.abort(err);
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
export {};
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { PassThrough, Readable, Writable } from "node:stream";
|
|
2
|
+
import { pipeline } from "stream/promises";
|
|
3
|
+
import { MessageChannel } from "node:worker_threads";
|
|
4
|
+
import { Agent, createServer, request } from "node:http";
|
|
5
|
+
import { Agent as Agent$1, request as request$1 } from "node:https";
|
|
6
|
+
import { isMainThread, parentPort } from "worker_threads";
|
|
7
|
+
//#region dist/nodejs/http/error.js
|
|
8
|
+
/**
|
|
9
|
+
* Custom error class for WASI http interface that properly formats payload for Jco compatibility
|
|
10
|
+
*
|
|
11
|
+
* https://bytecodealliance.github.io/jco/wit-type-representations.html#result-considerations-idiomatic-js-errors-for-host-implementations
|
|
12
|
+
*/
|
|
13
|
+
var HttpError = class HttpError extends Error {
|
|
14
|
+
payload;
|
|
15
|
+
/**
|
|
16
|
+
* Create a new WASI http error
|
|
17
|
+
* @param {string} tag - The error tag/type
|
|
18
|
+
* @param {string} [message] - Human-readable error message
|
|
19
|
+
* @param {any} [val] - Optional value/data for the error
|
|
20
|
+
*/
|
|
21
|
+
constructor(tag, message = void 0, val = void 0, opts = {}) {
|
|
22
|
+
super(message || `Error: ${tag}`, { cause: opts.cause });
|
|
23
|
+
this.name = "HttpError";
|
|
24
|
+
this.payload = val !== void 0 ? {
|
|
25
|
+
tag,
|
|
26
|
+
val
|
|
27
|
+
} : { tag };
|
|
28
|
+
}
|
|
29
|
+
static from(err) {
|
|
30
|
+
if (err instanceof HttpError) return err;
|
|
31
|
+
if (err?.tag) return new HttpError(err.tag, void 0, err.val);
|
|
32
|
+
const e = getFirstError(err);
|
|
33
|
+
let tag, val;
|
|
34
|
+
switch (e.code) {
|
|
35
|
+
case "ECONNRESET":
|
|
36
|
+
tag = "HTTP-protocol-error";
|
|
37
|
+
break;
|
|
38
|
+
case "ENOTFOUND":
|
|
39
|
+
tag = "DNS-error";
|
|
40
|
+
val = {
|
|
41
|
+
rcode: e.code,
|
|
42
|
+
infoCode: e.errno < 0 ? -e.errno : e.errno
|
|
43
|
+
};
|
|
44
|
+
break;
|
|
45
|
+
case "ECONNREFUSED":
|
|
46
|
+
tag = "connection-refused";
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
tag = "internal-error";
|
|
50
|
+
val = e.toString();
|
|
51
|
+
}
|
|
52
|
+
const httpErr = new HttpError(tag, e.message, val, { cause: e });
|
|
53
|
+
Error.captureStackTrace(httpErr, HttpError.from);
|
|
54
|
+
return httpErr;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function getFirstError(e) {
|
|
58
|
+
if (typeof e !== "object" || e === null) return e;
|
|
59
|
+
if (e.cause) return getFirstError(e.cause);
|
|
60
|
+
if (e instanceof AggregateError) return getFirstError(e.errors[0]);
|
|
61
|
+
return e;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region dist/nodejs/workers/resource-worker.js
|
|
65
|
+
function Router() {
|
|
66
|
+
const _hooks = {
|
|
67
|
+
beforeAll: null,
|
|
68
|
+
ops: /* @__PURE__ */ new Map()
|
|
69
|
+
};
|
|
70
|
+
if (isMainThread || !parentPort) throw new Error("Router can only be used in worker threads");
|
|
71
|
+
parentPort.on("message", async (msg) => {
|
|
72
|
+
const { _reply, _condvar, ...rest } = msg;
|
|
73
|
+
let result, error, transfer = [];
|
|
74
|
+
try {
|
|
75
|
+
if (_hooks.beforeAll) _hooks.beforeAll(rest);
|
|
76
|
+
const handler = _hooks.ops.get(rest.op);
|
|
77
|
+
if (!handler) throw new Error(`Unknown op ${rest.op}`);
|
|
78
|
+
const outcome = await handler(rest);
|
|
79
|
+
if (outcome && Array.isArray(outcome.transferable)) {
|
|
80
|
+
result = outcome.result;
|
|
81
|
+
transfer = outcome.transferable;
|
|
82
|
+
} else result = outcome;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
error = serializeError(err);
|
|
85
|
+
}
|
|
86
|
+
_reply.postMessage({
|
|
87
|
+
result,
|
|
88
|
+
error
|
|
89
|
+
}, transfer);
|
|
90
|
+
_reply.close();
|
|
91
|
+
if (_condvar) notify(_condvar);
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
beforeAll(fn) {
|
|
95
|
+
_hooks.beforeAll = fn;
|
|
96
|
+
return this;
|
|
97
|
+
},
|
|
98
|
+
op(name, fn) {
|
|
99
|
+
_hooks.ops.set(name, fn);
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function serializeError(err) {
|
|
105
|
+
if (!(err instanceof Error)) return err;
|
|
106
|
+
const serialized = {
|
|
107
|
+
__resourceWorkerError: true,
|
|
108
|
+
name: err.name,
|
|
109
|
+
message: err.message,
|
|
110
|
+
stack: err.stack
|
|
111
|
+
};
|
|
112
|
+
for (const key of Reflect.ownKeys(err)) serialized[key] = err[key];
|
|
113
|
+
if (err.cause !== void 0) serialized.cause = serializeError(err.cause);
|
|
114
|
+
return serialized;
|
|
115
|
+
}
|
|
116
|
+
function notify(condvar) {
|
|
117
|
+
const lock = new Int32Array(condvar);
|
|
118
|
+
Atomics.store(lock, 0, 1);
|
|
119
|
+
Atomics.notify(lock, 0, 1);
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region dist/nodejs/workers/http-worker.js
|
|
123
|
+
let NEXT_SERVER_ID = 0n;
|
|
124
|
+
let NEXT_REQUEST_ID = 0n;
|
|
125
|
+
Router().op("server-start", handleHttpServerStart).op("server-stop", handleHttpServerStop).op("server-next", handleNext).op("server-response", handleResponse).op("server-close", handleHttpServerClose).op("client-request", handleRequest);
|
|
126
|
+
var Queue = class {
|
|
127
|
+
_items = [];
|
|
128
|
+
_resolvers = [];
|
|
129
|
+
constructor() {
|
|
130
|
+
this._items = [];
|
|
131
|
+
this._resolvers = [];
|
|
132
|
+
}
|
|
133
|
+
push(item) {
|
|
134
|
+
if (this._resolvers.length) this._resolvers.shift()(item);
|
|
135
|
+
else this._items.push(item);
|
|
136
|
+
}
|
|
137
|
+
async pop() {
|
|
138
|
+
if (this._items.length) return this._items.shift();
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
this._resolvers.push(resolve);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const servers = /* @__PURE__ */ new Map();
|
|
145
|
+
async function handleHttpServerStart({ port, host }) {
|
|
146
|
+
const serverId = NEXT_SERVER_ID++;
|
|
147
|
+
const pending = new Queue();
|
|
148
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
149
|
+
const server = createServer((req, res) => {
|
|
150
|
+
const requestId = NEXT_REQUEST_ID++;
|
|
151
|
+
pending.push(requestId);
|
|
152
|
+
inflight.set(requestId, {
|
|
153
|
+
req,
|
|
154
|
+
res
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
await new Promise((resolve, reject) => {
|
|
158
|
+
server.on("error", reject);
|
|
159
|
+
server.listen(port, host, () => resolve(void 0));
|
|
160
|
+
});
|
|
161
|
+
servers.set(serverId, {
|
|
162
|
+
server,
|
|
163
|
+
pending,
|
|
164
|
+
inflight
|
|
165
|
+
});
|
|
166
|
+
return serverId;
|
|
167
|
+
}
|
|
168
|
+
async function handleNext({ serverId }) {
|
|
169
|
+
const srv = servers.get(serverId);
|
|
170
|
+
if (!srv) throw new Error(`No such server: ${serverId}`);
|
|
171
|
+
const requestId = await srv.pending.pop();
|
|
172
|
+
if (requestId == null) return null;
|
|
173
|
+
const { req } = srv.inflight.get(requestId);
|
|
174
|
+
const { port1: tx, port2: trailers } = new MessageChannel();
|
|
175
|
+
const { readable: body, writable } = new TransformStream();
|
|
176
|
+
pipeline(req, Writable.fromWeb(writable)).then(() => tx.postMessage({ val: toEntries(req.trailersDistinct || req.trailers || {}) })).catch((err) => tx.postMessage({ err })).finally(() => tx.close());
|
|
177
|
+
const headers = toEntries(req.headersDistinct);
|
|
178
|
+
const { method, url } = req;
|
|
179
|
+
return {
|
|
180
|
+
result: {
|
|
181
|
+
requestId,
|
|
182
|
+
headers,
|
|
183
|
+
body,
|
|
184
|
+
trailers,
|
|
185
|
+
method,
|
|
186
|
+
url
|
|
187
|
+
},
|
|
188
|
+
transferable: [body, trailers]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function handleResponse({ serverId, requestId, statusCode, headers, trailers, stream }) {
|
|
192
|
+
const srv = servers.get(serverId);
|
|
193
|
+
if (!srv) throw new Error(`No such server: ${serverId}`);
|
|
194
|
+
const entry = srv.inflight.get(requestId);
|
|
195
|
+
if (!entry) throw new Error(`No inflight ${requestId}`);
|
|
196
|
+
const { res } = entry;
|
|
197
|
+
try {
|
|
198
|
+
res.writeHead(statusCode, toObject(headers));
|
|
199
|
+
if (stream) {
|
|
200
|
+
await pipeline(Readable.fromWeb(stream), res, { end: false });
|
|
201
|
+
const fields = await recvTrailers(trailers);
|
|
202
|
+
if (fields) res.addTrailers(toObject(fields));
|
|
203
|
+
res.end();
|
|
204
|
+
} else res.end();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
res.destroy(err);
|
|
207
|
+
throw err;
|
|
208
|
+
} finally {
|
|
209
|
+
srv.inflight.delete(requestId);
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
async function handleRequest(args) {
|
|
214
|
+
const transmit = transmitReporter(args.transmit);
|
|
215
|
+
try {
|
|
216
|
+
return await doHandleRequest(args, transmit);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
transmit.err(err);
|
|
219
|
+
throw HttpError.from(err).payload;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function doHandleRequest({ url, method, headers, trailers, body, timeouts }, transmit) {
|
|
223
|
+
const parsed = new URL(url);
|
|
224
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new HttpError("HTTP-protocol-error");
|
|
225
|
+
const { connectTimeoutNs, firstByteTimeoutNs, betweenBytesTimeoutNs } = timeouts;
|
|
226
|
+
const isHttps = parsed.protocol === "https:";
|
|
227
|
+
const agent = new (isHttps ? Agent$1 : Agent)({ keepAlive: true });
|
|
228
|
+
const req = (isHttps ? request$1 : request)({
|
|
229
|
+
agent,
|
|
230
|
+
method,
|
|
231
|
+
host: parsed.hostname,
|
|
232
|
+
port: parsed.port,
|
|
233
|
+
path: parsed.pathname + parsed.search,
|
|
234
|
+
headers: toObject(headers),
|
|
235
|
+
timeout: connectTimeoutNs ? msecs(connectTimeoutNs) : void 0
|
|
236
|
+
});
|
|
237
|
+
if (firstByteTimeoutNs) req.setTimeout(msecs(firstByteTimeoutNs));
|
|
238
|
+
let resStarted = false;
|
|
239
|
+
const response = new Promise((resolve, reject) => {
|
|
240
|
+
const onResponse = (response) => {
|
|
241
|
+
resStarted = true;
|
|
242
|
+
cleanup();
|
|
243
|
+
resolve(response);
|
|
244
|
+
};
|
|
245
|
+
const onConnect = (_response, socket) => {
|
|
246
|
+
cleanup();
|
|
247
|
+
socket.destroy();
|
|
248
|
+
reject(new HttpError("HTTP-protocol-error"));
|
|
249
|
+
};
|
|
250
|
+
const onError = (err) => {
|
|
251
|
+
cleanup();
|
|
252
|
+
reject(err);
|
|
253
|
+
};
|
|
254
|
+
const onTimeout = () => {
|
|
255
|
+
cleanup();
|
|
256
|
+
req.on("error", () => {});
|
|
257
|
+
req.destroy();
|
|
258
|
+
reject(new HttpError("connection-timeout"));
|
|
259
|
+
};
|
|
260
|
+
const onClose = () => {
|
|
261
|
+
cleanup();
|
|
262
|
+
reject(new HttpError("connection-terminated"));
|
|
263
|
+
};
|
|
264
|
+
const cleanup = () => {
|
|
265
|
+
req.off("response", onResponse);
|
|
266
|
+
req.off("connect", onConnect);
|
|
267
|
+
req.off("error", onError);
|
|
268
|
+
req.off("timeout", onTimeout);
|
|
269
|
+
req.off("close", onClose);
|
|
270
|
+
};
|
|
271
|
+
req.once("response", onResponse);
|
|
272
|
+
req.once("connect", onConnect);
|
|
273
|
+
req.once("error", onError);
|
|
274
|
+
req.once("timeout", onTimeout);
|
|
275
|
+
req.once("close", onClose);
|
|
276
|
+
});
|
|
277
|
+
const expectedLength = contentLength(headers);
|
|
278
|
+
(body ? sendRequestBody(req, body, trailers, () => resStarted, expectedLength) : endRequest(req)).then(() => transmit.ok(), (err) => transmit.err(err));
|
|
279
|
+
let res;
|
|
280
|
+
try {
|
|
281
|
+
res = await response;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
req.destroy();
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
if (betweenBytesTimeoutNs) res.once("readable", () => res.setTimeout(msecs(betweenBytesTimeoutNs), () => {
|
|
287
|
+
res.destroy(new HttpError("connection-timeout"));
|
|
288
|
+
}));
|
|
289
|
+
const pass = new PassThrough();
|
|
290
|
+
const web = Readable.toWeb(pass);
|
|
291
|
+
const { port1: tx, port2: rx } = new MessageChannel();
|
|
292
|
+
pipeline(res, pass).then(() => tx.postMessage({ val: toEntries(res.trailersDistinct || res.trailers || {}) })).catch((err) => tx.postMessage({ err })).finally(() => tx.close());
|
|
293
|
+
return {
|
|
294
|
+
result: {
|
|
295
|
+
statusCode: res.statusCode,
|
|
296
|
+
headers: toEntries(res.headersDistinct),
|
|
297
|
+
body: web,
|
|
298
|
+
trailers: rx
|
|
299
|
+
},
|
|
300
|
+
transferable: [web, rx]
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function handleHttpServerStop({ serverId }) {
|
|
304
|
+
const srv = servers.get(serverId);
|
|
305
|
+
if (!srv) throw new Error(`No such server: ${serverId}`);
|
|
306
|
+
srv.pending.push(null);
|
|
307
|
+
return serverId;
|
|
308
|
+
}
|
|
309
|
+
async function handleHttpServerClose({ serverId }) {
|
|
310
|
+
const srv = servers.get(serverId);
|
|
311
|
+
if (!srv) throw new Error(`No such server: ${serverId}`);
|
|
312
|
+
await new Promise((resolve) => srv.server.close(resolve));
|
|
313
|
+
servers.delete(serverId);
|
|
314
|
+
return serverId;
|
|
315
|
+
}
|
|
316
|
+
async function sendRequestBody(req, body, trailers, resStarted, expectedLength) {
|
|
317
|
+
try {
|
|
318
|
+
req.flushHeaders();
|
|
319
|
+
await pipeline(validateRequestBody(Readable.fromWeb(body), expectedLength), req, { end: false });
|
|
320
|
+
const fields = await recvTrailers(trailers);
|
|
321
|
+
if (fields) req.addTrailers(toObject(fields));
|
|
322
|
+
await endRequest(req);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (!resStarted()) req.destroy(err);
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function* validateRequestBody(body, expectedLength) {
|
|
329
|
+
let bytes = 0n;
|
|
330
|
+
for await (const chunk of body) {
|
|
331
|
+
bytes += BigInt(chunk.byteLength);
|
|
332
|
+
if (expectedLength !== null && bytes > expectedLength) throw new HttpError("HTTP-request-body-size", void 0, bytes);
|
|
333
|
+
yield chunk;
|
|
334
|
+
}
|
|
335
|
+
if (expectedLength !== null && bytes < expectedLength) throw new HttpError("HTTP-request-body-size", void 0, bytes);
|
|
336
|
+
}
|
|
337
|
+
function endRequest(req) {
|
|
338
|
+
return new Promise((resolve, reject) => {
|
|
339
|
+
let settled = false;
|
|
340
|
+
const settle = (fn, value) => {
|
|
341
|
+
if (settled) return;
|
|
342
|
+
settled = true;
|
|
343
|
+
req.off("error", onError);
|
|
344
|
+
req.off("close", onClose);
|
|
345
|
+
fn(value);
|
|
346
|
+
};
|
|
347
|
+
const onError = (err) => settle(reject, err);
|
|
348
|
+
const onClose = () => settle(reject, new HttpError("connection-terminated"));
|
|
349
|
+
req.once("error", onError);
|
|
350
|
+
req.once("close", onClose);
|
|
351
|
+
try {
|
|
352
|
+
req.end(() => settle(resolve, void 0));
|
|
353
|
+
} catch (err) {
|
|
354
|
+
settle(reject, err);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function transmitReporter(port) {
|
|
359
|
+
let settled = false;
|
|
360
|
+
const send = (msg) => {
|
|
361
|
+
if (settled || !port) return;
|
|
362
|
+
settled = true;
|
|
363
|
+
port.postMessage(msg);
|
|
364
|
+
port.close();
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
ok() {
|
|
368
|
+
send({ val: void 0 });
|
|
369
|
+
},
|
|
370
|
+
err(err) {
|
|
371
|
+
send({ err: HttpError.from(err).payload });
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function recvTrailers(port) {
|
|
376
|
+
if (!port) return null;
|
|
377
|
+
return new Promise((resolve, reject) => {
|
|
378
|
+
port.once("message", ({ val, err }) => {
|
|
379
|
+
port.close();
|
|
380
|
+
if (err) reject(err);
|
|
381
|
+
else resolve(val);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const msecs = (time) => {
|
|
386
|
+
return typeof time === "bigint" ? Number(time / 1000000n) : time / 1e6;
|
|
387
|
+
};
|
|
388
|
+
const decoder = new TextDecoder();
|
|
389
|
+
const contentLength = (entries) => {
|
|
390
|
+
const entry = entries.findLast(([name]) => name.toLowerCase() === "content-length");
|
|
391
|
+
if (!entry) return null;
|
|
392
|
+
const value = decoder.decode(entry[1]);
|
|
393
|
+
return /^\d+$/.test(value) ? BigInt(value) : null;
|
|
394
|
+
};
|
|
395
|
+
const toObject = (entries) => {
|
|
396
|
+
return Object.fromEntries(entries.map(([key, val]) => [key, decoder.decode(val)]));
|
|
397
|
+
};
|
|
398
|
+
const encoder = new TextEncoder();
|
|
399
|
+
const toEntries = (obj) => {
|
|
400
|
+
return Object.entries(obj).flatMap(([k, v]) => Array.isArray(v) ? v.map((val) => [k, encoder.encode(val)]) : [[k, encoder.encode(v)]]);
|
|
401
|
+
};
|
|
402
|
+
//#endregion
|
|
403
|
+
export {};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parentPort, isMainThread, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
2
2
|
export class ResourceWorker {
|
|
3
3
|
#worker = null;
|
|
4
|
-
#
|
|
4
|
+
#createWorker = null;
|
|
5
5
|
#pending = new Map();
|
|
6
|
-
constructor(
|
|
7
|
-
this.#
|
|
6
|
+
constructor(createWorker) {
|
|
7
|
+
this.#createWorker = createWorker;
|
|
8
8
|
}
|
|
9
9
|
#getWorker() {
|
|
10
10
|
if (this.#worker) {
|
|
11
11
|
return this.#worker;
|
|
12
12
|
}
|
|
13
|
-
this.#worker =
|
|
13
|
+
this.#worker = this.#createWorker();
|
|
14
14
|
this.#worker.unref();
|
|
15
15
|
return this.#worker;
|
|
16
16
|
}
|