Reject modal now requires a reason — stored as an auto-comment on the version so context stays in the thread. Email alert fires on first play of a shared link (fire-and-forget, no-op without RESEND_API_KEY). SSE endpoint per track broadcasts version:new, version:status and comment:new events; track page subscribes and reloads data live. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50 lines
1.5 KiB
TypeScript
50 lines
1.5 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { eq, and } from 'drizzle-orm';
|
|
import { tracks, projectMembers } from '@music-hub/db';
|
|
import { requireAuth } from '../middleware/auth.js';
|
|
import { subscribe } from '../services/sse.js';
|
|
import type { AppEnv } from '../types.js';
|
|
|
|
export const sseRoutes = new Hono<AppEnv>()
|
|
.use('*', requireAuth)
|
|
|
|
.get('/track/:trackId', async (c) => {
|
|
const db = c.get('db');
|
|
const userId = c.get('userId');
|
|
const trackId = c.req.param('trackId');
|
|
|
|
const [track] = await db.select().from(tracks).where(eq(tracks.id, trackId)).limit(1);
|
|
if (!track) return c.json({ error: 'Not found' }, 404);
|
|
|
|
const [membership] = await db
|
|
.select()
|
|
.from(projectMembers)
|
|
.where(and(eq(projectMembers.projectId, track.projectId), eq(projectMembers.userId, userId)))
|
|
.limit(1);
|
|
if (!membership) return c.json({ error: 'Forbidden' }, 403);
|
|
|
|
const { readable, writable } = new TransformStream();
|
|
const writer = writable.getWriter();
|
|
const encoder = new TextEncoder();
|
|
|
|
const send = (data: string) => writer.write(encoder.encode(data)).catch(() => {});
|
|
const unsubscribe = subscribe(trackId, send);
|
|
|
|
// Initial ping
|
|
send(': connected\n\n');
|
|
|
|
c.req.raw.signal.addEventListener('abort', () => {
|
|
unsubscribe();
|
|
writer.close().catch(() => {});
|
|
});
|
|
|
|
return new Response(readable, {
|
|
headers: {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'X-Accel-Buffering': 'no',
|
|
},
|
|
});
|
|
});
|