@nimbus-sh/core 0.3.0 → 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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/_shared/error-text.d.ts +15 -0
  4. package/dist/_shared/error-text.d.ts.map +1 -0
  5. package/dist/_shared/error-text.js +21 -0
  6. package/dist/_shared/exports-resolver.d.ts +19 -7
  7. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  8. package/dist/_shared/exports-resolver.js +1 -1
  9. package/dist/_shared/retry.d.ts.map +1 -1
  10. package/dist/_shared/retry.js +3 -1
  11. package/dist/_shared/tarball-stream.d.ts +93 -0
  12. package/dist/_shared/tarball-stream.d.ts.map +1 -0
  13. package/dist/_shared/tarball-stream.js +235 -0
  14. package/dist/_shared/tarball.d.ts +17 -0
  15. package/dist/_shared/tarball.d.ts.map +1 -0
  16. package/dist/_shared/tarball.js +39 -0
  17. package/dist/_shared/w7-frame.js +2 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/constants.js +30 -16
  20. package/dist/observability/oom-classify.d.ts +1 -1
  21. package/dist/observability/oom-classify.d.ts.map +1 -1
  22. package/dist/observability/oom-classify.js +7 -0
  23. package/dist/observability/oom-discriminator.d.ts +1 -1
  24. package/dist/runtime/clang-runner.d.ts +38 -0
  25. package/dist/runtime/clang-runner.d.ts.map +1 -0
  26. package/dist/runtime/clang-runner.js +866 -0
  27. package/dist/runtime/esbuild-service.d.ts +4 -3
  28. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  29. package/dist/runtime/esbuild-service.js +9 -4
  30. package/dist/runtime/facet-host.d.ts +12 -0
  31. package/dist/runtime/facet-host.d.ts.map +1 -1
  32. package/dist/runtime/installed-runtimes.d.ts +4 -3
  33. package/dist/runtime/installed-runtimes.d.ts.map +1 -1
  34. package/dist/runtime/local-facet-host.d.ts.map +1 -1
  35. package/dist/runtime/local-facet-host.js +29 -9
  36. package/dist/runtime/long-running-handle.d.ts +3 -3
  37. package/dist/runtime/long-running-handle.d.ts.map +1 -1
  38. package/dist/runtime/os-contracts.d.ts +7 -5
  39. package/dist/runtime/os-contracts.d.ts.map +1 -1
  40. package/dist/runtime/port-registry.d.ts +28 -0
  41. package/dist/runtime/port-registry.d.ts.map +1 -1
  42. package/dist/runtime/port-registry.js +79 -9
  43. package/dist/runtime/process-logs.d.ts +9 -1
  44. package/dist/runtime/process-logs.d.ts.map +1 -1
  45. package/dist/runtime/project-detect.d.ts +6 -1
  46. package/dist/runtime/project-detect.d.ts.map +1 -1
  47. package/dist/runtime/require-resolver.d.ts.map +1 -1
  48. package/dist/runtime/require-resolver.js +3 -4
  49. package/dist/runtime/ruby-gems.d.ts +30 -0
  50. package/dist/runtime/ruby-gems.d.ts.map +1 -0
  51. package/dist/runtime/ruby-gems.js +636 -0
  52. package/dist/runtime/ruby-runner.d.ts +127 -0
  53. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  54. package/dist/runtime/ruby-runner.js +1357 -0
  55. package/dist/runtime/runtime-registry.d.ts +4 -3
  56. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  57. package/dist/runtime/runtime-registry.js +2 -1
  58. package/dist/runtime/session-process-supervisor.d.ts +15 -0
  59. package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
  60. package/dist/runtime/session-process-supervisor.js +30 -0
  61. package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
  62. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
  63. package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
  64. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  65. package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
  66. package/dist/runtime/virtual-socket-kernel.js +10 -5
  67. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  68. package/dist/runtime/wasm-runner.js +22 -14
  69. package/dist/shell/features.d.ts.map +1 -1
  70. package/dist/shell/features.js +5 -4
  71. package/dist/shell/unix-commands.d.ts +14 -1
  72. package/dist/shell/unix-commands.d.ts.map +1 -1
  73. package/dist/shell/unix-commands.js +115 -69
  74. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
  75. package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
  76. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
  77. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
  78. package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
  79. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
  80. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
  81. package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
  82. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
  83. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
  84. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
  85. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
  86. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
  87. package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
  88. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
  89. package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
  90. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
  91. package/dist/substrate/lifo/node-compat/index.js +3 -2
  92. package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
  93. package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
  94. package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
  95. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
  96. package/dist/substrate/lifo/node-compat/process.js +6 -3
  97. package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
  98. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
  99. package/dist/substrate/lifo/shell/Shell.js +86 -75
  100. package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
  101. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
  102. package/dist/substrate/lifo/shell/lexer.js +40 -0
  103. package/dist/vfs/seed-project.d.ts.map +1 -1
  104. package/dist/vfs/seed-project.js +8 -2
  105. package/dist/vfs/sqlite-vfs.d.ts +61 -1
  106. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  107. package/dist/vfs/sqlite-vfs.js +199 -35
  108. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  109. package/dist/workspace/nimbus-workspace.js +9 -2
  110. package/package.json +4 -2
  111. package/src/_shared/error-text.ts +21 -0
  112. package/src/_shared/exports-resolver.ts +24 -4
  113. package/src/_shared/retry.ts +4 -2
  114. package/src/_shared/tarball-stream.ts +263 -0
  115. package/src/_shared/tarball.ts +46 -0
  116. package/src/_shared/w7-frame.ts +2 -1
  117. package/src/constants.ts +30 -16
  118. package/src/observability/oom-classify.ts +7 -0
  119. package/src/observability/oom-discriminator.ts +1 -1
  120. package/src/runtime/clang-runner.ts +924 -0
  121. package/src/runtime/esbuild-service.ts +16 -11
  122. package/src/runtime/facet-host.ts +12 -0
  123. package/src/runtime/installed-runtimes.ts +4 -3
  124. package/src/runtime/local-facet-host.ts +28 -8
  125. package/src/runtime/long-running-handle.ts +3 -3
  126. package/src/runtime/os-contracts.ts +7 -5
  127. package/src/runtime/port-registry.ts +97 -9
  128. package/src/runtime/process-logs.ts +12 -3
  129. package/src/runtime/project-detect.ts +7 -1
  130. package/src/runtime/require-resolver.ts +7 -7
  131. package/src/runtime/ruby-gems.ts +682 -0
  132. package/src/runtime/ruby-runner.ts +1484 -0
  133. package/src/runtime/runtime-registry.ts +9 -7
  134. package/src/runtime/session-process-supervisor.ts +27 -0
  135. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  136. package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
  137. package/src/runtime/virtual-socket-kernel.ts +10 -5
  138. package/src/runtime/wasm-runner.ts +55 -29
  139. package/src/shell/features.ts +5 -4
  140. package/src/shell/unix-commands.ts +273 -128
  141. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  142. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  143. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  144. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  145. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  146. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  147. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  148. package/src/substrate/lifo/node-compat/index.ts +3 -2
  149. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  150. package/src/substrate/lifo/node-compat/process.ts +5 -2
  151. package/src/substrate/lifo/shell/Shell.ts +92 -70
  152. package/src/substrate/lifo/shell/lexer.ts +38 -0
  153. package/src/vfs/seed-project.ts +10 -3
  154. package/src/vfs/sqlite-vfs.ts +209 -36
  155. package/src/workspace/nimbus-workspace.ts +10 -1
