Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

WebSocket Subscriptions

The SDK provides real-time event subscriptions via WebSocket. Subscribe to game events for live leaderboards, activity feeds, and instant notifications.

Architecture

Events flow from the blockchain through a multi-layered pipeline:

Blockchain Event (e.g. score update)

Apibara Indexer → writes to PostgreSQL

PostgreSQL LISTEN/NOTIFY triggers

API Server broadcasts to subscribed WebSocket clients

denshokan-sdk WebSocketManager

Event mappers (snake_case → camelCase)

React hooks / callback handlers

The API server uses PostgreSQL triggers on the tokens, games, and minters tables. When data changes, triggers fire NOTIFY on named channels, and the API server broadcasts to all subscribed WebSocket clients.

Channels

ChannelEvent TypeDescription
tokensTokenUpdateEventAny token state change (score update, game over, or mint)
scoresScoreEventWhen a token's score changes
game_overGameOverEventWhen a game session ends
mintsMintEventWhen new game tokens are minted
gamesNewGameEventWhen a new game is registered
mintersNewMinterEventWhen a new minter is registered
settingsNewSettingEventWhen a new game setting is created
objectivesNewObjectiveEventWhen a new game objective is created

Event Payloads

Each channel delivers a typed event. The SDK automatically maps snake_case server data to camelCase TypeScript types.

interface ScoreEvent {
  tokenId: string;
  gameId: number;
  score: number;
  ownerAddress: string;
  playerName: string;
}
 
interface GameOverEvent {
  tokenId: string;
  gameId: number;
  score: number;
  ownerAddress: string;
  playerName: string;
  completedAllObjectives: boolean;
}
 
interface MintEvent {
  tokenId: string;
  gameId: number;
  ownerAddress: string;
  mintedBy: string;
  settingsId: number;
}
 
// TokenUpdateEvent is polymorphic — it aggregates all token-related events
interface TokenUpdateEvent {
  type: "scoreUpdate" | "gameOver" | "minted";
  tokenId: string;
  gameId: number;
  score?: number;
  ownerAddress?: string;
}
 
interface NewGameEvent {
  gameId: number;
  contractAddress: string;
  name: string;
}
 
interface NewMinterEvent {
  minterId: string;
  contractAddress: string;
  name: string;
  blockNumber: string;
}
 
interface NewSettingEvent {
  gameAddress: string;
  settingsId: number;
  creatorAddress: string;
  settingsData: string | null;
}
 
interface NewObjectiveEvent {
  gameAddress: string;
  objectiveId: number;
  settingsId: number;
  creatorAddress: string;
  objectiveData: string | null;
}

All types are exported from the SDK:

import type {
  WSChannel,
  WSMessage,
  ScoreEvent,
  GameOverEvent,
  MintEvent,
  TokenUpdateEvent,
  NewGameEvent,
  NewMinterEvent,
  NewSettingEvent,
  NewObjectiveEvent,
  WSChannelPayloadMap,
} from "@provable-games/denshokan-sdk";

Using the Client

import { DenshokanClient } from "@provable-games/denshokan-sdk";
 
const client = new DenshokanClient({ chain: "mainnet" });
 
// Subscribe to score updates for game ID 1
const unsubscribe = client.subscribe(
  {
    channels: ["scores", "game_over"],
    gameIds: [1],
  },
  (message) => {
    console.log(`Channel: ${message.channel}`);
    console.log(`Data:`, message.data);
  },
);
 
// Later: unsubscribe
unsubscribe();
 
// Cleanup all connections
client.disconnect();

Typed React Hooks (Recommended)

The SDK provides eight typed hooks — one per channel. Each returns the last event, a buffered event history, and connection state.

useScoreUpdates

import { useScoreUpdates } from "@provable-games/denshokan-sdk/react";
 
function LiveScores({ gameId }: { gameId: number }) {
  const { lastEvent, events, isConnected } = useScoreUpdates({
    gameIds: [gameId],
    bufferSize: 100,
  });
 
  return (
    <div>
      <p>Connected: {isConnected ? "Yes" : "No"}</p>
      <p>Latest: {lastEvent?.playerName} scored {lastEvent?.score}</p>
      <ul>
        {events.map((e, i) => (
          <li key={i}>{e.playerName}: {e.score}</li>
        ))}
      </ul>
    </div>
  );
}

All Typed Hooks

HookChannelPayload
useScoreUpdatesscoresScoreEvent
useGameOverEventsgame_overGameOverEvent
useMintEventsmintsMintEvent
useTokenUpdatestokensTokenUpdateEvent
useNewGamesgamesNewGameEvent
useNewMintersmintersNewMinterEvent
useNewSettingssettingsNewSettingEvent
useNewObjectivesobjectivesNewObjectiveEvent

