All skills

WebSocket and realtime communication with sails-hook-sockets for Sails.js applications. Use this skill when building chat, notifications, live updates, presence tracking, or any feature that requires persistent bidirectional communication between server and client.

Use this Skill: https://skilld.dev/gh/sailscastshq/boring-stack/realtime

This session only. Nothing lands on disk.

rulesclient-side.md

≈2.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Client-Side Integration

The sails.io.js client library provides the browser-side SDK for connecting to the Sails WebSocket server. It auto-connects on load and exposes methods that mirror HTTP verbs.

Including sails.io.js

Script Tag

<!-- Load Socket.IO client first -->
<script src="/dependencies/socket.io.min.js"></script>
<!-- Then load sails.io.js -->
<script src="/dependencies/sails.io.js"></script>

With Inertia.js / Module Bundler

Place sails.io.js in assets/dependencies/ and import it in your app entry:

// assets/js/app.js
import '../dependencies/sails.io.js'

Or load it via a script tag in your layout template so it's available globally.

io.sails — Global Configuration

Configure the socket connection before it auto-connects:

<script>
  // Must be set BEFORE sails.io.js loads (or before auto-connect fires)
  io.sails.url = 'https://api.example.com'
  io.sails.autoConnect = true // Default: true
  io.sails.reconnection = true // Default: true
  io.sails.environment = 'production' // Default: inferred from URL
  io.sails.transports = ['websocket'] // Default: ['websocket']
  io.sails.headers = {
    // Custom headers sent with handshake
    'x-custom-header': 'value'
  }
</script>

Configuration Options

Option Default Description
url undefined (same origin) Server URL to connect to
autoConnect true Auto-connect when sails.io.js loads
reconnection true Auto-reconnect on disconnect
transports ['websocket'] Transport methods to use
environment inferred 'production' or 'development'
headers {} Custom headers for handshake

io.socket — Virtual Request Methods

Once connected, io.socket provides methods that send WebSocket messages routed through the Sails router as virtual HTTP requests.

io.socket.get(url, [data], callback)

io.socket.get('/api/users', (data, jwr) => {
  console.log('Users:', data)
  console.log('Status:', jwr.statusCode)
})

// With query parameters
io.socket.get('/api/users', { status: 'active', limit: 10 }, (data, jwr) => {
  console.log('Active users:', data)
})

io.socket.post(url, [data], callback)

io.socket.post(
  '/api/messages',
  {
    text: 'Hello!',
    room: 'general'
  },
  (data, jwr) => {
    if (jwr.statusCode === 200) {
      console.log('Message sent:', data)
    }
  }
)

io.socket.put(url, [data], callback)

io.socket.put(
  '/api/users/42',
  {
    fullName: 'Jane Smith'
  },
  (data, jwr) => {
    console.log('Updated:', data)
  }
)

io.socket.patch(url, [data], callback)

io.socket.patch(
  '/api/users/42',
  {
    status: 'away'
  },
  (data, jwr) => {
    console.log('Patched:', data)
  }
)

io.socket.delete(url, [data], callback)

io.socket.delete('/api/messages/99', (data, jwr) => {
  if (jwr.statusCode === 200) {
    console.log('Deleted')
  }
})

io.socket.request(options, callback)

Low-level method for custom requests:

io.socket.request(
  {
    method: 'post',
    url: '/api/messages',
    data: { text: 'Hello' },
    headers: { 'x-custom': 'value' }
  },
  (data, jwr) => {
    console.log(data)
  }
)

JWR (JSON WebSocket Response)

All callback functions receive (data, jwr) where jwr is the JSON WebSocket Response:

io.socket.get('/api/users/42', (data, jwr) => {
  jwr.body // Response body (same as `data`)
  jwr.statusCode // HTTP status code (200, 404, 500, etc.)
  jwr.headers // Response headers
})

Error Handling

io.socket.get('/api/users/42', (data, jwr) => {
  if (jwr.statusCode >= 400) {
    console.error('Error:', jwr.statusCode, data)
    return
  }
  // Handle success
})

Event Listeners

io.socket.on(eventName, handler)

Listen for server-pushed events:

// Listen for custom events from sails.sockets.broadcast()
io.socket.on('newMessage', (data) => {
  console.log('New message:', data.text, 'from', data.sender)
})

// Listen for model events from Resourceful PubSub
io.socket.on('user', (event) => {
  switch (event.verb) {
    case 'updated':
      console.log('User updated:', event.id, event.data)
      break
    case 'destroyed':
      console.log('User deleted:', event.id)
      break
  }
})

// Default event name ('message') from broadcast() without eventName
io.socket.on('message', (data) => {
  console.log('Message:', data)
})

io.socket.off(eventName, handler)

Remove an event listener:

function handleMessage(data) {
  console.log(data)
}

io.socket.on('newMessage', handleMessage)

// Later, remove the listener
io.socket.off('newMessage', handleMessage)

Special Events

