/** * Plugin side of the RPC channel. A plugin bundle calls `createPlugin(...)` * once; it wires up the message listener, answers the host's lifecycle calls * with the supplied handlers, and returns a typed `host` proxy for calling the * plugin API back. */ import { CAPABILITY_METHODS, CAPABILITIES } from './capabilities'; import { createRpcEndpoint, type RpcRequestOptions, type RpcTransport } from './rpc'; /** Page outline entry surfaced by `readCurrentPage.getOutline()` / TOC tools. */ export interface OutlineEntry { id: string; level: number; text: string; } export interface PageMeta { id: string; title: string; pondId: string; slug: string; } export interface PageSummary { id: string; title: string; slug: string; /** Names of the page's labels — the page-index reference plugin filters on * these (issue #77). Only pages the viewer may read arrive here at all. */ labels: string[]; } /** * The plugin API surface, grouped by capability. A plugin may call only the * groups it declared in its manifest `permissions`; undeclared calls reject * with `capability_not_permitted` at the host. Payload shapes are intentionally * loose here (v1) and tightened by the capability endpoints in #74. */ export interface PluginHost { readCurrentPage: { getOutline: () => Promise; getContent: () => Promise; getMeta: () => Promise; }; readPond: { listPages: () => Promise; getPageOutline: (pageId: string) => Promise; getPageContent: (pageId: string) => Promise; }; readBlock: { getBlock: (pageId: string, blockId: string) => Promise; }; blockData: { getData: () => Promise; setData: (data: unknown) => Promise; }; ui: { resize: (height: number) => Promise; openPage: (pageId: string) => Promise; toast: (messageKey: string) => Promise; /** Scroll the host page to a heading by its outline id (issue #77). */ scrollToHeading: (headingId: string) => Promise; /** * Promote this surface's frame to a viewport-covering overlay — for * plugins whose editing UI needs the whole screen (drawio). The frame * stays the same sandboxed iframe; only its geometry changes. Always * pair with {@link exitFullscreen}; the host also restores normal * geometry when the surface is destroyed. */ enterFullscreen: () => Promise; exitFullscreen: () => Promise; }; } /** Params the host passes to lifecycle handlers. */ export interface RenderContext { /** The plugin surface being rendered (extension point id). */ extensionPointId: string; /** Locale to render in, e.g. `"de"`. */ locale: string; /** For a `block` surface: the stored block data (may be undefined on first * render of a fresh block). */ data?: unknown; } export interface PluginLifecycle { /** Draw the surface. Called on mount and whenever inputs change. */ onRender?: (context: RenderContext) => void | Promise; /** Enter edit mode for a block surface. */ onEdit?: (context: RenderContext) => void | Promise; /** Release resources before the frame is torn down. */ onDestroy?: () => void | Promise; } export interface CreatePluginOptions extends PluginLifecycle { transport: RpcTransport; /** Default timeout for plugin→host calls, in ms. */ timeoutMs?: number; } export interface PluginInstance { host: PluginHost; dispose: () => void; } // A single-argument method sends its argument as `params`; a multi-argument // method sends the positional array. This keeps the wire format simple while // letting the host implementation destructure as needed. function toParams(args: unknown[]): unknown { if (args.length === 0) return undefined; if (args.length === 1) return args[0]; return args; } /** * Initializes a plugin inside its sandboxed frame. Returns the `host` proxy * (typed capability calls) and a `dispose()` to detach. The returned proxy * issues real RPC calls lazily, so importing the SDK has no side effects until * a method is invoked. */ export function createPlugin(options: CreatePluginOptions): PluginInstance { const endpoint = createRpcEndpoint({ ...options.transport, timeoutMs: options.timeoutMs, handlers: { render: (params) => options.onRender?.(params as RenderContext), edit: (params) => options.onEdit?.(params as RenderContext), destroy: () => options.onDestroy?.(), }, }); const call = (method: string, args: unknown[], requestOptions?: RpcRequestOptions) => endpoint.request(method, toParams(args), requestOptions); // Build the grouped `host` proxy from the capability→methods map so it stays // in lock-step with the protocol without hand-listing every method. const host = {} as Record Promise>>; for (const capability of CAPABILITIES) { const group: Record Promise> = {}; for (const method of CAPABILITY_METHODS[capability]) { group[method] = (...args: unknown[]) => call(method, args); } host[capability] = group; } return { host: host as unknown as PluginHost, dispose: () => endpoint.dispose(), }; }