Skip to main content

WebSocket Client

The WebSocket client provides real-time communication with the Heimdall API. It's designed for client-side use only.

createWebSocket

Create a WebSocket connection with auto-reconnect support. The first argument is the ApiClientConfig — the connection URL is derived from it (config.wsUrl, falling back to config.baseUrl with httpws and /v1/ws). There is no longer a separate endpoint argument.

import { getApiConfig, createWebSocket } from "@/lib/api";

const ws = createWebSocket(getApiConfig(), {
accessToken: session.accessToken,
});

Parameters

ParameterTypeRequiredDescription
configApiClientConfigYesAPI client config (baseUrl, optional wsUrl) — use getApiConfig()
optionsWebSocketConfigNoConnection options (token, reconnect behaviour)

Connection Options

interface WebSocketConfig {
accessToken?: string;
autoReconnect?: boolean;
reconnectDelay?: number;
maxReconnectAttempts?: number;
}
OptionTypeDefaultDescription
accessTokenstring-OAuth access token for auth
autoReconnectbooleantrueAuto-reconnect on disconnect
reconnectDelaynumber3000Delay between reconnects (ms)
maxReconnectAttemptsnumber5Max reconnection attempts

Returns

interface WebSocketClient {
socket: WebSocket;
send: (data: unknown) => boolean;
readonly isOpen: boolean;
close: () => void;
onOpen: (handler: () => void) => void;
onMessage: (handler: (data: unknown) => void) => void;
onError: (handler: (error: Event) => void) => void;
onClose: (handler: (event: CloseEvent) => void) => void;
}

send returns true if the message was sent (socket open), false otherwise. Use the isOpen getter to check connection state, and onOpen to run logic once the connection is established.

Example

const ws = createWebSocket(getApiConfig(), {
accessToken: session.accessToken,
autoReconnect: true,
reconnectDelay: 5000,
maxReconnectAttempts: 3,
});

ws.onMessage((data) => {
console.log("Received:", data);
});

ws.onError((error) => {
console.error("WebSocket error:", error);
});

ws.onClose((event) => {
console.log("Connection closed:", event.code);
});

// Send a message
ws.send({ type: "subscribe", channel: "updates" });

// Close when done
ws.close();

useWebSocket Hook

React hook for WebSocket connections with state management. Like createWebSocket, its first argument is the ApiClientConfig — pass getApiConfig().

import { getApiConfig, useWebSocket } from "@/lib/api";

function LiveUpdates() {
const { isConnected, lastMessage, send, connect, disconnect } = useWebSocket(
getApiConfig(),
{ accessToken: session.accessToken }
);

return (
<div>
<p>Status: {isConnected ? "Connected" : "Disconnected"}</p>
<button onClick={() => send({ type: "ping" })}>Ping</button>
</div>
);
}

Parameters

ParameterTypeRequiredDescription
configApiClientConfigYesAPI client config — use getApiConfig()
optionsUseWebSocketOptionsNoConnection options (extends WebSocketConfig with immediate)

Options

interface UseWebSocketOptions extends WebSocketConfig {
immediate?: boolean; // Connect immediately (default: true)
}

Returns

interface UseWebSocketReturn {
isConnected: boolean;
lastMessage: unknown;
send: (data: unknown) => void;
connect: () => void;
disconnect: () => void;
}

Example with Message Handling

function NotificationListener() {
const { isConnected, lastMessage } = useWebSocket(getApiConfig(), {
accessToken: session.accessToken,
});

useEffect(() => {
if (lastMessage) {
const msg = lastMessage as { type: string; payload: unknown };

switch (msg.type) {
case "notification":
showNotification(msg.payload);
break;
case "update":
refreshData();
break;
}
}
}, [lastMessage]);

return <ConnectionStatus connected={isConnected} />;
}

Server Message Types

The library includes TypeScript types for all server-sent WebSocket messages.

User Status Messages

Sent when user account status changes:

import type { UserStatusMessage } from "@/lib/api";

// Account deleted
interface AccountDeletedMessage {
type: "accountDeleted";
userId: string;
reason?: string;
timestamp: string;
}

