Arcwayv0.3.0

WebSocket Real-Time

Real-time WebSocket support with Socket.IO

Arcway supports real-time updates over WebSocket using Socket.IO. Add a ws() function to any GET route to make it real-time — clients using useApi() automatically subscribe and receive live updates.

How It Works

  1. Arcway manages a single WebSocket connection per client at /ws (configurable)
  2. Routes with a ws() function on their GET export become real-time
  3. useApi() auto-subscribes when the provider has a wsUrl configured
  4. Mutations (post, put, del) go over WebSocket when connected, with HTTP fallback

No new concepts to learn. You write REST handlers — adding ws() makes them real-time.

Backend: Adding Real-Time to a Route

Any route can become real-time by adding an async ws() function to its GET export:

// api/chat/rooms/[id].js
export const GET = {
  handler: async (ctx) => {
    const messages = await ctx.db('messages').where('room_id', ctx.req.query.id);
    return { data: messages };
  },

  // Presence of ws() enables WebSocket subscriptions on this route.
  // Called when a client subscribes. Return a cleanup function (like useEffect).
  async ws(ctx) {
    // Optional: set up event listeners, timers, etc.
    // The cleanup function runs when the client unsubscribes or disconnects.
  },
};

export const POST = {
  handler: async (ctx) => {
    const { text } = ctx.req.body;
    const [id] = await ctx.db('messages').insert({ room_id: ctx.req.query.id, text });
    const message = await ctx.db('messages').where('id', id).first();
    return { status: 201, data: message };
    // Arcway automatically broadcasts this response to all other subscribers
  },
};

The ws() Function

  • Receives: A ctx object with req (same as other handlers — req.query, req.session, req.headers, etc., plus req.socketId identifying the WebSocket connection) and infrastructure services (db, log, events, etc.)
  • Returns: An optional cleanup function called when the client unsubscribes or disconnects
  • Lifecycle: ws() runs first, then Arcway automatically calls handler() and sends the initial data to the subscriber

Automatic Broadcasting

When a client sends a mutation (POST, PUT, PATCH, DELETE) over WebSocket, Arcway automatically broadcasts the handler's response to all other clients subscribed to the same path. No extra code needed — just return { data } from your handler and all subscribers get the update.

Server-Initiated Push (Advanced)

For cases where updates originate from the server (not from a client request), Arcway provides two helper functions:

import { wsBroadcastToPath, wsSendToSocket } from 'arcway';

// Push to ALL subscribers of a path (e.g. from a cron job or event listener)
wsBroadcastToPath('/chat/rooms/123', { data: updatedMessages });

// Push to ONE specific client (e.g. notify a user their background job finished)
wsSendToSocket(ctx.req.socketId, '/chat/rooms/123', { data: personalNotification });

These are optional — most apps only need the automatic handler-response broadcasting.

Frontend: Using Real-Time Routes

Provider Setup

Add wsUrl to your Provider:

import { Provider } from 'arcway/lib/client';

function App() {
  return (
    <Provider pathPrefix="/api" wsUrl="ws://localhost:3000/ws">
      <ChatRoom roomId="123" />
    </Provider>
  );
}

useApi with Real-Time

useApi automatically subscribes via WebSocket when the route supports it:

import { useApi } from 'arcway/lib/client';

function ChatRoom({ roomId }) {
  // Auto-subscribes to /api/chat/rooms/123 via WebSocket
  // data updates in real-time when the server pushes changes
  const { data, post, del, loading } = useApi(`/chat/rooms/${roomId}`);

  // Mutations go over WebSocket (falls back to HTTP if disconnected)
  const sendMessage = () => post({ text: 'hello' });
  const deleteMessage = (id) => del({ messageId: id });

  if (loading) return <p>Loading...</p>;
  return (
    <div>
      {data?.map(msg => <p key={msg.id}>{msg.text}</p>)}
      <button onClick={sendMessage}>Send</button>
    </div>
  );
}

Disabling WebSocket

Disable WebSocket for a specific hook:

// This hook uses HTTP only, no WebSocket subscription
const { data } = useApi('/stats', undefined, { ws: false });

Mutation Methods

useApi returns mutation helpers that route over WebSocket when available:

MethodHTTP equivalent
post(body)POST
put(body)PUT
patch(body)PATCH
del(body)DELETE

All return promises and throw ApiError on failure — same behavior as HTTP requests.

Wire Protocol

The WebSocket uses Socket.IO with JSON messages:

Client → Server (via msg event):

{ "path": "/chat/rooms/123", "method": "SUBSCRIBE" }
{ "path": "/chat/rooms/123", "method": "UNSUBSCRIBE" }
{ "path": "/chat/rooms/123", "method": "POST", "body": { "text": "hello" }, "id": "1" }
{ "path": "/chat/rooms/123", "method": "GET", "id": "2" }

