HyperHQ Plugin Socket.IO Connection Guide
This guide explains how to connect your plugin to HyperHQ using Socket.IO for real-time, bidirectional communication.
Table of Contents
- Overview
- Connection Architecture
- Getting Started
- Complete Connection Flow
- Event Communication
- Data Requests
- Error Handling
- Language Examples
- Troubleshooting
Overview
HyperHQ provides a Socket.IO server that enables real-time communication between the frontend and plugins. This allows for:
- Real-time Updates: Instant notifications and data synchronization
- Bidirectional Communication: Both HyperHQ and plugins can initiate communication
- Event-Driven Architecture: React to system events as they happen
- Progress Reporting: Live progress updates for long-running operations
- Persistent Connection: Maintains connection with automatic reconnection
Connection Architecture
Default Configuration
- Server URL:
http://localhost:${HYPERHQ_SOCKET_PORT} - Namespace: default Socket.IO namespace (
/) - Transport: WebSocket (with polling fallback)
- Port Selection: HyperHQ tries 52789 first, then 52790-52818, then an OS-assigned port. Always read
HYPERHQ_SOCKET_PORT.
Getting Started
Step 1: Define Socket.IO Configuration in Manifest
Add Socket.IO configuration to your plugin.json:
{
"id": "your-plugin-id",
"name": "Your Plugin",
"version": "1.0.0",
"type": "executable",
"executable": "plugin.exe",
"communication": {
"preferred": "socketio",
"fallback": "stdio",
"socketio": {
"enabled": true,
"autoReconnect": true,
"heartbeat": 30000,
"events": ["gameLaunched", "gameClosed", "mediaProgress"],
"fileStreaming": true,
"dataRequests": true
}
},
"capabilities": [
{ "name": "game-import", "description": "Import games", "required": true }
],
"permissions": [
{ "type": "network", "scope": "external-apis", "description": "Call external APIs" }
]
}
Step 2: Install Socket.IO Client Library
Choose the appropriate client library for your language:
JavaScript/Node.js
npm install socket.io-client
Python
pip install python-socketio
C#/.NET
<PackageReference Include="SocketIOClient" Version="3.0.8" />
Go
go get github.com/googollee/go-socket.io
Step 3: Understand Authentication
HyperHQ uses a challenge-response authentication model for plugin security:
- HyperHQ launches your plugin and passes an authentication challenge via environment variable
- Your plugin connects to Socket.IO and sends the challenge back
- HyperHQ validates the challenge and returns a session token
- Your plugin uses the session token for all subsequent requests
Environment Variables
When HyperHQ launches your plugin, it provides these environment variables:
| Variable | Description | Example |
|---|---|---|
HYPERHQ_PLUGIN_ID | Your plugin's unique ID | "my-plugin" |
HYPERHQ_SOCKET_PORT | Socket.IO server port | "52789" |
HYPERHQ_AUTH_CHALLENGE | One-time authentication challenge | "abc123..." |
HYPERHQ_PLUGIN_NAME | Your plugin's name | "My Plugin" |
HYPERHQ_PLUGIN_VERSION | Your plugin's version | "1.0.0" |
Authentication Flow Diagram
Step 4: Connect and Authenticate
Complete connection example with authentication:
// JavaScript/Node.js
const io = require('socket.io-client');
// Read environment variables
const pluginId = process.env.HYPERHQ_PLUGIN_ID;
const socketPort = process.env.HYPERHQ_SOCKET_PORT || '52789';
const authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE;
// Connect to Socket.IO server
const socket = io(`http://localhost:${socketPort}`, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 2000
});
let sessionToken = null;
socket.on('connect', () => {
console.log('Connected to HyperHQ Socket.IO server');
// Authenticate with challenge
socket.emit('authenticate', {
pluginId: pluginId,
challenge: authChallenge
});
});
socket.on('authenticated', (response) => {
if (response.success) {
console.log('Authentication successful');
sessionToken = response.sessionToken;
} else {
console.error('Authentication failed:', response.error);
}
});
// Include session token in all requests
socket.emit('requestData', {
method: 'getSystems',
params: {},
requestId: `getSystems-${Date.now()}`,
sessionToken
});
Complete Connection Flow
Overview
The complete plugin connection lifecycle:
Authentication Details
Challenge Expiration
- Challenges are valid for 30 seconds after plugin launch
- Each challenge can only be used once
- If authentication fails, HyperHQ will reject all requests
Session Token
- Session tokens are generated after successful authentication
- Tokens are unique per plugin instance (new token on each restart)
- Tokens expire when the plugin disconnects
- Must be included in all data requests for security
Security Notes
- Never hardcode authentication tokens in your plugin
- Always read the challenge from
HYPERHQ_AUTH_CHALLENGEenvironment variable - Store the session token securely in memory (not in files)
- If authentication fails, log the error and exit gracefully
Event Communication
Implemented Events
These are the Socket.IO events implemented by HyperHQ's plugin server.
Incoming Events (HyperHQ -> Plugin)
| Event | Description | Payload |
|---|---|---|
authenticated | Authentication result | {success, pluginId?, sessionToken?, serverPort?, error?} |
request | Method execution request | {id, method, data} |
dataResponse | Response to requestData | {requestId, success, data?, error?} |
data_response | Snake_case alias emitted with dataResponse | Same as dataResponse |
eventsSubscribed | Subscription confirmation | {events, pluginId} |
hyperHqEvent | Subscribed HyperHQ event | {type, data, timestamp} |
fileData | Response to requestFile | {requestId, filePath, success, data?, size?, error?} |
error | Socket request error | {message, requestId?, error?} |
Outgoing Events (Plugin -> HyperHQ)
| Event | Description | Payload |
|---|---|---|
authenticate | Authenticate or reconnect | {pluginId, challenge} or {pluginId, sessionToken} |
response | Respond to a request | {id, type: "response", data} |
requestData | Request data/action from HyperHQ | {method, params, requestId, sessionToken?} |
request_data | Snake_case alias for requestData | Same as requestData |
subscribeEvents | Subscribe to broadcast events | Array of event names |
statusUpdate | Send short status/progress text | {status, message?} |
requestFile | Read a file from the plugin install directory | {filePath, requestId, sessionToken} |
thumbnail:created | Notify HyperHQ that a thumbnail was created | {videoPath, thumbnailPath, timestamp} |
thumbnail:progress | Thumbnail generation progress | {processed, total, generated, skipped, failed, percentComplete} |
thumbnail:complete | Thumbnail generation finished | {generated, skipped, failed, durationMs} |
HyperHQ does not currently handle plugin:register, plugin:response, plugin:progress, plugin:notify, plugin:error, plugin:status, plugin_log, or pluginLog on the plugin Socket.IO server. Use the implemented events above.
Data Requests
Requesting Data from HyperHQ
Plugins can request various data from HyperHQ:
Get Systems
socket.emit('requestData', {
method: 'getSystems',
params: {},
requestId: 'get-systems-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'get-systems-001') {
const systems = response.data; // Array of system objects
console.log('Systems:', systems);
}
});
Get Media Folders
socket.emit('requestData', {
method: 'getMediaFolders',
params: {
systemReferenceId: 'steam',
mediaTypes: ['boxart', 'background', 'screenshot', 'logo']
},
requestId: 'get-folders-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'get-folders-001' && response.success) {
const folders = response.data.folders;
console.log('Media folders:', folders);
}
});
Create System
socket.emit('requestData', {
method: 'createSystem',
params: {
name: 'Steam',
description: 'Steam games library',
platform: 'PC',
referenceId: 'steam',
allowedExtensions: '.exe',
enabled: true
},
requestId: 'create-system-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'create-system-001') {
if (response.success) {
console.log('System created:', response.data.id);
} else {
console.error('Failed:', response.error);
}
}
});
Add Games
socket.emit('requestData', {
method: 'addGames',
params: {
systemId: 'steam',
games: [
{
name: 'Half-Life 2',
fileName: 'hl2.exe',
romPath: 'C:\\Steam\\HL2',
referenceId: 'steam_220',
developer: 'Valve',
enabled: true
}
]
},
requestId: 'add-games-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'add-games-001' && response.success) {
console.log('Games added successfully');
}
});
Progress Reporting
For long-running operations, send statusUpdate messages. HyperHQ uses these updates to keep an active plugin request alive and to surface short status text.
async function longOperation() {
socket.emit('statusUpdate', {
status: 'scanning',
message: 'Scanning game folders'
});
for (let processed = 0; processed <= 100; processed += 10) {
socket.emit('statusUpdate', {
status: 'scanning',
message: `Processed ${processed}%`
});
await sleep(250);
}
socket.emit('statusUpdate', {
status: 'complete',
message: 'Scan complete'
});
}
Error Handling
Connection Errors
socket.on('connect_error', (error) => {
console.error('Connection failed:', error.message);
// Implement retry logic or fallback
});
socket.on('disconnect', (reason) => {
console.warn('Disconnected:', reason);
if (reason === 'io server disconnect') {
// Server disconnected, try reconnecting
socket.connect();
}
});
socket.on('reconnect', (attemptNumber) => {
console.log(`Reconnected after ${attemptNumber} attempts`);
// Re-register plugin
registerPlugin();
});
socket.on('reconnect_error', (error) => {
console.error('Reconnection failed:', error);
});
socket.on('reconnect_failed', () => {
console.error('Failed to reconnect after maximum attempts');
// Handle permanent disconnection or show error
});
Request Timeouts
Implement timeouts for requests:
function requestDataWithTimeout(method, params, timeoutMs = 30000) {
return new Promise((resolve, reject) => {
const requestId = `${method}-${Date.now()}`;
const timer = setTimeout(() => {
reject(new Error(`Request timeout: ${method}`));
}, timeoutMs);
const handler = (response) => {
if (response.requestId !== requestId) return;
socket.off('dataResponse', handler);
clearTimeout(timer);
response.success ? resolve(response.data) : reject(new Error(response.error));
};
socket.on('dataResponse', handler);
socket.emit('requestData', {
method,
params,
requestId,
sessionToken
});
});
}
// Usage
try {
const games = await requestDataWithTimeout('getGamesForSystem', { systemId: 'arcade' });
console.log('Received games:', games);
} catch (error) {
console.error('Request failed:', error);
}
Language Examples
JavaScript/Node.js Complete Example
const io = require('socket.io-client');
class HyperHQPlugin {
constructor() {
this.socket = null;
this.connected = false;
this.authenticated = false;
this.sessionToken = null;
this.settings = {};
// Read environment variables
this.pluginId = process.env.HYPERHQ_PLUGIN_ID;
this.authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE;
this.socketPort = process.env.HYPERHQ_SOCKET_PORT || '52789';
if (!this.pluginId || !this.authChallenge) {
console.error('Missing environment variables - plugin must be launched by HyperHQ');
process.exit(1);
}
}
async initialize(settings) {
this.settings = settings;
await this.connect();
return 'initialized';
}
async connect() {
const serverUrl = `http://localhost:${this.socketPort}`;
this.socket = io(`${serverUrl}`, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 2000
});
this.socket.on('connect', () => {
this.connected = true;
console.log('Connected to HyperHQ Socket.IO server');
// Authenticate with challenge
this.socket.emit('authenticate', {
pluginId: this.pluginId,
challenge: this.authChallenge
});
});
this.socket.on('authenticated', (response) => {
if (response.success) {
this.authenticated = true;
this.sessionToken = response.sessionToken;
console.log('Authentication successful');
// Ready to receive `request` events and call `requestData`.
} else {
console.error('Authentication failed:', response.error);
process.exit(1);
}
});
this.socket.on('disconnect', () => {
this.connected = false;
this.authenticated = false;
console.log('Disconnected from HyperHQ');
});
this.socket.on('error', (error) => {
console.error('Socket.IO error:', error);
});
// Handle incoming requests from HyperHQ
this.socket.on('request', async (msg) => {
const response = await this.handleRequest(msg);
this.socket.emit('response', {
id: msg.id,
type: 'response',
data: response,
sessionToken: this.sessionToken
});
});
// Handle data responses
this.socket.on('dataResponse', (response) => {
this.handleDataResponse(response);
});
// Wait for authentication
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Authentication timeout'));
}, 10000);
this.socket.once('authenticated', (response) => {
clearTimeout(timeout);
if (response.success) resolve();
else reject(new Error(response.error));
});
});
}
// Helper to make authenticated data requests
async requestData(method, params) {
if (!this.authenticated) {
throw new Error('Not authenticated');
}
return new Promise((resolve, reject) => {
const requestId = `${method}-${Date.now()}`;
const timeout = setTimeout(() => {
reject(new Error(`Request timeout: ${method}`));
}, 30000);
// Store the resolver for this request
const handler = (response) => {
if (response.requestId === requestId) {
clearTimeout(timeout);
this.socket.off('dataResponse', handler);
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error || 'Request failed'));
}
}
};
this.socket.on('dataResponse', handler);
// Send the request
this.socket.emit('requestData', {
method,
params,
requestId,
sessionToken: this.sessionToken
});
console.log(`Data request sent: ${method} (ID: ${requestId})`);
});
}
handleDataResponse(response) {
console.log('Received data response:', response.requestId);
// Responses are handled by the promise in requestData()
}
async handleRequest(message) {
switch (message.method) {
case 'initialize':
return this.initialize(message.data);
case 'execute':
return await this.execute(message.data);
case 'test':
return this.test();
case 'shutdown':
return this.shutdown();
default:
return { error: `Unknown method: ${message.method}` };
}
}
async execute(data) {
const action = data.action || 'default';
switch (action) {
case 'sync':
return await this.syncGames();
case 'get_status':
return this.getStatus();
default:
return { error: `Unknown action: ${action}` };
}
}
async syncGames() {
try {
// Request systems from HyperHQ
const systems = await this.requestData('getSystems', {});
console.log(`Found ${systems.length} systems`);
// Get media folders for a specific system
if (systems.length > 0) {
const folders = await this.requestData('getMediaFolders', {
systemReferenceId: systems[0].referenceId,
mediaTypes: ['boxart', 'background']
});
console.log('Media folders:', folders);
}
return { success: true, systemCount: systems.length };
} catch (error) {
console.error('Sync failed:', error);
return { error: error.message };
}
}
getStatus() {
return {
connected: this.connected,
authenticated: this.authenticated,
pluginId: this.pluginId
};
}
test() {
return this.connected && this.authenticated;
}
shutdown() {
if (this.socket) {
this.socket.close();
}
return 'ok';
}
}
// Usage
const plugin = new HyperHQPlugin();
plugin.initialize({}).catch(error => {
console.error('Failed to initialize plugin:', error);
process.exit(1);
});
Python Complete Example
import socketio
import asyncio
import json
import os
import sys
class HyperHQPlugin:
def __init__(self):
self.sio = socketio.AsyncClient()
self.connected = False
self.authenticated = False
self.session_token = None
self.settings = {}
# Read environment variables
self.plugin_id = os.environ.get('HYPERHQ_PLUGIN_ID')
self.auth_challenge = os.environ.get('HYPERHQ_AUTH_CHALLENGE')
self.socket_port = os.environ.get('HYPERHQ_SOCKET_PORT', '52789')
# Setup event handlers
self.setup_handlers()
def setup_handlers(self):
@self.sio.on('connect')
async def on_connect():
self.connected = True
print('Connected to HyperHQ Socket.IO server')
# Authenticate with challenge
await self.sio.emit('authenticate', {
'pluginId': self.plugin_id,
'challenge': self.auth_challenge
})
@self.sio.on('authenticated')
async def on_authenticated(response):
if response.get('success'):
self.authenticated = True
self.session_token = response.get('sessionToken')
print('Authentication successful')
await self.sio.emit('subscribeEvents', [
'gameLaunched',
'gameClosed',
'mediaProgress'
])
# Ready to receive `request` events and call `requestData`.
else:
print(f"Authentication failed: {response.get('error')}")
sys.exit(1)
@self.sio.on('disconnect')
def on_disconnect():
self.connected = False
self.authenticated = False
print('Disconnected from HyperHQ')
@self.sio.on('request')
async def on_request(data):
response = await self.handle_request(data)
await self.sio.emit('response', {
'id': data['id'],
'type': 'response',
'data': response,
'sessionToken': self.session_token
})
@self.sio.on('eventsSubscribed')
async def on_events_subscribed(data):
print(f"Subscribed to HyperHQ events: {data.get('events', [])}")
@self.sio.on('hyperHqEvent')
async def on_hyper_hq_event(event):
event_type = event.get('type')
if event_type == 'gameLaunched':
self.handle_game_launched(event.get('data'))
async def initialize(self, settings):
self.settings = settings
await self.connect()
return 'initialized'
async def connect(self):
server_url = f"http://localhost:{self.socket_port}"
await self.sio.connect(server_url)
# Wait for authentication to complete
timeout = 10 # 10 second timeout
start_time = asyncio.get_event_loop().time()
while not self.authenticated:
if asyncio.get_event_loop().time() - start_time > timeout:
raise TimeoutError('Authentication timeout')
await asyncio.sleep(0.1)
async def request_data(self, method, params):
"""Helper to make authenticated data requests"""
if not self.authenticated:
raise Exception('Not authenticated')
request_id = f"req_{asyncio.get_event_loop().time()}"
await self.sio.emit('requestData', {
'method': method,
'params': params,
'requestId': request_id,
'sessionToken': self.session_token
})
# Wait for response (simplified - in production use proper event handling)
# This is just an example pattern
return await self._wait_for_response(request_id)
async def handle_request(self, message):
method = message.get('method')
data = message.get('data', {})
if method == 'execute':
return await self.execute(data)
elif method == 'test':
return self.test()
else:
return {'error': f'Unknown method: {method}'}
async def execute(self, data):
action = data.get('action', 'default')
# Send progress update
await self.sio.emit('statusUpdate', {
'status': action,
'message': 'Processing...'
})
# Do work...
await asyncio.sleep(1)
return {'success': True, 'result': 'completed'}
def test(self):
return self.connected
Troubleshooting
Connection Issues
Plugin cannot connect to Socket.IO server
-
Check HyperHQ is running
- Verify HyperHQ is started and the Socket.IO server is active
- Check HyperHQ logs for Socket.IO server status
-
Verify server URL
- Default:
http://localhost:52789 - Check if custom port is configured in HyperHQ
- Default:
-
Firewall/Antivirus
- Add exception for localhost:52789
- Temporarily disable to test
-
Check namespace
- Use the default Socket.IO namespace
- Example:
io('http://localhost:52789')
Authentication Issues
-
Authentication Failed - Invalid Challenge
- Cause: Challenge not found or already used
- Solution: Ensure you're reading
HYPERHQ_AUTH_CHALLENGEfrom environment variables - Solution: Don't cache the challenge - read it fresh on each plugin start
-
Authentication Timeout
- Cause: Plugin took too long to authenticate (>30 seconds)
- Solution: Connect and authenticate immediately after plugin starts
- Solution: Don't perform long operations before authentication
-
Session Token Expired
- Cause: Plugin disconnected and session token is no longer valid
- Solution: On reconnection, authenticate again with the original challenge
- Note: Each plugin launch gets a new challenge/session
-
Missing Environment Variables
- Check: Verify HyperHQ is passing environment variables
- Debug: Print
process.env.HYPERHQ_AUTH_CHALLENGEat startup - Common Issue: Running plugin manually without HyperHQ launching it
Connection drops frequently
-
Increase reconnection attempts
"communication": {
"preferred": "socketio",
"socketio": {
"enabled": true,
"autoReconnect": true,
"heartbeat": 30000
}
} -
Implement heartbeat
setInterval(() => {
if (socket.connected) {
socket.emit('statusUpdate', {
status: 'running',
message: 'Plugin heartbeat'
});
}
}, 30000); -
Check for memory leaks
- Remove event listeners when not needed
- Clear large data structures
Event Issues
Events not received
-
Verify event names match exactly
- Event names are case-sensitive
- Check for typos
-
Ensure proper namespace
// Correct
socket.on('request', handler);
// Wrong - no namespace needed in event name
socket.on('/plugin/request', handler); -
Check authentication
- Plugin must authenticate before receiving requests or subscribing to events
- Verify the
authenticatedresponse hassuccess: true
Response timeout
-
Implement proper async handling
socket.on('request', async (msg) => {
try {
const result = await processRequest(msg);
socket.emit('response', { id: msg.id, type: 'response', data: result });
} catch (error) {
socket.emit('response', {
id: msg.id,
type: 'response',
data: { error: error.message }
});
}
}); -
Send progress for long operations
// Prevent timeout by sending progress
const interval = setInterval(() => {
socket.emit('statusUpdate', {
status: 'processing',
message: `Processed ${count} items`
});
}, 1000);
Performance Issues
High CPU usage
-
Throttle event emissions
const throttle = (func, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
};
const throttledProgress = throttle((message) => {
socket.emit('statusUpdate', {
status: 'processing',
message
});
}, 100); -
Request files through HyperHQ for large data
// Send large data as binary
const buffer = Buffer.from(largeData);
socket.emit('requestFile', {
filePath: 'assets/preview.png',
requestId: `file-${Date.now()}`,
sessionToken: this.sessionToken
});
Best Practices
- Always implement reconnection logic
- Send regular progress updates for long operations
- Handle all error cases gracefully
- Clean up resources on disconnect
- Use structured logging for debugging
- Implement request timeouts
- Validate all incoming data
- Use TypeScript/type hints for better IDE support
Security Best Practices
Authentication Security
-
Never Hardcode Credentials
// WRONG - Don't hardcode tokens
const authChallenge = "abc123hardcoded";
// CORRECT - Always read from environment
const authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE; -
Validate Environment Variables
// Check that required variables are present
if (!process.env.HYPERHQ_AUTH_CHALLENGE) {
console.error('Missing HYPERHQ_AUTH_CHALLENGE - plugin must be launched by HyperHQ');
process.exit(1);
} -
Store Session Token Securely
- Keep session token in memory only (never write to disk)
- Don't log the session token in plain text
- Clear token on disconnect
-
Handle Authentication Failures
socket.on('authenticated', (response) => {
if (!response.success) {
console.error('Authentication failed:', response.error);
// Exit gracefully - don't continue without authentication
process.exit(1);
}
}); -
Include Session Token in Authenticated Requests
// Recommended for requestData; required for requestFile
socket.emit('requestData', {
method: 'getSystems',
params: {},
requestId: `getSystems-${Date.now()}`,
sessionToken: this.sessionToken
});
socket.emit('requestFile', {
filePath: 'README.md',
requestId: `file-${Date.now()}`,
sessionToken: this.sessionToken
});
Data Security
-
Validate All Incoming Data
socket.on('request', (message) => {
// Validate message structure
if (!message.id || !message.method) {
console.error('Invalid message format');
return;
}
// Validate method is expected
const allowedMethods = ['initialize', 'execute', 'test', 'shutdown'];
if (!allowedMethods.includes(message.method)) {
console.error('Unknown method:', message.method);
return;
}
}); -
Sanitize User Input
- Never directly execute user-provided code
- Validate file paths before accessing files
- Use parameterized queries for databases
-
Respect Permissions
- Only access resources specified in your plugin manifest
- Don't attempt to bypass permission checks
- Request minimal permissions needed
Network Security
-
Use Localhost Only
- Socket.IO server runs on
localhost- never connect to external hosts - HyperHQ provides the port via
HYPERHQ_SOCKET_PORT
- Socket.IO server runs on
-
Timeout All Requests
const timeout = setTimeout(() => {
reject(new Error('Request timeout'));
}, 30000); -
Handle Connection Errors
- Implement proper error handling for network failures
- Don't expose sensitive information in error messages
Next Steps
- Review the Plugin Developer Guide for complete plugin development information
- Check the API Reference for all available methods
- See template examples in
templates/for working implementations - Join the HyperHQ Discord for community support
With the Socket.IO contract in place, your plugin can authenticate, respond to HyperHQ requests, ask for data, and subscribe to the events it actually needs.