// Account banned
interface AccountBannedMessage {
type: "accountBanned";
userId: string;
reason?: string;
expiresAt?: string;
isPermanent: boolean;
timestamp: string;
}

// Session revoked
interface SessionRevokedMessage {
type: "sessionRevoked";
userId: string;
sessionId?: string;
reason?: string;
timestamp: string;
}

// Force logout
interface ForceLogoutMessage {
type: "forceLogout";
userId: string;
reason?: string;
timestamp: string;
}

Permission Messages

Sent when user permissions change:

import type { PermissionMessage } from "@/lib/api";

// Roles updated
interface RolesUpdatedMessage {
type: "rolesUpdated";
userId: string;
/** Role names (e.g., "Admin", "Developer") - for display */
roles: string[];
/** Role IDs (e.g., "role_admin", "role_developer") - for programmatic checks */
roleIds: string[];
timestamp: string;
}

// Permissions updated
interface PermissionsUpdatedMessage {
type: "permissionsUpdated";
userId: string;
permissions: string[];
timestamp: string;
}

// Role permissions changed (system-wide)
interface RolePermissionsChangedMessage {
type: "rolePermissionsChanged";
roleId: string;
roleName: string;
timestamp: string;
}

Sent when account linking events occur:

import type { AccountLinkMessage } from "@/lib/api";

// Email link verified
interface EmailLinkVerifiedMessage {
type: "emailLinkVerified";
userId: string;
email: string;
platformAccountId: string;
timestamp: string;
}

// Email change verified
interface EmailChangeVerifiedMessage {
type: "emailChangeVerified";
userId: string;
oldEmail: string;
newEmail: string;
timestamp: string;
}

OAuth & GPS Messages

Sent for OAuth consent changes and GPS/AIS/geofence tracking:

import type { OAuthConsentRevokedMessage, ServerMessage } from "@/lib/api";

// OAuth consent revoked for a client app
interface OAuthConsentRevokedMessage {
type: "oAuthConsentRevoked";
userId: string;
clientId: string;
clientName: string;
timestamp: string;
}

// The GPS messages are part of the exported `ServerMessage` union but are NOT
// exported as standalone names. Narrow them by switching on `type`:
// - "gpsUpdate" (GpsUpdateMessage) new GPS data point for a device
// - "vesselsUpdate" (VesselsUpdateMessage) current AIS vessel positions
// - "geofenceEvent" (GeofenceEventMessage) device entered/exited a geofence

Handling Server Messages

import type { ServerMessage } from "@/lib/api";

ws.onMessage((data: unknown) => {
const message = data as ServerMessage;

switch (message.type) {
case "accountBanned":
handleBan(message);
break;
case "forceLogout":
signOut();
break;
case "rolesUpdated":
refreshPermissions();
break;
case "emailLinkVerified":
refreshAccounts();
break;
case "oAuthConsentRevoked":
refreshConnectedApps();
break;
case "gpsUpdate":
updateDevicePosition(message.data);
break;
case "vesselsUpdate":
updateVessels(message.data.vessels);
break;
case "geofenceEvent":
handleGeofenceEvent(message.data);
break;
}
});

Best Practices

Cleanup on Unmount

Always close WebSocket connections when components unmount:

useEffect(() => {
const ws = createWebSocket(getApiConfig(), { accessToken });

ws.onMessage(handleMessage);

return () => {
ws.close(); // Important: cleanup
};
}, [accessToken]);

The useWebSocket hook handles this automatically.

Reconnection Handling

The client auto-reconnects by default. Customize behavior:

const ws = createWebSocket(getApiConfig(), {
accessToken,
autoReconnect: true,
reconnectDelay: 5000, // Wait 5s between attempts
maxReconnectAttempts: 10, // Give up after 10 attempts
});

Connection State UI

function ConnectionIndicator() {
const { isConnected } = useWebSocket(getApiConfig(), { accessToken });

return (
<div className={isConnected ? "text-green-500" : "text-red-500"}>
{isConnected ? "Connected" : "Reconnecting..."}
</div>
);
}