openpetswithchatandmcp/apps/desktop/src/plugin-sdk-events.ts
2026-06-11 16:54:54 +02:00

41 lines
2.4 KiB
TypeScript

import type { PluginPermission } from "./plugin-manifest.js";
import type { PluginHostCapabilities } from "./plugin-sdk-bridge.js";
import type { PluginRuntimeState } from "./plugin-sdk-state.js";
import { registerConfigChangedEvent } from "./plugin-sdk-config.js";
export function createPluginEventsApi(options: {
readonly state: PluginRuntimeState;
readonly capabilities: PluginHostCapabilities;
readonly requirePermission: (permission: PluginPermission) => void;
readonly guardCallback: <A extends unknown[]>(fn: (...args: A) => unknown) => ((...args: A) => void);
readonly allowedEventNames: ReadonlySet<string>;
readonly eventSubscriptionsQuota: number;
}) {
const { state, capabilities, requirePermission, guardCallback, allowedEventNames, eventSubscriptionsQuota } = options;
return {
on: (event: unknown, handler: (payload: Record<string, unknown>) => void) => {
requirePermission("events");
const name = String(event);
if (!allowedEventNames.has(name)) throw new Error(`Unknown plugin event: ${name}`);
if (name === "pet:drop") requirePermission("pet:drop");
if (state.eventSubscriptions.size >= eventSubscriptionsQuota) throw new Error("Plugin event subscription quota exceeded.");
const subId = opaqueId("event");
if (name === "config:changed") {
registerConfigChangedEvent({ state, subscriptionId: subId, handler, guardCallback });
} else if (name === "pet:drop") {
const wrapped = guardCallback((payload: Record<string, unknown>) => {
if (Array.isArray(payload.files)) for (const file of payload.files) { if (isRecord(file) && typeof file.fileId === "string") state.pickedFiles.add(file.fileId); }
handler(payload);
});
state.eventSubscriptions.set(subId, capabilities.events.subscribe(name, wrapped));
} else {
state.eventSubscriptions.set(subId, capabilities.events.subscribe(name, guardCallback(handler)));
}
return { subscriptionId: subId };
},
off: (subscriptionId: unknown) => { state.eventSubscriptions.get(String(subscriptionId))?.(); state.eventSubscriptions.delete(String(subscriptionId)); },
};
}
function opaqueId(prefix: string): string { return `${prefix}-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`; }
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }