@volter/tabnode 0.4.1 → 0.5.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/__sw__.js +259 -5
- package/dist/assets/{runtime-worker-LWfW6VWE.js → runtime-worker-B9buriZs.js} +4004 -2955
- package/dist/fetch-transport.d.ts +20 -0
- package/dist/guest-timers.d.ts +15 -0
- package/dist/http-client-transport.d.ts +19 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.mjs +5187 -3051
- package/dist/native-stream-binding.d.ts +57 -0
- package/dist/native-stream-owner.d.ts +121 -0
- package/dist/node-lib/binding/duplex_stream.d.ts +26 -0
- package/dist/node-lib/binding/fds.d.ts +8 -0
- package/dist/node-lib/binding/fs.d.ts +311 -78
- package/dist/node-lib/binding/misc.d.ts +1 -0
- package/dist/node-lib/binding/pipe_wrap.d.ts +4 -4
- package/dist/node-lib/binding/process_wrap.d.ts +12 -1
- package/dist/node-lib/binding/stream_wrap.d.ts +27 -18
- package/dist/node-lib/binding/tcp_wrap.d.ts +0 -1
- package/dist/node-lib/binding/tty_wrap.d.ts +43 -2
- package/dist/node-lib/binding/util.d.ts +83 -0
- package/dist/node-lib/binding/uv.d.ts +4 -0
- package/dist/node-lib/fs-module.d.ts +1 -0
- package/dist/node-lib/internals/buffer-and-streams.d.ts +30 -17
- package/dist/node-lib/internals/index.d.ts +1 -1
- package/dist/node-lib/load.d.ts +17 -3
- package/dist/node-lib/module-hooks.d.ts +1 -0
- package/dist/node-lib/public-modules.d.ts +1 -1
- package/dist/node-lib/timers.d.ts +38 -0
- package/dist/node-process-host.d.ts +41 -0
- package/dist/process-registry.d.ts +39 -0
- package/dist/process-tokens.d.ts +11 -0
- package/dist/promise-ownership.d.ts +3 -0
- package/dist/runtime.d.ts +10 -9
- package/dist/shims/child_process.d.ts +2 -0
- package/dist/shims/crypto.d.ts +660 -119
- package/dist/shims/dns.d.ts +76 -2
- package/dist/shims/process.d.ts +1 -0
- package/dist/shims/string_decoder.d.ts +19 -3
- package/dist/shims/tls.d.ts +853 -31
- package/dist/source-edits.d.ts +7 -0
- package/package.json +1 -1
- package/src/code-transforms.ts +2 -3
- package/src/fetch-transport.ts +98 -0
- package/src/guest-timers.ts +89 -0
- package/src/http-client-transport.ts +169 -0
- package/src/index.ts +12 -0
- package/src/native-stream-binding.ts +393 -0
- package/src/native-stream-owner.ts +317 -0
- package/src/node-lib/binding/duplex_stream.ts +95 -0
- package/src/node-lib/binding/fds.ts +18 -1
- package/src/node-lib/binding/fs.ts +45 -17
- package/src/node-lib/binding/misc.ts +2 -0
- package/src/node-lib/binding/pipe_wrap.ts +35 -12
- package/src/node-lib/binding/process_wrap.ts +50 -9
- package/src/node-lib/binding/stream_wrap.ts +196 -72
- package/src/node-lib/binding/tcp_wrap.ts +43 -8
- package/src/node-lib/binding/tty_wrap.ts +135 -8
- package/src/node-lib/binding/util.ts +24 -15
- package/src/node-lib/binding/uv.ts +4 -0
- package/src/node-lib/fs-module.ts +2 -18
- package/src/node-lib/internal/navigator.js +149 -0
- package/src/node-lib/internals/buffer-and-streams.ts +50 -11
- package/src/node-lib/internals/events-util.ts +5 -0
- package/src/node-lib/internals/index.ts +12 -2
- package/src/node-lib/load.ts +170 -62
- package/src/node-lib/module-hooks.ts +5 -2
- package/src/node-lib/public-modules.ts +12 -48
- package/src/node-lib/sources.ts +4 -0
- package/src/node-lib/timers.ts +93 -0
- package/src/node-process-host.ts +124 -0
- package/src/process-registry.ts +144 -0
- package/src/process-tokens.ts +55 -6
- package/src/promise-ownership.ts +28 -0
- package/src/runtime.ts +90 -214
- package/src/shims/child_process.ts +310 -80
- package/src/shims/crypto.ts +115 -100
- package/src/shims/dns.ts +17 -1
- package/src/shims/process.ts +33 -11
- package/src/shims/string_decoder.ts +10 -6
- package/src/shims/tls.ts +49 -19
- package/src/source-edits.ts +20 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply non-overlapping edits in descending source order. The AST visitors
|
|
3
|
+
* decide the replacements; assembly must not copy a large module once per
|
|
4
|
+
* reference. For equal-position insertions, later edits appear before earlier
|
|
5
|
+
* ones, matching the former repeated slice/concatenate operation.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applySourceEdits(source: string, edits: readonly (readonly [start: number, end: number, text: string])[]): string;
|
package/package.json
CHANGED
package/src/code-transforms.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { forGuestRealm, takeFromHost } from './host-globals';
|
|
8
8
|
import * as acorn from 'acorn';
|
|
9
|
+
import { applySourceEdits } from './source-edits';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A module's lowered exports go to the module's exports whatever the module
|
|
@@ -335,9 +336,7 @@ function __substrateLiveImportBindings(code: any) {
|
|
|
335
336
|
|
|
336
337
|
visit(ast, new Set(reads.keys()));
|
|
337
338
|
edits.sort((a, b) => b[0] - a[0]);
|
|
338
|
-
|
|
339
|
-
for (const [start, end, text] of edits) result = result.slice(0, start) + text + result.slice(end);
|
|
340
|
-
return result;
|
|
339
|
+
return applySourceEdits(code, edits);
|
|
341
340
|
}
|
|
342
341
|
|
|
343
342
|
/** AST-based ESM→CJS transform using acorn. */
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Host Fetch I/O uses the same process-owned liveness registry as sockets. */
|
|
2
|
+
import type { Process } from './shims/process';
|
|
3
|
+
import { __tokenForProcess, __runFor, enterRun, type ProcessToken } from './process-tokens';
|
|
4
|
+
import { registerHandle, __adoptHandle, refHandle, unrefHandle, releaseHandle } from './node-lib/binding/handles';
|
|
5
|
+
|
|
6
|
+
export interface FetchActivity {
|
|
7
|
+
/** One pending transport operation; the returned release is idempotent. */
|
|
8
|
+
hold(): () => void;
|
|
9
|
+
/** EOF/error/cancellation ends ownership, without cancelling completed I/O. */
|
|
10
|
+
release(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FetchTransportContext {
|
|
14
|
+
/** Retain cancellation ownership while idle, but do not keep the run alive. */
|
|
15
|
+
begin(cancel: () => void): FetchActivity;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type FetchTransport = (input: RequestInfo | URL, init: RequestInit | undefined, context: FetchTransportContext) => Promise<Response>;
|
|
19
|
+
let transport: FetchTransport | undefined;
|
|
20
|
+
|
|
21
|
+
// Request.body exposes a stream even for a string, Blob or FormData. Keep
|
|
22
|
+
// the native constructor's source distinction for transports crossing realms:
|
|
23
|
+
// a finite body must not acquire streaming-upload restrictions in transit.
|
|
24
|
+
const staticRequestBodies = new WeakSet<Request>();
|
|
25
|
+
|
|
26
|
+
export function rememberRequestBodySource(request: Request, input: RequestInfo | URL, init?: RequestInit): void {
|
|
27
|
+
if (!request.body) return;
|
|
28
|
+
if (init?.body != null
|
|
29
|
+
? !(init.body instanceof ReadableStream)
|
|
30
|
+
: typeof input === 'object' && staticRequestBodies.has(input as Request)) {
|
|
31
|
+
staticRequestBodies.add(request);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Host transport metadata; it carries no network permission. */
|
|
36
|
+
export function requestHasStaticBody(request: Request): boolean {
|
|
37
|
+
return staticRequestBodies.has(request);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Host-only seam: the adapter reports pending headers/pulls and final cleanup. */
|
|
41
|
+
export function installFetchTransport(adapter: FetchTransport): () => void {
|
|
42
|
+
const previous = transport;
|
|
43
|
+
transport = adapter;
|
|
44
|
+
return () => { if (transport === adapter) transport = previous; };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function activity(owner: ProcessToken | null, cancel: () => void): FetchActivity {
|
|
48
|
+
let ended = false;
|
|
49
|
+
let pending = 0;
|
|
50
|
+
const handle = {
|
|
51
|
+
close() {
|
|
52
|
+
if (ended) return;
|
|
53
|
+
release();
|
|
54
|
+
cancel();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const release = (): void => {
|
|
58
|
+
if (ended) return;
|
|
59
|
+
ended = true;
|
|
60
|
+
releaseHandle(handle);
|
|
61
|
+
};
|
|
62
|
+
registerHandle(handle);
|
|
63
|
+
// Never attribute a delayed request to the last process that happened to run.
|
|
64
|
+
__adoptHandle(handle, owner);
|
|
65
|
+
unrefHandle(handle);
|
|
66
|
+
return {
|
|
67
|
+
hold() {
|
|
68
|
+
if (ended) throw new Error('Fetch request has already ended.');
|
|
69
|
+
pending += 1;
|
|
70
|
+
refHandle(handle);
|
|
71
|
+
let held = true;
|
|
72
|
+
return () => {
|
|
73
|
+
if (!held) return;
|
|
74
|
+
held = false;
|
|
75
|
+
if (--pending === 0 && !ended) unrefHandle(handle);
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
release,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Each process may replace its own fetch; the host adapter remains private. */
|
|
83
|
+
export function guestFetch(process: Process, fallback: typeof globalThis.fetch): typeof globalThis.fetch {
|
|
84
|
+
return function fetch(input, init) {
|
|
85
|
+
const adapter = transport;
|
|
86
|
+
if (!adapter) return fallback.call(globalThis, input, init);
|
|
87
|
+
const owner = __tokenForProcess(process);
|
|
88
|
+
const ended = () => owner !== null && __runFor(owner)?.process !== process;
|
|
89
|
+
const stopped = () => new DOMException('The requesting process has ended.', 'AbortError');
|
|
90
|
+
if (ended()) return Promise.reject(stopped());
|
|
91
|
+
const context: FetchTransportContext = { begin(cancel) {
|
|
92
|
+
if (ended()) throw stopped();
|
|
93
|
+
return activity(owner, cancel);
|
|
94
|
+
} };
|
|
95
|
+
const dispatch = () => adapter(input, init, context);
|
|
96
|
+
return owner === null ? dispatch() : enterRun(owner, dispatch);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { __reportUncaughtException } from './shims/process';
|
|
2
|
+
import { nodeTimeout, timerHandleOf } from './node-lib/timers';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The timers a guest process has pending: what Node's loop counts to decide
|
|
6
|
+
* a program is done. A timer the guest sets through its globals or module is
|
|
7
|
+
* held here until it fires, is cleared, or is unref'd, and the `node`
|
|
8
|
+
* command's end-of-program rule asks how many remain.
|
|
9
|
+
*/
|
|
10
|
+
const __substratePendingTimers = new WeakMap<object, Set<unknown>>();
|
|
11
|
+
export function pendingGuestTimers(process: object): number {
|
|
12
|
+
return __substratePendingTimers.get(process)?.size ?? 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Clear every timer a guest still holds, as ending a Node process clears the
|
|
16
|
+
* ones its loop was waiting on. The ids were made by the host's own
|
|
17
|
+
* `setTimeout`/`setInterval` through the guest's global view, so the host's
|
|
18
|
+
* own clears end them; a `Timeout` is cleared by either in Node, and either
|
|
19
|
+
* refusing an id of the other kind is not an error here.
|
|
20
|
+
*/
|
|
21
|
+
export function stopGuestTimers(process: object): void {
|
|
22
|
+
const held = __substratePendingTimers.get(process);
|
|
23
|
+
if (!held) return;
|
|
24
|
+
const host = globalThis as unknown as Record<string, unknown>;
|
|
25
|
+
const clearOnce = host.clearTimeout as ((id: unknown) => void) | undefined;
|
|
26
|
+
const clearRepeating = host.clearInterval as ((id: unknown) => void) | undefined;
|
|
27
|
+
for (const id of [...held]) {
|
|
28
|
+
try { clearOnce?.call(host, id); } catch {}
|
|
29
|
+
try { clearRepeating?.call(host, id); } catch {}
|
|
30
|
+
}
|
|
31
|
+
held.clear();
|
|
32
|
+
}
|
|
33
|
+
export function guestTimerFunctions(process: object, host: Record<string, unknown>) {
|
|
34
|
+
let pending = __substratePendingTimers.get(process);
|
|
35
|
+
if (!pending) { pending = new Set(); __substratePendingTimers.set(process, pending); }
|
|
36
|
+
const held = pending;
|
|
37
|
+
const hostSetTimeout = host.setTimeout as (fn: (...a: unknown[]) => void, ms?: number, ...rest: unknown[]) => unknown;
|
|
38
|
+
const hostSetInterval = host.setInterval as (fn: (...a: unknown[]) => void, ms?: number, ...rest: unknown[]) => unknown;
|
|
39
|
+
const hostClearTimeout = host.clearTimeout as (id: unknown) => void;
|
|
40
|
+
const hostClearInterval = host.clearInterval as (id: unknown) => void;
|
|
41
|
+
// Node's timers answer a `Timeout`: an object with `ref`, `unref`,
|
|
42
|
+
// `hasRef` and `refresh`, which vitest and many packages call. A browser's
|
|
43
|
+
// answer a number. The engine used to correct this by replacing the realm's
|
|
44
|
+
// `setTimeout`, which in a Node host replaced the host's own; the
|
|
45
|
+
// correction belongs to the guest that reads Node's shape, so it is made
|
|
46
|
+
// here, on the guest's timer, and a host that already answers a `Timeout`
|
|
47
|
+
// keeps its own. The shape itself is `src/node-lib/timers.ts`: Node's own
|
|
48
|
+
// files take their timers from `require('timers')` rather than from the
|
|
49
|
+
// realm, and a timer made through one is cleared through the other.
|
|
50
|
+
const handleOf = timerHandleOf;
|
|
51
|
+
// An exception a timer callback throws is that program's uncaught
|
|
52
|
+
// exception: Node gives it to the process's `uncaughtException` listeners,
|
|
53
|
+
// else prints the stack to that process's stderr and exits it 1, and every
|
|
54
|
+
// other process is untouched. The engine's timers are the realm's own, so
|
|
55
|
+
// an uncaught throw left the guest's frames and reached the realm's global
|
|
56
|
+
// `error` event — in the tab the worker's, which the substrate's container
|
|
57
|
+
// reads as a dead host and disposes, killing openvscode-server's extension
|
|
58
|
+
// host and every other program in the tab along with the one that threw.
|
|
59
|
+
// A callback with no owning program (the engine imported as a library) is
|
|
60
|
+
// rethrown, so the host reports it as it always did.
|
|
61
|
+
const inProcess = (fn: (...a: unknown[]) => void) => (...args: unknown[]): void => {
|
|
62
|
+
try { fn(...args); }
|
|
63
|
+
catch (error) { if (!__reportUncaughtException(process, error)) throw error; }
|
|
64
|
+
};
|
|
65
|
+
const track = (id: unknown): unknown => {
|
|
66
|
+
held.add(id);
|
|
67
|
+
if (id && typeof id === "object" && typeof (id as { unref?: unknown }).unref === "function") {
|
|
68
|
+
const handle = id as { unref: () => unknown; ref: () => unknown };
|
|
69
|
+
const unref = handle.unref.bind(handle);
|
|
70
|
+
const ref = handle.ref.bind(handle);
|
|
71
|
+
handle.unref = () => { held.delete(id); return unref(); };
|
|
72
|
+
handle.ref = () => { held.add(id); return ref(); };
|
|
73
|
+
}
|
|
74
|
+
return id;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
setTimeout(fn: (...a: unknown[]) => void, ms?: number, ...rest: unknown[]) {
|
|
78
|
+
let id: unknown;
|
|
79
|
+
const call = inProcess(fn);
|
|
80
|
+
id = nodeTimeout(hostSetTimeout.call(host, (...args: unknown[]) => { held.delete(id); call(...args); }, ms, ...rest));
|
|
81
|
+
return track(id);
|
|
82
|
+
},
|
|
83
|
+
setInterval(fn: (...a: unknown[]) => void, ms?: number, ...rest: unknown[]) {
|
|
84
|
+
return track(nodeTimeout(hostSetInterval.call(host, inProcess(fn), ms, ...rest)));
|
|
85
|
+
},
|
|
86
|
+
clearTimeout(id: unknown) { held.delete(id); return hostClearTimeout.call(host, handleOf(id)); },
|
|
87
|
+
clearInterval(id: unknown) { held.delete(id); return hostClearInterval.call(host, handleOf(id)); },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node's Agent connection seam over a host HTTP exchange. Both sides of the
|
|
3
|
+
* internal connection use Node's unmodified HTTP library: it parses requests,
|
|
4
|
+
* generates responses and owns ClientRequest/IncomingMessage and backpressure.
|
|
5
|
+
* No TLS socket or network metadata is fabricated. The host admits the target
|
|
6
|
+
* and capabilities before sending, and returns one response without retries.
|
|
7
|
+
*/
|
|
8
|
+
import { httpModule, httpsModule } from './node-lib/http-module';
|
|
9
|
+
import { netModule } from './node-lib/net-module';
|
|
10
|
+
import { streamModule } from './node-lib/stream-module';
|
|
11
|
+
import { onNodeLibLoaded, type NodeLibRequire } from './node-lib/load';
|
|
12
|
+
import { DuplexStreamHandle } from './node-lib/binding/duplex_stream';
|
|
13
|
+
|
|
14
|
+
export interface HttpClientExchangeRequest {
|
|
15
|
+
url: string;
|
|
16
|
+
method: string;
|
|
17
|
+
headers: Array<[string, string]>;
|
|
18
|
+
body?: ReadableStream<Uint8Array>;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
profile: 'server-http';
|
|
21
|
+
}
|
|
22
|
+
export interface HttpClientExchangeResponse {
|
|
23
|
+
status: number;
|
|
24
|
+
statusText: string;
|
|
25
|
+
headers: Array<[string, string]>;
|
|
26
|
+
body: ReadableStream<Uint8Array> | null;
|
|
27
|
+
}
|
|
28
|
+
export type HttpClientExchange = (request: HttpClientExchangeRequest) => Promise<HttpClientExchangeResponse>;
|
|
29
|
+
/** Capture process attribution synchronously when the connection is requested. */
|
|
30
|
+
export type HttpClientTransportFactory = () => HttpClientExchange;
|
|
31
|
+
|
|
32
|
+
const unsupported = (detail: string): Error => Object.assign(new Error(`HTTP transport does not support ${detail}.`), { code: 'ERR_HTTP_TRANSPORT_UNSUPPORTED' });
|
|
33
|
+
const local = (host: string): boolean => host === 'localhost' || host === '::1' || host === '[::1]' || /^127\./u.test(host);
|
|
34
|
+
const tlsOptions = ['ca', 'cert', 'key', 'pfx', 'passphrase', 'ciphers', 'secureContext', 'checkServerIdentity', 'minVersion', 'maxVersion', 'secureProtocol', 'secureOptions', 'session', 'ALPNProtocols', 'pskCallback'];
|
|
35
|
+
|
|
36
|
+
/** Host-only installation; guest modules receive Node's normal exports. */
|
|
37
|
+
export function installHttpClientTransport(factory: HttpClientTransportFactory): () => void {
|
|
38
|
+
type AgentPrototype = { createConnection: (...args: any[]) => any };
|
|
39
|
+
const installed = new WeakMap<AgentPrototype, { original: AgentPrototype['createConnection']; connect: AgentPrototype['createConnection'] }>();
|
|
40
|
+
const live = new Set<WeakRef<AgentPrototype>>();
|
|
41
|
+
const collected = new FinalizationRegistry<WeakRef<AgentPrototype>>(ref => live.delete(ref));
|
|
42
|
+
const install = (Agent: { prototype: AgentPrototype }, protocol: string, require?: NodeLibRequire): void => {
|
|
43
|
+
const prototype = Agent.prototype;
|
|
44
|
+
if (installed.has(prototype)) return;
|
|
45
|
+
const original = prototype.createConnection;
|
|
46
|
+
const connect = function(this: unknown, options: Record<string, any>, callback: (error: Error | null, socket?: unknown) => void): unknown {
|
|
47
|
+
const host = String(options.hostname ?? options.host ?? 'localhost').toLowerCase();
|
|
48
|
+
// Plain local HTTP is the engine's existing virtual network.
|
|
49
|
+
if (protocol === 'http:' && (local(host) || options.socketPath)) return original.call(this, options, callback);
|
|
50
|
+
try {
|
|
51
|
+
if (options.socketPath || options.localAddress || options.localPort || options.lookup || options.family) throw unsupported('custom socket routing');
|
|
52
|
+
if (tlsOptions.some(key => options[key] !== undefined) || options.rejectUnauthorized === false
|
|
53
|
+
|| (options.servername !== undefined && options.servername !== host)) throw unsupported('custom TLS options');
|
|
54
|
+
const authority = new URL(`${protocol}//${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${options.port ?? (protocol === 'https:' ? 443 : 80)}`);
|
|
55
|
+
return connection(authority, factory(), require);
|
|
56
|
+
} catch (cause) {
|
|
57
|
+
queueMicrotask(() => callback(cause instanceof Error ? cause : new Error(String(cause))));
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
prototype.createConnection = connect;
|
|
62
|
+
installed.set(prototype, { original, connect });
|
|
63
|
+
const ref = new WeakRef(prototype);
|
|
64
|
+
live.add(ref);
|
|
65
|
+
collected.register(prototype, ref, ref);
|
|
66
|
+
};
|
|
67
|
+
// The host graph serves engine-internal calls. Every guest graph gets the
|
|
68
|
+
// same host adapter at bootstrap, before application monkey patches run.
|
|
69
|
+
install(httpModule.Agent, 'http:');
|
|
70
|
+
install(httpsModule.Agent, 'https:');
|
|
71
|
+
const unsubscribe = onNodeLibLoaded((name, value, require) => {
|
|
72
|
+
if (name === '_http_agent') install(value.Agent, 'http:', require);
|
|
73
|
+
if (name === 'https') install(value.Agent, 'https:', require);
|
|
74
|
+
});
|
|
75
|
+
return () => {
|
|
76
|
+
unsubscribe();
|
|
77
|
+
for (const ref of live) {
|
|
78
|
+
const prototype = ref.deref();
|
|
79
|
+
const held = prototype && installed.get(prototype);
|
|
80
|
+
if (prototype && held && prototype.createConnection === held.connect) prototype.createConnection = held.original;
|
|
81
|
+
collected.unregister(ref);
|
|
82
|
+
}
|
|
83
|
+
live.clear();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function connection(origin: URL, exchange: HttpClientExchange, require?: NodeLibRequire): unknown {
|
|
88
|
+
// Resolve at connection time; eagerly loading during Agent compilation
|
|
89
|
+
// would enter a partially initialized HTTP/stream dependency cycle.
|
|
90
|
+
const streams = require ? require('stream') : streamModule;
|
|
91
|
+
const net = require ? require('net') : netModule;
|
|
92
|
+
const http = require ? require('http') : httpModule;
|
|
93
|
+
const pair = streams.duplexPair({ highWaterMark: 64 * 1024 }) as [any, any];
|
|
94
|
+
const client = new net.Socket({ handle: new DuplexStreamHandle(pair[0]), readable: true, writable: true });
|
|
95
|
+
const peer = new net.Socket({ handle: new DuplexStreamHandle(pair[1]), readable: true, writable: true });
|
|
96
|
+
peer.unref();
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
99
|
+
const fail = (cause: unknown): void => {
|
|
100
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
101
|
+
controller.abort(error);
|
|
102
|
+
client.destroy(error);
|
|
103
|
+
peer.destroy();
|
|
104
|
+
};
|
|
105
|
+
client.once('close', (() => {
|
|
106
|
+
controller.abort(new Error('HTTP client connection closed.'));
|
|
107
|
+
void reader?.cancel(controller.signal.reason).catch(() => {});
|
|
108
|
+
peer.destroy();
|
|
109
|
+
}) as never);
|
|
110
|
+
peer.on('error', fail as never);
|
|
111
|
+
const server = http.createServer({ maxHeaderSize: 32 * 1024, requestTimeout: 0, headersTimeout: 0 }, (request: any, response: any) => {
|
|
112
|
+
void (async () => {
|
|
113
|
+
if (!request.url.startsWith('/') || request.url.startsWith('//')) throw unsupported('absolute-form request targets');
|
|
114
|
+
const url = new URL(request.url, origin);
|
|
115
|
+
if (url.origin !== origin.origin) throw unsupported('a request target that changes origin');
|
|
116
|
+
const headers: Array<[string, string]> = [];
|
|
117
|
+
const connectionHeaders = new Set<string>();
|
|
118
|
+
for (let i = 0; i < request.rawHeaders.length; i += 2) {
|
|
119
|
+
const name = request.rawHeaders[i], value = request.rawHeaders[i + 1];
|
|
120
|
+
const lower = name.toLowerCase();
|
|
121
|
+
if (['expect', 'upgrade', 'trailer', 'te'].includes(lower)) throw unsupported(`${lower} headers`);
|
|
122
|
+
if (lower === 'host') {
|
|
123
|
+
if (value.toLowerCase() !== origin.host) throw unsupported('a Host header different from the destination');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (lower === 'connection') for (const token of value.split(',')) connectionHeaders.add(token.trim().toLowerCase());
|
|
127
|
+
headers.push([name, value]);
|
|
128
|
+
}
|
|
129
|
+
// Preserve the parsed length as metadata. The host can send a bounded,
|
|
130
|
+
// known-small body without Fetch's HTTP/2-only streaming upload mode;
|
|
131
|
+
// the remote transport still generates its own HTTP framing.
|
|
132
|
+
const outgoing = headers.filter(([name]) => !['connection', 'keep-alive', 'transfer-encoding'].includes(name.toLowerCase()) && !connectionHeaders.has(name.toLowerCase()));
|
|
133
|
+
const carriesBody = request.headers['transfer-encoding'] !== undefined || Number(request.headers['content-length'] ?? 0) > 0;
|
|
134
|
+
const Readable = streams.Readable as any;
|
|
135
|
+
const body = carriesBody ? Readable.toWeb(request, { strategy: { highWaterMark: 64 * 1024, size: (chunk: Uint8Array) => chunk.byteLength } }) : undefined;
|
|
136
|
+
if (!carriesBody) request.resume();
|
|
137
|
+
const result = await exchange({ url: url.href, method: request.method, headers: outgoing, ...(body ? { body } : {}), signal: controller.signal, profile: 'server-http' });
|
|
138
|
+
if (controller.signal.aborted) { await result.body?.cancel(controller.signal.reason); return; }
|
|
139
|
+
// This virtual connection carries one exchange. Framing is Node's; the
|
|
140
|
+
// remote connection's hop-by-hop state is never a local socket promise.
|
|
141
|
+
response.shouldKeepAlive = false;
|
|
142
|
+
response.sendDate = false;
|
|
143
|
+
response.writeHead(result.status, result.statusText, result.headers.flat());
|
|
144
|
+
reader = result.body?.getReader();
|
|
145
|
+
try {
|
|
146
|
+
for (;;) {
|
|
147
|
+
controller.signal.throwIfAborted();
|
|
148
|
+
const next = reader ? await reader.read() : { done: true, value: undefined };
|
|
149
|
+
if (next.done) break;
|
|
150
|
+
if (!response.write(next.value)) await new Promise<void>((resolve, reject) => {
|
|
151
|
+
const abort = (): void => { response.removeListener('drain', drain); reject(controller.signal.reason); };
|
|
152
|
+
const drain = (): void => { controller.signal.removeEventListener('abort', abort); resolve(); };
|
|
153
|
+
response.once('drain', drain);
|
|
154
|
+
controller.signal.addEventListener('abort', abort, { once: true });
|
|
155
|
+
if (controller.signal.aborted) abort();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
response.end();
|
|
159
|
+
} catch (cause) {
|
|
160
|
+
await reader?.cancel(cause).catch(() => {});
|
|
161
|
+
throw cause;
|
|
162
|
+
} finally { reader?.releaseLock(); reader = undefined; }
|
|
163
|
+
})().catch(fail);
|
|
164
|
+
}) as any;
|
|
165
|
+
for (const name of ['connect', 'upgrade', 'checkContinue', 'checkExpectation']) server.on(name, () => fail(unsupported(name)));
|
|
166
|
+
server.on('clientError', fail);
|
|
167
|
+
server.emit('connection', peer);
|
|
168
|
+
return client;
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { createProcess } from './shims/process';
|
|
|
24
24
|
export type { Process, ProcessEnv } from './shims/process';
|
|
25
25
|
export * as path from './shims/path';
|
|
26
26
|
export { httpModule as http, httpsModule as https } from './node-lib/http-module';
|
|
27
|
+
export { installHttpClientTransport } from './http-client-transport';
|
|
28
|
+
export type { HttpClientExchange, HttpClientExchangeRequest, HttpClientExchangeResponse, HttpClientTransportFactory } from './http-client-transport';
|
|
27
29
|
import { __releaseOwnedServers, __ownedServerPorts } from './node-lib/net-module';
|
|
28
30
|
export { netModule as net } from './node-lib/net-module';
|
|
29
31
|
export { eventsModule as events } from './node-lib/events-module';
|
|
@@ -55,6 +57,13 @@ import { Server as NetServer, __releaseOwnedHandles, type Socket as NetSocket }
|
|
|
55
57
|
|
|
56
58
|
import { __currentProcessToken, __runFor, __stopOwnedProcess, runPid, processByPid } from './process-tokens';
|
|
57
59
|
export { runPid, processByPid };
|
|
60
|
+
export { createProcessRegistryScope, installProcessRegistry, ownerProcessRegistryScope } from './process-tokens';
|
|
61
|
+
export { installNodeProcessHost, nodeProcessHostInstalled } from './node-process-host';
|
|
62
|
+
export type { NodeProcessLaunch, NodeProcessHost } from './node-process-host';
|
|
63
|
+
export type { ProcessIdentity, ProcessRegistry, ProcessRegistryScope, InitialProcessRegistration } from './process-registry';
|
|
64
|
+
export { NativeStreamScope } from './native-stream-owner';
|
|
65
|
+
export { installNativeStreamTransport, nativeStreamDescriptor } from './native-stream-binding';
|
|
66
|
+
export type { NativeStreamDescriptor, NativeStreamLimits, NativeStreamEvent, NativeStreamOperation, NativeStreamReply, NativeStreamTransport } from './native-stream-owner';
|
|
58
67
|
|
|
59
68
|
export interface RunResult {
|
|
60
69
|
stdout: string;
|
|
@@ -234,6 +243,9 @@ export function createContainer(options?: ContainerOptions): {
|
|
|
234
243
|
|
|
235
244
|
export default createContainer;
|
|
236
245
|
|
|
246
|
+
export { installFetchTransport, requestHasStaticBody } from './fetch-transport';
|
|
247
|
+
export type { FetchActivity, FetchTransport, FetchTransportContext } from './fetch-transport';
|
|
248
|
+
|
|
237
249
|
// Node's module resolution as a function over any filesystem, for a host
|
|
238
250
|
// that resolves the way the engine does (the substrate's stand-ins do).
|
|
239
251
|
export { createNodeResolver } from './node-resolution';
|