* feat: add packages/dispatcher — dual-loop OGraph projection watcher + OC scheduler Adds a new Node.js daemon that: - Loop A (ProjectionWatcher): polls OGraph projections, diffs against snapshot, merges changes into a pending queue. - Idle: 30s poll interval; active (changes detected): 5s - Loop B (OcScheduler): polls OC session-status, pushes pending queue when OC has available slots (>= minAvailable). - Idle (no pending): 60s; active (pending): 5s - Cooldown of 60s after each push to avoid spam Tech: - TypeScript + esbuild (zero runtime external deps) - Graceful error handling: each poll is independent try-catch, errors logged but never crash the process - Config from ~/.config/ograph/dispatcher.json + env-var overrides - OGRAPH_CONFIG_FILE env var for config path override - Push via /tmp/ograph-dispatch.json + openclaw message send (best-effort) Build: npm run build → dist/index.js Run: node dist/index.js * fix: address PR #17 review — package name, tests, shell safety, first-run --------- Co-authored-by: 小墨 <xiaomooo@shazhou.work>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
// OGraph Dispatcher — entry point
|
|
// Starts Loop A (ProjectionWatcher) and Loop B (OcScheduler) independently.
|
|
|
|
import { loadConfig } from './config.js';
|
|
import { ProjectionWatcher } from './watcher.js';
|
|
import { OcScheduler } from './scheduler.js';
|
|
import type { PendingEntry } from './types.js';
|
|
|
|
function ts(): string {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
console.log(`[${ts()}] [dispatcher] OGraph Dispatcher starting...`);
|
|
|
|
const config = loadConfig();
|
|
|
|
console.log(`[${ts()}] [dispatcher] config loaded`);
|
|
console.log(`[${ts()}] [dispatcher] ograph.endpoint = ${config.ograph.endpoint}`);
|
|
console.log(`[${ts()}] [dispatcher] ograph.projections = [${config.ograph.projections.join(', ')}]`);
|
|
console.log(`[${ts()}] [dispatcher] oc.statusEndpoint = ${config.oc.statusEndpoint}`);
|
|
console.log(`[${ts()}] [dispatcher] oc.minAvailable = ${config.oc.minAvailable}`);
|
|
console.log(`[${ts()}] [dispatcher] intervals.watcherIdle = ${config.intervals.watcherIdle}ms`);
|
|
console.log(`[${ts()}] [dispatcher] intervals.watcherActive = ${config.intervals.watcherActive}ms`);
|
|
console.log(`[${ts()}] [dispatcher] intervals.schedulerIdle = ${config.intervals.schedulerIdle}ms`);
|
|
console.log(`[${ts()}] [dispatcher] intervals.schedulerActive= ${config.intervals.schedulerActive}ms`);
|
|
console.log(`[${ts()}] [dispatcher] intervals.cooldownAfterPush = ${config.intervals.cooldownAfterPush}ms`);
|
|
|
|
// Shared pending queue — Watcher writes, Scheduler reads + clears
|
|
const pending: Map<string, PendingEntry> = new Map();
|
|
|
|
const watcher = new ProjectionWatcher(config, pending);
|
|
const scheduler = new OcScheduler(config, pending);
|
|
|
|
// Start both loops independently
|
|
watcher.start();
|
|
scheduler.start();
|
|
|
|
console.log(`[${ts()}] [dispatcher] both loops running. Press Ctrl+C to stop.`);
|
|
|
|
// Graceful shutdown
|
|
const shutdown = (): void => {
|
|
console.log(`\n[${ts()}] [dispatcher] shutting down...`);
|
|
watcher.stop();
|
|
scheduler.stop();
|
|
process.exit(0);
|
|
};
|
|
|
|
process.on('SIGINT', shutdown);
|
|
process.on('SIGTERM', shutdown);
|
|
|
|
// Keep the process alive (the timers are enough, but be explicit)
|
|
await new Promise<void>(() => {
|
|
// never resolves — loops keep running via setTimeout chains
|
|
});
|
|
}
|
|
|
|
main().catch((err: unknown) => {
|
|
console.error(`[${new Date().toISOString()}] [dispatcher] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
process.exit(1);
|
|
});
|