UNPKG

every-plugin

Version:
255 lines (224 loc) 8.5 kB
import type { InferSchemaInput, InferSchemaOutput } from "@orpc/contract"; import { Context, Effect, Scope } from "effect"; import type { z } from "zod"; import type { AnyPlugin, AnyPluginConstructor, InitializedPlugin, LoadedPlugin, PluginInstance, PluginMetadata, PluginRegistry, } from "../../types"; import { PluginRuntimeError, toPluginRuntimeError } from "../errors"; import { validate } from "../validation"; import { ModuleFederationService } from "./module-federation.service"; import { SecretsService } from "./secrets.service"; export class PluginRegistryTag extends Context.Tag("PluginRegistry")< PluginRegistryTag, PluginRegistry >() {} export class PluginMapTag extends Context.Tag("PluginMap")< PluginMapTag, Record<string, AnyPluginConstructor> >() {} export class RegistryService extends Effect.Service<RegistryService>()("RegistryService", { effect: Effect.gen(function* () { const registry = yield* PluginRegistryTag; const pluginMap = yield* PluginMapTag; return { get: (pluginId: string) => Effect.gen(function* () { const entry = registry[pluginId]; if (!entry) { return yield* Effect.fail( new PluginRuntimeError({ pluginId, operation: "validate-plugin-id", cause: new Error(`Plugin ${pluginId} not found in registry`), retryable: false, }), ); } if ("module" in entry) { return { constructor: entry.module, metadata: { remoteUrl: entry.remote || "", version: entry.version, description: entry.description, } as PluginMetadata, }; } return { constructor: null, metadata: { remoteUrl: entry.remote, version: entry.version, description: entry.description, } as PluginMetadata, }; }), getModule: (pluginId: string) => Effect.succeed(pluginMap[pluginId] || null), }; }), }) {} export class PluginLoaderService extends Effect.Service<PluginLoaderService>()( "PluginLoaderService", { effect: Effect.gen(function* () { const moduleFederationService = yield* ModuleFederationService; const secretsService = yield* SecretsService; const registryService = yield* RegistryService; const resolveUrl = (baseUrl: string, version?: string): string => version && version !== "latest" ? baseUrl.replace("@latest", `@${version}`) : baseUrl; return { loadPlugin: (pluginId: string) => Effect.gen(function* () { const entry = yield* registryService.get(pluginId); if (entry.constructor) { yield* Effect.logDebug("Loading plugin from direct module", { pluginId }); return { ctor: entry.constructor, metadata: entry.metadata, } satisfies LoadedPlugin; } const url = entry.metadata.remoteUrl; if (!url) { return yield* Effect.fail( new PluginRuntimeError({ pluginId, operation: "load-plugin", cause: new Error(`Plugin ${pluginId} has no module or remote URL configured`), retryable: false, }), ); } const resolvedUrl = resolveUrl(url); yield* moduleFederationService .registerRemote(pluginId, resolvedUrl) .pipe( Effect.mapError((error) => toPluginRuntimeError(error, pluginId, undefined, "register-remote", true), ), ); yield* Effect.logDebug("Loading plugin from remote", { pluginId, url: resolvedUrl }); const ctor = yield* moduleFederationService .loadRemoteConstructor(pluginId, resolvedUrl) .pipe( Effect.mapError((error) => toPluginRuntimeError(error, pluginId, undefined, "load-remote", false), ), ); return { ctor, metadata: entry.metadata, } satisfies LoadedPlugin; }), instantiatePlugin: <T extends AnyPlugin>(pluginId: string, loadedPlugin: LoadedPlugin<T>) => Effect.gen(function* () { const instance = yield* Effect.try(() => new loadedPlugin.ctor()).pipe( Effect.mapError((error) => toPluginRuntimeError(error, pluginId, undefined, "instantiate-plugin", false), ), ); (instance.id as string) = pluginId; return { plugin: instance, metadata: loadedPlugin.metadata, } satisfies PluginInstance<T>; }), initializePlugin: <T extends AnyPlugin>( pluginInstance: PluginInstance<T>, config: { variables: InferSchemaInput<T["configSchema"]["variables"]>; secrets: InferSchemaInput<T["configSchema"]["secrets"]>; }, plugins?: Record<string, unknown>, ) => Effect.gen(function* () { const { plugin } = pluginInstance; // Validate and hydrate config const validatedVariables = yield* validate( plugin.configSchema.variables as z.ZodSchema< InferSchemaOutput<T["configSchema"]["variables"]> >, config.variables, plugin.id, "config", ).pipe( Effect.mapError( (validationError) => new PluginRuntimeError({ pluginId: plugin.id, operation: "validate-config", cause: validationError.zodError, retryable: false, }), ), ); // Validate secrets const validatedSecrets = yield* validate( plugin.configSchema.secrets as z.ZodSchema< InferSchemaOutput<T["configSchema"]["secrets"]> >, config.secrets, plugin.id, "config", ).pipe( Effect.mapError( (validationError) => new PluginRuntimeError({ pluginId: plugin.id, operation: "validate-secrets", cause: validationError.zodError, retryable: false, }), ), ); // Hydrate secrets in variables const hydratedConfig = yield* secretsService.hydrateSecrets({ variables: validatedVariables, secrets: validatedSecrets, }); const _variables = yield* validate( plugin.configSchema.variables as z.ZodSchema< InferSchemaOutput<T["configSchema"]["variables"]> >, hydratedConfig.variables, plugin.id, "config", ).pipe( Effect.mapError( (validationError) => new PluginRuntimeError({ pluginId: plugin.id, operation: "validate-hydrated-config", cause: validationError.zodError, retryable: false, }), ), ); // Create a long-lived scope for this plugin instance const scope = yield* Scope.make(); // Initialize plugin within the scope const context = yield* plugin .initialize({ variables: _variables, secrets: hydratedConfig.secrets }, plugins ?? {}) .pipe( Effect.provideService(Scope.Scope, scope), Effect.mapError((error) => toPluginRuntimeError(error, plugin.id, undefined, "initialize-plugin", false), ), ); return { plugin, metadata: pluginInstance.metadata, config: { variables: _variables, secrets: hydratedConfig.secrets }, context, scope, } satisfies InitializedPlugin<T>; }), }; }), }, ) {}