A custom implementation of the Channels API for communicating with an Elixir/Phoenix backend via WebSockets.
npx @tessl/cli install tessl/npm-phoenix-websocket@2.0.00
# Phoenix WebSocket
1
2
Phoenix WebSocket is a TypeScript WebSocket client library for communicating with Elixir/Phoenix Channels. It provides an async/await-based API for establishing WebSocket connections, subscribing to channels/topics, sending messages, and handling server responses with automatic reconnection and comprehensive error handling.
3
4
## Package Information
5
6
- **Package Name**: phoenix-websocket
7
- **Package Type**: npm
8
- **Language**: TypeScript
9
- **Installation**: `npm install phoenix-websocket`
10
11
## Core Imports
12
13
```typescript
14
import {
15
PhoenixWebsocket,
16
WebsocketStatuses,
17
PhoenixWebsocketLogLevels,
18
TopicStatuses,
19
TopicMessageHandler,
20
PhoenixError,
21
PhoenixReply
22
} from "phoenix-websocket";
23
```
24
25
For CommonJS:
26
27
```javascript
28
const {
29
PhoenixWebsocket,
30
WebsocketStatuses,
31
PhoenixWebsocketLogLevels,
32
TopicStatuses,
33
TopicMessageHandler,
34
PhoenixError,
35
PhoenixReply
36
} = require("phoenix-websocket");
37
```
38
39
## Basic Usage
40
41
```typescript
42
import { PhoenixWebsocket } from "phoenix-websocket";
43
44
// Create connection instance
45
const socket = new PhoenixWebsocket("wss://example.io/channel-endpoint", {
46
token: "auth-token"
47
});
48
49
// Set up callbacks
50
socket.onConnectedCallback = () => {
51
console.log("Connected to Phoenix server");
52
};
53
54
socket.onDisconnectedCallback = () => {
55
console.log("Disconnected from Phoenix server");
56
};
57
58
// Connect and subscribe to a topic
59
await socket.connect();
60
await socket.subscribeToTopic("lobby", { user_id: "123" }, {
61
user_joined: (payload) => console.log("User joined:", payload),
62
user_left: (payload) => console.log("User left:", payload),
63
});
64
65
// Send a message
66
const reply = await socket.sendMessage("lobby", "new_message", {
67
text: "Hello, world!"
68
});
69
```
70
71
## Architecture
72
73
Phoenix WebSocket is built around several key components:
74
75
- **Connection Management**: Automatic connection handling with configurable retry logic and timeout settings
76
- **Topic Subscription System**: Manage multiple topic subscriptions with individual message handlers
77
- **Message Queue System**: Promise-based message sending with reply handling and timeout management
78
- **Error Handling**: Comprehensive error types for different failure scenarios
79
- **Reconnection Logic**: Automatic reconnection with exponential backoff and online/offline detection
80
81
## Capabilities
82
83
### Connection Management
84
85
Core connection functionality for establishing and maintaining WebSocket connections to Phoenix servers.
86
87
```typescript { .api }
88
class PhoenixWebsocket {
89
constructor(
90
url: string,
91
queryParams?: { [key: string]: string },
92
timeoutInMs?: number
93
);
94
95
connect(): Promise<void>;
96
disconnect(clearTopics?: boolean): void;
97
98
get connectionStatus(): WebsocketStatuses;
99
get subscribedTopics(): string[];
100
101
onConnectedCallback?: (() => void) | undefined;
102
onDisconnectedCallback?: (() => void) | undefined;
103
104
setLogLevel(logLevel: PhoenixWebsocketLogLevels): void;
105
disposeEvents(): void;
106
}
107
```
108
109
**Usage Examples:**
110
111
```typescript
112
// Basic connection
113
const socket = new PhoenixWebsocket("wss://localhost:4000/socket");
114
await socket.connect();
115
116
// With query parameters and timeout
117
const socket = new PhoenixWebsocket(
118
"wss://example.com/socket",
119
{ token: "abc123", user: "alice" },
120
30000 // 30 second timeout
121
);
122
123
// Check connection status
124
if (socket.connectionStatus === WebsocketStatuses.Connected) {
125
console.log("Socket is connected");
126
}
127
128
// Clean disconnect (removes all topics)
129
socket.disconnect(true);
130
```
131
132
### Topic Subscription
133
134
Topic subscription system for joining Phoenix channels and handling server messages.
135
136
```typescript { .api }
137
type TopicMessageHandler = (data: { [key: string]: any } | undefined) => void;
138
139
// Object-based message handlers
140
subscribeToTopic(
141
topic: string,
142
payload?: { [key: string]: any },
143
messageHandlers?: { [message: string]: TopicMessageHandler },
144
reconnectHandler?: (reconnectPromise: Promise<void>) => void
145
): Promise<void>;
146
147
// Map-based message handlers
148
subscribeToTopic(
149
topic: string,
150
payload?: { [key: string]: any },
151
messageHandlers?: Map<string, TopicMessageHandler>,
152
reconnectHandler?: (reconnectPromise: Promise<void>) => void
153
): Promise<void>;
154
155
unsubscribeToTopic(topic: string): void;
156
```
157
158
**Usage Examples:**
159
160
```typescript
161
// Simple subscription without message handlers
162
await socket.subscribeToTopic("lobby");
163
164
// With join payload
165
await socket.subscribeToTopic("room:123", { user_id: "alice" });
166
167
// With message handlers
168
await socket.subscribeToTopic("chat", { user: "alice" }, {
169
user_joined: (payload) => {
170
console.log(`${payload.username} joined the chat`);
171
},
172
user_left: (payload) => {
173
console.log(`${payload.username} left the chat`);
174
},
175
new_message: (payload) => {
176
console.log(`Message: ${payload.text}`);
177
}
178
});
179
180
// With Map-based handlers
181
const handlers = new Map([
182
["user_joined", (data) => console.log("User joined:", data)],
183
["user_left", (data) => console.log("User left:", data)]
184
]);
185
await socket.subscribeToTopic("lobby", {}, handlers);
186
187
// With reconnect handler for error recovery
188
await socket.subscribeToTopic("critical_topic", { userId: "123" }, {
189
error: (payload) => console.error("Topic error:", payload)
190
}, (reconnectPromise) => {
191
// Called on rejoin attempts (NOT initial join)
192
console.log("Topic reconnecting...");
193
reconnectPromise.catch((error) => {
194
if (error instanceof PhoenixRespondedWithError) {
195
if (error.reply?.response?.reason === "Invalid User") {
196
console.error("Invalid user on reconnect");
197
}
198
}
199
});
200
});
201
202
// Handle join errors
203
try {
204
await socket.subscribeToTopic("restricted_topic", { userId: "123" });
205
} catch (error) {
206
if (error instanceof PhoenixRespondedWithError) {
207
if (error.reply?.response?.reason === "Invalid User") {
208
console.error("Access denied: Invalid user");
209
}
210
}
211
}
212
213
// Unsubscribe from topic
214
socket.unsubscribeToTopic("lobby");
215
```
216
217
### Message Sending
218
219
Message sending functionality for communicating with Phoenix channels.
220
221
```typescript { .api }
222
sendMessage(
223
topic: string,
224
message: string,
225
payload?: { [key: string]: any }
226
): Promise<PhoenixReply>;
227
```
228
229
**Usage Examples:**
230
231
```typescript
232
// Send message without payload
233
const reply = await socket.sendMessage("lobby", "ping");
234
235
// Send message with payload
236
const reply = await socket.sendMessage("chat", "new_message", {
237
text: "Hello everyone!",
238
user_id: "alice"
239
});
240
241
// Handle reply
242
if (reply.status === "ok") {
243
console.log("Message sent successfully:", reply.response);
244
} else {
245
console.error("Message failed:", reply.response);
246
}
247
248
// Error handling
249
try {
250
const reply = await socket.sendMessage("room:123", "join_request");
251
} catch (error) {
252
if (error instanceof PhoenixInvalidTopicError) {
253
console.error("Not subscribed to topic");
254
} else if (error instanceof PhoenixTimeoutError) {
255
console.error("Message timed out");
256
}
257
}
258
```
259
260
## Types
261
262
### Message Handler
263
264
```typescript { .api }
265
type TopicMessageHandler = (data: { [key: string]: any } | undefined) => void;
266
```
267
268
### Connection Status
269
270
```typescript { .api }
271
enum WebsocketStatuses {
272
Disconnected = 0,
273
Connected = 1,
274
Disconnecting = 2,
275
Reconnecting = 3,
276
}
277
```
278
279
### Topic Status
280
281
```typescript { .api }
282
enum TopicStatuses {
283
Unsubscribed = 0,
284
Leaving = 1,
285
Joining = 2,
286
Subscribed = 3,
287
}
288
```
289
290
### Logging Levels
291
292
```typescript { .api }
293
enum PhoenixWebsocketLogLevels {
294
Informative = 1,
295
Warnings = 2,
296
Errors = 3,
297
Quiet = 4,
298
}
299
```
300
301
### Server Replies
302
303
```typescript { .api }
304
type PhoenixReply = PhoenixOkReply | PhoenixErrorReply;
305
306
type PhoenixOkReply = {
307
response: { [key: string]: any } | string;
308
status: "ok";
309
};
310
311
type PhoenixErrorReply = {
312
response: { [key: string]: any } | string;
313
status: "error";
314
};
315
```
316
317
### Error Types
318
319
```typescript { .api }
320
abstract class PhoenixError extends Error {}
321
322
class PhoenixInvalidTopicError extends PhoenixError {
323
constructor(topic?: string);
324
}
325
326
class PhoenixInvalidStateError extends PhoenixError {
327
constructor();
328
}
329
330
class PhoenixConnectionError extends PhoenixError {
331
constructor(topic?: string);
332
}
333
334
class PhoenixInternalServerError extends PhoenixError {
335
constructor();
336
}
337
338
class PhoenixRespondedWithError extends PhoenixError {
339
constructor(reply?: PhoenixReply);
340
reply?: PhoenixReply;
341
}
342
343
class PhoenixDisconnectedError extends PhoenixError {
344
constructor();
345
}
346
347
class PhoenixTimeoutError extends PhoenixError {
348
constructor();
349
}
350
```
351
352
## Error Handling
353
354
The library provides comprehensive error handling with specific error types:
355
356
```typescript
357
import {
358
PhoenixInvalidTopicError,
359
PhoenixInvalidStateError,
360
PhoenixConnectionError,
361
PhoenixTimeoutError
362
} from "phoenix-websocket";
363
364
try {
365
await socket.sendMessage("nonexistent_topic", "hello");
366
} catch (error) {
367
if (error instanceof PhoenixInvalidTopicError) {
368
console.error("Topic not subscribed");
369
} else if (error instanceof PhoenixInvalidStateError) {
370
console.error("WebSocket not connected");
371
} else if (error instanceof PhoenixConnectionError) {
372
console.error("Connection error occurred");
373
} else if (error instanceof PhoenixTimeoutError) {
374
console.error("Request timed out");
375
}
376
}
377
```
378
379
## Advanced Configuration
380
381
### Logging Configuration
382
383
```typescript
384
socket.setLogLevel(PhoenixWebsocketLogLevels.Errors); // Only show errors
385
socket.setLogLevel(PhoenixWebsocketLogLevels.Quiet); // No logging
386
```
387
388
### Connection Callbacks
389
390
```typescript
391
socket.onConnectedCallback = () => {
392
// Called on initial connection and reconnections
393
console.log("WebSocket connected");
394
};
395
396
socket.onDisconnectedCallback = () => {
397
// Called when WebSocket disconnects
398
console.log("WebSocket disconnected");
399
};
400
```
401
402
### Reconnection Handling
403
404
```typescript
405
// Subscribe with reconnection handler
406
await socket.subscribeToTopic("important_topic", {}, {
407
data_update: (payload) => console.log("Update:", payload)
408
}, async (reconnectPromise) => {
409
console.log("Topic is reconnecting...");
410
try {
411
await reconnectPromise;
412
console.log("Topic successfully reconnected");
413
} catch (error) {
414
console.error("Topic reconnection failed:", error);
415
}
416
});
417
```
418
419
### Resource Cleanup
420
421
```typescript
422
// Clean up event listeners
423
socket.disposeEvents();
424
425
// Disconnect and clear all topics
426
socket.disconnect(true);
427
428
// Disconnect but keep topics for reconnection
429
socket.disconnect(false);
430
```