Commit 47dbd93
authored
fix(client): reject calls when websocket is not open instead of sending into a closed socket (#1581)
## Problem
`LinkWebsocketClient` calls `websocket.send()` without checking the
socket state. With a plain WebSocket this doesn't matter, but with
reconnecting sockets like partysocket (which the docs recommend) it
breaks in two ways:
- Frames sent while disconnected are buffered and replayed onto the next
connection. By then the client peer has already rejected those calls and
forgotten their ids (the close handler calls `peer.close()`), so the
server ends up with ghost requests, e.g. duplicate event-iterator
subscriptions streaming everything twice (or N times), and mutations
that get re-executed.
- With `maxEnqueuedMessages: 0` on partysocket's side: the frame is
silently dropped, but the peer still awaits a response forever. The call
never settles, so caller retry logic never runs.
Easy to reproduce: subscribe to an event iterator, kill the server for
~20s while the client retries, bring it back. Each retry leaves one
buffered REQUEST frame, and on reconnect they all become live
server-side subscriptions.
## Fix
Each send now dispatches on `readyState`:
- **`OPEN`** => send, as before.
- **`CONNECTING`** => wait for the next `open`/`close` event, then
re-check. This covers the initial connect (replacing the
constructor-level `untilOpen`) and also reconnect windows on
reconnecting sockets: a call made while an attempt is in flight is sent
once the connection opens, or rejected if the attempt fails, instead of
erroring out during a routinely-occurring ~50ms window.
- **`CLOSING`/`CLOSED`** => throw. `peer.request()` already handles a
throwing send by rejecting the call, so callers get an immediate error
instead of a hang or a ghost frame.
Native WebSocket behavior is unchanged: `CONNECTING` only happens before
the first open, and after a close all pending calls are rejected anyway.
The guard only turns a send into a dead socket from a silent no-op into
a proper rejection.
One behavioral note: frames can no longer be buffered across a reconnect
by the socket wrapper. I think that's correct: request ids are
connection-scoped, so replaying frames onto a new connection is never
safe, and callers already have to handle rejection since close rejects
everything in flight. (The wait-during-CONNECTING path is safe for the
same reason in reverse: if a call is rejected while its frame is
waiting, `ClientPeer`'s id-guard drops the frame instead of sending it.)1 parent e801713 commit 47dbd93
2 files changed
Lines changed: 115 additions & 10 deletions
Lines changed: 16 additions & 10 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | | - | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
20 | | - | |
| 21 | + | |
21 | 22 | | |
22 | | - | |
23 | | - | |
24 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
25 | 33 | | |
26 | | - | |
27 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
28 | 37 | | |
29 | | - | |
30 | 38 | | |
31 | | - | |
32 | | - | |
33 | 39 | | |
34 | 40 | | |
35 | 41 | | |
| |||
Lines changed: 99 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
88 | 89 | | |
89 | 90 | | |
90 | 91 | | |
| 92 | + | |
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
| |||
99 | 101 | | |
100 | 102 | | |
101 | 103 | | |
| 104 | + | |
102 | 105 | | |
103 | 106 | | |
104 | 107 | | |
| |||
107 | 110 | | |
108 | 111 | | |
109 | 112 | | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
110 | 209 | | |
0 commit comments