Server → Client (via msg event):

{ "path": "/ws", "data": { "socketId": "uuid" }, "status": 200 }
{ "path": "/chat/rooms/123", "data": [...], "id": "1" }
{ "path": "/chat/rooms/123", "error": { "code": "NOT_FOUND", "message": "..." }, "status": 404 }

Methods: GET, POST, PUT, PATCH, DELETE, SUBSCRIBE, UNSUBSCRIBE.

The id field is optional — used to correlate request/response pairs.

Configuration

WebSocket is automatically enabled when any route has a ws() function. No configuration needed.

// arcway.config.js
export default {
  websocket: {
    enabled: false,        // Disable WebSocket entirely (default: true when routes have ws())
    path: '/realtime',     // Custom endpoint path (default: '/ws')
    pingIntervalMs: 30000, // Ping interval for dead client detection (default: 30s, 0 to disable)
    driver: 'memory',      // 'memory' (default) | 'redis' — see "Cluster mode" below
  },
};

Cluster Mode

By default, wsBroadcastToPath and wsSendToSocket only reach clients connected to the same worker process. In a multi-process deployment (PM2 cluster, Node cluster module), each worker has its own in-memory registry, so a broadcast from worker A won't reach clients on worker B.

To fix this, set websocket.driver: 'redis'. Arcway will use a Redis pub/sub channel as a backplane — every broadcast or targeted send is published to Redis and forwarded to all workers, which deliver it to their local clients.

Enabling the Redis backplane

// arcway.config.js
export default {
  redis: {
    url: process.env.REDIS_URL, // e.g. redis://localhost:6379
  },
  websocket: {
    driver: 'redis', // enable cross-worker broadcasts
  },
};

That's it. The wsBroadcastToPath and wsSendToSocket APIs are unchanged — no application code needs to change.

What happens without the backplane

ScenarioBehaviour
driver: 'memory' (default), single processFull delivery — all subscribers reached
driver: 'memory', multiple workers detectedWarning logged at boot; broadcasts only reach clients on the same worker
driver: 'redis', Redis availableFull cross-worker delivery via pub/sub backplane
driver: 'redis', no Redis configured or unreachableWarning logged at boot; falls back to local fan-out (no crash)

Arcway detects a likely cluster environment by checking process.env.pm_id (PM2), process.env.NODE_APP_INSTANCE (PM2/other managers), and cluster.isWorker (Node cluster). It warns you at startup if you're running in cluster mode with driver: 'memory'.

Self-filter

When using the Redis backplane, each worker tags its published messages with a unique worker ID. Workers ignore messages they published themselves, preventing double-delivery to local subscribers.

PM2 cluster example

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: 'npx',
    args: 'arcway start',
    instances: 4,       // 4 workers — broadcasts need driver: 'redis'
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      REDIS_URL: 'redis://localhost:6379',
    },
  }],
};
// arcway.config.js
export default {
  redis: { url: process.env.REDIS_URL },
  websocket: { driver: 'redis' },
};

Dead Client Detection

Arcway pings all connected clients at the configured interval (default: 30 seconds). Clients that don't respond with a pong are terminated. Socket.IO handles pong responses automatically — no client-side code needed.

Auto-Reconnection

The client-side WsManager automatically reconnects with exponential backoff (1s, 2s, 4s, ... up to 30s). After reconnecting, it re-subscribes to all active subscriptions. No manual handling needed — useApi hooks seamlessly resume receiving updates.

How It Fits Together

┌─────────────────────────────────────────────────────┐
│  Client                                             │
│                                                     │
│  useApi('/chat/rooms/123')                          │
│    ├── GET /chat/rooms/123 (HTTP, initial fetch)    │
│    ├── SUBSCRIBE /chat/rooms/123 (WebSocket)        │
│    │   └── receives real-time data pushes           │
│    ├── post({text}) → POST over WS (or HTTP)        │
│    └── unmount → UNSUBSCRIBE (WebSocket)            │
│                                                     │
├─────────────────────────────────────────────────────┤
│  Arcway Server                                      │
│                                                     │
│  /ws endpoint (single WebSocket per client)         │
│    ├── SUBSCRIBE → ws(ctx) → handler(ctx)           │
│    ├── UNSUBSCRIBE → cleanup function               │
│    ├── POST/PUT/DELETE → route to handler            │
│    └── broadcast/send → push to subscribers          │
│                                                     │
│  api/chat/rooms/[id].js                             │
│    ├── GET.handler(ctx) → { data }                  │
│    ├── GET.ws(ctx) → setup + cleanup                │
│    └── POST.handler(ctx) → { data } + broadcast     │
└─────────────────────────────────────────────────────┘

On this page