task logs: migrate from filesystem to postgresql; refactor sidecars into submodules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:49:39 +00:00
co-authored by Claude Opus 4.6
parent 5129f7827f
commit d88fe3cac7
22 changed files with 621 additions and 545 deletions
+100
View File
@@ -0,0 +1,100 @@
import type { SidecarEvent } from '../protocol';
import type { Job, EnqueueParams } from '../../queue/types';
import { initEmailCron, stopEmailCron } from './email-cron';
import { createSidecarConnector } from '../connect';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
// ── Queue access via WS ──
let reqCounter = 0;
const pendingQueue = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: Timer }>();
function nextQueueId(): string {
return `eq_${Date.now()}_${++reqCounter}`;
}
function sendQueueCommand(cmd: Record<string, unknown>): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = cmd.id as string;
const timer = setTimeout(() => {
pendingQueue.delete(id);
reject(new Error(`Queue command ${cmd.type} timed out`));
}, 30_000);
pendingQueue.set(id, { resolve, reject, timer });
connection.send(cmd as SidecarEvent);
});
}
async function enqueueViaWs(params: EnqueueParams): Promise<Job> {
const res = (await sendQueueCommand({ type: 'queue:enqueue', id: nextQueueId(), params })) as Record<string, unknown>;
if (res.type === 'queue:enqueued') return res.job as Job;
if (res.type === 'queue:error') throw new Error(res.error as string);
throw new Error('Unexpected response');
}
async function listJobsViaWs(): Promise<Job[]> {
const res = (await sendQueueCommand({ type: 'queue:list', id: nextQueueId() })) as Record<string, unknown>;
if (res.type === 'queue:list') return res.jobs as Job[];
throw new Error('Unexpected response');
}
// ── Command handlers ──
type ReplyFn = (msg: SidecarEvent) => void;
function handleCommand(cmd: Record<string, unknown>, reply: ReplyFn) {
switch (cmd.type) {
case 'ping':
reply({ type: 'pong', id: cmd.id as string });
break;
default:
// Check if this is a queue response (from API server responding to our queue commands)
if (
typeof cmd.type === 'string' &&
cmd.type.startsWith('queue:') &&
cmd.id &&
pendingQueue.has(cmd.id as string)
) {
const pending = pendingQueue.get(cmd.id as string)!;
pendingQueue.delete(cmd.id as string);
clearTimeout(pending.timer);
pending.resolve(cmd);
return;
}
reply({
type: 'error',
id: cmd.id as string,
error: `Unknown command type: ${cmd.type}`,
});
}
}
// ── Connect to API server ──
const connection = createSidecarConnector({
apiUrl: `${API_URL}/api/sidecar/register`,
name: 'email',
capabilities: ['email'],
onCommand(cmd, reply) {
handleCommand(cmd as Record<string, unknown>, reply as ReplyFn);
},
onConnected() {
// Start email cron once connected (so queue commands can reach API server)
// initEmailCron({ enqueue: enqueueViaWs, listJobs: listJobsViaWs }); // TODO: re-enable after testing
},
});
// ── Graceful shutdown ──
function shutdown(signal: string) {
console.log(`[email] ${signal} received, shutting down...`);
stopEmailCron();
connection.destroy();
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));