Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2148fbb
Add websocket callbacks
T4rk1n Apr 14, 2026
ad3693e
websocket flag for individual callbacks
T4rk1n Apr 14, 2026
1a20893
ensure connected for ws callbacks
T4rk1n Apr 14, 2026
aff6dee
websocket origin validation
T4rk1n Apr 14, 2026
a4f10fd
add close method to DashWebsocket
T4rk1n Apr 14, 2026
206c9d8
add websocket hooks
T4rk1n Apr 14, 2026
fed2311
Add websocket inactivity disconnect
T4rk1n Apr 16, 2026
7d01f53
no storing of rendererId in session storage
T4rk1n Apr 16, 2026
b91c5d5
rename allowed_websocket_origins -> websocket_allowed_origins
T4rk1n Apr 16, 2026
96824a9
update architecture docs with websocket details
T4rk1n Apr 17, 2026
290ba68
Merge branch 'v4.2' into websocket-callbacks
T4rk1n Apr 23, 2026
3a14ef3
add websocket callback validation and tests
T4rk1n Apr 23, 2026
2ba5557
fixes
T4rk1n Apr 23, 2026
104a71d
add quart websocket callback implementation
T4rk1n Apr 24, 2026
2070708
fix tests
T4rk1n Apr 24, 2026
750a585
remove dev bundle reference
T4rk1n Apr 24, 2026
deda670
version 4.2.0rc1
T4rk1n Apr 24, 2026
e4849eb
Fix websocket callback set_props() with Patch object problems
CNFeffery Apr 26, 2026
70cbf48
Add websocket callback set_props patch tests
CNFeffery Apr 26, 2026
68a696a
Update CHANGELOG
CNFeffery Apr 26, 2026
65f3dc2
Format code
CNFeffery Apr 26, 2026
6460335
Fix websocket callback update component prop via set_props()
CNFeffery Apr 26, 2026
4b19d9a
Add websocket callback update component prop tests
CNFeffery Apr 26, 2026
411b467
Add websocket callback update component prop tests
CNFeffery Apr 26, 2026
1abd7bf
Update CHANGELOG
CNFeffery Apr 27, 2026
96af07d
Enhance websocket set_props with plotly JSON for full prop type compa…
CNFeffery Apr 28, 2026
649eadb
Fix format
CNFeffery Apr 28, 2026
c7b3f74
Optimize websocket set_props by centralizing serialization with _send…
CNFeffery Apr 28, 2026
a8fbd57
Merge pull request #3759 from CNFeffery/websocket-callbacks
T4rk1n Apr 30, 2026
fe5489d
Merge branch 'v4.2' into websocket-callbacks
T4rk1n Apr 30, 2026
8b86163
threadpool for ws callback execution
T4rk1n May 1, 2026
727626d
move websocket code out of base_server
T4rk1n May 1, 2026
86e3263
add future annotations
T4rk1n May 1, 2026
e4703ea
rejects all pending callbacks when a DISCONNECTED message is received
T4rk1n May 1, 2026
379e847
reset retryCount on connection
T4rk1n May 1, 2026
bf7e97f
Version 4.2.0rc2
T4rk1n May 1, 2026
c35e6ad
better handling of disconnect for persistent callbacks
T4rk1n May 4, 2026
ceaea5b
silence websocket disconnected
T4rk1n May 4, 2026
6a72fe9
reset retry count onn connection
T4rk1n May 5, 2026
5825c2d
fastapi requirement uvicorn[standard]
T4rk1n May 5, 2026
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