nchan/WebSocket Integration

Overview

Unraid uses nchan for real-time updates via WebSockets and Server-Sent Events (SSE). This allows plugins to push updates to the browser without polling, enabling responsive UIs that update instantly when system state changes.

Architecture

flowchart TB
    subgraph Browser["Browser"]
        ES["EventSource('/sub/channel')"]
    end
    
    subgraph Server["nginx + nchan"]
        SUB["/sub/channel - subscribes clients"]
        PUB["/pub/channel - accepts messages"]
    end
    
    subgraph Backend["Plugin/Backend"]
        PHP["publish_nchan('channel', data)"]
    end
    
    ES <-->|"HTTP/WebSocket"| SUB
    PHP -->|"HTTP POST"| PUB
    PUB -->|"Broadcast"| SUB
    
    style Browser fill:#4a9eff,color:#fff
    style Server fill:#ff9800,color:#fff
    style Backend fill:#4caf50,color:#fff

📷 Screenshot needed: DevTools Network tab showing WebSocket connection

DevTools network

Subscribing to Channels (Client-Side)

Using EventSource (SSE)

// Subscribe to a channel using Server-Sent Events
var eventSource = new EventSource('/sub/yourplugin');

eventSource.onmessage = function(event) {
    var data = JSON.parse(event.data);
    console.log('Received:', data);
    
    // Update your UI
    updateStatus(data);
};

eventSource.onerror = function(event) {
    console.error('EventSource error:', event);
    // Handle reconnection
    setTimeout(function() {
        // EventSource automatically reconnects
    }, 5000);
};

// Clean up when leaving the page
window.addEventListener('beforeunload', function() {
    eventSource.close();
});

Using WebSocket

// Alternative: Using WebSocket directly
var ws = new WebSocket('ws://' + location.host + '/sub/yourplugin');

ws.onopen = function() {
    console.log('WebSocket connected');
};

ws.onmessage = function(event) {
    var data = JSON.parse(event.data);
    updateStatus(data);
};

ws.onclose = function() {
    console.log('WebSocket closed, reconnecting...');
    setTimeout(connectWebSocket, 5000);
};

ws.onerror = function(error) {
    console.error('WebSocket error:', error);
};

Subscribing to Multiple Channels

// Subscribe to multiple channels
var channels = ['diskstatus', 'dockerstatus', 'yourplugin'];
var sources = {};

channels.forEach(function(channel) {
    sources[channel] = new EventSource('/sub/' + channel);
    sources[channel].onmessage = function(event) {
        handleMessage(channel, JSON.parse(event.data));
    };
});

function handleMessage(channel, data) {
    switch(channel) {
        case 'diskstatus':
            updateDiskStatus(data);
            break;
        case 'yourplugin':
            updatePluginStatus(data);
            break;
    }
}

Publishing to Channels (Server-Side)

Using curl (Shell)

#!/bin/bash
# Publish a message to nchan channel

CHANNEL="yourplugin"
MESSAGE='{"status":"updated","value":42}'

curl -s -X POST "http://localhost/pub/$CHANNEL" \
    -H "Content-Type: application/json" \
    -d "$MESSAGE"

Using PHP

<?
/**
 * Publish a message to an nchan channel
 * 
 * @param string $channel Channel name
 * @param mixed $data Data to publish (will be JSON encoded)
 * @return bool Success
 */
function publish_nchan($channel, $data) {
    $url = "http://localhost/pub/$channel";
    $json = is_string($data) ? $data : json_encode($data);
    
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $json,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 5
    ]);
    
    $result = curl_exec($ch);
    $success = curl_getinfo($ch, CURLINFO_HTTP_CODE) == 200;
    curl_close($ch);
    
    return $success;
}

// Usage
publish_nchan('yourplugin', [
    'status' => 'updated',
    'timestamp' => time(),
    'data' => $someData
]);
?>

Using file_get_contents (Simpler)

<?
function publish_nchan_simple($channel, $data) {
    $json = is_string($data) ? $data : json_encode($data);
    
    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => "Content-Type: application/json\r\n",
            'content' => $json,
            'timeout' => 5
        ]
    ]);
    
    return @file_get_contents("http://localhost/pub/$channel", false, $context);
}
?>

Built-in Channels

Unraid provides several built-in channels that plugins can subscribe to:

Channel Description Data Format
/sub/diskload Disk I/O statistics Disk read/write stats
/sub/cpuload CPU usage CPU percentage array
/sub/var System variable updates var.ini changes
/sub/disks Disk status changes Disk state changes
/sub/shares Share updates Share modifications
/sub/notify Notification updates New notifications
/sub/session Session status Login/logout events

Subscribing to Built-in Channels

// Monitor disk activity
var diskSource = new EventSource('/sub/diskload');
diskSource.onmessage = function(event) {
    var disks = JSON.parse(event.data);
    // disks contains read/write stats per disk
    updateDiskGraph(disks);
};

