The soft shutdown chained ->then() on Handler::onClose(), which returns
void. The error became a silently dropped promise rejection, loop->stop()
never ran, and the server kept accepting and closing every new socket
without connection_established until a hard restart. Clients queued their
sends forever in that window.
- Close connections directly (Ratchet runs onClose once per stream), stop
the loop after a 1s grace, on a listing error, or at a 5s deadline.
- Websocket\ChannelManager::unsubscribeFromAllChannels returns early for a
connection declined before it got an app, like LocalChannelManager. That
was the "Attempt to read property id on null" flood in the log.
- Tests: SoftShutdownTest drives the shutdown on a fresh loop, StartServerTest
fails on any error logged during a soft shutdown, ClosedBeforeOpenTest.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Introduced `helpers-and-testing.md` to document global helpers and WebsocketService class usage.
- Created `HandlerLifecycleTest.php` to test the full WebSocket handler lifecycle, including connection management, channel subscriptions, and message routing.
- Added `WebsocketServiceTest.php` to validate state tracking methods in WebsocketService, covering user authentication, channel tracking, and broadcast functionality.