/** * ThreadForge Plugin System * * Plugins add infrastructure to your app without boilerplate. * They handle connection management, health checks, nginx routing, * graceful shutdown, and inject clients into every service. * * ═══════════════════════════════════════════════════════════════ * USAGE * ═══════════════════════════════════════════════════════════════ * * // forge.config.ts * import { defineServices } from 'threadforge'; * import { redis, postgres, s3 } from 'threadforge/plugins'; * * export default defineServices({ * plugins: [ * redis({ url: 'redis://localhost:6379' }), * postgres({ url: 'postgres://user:pass@localhost:5432/mydb' }), * s3({ bucket: 'my-uploads', region: 'us-east-1' }), * ], * * users: { * entry: './services/users.ts', * port: 3001, * plugins: ['redis', 'postgres'], // opt-in per service * }, * * uploads: { * entry: './services/uploads.ts', * port: 3002, * plugins: ['s3'], * }, * }); * * In your service: * * @Prefix('/api/users') * class UserService extends Service { * async onStart(ctx) { * // Injected by plugins — ready to use, connection managed * const cached = await this.redis.get('user:123'); * const rows = await this.postgres.query('SELECT * FROM users WHERE id = $1', [123]); * } * } * * ═══════════════════════════════════════════════════════════════ * PLUGIN LIFECYCLE * ═══════════════════════════════════════════════════════════════ * * forge start * │ * ▼ Supervisor reads config, finds plugins * │ * ▼ plugin.validate() — check config, throw if bad * ▼ plugin.env() — env vars for workers (connection strings) * ▼ plugin.nginx() — nginx location blocks (admin UIs, proxying) * │ * ▼ Workers fork * │ * ▼ plugin.connect() — create client, connect, return instance * ▼ service.redis = client — injected onto service instance * ▼ service.onStart(ctx) — your code runs, clients ready * │ * ▼ plugin.healthCheck() — called by /health endpoint * │ * ▼ shutdown * ▼ plugin.disconnect() — close connections, drain pools * * ═══════════════════════════════════════════════════════════════ * WRITING A PLUGIN * ═══════════════════════════════════════════════════════════════ * * export function myPlugin(options) { * return { * name: 'myPlugin', * version: '1.0.0', * * // What property name to inject on the service (this.myPlugin) * inject: 'myPlugin', * * // Validate config before startup (fail fast) * validate() { ... }, * * // Env vars to pass to workers * env() { return { MY_PLUGIN_URL: options.url }; }, * * // Create and return the client (called once per worker) * async connect(ctx) { * const client = new MyClient(options.url); * await client.connect(); * return client; * }, * * // Health check (called by GET /health) * async healthCheck(client) { * await client.ping(); * return { status: 'ok' }; * }, * * // Clean shutdown * async disconnect(client) { * await client.quit(); * }, * * // Nginx config blocks (optional) * nginx() { * return { * upstreams: [{ name: 'myPlugin', host: 'localhost', port: 8080 }], * locations: [{ path: '/admin/myplugin', proxy_pass: 'myPlugin' }], * }; * }, * * // Prometheus metrics (optional) * metrics(client) { * return { pool_size: client.pool.totalCount }; * }, * }; * } */ import type { Logger, PluginContext, PluginManagerHealthCheckResult } from "./types.js"; interface NginxUpstream { name: string; host: string; port: number; } interface NginxLocation { path: string; proxy_pass: string; } interface NginxBlocks { upstreams: NginxUpstream[]; locations: NginxLocation[]; } interface NginxConfig { upstreams?: NginxUpstream[]; locations?: NginxLocation[]; } interface WsUpgradeResult { allow: boolean; statusCode?: number; reason?: string; } interface MetricsCollector { gauge(name: string, value: number): void; } type MiddlewareFunction = (req: unknown, res: unknown, next: () => void) => void; interface ForgePlugin { name: string; version?: string; inject: string; /** Preferred inject key (defaults to `inject`). When set, the client is * injected under this name AND the legacy `inject` name. Accessing the * legacy name logs a one-time deprecation warning. */ injectAs?: string; validate?: () => void; env?: () => Record; connect: (ctx: PluginContext) => Promise; healthCheck?: (client: unknown) => Promise; disconnect?: (client: unknown) => Promise; nginx?: () => NginxConfig; metrics?: (client: unknown) => Record; middleware?: (client: unknown) => MiddlewareFunction; onWsUpgrade?: (client: unknown, ctx: unknown) => Promise; onWsConnect?: (client: unknown, ctx: unknown) => Promise; onWsMessage?: (client: unknown, ctx: unknown) => Promise; onWsClose?: (client: unknown, ctx: unknown) => Promise; [key: string]: unknown; } interface WebSocketHook { name: string; onWsUpgrade?: (ctx: unknown) => Promise; onWsConnect?: (ctx: unknown) => Promise; onWsMessage?: (ctx: unknown) => Promise; onWsClose?: (ctx: unknown) => Promise; } interface DisconnectError { name: string; error: string; } interface PluginManagerOptions { logger?: Logger; } export declare class PluginManager { plugins: Map; clients: Map; _connecting: Map>; _logger: Logger; _deprecationWarned: Set; constructor(options?: PluginManagerOptions); register(plugins: ForgePlugin[] | null | undefined): void; validate(): void; env(): Record; connectForService(servicePlugins: string[] | null | undefined, ctx: PluginContext): Promise>; healthCheck(): Promise>; collectMetrics(metricsCollector: MetricsCollector): void; disconnectAll(logger?: Logger): Promise; /** * Set client on the result map under the primary inject key (injectAs ?? inject). * If the plugin defines injectAs and it differs from inject, also set a deprecation * proxy under the legacy inject key that logs a warning on first access. */ _setClientWithDeprecation(clients: Map, plugin: ForgePlugin, client: unknown, ctx: PluginContext): void; nginxBlocks(): NginxBlocks; getMiddleware(servicePlugins?: string[] | null): MiddlewareFunction[]; getWebSocketHooks(servicePlugins?: string[] | null): WebSocketHook[]; } export {}; //# sourceMappingURL=PluginManager.d.ts.map