Uniview

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

ModeIsolationEnvironmentUse Case
WorkerFull sandboxBrowserProduction, untrusted plugins
WebSocketProcess boundaryNode.js/Deno/BunServer-side plugins, full runtime
Main ThreadNoneBrowserDevelopment, 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 localStorage or sessionStorage
  • Import Node.js modules (fs, path, etc.)
  • Make synchronous calls to the host

Allowed APIs

  • fetch() for network requests
  • console.log() and other console methods
  • setTimeout(), 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

shield

Use Worker Mode

  • Production browser deployments - Third-party or untrusted plugins - Security-sensitive applications - Standard web applications
server

Use WebSocket Mode

  • Server-side plugin logic - Database or file system access - Heavy computation offloading - Multi-tenant architectures
bug

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 → onmessage

WebSocket Mode:

Host → ws.send() → Server → ws.onmessage
Server → ws.send() → Host → ws.onmessage

Main Thread Mode:

Host → direct function call → Plugin
Plugin → direct function call → Host

Latency

ModeTypical LatencyNotes
Main Thread< 1msDirect calls
Worker1-5msSerialization overhead
WebSocket10-100msNetwork + serialization

Capabilities

CapabilityWorkerWebSocketMain Thread
DOM AccessNoNoYes
File SystemNoYesYes
Fetch APIYesYesYes
Node.js APIsNoYesNo
Hot ReloadLimitedNoYes
DebuggingDifficultModerateEasy

Migration Between Modes

The PluginController interface is identical across all modes. To switch modes:

  1. Change the controller creation
  2. Update plugin build configuration
  3. 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();

On this page