Runtime Modes
Understanding Worker, WebSocket, and Main Thread execution modes
Uniview supports multiple runtime modes for different use cases. Each mode provides different trade-offs between isolation, performance, and capabilities.
Overview
| Mode | Isolation | Environment | Use Case |
|---|---|---|---|
| Worker | Full sandbox | Browser | Production, untrusted plugins |
| WebSocket | Process boundary | Node.js/Deno/Bun | Server-side plugins, full runtime |
| Main Thread | None | Browser | Development, debugging |
Web Worker Mode
The default and recommended mode for browser plugins.
Characteristics
- Full isolation: Plugin cannot access DOM,
window, or host memory - Secure: Safe to run untrusted third-party plugins
- Async only: All communication via
postMessage - Browser-compatible: Works in all modern browsers
Setup
Plugin side:
// worker.ts
import { startWorkerPlugin } from "@uniview/runtime";
import App from "./App";
startWorkerPlugin({ App });Host side:
import { createWorkerController } from "@uniview/host-sdk";
const controller = createWorkerController({
pluginUrl: "/plugins/my-plugin.js",
initialProps: { theme: "dark" },
});
await controller.connect();Restrictions
Plugins in Worker mode cannot:
- Access
window,document, or DOM APIs - Use
localStorageorsessionStorage - Import Node.js modules (
fs,path, etc.) - Make synchronous calls to the host
Allowed APIs
fetch()for network requestsconsole.log()and other console methodssetTimeout(),setInterval()- Web Worker global APIs
- All React hooks and patterns
WebSocket Mode
For server-side plugins with full runtime access, using a Bridge architecture.
Characteristics
- Bridge Architecture: Plugins connect as clients to a central bridge server
- Full runtime: Access to file system, network, databases, and Node.js APIs
- Process isolation: Plugin runs in a separate Node.js/Deno/Bun process
- Persistent: Maintains long-running connections through the bridge
- Routing: The bridge server routes traffic between specific plugin instances and hosts
Bridge Server
In the new architecture, a central Bridge Server (typically built with Elysia or similar) acts as a transparent byte forwarder. Instead of each plugin running its own WebSocket server, they connect to the bridge.
- Plugin Connection:
ws://bridge:3000/plugins/:pluginId - Host Connection:
ws://bridge:3000/host/:pluginId
The bridge server manages the pairing and ensures that RPC messages are forwarded correctly between the host and the plugin.
Setup
Plugin side (Node.js/Deno/Bun):
// server-plugin.ts (Recommended)
import { connectToHostServer } from "@uniview/runtime/ws-client";
import App from "./App";
connectToHostServer({
App,
serverUrl: "ws://localhost:3000", // Bridge server
pluginId: "my-plugin",
});Deprecated Approach The old method where the plugin runs its own server is still supported but deprecated:
import { startWSServerPlugin } from "@uniview/runtime/ws-server";
startWSServerPlugin({ App, port: 3001 });Host side:
import { createWebSocketController } from "@uniview/host-sdk";
const controller = createWebSocketController({
serverUrl: "ws://localhost:3000", // Bridge server
pluginId: "my-plugin", // Required for bridge routing
initialProps: { userId: "abc" },
});
await controller.connect();Use Cases
- Plugins that need database access or read/write files
- Plugins that call external APIs with secrets
- Plugins that need heavy computation (AI, image processing)
- Multi-user collaborative plugins
- Centralized management of multiple server-side plugins
Architecture
┌─────────────────┐ WebSocket ┌─────────────────┐ WebSocket ┌─────────────────┐
│ Browser Host │ <───────────────────> │ Bridge Server │ <───────────────────> │ Plugin Client │
│ (Svelte/React) │ /host/:pluginId │ (Elysia) │ /plugins/:pluginId │ (Node.js/Deno) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Database, │
│ File System, │
│ External APIs │
└─────────────────┘Main Thread Mode
For development and debugging only.
Never use Main Thread mode in production. Plugins have full access to your application's memory and DOM.
Characteristics
- No isolation: Plugin runs in same context as host
- Synchronous: Direct function calls, no serialization overhead
- Full debugging: Use browser DevTools normally
- Hot reload: Works with Vite/webpack HMR
Setup
import { createMainController } from "@uniview/host-sdk";
import { SimpleDemo } from "@uniview/example-plugin";
const controller = createMainController({
App: SimpleDemo,
initialProps: { debug: true },
});
await controller.connect();When to Use
- Local development with HMR
- Debugging plugin issues
- Performance profiling
- Testing new components
Development Workflow
// In development, switch based on environment
const controller = import.meta.env.DEV
? createMainController({ App: SimpleDemo })
: createWorkerController({ pluginUrl: "/plugins/simple.js" });Choosing a Mode
Use Worker Mode
- Production browser deployments - Third-party or untrusted plugins - Security-sensitive applications - Standard web applications
Use WebSocket Mode
- Server-side plugin logic - Database or file system access - Heavy computation offloading - Multi-tenant architectures
Use Main Thread Mode
- Local development only - Debugging issues - Performance profiling - Never in production
Mode Comparison
Communication Flow
Worker Mode:
Host → postMessage → Worker → onmessage
Worker → postMessage → Host → onmessageWebSocket Mode:
Host → ws.send() → Server → ws.onmessage
Server → ws.send() → Host → ws.onmessageMain Thread Mode:
Host → direct function call → Plugin
Plugin → direct function call → HostLatency
| Mode | Typical Latency | Notes |
|---|---|---|
| Main Thread | < 1ms | Direct calls |
| Worker | 1-5ms | Serialization overhead |
| WebSocket | 10-100ms | Network + serialization |
Capabilities
| Capability | Worker | WebSocket | Main Thread |
|---|---|---|---|
| DOM Access | No | No | Yes |
| File System | No | Yes | Yes |
| Fetch API | Yes | Yes | Yes |
| Node.js APIs | No | Yes | No |
| Hot Reload | Limited | No | Yes |
| Debugging | Difficult | Moderate | Easy |
Migration Between Modes
The PluginController interface is identical across all modes. To switch modes:
- Change the controller creation
- Update plugin build configuration
- Deploy plugin to appropriate environment
// Before: Worker mode
const controller = createWorkerController({
pluginUrl: "/plugins/app.js",
});
// After: WebSocket mode
const controller = createWebSocketController({
serverUrl: "ws://bridge-server:3000",
pluginId: "my-plugin",
});
// The rest of your code stays the same!
controller.subscribe((tree) => renderTree(tree));
await controller.connect();