feat(web): bridge logs to typed event stream
This commit is contained in:
1 parent
d6d29badcd
commit
9919b12aab
3 files changed
+41
-1
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, RequestHandler, Response } from 'express';
|
||||||
import type { LogEntry } from './types.js';
|
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 MAX_LOGS = 500;
|
||||||
const INITIAL_LOGS_ON_CONNECT = 50;
|
const INITIAL_LOGS_ON_CONNECT = 50;
|
||||||
@@ -11,6 +13,13 @@ const INITIAL_LOGS_ON_CONNECT = 50;
|
|||||||
export class LogBroadcaster {
|
export class LogBroadcaster {
|
||||||
private entries: LogEntry[] = [];
|
private entries: LogEntry[] = [];
|
||||||
private clients = new Set<Response>();
|
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 {
|
push(level: LogEntry['level'], message: string, data?: unknown): void {
|
||||||
const entry: LogEntry = { timestamp: Date.now(), level, message, data };
|
const entry: LogEntry = { timestamp: Date.now(), level, message, data };
|
||||||
@@ -19,6 +28,10 @@ export class LogBroadcaster {
|
|||||||
this.entries = this.entries.slice(-MAX_LOGS);
|
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 }));
|
this.fanout(JSON.stringify({ type: 'log', ...entry }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,5 +89,6 @@ export class LogBroadcaster {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.clients.clear();
|
this.clients.clear();
|
||||||
|
this.eventHub.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import type { WebContext } from '../context.js';
|
|||||||
export function createLogsRouter(ctx: WebContext): Router {
|
export function createLogsRouter(ctx: WebContext): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
router.get('/logs/stream', ctx.broadcaster.stream);
|
router.get('/logs/stream', ctx.broadcaster.stream);
|
||||||
|
router.get('/events/stream', ctx.broadcaster.events);
|
||||||
router.get('/logs', ctx.broadcaster.recent);
|
router.get('/logs', ctx.broadcaster.recent);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
@@ -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 },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user