IRealtimeChannel
By default the core opens realtime connections using a thin wrapper around the browser’s native WebSocket.
It satisfies the IRealtimeChannel shape and gets out of the way, so the plugin that calls this.websocket() never touches a WebSocket directly.
That works fine for most setups, but sometimes you’re already running SignalR or Socket.IO in the same app, and spinning up bare WebSockets next to them creates two competing connection stacks.
When that happens, you swap the transport by passing a factory to setup({ websocketFactory }).
Every plugin that opens a channel then gets your transport instead, and nothing else changes.
You can also override the transport for a single connection by passing factory in the per-call options, without touching the global default.
Everything you import from this page lives in these two lines:
import { nativeWebSocketAdapter } from '@nomercy-entertainment/nomercy-player-core';
import type { IRealtimeChannel, RealtimeFactory, RealtimeFactoryOptions } from '@nomercy-entertainment/nomercy-player-core/adapters/realtime';
Built-in adapter
nativeWebSocketAdapter
A RealtimeFactory that wraps the browser’s native WebSocket to satisfy IRealtimeChannel.
The core uses it as the final fallback when no websocketFactory is configured.
It normalises the four DOM events (open, message, close, error) into the channel’s on/off listener model, maps readyState from numeric constants to the string literals the interface defines, and shields listeners from each other so an error in one handler doesn’t stop the rest from firing.
Reconnection is not built into the adapter itself. The lifecycle layer above it handles reconnect and disposal so every transport, including custom ones, gets that behaviour for free.
Setting the global transport
Pass a factory at setup() time to replace the default for every connection opened by every plugin:
nmplayer('main').setup({
websocketFactory: nativeWebSocketAdapter, // the default, shown for illustration
});
Swapping for SignalR
This example wraps @microsoft/signalr in the IRealtimeChannel shape and registers it as the global transport:
import * as signalR from '@microsoft/signalr';
const signalRFactory: RealtimeFactory = (url, opts) => {
const connection = new signalR.HubConnectionBuilder()
.withUrl(url)
.withAutomaticReconnect()
.build();
const listeners = new Map<string, Set<(data?: unknown) => void>>();
const dispatch = (event: string, data?: unknown): void => {
const set = listeners.get(event);
if (!set) return;
for (const fn of [...set]) {
try {
fn(data);
}
catch (err) { void err; }
}
};
connection.onclose(() => dispatch('close', { code: 1000, reason: '' }));
connection.onreconnected(() => dispatch('open'));
connection.on('message', (payload: unknown) => dispatch('message', payload));
connection.start()
.then(() => dispatch('open'))
.catch((err: unknown) => dispatch('error', err));
return {
send(data) {
void connection.invoke('message', data);
},
close(code, reason) {
void connection.stop();
},
on(event, fn) {
let set = listeners.get(event);
if (!set) {
set = new Set();
listeners.set(event, set);
}
set.add(fn);
},
off(event, fn) {
listeners.get(event)?.delete(fn);
},
get readyState() {
switch (connection.state) {
case signalR.HubConnectionState.Connecting:
case signalR.HubConnectionState.Reconnecting:
return 'connecting';
case signalR.HubConnectionState.Connected:
return 'open';
case signalR.HubConnectionState.Disconnecting:
return 'closing';
default:
return 'closed';
}
},
};
};
nmplayer('main').setup({
source: { src: 'wss://your-server.example.com/stream' },
websocketFactory: signalRFactory,
});
Per-call override
When you need a different transport for one specific plugin connection without changing the global default, pass factory in the call options:
// Inside a plugin
const channel = this.websocket('wss://your-server.example.com/notify', {
factory: signalRFactory,
});
Interface
interface IRealtimeChannel {
// Send a message. Call only when readyState === 'open'.
send(data: string | ArrayBuffer | Blob): void;
// Close the connection. Omit both arguments for a clean 1000 close.
close(code?: number, reason?: string): void;
// Register a listener for a lifecycle or data event.
on(event: 'open' | 'message' | 'close' | 'error', fn: (data?: unknown) => void): void;
// Remove a listener registered with on().
off(event: 'open' | 'message' | 'close' | 'error', fn: (data?: unknown) => void): void;
// Current transport state. Mirrors WebSocket readyState semantics.
readonly readyState: 'connecting' | 'open' | 'closing' | 'closed';
}
type RealtimeFactory = (url: string, opts?: RealtimeFactoryOptions) => IRealtimeChannel;
interface RealtimeFactoryOptions {
// Sub-protocol strings forwarded to the WebSocket handshake.
protocols?: string[];
// Attempt automatic reconnection on unexpected close.
reconnect?: boolean;
// Initial back-off delay in milliseconds. Defaults to 1000.
baseDelayMs?: number;
// Maximum back-off delay in milliseconds. Defaults to 30000.
maxDelayMs?: number;
// Per-call factory override, skips the global websocketFactory for this connection.
factory?: RealtimeFactory;
}
Custom implementation
Any object that satisfies IRealtimeChannel works.
The only contract the core enforces is the shape: send, close, on, off, and readyState.
Plugin authors calling this.websocket() never construct a channel themselves.
The core creates one and hands it back, using whichever factory is in effect at that point.
See also
- Adapters, the full port catalog
- Plugin API, how plugins open and manage channel lifetimes