feat(tsconnect/wasm): add shutdown() to jsIPN #12

Merged
codinget merged 1 commits from shutdown-ipn-wasm into webnet 2026-06-13 22:21:25 +02:00
Owner

What

Adds a shutdown() method to jsIPN in the Go WASM bridge (cmd/tsconnect/wasm/wasm_js.go).

Why

Without this, the Go WASM runtime is kept alive indefinitely after an IPN instance is no longer needed. The main() goroutine blocks on a channel that is never closed, which:

  • Prevents Node.js processes from exiting naturally (the event loop is kept alive by the Go runtime)
  • Prevents browser service workers from being paused/killed for inactivity (open goroutines, timers, and WebSocket refs hold the runtime open)

How

  • main() now blocks on a shutdownCh chan struct{} instead of <-make(chan bool). Closing the channel lets the Go runtime's main goroutine return, which terminates the WASM module.
  • jsIPN gains three new fields: ln net.Listener (the safesocket listener), shutdownCh chan struct{}, and shutdownOnce sync.Once.
  • The safesocket listener is now created synchronously in run() (before spawning the goroutine) and stored as i.ln, so shutdown() can close it without a data race.
  • jsIPN.shutdown() (exposed to JS via makePromise):
    1. Calls lb.Shutdown() — stops the LocalBackend, WireGuard engine, netstack, etc.
    2. Closes i.ln — unblocks srv.Run's accept loop so the goroutine exits.
    3. Closes i.shutdownCh — unblocks main(), letting the Go runtime exit.
      All three are protected by sync.Once so the method is idempotent.
  • log.Fatalf on ipnserver.Run exit is demoted: net.ErrClosed (the expected error when the listener is closed during a clean shutdown) is silently ignored instead of crashing.

AI disclosure

This commit was authored by Claude Code (Claude Sonnet 4.6).

## What Adds a `shutdown()` method to `jsIPN` in the Go WASM bridge (`cmd/tsconnect/wasm/wasm_js.go`). ## Why Without this, the Go WASM runtime is kept alive indefinitely after an IPN instance is no longer needed. The `main()` goroutine blocks on a channel that is never closed, which: - Prevents Node.js processes from exiting naturally (the event loop is kept alive by the Go runtime) - Prevents browser service workers from being paused/killed for inactivity (open goroutines, timers, and WebSocket refs hold the runtime open) ## How - `main()` now blocks on a `shutdownCh chan struct{}` instead of `<-make(chan bool)`. Closing the channel lets the Go runtime's main goroutine return, which terminates the WASM module. - `jsIPN` gains three new fields: `ln net.Listener` (the safesocket listener), `shutdownCh chan struct{}`, and `shutdownOnce sync.Once`. - The safesocket listener is now created synchronously in `run()` (before spawning the goroutine) and stored as `i.ln`, so `shutdown()` can close it without a data race. - `jsIPN.shutdown()` (exposed to JS via `makePromise`): 1. Calls `lb.Shutdown()` — stops the `LocalBackend`, WireGuard engine, netstack, etc. 2. Closes `i.ln` — unblocks `srv.Run`'s accept loop so the goroutine exits. 3. Closes `i.shutdownCh` — unblocks `main()`, letting the Go runtime exit. All three are protected by `sync.Once` so the method is idempotent. - `log.Fatalf` on `ipnserver.Run` exit is demoted: `net.ErrClosed` (the expected error when the listener is closed during a clean shutdown) is silently ignored instead of crashing. ## AI disclosure This commit was authored by Claude Code (Claude Sonnet 4.6).
codinget added 1 commit 2026-06-13 02:25:33 +02:00
Expose a shutdown() method on the JS-side IPN object that stops the
LocalBackend, closes the safesocket listener (which unblocks srv.Run),
and signals main() to return so the Go runtime exits cleanly.

This allows the host environment (Node.js process or browser service
worker) to terminate normally once the Tailscale WASM module is no
longer needed, instead of being kept alive indefinitely by open handles,
goroutines, or the Go runtime's blocking main goroutine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
codinget marked the pull request as ready for review 2026-06-13 22:21:16 +02:00
codinget merged commit 6e83d5291b into webnet 2026-06-13 22:21:25 +02:00
codinget deleted branch shutdown-ipn-wasm 2026-06-13 22:21:25 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: webnet/tailscale#12