-
Notifications
You must be signed in to change notification settings - Fork 4
mpsc Sender.close() doesn't wake blocked receivers when buffer has data + null sentinel breaks typed channels #3
Description
Summary
Found two concurrency bugs in the mpsc channel implementation via code audit.
Bug 1: Sender.close() doesn't wake blocked receivers when buffer has data (HIGH)
File: src/sync/mpsc.ts, lines 201-208
`Sender.close()` only calls `wakeRecvWaitersWithNull()` when `senderCount === 0 && buffer.isEmpty()`. If the last sender closes while there is still data in the buffer AND a receiver is waiting, the receiver will never be woken. It remains blocked forever.
Fix: After decrementing `senderCount`, if it reaches 0, wake all receivers regardless of buffer state. Receivers will drain the buffer first and then get `null`.
Same issue exists in `UnboundedSender.close()` at lines 386-390.
Bug 2: AsyncIterator uses null as termination sentinel (HIGH)
File: src/sync/mpsc.ts, lines 321-327
The `[Symbol.asyncIterator]` uses `if (value === null) return;` to detect channel closure. But `null` is a valid value for `Receiver` or `Receiver<T | null>`. Sending `null` terminates the iterator prematurely, losing all subsequent messages. Same in `UnboundedReceiver` at lines 448-454.
Tokio avoids this with `Option`. Consider using a wrapper object or documenting the limitation.
Bug 3: select() swallows errors from losing branches (MEDIUM)
File: src/sync/select.ts, lines 24-29
When `Promise.race` picks the winner, losing branches that already rejected become unhandled promise rejections.
Fix: Add `.catch(() => {})` to losing promises after the race completes.
Bug 4: Watch changed() rejects before checking unseen values (MEDIUM)
File: src/sync/watch.ts, lines 132-134
`changed()` checks `senderClosed` before checking `version !== lastSeenVersion`. If the sender sends a final value then closes, a receiver that hasn't seen the latest value gets `RecvError` instead of being told the value changed. Tokio returns Ok if there is an unseen value, even if the sender closed.
Fix: Swap the order: check version mismatch first, then sender closed.
Found via code audit.