atmosphere explorer pds.ls
tool typescript atproto

show websocket close event in UI #23

merged opened by bas.sh targeting main from bas.sh/pdsls: push-qmpokmzpzkqu

hiii :3 this is not necessarily a pdsls issue, but i just noticed that some PDSes seem to abnormally disconnect after a short while when used as the firehose instance, but without @skyware/firehose emitting any errors, which means the connection just dies while nothing changes in the UI.

some examples: https://pdsls.dev/firehose?instance=wss%3A%2F%2Fpds.finfet.sh&cursor=0 consistently disconnects after ~30 seconds https://pdsls.dev/firehose?instance=wss%3A%2F%2Fat.nogrp.net&cursor=0 consistently disconnects after ~60 seconds

from what i can see the WebSocket API does not report this as an error (for whatever reason), but instead it's reported as e.g. code 1006 in the CloseEvent, which @skyware/firehose neither handles nor passes on to the close event handler.

tried with goat as well and it also disconnects (but actually stops the program too).

this is most likely an error on their side (web server/reverse proxy connection timeout i imagine), but i still think it would be nice to actually show when the connection drops, as this could also help PDS hosters with debugging this issue. I've added the different connection close reasons (idk if any of them besides 1006 would ever get used though) and handle them basically the same as errors for both the firehose and jetstream, but feel free to change it around of course.

in the case of an error event i first remove the close event listener to avoid it from overwriting the error message (i'm not entirely sure which one should take precedence here though but i think this is fine)

it could also be an idea to automatically reconnect when the connection drops abnormally but i couldn't really figure out how to do that nicely using @skyware/firehose. I tried switching to @atcute/firehose (which does have automatic reconnecting built-in as it uses PartySocket underneath) but that seemed kinda difficult as the op objects it returns appear to have a slightly different shape and i don't know enough about atproto to know why, so i'll leave that up to you if you ever want to do that in the future lol

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:c52wep6lj4sfbsqiz3yvb55h/sh.tangled.repo.pull/3mfrnigmfcc22
+33
Diff #0
+20
src/utils/websocket.ts
···
··· 1 + const _websocketCloseReasons = { 2 + 1000: "Normal Closure", 3 + 1001: "Going Away", 4 + 1002: "Protocol Error", 5 + 1003: "Unsupported Data", 6 + 1005: "No Status Received", 7 + 1006: "Abnormal Closure", 8 + 1007: "Invalid frame payload data", 9 + 1008: "Policy Violation", 10 + 1009: "Message too big", 11 + 1010: "Missing Extension", 12 + 1011: "Internal Error", 13 + 1012: "Service Restart", 14 + 1013: "Try Again Later", 15 + 1014: "Bad Gateway", 16 + 1015: "TLS Handshake", 17 + } as const; 18 + 19 + export const websocketCloseReasons = _websocketCloseReasons as typeof _websocketCloseReasons & 20 + Record<string, string>;
+13
src/views/stream/index.tsx
··· 7 import { JSONValue } from "../../components/json"; 8 import { TextInput } from "../../components/text-input"; 9 import { addToClipboard } from "../../utils/copy"; 10 import { getStreamType, STREAM_CONFIGS, STREAM_TYPES, StreamType } from "./config"; 11 import { StreamStats, StreamStatsPanel } from "./stats"; 12 ··· 183 setStats((prev) => ({ ...prev, eventsPerSecond: 0 })); 184 }; 185 186 const connectStream = async (formData: FormData) => { 187 setNotice(""); 188 if (connected()) { ··· 251 if (!isFilteredEvent || streamType !== "jetstream" || searchParams.allEvents === "on") 252 addRecord(rec); 253 }); 254 socket.addEventListener("error", () => { 255 setNotice("Connection error"); 256 disconnect(); 257 }); ··· 262 cursor: cursor, 263 autoReconnect: false, 264 }); 265 firehose.on("error", (err) => { 266 console.error(err); 267 const message = err instanceof Error ? err.message : "Unknown error"; 268 setNotice(`Connection error: ${message}`);
··· 7 import { JSONValue } from "../../components/json"; 8 import { TextInput } from "../../components/text-input"; 9 import { addToClipboard } from "../../utils/copy"; 10 + import { websocketCloseReasons } from "../../utils/websocket"; 11 import { getStreamType, STREAM_CONFIGS, STREAM_TYPES, StreamType } from "./config"; 12 import { StreamStats, StreamStatsPanel } from "./stats"; 13 ··· 184 setStats((prev) => ({ ...prev, eventsPerSecond: 0 })); 185 }; 186 187 + const onWebsocketClose = (event: CloseEvent) => { 188 + const code = event.code.toString(); 189 + if (code === "1000" || code === "1005") return; 190 + 191 + setNotice(`Connection closed: ${websocketCloseReasons[code] ?? "Unknown reason"}`); 192 + disconnect(); 193 + }; 194 + 195 const connectStream = async (formData: FormData) => { 196 setNotice(""); 197 if (connected()) { ··· 260 if (!isFilteredEvent || streamType !== "jetstream" || searchParams.allEvents === "on") 261 addRecord(rec); 262 }); 263 + socket.addEventListener("close", onWebsocketClose); 264 socket.addEventListener("error", () => { 265 + socket.removeEventListener("close", onWebsocketClose); 266 setNotice("Connection error"); 267 disconnect(); 268 }); ··· 273 cursor: cursor, 274 autoReconnect: false, 275 }); 276 + firehose.ws.addEventListener("close", onWebsocketClose); 277 firehose.on("error", (err) => { 278 + firehose.ws.removeEventListener("close", onWebsocketClose); 279 console.error(err); 280 const message = err instanceof Error ? err.message : "Unknown error"; 281 setNotice(`Connection error: ${message}`);

History

1 round 0 comments
sign up or login to add to the discussion
bas.sh submitted #0
1 commit
expand
show websocket close event
expand 0 comments
pull request successfully merged