Hook Options

interface UseChannelOptions<C extends WSChannel> {
  gameIds?: number[];       // Filter to specific game IDs (server-side)
  bufferSize?: number;      // Max events to keep in memory (default: 50)
  enabled?: boolean;        // Enable/disable the subscription (default: true)
  onEvent?: (event: WSChannelPayloadMap[C]) => void;  // Callback per event
}

Hook Return Value

interface UseChannelResult<C extends WSChannel> {
  lastEvent: WSChannelPayloadMap[C] | null;  // Most recent event
  events: WSChannelPayloadMap[C][];          // Buffered event history
  isConnected: boolean;                       // WebSocket connection state
  clear: () => void;                          // Clear event buffer
}

useChannelSubscription (Generic)

All typed hooks wrap useChannelSubscription. You can use it directly for type-safe access to any channel:

import { useChannelSubscription } from "@provable-games/denshokan-sdk/react";
 
const { lastEvent, events, isConnected } = useChannelSubscription("scores", {
  gameIds: [1, 2],
  bufferSize: 25,
  onEvent: (event) => {
    console.log("New score:", event.score);
  },
});

Low-Level Hook

For raw message access, use useSubscription:

import { useSubscription } from "@provable-games/denshokan-sdk/react";
 
function LiveLeaderboard({ gameId }: { gameId: number }) {
  const [scores, setScores] = useState<Map<string, number>>(new Map());
 
  useSubscription(
    ["scores", "game_over"],
    (message) => {
      if (message.channel === "scores") {
        const data = message.data as ScoreEvent;
        setScores(prev => new Map(prev).set(data.tokenId, data.score));
      }
    },
    [gameId],
  );
 
  return (
    <ul>
      {[...scores.entries()]
        .sort(([, a], [, b]) => b - a)
        .map(([id, score]) => (
          <li key={id}>Token {id}: {score}</li>
        ))}
    </ul>
  );
}

useSubscription Signature

function useSubscription(
  channels: WSChannel[],
  handler: WSEventHandler,    // (message: WSMessage) => void
  gameIds?: number[],
): void;

The hook manages connection lifecycle automatically:

  • Connects when the component mounts
  • Re-subscribes if channels or gameIds change
  • Disconnects when the component unmounts

Connection Status

Monitor WebSocket connection state in your UI:

import { useConnectionStatus } from "@provable-games/denshokan-sdk/react";
 
function ConnectionBadge() {
  const { isConnected } = useConnectionStatus();
 
  return (
    <span style={{ color: isConnected ? "green" : "red" }}>
      {isConnected ? "Live" : "Reconnecting..."}
    </span>
  );
}

For programmatic use outside React:

// Check current status
const connected = client.wsConnected;
 
// Listen for changes
const unsub = client.onWsConnectionChange((connected) => {
  console.log("WebSocket:", connected ? "connected" : "disconnected");
});

Message Format

interface WSMessage {
  channel: string;     // "scores", "game_over", "tokens", "mints", "games", "minters", "settings", "objectives"
  data: unknown;       // Channel-specific payload (use typed hooks for automatic typing)
  _timing?: {
    serverTs: number;  // Server timestamp for latency measurement
  };
}

Connection Management

The WebSocket manager handles:

  • Auto-reconnect with exponential backoff (1s, 2s, 4s, ... capped at 30s)
  • Max 10 reconnect attempts before giving up
  • Re-subscription on reconnect (all active subscriptions are restored)
  • Smart retry — only attempts reconnection if active subscriptions exist

Manual Connection

// Connect manually (usually not needed - subscribe() auto-connects)
client.connect();
 
// Check connection status
const connected = client.wsConnected;
 
// Disconnect
client.disconnect();

Combining with Data Hooks

A common pattern is to use data hooks for initial load and typed subscription hooks for live updates:

function GameDashboard({ gameAddress }: { gameAddress: string }) {
  const { data: tokens, refetch } = useTokens({
    gameAddress,
    gameOver: false,
    limit: 50,
  });
 
  // Automatically refetch when a game completes
  useGameOverEvents({
    onEvent: () => refetch(),
  });
 
  // Show live score updates
  const { lastEvent: latestScore } = useScoreUpdates();
 
  return (
    <div>
      {latestScore && (
        <p>Latest: {latestScore.playerName} scored {latestScore.score}</p>
      )}
      <TokenList tokens={tokens?.data ?? []} />
    </div>
  );
}

This gives you:

  • Instant initial render from the API
  • Automatic list refresh when games complete
  • Live score ticker from WebSocket