packages/plugin-sdk/docs/design/multi-transport.md
Introduce a host-side transport-aware context factory that provides one Eventa context per plugin instance. Plugin SDK APIs become context-bound factories, allowing local (in-memory/worker) and remote (WebSocket) plugins to share the same API surface while using different transports. This enables multiple plugins within a single Plugin Host without cross-talk or global channel coupling.
plugin-sdk currently exposes APIs (for example providers.listProviders) that call defineInvoke on a globally imported channel. This couples plugins to a single shared context and prevents the host from isolating multiple plugins or using different transports per plugin. We also need a path to support local plugins (in-process or worker) and remote plugins (WebSocket) with consistent ergonomics.
Eventa is context-oriented: contexts are created per transport (in-memory, WebSocket, worker, electron) and the invoke/handler APIs attach to that context. Multiple contexts can co-exist in the same process.
ManifestV1 entrypoints.createPluginContext(transport) factory that returns an Eventa context bound to the plugin's transport.createApis(ctx)), replacing global channel usage.init().plugin-host/runtimes/node and plugin-host/runtimes/web to handle different transport adapters.Transport-aware contexts for isolated multi-plugin hosts.
Define a small transport config type owned by the Plugin Host:
export type PluginTransport
= | { kind: 'in-memory' }
| { kind: 'websocket', url: string, protocols?: string[] }
| { kind: 'web-worker', worker: Worker }
| { kind: 'node-worker', worker: import('node:worker_threads').Worker }
| { kind: 'electron', target: 'main' | 'renderer', webContentsId?: number }
createPluginContext(transport) creates and returns an Eventa context based on the transport adapter (in-memory, WebSocket, worker, electron).
Context creation happens during host setup, before any plugin lifecycle method is called.
createPluginContext(transport).createApis(ctx).plugin.init({ host: ctx, apis }).packages/plugin-sdk/src/plugin-host/transports/:
packages/plugin-sdk/src/plugin-host/runtimes/node/:
packages/plugin-sdk/src/plugin-host/runtimes/web/:
packages/plugin-sdk/src/plugin-host/index.ts:
createPluginContext via conditional exportsReplace direct channel usage with context-bound factories:
export function createProviders(ctx: EventaContext) {
return {
listProviders() {
return defineInvoke(ctx, protocolListProviders)()
},
}
}
export function createApis(ctx: EventaContext) {
return { providers: createProviders(ctx) }
}
Plugins call createApis(ctx) provided by the host instead of importing global singletons.
in-memory for simplest casenode-worker or web-worker for isolationwebsocket transport bound to a specific URL or connectionTransport selection is a host concern; plugins are transport-agnostic.
Each plugin has its own context and transport. APIs are bound to that context, preventing cross-talk. The host keeps a registry mapping plugin ID to its context, transport, and loaded module for lifecycle management.
createApis(ctx).createPluginContext for node/web runtimes (in-memory, websocket, worker, and electron where available).Q: Why not keep global channels and just switch the active channel? A: Global channels make multi-plugin isolation impossible and require global state mutation. Context-per-plugin avoids cross-talk and matches Eventa's design.
Q: Do plugins need to know about transports? A: No. The host injects the context and APIs; plugins remain transport-agnostic.
Q: Should we build a shared reliable WebSocket package? A: Only if we need custom reconnection/heartbeat logic across multiple packages. Start with Eventa adapters; factor out shared logic later if required.
Q: Can workers be used for plugin isolation? A: Yes. Use Eventa web-worker or node-worker adapters to bridge a per-plugin context to the worker.