| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import type { PluginLifecycleManager } from "./plugin-lifecycle.js"; |
| import type { PluginJobScheduler } from "./plugin-job-scheduler.js"; |
| import type { PluginJobStore } from "./plugin-job-store.js"; |
| import { pluginRegistryService } from "./plugin-registry.js"; |
| import type { Db } from "@paperclipai/db"; |
| import { logger } from "../middleware/logger.js"; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| export interface PluginJobCoordinatorOptions { |
| |
| db: Db; |
| |
| lifecycle: PluginLifecycleManager; |
| |
| scheduler: PluginJobScheduler; |
| |
| jobStore: PluginJobStore; |
| } |
|
|
| |
| |
| |
| export interface PluginJobCoordinator { |
| |
| |
| |
| |
| |
| |
| start(): void; |
|
|
| |
| |
| |
| |
| |
| stop(): void; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function createPluginJobCoordinator( |
| options: PluginJobCoordinatorOptions, |
| ): PluginJobCoordinator { |
| const { db, lifecycle, scheduler, jobStore } = options; |
| const log = logger.child({ service: "plugin-job-coordinator" }); |
| const registry = pluginRegistryService(db); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| async function onPluginLoaded(payload: { pluginId: string; pluginKey: string }): Promise<void> { |
| const { pluginId, pluginKey } = payload; |
| log.info({ pluginId, pluginKey }, "plugin loaded — syncing jobs and registering with scheduler"); |
|
|
| try { |
| |
| const plugin = await registry.getById(pluginId); |
| if (!plugin?.manifestJson) { |
| log.warn({ pluginId, pluginKey }, "plugin loaded but no manifest found — skipping job sync"); |
| return; |
| } |
|
|
| |
| const manifest = plugin.manifestJson; |
| const jobDeclarations = manifest.jobs ?? []; |
|
|
| if (jobDeclarations.length > 0) { |
| log.info( |
| { pluginId, pluginKey, jobCount: jobDeclarations.length }, |
| "syncing job declarations from manifest", |
| ); |
| await jobStore.syncJobDeclarations(pluginId, jobDeclarations); |
| } |
|
|
| |
| await scheduler.registerPlugin(pluginId); |
| } catch (err) { |
| log.error( |
| { |
| pluginId, |
| pluginKey, |
| err: err instanceof Error ? err.message : String(err), |
| }, |
| "failed to sync jobs or register plugin with scheduler", |
| ); |
| } |
| } |
|
|
| |
| |
| |
| |
| async function onPluginDisabled(payload: { |
| pluginId: string; |
| pluginKey: string; |
| reason?: string; |
| }): Promise<void> { |
| const { pluginId, pluginKey, reason } = payload; |
| log.info( |
| { pluginId, pluginKey, reason }, |
| "plugin disabled — unregistering from scheduler", |
| ); |
|
|
| try { |
| await scheduler.unregisterPlugin(pluginId); |
| } catch (err) { |
| log.error( |
| { |
| pluginId, |
| pluginKey, |
| err: err instanceof Error ? err.message : String(err), |
| }, |
| "failed to unregister plugin from scheduler", |
| ); |
| } |
| } |
|
|
| |
| |
| |
| async function onPluginUnloaded(payload: { |
| pluginId: string; |
| pluginKey: string; |
| removeData: boolean; |
| }): Promise<void> { |
| const { pluginId, pluginKey, removeData } = payload; |
| log.info( |
| { pluginId, pluginKey, removeData }, |
| "plugin unloaded — unregistering from scheduler", |
| ); |
|
|
| try { |
| await scheduler.unregisterPlugin(pluginId); |
|
|
| |
| if (removeData) { |
| log.info({ pluginId, pluginKey }, "purging job data for uninstalled plugin"); |
| await jobStore.deleteAllJobs(pluginId); |
| } |
| } catch (err) { |
| log.error( |
| { |
| pluginId, |
| pluginKey, |
| err: err instanceof Error ? err.message : String(err), |
| }, |
| "failed to unregister plugin from scheduler during unload", |
| ); |
| } |
| } |
|
|
| |
| |
| |
|
|
| let attached = false; |
|
|
| |
| |
| |
| |
| const boundOnLoaded = (payload: { pluginId: string; pluginKey: string }) => { |
| void onPluginLoaded(payload); |
| }; |
| const boundOnDisabled = (payload: { pluginId: string; pluginKey: string; reason?: string }) => { |
| void onPluginDisabled(payload); |
| }; |
| const boundOnUnloaded = (payload: { pluginId: string; pluginKey: string; removeData: boolean }) => { |
| void onPluginUnloaded(payload); |
| }; |
|
|
| |
| |
| |
|
|
| return { |
| start(): void { |
| if (attached) return; |
| attached = true; |
|
|
| lifecycle.on("plugin.loaded", boundOnLoaded); |
| lifecycle.on("plugin.disabled", boundOnDisabled); |
| lifecycle.on("plugin.unloaded", boundOnUnloaded); |
|
|
| log.info("plugin job coordinator started — listening to lifecycle events"); |
| }, |
|
|
| stop(): void { |
| if (!attached) return; |
| attached = false; |
|
|
| lifecycle.off("plugin.loaded", boundOnLoaded); |
| lifecycle.off("plugin.disabled", boundOnDisabled); |
| lifecycle.off("plugin.unloaded", boundOnUnloaded); |
|
|
| log.info("plugin job coordinator stopped"); |
| }, |
| }; |
| } |
|
|