Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions @plotly/dash-websocket-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Dash websocket worker

Worker for websocket based callbacks.
29 changes: 29 additions & 0 deletions @plotly/dash-websocket-worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@plotly/dash-websocket-worker",
"version": "1.0.0",
"description": "SharedWorker for WebSocket-based Dash callbacks",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"watch": "webpack --mode development --watch",
"clean": "rm -rf dist"
},
"files": [
"dist"
],
"keywords": [
"dash",
"websocket",
"sharedworker"
],
"author": "Plotly",
"license": "MIT",
"devDependencies": {
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0",
"ts-loader": "^9.0.0"
}
}
207 changes: 207 additions & 0 deletions @plotly/dash-websocket-worker/src/MessageRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
WorkerMessageType,
WorkerMessage,
CallbackRequestMessage,
GetPropsResponseMessage,
SetPropsMessage,
GetPropsRequestMessage,
CallbackResponseMessage
} from './types';

/**
* Routes messages between renderers (via MessagePorts) and the WebSocket server.
*/
export class MessageRouter {
/** Map of renderer IDs to their MessagePorts */
private renderers: Map<string, MessagePort> = new Map();

/** Callback to send messages to the WebSocket server */
public sendToServer: ((message: unknown) => void) | null = null;

/**
* Register a renderer with its MessagePort.
* @param rendererId Unique identifier for the renderer
* @param port The MessagePort for communication
*/
public registerRenderer(rendererId: string, port: MessagePort): void {
this.renderers.set(rendererId, port);
}

/**
* Unregister a renderer.
* @param rendererId The renderer to unregister
*/
public unregisterRenderer(rendererId: string): void {
this.renderers.delete(rendererId);
}

/**
* Get the number of connected renderers.
*/
public get rendererCount(): number {
return this.renderers.size;
}

/**
* Handle a message from a renderer.
* @param rendererId The ID of the renderer that sent the message
* @param message The message from the renderer
*/
public handleRendererMessage(rendererId: string, message: WorkerMessage): void {
switch (message.type) {
case WorkerMessageType.CALLBACK_REQUEST:
this.forwardCallbackRequest(rendererId, message as CallbackRequestMessage);
break;

case WorkerMessageType.GET_PROPS_RESPONSE:
this.forwardGetPropsResponse(rendererId, message as GetPropsResponseMessage);
break;

default:
console.warn(`Unknown message type from renderer: ${message.type}`);
}
}

/**
* Handle a message from the WebSocket server.
* @param message The message from the server
*/
public handleServerMessage(message: unknown): void {
const msg = message as WorkerMessage;
const rendererId = msg.rendererId;

switch (msg.type) {
case WorkerMessageType.CALLBACK_RESPONSE:
this.forwardToRenderer(rendererId, msg as CallbackResponseMessage);
break;

case WorkerMessageType.SET_PROPS:
this.forwardSetProps(rendererId, msg as SetPropsMessage);
break;

case WorkerMessageType.GET_PROPS_REQUEST:
this.forwardGetPropsRequest(rendererId, msg as GetPropsRequestMessage);
break;

case WorkerMessageType.ERROR:
this.forwardToRenderer(rendererId, msg);
break;

default:
console.warn(`Unknown message type from server: ${msg.type}`);
}
}

/**
* Send a message to all connected renderers.
* @param message The message to broadcast
*/
public broadcastToRenderers(message: WorkerMessage): void {
for (const [rendererId, port] of this.renderers) {
try {
port.postMessage(message);
} catch (error) {
// Port may be closed if tab was closed
console.warn(`Failed to send to renderer ${rendererId}, removing`);
this.renderers.delete(rendererId);
}
}
}

/**
* Send a connected notification to a specific renderer.
* @param rendererId The renderer to notify
*/
public notifyConnected(rendererId: string): void {
const port = this.renderers.get(rendererId);
if (port) {
try {
port.postMessage({
type: WorkerMessageType.CONNECTED,
rendererId
});
} catch (error) {
console.warn(`Failed to notify renderer ${rendererId}, removing`);
this.renderers.delete(rendererId);
}
}
}

/**
* Send a disconnected notification to all renderers.
* @param reason Optional reason for disconnection
*/
public notifyDisconnected(reason?: string): void {
this.broadcastToRenderers({
type: WorkerMessageType.DISCONNECTED,
rendererId: '',
payload: { reason }
});
}

/**
* Send an error notification to a specific renderer.
* @param rendererId The renderer to notify
* @param message Error message
* @param code Optional error code
*/
public notifyError(rendererId: string, message: string, code?: string): void {
const port = this.renderers.get(rendererId);
if (port) {
try {
port.postMessage({
type: WorkerMessageType.ERROR,
rendererId,
payload: { message, code }
});
} catch (error) {
console.warn(`Failed to send error to renderer ${rendererId}, removing`);
this.renderers.delete(rendererId);
}
}
}

private forwardCallbackRequest(rendererId: string, message: CallbackRequestMessage): void {
if (this.sendToServer) {
this.sendToServer({
type: WorkerMessageType.CALLBACK_REQUEST,
rendererId,
requestId: message.requestId,
payload: message.payload
});
}
}

private forwardGetPropsResponse(rendererId: string, message: GetPropsResponseMessage): void {
if (this.sendToServer) {
this.sendToServer({
type: WorkerMessageType.GET_PROPS_RESPONSE,
rendererId,
requestId: message.requestId,
payload: message.payload
});
}
}

private forwardToRenderer(rendererId: string, message: WorkerMessage): void {
const port = this.renderers.get(rendererId);
if (port) {
try {
port.postMessage(message);
} catch (error) {
console.warn(`Failed to forward to renderer ${rendererId}, removing`);
this.renderers.delete(rendererId);
}
} else {
console.warn(`Renderer ${rendererId} not found for message`);
}
}

private forwardSetProps(rendererId: string, message: SetPropsMessage): void {
this.forwardToRenderer(rendererId, message);
}

private forwardGetPropsRequest(rendererId: string, message: GetPropsRequestMessage): void {
this.forwardToRenderer(rendererId, message);
}
}
Loading
Loading