
Show HN: Steady-socket – A WebSocket client that survives dead connections
A WebSocket client that stays connected — on the web and in React Native.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
A WebSocket client that stays connected — on the web and in React Native.
Many apps get live updates from a server: the score of a match, a chat message, a delivery moving on a map. They usually do it with a WebSocket, one connection that stays open so the server can send data whenever it wants.
That connection breaks more often than you would expect. The Wi-Fi drops, a phone locks its screen, the server restarts, a login expires, or a router quietly stops passing data and nothing tells the app. Left alone, the app shows stale data, reconnects so eagerly that it hammers the server, or forgets what it was listening to.
steady-socket wraps the platform WebSocket with the parts every real-time app ends up rewriting: heartbeats that catch dead connections, reconnects with backoff and jitter, fresh auth on every reconnect, subscriptions that come back by themselves, and awareness of when a mobile app goes to the background.
A thin React layer sits on top and runs unchanged in Next.js and Expo.
Status: early release (0.x). The API is complete for what is described below and is covered by tests, but it may still change in minor versions until 1.0. Changes are listed in CHANGELOG.md; the plan is in ROADMAP.md.
How it works and why, including what it does badly: design decisions.
A WebSocket that works in a demo and one that works in production are different things. Reconnecting clients such as partysocket and reconnecting-websocket already cover backoff and a fresh URL on each connect, and react-use-websocket and Socket.IO have heartbeats. What the plain-WebSocket libraries leave to you is the part that sits on top of the connection (see how it compares):
All of it sits behind a small, explicit state machine you can observe and test, with no dependencies in the core.
Read from each library's published code (versions below), not from memory or marketing.
How each cell was checked: versions as published on npm on the day of writing; features read from each package's source and types (reconnecting-websocket has had no release since 2020); sizes are the main entry bundled with esbuild, minified and gzipped with React external, the same way for every library. They compare what you ship, not how much each library does.
None of this is magic, and you can write it yourself. examples/hand-written.ts is a careful version for a plain WebSocket: backoff with jitter, a fresh token on every connection, a heartbeat, subscriptions sent again after every reconnect, a stop on close code 4401, and a wake-up when the page comes back. It is 141 lines of code and it passes the same four scenarios as the library against the demo server: a connection that goes silent, a server restart, an expired login, and subscriptions after a reconnect (the tests).
examples/with-steady-socket.ts is the same behaviour with the library, 22 lines:
npm install steady-socket
React and React Native support are included as subpath exports. react is an optional peer dependency, and steady-socket never imports react-native directly.
React Native. Pass AppState in; the package never imports react-native. When the app comes back to active, the socket does not wait for its next scheduled attempt: if it is reconnecting, it connects straight away; if it looks open, it pings straight away (needs heartbeat), so a connection the OS dropped in the background is replaced in seconds.
import { AppState } from 'react-native'; import { appStateLifecycle } from 'steady-socket/react-native';
const socket = createSocket({ // … heartbeat: {/* … */}, lifecycle: appStateLifecycle(AppState), });
Web. browserLifecycle() listens to the browser:
import { browserLifecycle, createSocket } from 'steady-socket';
const socket = createSocket({ /* … */ lifecycle: browserLifecycle() });
Pinging on wake only works if you configured heartbeat; without it, an open socket is left alone because there is nothing to ask it.
idle ──connect()──▶ connecting ──open──▶ open ▲ │ │ close / heartbeat timeout │ ▼ └──backoff──── reconnecting │ close() from any state ─────┴────▶ closed
close() is final: it works from any state, cancels pending timers, and the socket never reconnects. connect() only does something from idle. When a socket ends up closed, socket.closeReason says why ('closed-by-user', 'connection-lost' when reconnecting is off, 'max-attempts', or 'auth-failure' when isAuthFailure returned true for the close event).
With heartbeat set, a pong that does not arrive within timeout closes the socket with code 4000 and goes to reconnecting straight away, without waiting for a close event that a dead connection may never send. Pongs are consumed by the heartbeat and are not passed to onMessage listeners.
socket.state always holds the current state, and onState notifies every transition. A connection that fails auth (isAuthFailure) goes to closed with a reason, rather than retrying forever.
createSocket({ // … onError: (error) => reportToYourErrorTracker(error), });
Without it, the error is rethrown from a timer, so it still shows up as an uncaught error (like a DOM event listener that throws) and is not swallowed. In Node with ws, that means the process sees it, so pass onError if a malformed message from a server should not be fatal.
Because the WebSocket constructor can be injected, you can test code that uses steady-socket without a server. The package's own tests use a fake socket and fake timers to drive every transition, including heartbeats that never get a pong.
Try the web demo: press the buttons that break the connection and watch it recover. The server behind it runs on a free plan that sleeps when idle, so the first visit can take up to a minute to connect; the page says so and steady-socket keeps retrying until the server answers. Each visitor's buttons only affect their own connection.
apps/demo-server is a WebSocket server that can go silent while staying open, restart, and expire tokens on request. apps/web is a Next.js page that uses steady-socket against it, so you can break the connection and watch it recover. apps/mobile is the same screen as an Expo app, using appStateLifecycle(AppState); it has been run on a phone, and CI bundles it but has no device to run it on. See the web demo, the mobile demo and the server.
Issues and PRs are welcome. pnpm test runs the suite and pnpm changeset records a change for the next release. RELEASING.md explains how releases are published.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。