Merge pull request #20 from techops-services/feat/plugin-cron-integration
feat(gateway): load plugin cron jobs on startup
This commit is contained in:
commit
5a4a164a11
159
src/gateway/server-plugin-cron.ts
Normal file
159
src/gateway/server-plugin-cron.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* Plugin cron job integration.
|
||||||
|
* Discovers and registers cron jobs from plugins that expose a `*.getCronJobs` gateway method.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CronService } from "../cron/service.js";
|
||||||
|
import type { CronJobCreate } from "../cron/types.js";
|
||||||
|
import type { GatewayRequestHandler } from "./server-methods/types.js";
|
||||||
|
|
||||||
|
type PluginCronResult = {
|
||||||
|
jobs?: CronJobCreate[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a plugin gateway handler that returns cron jobs.
|
||||||
|
* Plugin handlers may either:
|
||||||
|
* 1. Return the result directly (simpler pattern used by some plugins)
|
||||||
|
* 2. Call respond() with the result (standard gateway handler pattern)
|
||||||
|
*
|
||||||
|
* This helper supports both patterns.
|
||||||
|
*/
|
||||||
|
async function callCronJobsHandler(handler: GatewayRequestHandler): Promise<PluginCronResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
// Create a respond function that captures the result
|
||||||
|
const respond = (ok: boolean, payload?: unknown) => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
resolve(ok ? (payload as PluginCronResult) : { jobs: [] });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create minimal mock options for the handler
|
||||||
|
const mockOpts = {
|
||||||
|
req: { type: "req" as const, method: "getCronJobs", id: "plugin-cron-init" },
|
||||||
|
params: {},
|
||||||
|
client: null,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
|
respond,
|
||||||
|
context: {} as never, // Handlers that need context won't work, but getCronJobs doesn't need it
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the handler - it may return a value or call respond()
|
||||||
|
const result = handler(mockOpts);
|
||||||
|
|
||||||
|
// Handle direct return value (async or sync)
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
result
|
||||||
|
.then((value) => {
|
||||||
|
if (!resolved && value !== undefined) {
|
||||||
|
resolved = true;
|
||||||
|
resolve(value as PluginCronResult);
|
||||||
|
} else if (!resolved) {
|
||||||
|
// Handler completed without returning or calling respond - return empty
|
||||||
|
resolved = true;
|
||||||
|
resolve({ jobs: [] });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
resolve({ jobs: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!resolved && result !== undefined) {
|
||||||
|
// Sync handler returned a value
|
||||||
|
resolved = true;
|
||||||
|
resolve(result as PluginCronResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback timeout in case handler doesn't respond
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
resolve({ jobs: [] });
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers and registers cron jobs from plugins.
|
||||||
|
* Looks for gateway methods matching the pattern `*.getCronJobs` and calls them
|
||||||
|
* to retrieve cron job configurations, then registers those jobs with the cron service.
|
||||||
|
*
|
||||||
|
* @param params.cron - The cron service to register jobs with
|
||||||
|
* @param params.handlers - Plugin gateway handlers to scan for getCronJobs methods
|
||||||
|
* @param params.log - Logger for status messages
|
||||||
|
*/
|
||||||
|
export async function loadPluginCronJobs(params: {
|
||||||
|
cron: CronService;
|
||||||
|
handlers: Record<string, GatewayRequestHandler>;
|
||||||
|
log: {
|
||||||
|
info: (msg: string) => void;
|
||||||
|
warn: (msg: string) => void;
|
||||||
|
error: (msg: string) => void;
|
||||||
|
};
|
||||||
|
}): Promise<void> {
|
||||||
|
const { cron, handlers, log } = params;
|
||||||
|
|
||||||
|
// Find all handlers matching *.getCronJobs pattern
|
||||||
|
const cronMethods = Object.entries(handlers).filter(([method]) =>
|
||||||
|
method.endsWith(".getCronJobs"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cronMethods.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalJobs = 0;
|
||||||
|
|
||||||
|
for (const [method, handler] of cronMethods) {
|
||||||
|
const pluginId = method.replace(".getCronJobs", "");
|
||||||
|
try {
|
||||||
|
// Call the gateway method to get cron jobs
|
||||||
|
const result = await callCronJobsHandler(handler);
|
||||||
|
const jobs = result?.jobs;
|
||||||
|
|
||||||
|
if (!Array.isArray(jobs) || jobs.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register each job with the cron service
|
||||||
|
for (const job of jobs) {
|
||||||
|
try {
|
||||||
|
// Check if job already exists (by name)
|
||||||
|
const existing = await cron.list();
|
||||||
|
const existingJob = existing.find((j) => j.name === job.name);
|
||||||
|
|
||||||
|
if (existingJob) {
|
||||||
|
// Update existing job schedule if it changed
|
||||||
|
if (
|
||||||
|
JSON.stringify(existingJob.schedule) !== JSON.stringify(job.schedule) ||
|
||||||
|
existingJob.enabled !== job.enabled
|
||||||
|
) {
|
||||||
|
await cron.update(existingJob.id, {
|
||||||
|
schedule: job.schedule,
|
||||||
|
enabled: job.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add new job
|
||||||
|
await cron.add(job);
|
||||||
|
totalJobs++;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(`[${pluginId}] failed to register cron job "${job.name}": ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`[${pluginId}] getCronJobs failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalJobs > 0) {
|
||||||
|
log.info(`registered ${totalJobs} cron job(s) from plugins`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -52,6 +52,7 @@ import { createGatewayCloseHandler } from "./server-close.js";
|
|||||||
import { buildGatewayCronService } from "./server-cron.js";
|
import { buildGatewayCronService } from "./server-cron.js";
|
||||||
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
|
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
|
||||||
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
|
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
|
||||||
|
import { loadPluginCronJobs } from "./server-plugin-cron.js";
|
||||||
import { coreGatewayHandlers } from "./server-methods.js";
|
import { coreGatewayHandlers } from "./server-methods.js";
|
||||||
import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js";
|
import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js";
|
||||||
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
|
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
|
||||||
@ -504,6 +505,18 @@ export async function startGatewayServer(
|
|||||||
logBrowser,
|
logBrowser,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Load cron jobs from plugins that expose a *.getCronJobs gateway method.
|
||||||
|
// This runs after plugin services start so storage is initialized.
|
||||||
|
void loadPluginCronJobs({
|
||||||
|
cron,
|
||||||
|
handlers: pluginRegistry.gatewayHandlers,
|
||||||
|
log: {
|
||||||
|
info: (msg) => logCron.info(msg),
|
||||||
|
warn: (msg) => logCron.warn(msg),
|
||||||
|
error: (msg) => logCron.error(msg),
|
||||||
|
},
|
||||||
|
}).catch((err) => logCron.error(`failed to load plugin cron jobs: ${String(err)}`));
|
||||||
|
|
||||||
const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
|
const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
|
||||||
deps,
|
deps,
|
||||||
broadcast,
|
broadcast,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user