# Plugin RPC Documentation ## Overview Plugins communicate peer-to-peer using **protomux-rpc only** (JSON payloads). Each registered protocol gets one RPC mux per peer (`{pluginDomain}-{protocol}-rpc`). There is no separate protomux message channel. ## Features - **Scoped protocol names**: Wire name `{pluginDomain}-{protocol}-rpc` - **Named methods**: `request`, `event`, and `broadcast(protocol, method, value)` - **Reserved keepalive**: `__p2ns.ping` / `__p2ns.pong` (handled internally; do not register `__p2ns.*` methods) - **Auto-reconnection**: RPC sessions recreate when peers reconnect - **Lifecycle hooks**: `onPeerOpen` / `onPeerClose` ## Breaking migration Upgrade **all nodes and plugins together**. Mixed old (message channel + RPC) and new (RPC-only) peers will not interoperate on plugin protocols. | Old | New | |-----|-----| | `createChannel('x', { onMessage })` | `register('x', { methods: { 'my.event': handler } })` | | `broadcast('x', data)` | `broadcast('x', 'my.event', data)` | | `channels.rpc.register` | `channels.register` with `methods` | | `send('x', peer, data)` | `event('x', peer, 'message', data)` or a named method | | `closeChannel('x')` | `unregister('x')` | ## Basic usage > **Note:** The SDK is available via `require('../../includes/plugins/sdk')` from `plugin-sites/`. ### Register a protocol ```javascript const sdk = require('../../includes/plugins/sdk'); sdk.channels.register('chat', { methods: { 'chat.message': async (data, { peerId }) => { sdk.log.debug('chat', `From ${peerId}: ${JSON.stringify(data)}`); return null; }, 'chat.ping': async (_value, { peerId }) => ({ pong: true, peerId }) }, onPeerOpen: (peerId) => sdk.log.debug('chat', `RPC ready: ${peerId}`), onPeerClose: (peerId) => sdk.log.debug('chat', `RPC closed: ${peerId}`), autoReconnect: true }); ``` ### Request, event, broadcast ```javascript const result = await sdk.channels.request('chat', peerId, 'chat.ping', { ts: Date.now() }); sdk.channels.event('chat', peerId, 'chat.typing', { active: true }); const sentCount = sdk.channels.broadcast('chat', 'chat.message', { type: 'announcement', text: 'Server restarting in 5 minutes' }); ``` Use `sdk.channels.isRpcReady('chat', peerId)` before sending if the peer just connected. ### Protocol information ```javascript const info = sdk.channels.getProtocol('chat'); // alias: getChannel() const protocols = sdk.channels.listProtocols(); // alias: listChannels() const peers = sdk.channels.getConnectedPeers('chat'); const ready = sdk.channels.isRpcReady('chat', peerId); ``` `getProtocol()` returns RPC-centric state: `transport: 'rpc'`, `peerChannels` map with `rpc` handles per peer (no legacy message `channel` object). ## Register options | Option | Description | |--------|-------------| | `methods` | Map of method name → `async (value, { peerId, pluginDomain, protocol }) => result` | | `onPeerOpen` | Called when RPC is open for a peer | | `onPeerClose` | Called when RPC closes for a peer | | `autoReconnect` | Recreate RPC when peer reconnects (default: `true`) | Method names must not start with `__p2ns.` (reserved for keepalive). ## Complete example: chat plugin ```javascript const sdk = require('../../includes/plugins/sdk'); async function onInit() { sdk.channels.register('chat', { methods: { 'chat.message': async (data, { peerId }) => { sdk.websocket.broadcast({ type: 'chat', from: peerId, ...data }); return null; } }, onPeerOpen: (peerId) => { sdk.channels.event('chat', peerId, 'chat.sync', { since: Date.now() }); } }); } async function onShutdown() { sdk.channels.unregister('chat'); } module.exports = { handler, onInit, onShutdown }; ``` ## Best practices 1. **Register in `onInit`** so RPC is ready before peers attach. 2. **Use explicit method names** (`domain.action`) instead of opaque blobs. 3. **Check `isRpcReady`** before `request` on freshly connected peers. 4. **Unregister in `onShutdown`** to release handlers and peer state. 5. **Return JSON-serializable values** from request handlers; use `null` for fire-and-forget handlers. ## Protocol naming - Plugin domain: `peer.chat` - Protocol: `chat` - Wire mux: `peer.chat-chat-rpc` Use the short protocol name in the SDK; the manager adds the domain and `-rpc` suffix. ## Troubleshooting | Issue | Checks | |-------|--------| | RPC not registered | `PLUGIN_DOMAIN` set; `register()` in `onInit`; no reserved method names | | Peer not receiving | Both nodes upgraded; `isRpcReady`; method name matches on both sides | | `request` hangs | RPC open timeout; peer disconnected; handler throws | ## API reference | Method | Description | |--------|-------------| | `register(protocol, config)` | Register RPC protocol and handlers | | `unregister(protocol)` | Tear down protocol | | `request(protocol, peerId, method, value?, timeoutMs?)` | Request/response | | `event(protocol, peerId, method, value?)` | Fire-and-forget | | `broadcast(protocol, method, value)` | Fan-out event to all peers with open RPC | | `getProtocol(protocol)` | Protocol + per-peer RPC state | | `listProtocols()` | Registered protocol names | | `getConnectedPeers(protocol)` | Peer IDs with attached RPC state | | `isRpcReady(protocol, peerId)` | Whether RPC mux is open | | `getPeerRpc(protocol, peerId)` | Underlying ProtomuxRPC instance | Deprecated shims (`createChannel`, `send`, `sendAsync`, nested `channels.rpc`) may still exist but should not be used in new code. ## See also - [Plugin SDK](PLUGIN_SDK.md) - [Plugin system](README.md) - [example.plugin](../../plugin-sites/example.plugin/index.js) - [global.profile](../../plugin-sites/global.profile/index.js) — `profile.update` broadcast pattern