email: push new-mail to /email via SSE (IMAP IDLE -> sidecar -> server -> EventSource)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,60 @@ export const emailRouter = createRouter();
|
||||
|
||||
emailRouter.route('/accounts', accountsRouter);
|
||||
|
||||
// ── Real-time: per-user SSE stream of email events (fed by the IMAP IDLE watcher) ──
|
||||
const emailSseClients = new Map<string, Set<ReadableStreamDefaultController<Uint8Array>>>();
|
||||
|
||||
// Called from the sidecar-message handler when the IDLE watcher saves new mail for a user.
|
||||
export function broadcastEmailNew(userEmail: string): void {
|
||||
const set = emailSseClients.get(userEmail);
|
||||
if (!set || set.size === 0) return;
|
||||
const payload = new TextEncoder().encode(`data: ${JSON.stringify({ type: 'new-mail' })}\n\n`);
|
||||
for (const ctrl of set) {
|
||||
try {
|
||||
ctrl.enqueue(payload);
|
||||
} catch {
|
||||
/* dead controller — cleaned up on cancel */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EventSource endpoint (auth via ?token= handled by userMiddleware). The frontend opens this only
|
||||
// while on /email, so events stop the moment you navigate away.
|
||||
emailRouter.get('/events', (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const enc = new TextEncoder();
|
||||
let controllerRef: ReadableStreamDefaultController<Uint8Array>;
|
||||
let ping: ReturnType<typeof setInterval>;
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controllerRef = controller;
|
||||
let set = emailSseClients.get(email);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
emailSseClients.set(email, set);
|
||||
}
|
||||
set.add(controller);
|
||||
controller.enqueue(enc.encode('retry: 3000\n\n'));
|
||||
ping = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(enc.encode(': ping\n\n'));
|
||||
} catch {
|
||||
/* closed */
|
||||
}
|
||||
}, 25_000);
|
||||
},
|
||||
cancel() {
|
||||
clearInterval(ping);
|
||||
const set = emailSseClients.get(email);
|
||||
set?.delete(controllerRef);
|
||||
if (set && set.size === 0) emailSseClients.delete(email);
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' },
|
||||
});
|
||||
});
|
||||
|
||||
emailRouter.get('/messages', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const page = Number(ctx.req.query('page') ?? '1');
|
||||
|
||||
Reference in New Issue
Block a user