mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
feat(apps): run an app's own agents when its data/config.json changes
Apps store user settings in data/config.json (e.g. pr-dashboard's tracked repo) and rely on their bundled agents to turn config into data. Without this, changing a setting meant staring at an empty app until the next cron tick. The apps-server watcher now debounces config.json writes and fires a one-shot manual run of the app's owned tasks (app--<slug>--*). Generic for any app; dynamic import keeps the server decoupled from the bg-task runner at load time.
This commit is contained in:
parent
7d38dc84a2
commit
5df26e6fca
1 changed files with 37 additions and 0 deletions
|
|
@ -683,6 +683,38 @@ function slugFromAbsolutePath(absolutePath: string): { slug: string; rel: string
|
||||||
return { slug, rel: segments.slice(1).join('/') };
|
return { slug, rel: segments.slice(1).join('/') };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config-change → one-shot agent run. An app writes data/config.json when the
|
||||||
|
// user changes its settings (e.g. picks the repo to track); its bundled agents
|
||||||
|
// are what turn that config into fresh data. Without this kick the user would
|
||||||
|
// stare at an empty app until the next cron tick. Generic: any app, any agent
|
||||||
|
// the app owns (app--<slug>--*). Dynamic import keeps apps/server decoupled
|
||||||
|
// from the bg-task runner at module-load time.
|
||||||
|
const agentKickTimers = new Map<string, NodeJS.Timeout>();
|
||||||
|
function scheduleAgentKick(slug: string): void {
|
||||||
|
const existing = agentKickTimers.get(slug);
|
||||||
|
if (existing) clearTimeout(existing);
|
||||||
|
agentKickTimers.set(slug, setTimeout(() => {
|
||||||
|
agentKickTimers.delete(slug);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const tasksDir = path.join(path.dirname(APPS_DIR), 'bg-tasks');
|
||||||
|
const entries = await fsp.readdir(tasksDir).catch(() => [] as string[]);
|
||||||
|
const owned = entries.filter((e) => e.startsWith(`app--${slug}--`));
|
||||||
|
if (!owned.length) return;
|
||||||
|
const { runBackgroundTask } = await import('../background-tasks/runner.js');
|
||||||
|
for (const taskSlug of owned) {
|
||||||
|
console.log(`[Apps] ${slug}/data/config.json changed — running ${taskSlug}`);
|
||||||
|
void runBackgroundTask(taskSlug, 'manual').catch((e: unknown) => {
|
||||||
|
console.warn(`[Apps] config-change agent run failed for ${taskSlug}:`, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[Apps] config-change agent kick failed for ${slug}:`, e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, 400));
|
||||||
|
}
|
||||||
|
|
||||||
async function startWatcher(): Promise<void> {
|
async function startWatcher(): Promise<void> {
|
||||||
if (watcher) return;
|
if (watcher) return;
|
||||||
const w = chokidar.watch(APPS_DIR, {
|
const w = chokidar.watch(APPS_DIR, {
|
||||||
|
|
@ -695,6 +727,9 @@ async function startWatcher(): Promise<void> {
|
||||||
if (!hit || hit.rel.endsWith('.tmp') || /\.tmp-[0-9a-f]+$/.test(hit.rel)) return;
|
if (!hit || hit.rel.endsWith('.tmp') || /\.tmp-[0-9a-f]+$/.test(hit.rel)) return;
|
||||||
const area: 'dist' | 'data' = hit.rel === 'data' || hit.rel.startsWith('data/') ? 'data' : 'dist';
|
const area: 'dist' | 'data' = hit.rel === 'data' || hit.rel.startsWith('data/') ? 'data' : 'dist';
|
||||||
scheduleChangeBroadcast(hit.slug, area, hit.rel);
|
scheduleChangeBroadcast(hit.slug, area, hit.rel);
|
||||||
|
if (hit.rel === 'data/config.json' && (eventName === 'add' || eventName === 'change')) {
|
||||||
|
scheduleAgentKick(hit.slug);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
w.on('error', (error: unknown) => {
|
w.on('error', (error: unknown) => {
|
||||||
console.error('[Apps] watcher error:', error);
|
console.error('[Apps] watcher error:', error);
|
||||||
|
|
@ -795,6 +830,8 @@ export async function shutdown(): Promise<void> {
|
||||||
|
|
||||||
for (const timer of reloadTimers.values()) clearTimeout(timer);
|
for (const timer of reloadTimers.values()) clearTimeout(timer);
|
||||||
reloadTimers.clear();
|
reloadTimers.clear();
|
||||||
|
for (const timer of agentKickTimers.values()) clearTimeout(timer);
|
||||||
|
agentKickTimers.clear();
|
||||||
|
|
||||||
for (const clients of eventClients.values()) {
|
for (const clients of eventClients.values()) {
|
||||||
for (const res of clients) {
|
for (const res of clients) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue