feat(tsconnect): Node.js support — WasmSource, fs shim, safesocket fix, test suite #33
Reference in New Issue
Block a user
Delete Branch "worktree-bridge-cse_01CETAfxGfBA7CRyPrbuDYv1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this PR does
Extends
@webnet/tsconnectwith full Node.js support, covering three areas.1.
WasmSource— multi-environment WASM loading (initIPN)initIPNpreviously only accepted a URL string and calledfetch(), which does not work withfile://paths in Node.js. The parameter type is now aWasmSourceunion:string | URL→fetch()+instantiateStreaming(browser / CDN)ArrayBuffer | ArrayBufferView→instantiate(Node.jsfs.readFile, Bun)Response→instantiateStreaming(Cloudflare Workers module binding)ReadableStream<Uint8Array>→ wrappedResponse+instantiateStreaming2.
wasm_exec.js/ Node.js environmentwasm_exec.jsinstalls ENOSYS stubs forglobalThis.fswhen it is falsy. In Node.js,globalThis.fsis undefined, so the stubs are installed — and this is the correct behaviour. tsconnect's WASM routes all network calls through JavaScript'sfetch()and WebSocket APIs, which use Node.js's own DNS resolver. The Go net package's attempt to read/etc/resolv.confshould remain a no-op via the ENOSYS stub. An earlier iteration of this branch setglobalThis.fsto Node.js's realfs, which caused Go to pick up nameservers from the host'sresolv.confand try them directly — breaking in environments where those servers are unreachable.wasm_exec.jsis now imported directly inindex.tsandGois extracted fromglobalThisinline. A separateenv-node.ts/env-web.tssplit with a#envpackage.jsonimports condition was explored, but both files were identical so the indirection was removed.3.
IPN.shutdown()A
shutdown()method onIPN(TypeScript) andjsIPN(Go/WASM) stops theLocalBackend, closes the safesocket listener, and signalsmain()to return so the Go runtime exits cleanly. The TypeScript side awaits thego.run()Promise as the authoritative exit signal, avoiding a race where Go deletes_instbefore a callback-based resolve could fire. Go-side nil guards were added forlbandlnsoshutdown()is safe even whenrun()was never called.4. Two Go-side bug fixes in the
tailscalesubmodulesafesocket_js.go: hardcoded"Tailscale-IPN"memconn address causedlog.Fatalon a secondnewIPN()call in the same WASM process. An atomic counter now gives each instance a unique name.wasm_js.go:listen()rejected the standard":port"(any-interface) address form;netstack.ListenTCPrequires an explicit bind address. Now normalises:port→0.0.0.0:port.5. Test suite
InMemoryFileOpsandInMemoryState(no WASM needed)/localapi/v0/status, and two-node TCP dial/listengetFactory()singleton (WASM compiled once) and a top-levelbefore()/after()pair —shutdown()kills the entire Go WASM runtime so per-test teardown is not possible--test-force-exitkeeps the runner from hanging on tsx's open handles after Go exits--test-timeout=180000to accommodate the time nodes need to reach Running statetest:coveragescript added for consistency with other packagesRunning the integration tests
Or set them in
.env.localat the repo root (gitignored).WIP: feat(tsconnect): accept ArrayBuffer/Uint8Array/Response/ReadableStream in initIPNto feat(tsconnect): Node.js support — WasmSource, fs shim, safesocket fix, test suiteca10c7a026to1a660613160501c2fe64toa9d26d965ba9d26d965bto04ff2aa76004ff2aa760toa502735fd3