or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

tessl/npm-phoenix-websocket

A custom implementation of the Channels API for communicating with an Elixir/Phoenix backend via WebSockets.

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/phoenix-websocket@2.0.x

To install, run

npx @tessl/cli install tessl/npm-phoenix-websocket@2.0.0

index.mddocs/

Phoenix WebSocket

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.

Package Information

  • Package Name: phoenix-websocket
  • Package Type: npm
  • Language: TypeScript
  • Installation: npm install phoenix-websocket

Core Imports

import {
  PhoenixWebsocket,
  WebsocketStatuses,
  PhoenixWebsocketLogLevels,
  TopicStatuses,
  TopicMessageHandler,
  PhoenixError,
  PhoenixReply
} from "phoenix-websocket";

For CommonJS:

const {
  PhoenixWebsocket,
  WebsocketStatuses,
  PhoenixWebsocketLogLevels,
  TopicStatuses,
  TopicMessageHandler,
  PhoenixError,
  PhoenixReply
} = require("phoenix-websocket");

Basic Usage

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!"
});

Architecture

Phoenix WebSocket is built around several key components:

  • Connection Management: Automatic connection handling with configurable retry logic and timeout settings
  • Topic Subscription System: Manage multiple topic subscriptions with individual message handlers
  • Message Queue System: Promise-based message sending with reply handling and timeout management
  • Error Handling: Comprehensive error types for different failure scenarios
  • Reconnection Logic: Automatic reconnection with exponential backoff and online/offline detection

Capabilities

Connection Management

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

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

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");
  }
}

Types

Message Handler

type TopicMessageHandler = (data: { [key: string]: any } | undefined) => void;

Connection Status

enum WebsocketStatuses {
  Disconnected = 0,
  Connected = 1,
  Disconnecting = 2,
  Reconnecting = 3,
}

Topic Status

enum TopicStatuses {
  Unsubscribed = 0,
  Leaving = 1,
  Joining = 2,
  Subscribed = 3,
}

Logging Levels

enum PhoenixWebsocketLogLevels {
  Informative = 1,
  Warnings = 2,
  Errors = 3,
  Quiet = 4,
}

Server Replies

type PhoenixReply = PhoenixOkReply | PhoenixErrorReply;

type PhoenixOkReply = {
  response: { [key: string]: any } | string;
  status: "ok";
};

type PhoenixErrorReply = {
  response: { [key: string]: any } | string;
  status: "error";
};

Error Types

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();
}

Error Handling

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");
  }
}

Advanced Configuration

Logging Configuration

socket.setLogLevel(PhoenixWebsocketLogLevels.Errors); // Only show errors
socket.setLogLevel(PhoenixWebsocketLogLevels.Quiet);  // No logging

Connection Callbacks

socket.onConnectedCallback = () => {
  // Called on initial connection and reconnections
  console.log("WebSocket connected");
};

socket.onDisconnectedCallback = () => {
  // Called when WebSocket disconnects
  console.log("WebSocket disconnected");
};

Reconnection Handling

// 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);
  }
});

Resource Cleanup

// Clean up event listeners
socket.disposeEvents();

// Disconnect and clear all topics
socket.disconnect(true);

// Disconnect but keep topics for reconnection
socket.disconnect(false);