// Monitor system variables
var varSource = new EventSource('/sub/var');
varSource.onmessage = function(event) {
    var data = JSON.parse(event.data);
    // data contains changed var.ini values
    if (data.fsState) {
        updateArrayStatus(data.fsState);
    }
};

Creating Custom Channels

Channel Naming

Use your plugin name as the channel prefix to avoid conflicts:

/sub/yourplugin           # Main channel
/sub/yourplugin_status    # Status updates
/sub/yourplugin_logs      # Log streaming

Channel Configuration

nchan channels are automatically created on first publish or subscribe. No configuration needed for basic usage.

Real-Time Dashboard Example

PHP Backend (status.php)

<?
// /plugins/yourplugin/include/status.php

function getStatus() {
    // Gather status data
    return [
        'running' => isServiceRunning(),
        'tasks' => getActiveTasks(),
        'lastRun' => getLastRunTime(),
        'updated' => time()
    ];
}

function broadcastStatus() {
    $status = getStatus();
    publish_nchan('yourplugin_status', $status);
}
?>

JavaScript Frontend

<div id="status">
    <span id="running-status">Unknown</span>
    <span id="task-count">-</span>
    <span id="last-update">Never</span>
</div>

<script>
$(function() {
    var statusSource = new EventSource('/sub/yourplugin_status');
    
    statusSource.onmessage = function(event) {
        var status = JSON.parse(event.data);
        
        $('#running-status').text(status.running ? 'Running' : 'Stopped')
            .css('color', status.running ? 'green' : 'red');
        $('#task-count').text(status.tasks + ' tasks');
        $('#last-update').text('Updated ' + new Date(status.updated * 1000).toLocaleTimeString());
    };
    
    statusSource.onerror = function() {
        $('#running-status').text('Connection lost').css('color', 'orange');
    };
});
</script>

Reconnection Handling

Automatic Reconnection with EventSource

EventSource automatically reconnects on disconnection. To customize:

var eventSource;
var reconnectDelay = 1000;
var maxDelay = 30000;

function connect() {
    eventSource = new EventSource('/sub/yourplugin');
    
    eventSource.onopen = function() {
        console.log('Connected');
        reconnectDelay = 1000;  // Reset delay on success
    };
    
    eventSource.onmessage = function(event) {
        handleMessage(JSON.parse(event.data));
    };
    
    eventSource.onerror = function() {
        eventSource.close();
        
        // Exponential backoff
        console.log('Reconnecting in ' + reconnectDelay + 'ms');
        setTimeout(connect, reconnectDelay);
        reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
    };
}

connect();

Heartbeat Pattern

Implement heartbeat to detect stale connections:

var lastMessage = Date.now();
var heartbeatInterval = setInterval(function() {
    if (Date.now() - lastMessage > 60000) {
        console.log('No message in 60s, reconnecting...');
        eventSource.close();
        connect();
    }
}, 30000);

eventSource.onmessage = function(event) {
    lastMessage = Date.now();
    // Handle message
};

Best Practices

1. Throttle Updates

Don’t publish too frequently:

<?
$lastPublish = 0;
$minInterval = 1;  // Minimum 1 second between updates

function throttledPublish($channel, $data) {
    global $lastPublish, $minInterval;
    
    $now = time();
    if ($now - $lastPublish >= $minInterval) {
        publish_nchan($channel, $data);
        $lastPublish = $now;
    }
}
?>

2. Batch Updates

Combine multiple small updates:

<?
$pendingUpdates = [];

function queueUpdate($key, $value) {
    global $pendingUpdates;
    $pendingUpdates[$key] = $value;
}

function flushUpdates() {
    global $pendingUpdates;
    if (!empty($pendingUpdates)) {
        publish_nchan('yourplugin', $pendingUpdates);
        $pendingUpdates = [];
    }
}

// Call flushUpdates() periodically or at end of script
?>

3. Handle Offline State

var isOnline = navigator.onLine;

window.addEventListener('online', function() {
    isOnline = true;
    connect();  // Reconnect when back online
});

window.addEventListener('offline', function() {
    isOnline = false;
    eventSource.close();
});

4. Clean Up Resources

// Clean up when leaving page
window.addEventListener('beforeunload', function() {
    if (eventSource) {
        eventSource.close();
    }
    clearInterval(heartbeatInterval);
});

Debugging

Check Channel Status

# Check if nchan is receiving messages
curl -s "http://localhost/pub/yourplugin?action=info"

Monitor Messages

# Subscribe from command line to see messages
curl -s -N "http://localhost/sub/yourplugin"

Browser Developer Tools

  1. Open Network tab
  2. Filter by “EventSource” or “WS”
  3. Watch messages in real-time

References