every-plugin
Version:
255 lines (224 loc) • 8.5 kB
text/typescript
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>;
}),
};
}),
},
) {}