feat(web): bridge logs to typed event stream

This commit is contained in:
jjbrunton committed 2026-08-08 15:00:02 +01:00
1 parent d6d29badcd
commit 9919b12aab
3 files changed
+41 -1

No files matched your search

+15 -1
View File
@@ -1,5 +1,7 @@
import type { Request, Response } from 'express';
import type { Request, RequestHandler, Response } from 'express';
import type { LogEntry } from './types.js';
import { RealtimeEventHub } from './events/index.js';
import { createExpressSseHandler } from './events/express-sse-adapter.js';
const MAX_LOGS = 500;
const INITIAL_LOGS_ON_CONNECT = 50;
@@ -11,6 +13,13 @@ const INITIAL_LOGS_ON_CONNECT = 50;
export class LogBroadcaster {
private entries: LogEntry[] = [];
private clients = new Set<Response>();
private readonly eventHub: RealtimeEventHub;
readonly events: RequestHandler;
constructor(eventHub?: RealtimeEventHub) {
this.eventHub = eventHub ?? new RealtimeEventHub({ capacity: MAX_LOGS });
this.events = createExpressSseHandler({ hub: this.eventHub });
}
push(level: LogEntry['level'], message: string, data?: unknown): void {
const entry: LogEntry = { timestamp: Date.now(), level, message, data };
@@ -19,6 +28,10 @@ export class LogBroadcaster {
this.entries = this.entries.slice(-MAX_LOGS);
}
if (!this.eventHub.closed) {
this.eventHub.publish('log.created', { level, message, data });
}
this.fanout(JSON.stringify({ type: 'log', ...entry }));
}
@@ -76,5 +89,6 @@ export class LogBroadcaster {
}
}
this.clients.clear();
this.eventHub.close();
}
}
+1
View File
@@ -4,6 +4,7 @@ import type { WebContext } from '../context.js';
export function createLogsRouter(ctx: WebContext): Router {
const router = Router();
router.get('/logs/stream', ctx.broadcaster.stream);
router.get('/events/stream', ctx.broadcaster.events);
router.get('/logs', ctx.broadcaster.recent);
return router;
}
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { RealtimeEventHub, type RealtimeEvent } from '../src/web/events/index.js';
import { LogBroadcaster } from '../src/web/log-broadcaster.js';
describe('LogBroadcaster realtime bridge', () => {
it('publishes each log through the typed event hub', () => {
const hub = new RealtimeEventHub({ capacity: 10 });
const received: RealtimeEvent[] = [];
hub.subscribe((event) => received.push(event));
const broadcaster = new LogBroadcaster(hub);
broadcaster.push('warn', 'Connection is stale', { attempts: 2 });
expect(received).toEqual([
expect.objectContaining({
type: 'log.created',
payload: {
level: 'warn',
message: 'Connection is stale',
data: { attempts: 2 },
},
}),
]);
});
});