Skip to main content

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:

ParameterTypeRequiredDescription
urlstringYesThe WebSocket URL (ws:// or wss://).
options.headersRecord<string, string>NoHTTP 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.

ParameterTypeDescription
datastring | objectData 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.
  • Map or List -- encoded to a JSON string with jsonEncode before 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
  1. Validation -- the URL is validated before any connection attempt. Blocked URLs cause immediate rejection.
  2. Limit check -- the per-extension connection limit is enforced. If the limit is reached, the call rejects.
  3. Handshake -- a native WebSocket.connect() is performed with optional custom headers.
  4. Active -- the connection is stored with a unique connectionId in the format {extensionId}_ws_{counter}. Data flows through onMessage.
  5. 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:

FieldTypeRequired
urlstringYes
headersobjectNo

Response: { connectionId: string }

network.wsSend

Sends data on an open connection.

Parameters:

FieldTypeRequired
connectionIdstringYes
datastring | objectYes

Response: null

network.wsClose

Closes an open connection.

Parameters:

FieldTypeRequired
connectionIdstringYes

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

LimitValue
Max concurrent WebSocket connections per extension5

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.0 are 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();
});
});