Skip to content

Commit ae3fb0e

Browse files
committed
Improved naming. Can now pass connection options (timeouts)
1 parent 2923298 commit ae3fb0e

File tree

10 files changed

+75
-69
lines changed

10 files changed

+75
-69
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ Over the past few years, I’ve had to implement WebSocket communication many ti
1313

1414
This library grew out of my work on [theshutter.app](https://theshutter.app) ([@heyshutterapp](https://x.com/heyshutterapp)) — a platform for running remote photoshoots and recording video interviews — which involves a lot of real-time communication over WebSockets and WebRTC.
1515

16-
### AlwaysConnected
16+
### WsConnectionMonitor
1717

18-
The core of this library is `AlwaysConnected` — a finite state machine that keeps a WebSocket connection alive. It handles reconnection automatically, so you don't have to.
18+
The core of this library is `WsConnectionMonitor` — a finite state machine that keeps a WebSocket connection alive. It handles reconnection automatically, so you don't have to.
1919

2020
#### State machine
2121

@@ -34,23 +34,23 @@ Disconnected → Connecting → Limbo → Connected
3434

3535
#### Application-level connection
3636

37-
`AlwaysConnected` is protocol-agnostic. When the WebSocket opens, it transitions to **limbo** and calls your `onConnectedFn()`. This is where you authenticate, join a room, subscribe to topics, etc. Return `true` to confirm the connection is ready, which transitions to **connected**.
37+
`WsConnectionMonitor` is protocol-agnostic. When the WebSocket opens, it transitions to **limbo** and calls your `onConnectedFn()`. This is where you authenticate, join a room, subscribe to topics, etc. Return `true` to confirm the connection is ready, which transitions to **connected**.
3838

3939
If your handshake doesn't complete within `connectionTimeout`, the connection is considered failed and reconnection is triggered.
4040

4141
#### Heartbeats
4242

4343
Heartbeat handling is split by responsibility:
4444

45-
- **Outbound**: `AlwaysConnected` calls your `sendHeartbeat(ws, interval)` function at `heartbeatInterval`. You decide what to send (ping frame, JSON message, etc.).
45+
- **Outbound**: `WsConnectionMonitor` calls your `sendHeartbeat(ws, interval)` function at `heartbeatInterval`. You decide what to send (ping frame, JSON message, etc.).
4646

4747
- **Inbound**: Your application tracks incoming heartbeats. When you detect a missing one, call `handleWebSocketHeartbeatTimeout()` to trigger reconnection.
4848

49-
This design keeps `AlwaysConnected` unaware of your wire protocol.
49+
This design keeps `WsConnectionMonitor` unaware of your wire protocol.
5050

5151
#### WebSocket factory
5252

53-
`AlwaysConnected` doesn't create WebSockets directly. Instead, you provide a `createWs()` factory function. This allows you to:
53+
`WsConnectionMonitor` doesn't create WebSockets directly. Instead, you provide a `createWs()` factory function. This allows you to:
5454
- Use custom WebSocket implementations
5555
- Add logging or instrumentation
5656

@@ -68,7 +68,7 @@ All timing is configurable via the options object:
6868

6969
### GreatWebSocket
7070

71-
`GreatWebSocket` is the main class you'll use. It wraps `AlwaysConnected` and adds:
71+
`GreatWebSocket` is the main class you'll use. It wraps `WsConnectionMonitor` and adds:
7272
- Convenient `send()` method with connection checking
7373
- RPC-style request/response via `call()` and `tryHandleAsControlMessage()`
7474
- Connection state events

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@deilux/websocket-js",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Make WebSockets great again",
55
"main": "dist/cjs/index.js",
66
"module": "dist/esm/index.js",

src/events.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { ConnectionState as ConnectionStateType } from "./models";
2-
import { ConnectionState } from "./models";
32

4-
export interface GreatWebSocketEventMap {
3+
export interface WebSocketEventMap {
54
statechange: ConnectionStateChangeEvent;
65
connectiontimeout: Event;
76
}

src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
export { ConnectionStateChangeEvent } from "./events";
2-
export { AlwaysConnected, AlwaysConnectedOptions } from "./keep-online";
32
export { ConnectionState, createWebSocketFn, heartbeatFn } from "./models";
43
export { RemoteCommand, ResponseMatcher } from "./rpc";
4+
export {
5+
WsConnectionMonitor as AlwaysConnected,
6+
WsConnectionOptions as AlwaysConnectedOptions,
7+
} from "./state-machine";
58
export { GreatWebSocket } from "./websocket";
6-
export { createWebSocket } from "./websocket-factory";
9+
export { createDefaultWebSocket as createWebSocket } from "./websocket-factory";

src/models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface WebSocketIsh {
1515
}
1616

1717
export type createWebSocketFn = () => WebSocketIsh;
18+
1819
export type heartbeatFn = (
1920
ws: WebSocketIsh,
2021
timeSinceLastHeartbeat: number,
Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {
2-
ConnectionStateChangeEvent,
3-
type GreatWebSocketEventMap,
4-
} from "./events";
1+
import { ConnectionStateChangeEvent, type WebSocketEventMap } from "./events";
52
import {
63
ConnectionState,
74
type ConnectionState as ConnectionStateType,
@@ -10,13 +7,13 @@ import {
107
type WebSocketIsh,
118
} from "./models";
129

13-
export interface AlwaysConnectedOptions {
10+
export interface WsConnectionOptions {
1411
heartbeatInterval: number;
1512
reconnectDelay: number;
1613
connectionTimeout: number;
1714
}
1815

19-
export class AlwaysConnected extends EventTarget {
16+
export class WsConnectionMonitor extends EventTarget {
2017
#active = false;
2118
#ws: WebSocketIsh | null = null;
2219
#state: ConnectionStateType = ConnectionState.Disconnected;
@@ -32,7 +29,7 @@ export class AlwaysConnected extends EventTarget {
3229
private readonly createWs: createWebSocketFn,
3330
private readonly onConnectedFn: () => Promise<boolean>,
3431
private readonly sendHeartbeat: heartbeatFn,
35-
private readonly options: AlwaysConnectedOptions,
32+
private readonly options: WsConnectionOptions,
3633
) {
3734
super();
3835
}
@@ -179,15 +176,15 @@ export class AlwaysConnected extends EventTarget {
179176
}, this.options.reconnectDelay);
180177
}
181178

182-
addEventListener<K extends keyof GreatWebSocketEventMap>(
179+
addEventListener<K extends keyof WebSocketEventMap>(
183180
type: K,
184181
listener: EventListenerOrEventListenerObject,
185182
options?: boolean | AddEventListenerOptions,
186183
): void {
187184
super.addEventListener(type, listener, options);
188185
}
189186

190-
removeEventListener<K extends keyof GreatWebSocketEventMap>(
187+
removeEventListener<K extends keyof WebSocketEventMap>(
191188
type: K,
192189
listener: EventListenerOrEventListenerObject,
193190
options?: boolean | EventListenerOptions,

src/websocket-factory.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
import type { WebSocketIsh } from "./models";
22

3-
export interface Operator {
3+
export interface ConnectionLifecycleHandler {
44
handleWebSocketOpen(): void;
55
handleWebSocketClosed(ws: WebSocket): void;
66
handleWebSocketError(ws: WebSocket): void;
77
handleWebSocketHeartbeatTimeout(): void;
88
}
99

10-
export const createWebSocket = (
10+
export const createDefaultWebSocket = (
1111
wsUrl: string,
12-
operator: Operator,
12+
lifecycleHandler: ConnectionLifecycleHandler,
1313
onMessageFn: (ws: WebSocketIsh, ev: MessageEvent) => void,
1414
): WebSocketIsh => {
1515
const ws = new WebSocket(wsUrl);
1616
ws.onerror = (error) => {
1717
console.error("WS error: ", error);
18-
operator.handleWebSocketError(ws);
18+
lifecycleHandler.handleWebSocketError(ws);
1919
};
2020

2121
ws.onopen = async () => {
2222
console.log("Websocket connected");
2323

2424
ws.onclose = (ev) => {
2525
console.log("WS closed: ", ev.reason, ev);
26-
operator.handleWebSocketClosed(ws);
26+
lifecycleHandler.handleWebSocketClosed(ws);
2727
};
2828

2929
ws.onmessage = (ev) => {
3030
onMessageFn(ws, ev);
3131
};
3232

33-
operator.handleWebSocketOpen();
33+
lifecycleHandler.handleWebSocketOpen();
3434
};
3535

3636
return ws;

src/websocket.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
1-
import type { GreatWebSocketEventMap } from "./events";
1+
import type { WebSocketEventMap } from "./events";
22
import type { PendingCommand } from "./internal";
3-
import { AlwaysConnected } from "./keep-online";
43
import { ConnectionState, type heartbeatFn } from "./models";
54
import type { RemoteCommand } from "./rpc";
6-
import { createWebSocket, type Operator } from "./websocket-factory";
7-
8-
export class GreatWebSocket implements Operator {
9-
#ws: AlwaysConnected | null = null;
5+
import { WsConnectionMonitor, type WsConnectionOptions } from "./state-machine";
6+
import {
7+
type ConnectionLifecycleHandler,
8+
createDefaultWebSocket,
9+
} from "./websocket-factory";
10+
11+
export const DEFAULT_CONNECTION_OPTIONS = {
12+
heartbeatInterval: 15000,
13+
reconnectDelay: 2000,
14+
connectionTimeout: 15000,
15+
} as const satisfies WsConnectionOptions;
16+
17+
export class GreatWebSocket implements ConnectionLifecycleHandler {
18+
#ws: WsConnectionMonitor | null = null;
1019
#pendingCommands: PendingCommand[] = [];
1120

1221
constructor(
1322
url: string,
1423
private readonly onConnectedFn: () => Promise<boolean>,
1524
private readonly onMessageFn: (ws: WebSocket, ev: MessageEvent) => void,
1625
private readonly sendHeartbeat: heartbeatFn,
26+
private readonly options: WsConnectionOptions = DEFAULT_CONNECTION_OPTIONS,
1727
) {
18-
this.#ws = new AlwaysConnected(
28+
this.#ws = new WsConnectionMonitor(
1929
() =>
20-
createWebSocket(
30+
createDefaultWebSocket(
2131
url,
2232
this,
2333
this.onMessageFn as (ws: unknown, ev: MessageEvent) => void,
2434
),
2535
this.onConnectedFn,
2636
this.sendHeartbeat,
27-
{
28-
heartbeatInterval: 15000,
29-
reconnectDelay: 2000,
30-
connectionTimeout: 15000,
31-
},
37+
this.options,
3238
);
3339
}
3440

@@ -76,15 +82,15 @@ export class GreatWebSocket implements Operator {
7682

7783
//#region Events
7884

79-
addEventListener<K extends keyof GreatWebSocketEventMap>(
85+
addEventListener<K extends keyof WebSocketEventMap>(
8086
type: K,
8187
listener: EventListenerOrEventListenerObject,
8288
options?: boolean | AddEventListenerOptions,
8389
): void {
8490
this.#ws?.addEventListener(type, listener, options);
8591
}
8692

87-
removeEventListener<K extends keyof GreatWebSocketEventMap>(
93+
removeEventListener<K extends keyof WebSocketEventMap>(
8894
type: K,
8995
listener: EventListenerOrEventListenerObject,
9096
options?: boolean | EventListenerOptions,

test/keep-online.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import { ConnectionStateChangeEvent } from "../src/events";
3-
import { AlwaysConnected } from "../src/keep-online";
43
import { ConnectionState } from "../src/models";
4+
import { WsConnectionMonitor } from "../src/state-machine";
55

66
// Mock implementations
77
class MockWebSocket {
@@ -12,12 +12,12 @@ class MockWebSocket {
1212
public removeEventListener = vi.fn();
1313
}
1414

15-
describe("AlwaysConnected", () => {
15+
describe("WsConnectionMonitor", () => {
1616
let mockWebSocket: MockWebSocket;
1717
let createWebSocketFn: vi.MockedFunction<() => WebSocket>;
1818
let onConnectedFn: vi.MockedFunction<() => Promise<boolean>>;
1919
let sendHeartbeatFn: vi.MockedFunction<(ws: WebSocket) => void>;
20-
let alwaysConnected: AlwaysConnected;
20+
let alwaysConnected: WsConnectionMonitor;
2121

2222
beforeEach(() => {
2323
vi.clearAllMocks();
@@ -29,7 +29,7 @@ describe("AlwaysConnected", () => {
2929
onConnectedFn = vi.fn().mockResolvedValue(true);
3030
sendHeartbeatFn = vi.fn();
3131

32-
alwaysConnected = new AlwaysConnected(
32+
alwaysConnected = new WsConnectionMonitor(
3333
createWebSocketFn,
3434
onConnectedFn,
3535
sendHeartbeatFn,

0 commit comments

Comments
 (0)