GoogleChromeLabs / GoogleChromeLabs/comlink
In-flight requests are silently dropped when the proxy is GC'd mid-await
- Dominant language
- TypeScript
- Stars
- 12.8k
- Forks
- 435
- PR merge metrics
- No merged PRs in 30d
Description
Hit this in production. `Comlink.wrap(worker).slow()` style calls intermittently hang forever on Chrome with no error and nothing in the console. The trigger is V8 collecting the transient proxy during the suspended await; under memory pressure it fires reliably.
When the proxy is collected, comlink's `FinalizationRegistry` cleanup runs `releaseEndpoint`, which sends RELEASE, closes the port, and clears `pendingListeners`, without checking whether any of those listeners are still mid-flight. The worker's reply lands on a closed port and the Promise never resolves.
MDN is pretty explicit about not relying on FR semantics for correctness:
> Developers shouldn't rely on cleanup callbacks for essential program logic.
[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)
## Repro
https://github.com/nyan-left/comlink-repro
## Workaround
Pin the proxy on a longer-lived ref so it stays reachable across the await:
```js
// Before, intermittently hangs
const result = await Comlink.wrap(worker).slow();
// After (works):
const proxy = Comlink.wrap(worker);
this._keepAlive = proxy;
try {
return await proxy.slow();
} finally {
this._keepAlive = undefined;
}
```
Awkward to ask every comlink user to remember though.
## Proposed fix
Open PR: https://github.com/GoogleChromeLabs/comlink/pull/693
## Related
- #600 - Safari 16 GC'ing MessageChannel mid-callback. Same family of bug.
- #601 - rejecting pending requests on explicit `releaseProxy()`. Related but doesn't cover GC-triggered path.
Contributor guide
Assessment
This issue has not been assessed yet.