UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

76 lines (75 loc) 3.29 kB
import { c as isRecord } from "../record-coerce-DItp3I4t.js"; //#region src/plugin-sdk/feature-contract.ts /** Browser-safe typed operations over the existing plugin session-action transport. */ function defineFeatureContract(contract) { if (!contract.pluginId.trim() || Object.keys(contract.operations).length > 64 || Object.keys(contract.events).length > 64) throw new Error("Feature contracts require a plugin id and at most 64 operations and events"); for (const id of Object.keys(contract.operations)) if (!/^[a-z][a-z0-9._-]{0,127}$/u.test(id)) throw new Error(`Invalid feature operation id: ${id}`); for (const id of Object.keys(contract.events)) if (!/^[a-z][a-z0-9_-]{0,127}$/u.test(id)) throw new Error(`Invalid feature event id: ${id}`); return contract; } /** Uses only the current browser connection; it does not mint scopes or a backend client. */ function createFeatureClient(contract, host) { if (host.pluginId !== contract.pluginId) throw new Error("Feature contract must belong to the active browser plugin"); const invoke = async (operation, input, options = {}) => { host.signal.throwIfAborted(); const result = await host.request("plugins.sessionAction", { pluginId: contract.pluginId, actionId: operation, payload: input, ...options }); host.signal.throwIfAborted(); if (!isRecord(result) || result.ok !== true) throw new Error(isRecord(result) && typeof result.error === "string" ? result.error : "Feature operation returned an invalid response"); return result.result; }; const on = (event, listener) => host.onEvent(`plugin.${contract.pluginId}.${event}`, (payload) => listener(payload)); const watch = (operation, input, options) => { if (contract.operations[operation]?.kind !== "query") throw new Error("Only feature queries can be watched"); let disposed = false; let generation = 0; let scheduled = false; let connected = host.connection.connected; const refresh = () => { generation += 1; if (disposed || !host.connection.connected || scheduled) return; scheduled = true; queueMicrotask(() => { scheduled = false; if (disposed || !host.connection.connected) return; const current = generation; invoke(operation, input, { sessionKey: options.sessionKey, agentId: options.agentId }).then((output) => { if (!disposed && current === generation && host.connection.connected) options.onChange(output); }, (error) => { if (!disposed && current === generation && host.connection.connected) options.onError(error instanceof Error ? error : new Error(String(error))); }); }); }; const subscriptions = options.events.map((event) => on(event, refresh)); subscriptions.push(host.subscribe(() => { if (connected !== host.connection.connected) { connected = host.connection.connected; refresh(); } })); const dispose = () => { disposed = true; generation += 1; for (const unsubscribe of subscriptions) unsubscribe(); host.signal.removeEventListener("abort", dispose); }; host.signal.addEventListener("abort", dispose, { once: true }); if (host.signal.aborted) dispose(); else refresh(); return dispose; }; return { invoke, on, watch }; } //#endregion export { createFeatureClient, defineFeatureContract };