WebSocket API
Real-time, bidirectional communication for extensions using managed WebSocket connections.
Overview
The WebSocket API lets extensions open persistent connections to remote servers for use cases such as live data feeds, chat protocols, real-time collaboration, and streaming APIs. Connections are fully managed by the host app: each extension gets an isolated connection pool, automatic cleanup on deactivation, and the same security validation applied to HTTP requests.
No special permission is required in the extension manifest. The API is available through the network namespace of the SDK.
Quick Start
import editor from '@srcnexus/ext-sdk';
editor.onLoad(function () {
editor.network.createWebSocket('wss://echo.websocket.org').then(function (ws) {
ws.onMessage(function (msg) {
console.log('Received:', msg.data);
});
ws.onClose(function (info) {
console.log('Closed:', info.code, info.reason);
});
ws.onError(function (err) {
console.log('Error:', err.message);
});
ws.send('Hello, server!');
});
});
SDK Method
network.createWebSocket(url, options?)
Opens a managed WebSocket connection and returns a WebSocketConnection object.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The WebSocket URL (ws:// or wss://). |
options.headers | Record<string, string> | No | HTTP headers sent during the upgrade handshake. |
Returns: Promise<WebSocketConnection>
// Basic connection
let ws = await editor.network.createWebSocket('wss://api.example.com/stream');
// Connection with custom headers
let ws = await editor.network.createWebSocket('wss://api.example.com/stream', {
headers: {
'Authorization': 'Bearer my-token',
'X-Custom-Header': 'value'
}
});
WebSocketConnection Object
The object returned by createWebSocket exposes methods for sending data, closing the connection, and subscribing to events.
send(data)
Send data over the connection.
| Parameter | Type | Description |
|---|---|---|
data | string | object | Data to send. Objects are automatically serialized to JSON. |
Returns: Promise<void>
// Send a plain string
ws.send('ping');
// Send a JSON object (auto-serialized)
ws.send({ type: 'subscribe', channel: 'updates' });
At the Dart layer, the following types are accepted for the data RPC parameter:
String-- sent as-is.List<int>-- sent as binary.MaporList-- encoded to a JSON string withjsonEncodebefore sending.
close()
Close the connection gracefully and clean up internal event handlers.
Returns: Promise<void>
await ws.close();
Calling close() is idempotent. Closing an already-closed or non-existent connection is a safe no-op.
onMessage(handler)
Register a handler for incoming messages.
Callback receives: { data: string } -- the raw data from the server.
Returns: () => void -- an unsubscribe function.
let unsub = ws.onMessage(function (msg) {
console.log('Server says:', msg.data);
});
// Later, stop listening:
unsub();
onClose(handler)
Register a handler for connection close events.
Callback receives: { code: number, reason: string }
Returns: () => void
ws.onClose(function (info) {
console.log('Connection closed:', info.code, info.reason);
});
onError(handler)
Register a handler for connection errors.
Callback receives: { message: string }
Returns: () => void
ws.onError(function (err) {
console.error('WebSocket error:', err.message);
});
connectionId
A read-only string that uniquely identifies this connection. Useful for logging and debugging.
console.log('Connected with ID:', ws.connectionId);
// e.g. "my-extension_ws_0"
Connection Lifecycle
createWebSocket(url)
|
v
[Validate URL] ──> blocked? ──> Promise rejects with error
|
v
[Check limit] ──> >= 5 active? ──> Promise rejects with error
|
v
[TCP + WS handshake]
|
v
[Connected] ──> connectionId returned
|
┌────┴────┐
v v
onMessage onError ──> connection removed
|
v
onClose ──> connection removed
- Validation -- the URL is validated before any connection attempt. Blocked URLs cause immediate rejection.
- Limit check -- the per-extension connection limit is enforced. If the limit is reached, the call rejects.
- Handshake -- a native
WebSocket.connect()is performed with optional custom headers. - Active -- the connection is stored with a unique
connectionIdin the format{extensionId}_ws_{counter}. Data flows throughonMessage. - Termination -- when the server closes the connection, or
close()is called, or an error occurs, the connection is removed from the internal pool.
RPC Methods (Low-Level)
The SDK wraps three RPC methods. Extension authors should use the SDK (network.createWebSocket) rather than calling these directly, but they are documented here for completeness.
network.wsConnect
Opens a WebSocket connection.
Parameters:
| Field | Type | Required |
|---|---|---|
url | string | Yes |
headers | object | No |
Response: { connectionId: string }
network.wsSend
Sends data on an open connection.
Parameters:
| Field | Type | Required |
|---|---|---|
connectionId | string | Yes |
data | string | object | Yes |
Response: null
network.wsClose
Closes an open connection.
Parameters:
| Field | Type | Required |
|---|---|---|
connectionId | string | Yes |
Response: null
Events
Events are delivered from the host app to the extension runtime via the __pushWsEvent global function. The SDK dispatches these to the appropriate handler callbacks registered on the WebSocketConnection object.
Each event is a JSON envelope with the shape:
{
"connectionId": "my-extension_ws_0",
"event": "message | close | error",
"data": { ... }
}
message
Fired when the server sends data.
{ "data": "<raw data from server>" }
close
Fired when the connection is closed (by either side).
{ "code": 1000, "reason": "Normal closure" }
After this event, the connection is automatically removed from the internal pool.
error
Fired when a connection-level error occurs.
{ "message": "Connection reset by peer" }
After this event, the connection is closed and removed from the internal pool.
Limits
| Limit | Value |
|---|---|
| Max concurrent WebSocket connections per extension | 5 |
Attempting to open a sixth connection while five are active produces an error:
Too many WebSocket connections (max 5 per extension)
Close unused connections before opening new ones, or design your protocol to multiplex channels over a single connection.
Automatic Cleanup
When an extension is deactivated, the host app closes all of its WebSocket connections and removes them from the internal pool.
Security
WebSocket connections go through the same URL validation as HTTP fetch requests:
- URL security validation is called before the connection attempt.
- Blocked localhost ports (4820, 3200) are rejected. These ports are reserved for internal app services (terminal server, project server).
- Blocked hosts:
localhost,127.0.0.1,::1,0.0.0.0are blocked only on the reserved ports listed above. Connections to other ports on localhost are allowed. - Unparseable URLs are rejected.
If validation fails, createWebSocket rejects with an error message describing the restriction.
Ownership Isolation
Each WebSocket connection is tagged with the extensionId of the extension that created it. The host enforces strict ownership:
send()on a connection owned by a different extension throws:WebSocket {id} is not owned by {extensionId}close()on a connection owned by a different extension throws the same error.- An extension cannot discover or enumerate connections belonging to other extensions.
This prevents one extension from interfering with another extension's real-time communication.
Examples
Echo Client
import editor from '@srcnexus/ext-sdk';
editor.onLoad(async function () {
let ws = await editor.network.createWebSocket('wss://echo.websocket.org');
ws.onMessage(function (msg) {
editor.window.showToast('Echo: ' + msg.data);
});
ws.onError(function (err) {
editor.window.showToast('WS Error: ' + err.message);
});
ws.send('Hello from my extension!');
});
Reconnecting Client
import editor from '@srcnexus/ext-sdk';
let ws = null;
function connect() {
editor.network.createWebSocket('wss://api.example.com/events').then(function (conn) {
ws = conn;
ws.onMessage(function (msg) {
let event = JSON.parse(msg.data);
console.log('Event:', event.type, event.payload);
});
ws.onClose(function (info) {
console.log('Disconnected:', info.code, info.reason);
// Reconnect after a delay (unless it was a clean close by us)
if (info.code !== 1000) {
setTimeout(connect, 3000);
}
});
ws.onError(function (err) {
console.error('Connection error:', err.message);
});
}).catch(function (err) {
console.error('Failed to connect:', err);
setTimeout(connect, 5000);
});
}
editor.onLoad(function () {
connect();
});
editor.onDispose(function () {
if (ws) {
ws.close();
ws = null;
}
});
Streaming JSON Messages
import editor from '@srcnexus/ext-sdk';
editor.onLoad(async function () {
let ws = await editor.network.createWebSocket('wss://stream.example.com/data');
ws.onMessage(function (msg) {
try {
let payload = JSON.parse(msg.data);
// Process structured data from the server
console.log('Price update:', payload.symbol, payload.price);
} catch (e) {
console.warn('Non-JSON message:', msg.data);
}
});
// Subscribe to a specific channel by sending a JSON object
// (the SDK auto-serializes objects to JSON)
ws.send({ action: 'subscribe', channels: ['prices', 'alerts'] });
});
Multiple Connections
import editor from '@srcnexus/ext-sdk';
editor.onLoad(async function () {
// Open two connections to different services (max 5 per extension)
let priceWs = await editor.network.createWebSocket('wss://prices.example.com');
let chatWs = await editor.network.createWebSocket('wss://chat.example.com');
priceWs.onMessage(function (msg) {
console.log('[Prices]', msg.data);
});
chatWs.onMessage(function (msg) {
console.log('[Chat]', msg.data);
});
// Clean up both on dispose
editor.onDispose(async function () {
await priceWs.close();
await chatWs.close();
});
});