feat: migrate task jobs to workflow notifications + timeline
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { getWorld } from 'workflow/runtime';
|
||||
import type {
|
||||
Filing,
|
||||
FinancialHistoryWindow,
|
||||
@@ -31,9 +32,12 @@ import {
|
||||
import { getPriceHistory, getQuote } from '@/lib/server/prices';
|
||||
import {
|
||||
enqueueTask,
|
||||
findInFlightTask,
|
||||
getTaskById,
|
||||
getTaskTimeline,
|
||||
getTaskQueueSnapshot,
|
||||
listRecentTasks
|
||||
listRecentTasks,
|
||||
updateTaskNotification
|
||||
} from '@/lib/server/tasks';
|
||||
|
||||
const ALLOWED_STATUSES: TaskStatus[] = ['queued', 'running', 'completed', 'failed'];
|
||||
@@ -120,7 +124,8 @@ async function queueAutoFilingSync(userId: string, ticker: string) {
|
||||
ticker,
|
||||
limit: AUTO_FILING_SYNC_LIMIT
|
||||
},
|
||||
priority: 90
|
||||
priority: 90,
|
||||
resourceKey: `sync_filings:${ticker}`
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -132,18 +137,63 @@ async function queueAutoFilingSync(userId: string, ticker: string) {
|
||||
|
||||
const authHandler = ({ request }: { request: Request }) => auth.handler(request);
|
||||
|
||||
async function checkWorkflowBackend() {
|
||||
try {
|
||||
const world = getWorld();
|
||||
await world.runs.list({
|
||||
pagination: { limit: 1 },
|
||||
resolveData: 'none'
|
||||
});
|
||||
|
||||
return { ok: true } as const;
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: asErrorMessage(error, 'Workflow backend unavailable')
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
||||
export const app = new Elysia({ prefix: '/api' })
|
||||
.all('/auth', authHandler)
|
||||
.all('/auth/*', authHandler)
|
||||
.get('/health', async () => {
|
||||
const queue = await getTaskQueueSnapshot();
|
||||
try {
|
||||
const [queue, workflowBackend] = await Promise.all([
|
||||
getTaskQueueSnapshot(),
|
||||
checkWorkflowBackend()
|
||||
]);
|
||||
|
||||
return Response.json({
|
||||
status: 'ok',
|
||||
version: '4.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
queue
|
||||
});
|
||||
if (!workflowBackend.ok) {
|
||||
return Response.json({
|
||||
status: 'degraded',
|
||||
version: '4.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
queue,
|
||||
workflow: {
|
||||
ok: false,
|
||||
reason: workflowBackend.reason
|
||||
}
|
||||
}, { status: 503 });
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
status: 'ok',
|
||||
version: '4.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
queue,
|
||||
workflow: {
|
||||
ok: true
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return Response.json({
|
||||
status: 'degraded',
|
||||
version: '4.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: asErrorMessage(error, 'Health check failed')
|
||||
}, { status: 503 });
|
||||
}
|
||||
})
|
||||
.get('/me', async () => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
@@ -375,7 +425,8 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
userId: session.user.id,
|
||||
taskType: 'refresh_prices',
|
||||
payload: {},
|
||||
priority: 80
|
||||
priority: 80,
|
||||
resourceKey: 'refresh_prices:portfolio'
|
||||
});
|
||||
|
||||
return Response.json({ task });
|
||||
@@ -394,7 +445,8 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
userId: session.user.id,
|
||||
taskType: 'portfolio_insights',
|
||||
payload: {},
|
||||
priority: 70
|
||||
priority: 70,
|
||||
resourceKey: 'portfolio_insights:portfolio'
|
||||
});
|
||||
|
||||
return Response.json({ task });
|
||||
@@ -543,7 +595,8 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
ticker,
|
||||
limit: defaultFinancialSyncLimit(window)
|
||||
},
|
||||
priority: 88
|
||||
priority: 88,
|
||||
resourceKey: `sync_filings:${ticker}`
|
||||
});
|
||||
queuedSync = true;
|
||||
} catch (error) {
|
||||
@@ -668,7 +721,8 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
ticker,
|
||||
limit: Number.isFinite(limit) ? limit : 20
|
||||
},
|
||||
priority: 90
|
||||
priority: 90,
|
||||
resourceKey: `sync_filings:${ticker}`
|
||||
});
|
||||
|
||||
return Response.json({ task });
|
||||
@@ -693,11 +747,23 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
}
|
||||
|
||||
try {
|
||||
const resourceKey = `analyze_filing:${accessionNumber}`;
|
||||
const existing = await findInFlightTask(
|
||||
session.user.id,
|
||||
'analyze_filing',
|
||||
resourceKey
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return Response.json({ task: existing });
|
||||
}
|
||||
|
||||
const task = await enqueueTask({
|
||||
userId: session.user.id,
|
||||
taskType: 'analyze_filing',
|
||||
payload: { accessionNumber },
|
||||
priority: 65
|
||||
priority: 65,
|
||||
resourceKey
|
||||
});
|
||||
|
||||
return Response.json({ task });
|
||||
@@ -760,6 +826,56 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
params: t.Object({
|
||||
taskId: t.String({ minLength: 1 })
|
||||
})
|
||||
})
|
||||
.get('/tasks/:taskId/timeline', async ({ params }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const timeline = await getTaskTimeline(params.taskId, session.user.id);
|
||||
if (!timeline) {
|
||||
return jsonError('Task not found', 404);
|
||||
}
|
||||
|
||||
return Response.json(timeline);
|
||||
}, {
|
||||
params: t.Object({
|
||||
taskId: t.String({ minLength: 1 })
|
||||
})
|
||||
})
|
||||
.patch('/tasks/:taskId/notification', async ({ params, body }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const payload = asRecord(body);
|
||||
const read = typeof payload.read === 'boolean' ? payload.read : undefined;
|
||||
const silenced = typeof payload.silenced === 'boolean' ? payload.silenced : undefined;
|
||||
|
||||
if (read === undefined && silenced === undefined) {
|
||||
return jsonError('read or silenced must be provided');
|
||||
}
|
||||
|
||||
const task = await updateTaskNotification(session.user.id, params.taskId, {
|
||||
read,
|
||||
silenced
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return jsonError('Task not found', 404);
|
||||
}
|
||||
|
||||
return Response.json({ task });
|
||||
}, {
|
||||
params: t.Object({
|
||||
taskId: t.String({ minLength: 1 })
|
||||
}),
|
||||
body: t.Object({
|
||||
read: t.Optional(t.Boolean()),
|
||||
silenced: t.Optional(t.Boolean())
|
||||
})
|
||||
});
|
||||
|
||||
export type App = typeof app;
|
||||
|
||||
Reference in New Issue
Block a user