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: (fn: (...args: A) => unknown) => ((...args: A) => void); readonly allowedEventNames: ReadonlySet; readonly eventSubscriptionsQuota: number; }) { const { state, capabilities, requirePermission, guardCallback, allowedEventNames, eventSubscriptionsQuota } = options; return { on: (event: unknown, handler: (payload: Record) => 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) => { 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 { return typeof value === "object" && value !== null && !Array.isArray(value); }