From #106 polish backlog. Severity: low UX foot-gun.
Code
kalshi/ws/client.py:529-532:
async def run_forever(self) -> None:
# docstring: "Block until the connection is closed. Use with callback API."
if self._recv_task:
await self._recv_task
No cooperative-shutdown hook. A SIGINT during run_forever() propagates CancelledError through await self._recv_task, which cancels the recv loop mid-frame. The application sees CancelledError leak out and there is no way to say "shut down cleanly: drain in-flight dispatches, close the WS, return."
The silent-return-on-missing-subscription case is tracked in a sibling foot-gun issue.
Fix
-
Add a stop() (or request_stop()) coroutine that:
- Sets an internal
_stop_requested event.
- Sends a WS-level close frame.
- Awaits recv loop completion bounded by a configurable timeout.
-
Optionally accept an asyncio.Event parameter on run_forever(stop_event=...) so callers can wire their own signal handlers cleanly:
stop = asyncio.Event()
asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop.set)
await ws.run_forever(stop_event=stop)
Acceptance
run_forever() can be terminated cooperatively without CancelledError leaking out.
- New test covering: register SIGINT handler → set stop event →
run_forever() returns cleanly with is_connected is False.
- README callback-mode example updated to show the stop-event pattern.
From #106 polish backlog. Severity: low UX foot-gun.
Code
kalshi/ws/client.py:529-532:No cooperative-shutdown hook. A
SIGINTduringrun_forever()propagatesCancelledErrorthroughawait self._recv_task, which cancels the recv loop mid-frame. The application seesCancelledErrorleak out and there is no way to say "shut down cleanly: drain in-flight dispatches, close the WS, return."The silent-return-on-missing-subscription case is tracked in a sibling foot-gun issue.
Fix
Add a
stop()(orrequest_stop()) coroutine that:_stop_requestedevent.Optionally accept an
asyncio.Eventparameter onrun_forever(stop_event=...)so callers can wire their own signal handlers cleanly:Acceptance
run_forever()can be terminated cooperatively withoutCancelledErrorleaking out.run_forever()returns cleanly withis_connected is False.