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.0Phoenix 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.
npm install phoenix-websocketimport {
PhoenixWebsocket,
WebsocketStatuses,
PhoenixWebsocketLogLevels,
TopicStatuses,
TopicMessageHandler,
PhoenixError,
PhoenixReply
} from "phoenix-websocket";For CommonJS:
const {
PhoenixWebsocket,
WebsocketStatuses,
PhoenixWebsocketLogLevels,
TopicStatuses,
TopicMessageHandler,
PhoenixError,
PhoenixReply
} = require("phoenix-websocket");import { PhoenixWebsocket } from "phoenix-websocket";
// Create connection instance
const socket = new PhoenixWebsocket("wss://example.io/channel-endpoint", {
token: "auth-token"
});
// Set up callbacks
socket.onConnectedCallback = () => {
console.log("Connected to Phoenix server");
};
socket.onDisconnectedCallback = () => {
console.log("Disconnected from Phoenix server");
};
// Connect and subscribe to a topic
await socket.connect();
await socket.subscribeToTopic("lobby", { user_id: "123" }, {
user_joined: (payload) => console.log("User joined:", payload),
user_left: (payload) => console.log("User left:", payload),
});
// Send a message
const reply = await socket.sendMessage("lobby", "new_message", {
text: "Hello, world!"
});Phoenix WebSocket is built around several key components:
Core connection functionality for establishing and maintaining WebSocket connections to Phoenix servers.
class PhoenixWebsocket {
constructor(
url: string,
queryParams?: { [key: string]: string },
timeoutInMs?: number
);
connect(): Promise<void>;
disconnect(clearTopics?: boolean): void;
get connectionStatus(): WebsocketStatuses;
get subscribedTopics(): string[];
onConnectedCallback?: (() => void) | undefined;
onDisconnectedCallback?: (() => void) | undefined;
setLogLevel(logLevel: PhoenixWebsocketLogLevels): void;
disposeEvents(): void;
}Usage Examples:
// Basic connection
const socket = new PhoenixWebsocket("wss://localhost:4000/socket");
await socket.connect();
// With query parameters and timeout
const socket = new PhoenixWebsocket(
"wss://example.com/socket",
{ token: "abc123", user: "alice" },
30000 // 30 second timeout
);
// Check connection status
if (socket.connectionStatus === WebsocketStatuses.Connected) {
console.log("Socket is connected");
}
// Clean disconnect (removes all topics)
socket.disconnect(true);Topic subscription system for joining Phoenix channels and handling server messages.
type TopicMessageHandler = (data: { [key: string]: any } | undefined) => void;
// Object-based message handlers
subscribeToTopic(
topic: string,
payload?: { [key: string]: any },
messageHandlers?: { [message: string]: TopicMessageHandler },
reconnectHandler?: (reconnectPromise: Promise<void>) => void
): Promise<void>;
// Map-based message handlers
subscribeToTopic(
topic: string,
payload?: { [key: string]: any },
messageHandlers?: Map<string, TopicMessageHandler>,
reconnectHandler?: (reconnectPromise: Promise<void>) => void
): Promise<void>;
unsubscribeToTopic(topic: string): void;Usage Examples:
// Simple subscription without message handlers
await socket.subscribeToTopic("lobby");
// With join payload
await socket.subscribeToTopic("room:123", { user_id: "alice" });
// With message handlers
await socket.subscribeToTopic("chat", { user: "alice" }, {
user_joined: (payload) => {
console.log(`${payload.username} joined the chat`);
},
user_left: (payload) => {
console.log(`${payload.username} left the chat`);
},
new_message: (payload) => {
console.log(`Message: ${payload.text}`);
}
});
// With Map-based handlers
const handlers = new Map([
["user_joined", (data) => console.log("User joined:", data)],
["user_left", (data) => console.log("User left:", data)]
]);
await socket.subscribeToTopic("lobby", {}, handlers);
// With reconnect handler for error recovery
await socket.subscribeToTopic("critical_topic", { userId: "123" }, {
error: (payload) => console.error("Topic error:", payload)
}, (reconnectPromise) => {
// Called on rejoin attempts (NOT initial join)
console.log("Topic reconnecting...");
reconnectPromise.catch((error) => {
if (error instanceof PhoenixRespondedWithError) {
if (error.reply?.response?.reason === "Invalid User") {
console.error("Invalid user on reconnect");
}
}
});
});
// Handle join errors
try {
await socket.subscribeToTopic("restricted_topic", { userId: "123" });
} catch (error) {
if (error instanceof PhoenixRespondedWithError) {
if (error.reply?.response?.reason === "Invalid User") {
console.error("Access denied: Invalid user");
}
}
}
// Unsubscribe from topic
socket.unsubscribeToTopic("lobby");Message sending functionality for communicating with Phoenix channels.
sendMessage(
topic: string,
message: string,
payload?: { [key: string]: any }
): Promise<PhoenixReply>;Usage Examples:
// Send message without payload
const reply = await socket.sendMessage("lobby", "ping");
// Send message with payload
const reply = await socket.sendMessage("chat", "new_message", {
text: "Hello everyone!",
user_id: "alice"
});
// Handle reply
if (reply.status === "ok") {
console.log("Message sent successfully:", reply.response);
} else {
console.error("Message failed:", reply.response);
}
// Error handling
try {
const reply = await socket.sendMessage("room:123", "join_request");
} catch (error) {
if (error instanceof PhoenixInvalidTopicError) {
console.error("Not subscribed to topic");
} else if (error instanceof PhoenixTimeoutError) {
console.error("Message timed out");
}
}type TopicMessageHandler = (data: { [key: string]: any } | undefined) => void;enum WebsocketStatuses {
Disconnected = 0,
Connected = 1,
Disconnecting = 2,
Reconnecting = 3,
}enum TopicStatuses {
Unsubscribed = 0,
Leaving = 1,
Joining = 2,
Subscribed = 3,
}enum PhoenixWebsocketLogLevels {
Informative = 1,
Warnings = 2,
Errors = 3,
Quiet = 4,
}type PhoenixReply = PhoenixOkReply | PhoenixErrorReply;
type PhoenixOkReply = {
response: { [key: string]: any } | string;
status: "ok";
};
type PhoenixErrorReply = {
response: { [key: string]: any } | string;
status: "error";
};abstract class PhoenixError extends Error {}
class PhoenixInvalidTopicError extends PhoenixError {
constructor(topic?: string);
}
class PhoenixInvalidStateError extends PhoenixError {
constructor();
}
class PhoenixConnectionError extends PhoenixError {
constructor(topic?: string);
}
class PhoenixInternalServerError extends PhoenixError {
constructor();
}
class PhoenixRespondedWithError extends PhoenixError {
constructor(reply?: PhoenixReply);
reply?: PhoenixReply;
}
class PhoenixDisconnectedError extends PhoenixError {
constructor();
}
class PhoenixTimeoutError extends PhoenixError {
constructor();
}The library provides comprehensive error handling with specific error types:
import {
PhoenixInvalidTopicError,
PhoenixInvalidStateError,
PhoenixConnectionError,
PhoenixTimeoutError
} from "phoenix-websocket";
try {
await socket.sendMessage("nonexistent_topic", "hello");
} catch (error) {
if (error instanceof PhoenixInvalidTopicError) {
console.error("Topic not subscribed");
} else if (error instanceof PhoenixInvalidStateError) {
console.error("WebSocket not connected");
} else if (error instanceof PhoenixConnectionError) {
console.error("Connection error occurred");
} else if (error instanceof PhoenixTimeoutError) {
console.error("Request timed out");
}
}socket.setLogLevel(PhoenixWebsocketLogLevels.Errors); // Only show errors
socket.setLogLevel(PhoenixWebsocketLogLevels.Quiet); // No loggingsocket.onConnectedCallback = () => {
// Called on initial connection and reconnections
console.log("WebSocket connected");
};
socket.onDisconnectedCallback = () => {
// Called when WebSocket disconnects
console.log("WebSocket disconnected");
};// Subscribe with reconnection handler
await socket.subscribeToTopic("important_topic", {}, {
data_update: (payload) => console.log("Update:", payload)
}, async (reconnectPromise) => {
console.log("Topic is reconnecting...");
try {
await reconnectPromise;
console.log("Topic successfully reconnected");
} catch (error) {
console.error("Topic reconnection failed:", error);
}
});// Clean up event listeners
socket.disposeEvents();
// Disconnect and clear all topics
socket.disconnect(true);
// Disconnect but keep topics for reconnection
socket.disconnect(false);