UNPKG

every-plugin

Version:
162 lines (160 loc) 5.89 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 { __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); return yield* pluginService.initializePlugin(instance, config, plugins); }).pipe(Effect.provide(this.runtime)); cachedPlugin = Effect.cached(operation).pipe(Effect.flatten); this.pluginCache.set(cacheKey, cachedPlugin); } const initialized = await this.runPromise(cachedPlugin); const createClient = (context) => { return createRouterClient(initialized.plugin.createRouter(initialized.context), { context: context ?? {} }); }; return { createClient, router: initialized.plugin.createRouter(initialized.context), metadata: initialized.metadata, initialized }; } async loadPlugin(pluginId) { const effect = Effect.gen(function* () { return yield* (yield* PluginService).loadPlugin(pluginId); }); return this.runPromise(effect); } async instantiatePlugin(pluginId, loadedPlugin) { const effect = Effect.gen(function* () { return yield* (yield* PluginService).instantiatePlugin(pluginId, loadedPlugin); }); return this.runPromise(effect); } async initializePlugin(instance, config, plugins) { const effect = Effect.gen(function* () { return yield* (yield* PluginService).initializePlugin(instance, config, plugins); }); 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) { const cacheKey = this.generateCacheKey(pluginId, config); 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); return new PluginRuntime(ManagedRuntime.make(layer), normalizedRegistry); } //#endregion export { PluginRuntime, createPluginRuntime }; //# sourceMappingURL=index.mjs.map