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 http→ws 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
| Parameter | Type | Required | Description |
|---|---|---|---|
config | ApiClientConfig | Yes | API client config (baseUrl, optional wsUrl) — use getApiConfig() |
options | WebSocketConfig | No | Connection options (token, reconnect behaviour) |
Connection Options
interface WebSocketConfig {
accessToken?: string;
autoReconnect?: boolean;
reconnectDelay?: number;
maxReconnectAttempts?: number;
}
| Option | Type | Default | Description |
|---|---|---|---|
accessToken | string | - | OAuth access token for auth |
autoReconnect | boolean | true | Auto-reconnect on disconnect |
reconnectDelay | number | 3000 | Delay between reconnects (ms) |
maxReconnectAttempts | number | 5 | Max 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
| Parameter | Type | Required | Description |
|---|---|---|---|
config | ApiClientConfig | Yes | API client config — use getApiConfig() |
options | UseWebSocketOptions | No | Connection 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;
}
Account Link Messages
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>
);
}