3 Commits

Author SHA1 Message Date
78d657111b Rename replay → initChannelState
All checks were successful
check / check (push) Successful in 2m20s
Rename the query parameter, function, and all related comments
from 'replay' to 'initChannelState' to better reflect the
semantics: the server initializes channel state for the
reconnecting client rather than replaying past events.
2026-03-09 17:00:56 -07:00
user
096fb2b207 docs: document ?replay=1 query parameter for GET /state 2026-03-09 17:00:56 -07:00
user
737686006e fix: replay channel state on SPA reconnect
When a client reconnects to an existing session (e.g. browser tab
closed and reopened), the server now enqueues synthetic JOIN messages
plus TOPIC/NAMES numerics for every channel the session belongs to.
These are delivered only to the reconnecting client, not broadcast
to other users.

Server changes:
- Add replayChannelState() to handlers that enqueues per-channel
  JOIN + join-numerics (332/353/366) to a specific client.
- HandleState accepts ?replay=1 query parameter to trigger replay.
- HandleLogin (password auth) also replays channel state for the
  new client since it creates a fresh client for an existing session.

SPA changes:
- On resume, call /state?replay=1 instead of /state so the server
  enqueues channel state into the message queue.
- processMessage now creates channel tabs when receiving a JOIN
  where msg.from matches the current nick (handles both live joins
  and replayed joins on reconnect).
- onLogin no longer re-sends JOIN commands for saved channels on
  resume — the server handles it via the replay mechanism, avoiding
  spurious JOIN broadcasts to other channel members.

Closes #60
2026-03-09 17:00:56 -07:00
5 changed files with 15 additions and 14 deletions

View File

@@ -1036,7 +1036,7 @@ Return the current user's session state.
| Parameter | Type | Default | Description |
|-----------|--------|---------|-------------|
| `initChannelState` | string | (none) | When set to `1`, enqueues synthetic JOIN + TOPIC + NAMES messages for every channel the session belongs to into the calling client's queue. Used by the SPA on reconnect to restore channel tabs without re-sending JOIN commands. |
| `initChannelState` | string | (none) | When set to `1`, enqueues synthetic JOIN + TOPIC + NAMES messages for every channel the session belongs to into the calling client's queue. Used by the SPA on reconnect to restore channel tabs without re-sending JOIN commands. |
**Response:** `200 OK`
```json
@@ -1070,7 +1070,7 @@ curl -s http://localhost:8080/api/v1/state \
-H "Authorization: Bearer $TOKEN" | jq .
```
**Reconnect with channel state init:**
**Reconnect with channel state initialization:**
```bash
curl -s "http://localhost:8080/api/v1/state?initChannelState=1" \
-H "Authorization: Bearer $TOKEN" | jq .

View File

@@ -445,10 +445,10 @@ func (hdlr *Handlers) enqueueNumeric(
// HandleState returns the current session's info and
// channels. When called with ?initChannelState=1, it also
// enqueues synthetic JOIN + TOPIC + NAMES messages for every
// channel the session belongs to so that a reconnecting
// client can rebuild its channel tabs from the message
// stream.
// enqueues synthetic JOIN + TOPIC + NAMES messages for
// every channel the session belongs to so that a
// reconnecting client can rebuild its channel tabs from
// the message stream.
func (hdlr *Handlers) HandleState() http.HandlerFunc {
return func(
writer http.ResponseWriter,

View File

@@ -182,8 +182,8 @@ func (hdlr *Handlers) handleLogin(
request, clientID, sessionID, payload.Nick,
)
// Init channel state so the new client knows which
// channels the session already belongs to.
// Initialize channel state so the new client knows
// which channels the session already belongs to.
hdlr.initChannelState(
request, clientID, sessionID, payload.Nick,
)

4
web/dist/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -335,7 +335,7 @@ function App() {
if (msg.to) addMessage(msg.to, { ...base, text, system: true });
if (msg.to && msg.to.startsWith("#")) {
// Create a tab when the current user joins a channel
// (including initial JOINs on reconnect).
// (including JOINs from initChannelState on reconnect).
if (msg.from === nickRef.current) {
setTabs((prev) => {
if (
@@ -656,9 +656,10 @@ function App() {
if (isResumed) {
// Request MOTD on resumed sessions (new sessions
// get it automatically from the server during
// creation). Channel state is initialised by the
// server via the message queue (?initChannelState=1), so we
// do not need to re-JOIN channels here.
// creation). Channel state is initialized by the
// server via the message queue
// (?initChannelState=1), so we do not need to
// re-JOIN channels here.
try {
await api("/messages", {
method: "POST",