// Connection established
io.socket.on('connect', () => {
  console.log('Connected to server')
})

// Connection lost
io.socket.on('disconnect', () => {
  console.log('Disconnected from server')
})

Connection Management

io.socket.isConnected()

Check connection status:

if (io.socket.isConnected()) {
  io.socket.post('/api/messages', { text: 'Hello' })
} else {
  console.log('Not connected — message will be queued')
}

io.socket.disconnect()

Manually disconnect:

io.socket.disconnect()

io.socket.reconnect()

Manually reconnect after disconnect:

io.socket.reconnect()

io.sails.connect(url, options)

Create a second socket connection (to a different server):

const analyticsSocket = io.sails.connect('https://analytics.example.com', {
  transports: ['websocket']
})

analyticsSocket.get('/api/events', (data) => {
  console.log('Analytics:', data)
})

Framework Integration

React

Use useEffect for subscribing to events and cleaning up listeners:

import { useEffect, useState } from 'react'

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // Subscribe to the room via WebSocket
    io.socket.get(`/api/chat/${roomId}/subscribe`, (initialMessages) => {
      setMessages(initialMessages)
    })

    // Listen for new messages
    function handleNewMessage(data) {
      setMessages((prev) => [...prev, data])
    }

    io.socket.on('newMessage', handleNewMessage)

    // Cleanup: remove listener when component unmounts or roomId changes
    return () => {
      io.socket.off('newMessage', handleNewMessage)
      io.socket.post(`/api/chat/${roomId}/unsubscribe`)
    }
  }, [roomId])

  return (
    <ul>
      {messages.map((msg) => (
        <li key={msg.id}>
          {msg.sender}: {msg.text}
        </li>
      ))}
    </ul>
  )
}

Vue (Composition API)

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const props = defineProps({ roomId: String })
const messages = ref([])

function handleNewMessage(data) {
  messages.value.push(data)
}

onMounted(() => {
  io.socket.get(`/api/chat/${props.roomId}/subscribe`, (initialMessages) => {
    messages.value = initialMessages
  })

  io.socket.on('newMessage', handleNewMessage)
})

onUnmounted(() => {
  io.socket.off('newMessage', handleNewMessage)
  io.socket.post(`/api/chat/${props.roomId}/unsubscribe`)
})
</script>

<template>
  <ul>
    <li v-for="msg in messages" :key="msg.id">
      {{ msg.sender }}: {{ msg.text }}
    </li>
  </ul>
</template>

Svelte

<script>
  import { onMount, onDestroy } from 'svelte'

  export let roomId

  let messages = []

  function handleNewMessage(data) {
    messages = [...messages, data]
  }

  onMount(() => {
    io.socket.get(`/api/chat/${roomId}/subscribe`, (initialMessages) => {
      messages = initialMessages
    })

    io.socket.on('newMessage', handleNewMessage)
  })

  onDestroy(() => {
    io.socket.off('newMessage', handleNewMessage)
    io.socket.post(`/api/chat/${roomId}/unsubscribe`)
  })
</script>

<ul>
  {#each messages as msg (msg.id)}
    <li>{msg.sender}: {msg.text}</li>
  {/each}
</ul>

Using with Inertia.js

When using Inertia.js, sails.io.js operates alongside Inertia's client-side router. The two don't conflict — Inertia handles page navigation (full page props replacement), while sails.io.js handles real-time event streams.

A common pattern is to use Inertia for initial page data and sails.io.js for live updates:

import { usePage } from '@inertiajs/react'
import { useEffect, useState } from 'react'

function Notifications() {
  // Initial data from Inertia page props
  const { notifications: initialNotifications } = usePage().props

  const [notifications, setNotifications] = useState(initialNotifications)

  useEffect(() => {
    // Subscribe to user-specific notifications via WebSocket
    io.socket.get('/api/notifications/subscribe')

    function handleNotification(data) {
      if (data.verb === 'created') {
        setNotifications((prev) => [data.data, ...prev])
      }
    }

    io.socket.on('notification', handleNotification)

    return () => {
      io.socket.off('notification', handleNotification)
    }
  }, [])

  return (
    <ul>
      {notifications.map((n) => (
        <li key={n.id}>{n.message}</li>
      ))}
    </ul>
  )
}

Source: SKILL.md on GitHub

2 warnings17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill provides a comprehensive guide and best practices for implementing realtime WebSocket features in Sails.js applications. It covers architectural concepts, room management, model-centric notifications, and client-side integration. The skill emphasizes security by documenting cross-site WebSocket hijacking protections and authorization patterns. No malicious behaviors or security risks were identified.

  • Socket17d

    1 alert: gptAnomaly

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    9/9 files flagged

Signed by skilld at bf19e10. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 8 months ago
Other metadata
metadata
{
  "author": "sailscastshq",
  "version": "1.0.0",
  "tags": "realtime, websockets, sockets, pubsub, socket.io, rooms, sails"
}

README badge

README badge for sailscastshq/boring-stack/realtime