microsoft / microsoft/TypeScript
wasip1: LSP over node:wasi hangs after `initialize` — signal watcher starves the scheduler (fix included)
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 132
Description
Split out from microsoft/TypeScript#63858. `tsgo --lsp --stdio` built for wasip1 and run under Node's `node:wasi` answers `initialize`, then ignores everything after it, spinning at 100% CPU. CLI invocations (`tsgo -p`) are unaffected — output is identical to the native binary on node >= 23.
**Cause:** `cmd/tsgo` calls `signal.NotifyContext` unconditionally. On wasip1 signals are never delivered, and Go's signal-watcher goroutine busy-spins (a `sched_yield` loop — we measured ~190k calls in 8s of idle). Wasm is single-threaded, so that always-runnable goroutine keeps the scheduler from ever reaching `poll_oneoff` — and the stdin reader, parked waiting for input, never wakes.
**Fix:** skip the signal watcher on wasip1 — since no signal can arrive, `context.WithCancel` is equivalent:
```go
func notifyContext(parent context.Context, sigs ...os.Signal) (context.Context, context.CancelFunc) {
if runtime.GOOS == "wasip1" {
return context.WithCancel(parent)
}
return signal.NotifyContext(parent, sigs...)
}
```
Patch applies at `typescript/v7.0.2`. With it, a full LSP session under node:wasi works — initialize, publishDiagnostics, hover, shutdown — with exit behavior matching native. It also lets the runtime idle in `poll_oneoff` instead of burning a core.
One note: this makes LSP-over-wasm node-only for now — wasmtime 46 refuses the non-blocking stdio call (`fd_fdstat_set_flags`) the LSP path needs, so it still freezes there even with the fix.
Minimal repro driver:
```js
import { readFile } from 'node:fs/promises';
import { WASI } from 'node:wasi';
const wasi = new WASI({ version: 'preview1', args: ['tsgo', '--lsp', '--stdio'],
env: { PWD: '/' }, preopens: { '/': process.cwd() }, returnOnExit: true });
const wasm = await WebAssembly.compile(await readFile('tsgo.wasm'));
const inst = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(inst);
```
Spawn it with piped stdio, send a framed `initialize` (answered), then anything else: the stock binary never reads it; the patched one proceeds.
Contributor guide
Assessment
This issue has not been assessed yet.