wasi-shims: Node backend for the à la carte sockets fragment - #124
Merged
Merged
Conversation
…ode:net) The provider classes were already platform-agnostic — they drive two structural connection shapes (DatagramConn, TcpConn). Those seams move to sockets_platform.ts, which now carries two backends and per-call detection: Deno-native APIs whenever a Deno global exists (unchanged contract — Deno.listenDatagram still wants --unstable-net, and we do NOT route around Deno's own capability gate via its node-compat layer), node builtins otherwise (process.getBuiltinModule — synchronous, and no static node: imports, so the module graph stays bundler- and browser-safe; real Node, and Bun through its compat, findings-only as everywhere). Node adapter facts, each verified empirically on pinned node 26.7.0, system node 24, and Deno's node-compat before the design committed: - dgram bind is made SYNCHRONOUS by a custom lookup whose callback fires synchronously (addresses here are always numeric): address() is valid on return and EADDRINUSE throws at the bind call site — exactly what the sync WIT bind + get-local-address funcs need (the iroh exam's create/bind/get-local-address driving order happens inside one guest activation, no event-loop turn between the calls). With the default async lookup, address() right after bind() throws EBADF. - dgram receive is push-shaped; the adapter bridges to the seam's pull shape with a BOUNDED queue (tail-drop past MAX_QUEUED_DATAGRAMS=256 — kernel-buffer semantics; a guest that stops reading must not grow host memory without bound). Zero-length datagrams (the iroh pump's self-wake) pass through; EMSGSIZE arrives as a coded error. - net.connect gets allowHalfOpen: true — Node's default auto-ends the write side on peer FIN, which would break the WIT shared-ownership / half-close contract. Reads pull via 'readable' + read()/unshift (one copy into the caller's buffer; Deno keeps its zero-extra-copy path); a persistent 'error' listener keeps socket errors off the process and routes them to in-flight operations. - error mapping: mapPlatformError gains a Node err.code table (a sharper channel than Deno's classes), tried after the Deno class checks and before the plain-message spellings; the adapters' synthetic closed-under-a-pending-op errors (ERR_SOCKET_DGRAM_NOT_RUNNING, ERR_STREAM_DESTROYED) map to invalid-state, mirroring BadResource. tcp bind/listen stay absent on BOTH backends for provider parity, even though node:net could express them (localAddress binding, createServer); a consumer linking them reopens that on #4. Tests, two lanes: - fake-node in-suite (tests/sockets_node_test.ts, 10 tests): detection forced to the node adapters via an @internal seam (forceNodeBackendForTests) and run under Deno's node-compat. Hiding the Deno global instead does NOT work: the compat layer's own internals reference the global (udp_wrap throws 'Deno is not defined'), which is why the seam exists. - pinned-Node smoke (tests/node_smoke.ts; just test-sockets-node, new gate wired into gates + gha::core): the same load-bearing semantics on genuine node:dgram/node:net plus the real no-Deno-global detection path. deno bundle resolves the workspace imports into one ESM file; runs on the tools/shell pinned node (26.7.0). Also verified by hand on system node 24.18. Not shimmed: @deno/shim-deno was evaluated and rejected — its connect swallows socket errors into console.error (a refused dial never rejects), createConnection lacks allowHalfOpen, and reads go through the raw fd behind the Socket's back; UDP is absent entirely. The structural seam is smaller than a corrective wrapper would have been. Gates: test-wasi-shims (98), test-sockets-node (new), publish-check, test-bundle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The decision
@deltic/wasi-shims/socketsnow serves real Node (and Bun via its node compat, findings-only) alongside Deno. The provider classes were already platform-agnostic — they drive two structural connection shapes — so the change is a platform seam module (sockets_platform.ts: seam types + both backends + per-call detection) and two adapters overnode:dgram/node:net, resolved throughprocess.getBuiltinModule(synchronous; no staticnode:imports, so the graph stays bundler- and browser-safe).Detection: Deno-native APIs whenever a
Denoglobal exists — unchanged contract, and deliberately not routing around Deno's--unstable-netcapability gate via its own node-compat — node builtins otherwise.The empirical keystones (all verified on pinned node 26.7.0, system node 24, and Deno's node-compat before committing to the design)
lookupwhose callback fires synchronously:address()valid on return,EADDRINUSEthrown at the call site. This is what makes the sync WITbind+get-local-addressfuncs servable at all — the iroh exam'screate/bind/get-local-addressdriving order runs inside one guest activation with no event-loop turn. (Default async lookup:address()right afterbind()throws EBADF.)allowHalfOpen: trueonnet.connect— Node's default auto-ends the write side on peer FIN, which would break the WIT shared-ownership/half-close contract.err.codetable inmapPlatformError(after Deno's classes, before the plain-message spellings); the adapters' synthetic closed-under-a-pending-op errors map toinvalid-state, mirroringBadResource.tcp bind/listenstay absent on both backends for provider parity (node could express them; a consumer linking them reopens #4).Tests: two lanes
@internalseam, run under Deno's node-compat. Hiding theDenoglobal does NOT work — the compat layer's own internals reference it (udp_wrapthrowsReferenceError: Deno is not defined), which is the seam's documented reason for existing.just test-sockets-node, new gate ingates+gha::core): the same load-bearing semantics on genuinenode:dgram/node:net, plus the real no-Deno-global detection path.deno bundleresolves workspace imports into one self-contained ESM file; runs on the tools/shell pinned node.Why not a polyfill
@deno/shim-denowas read and rejected: itsconnectswallows socket errors intoconsole.error(a refused dial never rejects — an infinite hang where we needconnection-refused), noallowHalfOpen, reads through the raw fd behind the Socket's back, and no UDP at all. The 10-member structural seam is smaller than the corrective wrapper would have been. (Consumers who want to inject aDeno-namespace polyfill can still do so — detection readsglobalThis— at their own fidelity risk.)Gates
just test-wasi-shims(98),just test-sockets-node(new),publish-check,test-bundle; runtime/ports/conformance untouched by this diff (wasi-shims + orchestration files only).Refs #4.