@@ -43,6 +43,8 @@ import { CRED_KERNEL, type VfsCred } from './os-contracts.js';
43
43
  import type { EsbuildService } from './esbuild-service.js';
44
44
  import { parseFacetBundleProfile, type FacetBundleProfile } from './bundle-profile.js';
45
45
  import { bindImportMetaResolve, importMetaDefines } from './import-meta-transform.js';
46
+ import type { Command, CommandContext } from '../substrate/lifo/commands/types.js';
47
+ import { errorText } from '../_shared/error-text.js';
46
48
 
47
49
  /**
48
50
  * Result shape that runtime-registry expects from a runner. Mirrors
@@ -130,7 +132,7 @@ export function resolveRuntimeScriptPath(
130
132
  * a second one.
131
133
  */
132
134
  export type RuntimeSubcommand = (
133
- ctx: any,
135
+ ctx: CommandContext,
134
136
  registry: ShellRegistry,
135
137
  runAsRuntime: (args: string[]) => Promise<number>,
136
138
  ) => Promise<number>;
@@ -181,7 +183,7 @@ export interface RuntimeSpec {
181
183
  * shell registry type tree when the runtime path only needs resolve().
182
184
  */
183
185
  export interface ShellRegistry {
184
- resolve(name: string): Promise<any> | any;
186
+ resolve(name: string): Promise<Command | null | undefined> | Command | null | undefined;
185
187
  }
186
188
 
187
189
  /**
@@ -201,7 +203,7 @@ export function buildRuntimeHandler(
201
203
  getEsbuild(): EsbuildService | Promise<EsbuildService>;
202
204
  registry: ShellRegistry;
203
205
  },
204
- ): (ctx: any) => Promise<number> {
206
+ ): Command {
205
207
  const { vfs, getEsbuild, registry } = ctx0;
206
208
  const fs = vfs.as(CRED_KERNEL);
207
209
 
@@ -211,7 +213,7 @@ export function buildRuntimeHandler(
211
213
  * caller has already consumed them — so a verb handler can delegate
212
214
  * back in with a rewritten argv without re-triggering itself.
213
215
  */
214
- async function runtimeInvocation(ctx: any, args: string[]): Promise<number> {
216
+ async function runtimeInvocation(ctx: CommandContext, args: string[]): Promise<number> {
215
217
  const name = spec.name;
216
218
  const nimbusCtx = ctx as {
217
219
  __nimbusCaptureOutput?: unknown;
@@ -449,8 +451,8 @@ export function buildRuntimeHandler(
449
451
  define: importMetaDefines(absUrl),
450
452
  });
451
453
  code = bindImportMetaResolve(transformed.code, absUrl);
452
- } catch (e: any) {
453
- ctx.stderr.write(`${name}: transform error for ${scriptPath}: ${e?.message}\n`);
454
+ } catch (e) {
455
+ ctx.stderr.write(`${name}: transform error for ${scriptPath}: ${errorText(e)}\n`);
454
456
  return 1;
455
457
  }
456
458
  }
@@ -488,7 +490,7 @@ export function buildRuntimeHandler(
488
490
  return result.exitCode;
489
491
  }
490
492
 
491
- return async function runtimeHandler(ctx: any): Promise<number> {
493
+ return async function runtimeHandler(ctx: CommandContext): Promise<number> {
492
494
  const args: string[] = ctx.args || [];
493
495
 
494
496
  // ── Subcommand dispatch ──
@@ -67,6 +67,8 @@ export class SessionProcessSupervisor {
67
67
  private terminators = new Map<number, () => void>();
68
68
  /** Fires after every appendOutput/markExit once log persistence is wired. */
69
69
  private logActivity: (() => void) | null = null;
70
+ /** Fires once per pid on its first terminal transition; see setOnTerminal. */
71
+ private onTerminalCb: ((pid: number) => void) | null = null;
70
72
 
71
73
  // ── Lifecycle / PID authority ─────────────────────────────────────────
72
74
 
@@ -130,10 +132,33 @@ export class SessionProcessSupervisor {
130
132
  return this.table.setUmask(pid, umask);
131
133
  }
132
134
 
135
+ /**
136
+ * Observe every pid's FIRST transition out of `running`, whichever door it
137
+ * leaves by — exit(), kill(), a facet's self-reported exit, a timeout abort:
138
+ * all of them end here, which is what makes this one callback a complete
139
+ * seam for per-pid durable state (the resident-launch journal) that must be
140
+ * released exactly when the process ends and never before.
141
+ *
142
+ * One slot, owned by the FacetManager. A second subscriber would mean two
143
+ * owners of process-end policy; grow this into a list only when a second
144
+ * genuine owner exists.
145
+ */
146
+ setOnTerminal(cb: (pid: number) => void): void {
147
+ this.onTerminalCb = cb;
148
+ }
149
+
150
+ private fireTerminal(pid: number, wasRunning: boolean): void {
151
+ if (!wasRunning || !this.onTerminalCb) return;
152
+ if (this.table.get(pid)?.state === 'running') return;
153
+ try { this.onTerminalCb(pid); } catch { /* the process is gone regardless */ }
154
+ }
155
+
133
156
  /** Mark a process as exited. First terminal state wins. */
134
157
  exit(pid: number, exitCode: number): void {
158
+ const wasRunning = this.table.get(pid)?.state === 'running';
135
159
  this.table.exit(pid, exitCode);
136
160
  this.terminators.delete(pid);
161
+ this.fireTerminal(pid, wasRunning);
137
162
  }
138
163
 
139
164
  /**
@@ -141,9 +166,11 @@ export class SessionProcessSupervisor {
141
166
  * stdin can't outlive the process.
142
167
  */
143
168
  kill(pid: number): boolean {
169
+ const wasRunning = this.table.get(pid)?.state === 'running';
144
170
  const killed = this.table.kill(pid);
145
171
  this.terminate(pid);
146
172
  this.input.close(pid);
173
+ this.fireTerminal(pid, wasRunning);
147
174
  return killed;
148
175
  }
149
176
 
@@ -455,12 +455,15 @@ function normalizeOpenFlags(flags: RuntimeOpenFlags): RuntimeFileHandle['flags']
455
455
  };
456
456
  }
457
457
 
458
- function fsError(code: string, syscall: string, path: string): Error {
459
- const err: any = new Error(`${code}: ${syscall} '${path}'`);
460
- err.code = code;
461
- err.syscall = syscall;
462
- err.path = path;
463
- return err;
458
+ /** An error carrying the fields Node's `fs` puts on a failed syscall. */
459
+ interface FsError extends Error {
460
+ code: string;
461
+ syscall: string;
462
+ path: string;
463
+ }
464
+
465
+ function fsError(code: string, syscall: string, path: string): FsError {
466
+ return Object.assign(new Error(`${code}: ${syscall} '${path}'`), { code, syscall, path });
464
467
  }
465
468
 
466
469
  function hasErrorCode(error: unknown, code: string): boolean {
@@ -8,7 +8,7 @@
8
8
  * Consumed by python-runner.ts and ruby-runner.ts: spliced into the
9
9
  * socket process worker module source passed to NimbusLoaderPool.
10
10
  *
11
- * Size: 33.72 KiB
11
+ * Size: 33.76 KiB
12
12
  */
13
13
 
14
- export const VIRTUAL_SOCKET_KERNEL_SRC: string = "(() => {\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";
14
+ export const VIRTUAL_SOCKET_KERNEL_SRC: string = "(() => {\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) => {\n lines.push(`${key}: ${value}`);\n });\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) => {\n lines.push(`${key}: ${value}`);\n });\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 this.readable ??= new Deferred();\n await this.readable.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 this.drained ??= new Deferred();\n return this.drained.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";
@@ -191,7 +191,9 @@ function encodeHttpRequest(request: Request, body: Uint8Array): Uint8Array {
191
191
  headers.set('Content-Length', String(body.byteLength));
192
192
  }
193
193
  const lines = [`${request.method} ${path} HTTP/1.1`];
194
- headers.forEach((value, key) => lines.push(`${key}: ${value}`));
194
+ headers.forEach((value, key) => {
195
+ lines.push(`${key}: ${value}`);
196
+ });
195
197
  lines.push('', '');
196
198
  return concatBytes([new TextEncoder().encode(lines.join('\r\n')), body]);
197
199
  }
@@ -213,7 +215,9 @@ function encodeHttpResponseHead(
213
215
  chunkedBody: boolean,
214
216
  ): Uint8Array {
215
217
  const lines = [`HTTP/1.1 ${status} ${statusText}`];
216
- headers.forEach((value, key) => lines.push(`${key}: ${value}`));
218
+ headers.forEach((value, key) => {
219
+ lines.push(`${key}: ${value}`);
220
+ });
217
221
  if (chunkedBody) lines.push('Transfer-Encoding: chunked');
218
222
  lines.push('Connection: close');
219
223
  lines.push('', '');
@@ -839,8 +843,8 @@ class LoopbackClientConnection implements VirtualSocketConnection {
839
843
  '(loopback sockets carry HTTP requests, not arbitrary byte streams)',
840
844
  );
841
845
  }
842
- const waiter = (this.readable ??= new Deferred<void>());
843
- await waiter.promise;
846
+ this.readable ??= new Deferred<void>();
847
+ await this.readable.promise;
844
848
  }
845
849
  }
846
850
 
@@ -963,7 +967,8 @@ class LoopbackClientConnection implements VirtualSocketConnection {
963
967
  /** Pause the body pump while the guest is behind, so a big stream cannot grow unbounded. */
964
968
  private awaitDrain(): Promise<void> {
965
969
  if (this.inbound.pendingBytes <= LOOPBACK_READ_HIGH_WATER_BYTES) return Promise.resolve();
966
- return (this.drained ??= new Deferred<void>()).promise;
970
+ this.drained ??= new Deferred<void>();
971
+ return this.drained.promise;
967
972
  }
968
973
 
969
974
  private withTimeout(promise: Promise<Response>): Promise<Response> {
@@ -57,6 +57,7 @@ import type { WasiInitOptions, WasiInstanceBundle, WasiMakeImportsOptions } from
57
57
  import type { WasiAbi } from './wasi-instance.js';
58
58
  import { inspectWasmThreads, wasiThreadsLoadError } from './wasi-threads.js';
59
59
  import { withMemoryLimit, DEFAULT_WASM_PROCESS_LIMIT_BYTES } from './wasm-memory.js';
60
+ import { errorText } from '../_shared/error-text.js';
60
61
 
61
62
  // ── facet-side globals injected by the WASI preamble ─────────────────
62
63
  // The preamble (WASI_INSTANCE_PREAMBLE_SRC) runs at facet module-init
@@ -365,6 +366,22 @@ export function makeWasmRunner(deps: {
365
366
  dirsDeleted: string[];
366
367
  };
367
368
  };
369
+ /**
370
+ * What `WebAssembly.instantiate` resolves to: the instance for a compiled
371
+ * module, `{ instance, module }` for bytes. The engine's own types describe
372
+ * only the first, which is what the facet host's table holds — the second
373
+ * is named because the checks below are what keep a table filled with
374
+ * anything else from yielding an instance-less object.
375
+ */
376
+ type WasmInstantiateResult =
377
+ | WebAssembly.Instance
378
+ | { instance: WebAssembly.Instance; module: WebAssembly.Module };
379
+ /**
380
+ * A direct-mode export: integer args in, one scalar — or nothing — out.
381
+ * `WebAssembly.Exports` types every export as a bare `Function`, which
382
+ * carries no signature of its own.
383
+ */
384
+ type WasmDirectExport = (...args: number[]) => number | bigint | undefined;
368
385
  const facetFn = async function wasmFacetCall(
369
386
  args: {
370
387
  mode: 'direct' | 'wasi';
@@ -405,7 +422,14 @@ export function makeWasmRunner(deps: {
405
422
  },
406
423
  facetEnv?: { SUPERVISOR?: unknown },
407
424
  ): Promise<WasmCallResult> {
408
- const wasmTable = (globalThis as any).__NIMBUS_WASM || {};
425
+ // This body is serialized into the facet isolate, where the supervisor's
426
+ // module graph — and so _shared/error-text.js — does not exist. Same
427
+ // fallback as errorText, spelled out locally.
428
+ const errText = (err: unknown): string =>
429
+ typeof err === 'object' && err !== null && 'message' in err && err.message
430
+ ? String(err.message)
431
+ : String(err);
432
+ const wasmTable = globalThis.__NIMBUS_WASM || {};
409
433
  const mod = wasmTable['user.wasm'];
410
434
  if (!mod) {
411
435
  return {
@@ -516,7 +540,7 @@ export function makeWasmRunner(deps: {
516
540
  maximum: args.threads.memory.maximum,
517
541
  shared: true,
518
542
  });
519
- } catch (e: any) {
543
+ } catch (e) {
520
544
  // A shared memory reserves its MAXIMUM up front, so an over-large
521
545
  // --max-memory fails here rather than when the program grows into
522
546
  // it. Say which number did it; the alternative message is a bare
@@ -527,7 +551,7 @@ export function makeWasmRunner(deps: {
527
551
  error:
528
552
  `wasi-threads: could not reserve the shared memory the module declares `
529
553
  + `(${args.threads.memory.initial}–${args.threads.memory.maximum} pages, `
530
- + `${(args.threads.memory.maximum * 64) / 1024} MiB): ${e?.message || e}. `
554
+ + `${(args.threads.memory.maximum * 64) / 1024} MiB): ${errText(e)}. `
531
555
  + 'A shared memory reserves its maximum immediately — lower --max-memory.',
532
556
  };
533
557
  }
@@ -538,22 +562,25 @@ export function makeWasmRunner(deps: {
538
562
  };
539
563
  sched = __wasiThreadsCreate({
540
564
  memory: shared,
541
- startThread: __wasiThreadsStarter(mod as WebAssembly.Module, importObject),
565
+ startThread: __wasiThreadsStarter(mod, importObject),
542
566
  });
543
567
  Object.assign(importObject, sched.hostImports());
544
568
  }
545
569
  let inst: WebAssembly.Instance;
546
570
  try {
547
- const result: any = await WebAssembly.instantiate(mod as any, importObject);
571
+ const result = await WebAssembly.instantiate(mod, importObject) as WasmInstantiateResult;
548
572
  inst = (result instanceof WebAssembly.Instance ? result : result.instance);
549
- } catch (e: any) {
573
+ } catch (e) {
550
574
  return {
551
575
  ok: false,
552
576
  mode: 'wasi',
553
- error: `instantiate failed: ${e?.message || e}`,
577
+ error: `instantiate failed: ${errText(e)}`,
554
578
  };
555
579
  }
556
- if (!memRef.mem) memRef.mem = (inst.exports as any).memory as WebAssembly.Memory;
580
+ if (!memRef.mem) {
581
+ const exported = inst.exports.memory;
582
+ if (exported instanceof WebAssembly.Memory) memRef.mem = exported;
583
+ }
557
584
  if (!memRef.mem) {
558
585
  return {
559
586
  ok: false,
@@ -593,17 +620,17 @@ export function makeWasmRunner(deps: {
593
620
  // Single-arg instantiate against a precompiled Module — this
594
621
  // is the form workerd's CSP DOES allow. The dynamic-bytes
595
622
  // form (instantiate(ArrayBuffer)) is what's blocked.
596
- const result: any = await WebAssembly.instantiate(mod as any, {});
623
+ const result = await WebAssembly.instantiate(mod, {}) as WasmInstantiateResult;
597
624
  inst = (result instanceof WebAssembly.Instance ? result : result.instance);
598
- } catch (e: any) {
625
+ } catch (e) {
599
626
  return {
600
627
  ok: false,
601
628
  mode: 'direct',
602
- error: `instantiate failed: ${e?.message || e}`,
629
+ error: `instantiate failed: ${errText(e)}`,
603
630
  };
604
631
  }
605
632
  const exportNames = Object.keys(inst.exports);
606
- const fn = (inst.exports as any)[args.exportName!];
633
+ const fn = inst.exports[args.exportName!];
607
634
  if (typeof fn !== 'function') {
608
635
  return {
609
636
  ok: false,
@@ -614,16 +641,16 @@ export function makeWasmRunner(deps: {
614
641
  `Available exports: ${exportNames.join(', ')}`,
615
642
  };
616
643
  }
617
- let out: any;
644
+ let out: number | bigint | undefined;
618
645
  try {
619
- out = fn(...(args.intArgs || []));
620
- } catch (e: any) {
646
+ out = (fn as WasmDirectExport)(...(args.intArgs || []));
647
+ } catch (e) {
621
648
  return {
622
649
  ok: false,
623
650
  mode: 'direct',
624
651
  exports: exportNames,
625
652
  error:
626
- `${args.exportName}(${(args.intArgs||[]).join(', ')}) threw: ${e?.message || e}`,
653
+ `${args.exportName}(${(args.intArgs||[]).join(', ')}) threw: ${errText(e)}`,
627
654
  };
628
655
  }
629
656
  // BigInt (i64) → string; everything else → as-is.
@@ -688,10 +715,11 @@ export function makeWasmRunner(deps: {
688
715
  wasiFsFiles = seed.files;
689
716
  }
690
717
 
691
- type DispatchOutcome =
692
- | { ok: true; mode: 'direct'; result?: number | string; exports?: string[] }
693
- | { ok: true; mode: 'wasi'; stdout?: string; stderr?: string; exitCode?: number; exports?: string[]; error?: string }
694
- | { ok: false; mode?: 'direct' | 'wasi'; error: string };
718
+ /**
719
+ * What the facet call resolved to, or the supervisor-side dispatch failure
720
+ * that never reached it and so names no mode.
721
+ */
722
+ type DispatchOutcome = WasmCallResult | { ok: false; mode?: undefined; error: string };
695
723
 
696
724
  let outcome: DispatchOutcome;
697
725
  let facet: Facet | null = null;
@@ -737,8 +765,8 @@ export function makeWasmRunner(deps: {
737
765
  timeoutMs: 30_000,
738
766
  },
739
767
  )) as DispatchOutcome;
740
- } catch (e: any) {
741
- outcome = { ok: false, error: `dispatch failed: ${e?.message || e}` };
768
+ } catch (e) {
769
+ outcome = { ok: false, error: `dispatch failed: ${errorText(e)}` };
742
770
  } finally {
743
771
  facet?.dispose();
744
772
  }
@@ -757,15 +785,13 @@ export function makeWasmRunner(deps: {
757
785
  // If runStart reported an `error` (wasm trapped, _start missing,
758
786
  // …), append it to stderr but still surface its exitCode (default
759
787
  // 1 from runStart on trap) so callers can distinguish.
760
- const wasiOut = outcome as Extract<DispatchOutcome, { mode: 'wasi' }> | (DispatchOutcome & { mode: 'wasi' });
761
- // Either branch carries optional stdout/stderr/exitCode/error.
762
- stdout = (wasiOut as any).stdout || '';
763
- stderr = (wasiOut as any).stderr || '';
764
- if ((wasiOut as any).error) {
788
+ stdout = outcome.stdout || '';
789
+ stderr = outcome.stderr || '';
790
+ if (outcome.error) {
765
791
  stderr = (stderr ? stderr : '') +
766
- `wasm-runner: wasi trap: ${(wasiOut as any).error}\n`;
792
+ `wasm-runner: wasi trap: ${outcome.error}\n`;
767
793
  }
768
- exitCode = (wasiOut as any).exitCode ?? ((wasiOut as any).ok ? 0 : 1);
794
+ exitCode = outcome.exitCode ?? (outcome.ok ? 0 : 1);
769
795
  } else if (!outcome.ok) {
770
796
  // Direct-mode failure or pre-instantiate dispatch failure — shell
771
797
  // sees rc=1 + stderr.
@@ -158,7 +158,7 @@ export class HeredocHandler {
158
158
  return;
159
159
  }
160
160
 
161
- return this._handleOriginalInput(data);
161
+ this._handleOriginalInput(data);
162
162
  };
163
163
 
164
164
  this.shell.executeLine = async (line: string): Promise<void> => {
@@ -185,7 +185,7 @@ export class HeredocHandler {
185
185
  this._accumulateLine('');
186
186
  return;
187
187
  }
188
- return this._printPrompt();
188
+ this._printPrompt();
189
189
  };
190
190
  }
191
191
 
@@ -414,7 +414,8 @@ export class LineEditorExtender {
414
414
  // shell-prompt input. Process-stdin input has its own model
415
415
  // (terminalStdin / stdinLineBuffer) handled by the shell.
416
416
  if (this.shell.running) {
417
- return this.originalHandleInput!(data);
417
+ this.originalHandleInput!(data);
418
+ return;
418
419
  }
419
420
 
420
421
  // Reverse-i-search sub-mode owns ALL input until exited.
@@ -432,7 +433,7 @@ export class LineEditorExtender {
432
433
  // Try each binding. First-match wins; on no match fall through.
433
434
  if (this._handleBinding(data)) return;
434
435
 
435
- return this.originalHandleInput!(data);
436
+ this.originalHandleInput!(data);
436
437
  };
437
438
  }
438
439