UNPKG

every-plugin

Version:
178 lines (176 loc) 6.46 kB
import { PluginRuntimeError } from "./errors.mjs"; import { PluginService } from "./services/plugin.service.mjs"; import { Cause, Effect, Exit, Hash, ManagedRuntime, Option } from "effect"; import { createRouterClient } from "@orpc/server"; //#region src/runtime/index.ts var PluginRuntime = class { runtime; registry; __registryType; pluginCache = /* @__PURE__ */ new Map(); constructor(runtime, registry) { this.runtime = runtime; this.registry = registry; } generateCacheKey(pluginId, config) { return `${pluginId}:${Hash.structure(config).toString()}`; } validatePluginId(pluginId) { if (!(pluginId in this.registry)) return Effect.fail(new PluginRuntimeError({ pluginId: String(pluginId), operation: "validate-plugin-id", cause: /* @__PURE__ */ new Error(`Plugin ID '${String(pluginId)}' not found in registry.`), retryable: false })); return Effect.succeed(String(pluginId)); } async runPromise(effect) { const exit = await this.runtime.runPromiseExit(effect); if (Exit.isFailure(exit)) { const error = Cause.failureOption(exit.cause); if (Option.isSome(error)) throw error.value; throw Cause.squash(exit.cause); } return exit.value; } async usePlugin(pluginId, config, plugins) { const cacheKey = this.generateCacheKey(pluginId, { ...config, __plugins: plugins ?? {} }); let cachedPlugin = this.pluginCache.get(cacheKey); if (!cachedPlugin) { const operation = Effect.gen(this, function* () { const pluginService = yield* PluginService; const validatedId = yield* this.validatePluginId(pluginId); const ctor = yield* pluginService.loadPlugin(validatedId); const instance = yield* pluginService.instantiatePlugin(pluginId, ctor); const initialized = yield* pluginService.initializePlugin(instance, config, plugins); yield* pluginService.registerPlugin(initialized); return initialized; }).pipe(Effect.annotateLogs({ plugin: pluginId }), Effect.provide(this.runtime)); cachedPlugin = Effect.cached(operation).pipe(Effect.flatten); this.pluginCache.set(cacheKey, cachedPlugin); } let initialized; try { initialized = await this.runPromise(cachedPlugin); } catch (error) { this.pluginCache.delete(cacheKey); throw error; } const router = initialized.plugin.createRouter(initialized.context); const createClient = (context) => createRouterClient(router, { context: context ?? {} }); return { createClient, router, metadata: initialized.metadata, initialized }; } async loadPlugin(pluginId) { const effect = Effect.gen(function* () { return yield* (yield* PluginService).loadPlugin(pluginId); }).pipe(Effect.annotateLogs({ plugin: pluginId })); return this.runPromise(effect); } async instantiatePlugin(pluginId, loadedPlugin) { const effect = Effect.gen(function* () { return yield* (yield* PluginService).instantiatePlugin(pluginId, loadedPlugin); }).pipe(Effect.annotateLogs({ plugin: pluginId })); return this.runPromise(effect); } async initializePlugin(instance, config, plugins) { const effect = Effect.gen(function* () { const pluginService = yield* PluginService; const initialized = yield* pluginService.initializePlugin(instance, config, plugins); yield* pluginService.registerPlugin(initialized); return initialized; }).pipe(Effect.annotateLogs({ plugin: instance.plugin.id })); return this.runPromise(effect); } async shutdown() { const effect = Effect.gen(function* () { yield* (yield* PluginService).cleanup(); }); await this.runPromise(effect); await this.runtime.dispose(); } async evictPlugin(pluginId, config, plugins) { const cacheKey = this.generateCacheKey(pluginId, { ...config, __plugins: plugins ?? {} }); const effect = Effect.gen(this, function* () { const pluginService = yield* PluginService; const cachedPlugin = this.pluginCache.get(cacheKey); if (cachedPlugin) { this.pluginCache.delete(cacheKey); const pluginResult = yield* cachedPlugin.pipe(Effect.catchAll(() => Effect.succeed(null))); if (pluginResult) yield* pluginService.shutdownPlugin(pluginResult).pipe(Effect.catchAll((error) => Effect.logWarning(`Failed to shutdown evicted plugin ${pluginId}`, error))); } }).pipe(Effect.catchAll((error) => Effect.logWarning(`Plugin eviction failed for ${pluginId}`, error))); return this.runPromise(effect); } }; /** * Normalizes a remote URL to ensure it points to remoteEntry.js * If the URL doesn't end with a file extension, appends /remoteEntry.js */ function normalizeRemoteUrl(url) { if (!url) return url; if (url.endsWith(".js") || url.endsWith(".json")) return url; return `${url.endsWith("/") ? url.slice(0, -1) : url}/remoteEntry.js`; } /** * Extract plugin map (module constructors) from registry entries */ function extractPluginMap(registry) { const pluginMap = {}; for (const [pluginId, entry] of Object.entries(registry)) if ("module" in entry && entry.module) pluginMap[pluginId] = entry.module; return pluginMap; } /** * Normalize registry entries - ensure remote URLs are properly formatted */ function normalizeRegistry(registry) { const normalized = {}; for (const [pluginId, entry] of Object.entries(registry)) if ("module" in entry) normalized[pluginId] = { ...entry, remote: entry.remote ? normalizeRemoteUrl(entry.remote) : void 0 }; else normalized[pluginId] = { ...entry, remote: normalizeRemoteUrl(entry.remote) }; return normalized; } /** * Creates a plugin runtime with support for both module and remote plugin entries. * * @example * ```typescript * // With module entries (types inferred automatically) * const runtime = createPluginRuntime({ * registry: { * telegram: { module: TelegramPlugin }, * gopher: { remote: "https://cdn.example.com/gopher/remoteEntry.js" } * }, * secrets: { API_KEY: "..." } * }); * * // Types are automatically inferred from module entries! * const { router } = await runtime.usePlugin("telegram", config); * ``` */ function createPluginRuntime(config) { const secrets = config.secrets || {}; const normalizedRegistry = normalizeRegistry(config.registry); const pluginMap = extractPluginMap(config.registry); const layer = PluginService.Live(normalizedRegistry, secrets, pluginMap); const runtime = ManagedRuntime.make(layer); return new PluginRuntime(runtime, normalizedRegistry); } //#endregion export { PluginRuntime, createPluginRuntime }; //# sourceMappingURL=index.mjs.map