From 29a4e38c68300b367d7ec00e3bfdbdf0ff82331a Mon Sep 17 00:00:00 2001 From: felixvippp-ai Date: Wed, 24 Jun 2026 11:46:44 -0400 Subject: [PATCH 1/4] perf: add ring event log --- src/events.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/src/events.ts b/src/events.ts index e830988..057f062 100644 --- a/src/events.ts +++ b/src/events.ts @@ -8,12 +8,103 @@ export type AppEvent = { }; export const EVENT_LOG_CAP = 10_000; -export const eventLog: AppEvent[] = []; + +/** Fixed-capacity chronological event log with O(1) insertion and eviction. */ +export class RingEventLog { + readonly capacity: number; + #items: AppEvent[]; + #start = 0; + #size = 0; + + constructor(capacity: number) { + if (!Number.isInteger(capacity) || capacity <= 0) { + throw new RangeError("event log capacity must be a positive integer"); + } + this.capacity = capacity; + this.#items = new Array(capacity); + } + + get length(): number { + return this.#size; + } + + clear(): void { + this.#items = new Array(this.capacity); + this.#start = 0; + this.#size = 0; + } + + push(event: AppEvent): void { + if (this.#size < this.capacity) { + this.#items[(this.#start + this.#size) % this.capacity] = event; + this.#size++; + return; + } + + this.#items[this.#start] = event; + this.#start = (this.#start + 1) % this.capacity; + } + + at(index: number): AppEvent | undefined { + const normalized = index < 0 ? this.#size + index : index; + if (normalized < 0 || normalized >= this.#size) return undefined; + return this.#items[(this.#start + normalized) % this.capacity]; + } + + toArray(): AppEvent[] { + const items: AppEvent[] = []; + for (let i = 0; i < this.#size; i++) { + const item = this.at(i); + if (item !== undefined) items.push(item); + } + return items; + } + + [Symbol.iterator](): IterableIterator { + return this.toArray()[Symbol.iterator](); + } +} + +export const eventLog = new RingEventLog(EVENT_LOG_CAP); /** * Appends an audit event to the bounded in-memory event log. + * + * The ring buffer capacity follows EVENT_LOG_CAP. Runtime config currently + * exposes eventLogCap for readback only; /api/v1/config does not allow changing + * it, so preserving the constant avoids a hidden mutable cap. */ export function recordEvent(type: string, payload: Record): void { eventLog.push({ id: randomUUID(), ts: Date.now(), type, payload }); - if (eventLog.length > EVENT_LOG_CAP) eventLog.shift(); +} + +export function clearEventLog(): void { + eventLog.clear(); +} + +export function listEvents( + { + limit, + since, + type, + }: { + limit: number; + since: number; + type?: string; + }, + log: RingEventLog = eventLog +): AppEvent[] { + return log + .toArray() + .filter((event) => event.ts >= since && (type === undefined || event.type === type)) + .slice(-limit); +} + +export function summarizeEvents(log: RingEventLog = eventLog): { + counts: Record; + total: number; +} { + const counts: Record = {}; + for (const event of log) counts[event.type] = (counts[event.type] ?? 0) + 1; + return { counts, total: log.length }; } From 2856e0ccf3728bf2c52f2b18729b8f974914509e Mon Sep 17 00:00:00 2001 From: felixvippp-ai Date: Wed, 24 Jun 2026 11:47:35 -0400 Subject: [PATCH 2/4] perf: route event reads through ring helpers --- src/routes/events.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/routes/events.ts b/src/routes/events.ts index b173993..2858901 100644 --- a/src/routes/events.ts +++ b/src/routes/events.ts @@ -1,5 +1,5 @@ import { Router, type Request, type Response } from "express"; -import { EVENT_LOG_CAP, eventLog } from "../events.js"; +import { EVENT_LOG_CAP, listEvents, summarizeEvents } from "../events.js"; /** * Builds read-only audit-event routes. @@ -8,9 +8,7 @@ export function createEventsRouter(): Router { const router = Router(); router.get("/api/v1/events/summary", (_req, res: Response) => { - const counts: Record = {}; - for (const e of eventLog) counts[e.type] = (counts[e.type] ?? 0) + 1; - res.json({ counts, total: eventLog.length }); + res.json(summarizeEvents()); }); router.get("/api/v1/events", (req: Request, res: Response) => { @@ -20,10 +18,7 @@ export function createEventsRouter(): Router { EVENT_LOG_CAP, Math.max(1, Number((req.query.limit as string) ?? 100)) ); - const items = eventLog - .filter((e) => e.ts >= since && (type === undefined || e.type === type)) - .slice(-limit); - res.json({ items }); + res.json({ items: listEvents({ limit, since, type }) }); }); return router; From 3764bae3b78c73b96cbb74f6a483c7e03e879f71 Mon Sep 17 00:00:00 2001 From: felixvippp-ai Date: Wed, 24 Jun 2026 11:48:06 -0400 Subject: [PATCH 3/4] test: clear ring event log in operational tests --- src/routes/operational.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/operational.test.ts b/src/routes/operational.test.ts index 16b7ab2..c0834e3 100644 --- a/src/routes/operational.test.ts +++ b/src/routes/operational.test.ts @@ -2,7 +2,7 @@ import { describe, it, beforeEach } from "node:test"; import assert from "node:assert"; import request from "supertest"; import { createApp } from "../index.js"; -import { eventLog } from "../events.js"; +import { clearEventLog } from "../events.js"; import { apiKeyStore, config, @@ -23,7 +23,7 @@ const defaultConfig = { beforeEach(() => { apiKeyStore.clear(); - eventLog.length = 0; + clearEventLog(); servicesDisabled.clear(); servicesMetadata.clear(); servicesStore.clear(); From ab1014dce28df68ec11bc783a79eb968a1cb84c8 Mon Sep 17 00:00:00 2001 From: felixvippp-ai Date: Wed, 24 Jun 2026 11:48:39 -0400 Subject: [PATCH 4/4] test: cover ring event log wraparound --- src/eventlog-ring.test.ts | 85 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/eventlog-ring.test.ts diff --git a/src/eventlog-ring.test.ts b/src/eventlog-ring.test.ts new file mode 100644 index 0000000..89c5694 --- /dev/null +++ b/src/eventlog-ring.test.ts @@ -0,0 +1,85 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { type AppEvent, listEvents, RingEventLog, summarizeEvents } from "./events.js"; + +function makeEvent(index: number, type = "usage.recorded"): AppEvent { + return { + id: `evt-${index}`, + ts: index, + type, + payload: { index }, + }; +} + +void describe("RingEventLog", () => { + void it("keeps chronological order below capacity", () => { + const log = new RingEventLog(3); + log.push(makeEvent(1)); + log.push(makeEvent(2)); + + assert.strictEqual(log.length, 2); + assert.deepStrictEqual( + log.toArray().map((event) => event.payload.index), + [1, 2] + ); + }); + + void it("keeps all entries exactly at capacity", () => { + const log = new RingEventLog(3); + log.push(makeEvent(1)); + log.push(makeEvent(2)); + log.push(makeEvent(3)); + + assert.strictEqual(log.length, 3); + assert.deepStrictEqual( + log.toArray().map((event) => event.payload.index), + [1, 2, 3] + ); + }); + + void it("evicts oldest entries in O(1) while preserving chronological reads", () => { + const log = new RingEventLog(3); + for (let i = 1; i <= 5; i++) log.push(makeEvent(i)); + + assert.strictEqual(log.length, 3); + assert.strictEqual(log.at(0)?.payload.index, 3); + assert.strictEqual(log.at(-1)?.payload.index, 5); + assert.deepStrictEqual( + log.toArray().map((event) => event.payload.index), + [3, 4, 5] + ); + }); + + void it("summarizes retained events after wraparound", () => { + const log = new RingEventLog(3); + log.push(makeEvent(1, "usage.recorded")); + log.push(makeEvent(2, "usage.settled")); + log.push(makeEvent(3, "usage.recorded")); + log.push(makeEvent(4, "webhook.test")); + + assert.deepStrictEqual(summarizeEvents(log), { + counts: { + "usage.recorded": 1, + "usage.settled": 1, + "webhook.test": 1, + }, + total: 3, + }); + }); + + void it("preserves since/type/limit query semantics after wraparound", () => { + const log = new RingEventLog(4); + log.push(makeEvent(1, "usage.recorded")); + log.push(makeEvent(2, "usage.settled")); + log.push(makeEvent(3, "usage.recorded")); + log.push(makeEvent(4, "usage.settled")); + log.push(makeEvent(5, "usage.recorded")); + + const items = listEvents({ since: 3, type: "usage.recorded", limit: 1 }, log); + + assert.deepStrictEqual( + items.map((event) => event.payload.index), + [5] + ); + }); +});