@uplevelhq/adonis-ws-client
v1.0.0-rc.1
Published
Browser WebSocket client for @uplevelhq/adonis-ws protocol v1
Maintainers
Readme
@uplevelhq/adonis-ws-client
Browser / WHATWG WebSocket client for @uplevelhq/adonis-ws protocol v1.
Framework-independent. Native WebSocket API. Zero runtime dependencies.
Version: 1.0.0-rc.1
Designed for: modern browsers implementing WebSocket
Validated in: Playwright 1.62.1 / Chromium 151.0.7922.34 (Safari/Firefox not covered by this repo’s E2E suite)
Documentation
| Doc | Purpose | | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | Getting Started | Server + client first integration | | Production Guide | Reconnect/resync, timeouts, checklists | | Client reference | SDK behavior in depth | | API Reference | Public export inventory | | Shared Calendar Example | React/Inertia usage (app choice, not required) |
Framework-agnostic: no React, Vue, Svelte, or Inertia dependency.
Installation
Release candidate — The current public release is a 1.0 release candidate. Install the
rcchannel explicitly until stable 1.0.0 is published.
npm install @uplevelhq/adonis-ws-client@rcAfter stable 1.0.0, the default install becomes npm install @uplevelhq/adonis-ws-client.
Construct + connect
import { AdonisWebSocketClient } from '@uplevelhq/adonis-ws-client'
const client = new AdonisWebSocketClient({
url: 'wss://example.com/__ws',
})
await client.connect()Construction does not open a socket. connect() waits for protocol ready (not merely browser open).
Constructor options
| Option | Required | Default | Notes |
| ------------------ | -------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| url | yes | — | Full WS URL including path |
| authenticate | no | — | Called after ready when server requires auth; every attempt including reconnect; credentials not retained |
| reconnect | no | disabled | Opt-in { enabled: true, ... } |
| requestTimeoutMs | no | 15_000 (DEFAULT_REQUEST_TIMEOUT_MS) | 0 disables |
| webSocket | no | globalThis.WebSocket | Supported public WHATWG constructor injection (Node, tests, alternate runtimes) |
Connection states
idle → connecting → connected → (reconnecting …) → closing → closed
connected means: socket open, ready received, auth completed when required, and (after reconnect) desired resubscription attempts finished before reconnected fires.
Auth callback
new AdonisWebSocketClient({
url,
authenticate: async () => {
const response = await fetch('/ws-ticket', { method: 'POST' })
return response.json()
},
})Typed contracts
interface ClientToServer {
'ping': { data: undefined; result: { pong: true } }
'project.update': {
data: { projectId: number; title: string }
result: { projectId: number; title: string; version: number }
}
}
interface ServerToClient {
'project.updated': { projectId: number; title: string; version: number }
}
const client = new AdonisWebSocketClient<ClientToServer, ServerToClient>({ url })
const result = await client.emit('ping')
await client.emit('project.update', { projectId: 1, title: 'New' })
client.on('project.updated', (project) => {
project.version
})Compile-time only — no runtime validation. Untyped usage remains supported.
Emit
const result = await client.emit('project.update', payload)- ACK returns handler result
- Server application errors →
WebSocketRequestError(codestable) - Timeouts →
WebSocketRequestTimeoutError(timeout ≠ server cancel) - Disconnect before ACK → reject; result unknown; no replay
Subscribe
const project = await client.subscribe('projects/123')
const stop = project.on('task.created', (task) => {})
await project.unsubscribe()- Duplicate subscribe of an active membership follows server semantics (no extra capacity)
- On reconnect, subscriptions suspend then become active after resubscribe
- Unsubscribe while suspended drops local intent (no network round-trip; no later resubscribe)
- One failed resubscribe does not fail the whole connection (
resubscribeFailed)
Lifecycle events
client.events.on('disconnected', (meta) => {})
client.events.on('reconnecting', ({ attempt, delayMs }) => {})
client.events.on('reconnected', ({ connectionId, attempt }) => {})
client.events.on('reconnectExhausted', ({ attempts }) => {})
client.events.on('resubscribeFailed', ({ channel, error }) => {})Reconnect (opt-in)
Defaults when reconnect: { enabled: true }:
| Field | Default |
| ---------------- | ------- |
| initialDelayMs | 1000 |
| maxDelayMs | 30000 |
| factor | 2 |
| jitter | 0.2 |
| maxAttempts | 10 |
disconnect
→ pending requests reject
→ SDK reconnect (bounded backoff + jitter)
→ fresh auth
→ resubscribe
→ reconnected
→ application refetches HTTP snapshotReconnect restores connection + auth + subscription intent. It does not recover missed application events.
Errors
| Error | When |
| ------------------------------ | -------------------------------------------------------- |
| AdonisWebSocketClientError | Base |
| WebSocketClientStateError | Local misuse / invalid arguments |
| WebSocketConnectionError | Transport / handshake failure |
| WebSocketProtocolError | Malformed / unexpected frames |
| WebSocketRequestError | Correlated server error (code, optional requestId) |
| WebSocketRequestTimeoutError | Local timeout (requestId, kind, timeoutMs) |
Explicit close
await client.close()Cancels reconnect, rejects outstanding requests, clears desired subscriptions.
Delivery / non-replay
- Ephemeral WebSocket delivery
- No durable replay / exactly-once / mutation auto-retry
- ACK = one correlated request processed
- After
reconnected, refetch canonical HTTP/Inertia state
License
MIT — see LICENSE.
