every-plugin
Version:
162 lines (160 loc) • 5.89 kB
JavaScript
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