@nimbus-sh/worker 0.2.0 → 0.2.2
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/facets/manager.d.ts +47 -2
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +139 -15
- package/dist/loaders/generated-workers.d.ts +2 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +3 -3
- package/dist/runtime/bash-repl.d.ts +1 -1
- package/dist/runtime/bash-repl.d.ts.map +1 -1
- package/dist/runtime/bash-repl.js +3 -2
- package/dist/runtime/cpython-resident.d.ts +27 -0
- package/dist/runtime/cpython-resident.d.ts.map +1 -0
- package/dist/runtime/cpython-resident.js +133 -0
- package/dist/runtime/facet-loader-host.d.ts +16 -1
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +42 -0
- package/dist/runtime/package-manager.d.ts +3 -58
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +6 -143
- package/dist/runtime/python-repl.d.ts +1 -1
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +2 -2
- package/dist/runtime/ruby-repl.js +1 -1
- package/dist/runtime/ruby-resident.d.ts +20 -0
- package/dist/runtime/ruby-resident.d.ts.map +1 -0
- package/dist/runtime/ruby-resident.js +278 -0
- package/dist/runtime/runtime-catalog.d.ts +4 -80
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +4 -67
- package/dist/session/init.d.ts +16 -7
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +210 -249
- package/dist/session/keys.d.ts +13 -0
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +13 -0
- package/dist/session/nimbus-session.d.ts +30 -2
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +49 -58
- package/dist/session/programmatic.d.ts +3 -3
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +3 -2
- package/dist/session/routes.js +1 -1
- package/dist/session/supervisor-rpc.d.ts +6 -13
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +7 -17
- package/package.json +2 -2
- package/scripts/bundle-facet-workers.mjs +9 -8
- package/scripts/bundle-runtime.mjs +276 -44
- package/dist/npm/tarball-stream.d.ts +0 -93
- package/dist/npm/tarball-stream.d.ts.map +0 -1
- package/dist/npm/tarball-stream.js +0 -235
- package/dist/npm/tarball.d.ts +0 -34
- package/dist/npm/tarball.d.ts.map +0 -1
- package/dist/npm/tarball.js +0 -97
- package/dist/runtime/bash-runner.d.ts +0 -63
- package/dist/runtime/bash-runner.d.ts.map +0 -1
- package/dist/runtime/bash-runner.generated.d.ts +0 -14
- package/dist/runtime/bash-runner.generated.d.ts.map +0 -1
- package/dist/runtime/bash-runner.generated.js +0 -13
- package/dist/runtime/bash-runner.js +0 -291
- package/dist/runtime/clang-runner.d.ts +0 -37
- package/dist/runtime/clang-runner.d.ts.map +0 -1
- package/dist/runtime/clang-runner.js +0 -865
- package/dist/runtime/cpython-runner.d.ts +0 -71
- package/dist/runtime/cpython-runner.d.ts.map +0 -1
- package/dist/runtime/cpython-runner.js +0 -535
- package/dist/runtime/python-pip.d.ts +0 -38
- package/dist/runtime/python-pip.d.ts.map +0 -1
- package/dist/runtime/python-pip.js +0 -1063
- package/dist/runtime/ruby-gems.d.ts +0 -30
- package/dist/runtime/ruby-gems.d.ts.map +0 -1
- package/dist/runtime/ruby-gems.js +0 -636
- package/dist/runtime/ruby-runner.d.ts +0 -75
- package/dist/runtime/ruby-runner.d.ts.map +0 -1
- package/dist/runtime/ruby-runner.js +0 -1577
- package/dist/runtime/runtime-registry.d.ts +0 -162
- package/dist/runtime/runtime-registry.d.ts.map +0 -1
- package/dist/runtime/runtime-registry.js +0 -364
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +0 -14
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +0 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +0 -13
- package/dist/runtime/wasm-runner.d.ts +0 -70
- package/dist/runtime/wasm-runner.d.ts.map +0 -1
- package/dist/runtime/wasm-runner.js +0 -664
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* runtime-registry.ts — shared shell-command factory for runtime
|
|
3
|
-
* dispatchers (node, bun, and future native-WASM / Python / Ruby /
|
|
4
|
-
* AssemblyScript runtimes).
|
|
5
|
-
*
|
|
6
|
-
* Why this exists
|
|
7
|
-
* ───────────────
|
|
8
|
-
* `node` and `bun` shell-command handlers in src/session/init.ts
|
|
9
|
-
* shared ~85% of their code: argv parsing for --version / --help /
|
|
10
|
-
* -e / script-path, VFS lookup, shebang strip, esbuild transform for
|
|
11
|
-
* .ts/.tsx/.jsx, dispatch to the runner. The duplication had drifted
|
|
12
|
-
* — only `node` had the primitive #1 nodeFlagSpan fix
|
|
13
|
-
* (init.ts:233-243), only `node` had primitive-#2 binSpawn ctx
|
|
14
|
-
* propagation (init.ts:391-403), only `bun` had install / run
|
|
15
|
-
* subcommand routing.
|
|
16
|
-
*
|
|
17
|
-
* `buildRuntimeHandler` returns a single shell-handler function that
|
|
18
|
-
* encodes the shared contract. Per-runtime variation is supplied
|
|
19
|
-
* via the `RuntimeSpec` parameter:
|
|
20
|
-
*
|
|
21
|
-
* - name + version + helpText
|
|
22
|
-
* - run(): runner fn (runFresh / runBunScript / wasm-runner)
|
|
23
|
-
* - subcommands: optional map of `<verb> → handler` for
|
|
24
|
-
* bun-style `bun install`, `bun run` (node has none today)
|
|
25
|
-
* - transform(): optional code rewriter (bun prepends BUN_SHIM_PREAMBLE)
|
|
26
|
-
* - supportsBinSpawn: true for node (the .bin handler propagates
|
|
27
|
-
* a callerPid); other runtimes use a plain spawn flow.
|
|
28
|
-
*
|
|
29
|
-
* Anti-requirements observed
|
|
30
|
-
* ──────────────────────────
|
|
31
|
-
* - NO setTimeout / NO retry / NO defensive-catch added.
|
|
32
|
-
* - NO behavioral change vs the pre-refactor handlers — every
|
|
33
|
-
* runtime-specific quirk is preserved exactly.
|
|
34
|
-
* - Per-runtime test parity: existing runtime-primitives probes
|
|
35
|
-
* (#1 npx / #2 .bin) and runtime-pkg probes (G1-G4) MUST still
|
|
36
|
-
* pass against the refactored handlers — the contract is
|
|
37
|
-
* observable behaviour, not implementation shape.
|
|
38
|
-
*/
|
|
39
|
-
import type { FacetManager } from '../facets/manager.js';
|
|
40
|
-
import type { SqliteVFS } from '@nimbus-sh/core/vfs/sqlite-vfs.js';
|
|
41
|
-
import { type VfsCred } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
42
|
-
import type { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
|
|
43
|
-
import { type FacetBundleProfile } from '@nimbus-sh/core/runtime/bundle-profile.js';
|
|
44
|
-
/**
|
|
45
|
-
* Result shape that runtime-registry expects from a runner. Mirrors
|
|
46
|
-
* the existing RunFreshResult / RunBunResult shapes — kept narrow so
|
|
47
|
-
* future runtimes don't have to plumb runtime-internal state.
|
|
48
|
-
*/
|
|
49
|
-
export interface RuntimeRunResult {
|
|
50
|
-
exitCode: number;
|
|
51
|
-
stdout: string;
|
|
52
|
-
stderr: string;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Options the handler passes to the runner. Mirrors RunFreshOpts.
|
|
56
|
-
*/
|
|
57
|
-
export interface RuntimeRunOpts {
|
|
58
|
-
argv: string[];
|
|
59
|
-
env: Record<string, string> | undefined;
|
|
60
|
-
cwd: string | undefined;
|
|
61
|
-
filename: string;
|
|
62
|
-
dirname: string;
|
|
63
|
-
command: string;
|
|
64
|
-
/** Primitive #1/G4 hooks. node-runner consumes these; other
|
|
65
|
-
* runtimes ignore them safely. */
|
|
66
|
-
skipSpawn?: boolean;
|
|
67
|
-
callerPid?: number;
|
|
68
|
-
/** Capture stdout/stderr in the result instead of streaming to the
|
|
69
|
-
* terminal supervisor. Used by child_process pipe semantics. */
|
|
70
|
-
captureOutput?: boolean;
|
|
71
|
-
forceLongRunning?: boolean;
|
|
72
|
-
attachedTty?: boolean;
|
|
73
|
-
bundleProfile?: FacetBundleProfile;
|
|
74
|
-
/** Invoking process credentials for credential-bound runtime snapshots. */
|
|
75
|
-
cred?: VfsCred;
|
|
76
|
-
}
|
|
77
|
-
/** The VFS surface script resolution needs. */
|
|
78
|
-
export interface ScriptResolutionFs {
|
|
79
|
-
isFile(path: string): boolean;
|
|
80
|
-
readFileString(path: string): string;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Resolve a runtime target — `./cli.ts`, `sub/x`, `.`, or a bare name — to a
|
|
84
|
-
* canonical VFS key, or null when nothing runnable sits there.
|
|
85
|
-
*
|
|
86
|
-
* A directory never resolves to itself: it falls through to the index
|
|
87
|
-
* candidates, so `bun ./tools` finds `tools/index.js` the way real bun does
|
|
88
|
-
* rather than trying to read the directory as source.
|
|
89
|
-
*/
|
|
90
|
-
export declare function resolveRuntimeScriptPath(fs: ScriptResolutionFs, cwd: string, target: string, opts?: {
|
|
91
|
-
preferModuleField?: boolean;
|
|
92
|
-
}): string | null;
|
|
93
|
-
/**
|
|
94
|
-
* A subcommand handler. `runAsRuntime` re-enters the standard flow — flag
|
|
95
|
-
* span, script resolution, transform, exec — with a rewritten argv, as if
|
|
96
|
-
* the verb had never been typed. `bun run <file>` uses it to hand a path
|
|
97
|
-
* to the very same execution path `bun <file>` takes, rather than growing
|
|
98
|
-
* a second one.
|
|
99
|
-
*/
|
|
100
|
-
export type RuntimeSubcommand = (ctx: any, registry: ShellRegistry, runAsRuntime: (args: string[]) => Promise<number>) => Promise<number>;
|
|
101
|
-
export interface RuntimeSpec {
|
|
102
|
-
/** Shell-command name: 'node' / 'bun' / 'wasm-runner' / 'python'. */
|
|
103
|
-
name: string;
|
|
104
|
-
/** Output of `<name> --version`. Includes the leading 'v' if the
|
|
105
|
-
* runtime convention does (Node: 'v20.0.0'; Bun: '1.1.42'). */
|
|
106
|
-
version: string;
|
|
107
|
-
/** Multi-line help text for `<name> --help`. */
|
|
108
|
-
helpText: string;
|
|
109
|
-
/**
|
|
110
|
-
* Runner function. Usually wraps facetMgr.exec / facetMgr.spawn.
|
|
111
|
-
* For native-WASM, this is a thin WebAssembly.instantiate +
|
|
112
|
-
* function-call helper.
|
|
113
|
-
*/
|
|
114
|
-
run(facetMgr: FacetManager, code: string, opts: RuntimeRunOpts): Promise<RuntimeRunResult>;
|
|
115
|
-
/**
|
|
116
|
-
* Subcommand router. When the first positional arg is a key in
|
|
117
|
-
* this map, the handler is invoked instead of the standard
|
|
118
|
-
* script-execution flow. Used by `bun install`, `bun run <script>`.
|
|
119
|
-
*/
|
|
120
|
-
subcommands?: Record<string, RuntimeSubcommand>;
|
|
121
|
-
/**
|
|
122
|
-
* When true, the runtime treats the args list as a binary file
|
|
123
|
-
* path (NOT a JS script). Used by `wasm-runner` — the args[0] is a
|
|
124
|
-
* .wasm path, args[1+] are the function name + integer args.
|
|
125
|
-
* The handler skips the read-and-transform-script flow and calls
|
|
126
|
-
* `run()` with a synthetic empty `code` — runtimes that set this
|
|
127
|
-
* flag implement the actual bytes-load inside their runner.
|
|
128
|
-
*/
|
|
129
|
-
bypassesScriptRead?: boolean;
|
|
130
|
-
/**
|
|
131
|
-
* Primitive #1 / G4 — when true, the script-execution branch
|
|
132
|
-
* propagates `ctx.__nimbusBinSpawn` into RuntimeRunOpts. Only
|
|
133
|
-
* `node` enables this; bun's runFresh chain doesn't share PID
|
|
134
|
-
* state with the .bin handler today. Future runtimes set this
|
|
135
|
-
* iff they share the runFresh contract.
|
|
136
|
-
*/
|
|
137
|
-
supportsBinSpawn?: boolean;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Minimal registry shape we depend on. Avoids importing the full vendored
|
|
141
|
-
* shell registry type tree when the runtime path only needs resolve().
|
|
142
|
-
*/
|
|
143
|
-
export interface ShellRegistry {
|
|
144
|
-
resolve(name: string): Promise<any> | any;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Build a shell-handler function for a runtime. The returned function
|
|
148
|
-
* is the value passed to `registry.register('<name>', handler)`.
|
|
149
|
-
*
|
|
150
|
-
* Captures `vfs`, `facetMgr`, `esbuild`, `getEsbuild` (for lazy init)
|
|
151
|
-
* + the spec. The same factory is used for every runtime; the only
|
|
152
|
-
* runtime-specific code lives in `spec`.
|
|
153
|
-
*/
|
|
154
|
-
export declare function buildRuntimeHandler(spec: RuntimeSpec, ctx0: {
|
|
155
|
-
vfs: SqliteVFS;
|
|
156
|
-
facetMgr: FacetManager;
|
|
157
|
-
/** Lazy esbuild initialiser. Called once per first .ts/.tsx/.jsx
|
|
158
|
-
* invocation — the host owns the init lifecycle. */
|
|
159
|
-
getEsbuild(): EsbuildService;
|
|
160
|
-
registry: ShellRegistry;
|
|
161
|
-
}): (ctx: any) => Promise<number>;
|
|
162
|
-
//# sourceMappingURL=runtime-registry.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-registry.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEnE,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4CAA4C,CAAC;AACjF,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAG7G;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACxC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB;uCACmC;IACnC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;qEACiE;IACjE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAKD,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,kBAAkB,EACtB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACrC,MAAM,GAAG,IAAI,CAmBf;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,KAC9C,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb;oEACgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3F;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CAC3C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE;IACJ,GAAG,EAAE,SAAS,CAAC;IACf,QAAQ,EAAE,YAAY,CAAC;IACvB;yDACqD;IACrD,UAAU,IAAI,cAAc,CAAC;IAC7B,QAAQ,EAAE,aAAa,CAAC;CACzB,GACA,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,MAAM,CAAC,CAmT/B"}
|
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* runtime-registry.ts — shared shell-command factory for runtime
|
|
3
|
-
* dispatchers (node, bun, and future native-WASM / Python / Ruby /
|
|
4
|
-
* AssemblyScript runtimes).
|
|
5
|
-
*
|
|
6
|
-
* Why this exists
|
|
7
|
-
* ───────────────
|
|
8
|
-
* `node` and `bun` shell-command handlers in src/session/init.ts
|
|
9
|
-
* shared ~85% of their code: argv parsing for --version / --help /
|
|
10
|
-
* -e / script-path, VFS lookup, shebang strip, esbuild transform for
|
|
11
|
-
* .ts/.tsx/.jsx, dispatch to the runner. The duplication had drifted
|
|
12
|
-
* — only `node` had the primitive #1 nodeFlagSpan fix
|
|
13
|
-
* (init.ts:233-243), only `node` had primitive-#2 binSpawn ctx
|
|
14
|
-
* propagation (init.ts:391-403), only `bun` had install / run
|
|
15
|
-
* subcommand routing.
|
|
16
|
-
*
|
|
17
|
-
* `buildRuntimeHandler` returns a single shell-handler function that
|
|
18
|
-
* encodes the shared contract. Per-runtime variation is supplied
|
|
19
|
-
* via the `RuntimeSpec` parameter:
|
|
20
|
-
*
|
|
21
|
-
* - name + version + helpText
|
|
22
|
-
* - run(): runner fn (runFresh / runBunScript / wasm-runner)
|
|
23
|
-
* - subcommands: optional map of `<verb> → handler` for
|
|
24
|
-
* bun-style `bun install`, `bun run` (node has none today)
|
|
25
|
-
* - transform(): optional code rewriter (bun prepends BUN_SHIM_PREAMBLE)
|
|
26
|
-
* - supportsBinSpawn: true for node (the .bin handler propagates
|
|
27
|
-
* a callerPid); other runtimes use a plain spawn flow.
|
|
28
|
-
*
|
|
29
|
-
* Anti-requirements observed
|
|
30
|
-
* ──────────────────────────
|
|
31
|
-
* - NO setTimeout / NO retry / NO defensive-catch added.
|
|
32
|
-
* - NO behavioral change vs the pre-refactor handlers — every
|
|
33
|
-
* runtime-specific quirk is preserved exactly.
|
|
34
|
-
* - Per-runtime test parity: existing runtime-primitives probes
|
|
35
|
-
* (#1 npx / #2 .bin) and runtime-pkg probes (G1-G4) MUST still
|
|
36
|
-
* pass against the refactored handlers — the contract is
|
|
37
|
-
* observable behaviour, not implementation shape.
|
|
38
|
-
*/
|
|
39
|
-
import { normalizeVfsPath, resolveVfsPath, vfsPathExtension } from '@nimbus-sh/core/vfs/path.js';
|
|
40
|
-
import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
41
|
-
import { parseFacetBundleProfile } from '@nimbus-sh/core/runtime/bundle-profile.js';
|
|
42
|
-
import { bindImportMetaResolve, importMetaDefines } from '@nimbus-sh/core/runtime/import-meta-transform.js';
|
|
43
|
-
/** Extensions probed when a target names no exact file, in Node's order. */
|
|
44
|
-
const SCRIPT_RESOLUTION_CANDIDATES = ['.js', '.ts', '.tsx', '.mjs', '.jsx', '/index.js', '/index.ts'];
|
|
45
|
-
/**
|
|
46
|
-
* Resolve a runtime target — `./cli.ts`, `sub/x`, `.`, or a bare name — to a
|
|
47
|
-
* canonical VFS key, or null when nothing runnable sits there.
|
|
48
|
-
*
|
|
49
|
-
* A directory never resolves to itself: it falls through to the index
|
|
50
|
-
* candidates, so `bun ./tools` finds `tools/index.js` the way real bun does
|
|
51
|
-
* rather than trying to read the directory as source.
|
|
52
|
-
*/
|
|
53
|
-
export function resolveRuntimeScriptPath(fs, cwd, target, opts) {
|
|
54
|
-
const base = normalizeVfsPath(cwd || '/home/user');
|
|
55
|
-
let resolved;
|
|
56
|
-
if (target === '.' || target === './') {
|
|
57
|
-
// `node .` / `bun .` — the package's declared entry point.
|
|
58
|
-
let main = 'index.js';
|
|
59
|
-
try {
|
|
60
|
-
const pkg = JSON.parse(fs.readFileString(`${base}/package.json`));
|
|
61
|
-
main = (opts?.preferModuleField ? pkg.module : undefined) || pkg.main || 'index.js';
|
|
62
|
-
}
|
|
63
|
-
catch { /* no readable package.json — index.js */ }
|
|
64
|
-
resolved = resolveVfsPath(main, base);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
resolved = resolveVfsPath(target, base);
|
|
68
|
-
}
|
|
69
|
-
if (fs.isFile(resolved))
|
|
70
|
-
return resolved;
|
|
71
|
-
for (const candidate of SCRIPT_RESOLUTION_CANDIDATES) {
|
|
72
|
-
if (fs.isFile(resolved + candidate))
|
|
73
|
-
return resolved + candidate;
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Build a shell-handler function for a runtime. The returned function
|
|
79
|
-
* is the value passed to `registry.register('<name>', handler)`.
|
|
80
|
-
*
|
|
81
|
-
* Captures `vfs`, `facetMgr`, `esbuild`, `getEsbuild` (for lazy init)
|
|
82
|
-
* + the spec. The same factory is used for every runtime; the only
|
|
83
|
-
* runtime-specific code lives in `spec`.
|
|
84
|
-
*/
|
|
85
|
-
export function buildRuntimeHandler(spec, ctx0) {
|
|
86
|
-
const { vfs, facetMgr, getEsbuild, registry } = ctx0;
|
|
87
|
-
const fs = vfs.as(CRED_KERNEL);
|
|
88
|
-
/**
|
|
89
|
-
* The standard invocation: flag span, --version/--help/-e, then the
|
|
90
|
-
* script-path flow. Subcommand verbs are NOT considered here — the
|
|
91
|
-
* caller has already consumed them — so a verb handler can delegate
|
|
92
|
-
* back in with a rewritten argv without re-triggering itself.
|
|
93
|
-
*/
|
|
94
|
-
async function runtimeInvocation(ctx, args) {
|
|
95
|
-
const name = spec.name;
|
|
96
|
-
const nimbusCtx = ctx;
|
|
97
|
-
// A facet-hosted runtime streams its output to the session terminal over
|
|
98
|
-
// the supervisor RPC and hands the shell an empty string. That is live and
|
|
99
|
-
// cheap, and it is only correct while the process's stdout IS the terminal:
|
|
100
|
-
// the stream bypasses the shell's stdout chain, so the moment fd 1 or fd 2
|
|
101
|
-
// is a file, a pipe, a command substitution, or the capture sink of a
|
|
102
|
-
// programmatic exec, the bytes have to come back in the result and be
|
|
103
|
-
// written through ctx.stdout instead. A context with no fd table of its own
|
|
104
|
-
// — the child_process broker synthesizes one — says so directly.
|
|
105
|
-
const captureOutput = !!nimbusCtx.__nimbusCaptureOutput
|
|
106
|
-
|| ctx.isFdTerminal?.(1) === false
|
|
107
|
-
|| ctx.isFdTerminal?.(2) === false;
|
|
108
|
-
const bundleProfile = parseFacetBundleProfile(nimbusCtx.__nimbusBundleProfile);
|
|
109
|
-
// ── Flag-span computation (primitive #1) ──
|
|
110
|
-
//
|
|
111
|
-
// Real-Node only treats args UP TO the first non-flag token as
|
|
112
|
-
// CLI flags. Pre-refactor, version/help/eval scanned the entire
|
|
113
|
-
// args array, breaking `node /path/to/tsc --version` (the user's
|
|
114
|
-
// --version was misinterpreted as a node flag).
|
|
115
|
-
let flagSpan = 0;
|
|
116
|
-
while (flagSpan < args.length && args[flagSpan].startsWith('-')) {
|
|
117
|
-
flagSpan++;
|
|
118
|
-
const prev = args[flagSpan - 1];
|
|
119
|
-
// -e / --eval consumes one value; advance past it.
|
|
120
|
-
if ((prev === '-e' || prev === '--eval') && flagSpan < args.length) {
|
|
121
|
-
flagSpan++;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
const flagSlice = args.slice(0, flagSpan);
|
|
125
|
-
// ── --version ──
|
|
126
|
-
if (flagSlice.includes('-v') || flagSlice.includes('--version')) {
|
|
127
|
-
ctx.stdout.write(spec.version + '\n');
|
|
128
|
-
return 0;
|
|
129
|
-
}
|
|
130
|
-
// ── --help ──
|
|
131
|
-
if (flagSlice.includes('--help') || flagSlice.includes('-h')) {
|
|
132
|
-
ctx.stdout.write(spec.helpText);
|
|
133
|
-
if (!spec.helpText.endsWith('\n'))
|
|
134
|
-
ctx.stdout.write('\n');
|
|
135
|
-
return 0;
|
|
136
|
-
}
|
|
137
|
-
// ── -e / --eval ──
|
|
138
|
-
const evalIdx = flagSlice.indexOf('-e') !== -1
|
|
139
|
-
? flagSlice.indexOf('-e')
|
|
140
|
-
: flagSlice.indexOf('--eval');
|
|
141
|
-
if (evalIdx !== -1) {
|
|
142
|
-
const code = args[evalIdx + 1];
|
|
143
|
-
if (!code) {
|
|
144
|
-
ctx.stderr.write(`${name}: -e requires an argument\n`);
|
|
145
|
-
return 1;
|
|
146
|
-
}
|
|
147
|
-
const result = await spec.run(facetMgr, code, {
|
|
148
|
-
cred: ctx.cred,
|
|
149
|
-
argv: args.slice(evalIdx + 2),
|
|
150
|
-
env: ctx.env,
|
|
151
|
-
cwd: ctx.cwd,
|
|
152
|
-
filename: '<eval>',
|
|
153
|
-
dirname: ctx.cwd || '/home/user',
|
|
154
|
-
command: `${name} -e ...`,
|
|
155
|
-
...(captureOutput ? { captureOutput: true } : {}),
|
|
156
|
-
...(bundleProfile ? { bundleProfile } : {}),
|
|
157
|
-
});
|
|
158
|
-
if (result.stdout)
|
|
159
|
-
ctx.stdout.write(result.stdout);
|
|
160
|
-
if (result.stderr)
|
|
161
|
-
ctx.stderr.write(result.stderr);
|
|
162
|
-
return result.exitCode;
|
|
163
|
-
}
|
|
164
|
-
// ── script path (or .wasm path for bypassesScriptRead) ──
|
|
165
|
-
const scriptIdx = flagSpan;
|
|
166
|
-
const scriptPath = args[scriptIdx];
|
|
167
|
-
if (!scriptPath) {
|
|
168
|
-
ctx.stderr.write(`${name}: REPL not supported. Use ${name} -e "code" or ${name} script.js\n`);
|
|
169
|
-
return 1;
|
|
170
|
-
}
|
|
171
|
-
// ── bypassesScriptRead branch (wasm-runner) ──
|
|
172
|
-
//
|
|
173
|
-
// The runner takes the path AS-IS (it's a .wasm, not JS source).
|
|
174
|
-
// We don't read or transform here; the runner reads the bytes
|
|
175
|
-
// and instantiates them. `.` resolution and extension probing are
|
|
176
|
-
// meaningless for a .wasm target and stay out of this branch.
|
|
177
|
-
if (spec.bypassesScriptRead) {
|
|
178
|
-
const filename = '/' + resolveVfsPath(scriptPath, ctx.cwd || '/home/user');
|
|
179
|
-
const dirname = filename.includes('/')
|
|
180
|
-
? filename.substring(0, filename.lastIndexOf('/'))
|
|
181
|
-
: '/';
|
|
182
|
-
// `args.slice(scriptIdx + 1)` are the runner's user args (e.g.
|
|
183
|
-
// [exportName, intArg1, intArg2, ...] for wasm-runner).
|
|
184
|
-
const result = await spec.run(facetMgr, '', {
|
|
185
|
-
cred: ctx.cred,
|
|
186
|
-
argv: args.slice(scriptIdx + 1),
|
|
187
|
-
env: ctx.env,
|
|
188
|
-
cwd: ctx.cwd,
|
|
189
|
-
filename,
|
|
190
|
-
dirname,
|
|
191
|
-
command: `${name} ${args.slice(0, scriptIdx + 1).join(' ')}`,
|
|
192
|
-
...(captureOutput ? { captureOutput: true } : {}),
|
|
193
|
-
...(bundleProfile ? { bundleProfile } : {}),
|
|
194
|
-
});
|
|
195
|
-
if (result.stdout)
|
|
196
|
-
ctx.stdout.write(result.stdout);
|
|
197
|
-
if (result.stderr)
|
|
198
|
-
ctx.stderr.write(result.stderr);
|
|
199
|
-
return result.exitCode;
|
|
200
|
-
}
|
|
201
|
-
// Resolve against cwd: `.` → the package entry, then extension probing.
|
|
202
|
-
const resolvedPath = resolveRuntimeScriptPath(fs, ctx.cwd || '/home/user', scriptPath, {
|
|
203
|
-
// bun prefers .module over .main when both exist; node uses .main.
|
|
204
|
-
preferModuleField: name === 'bun',
|
|
205
|
-
});
|
|
206
|
-
let code = null;
|
|
207
|
-
if (resolvedPath !== null) {
|
|
208
|
-
try {
|
|
209
|
-
code = fs.readFileString(resolvedPath);
|
|
210
|
-
}
|
|
211
|
-
catch { /* unreadable — reported below */ }
|
|
212
|
-
}
|
|
213
|
-
if (resolvedPath === null || code === null) {
|
|
214
|
-
ctx.stderr.write(`${name}: cannot find module '${scriptPath}'\n`);
|
|
215
|
-
return 1;
|
|
216
|
-
}
|
|
217
|
-
// Shebang strip (primitive #1).
|
|
218
|
-
if (code.startsWith('#!')) {
|
|
219
|
-
const nl = code.indexOf('\n');
|
|
220
|
-
code = nl >= 0 ? code.substring(nl + 1) : '';
|
|
221
|
-
}
|
|
222
|
-
// ── ESM-source detection (primitive: type:module entry scripts) ──
|
|
223
|
-
//
|
|
224
|
-
// Nimbus's facet pre-compile loop wraps every entry script in
|
|
225
|
-
// `new Function(...)` which runs it as CJS. A real `node script.js`
|
|
226
|
-
// dispatch honours the nearest package.json's `"type"` field
|
|
227
|
-
// (and the file extension) to decide whether to parse as ESM:
|
|
228
|
-
//
|
|
229
|
-
// - .mjs → always ESM
|
|
230
|
-
// - .cjs → always CJS
|
|
231
|
-
// - .js → ESM iff nearest package.json has "type": "module"
|
|
232
|
-
// - no extension → same rule as .js. Node allows an extensionless
|
|
233
|
-
// main entry and resolves its format from the
|
|
234
|
-
// package type, and that is the shape of nearly
|
|
235
|
-
// every npm `bin` script (typescript's `bin/tsc`,
|
|
236
|
-
// and the `node_modules/.bin/<cli>` target the bin
|
|
237
|
-
// dispatcher hands us).
|
|
238
|
-
//
|
|
239
|
-
// Without this, every modern ESM-only npm initialiser
|
|
240
|
-
// (create-vite, create-astro, create-svelte, modern create-*)
|
|
241
|
-
// crashes immediately with "Cannot use import statement outside
|
|
242
|
-
// a module" because their bin entry is `index.js` and the
|
|
243
|
-
// package.json declares `type: module`.
|
|
244
|
-
//
|
|
245
|
-
// We transform to CJS (format: 'cjs') so the facet's `new
|
|
246
|
-
// Function()` runs it as a CJS module body — same path that the
|
|
247
|
-
// bundle's `transformEsmInBundle` (W3.5 Fix B) takes for
|
|
248
|
-
// sub-module ESM files. esbuild's CJS output emits __require /
|
|
249
|
-
// module.exports / exports.X so the facet's pre-compile loop
|
|
250
|
-
// sees ordinary CJS source.
|
|
251
|
-
function nearestPackageTypeIsModule(absPath) {
|
|
252
|
-
// Walk up dirs looking for the nearest package.json. First one
|
|
253
|
-
// wins (Node spec); we do NOT consult ancestors past it.
|
|
254
|
-
let dir = absPath.replace(/^\/+/, '');
|
|
255
|
-
const slash = dir.lastIndexOf('/');
|
|
256
|
-
dir = slash > 0 ? dir.substring(0, slash) : '';
|
|
257
|
-
const visited = new Set();
|
|
258
|
-
while (dir && !visited.has(dir)) {
|
|
259
|
-
visited.add(dir);
|
|
260
|
-
const pj = dir + '/package.json';
|
|
261
|
-
if (fs.exists(pj)) {
|
|
262
|
-
try {
|
|
263
|
-
const pkg = JSON.parse(fs.readFileString(pj));
|
|
264
|
-
return pkg && pkg.type === 'module';
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const last = dir.lastIndexOf('/');
|
|
271
|
-
if (last <= 0)
|
|
272
|
-
break;
|
|
273
|
-
dir = dir.substring(0, last);
|
|
274
|
-
}
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
const scriptExt = vfsPathExtension(resolvedPath);
|
|
278
|
-
const needsEsmTransform = scriptExt === '.mjs' ||
|
|
279
|
-
((scriptExt === '.js' || scriptExt === '') && nearestPackageTypeIsModule(resolvedPath));
|
|
280
|
-
// esbuild transform for TypeScript / TSX / JSX (both node and bun)
|
|
281
|
-
// AND for ESM entry scripts (primitive ESM-detect).
|
|
282
|
-
if (scriptExt === '.ts' ||
|
|
283
|
-
scriptExt === '.tsx' ||
|
|
284
|
-
scriptExt === '.jsx' ||
|
|
285
|
-
needsEsmTransform) {
|
|
286
|
-
try {
|
|
287
|
-
const eb = getEsbuild();
|
|
288
|
-
const loader = scriptExt === '.tsx' ? 'tsx' :
|
|
289
|
-
scriptExt === '.jsx' ? 'jsx' :
|
|
290
|
-
scriptExt === '.ts' ? 'ts' :
|
|
291
|
-
'js';
|
|
292
|
-
// Substitute `import.meta.url` at compile-time so esbuild's
|
|
293
|
-
// CJS output doesn't reduce it to `undefined` (its default
|
|
294
|
-
// for unknown import.meta references). The substitution
|
|
295
|
-
// value is a real `file://<absolute-path>` URL — exactly
|
|
296
|
-
// what real Node returns when running this script. Tools
|
|
297
|
-
// that compute `fileURLToPath(import.meta.url)` (create-vite,
|
|
298
|
-
// most modern CLIs) then resolve relative paths against
|
|
299
|
-
// the actual script location.
|
|
300
|
-
//
|
|
301
|
-
// Without this, `create-vite` does
|
|
302
|
-
// r(import.meta.url) → fileURLToPath(undefined) → throws
|
|
303
|
-
// → falls into a different code path
|
|
304
|
-
// → readdirSync(wrong-template-dir) returns []
|
|
305
|
-
// → "Scaffolding..." but writes no files.
|
|
306
|
-
const absUrl = 'file:///' + resolvedPath.replace(/^\/+/, '');
|
|
307
|
-
const transformed = await eb.transform(code, {
|
|
308
|
-
loader,
|
|
309
|
-
format: 'cjs',
|
|
310
|
-
define: importMetaDefines(absUrl),
|
|
311
|
-
});
|
|
312
|
-
code = bindImportMetaResolve(transformed.code, absUrl);
|
|
313
|
-
}
|
|
314
|
-
catch (e) {
|
|
315
|
-
ctx.stderr.write(`${name}: transform error for ${scriptPath}: ${e?.message}\n`);
|
|
316
|
-
return 1;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
const filename = '/' + resolvedPath;
|
|
320
|
-
const dirname = filename.includes('/')
|
|
321
|
-
? filename.substring(0, filename.lastIndexOf('/'))
|
|
322
|
-
: '/';
|
|
323
|
-
// Primitive #1 / G4 — propagate bin-spawn ctx if the runtime
|
|
324
|
-
// supports it (currently node only).
|
|
325
|
-
const binSpawn = spec.supportsBinSpawn ? nimbusCtx.__nimbusBinSpawn : undefined;
|
|
326
|
-
const leadingFlags = args.slice(0, scriptIdx);
|
|
327
|
-
const result = await spec.run(facetMgr, code, {
|
|
328
|
-
cred: ctx.cred,
|
|
329
|
-
argv: [...leadingFlags, filename, ...args.slice(scriptIdx + 1)],
|
|
330
|
-
env: ctx.env,
|
|
331
|
-
cwd: ctx.cwd,
|
|
332
|
-
filename,
|
|
333
|
-
dirname,
|
|
334
|
-
command: binSpawn?.command || `${name} ${args.slice(0, scriptIdx + 1).join(' ')}`,
|
|
335
|
-
...(binSpawn ? {
|
|
336
|
-
skipSpawn: true,
|
|
337
|
-
callerPid: binSpawn.callerPid,
|
|
338
|
-
forceLongRunning: binSpawn.forceLongRunning === true,
|
|
339
|
-
attachedTty: binSpawn.attachedTty === true,
|
|
340
|
-
} : {}),
|
|
341
|
-
...(captureOutput ? { captureOutput: true } : {}),
|
|
342
|
-
...(bundleProfile ? { bundleProfile } : {}),
|
|
343
|
-
});
|
|
344
|
-
if (result.stdout)
|
|
345
|
-
ctx.stdout.write(result.stdout);
|
|
346
|
-
if (result.stderr)
|
|
347
|
-
ctx.stderr.write(result.stderr);
|
|
348
|
-
return result.exitCode;
|
|
349
|
-
}
|
|
350
|
-
return async function runtimeHandler(ctx) {
|
|
351
|
-
const args = ctx.args || [];
|
|
352
|
-
// ── Subcommand dispatch ──
|
|
353
|
-
//
|
|
354
|
-
// BEFORE flag-span computation: subcommands like `bun install`
|
|
355
|
-
// have their first positional arg as the verb, NOT a node-style
|
|
356
|
-
// flag. A verb owns the whole invocation, but it may hand a
|
|
357
|
-
// rewritten argv back to the standard flow — that is how
|
|
358
|
-
// `bun run <file>` reaches the same execution path as `bun <file>`.
|
|
359
|
-
if (spec.subcommands && args.length > 0 && spec.subcommands[args[0]]) {
|
|
360
|
-
return spec.subcommands[args[0]](ctx, registry, (rewritten) => runtimeInvocation(ctx, rewritten));
|
|
361
|
-
}
|
|
362
|
-
return runtimeInvocation(ctx, args);
|
|
363
|
-
};
|
|
364
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* virtual-socket-kernel.generated.ts — AUTO-GENERATED. DO NOT EDIT.
|
|
3
|
-
*
|
|
4
|
-
* Produced by scripts/bundle-facet-workers.mjs from:
|
|
5
|
-
* - @nimbus-sh/core src/runtime/virtual-socket-kernel.ts
|
|
6
|
-
*
|
|
7
|
-
* Self-contained IIFE that installs globalThis.__nimbusVirtualSockets.
|
|
8
|
-
* Consumed by python-runner.ts and ruby-runner.ts: spliced into the
|
|
9
|
-
* socket process worker module source passed to NimbusLoaderPool.
|
|
10
|
-
*
|
|
11
|
-
* Size: 33.72 KiB
|
|
12
|
-
*/
|
|
13
|
-
export declare const VIRTUAL_SOCKET_KERNEL_SRC: string;
|
|
14
|
-
//# sourceMappingURL=virtual-socket-kernel.generated.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"virtual-socket-kernel.generated.d.ts","sourceRoot":"","sources":["../../src/runtime/virtual-socket-kernel.generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,yBAAyB,EAAE,MAAu5lC,CAAC"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* virtual-socket-kernel.generated.ts — AUTO-GENERATED. DO NOT EDIT.
|
|
3
|
-
*
|
|
4
|
-
* Produced by scripts/bundle-facet-workers.mjs from:
|
|
5
|
-
* - @nimbus-sh/core src/runtime/virtual-socket-kernel.ts
|
|
6
|
-
*
|
|
7
|
-
* Self-contained IIFE that installs globalThis.__nimbusVirtualSockets.
|
|
8
|
-
* Consumed by python-runner.ts and ruby-runner.ts: spliced into the
|
|
9
|
-
* socket process worker module source passed to NimbusLoaderPool.
|
|
10
|
-
*
|
|
11
|
-
* Size: 33.72 KiB
|
|
12
|
-
*/
|
|
13
|
-
export const VIRTUAL_SOCKET_KERNEL_SRC = "(() => {\n // ../core/src/runtime/virtual-socket-kernel.ts\n var DEFAULT_LIMITS = {\n responseTimeoutMs: 3e4,\n maxRequestBodyBytes: 32 * 1024 * 1024,\n maxResponseBufferBytes: 64 * 1024 * 1024\n };\n var EMPTY_BYTES = new Uint8Array(0);\n var Deferred = class {\n promise;\n resolve;\n reject;\n constructor() {\n let resolve = () => {\n };\n let reject = () => {\n };\n this.promise = new Promise((res, rej) => {\n resolve = res;\n reject = rej;\n });\n this.resolve = resolve;\n this.reject = reject;\n }\n };\n function isPyodideProxyLike(value) {\n return value !== null && typeof value === \"object\" && typeof value.toJs === \"function\";\n }\n function toBytes(value) {\n if (value instanceof Uint8Array) return value;\n if (value instanceof ArrayBuffer) return new Uint8Array(value);\n if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);\n if (Array.isArray(value)) return Uint8Array.from(value);\n if (typeof value === \"string\") return new TextEncoder().encode(value);\n if (isPyodideProxyLike(value)) return toBytes(value.toJs());\n return EMPTY_BYTES;\n }\n function concatBytes(parts) {\n let length = 0;\n for (const part of parts) length += part.byteLength;\n const out = new Uint8Array(length);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.byteLength;\n }\n return out;\n }\n function responseCanHaveBody(requestMethod, status) {\n if (requestMethod.toUpperCase() === \"HEAD\") return false;\n return status !== 204 && status !== 205 && status !== 304;\n }\n function encodeHttpRequest(request, body) {\n const url = new URL(request.url);\n const path = (url.pathname || \"/\") + url.search;\n const headers = new Headers(request.headers);\n if (!headers.has(\"Host\")) headers.set(\"Host\", url.host || \"nimbus.local\");\n if (body.byteLength > 0 && !headers.has(\"Content-Length\")) {\n headers.set(\"Content-Length\", String(body.byteLength));\n }\n const lines = [`${request.method} ${path} HTTP/1.1`];\n headers.forEach((value, key) => lines.push(`${key}: ${value}`));\n lines.push(\"\", \"\");\n return concatBytes([new TextEncoder().encode(lines.join(\"\\r\\n\")), body]);\n }\n function encodeHttpResponseHead(status, statusText, headers, chunkedBody) {\n const lines = [`HTTP/1.1 ${status} ${statusText}`];\n headers.forEach((value, key) => lines.push(`${key}: ${value}`));\n if (chunkedBody) lines.push(\"Transfer-Encoding: chunked\");\n lines.push(\"Connection: close\");\n lines.push(\"\", \"\");\n return new TextEncoder().encode(lines.join(\"\\r\\n\"));\n }\n function encodeChunkedFrame(bytes) {\n return concatBytes([\n new TextEncoder().encode(`${bytes.byteLength.toString(16)}\\r\n`),\n bytes,\n new TextEncoder().encode(\"\\r\\n\")\n ]);\n }\n var CHUNKED_TERMINATOR = new TextEncoder().encode(\"0\\r\\n\\r\\n\");\n var LOOPBACK_READ_HIGH_WATER_BYTES = 1024 * 1024;\n var LOOPBACK_STREAM_CHUNK_BYTES = 64 * 1024;\n var ByteChunkQueue = class {\n constructor(limitTotalBytes, overflowLabel) {\n this.limitTotalBytes = limitTotalBytes;\n this.overflowLabel = overflowLabel;\n }\n limitTotalBytes;\n overflowLabel;\n chunks = [];\n headOffset = 0;\n enqueuedTotalBytes = 0;\n undrainedBytes = 0;\n /** Bytes enqueued but not yet read. Drives flow control on streamed queues. */\n get pendingBytes() {\n return this.undrainedBytes;\n }\n enqueue(bytes) {\n if (bytes.byteLength === 0) return;\n if (this.enqueuedTotalBytes + bytes.byteLength > this.limitTotalBytes) {\n throw new Error(\n `Nimbus virtual socket: ${this.overflowLabel} exceeds ${this.limitTotalBytes} bytes`\n );\n }\n this.enqueuedTotalBytes += bytes.byteLength;\n this.undrainedBytes += bytes.byteLength;\n this.chunks.push(bytes);\n }\n /** Drain up to maxBytes from the head chunk; empty result means no data is queued. */\n readUpTo(maxBytes) {\n const head = this.chunks[0];\n if (!head) return EMPTY_BYTES;\n const available = head.byteLength - this.headOffset;\n const take = Math.min(available, Math.max(1, maxBytes));\n const out = head.subarray(this.headOffset, this.headOffset + take);\n this.headOffset += take;\n this.undrainedBytes -= take;\n if (this.headOffset >= head.byteLength) {\n this.chunks.shift();\n this.headOffset = 0;\n }\n return out;\n }\n };\n var HttpMessageParser = class {\n phase = \"headers\";\n settled = null;\n headerBuffer = EMPTY_BYTES;\n headerScanOffset = 0;\n headerPairs = [];\n framing = \"none\";\n expectedBodyBytes = 0;\n bodyChunks = [];\n bodyByteCount = 0;\n chunkState = \"size\";\n chunkSizeLine = [];\n chunkDataRemaining = 0;\n get outcome() {\n return this.settled;\n }\n feed(chunk) {\n if (this.phase === \"done\" || chunk.byteLength === 0) return;\n if (this.phase === \"headers\") {\n this.feedHeaders(chunk);\n return;\n }\n this.feedBody(chunk);\n }\n /** EOF from the peer closing the connection (or the request being torn down). */\n finish() {\n if (this.phase === \"done\") return;\n if (this.phase === \"headers\") {\n this.fail(`connection closed before ${this.messageLabel} headers`);\n return;\n }\n if (this.framing === \"until-close\") {\n this.completeWithBody(concatBytes(this.bodyChunks));\n return;\n }\n this.fail(`connection closed before the ${this.messageLabel} completed`);\n }\n feedHeaders(chunk) {\n this.headerBuffer = concatBytes([this.headerBuffer, chunk]);\n const headerEnd = this.findHeaderEnd();\n if (headerEnd < 0) {\n this.headerScanOffset = Math.max(0, this.headerBuffer.byteLength - 3);\n return;\n }\n const parsed = this.parseHeaderBlock(this.headerBuffer.subarray(0, headerEnd));\n const leftover = this.headerBuffer.subarray(headerEnd);\n this.headerBuffer = EMPTY_BYTES;\n if (!parsed) return;\n if (!this.bodyAllowed()) {\n this.completeWithBody(null);\n return;\n }\n const transferEncoding = this.headerValue(\"Transfer-Encoding\");\n if (transferEncoding !== null && /chunked/i.test(transferEncoding)) {\n this.framing = \"chunked\";\n } else {\n const contentLength = this.headerValue(\"Content-Length\");\n const expected = contentLength === null ? Number.NaN : parseInt(contentLength, 10);\n if (Number.isFinite(expected) && expected >= 0) {\n this.framing = \"content-length\";\n this.expectedBodyBytes = expected;\n if (expected === 0) {\n this.completeWithBody(EMPTY_BYTES);\n return;\n }\n } else {\n this.framing = this.defaultFramingWhenUnframed();\n if (this.framing === \"none\") {\n this.completeWithBody(null);\n return;\n }\n }\n }\n this.phase = \"body\";\n this.feedBody(leftover);\n }\n feedBody(chunk) {\n if (this.framing === \"chunked\") {\n this.feedChunkedBody(chunk);\n return;\n }\n if (chunk.byteLength === 0) return;\n if (this.framing === \"content-length\") {\n const needed = this.expectedBodyBytes - this.bodyByteCount;\n this.appendBody(chunk.subarray(0, Math.min(needed, chunk.byteLength)));\n if (this.bodyByteCount >= this.expectedBodyBytes) {\n this.completeWithBody(concatBytes(this.bodyChunks));\n }\n return;\n }\n this.appendBody(chunk);\n }\n feedChunkedBody(chunk) {\n let i = 0;\n while (i < chunk.byteLength && this.phase === \"body\") {\n if (this.chunkState === \"size\") {\n const byte = chunk[i++];\n if (byte === 10) {\n const line = new TextDecoder().decode(Uint8Array.from(this.chunkSizeLine)).trim();\n this.chunkSizeLine.length = 0;\n const size = parseInt(line.split(\";\", 1)[0], 16);\n if (!Number.isFinite(size) || size < 0) {\n this.fail(\"malformed chunked transfer encoding\");\n return;\n }\n if (size === 0) {\n this.completeWithBody(concatBytes(this.bodyChunks));\n return;\n }\n this.chunkState = \"data\";\n this.chunkDataRemaining = size;\n } else if (byte !== 13) {\n this.chunkSizeLine.push(byte);\n }\n } else if (this.chunkState === \"data\") {\n const take = Math.min(this.chunkDataRemaining, chunk.byteLength - i);\n this.appendBody(chunk.subarray(i, i + take));\n i += take;\n this.chunkDataRemaining -= take;\n if (this.chunkDataRemaining === 0) this.chunkState = \"data-end\";\n } else {\n const byte = chunk[i++];\n if (byte === 10) {\n this.chunkState = \"size\";\n } else if (byte !== 13) {\n this.fail(\"malformed chunked transfer encoding\");\n return;\n }\n }\n }\n }\n appendBody(bytes) {\n if (bytes.byteLength === 0) return;\n this.bodyChunks.push(bytes);\n this.bodyByteCount += bytes.byteLength;\n }\n findHeaderEnd() {\n const bytes = this.headerBuffer;\n for (let i = this.headerScanOffset; i + 3 < bytes.byteLength; i++) {\n if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) {\n return i + 4;\n }\n }\n return -1;\n }\n /** Returns false when the start line is malformed (the parser has already failed). */\n parseHeaderBlock(headerBytes) {\n const headerText = new TextDecoder().decode(headerBytes);\n const lines = headerText.replace(/\\r\\n/g, \"\\n\").split(\"\\n\").filter((line) => line.length > 0);\n const startLine = lines.shift() ?? \"\";\n if (!this.parseStartLine(startLine)) return false;\n for (const line of lines) {\n const separator = line.indexOf(\":\");\n if (separator <= 0) continue;\n this.headerPairs.push([line.slice(0, separator), line.slice(separator + 1).trimStart()]);\n }\n return true;\n }\n headerValue(name) {\n const target = name.toLowerCase();\n for (const [key, value] of this.headerPairs) {\n if (key.toLowerCase() === target) return value;\n }\n return null;\n }\n completeWithBody(body) {\n this.phase = \"done\";\n this.settled = { kind: \"message\", message: this.buildMessage(body) };\n }\n fail(message) {\n this.phase = \"done\";\n this.settled = { kind: \"failed\", message };\n }\n };\n var HttpResponseParser = class extends HttpMessageParser {\n constructor(requestMethod) {\n super();\n this.requestMethod = requestMethod;\n }\n requestMethod;\n status = 200;\n statusText = \"\";\n get messageLabel() {\n return \"response\";\n }\n parseStartLine(line) {\n const match = /^HTTP\\/\\d(?:\\.\\d)?\\s+(\\d{3})(?:\\s+(.*))?$/.exec(line || \"HTTP/1.1 200 OK\");\n this.status = match ? parseInt(match[1], 10) : 200;\n this.statusText = match?.[2] ?? \"\";\n return true;\n }\n bodyAllowed() {\n return responseCanHaveBody(this.requestMethod, this.status);\n }\n defaultFramingWhenUnframed() {\n return \"until-close\";\n }\n buildMessage(body) {\n return { status: this.status, statusText: this.statusText, headerPairs: this.headerPairs, body };\n }\n };\n var HttpRequestParser = class extends HttpMessageParser {\n method = \"GET\";\n target = \"/\";\n get messageLabel() {\n return \"request\";\n }\n parseStartLine(line) {\n const match = /^([A-Za-z]+)\\s+(\\S+)\\s+HTTP\\/\\d(?:\\.\\d)?$/.exec(line);\n if (!match) {\n this.fail(`malformed HTTP request line: ${JSON.stringify(line.slice(0, 120))}`);\n return false;\n }\n this.method = match[1].toUpperCase();\n this.target = match[2];\n return true;\n }\n bodyAllowed() {\n return true;\n }\n defaultFramingWhenUnframed() {\n return \"none\";\n }\n buildMessage(body) {\n return { method: this.method, target: this.target, headerPairs: this.headerPairs, body };\n }\n };\n function errorResponse(status, message) {\n return {\n status,\n statusText: \"\",\n headerPairs: [[\"content-type\", \"text/plain; charset=utf-8\"]],\n body: new TextEncoder().encode(`Nimbus virtual socket: ${message}`)\n };\n }\n function toResponse(settled) {\n const headers = new Headers();\n for (const [key, value] of settled.headerPairs) headers.append(key, value);\n return new Response(settled.body, {\n status: settled.status,\n statusText: settled.statusText,\n headers\n });\n }\n var VirtualConnection = class {\n constructor(id, requestMethod, requestBytes, limits) {\n this.id = id;\n this.inbound = new ByteChunkQueue(requestBytes.byteLength, \"request buffer\");\n this.inbound.enqueue(requestBytes);\n this.outbound = new ByteChunkQueue(limits.maxResponseBufferBytes, \"response buffer\");\n this.parser = new HttpResponseParser(requestMethod);\n }\n id;\n /** Request bytes the guest server reads; filled in one shot in stage 1. */\n inbound;\n /** Response bytes the guest server writes; drained into the parser. */\n outbound;\n parser;\n responseReady = new Deferred();\n settled = false;\n closed = false;\n read(maxBytes) {\n return Array.from(this.inbound.readUpTo(Math.max(1, maxBytes | 0)));\n }\n /** The whole request is buffered before the connection is accepted, so nothing is ever awaited. */\n readAsync(maxBytes) {\n return Promise.resolve(this.read(maxBytes));\n }\n readBytesAsync(maxBytes) {\n return Promise.resolve(this.inbound.readUpTo(Math.max(1, maxBytes | 0)));\n }\n /** Same reason: an empty read on an accepted connection is always genuine EOF. */\n atEof() {\n return true;\n }\n write(bytesLike) {\n const bytes = toBytes(bytesLike);\n if (bytes.byteLength === 0 || this.settled || this.closed) return bytes.byteLength;\n this.outbound.enqueue(bytes.slice());\n this.pumpParser();\n return bytes.byteLength;\n }\n /** EOF from the server side (or request teardown); completes until-close bodies. */\n close() {\n if (this.closed) return;\n this.closed = true;\n if (this.settled) return;\n this.parser.finish();\n this.pumpParser();\n }\n /** Abort propagation: settle the pending preview request with a terminal status. */\n abort(message, status) {\n this.closed = true;\n this.settle(errorResponse(status, message));\n }\n async response(timeoutMs) {\n const timer = setTimeout(() => {\n this.settle(errorResponse(504, \"timed out waiting for response\"));\n }, Math.max(1, timeoutMs));\n try {\n return toResponse(await this.responseReady.promise);\n } finally {\n clearTimeout(timer);\n }\n }\n pumpParser() {\n for (; ; ) {\n const chunk = this.outbound.readUpTo(Number.MAX_SAFE_INTEGER);\n if (chunk.byteLength === 0) break;\n this.parser.feed(chunk);\n }\n const outcome = this.parser.outcome;\n if (!outcome) return;\n if (outcome.kind === \"failed\") {\n this.settle(errorResponse(502, outcome.message));\n return;\n }\n const { status, statusText, headerPairs, body } = outcome.message;\n this.settle({\n status,\n statusText,\n // The framing is consumed here; the Worker Response re-frames the body itself.\n headerPairs: headerPairs.filter(([key]) => !/^transfer-encoding$/i.test(key)),\n body\n });\n }\n settle(response) {\n if (this.settled) return;\n this.settled = true;\n this.responseReady.resolve(response);\n }\n };\n var LoopbackClientConnection = class {\n constructor(id, port, route, limits) {\n this.id = id;\n this.port = port;\n this.route = route;\n this.limits = limits;\n }\n id;\n port;\n route;\n limits;\n parser = new HttpRequestParser();\n /** Response bytes the guest client reads. */\n inbound = new ByteChunkQueue(Number.MAX_SAFE_INTEGER, \"loopback response buffer\");\n readable = null;\n drained = null;\n dispatched = false;\n eof = false;\n closed = false;\n write(bytesLike) {\n const bytes = toBytes(bytesLike);\n if (bytes.byteLength === 0 || this.closed) return bytes.byteLength;\n if (this.dispatched) {\n throw new Error(\n `Nimbus loopback socket: port ${this.port} answered with \"Connection: close\"; open a new connection for the next request`\n );\n }\n this.parser.feed(bytes.slice());\n const outcome = this.parser.outcome;\n if (!outcome) return bytes.byteLength;\n this.dispatched = true;\n if (outcome.kind === \"failed\") this.failWith(400, outcome.message);\n else void this.dispatch(outcome.message);\n return bytes.byteLength;\n }\n read(maxBytes) {\n return Array.from(this.readBytes(maxBytes));\n }\n /** Byte-array read. The fd-backed path uses this so a stream never round-trips through number[]. */\n readBytes(maxBytes) {\n const out = this.inbound.readUpTo(Math.max(1, maxBytes | 0));\n if (this.drained && this.inbound.pendingBytes <= LOOPBACK_READ_HIGH_WATER_BYTES) {\n const waiter = this.drained;\n this.drained = null;\n waiter.resolve();\n }\n return out;\n }\n atEof() {\n return (this.eof || this.closed) && this.inbound.pendingBytes === 0;\n }\n /** Blocks the guest until response bytes arrive; an empty result is EOF. */\n async readAsync(maxBytes) {\n return Array.from(await this.readBytesAsync(maxBytes));\n }\n async readBytesAsync(maxBytes) {\n for (; ; ) {\n const chunk = this.readBytes(maxBytes);\n if (chunk.byteLength > 0) return chunk;\n if (this.eof || this.closed) return EMPTY_BYTES;\n if (!this.dispatched) {\n throw new Error(\n \"Nimbus loopback socket: read before a complete HTTP request was written (loopback sockets carry HTTP requests, not arbitrary byte streams)\"\n );\n }\n const waiter = this.readable ??= new Deferred();\n await waiter.promise;\n }\n }\n close() {\n if (this.closed) return;\n this.closed = true;\n this.wakeReader();\n if (this.drained) {\n const waiter = this.drained;\n this.drained = null;\n waiter.resolve();\n }\n }\n async dispatch(parsed) {\n let request;\n try {\n request = this.buildRequest(parsed);\n } catch (error) {\n this.failWith(400, describeError(error));\n return;\n }\n let response;\n try {\n response = await this.withTimeout(this.route(this.port, request));\n } catch (error) {\n this.failWith(502, describeError(error));\n return;\n }\n await this.streamResponse(request.method, response);\n }\n buildRequest(parsed) {\n const headers = new Headers();\n let authority = `127.0.0.1:${this.port}`;\n for (const [key, value] of parsed.headerPairs) {\n if (/^host$/i.test(key)) {\n authority = value;\n continue;\n }\n if (/^(transfer-encoding|connection|content-length|keep-alive)$/i.test(key)) continue;\n headers.append(key, value);\n }\n const target = parsed.target.startsWith(\"/\") ? parsed.target : `/${parsed.target}`;\n const url = /^https?:\\/\\//i.test(parsed.target) ? parsed.target : `http://${authority}${target}`;\n const body = parsed.body && parsed.body.byteLength > 0 ? parsed.body : null;\n const methodTakesBody = parsed.method !== \"GET\" && parsed.method !== \"HEAD\";\n return new Request(url, {\n method: parsed.method,\n headers,\n body: methodTakesBody ? body : null\n });\n }\n async streamResponse(method, response) {\n const withBody = responseCanHaveBody(method, response.status) && response.body !== null;\n const headers = new Headers(response.headers);\n for (const hopByHop of [\"transfer-encoding\", \"content-length\", \"connection\", \"keep-alive\"]) {\n headers.delete(hopByHop);\n }\n this.pushBytes(encodeHttpResponseHead(response.status, response.statusText, headers, withBody));\n if (!withBody || !response.body) {\n this.finishStream();\n return;\n }\n const reader = response.body.getReader();\n try {\n for (; ; ) {\n const { value, done } = await reader.read();\n if (done) break;\n if (this.closed) return;\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n if (bytes.byteLength === 0) continue;\n this.pushBytes(encodeChunkedFrame(bytes));\n await this.awaitDrain();\n }\n this.pushBytes(CHUNKED_TERMINATOR);\n } catch {\n } finally {\n try {\n await reader.cancel();\n } catch {\n }\n this.finishStream();\n }\n }\n /** Synthesize a complete response so the guest sees an HTTP error, never a hang. */\n failWith(status, detail) {\n const body = new TextEncoder().encode(`Nimbus loopback socket: ${detail}`);\n const headers = new Headers({\n \"content-type\": \"text/plain; charset=utf-8\",\n \"content-length\": String(body.byteLength)\n });\n this.pushBytes(encodeHttpResponseHead(status, \"\", headers, false));\n this.pushBytes(body);\n this.finishStream();\n }\n pushBytes(bytes) {\n if (this.closed) return;\n this.inbound.enqueue(bytes);\n this.wakeReader();\n }\n finishStream() {\n this.eof = true;\n this.wakeReader();\n }\n wakeReader() {\n const waiter = this.readable;\n if (!waiter) return;\n this.readable = null;\n waiter.resolve();\n }\n /** Pause the body pump while the guest is behind, so a big stream cannot grow unbounded. */\n awaitDrain() {\n if (this.inbound.pendingBytes <= LOOPBACK_READ_HIGH_WATER_BYTES) return Promise.resolve();\n return (this.drained ??= new Deferred()).promise;\n }\n withTimeout(promise) {\n let timer = null;\n const timeout = new Promise((_resolve, reject) => {\n timer = setTimeout(\n () => reject(new Error(`port ${this.port} did not respond within ${this.limits.responseTimeoutMs}ms`)),\n Math.max(1, this.limits.responseTimeoutMs)\n );\n });\n return Promise.race([promise, timeout]).finally(() => {\n if (timer !== null) clearTimeout(timer);\n });\n }\n };\n function describeError(error) {\n return error instanceof Error ? error.message : String(error);\n }\n function socketStreamFor(conn) {\n return {\n // Neither direction has a handshake: an accepted connection already holds\n // the request, and a dialed one starts its exchange on the first write.\n opened: Promise.resolve(),\n readable: new ReadableStream(\n {\n async pull(controller) {\n const bytes = await conn.readBytesAsync(LOOPBACK_STREAM_CHUNK_BYTES);\n if (bytes.byteLength === 0) controller.close();\n else controller.enqueue(bytes);\n },\n cancel() {\n conn.close();\n }\n },\n // highWaterMark 0 so the stream never pulls speculatively. A default\n // strategy pulls once at construction, which on a client connection\n // means reading before the guest has written its request.\n { highWaterMark: 0 }\n ),\n writable: new WritableStream({\n write(chunk) {\n conn.write(chunk);\n },\n abort() {\n conn.close();\n }\n }),\n close() {\n conn.close();\n return Promise.resolve();\n }\n };\n }\n var VirtualListener = class {\n constructor(port) {\n this.port = port;\n }\n port;\n queue = [];\n acceptWaiters = [];\n push(conn) {\n const waiter = this.acceptWaiters.shift();\n if (waiter) waiter.resolve(conn);\n else this.queue.push(conn);\n }\n accept() {\n const queued = this.queue.shift();\n if (queued) return Promise.resolve(queued);\n const waiter = new Deferred();\n this.acceptWaiters.push(waiter);\n return waiter.promise;\n }\n take() {\n return this.queue.shift() ?? null;\n }\n pending() {\n return this.queue.length;\n }\n drainQueued() {\n return this.queue.splice(0);\n }\n rejectPendingAccepts(error) {\n for (const waiter of this.acceptWaiters.splice(0)) waiter.reject(error);\n }\n };\n var VirtualSocketKernel = class {\n constructor(host, limits) {\n this.host = host;\n this.limits = { ...DEFAULT_LIMITS, ...limits };\n }\n host;\n /** Public: runner glue inspects listeners.keys() for the default preview port. */\n listeners = /* @__PURE__ */ new Map();\n connections = /* @__PURE__ */ new Map();\n limits;\n nextConnectionId = 1;\n nextEphemeralPort = 49152;\n listenWaiters = [];\n readableWaiters = /* @__PURE__ */ new Set();\n listen(port) {\n let n = Number(port);\n if (!Number.isInteger(n) || n < 0 || n >= 65536) throw new Error(`invalid port: ${port}`);\n if (n === 0) {\n while (this.listeners.has(this.nextEphemeralPort)) {\n this.nextEphemeralPort++;\n if (this.nextEphemeralPort >= 65535) this.nextEphemeralPort = 49152;\n }\n n = this.nextEphemeralPort++;\n if (this.nextEphemeralPort >= 65535) this.nextEphemeralPort = 49152;\n }\n if (!this.listeners.has(n)) {\n this.listeners.set(n, new VirtualListener(n));\n try {\n this.host.__nimbusVirtualSocketDidListen?.(n);\n } catch {\n }\n for (const waiter of this.listenWaiters.splice(0)) waiter.resolve(n);\n }\n return n;\n }\n closeListener(port) {\n const n = Number(port);\n const listener = this.listeners.get(n);\n if (!listener) return;\n this.listeners.delete(n);\n for (const conn of listener.drainQueued()) {\n this.connections.delete(conn.id);\n conn.abort(`listener closed on port ${n}`, 502);\n }\n listener.rejectPendingAccepts(new Error(`port is not listening: ${n}`));\n }\n async accept(port) {\n const listener = this.listeners.get(Number(port));\n if (!listener) throw new Error(`port is not listening: ${port}`);\n const conn = await listener.accept();\n return { id: conn.id, host: \"127.0.0.1\", port: 0 };\n }\n acceptNow(port) {\n const listener = this.listeners.get(Number(port));\n if (!listener) throw new Error(`port is not listening: ${port}`);\n const conn = listener.take();\n return conn ? { id: conn.id, host: \"127.0.0.1\", port: 0 } : null;\n }\n /**\n * Open a client connection to `port` on the session's loopback.\n *\n * The returned id is an ordinary connection id: send() writes the\n * request, recv()/recvAsync() read the response, close() releases it.\n * Only HTTP/1.1 crosses a loopback connection - see\n * LoopbackClientConnection for why - and each one carries a single\n * request/response exchange.\n */\n connect(port) {\n const conn = this.openLoopbackClient(port);\n this.connections.set(conn.id, conn);\n return conn.id;\n }\n /**\n * The same client connection as `connect`, handed back in Cloudflare's\n * `Socket` shape.\n *\n * Guests whose sockets are real WASI file descriptors (ruby.wasm, and any\n * future wasm32-wasi program) reach loopback through this: the WASI shim\n * stores it in exactly the fd slot a `cloudflare:sockets` connection would\n * occupy, so `fd_read` on an in-session port is the same suspending read as\n * `fd_read` on a remote host.\n */\n connectStream(port) {\n return socketStreamFor(this.openLoopbackClient(port));\n }\n /**\n * An already-accepted connection in the same `Socket` shape, so a server's\n * accepted socket is the same kind of file descriptor as a client's dialed\n * one. `accept`/`acceptNow` still hand out the connection id, because accept\n * itself stays on the cooperative pump - this only binds the result.\n */\n streamFor(id) {\n const conn = this.connections.get(Number(id));\n if (!conn) throw new Error(`connection is closed: ${id}`);\n return socketStreamFor(conn);\n }\n openLoopbackClient(port) {\n const n = Number(port);\n if (!Number.isInteger(n) || n <= 0 || n >= 65536) throw new Error(`invalid port: ${port}`);\n const route = this.host.__nimbusVirtualSocketRouteLoopback;\n if (typeof route !== \"function\") {\n throw new Error(\"Nimbus loopback sockets are unavailable in this runtime\");\n }\n return new LoopbackClientConnection(this.nextConnectionId++, n, route, this.limits);\n }\n /** Plain number array: Pyodide bytes() and the ruby.wasm base64 bridge both consume it. */\n recv(id, maxBytes) {\n const conn = this.connections.get(Number(id));\n if (!conn) return [];\n return conn.read(Number(maxBytes));\n }\n /**\n * Suspending read. A client connection has nothing to hand back until\n * the loopback response arrives, so guests that can suspend (Pyodide\n * `run_sync`, a JSPI-suspending WASI call) await this instead of\n * spinning on recv(). An empty result is EOF.\n */\n async recvAsync(id, maxBytes) {\n const conn = this.connections.get(Number(id));\n if (!conn) return [];\n return conn.readAsync(Number(maxBytes));\n }\n /**\n * Whether an empty recv() means end-of-response rather than \"not yet\".\n * Guests that poll instead of suspending (ruby.wasm, whose JS bridge is\n * synchronous) need the two apart to avoid truncating a response.\n */\n atEof(id) {\n const conn = this.connections.get(Number(id));\n return conn ? conn.atEof() : true;\n }\n send(id, bytesLike) {\n const conn = this.connections.get(Number(id));\n if (!conn) throw new Error(`connection is closed: ${id}`);\n return conn.write(bytesLike);\n }\n close(id) {\n const conn = this.connections.get(Number(id));\n if (!conn) return;\n conn.close();\n this.connections.delete(Number(id));\n }\n pending(port) {\n return this.listeners.get(Number(port))?.pending() ?? 0;\n }\n firstListeningPort() {\n for (const port of this.listeners.keys()) return port;\n return null;\n }\n /** select()-style readiness: resolves ports with queued connections, [] on timeout. */\n waitReadable(ports, timeoutSeconds) {\n const normalized = (Array.isArray(ports) ? ports : []).map((p) => Number(p)).filter((p) => Number.isInteger(p));\n const readyNow = normalized.filter((port) => this.pending(port) > 0);\n if (readyNow.length > 0) return Promise.resolve(readyNow);\n const timeoutMs = timeoutSeconds == null ? this.limits.responseTimeoutMs : Math.max(0, Number(timeoutSeconds) * 1e3);\n const deferred = new Deferred();\n const waiter = {\n ports: normalized,\n deferred,\n timer: setTimeout(() => {\n this.readableWaiters.delete(waiter);\n deferred.resolve([]);\n }, timeoutMs)\n };\n this.readableWaiters.add(waiter);\n return deferred.promise;\n }\n async waitForListen(timeoutMs) {\n const existing = this.firstListeningPort();\n if (existing) return existing;\n const waiter = new Deferred();\n this.listenWaiters.push(waiter);\n let timer = null;\n const timeout = new Promise((resolve) => {\n timer = setTimeout(() => resolve(null), Math.max(1, timeoutMs ?? 5e3));\n });\n try {\n return await Promise.race([waiter.promise, timeout]);\n } finally {\n if (timer !== null) clearTimeout(timer);\n }\n }\n async handleHttpRequest(port, request) {\n const n = Number(port);\n let listener = this.listeners.get(n);\n if (!listener) {\n try {\n await this.host.__nimbusVirtualSocketEnsureListener?.(n);\n } catch {\n }\n listener = this.listeners.get(n);\n }\n if (!listener) {\n return new Response(`Nimbus virtual socket: no listener on port ${port}`, { status: 502 });\n }\n const body = request.method === \"GET\" || request.method === \"HEAD\" ? EMPTY_BYTES : new Uint8Array(await request.arrayBuffer());\n if (body.byteLength > this.limits.maxRequestBodyBytes) {\n return new Response(\n `Nimbus virtual socket: request body exceeds ${this.limits.maxRequestBodyBytes} bytes`,\n { status: 413 }\n );\n }\n const id = this.nextConnectionId++;\n const conn = new VirtualConnection(id, request.method, encodeHttpRequest(request, body), this.limits);\n this.connections.set(id, conn);\n listener.push(conn);\n this.notifyReadable(n);\n const signal = request.signal;\n const onAbort = () => conn.abort(\"client aborted the request\", 499);\n if (signal?.aborted) onAbort();\n else signal?.addEventListener(\"abort\", onAbort);\n try {\n try {\n const accepted = await this.host.__nimbusVirtualSocketRequestQueued?.(n);\n if (accepted === false) {\n const detail = typeof this.host.__nimbusVirtualSocketLastError === \"string\" ? this.host.__nimbusVirtualSocketLastError.trim() : \"\";\n const suffix = detail ? `: ${detail}` : \"\";\n return new Response(\n `Nimbus virtual socket: runtime handler did not accept the request${suffix}`,\n { status: 502 }\n );\n }\n } catch {\n }\n return await conn.response(this.limits.responseTimeoutMs);\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n conn.close();\n this.connections.delete(id);\n }\n }\n notifyReadable(port) {\n for (const waiter of Array.from(this.readableWaiters)) {\n if (!waiter.ports.includes(port)) continue;\n const ready = waiter.ports.filter((p) => this.pending(p) > 0);\n if (ready.length === 0) continue;\n clearTimeout(waiter.timer);\n this.readableWaiters.delete(waiter);\n waiter.deferred.resolve(ready);\n }\n }\n };\n function installVirtualSocketKernel(scope = globalThis) {\n if (!scope.__nimbusVirtualSockets) {\n scope.__nimbusVirtualSockets = new VirtualSocketKernel(scope);\n }\n return scope.__nimbusVirtualSockets;\n }\n\n // <stdin>\n installVirtualSocketKernel();\n})();\n";
|