From c4548a4dc587e1da689003e79cccda85c0ff3b43 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:12:11 +0000 Subject: [PATCH] fix(server): a WS board subscriber bootstraps the current board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wsSubscribe replayed the journal only for a run-trace topic; a board room (project/{key}/issues) fell through and returned having sent nothing, so a late subscriber saw an empty board until an issue next changed — broadcastBoards diffs against a server-global cursor, not per-connection, so an unchanged issue is never re-sent. Now a board subscribe dumps the current board to that socket, the same first-tick dump the SSE stream does per connection. Subscribe stays first so nothing published mid-dump is lost; the client keys by issue id. Verified live with a WS client (this surface has no unit tests; it needs the server extension). The residual stale-snapshot-vs-concurrent-change ordering is TODO 1d's per-message cursor. --- src/Server.php | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Server.php b/src/Server.php index 6da7ce7..8e357f8 100644 --- a/src/Server.php +++ b/src/Server.php @@ -1021,17 +1021,33 @@ private function wsSubscribe(WebSocket $ws, string $topic, int $since): void { $ws->subscribe($topic); - if (!\preg_match('#^project/([^/]+)/run/([^/]+)/trace$#', $topic, $m)) { + if (\preg_match('#^project/([^/]+)/run/([^/]+)/trace$#', $topic, $m)) { + foreach ($this->reader($m[1])->tail($m[2], $since) as $row) { + $ws->send(\json_encode([ + 'topic' => $topic, + 'kind' => 'trace', + 'seq' => $row['seq'], + 'data' => $row, + ], self::JSON)); + } + return; } - foreach ($this->reader($m[1])->tail($m[2], $since) as $row) { - $ws->send(\json_encode([ - 'topic' => $topic, - 'kind' => 'trace', - 'seq' => $row['seq'], - 'data' => $row, - ], self::JSON)); + // A board room. Bootstrap this subscriber with the current board — the same first-tick dump the + // SSE stream does from a per-connection cursor. Without it a late WS subscriber sees nothing + // until an issue next changes, because broadcastBoards() diffs against a cursor that is + // server-global, not per-connection: an issue unchanged since the server last published it is + // never re-sent, so a room a client joins later cannot be arrived at from subscribe alone. + // Subscribe FIRST (above) so nothing published mid-dump is lost; the client keys by issue id. + if (\preg_match('#^project/([^/]+)/issues$#', $topic, $m)) { + foreach ($this->issues($m[1]) as $issue) { + $ws->send(\json_encode([ + 'topic' => $topic, + 'kind' => 'issue', + 'data' => $issue, + ], self::JSON)); + } } }