Skip to content

Commit 91e762b

Browse files
LALA
authored andcommitted
refactor(daemon): extract media task registry into media/task-registry.ts
Strangler-fig slice 2 of the server.ts decomposition (follows the startChatRun extraction in PR nexu-io#5128). Moves the in-memory media-task registry — the `mediaTasks` Map plus hydrate/create/persist/append/ notify/snapshot helpers and the TTL constant — out of server.ts into a new `media/task-registry.ts` sibling module, layered over the existing SQLite persistence in `media/tasks.ts`. Byte-identical move: function bodies are unchanged; server.ts imports the helpers back and wires them into startServer's boot rehydration and the media route deps object exactly as before. The four db helpers the moved code needs (get/insert/update/deleteMediaTask) now import into the new module and leave server.ts's import block; the three still used by startServer (listMediaTasksByProject, listRecentMediaTasks, reconcileMediaTasksOnBoot) stay. server.ts 6172 -> 6062 lines. Public/runtime behavior unchanged. Validation: tsc -p apps/daemon/tsconfig.json --noEmit clean; pnpm guard clean; chat-run-sse-shapes characterization 4/4; media tasks-persistence + tasks-routes suites green (14/14); policy-routes' 7 failures proven pre-existing (identical on base d20acc8).
1 parent 97a039d commit 91e762b

2 files changed

Lines changed: 209 additions & 121 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Authors: Leon Aburime using Claude Fable 5
2+
// @ts-nocheck — carried over verbatim from server.ts's file-level @ts-nocheck.
3+
// The moved bodies are untyped JS-in-TS; typing them is a later effort and new
4+
// sibling code must NOT copy this.
5+
/** @module media/task-registry
6+
* In-memory registry for long-running media-generation tasks, layered over the
7+
* SQLite persistence in `./tasks.ts`.
8+
*
9+
* `mediaTasks` holds the live task objects (each with a `waiters` Set of SSE
10+
* wakers) keyed by task id; the helpers here create/hydrate/persist those
11+
* objects, fan progress out to waiters, and garbage-collect terminal tasks
12+
* after a TTL. Every mutating helper takes the `db` handle explicitly so the
13+
* registry stays a pure move out of server.ts with no module-scope database.
14+
*
15+
* Extracted verbatim from apps/daemon/src/server.ts (strangler-fig slice 2).
16+
* server.ts imports these back and wires them into startServer's boot
17+
* rehydration and the media route deps object.
18+
*/
19+
20+
import {
21+
deleteMediaTask,
22+
getMediaTask,
23+
insertMediaTask,
24+
updateMediaTask,
25+
} from './tasks.js';
26+
27+
/**
28+
* Live registry of in-flight media tasks, keyed by task id. Each value carries
29+
* the persisted task fields plus an in-memory `waiters` Set of SSE wakers.
30+
*/
31+
export const mediaTasks = new Map();
32+
/**
33+
* How long a terminal (done/failed/interrupted) task stays resident before it
34+
* is evicted from `mediaTasks` and deleted from the database.
35+
*/
36+
export const TASK_TTL_AFTER_DONE_MS = 10 * 60 * 1000;
37+
/** Statuses that make a task eligible for TTL-based garbage collection. */
38+
const MEDIA_TERMINAL_STATUSES = new Set(['done', 'failed', 'interrupted']);
39+
40+
/**
41+
* Build a live task object from a persisted row and register it in
42+
* `mediaTasks`. Used on boot rehydration and on cache misses.
43+
* @param row Persisted media-task row from the database.
44+
* @returns The hydrated, registered task object.
45+
*/
46+
export function hydrateMediaTask(row) {
47+
const task = {
48+
id: row.id,
49+
projectId: row.projectId,
50+
status: row.status,
51+
surface: row.surface,
52+
model: row.model,
53+
progress: Array.isArray(row.progress) ? row.progress.slice() : [],
54+
file: row.file ?? null,
55+
error: row.error ?? null,
56+
startedAt: row.startedAt,
57+
endedAt: row.endedAt,
58+
waiters: new Set(),
59+
};
60+
mediaTasks.set(task.id, task);
61+
return task;
62+
}
63+
64+
/**
65+
* Resolve a task from the in-memory registry, falling back to the database and
66+
* hydrating on a cache miss.
67+
* @param db Database handle.
68+
* @param taskId Task id to look up.
69+
* @returns The live task object, or null if it does not exist.
70+
*/
71+
export function getLiveMediaTask(db, taskId) {
72+
const cached = mediaTasks.get(taskId);
73+
if (cached) return cached;
74+
const row = getMediaTask(db, taskId);
75+
return row ? hydrateMediaTask(row) : null;
76+
}
77+
78+
/**
79+
* Create a new queued media task, register it in memory, and insert the
80+
* persisted row.
81+
* @param db Database handle.
82+
* @param taskId Caller-assigned task id.
83+
* @param projectId Owning project id.
84+
* @param info Optional `{ surface, model }` seed metadata.
85+
* @returns The newly created live task object.
86+
*/
87+
export function createMediaTask(db, taskId, projectId, info = {}) {
88+
const task = {
89+
id: taskId,
90+
projectId,
91+
status: 'queued',
92+
surface: info.surface,
93+
model: info.model,
94+
progress: [],
95+
file: null,
96+
error: null,
97+
startedAt: Date.now(),
98+
endedAt: null,
99+
waiters: new Set(),
100+
};
101+
mediaTasks.set(taskId, task);
102+
insertMediaTask(db, {
103+
id: taskId,
104+
projectId,
105+
status: task.status,
106+
surface: task.surface,
107+
model: task.model,
108+
progress: task.progress,
109+
file: task.file,
110+
error: task.error,
111+
startedAt: task.startedAt,
112+
endedAt: task.endedAt,
113+
});
114+
return task;
115+
}
116+
117+
/**
118+
* Flush the current in-memory task state to the persisted row.
119+
* @param db Database handle.
120+
* @param task Live task object to persist.
121+
*/
122+
export function persistMediaTask(db, task) {
123+
updateMediaTask(db, task.id, {
124+
status: task.status,
125+
surface: task.surface,
126+
model: task.model,
127+
progress: task.progress,
128+
file: task.file,
129+
error: task.error,
130+
startedAt: task.startedAt,
131+
endedAt: task.endedAt,
132+
});
133+
}
134+
135+
/**
136+
* Append a progress line to a task, persist it, and wake any SSE waiters.
137+
* @param db Database handle.
138+
* @param task Live task object.
139+
* @param line Progress line to append.
140+
*/
141+
export function appendTaskProgress(db, task, line) {
142+
task.progress.push(line);
143+
persistMediaTask(db, task);
144+
notifyTaskWaiters(db, task);
145+
}
146+
147+
/**
148+
* Wake every SSE waiter registered on a task, then schedule TTL-based garbage
149+
* collection when the task has reached a terminal status.
150+
* @param db Database handle.
151+
* @param task Live task object whose waiters should be notified.
152+
*/
153+
export function notifyTaskWaiters(db, task) {
154+
const wakers = Array.from(task.waiters);
155+
for (const w of wakers) {
156+
try {
157+
w();
158+
} catch {
159+
// Never let one bad waiter block the rest.
160+
}
161+
}
162+
if (
163+
MEDIA_TERMINAL_STATUSES.has(task.status) &&
164+
!task._gcScheduled
165+
) {
166+
task._gcScheduled = true;
167+
setTimeout(() => {
168+
if (task.waiters.size === 0) {
169+
mediaTasks.delete(task.id);
170+
deleteMediaTask(db, task.id);
171+
}
172+
}, TASK_TTL_AFTER_DONE_MS).unref?.();
173+
}
174+
}
175+
176+
/**
177+
* Produce the client-facing snapshot of a task's state for the SSE stream,
178+
* emitting only the progress lines after `since`.
179+
* @param task Live task object.
180+
* @param since Progress index already delivered to the client.
181+
* @returns Snapshot `{ taskId, status, startedAt, endedAt, progress, nextSince }`
182+
* plus `file` on done and `error` on failed/interrupted.
183+
*/
184+
export function mediaTaskSnapshot(task, since = 0) {
185+
const snapshot = {
186+
taskId: task.id,
187+
status: task.status,
188+
startedAt: task.startedAt,
189+
endedAt: task.endedAt,
190+
progress: task.progress.slice(since),
191+
nextSince: task.progress.length,
192+
};
193+
if (task.status === 'done') snapshot.file = task.file;
194+
if (task.status === 'failed' || task.status === 'interrupted') {
195+
snapshot.error = task.error;
196+
}
197+
return snapshot;
198+
}

apps/daemon/src/server.ts

Lines changed: 11 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -390,14 +390,21 @@ import {
390390
} from './media/models.js';
391391
import { readMaskedConfig, writeConfig } from './media/config.js';
392392
import {
393-
deleteMediaTask,
394-
getMediaTask,
395-
insertMediaTask,
396393
listMediaTasksByProject,
397394
listRecentMediaTasks,
398395
reconcileMediaTasksOnBoot,
399-
updateMediaTask,
400396
} from './media/tasks.js';
397+
import {
398+
appendTaskProgress,
399+
createMediaTask,
400+
getLiveMediaTask,
401+
hydrateMediaTask,
402+
mediaTaskSnapshot,
403+
mediaTasks,
404+
notifyTaskWaiters,
405+
persistMediaTask,
406+
TASK_TTL_AFTER_DONE_MS,
407+
} from './media/task-registry.js';
401408
import {
402409
MCP_TEMPLATES,
403410
buildAcpMcpServers,
@@ -3182,123 +3189,6 @@ function sendMulterError(res, err) {
31823189
return sendApiError(res, 500, 'INTERNAL_ERROR', 'upload failed');
31833190
}
31843191

3185-
const mediaTasks = new Map();
3186-
const TASK_TTL_AFTER_DONE_MS = 10 * 60 * 1000;
3187-
const MEDIA_TERMINAL_STATUSES = new Set(['done', 'failed', 'interrupted']);
3188-
3189-
function hydrateMediaTask(row) {
3190-
const task = {
3191-
id: row.id,
3192-
projectId: row.projectId,
3193-
status: row.status,
3194-
surface: row.surface,
3195-
model: row.model,
3196-
progress: Array.isArray(row.progress) ? row.progress.slice() : [],
3197-
file: row.file ?? null,
3198-
error: row.error ?? null,
3199-
startedAt: row.startedAt,
3200-
endedAt: row.endedAt,
3201-
waiters: new Set(),
3202-
};
3203-
mediaTasks.set(task.id, task);
3204-
return task;
3205-
}
3206-
3207-
function getLiveMediaTask(db, taskId) {
3208-
const cached = mediaTasks.get(taskId);
3209-
if (cached) return cached;
3210-
const row = getMediaTask(db, taskId);
3211-
return row ? hydrateMediaTask(row) : null;
3212-
}
3213-
3214-
function createMediaTask(db, taskId, projectId, info = {}) {
3215-
const task = {
3216-
id: taskId,
3217-
projectId,
3218-
status: 'queued',
3219-
surface: info.surface,
3220-
model: info.model,
3221-
progress: [],
3222-
file: null,
3223-
error: null,
3224-
startedAt: Date.now(),
3225-
endedAt: null,
3226-
waiters: new Set(),
3227-
};
3228-
mediaTasks.set(taskId, task);
3229-
insertMediaTask(db, {
3230-
id: taskId,
3231-
projectId,
3232-
status: task.status,
3233-
surface: task.surface,
3234-
model: task.model,
3235-
progress: task.progress,
3236-
file: task.file,
3237-
error: task.error,
3238-
startedAt: task.startedAt,
3239-
endedAt: task.endedAt,
3240-
});
3241-
return task;
3242-
}
3243-
3244-
function persistMediaTask(db, task) {
3245-
updateMediaTask(db, task.id, {
3246-
status: task.status,
3247-
surface: task.surface,
3248-
model: task.model,
3249-
progress: task.progress,
3250-
file: task.file,
3251-
error: task.error,
3252-
startedAt: task.startedAt,
3253-
endedAt: task.endedAt,
3254-
});
3255-
}
3256-
3257-
function appendTaskProgress(db, task, line) {
3258-
task.progress.push(line);
3259-
persistMediaTask(db, task);
3260-
notifyTaskWaiters(db, task);
3261-
}
3262-
3263-
function notifyTaskWaiters(db, task) {
3264-
const wakers = Array.from(task.waiters);
3265-
for (const w of wakers) {
3266-
try {
3267-
w();
3268-
} catch {
3269-
// Never let one bad waiter block the rest.
3270-
}
3271-
}
3272-
if (
3273-
MEDIA_TERMINAL_STATUSES.has(task.status) &&
3274-
!task._gcScheduled
3275-
) {
3276-
task._gcScheduled = true;
3277-
setTimeout(() => {
3278-
if (task.waiters.size === 0) {
3279-
mediaTasks.delete(task.id);
3280-
deleteMediaTask(db, task.id);
3281-
}
3282-
}, TASK_TTL_AFTER_DONE_MS).unref?.();
3283-
}
3284-
}
3285-
3286-
function mediaTaskSnapshot(task, since = 0) {
3287-
const snapshot = {
3288-
taskId: task.id,
3289-
status: task.status,
3290-
startedAt: task.startedAt,
3291-
endedAt: task.endedAt,
3292-
progress: task.progress.slice(since),
3293-
nextSince: task.progress.length,
3294-
};
3295-
if (task.status === 'done') snapshot.file = task.file;
3296-
if (task.status === 'failed' || task.status === 'interrupted') {
3297-
snapshot.error = task.error;
3298-
}
3299-
return snapshot;
3300-
}
3301-
33023192
export function createSseResponse(
33033193
res,
33043194
{ keepAliveIntervalMs = SSE_KEEPALIVE_INTERVAL_MS } = {},

0 commit comments

Comments
 (0)