feat(harness): add bounded event store
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import type {
|
||||
EventInput,
|
||||
HarnessEvent,
|
||||
HarnessEventOf,
|
||||
HarnessEventType,
|
||||
} from "@agent-studio/shared";
|
||||
|
||||
export type EventCursor = {
|
||||
eventId?: string;
|
||||
sequence?: number;
|
||||
};
|
||||
|
||||
export type EventReplay = {
|
||||
events: HarnessEvent[];
|
||||
gap: boolean;
|
||||
oldestSequence?: number;
|
||||
latestSequence?: number;
|
||||
};
|
||||
|
||||
export interface EventStore {
|
||||
append<T extends HarnessEventType>(
|
||||
sessionId: string,
|
||||
event: EventInput<T>,
|
||||
): HarnessEventOf<T>;
|
||||
listAfter(sessionId: string, cursor?: EventCursor): EventReplay;
|
||||
subscribe(sessionId: string, listener: (event: HarnessEvent) => void): () => void;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
HarnessEventSchema,
|
||||
type EventInput,
|
||||
type HarnessEvent,
|
||||
type HarnessEventOf,
|
||||
type HarnessEventType,
|
||||
} from "@agent-studio/shared";
|
||||
import type { EventCursor, EventReplay, EventStore } from "./event-store.js";
|
||||
|
||||
type SessionEvents = {
|
||||
nextSequence: number;
|
||||
events: HarnessEvent[];
|
||||
listeners: Set<(event: HarnessEvent) => void>;
|
||||
};
|
||||
|
||||
export class InMemoryEventStore implements EventStore {
|
||||
readonly #sessions = new Map<string, SessionEvents>();
|
||||
|
||||
public constructor(private readonly maxEventsPerSession = 1000) {
|
||||
if (!Number.isSafeInteger(maxEventsPerSession) || maxEventsPerSession < 1) {
|
||||
throw new RangeError("maxEventsPerSession must be a positive integer");
|
||||
}
|
||||
}
|
||||
|
||||
public append<T extends HarnessEventType>(
|
||||
sessionId: string,
|
||||
input: EventInput<T>,
|
||||
): HarnessEventOf<T> {
|
||||
const state = this.#state(sessionId);
|
||||
const event = HarnessEventSchema.parse({
|
||||
...input,
|
||||
eventId: `evt_${randomUUID()}`,
|
||||
sessionId,
|
||||
sequence: state.nextSequence,
|
||||
timestamp: new Date().toISOString(),
|
||||
}) as HarnessEventOf<T>;
|
||||
|
||||
state.nextSequence += 1;
|
||||
state.events.push(event);
|
||||
if (state.events.length > this.maxEventsPerSession) {
|
||||
state.events.splice(0, state.events.length - this.maxEventsPerSession);
|
||||
}
|
||||
|
||||
for (const listener of [...state.listeners]) {
|
||||
listener(event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
public listAfter(sessionId: string, cursor?: EventCursor): EventReplay {
|
||||
const state = this.#sessions.get(sessionId);
|
||||
if (state === undefined || state.events.length === 0) {
|
||||
return { events: [], gap: false };
|
||||
}
|
||||
|
||||
const oldestSequence = state.events[0]!.sequence;
|
||||
const latestSequence = state.events.at(-1)!.sequence;
|
||||
let events: HarnessEvent[];
|
||||
let gap = false;
|
||||
|
||||
if (cursor?.eventId !== undefined) {
|
||||
const index = state.events.findIndex((event) => event.eventId === cursor.eventId);
|
||||
gap = index < 0;
|
||||
events = index < 0 ? [...state.events] : state.events.slice(index + 1);
|
||||
} else if (cursor?.sequence !== undefined) {
|
||||
const sequence = cursor.sequence;
|
||||
gap = sequence < oldestSequence - 1;
|
||||
events = state.events.filter((event) => event.sequence > sequence);
|
||||
} else {
|
||||
events = [...state.events];
|
||||
}
|
||||
|
||||
return { events, gap, oldestSequence, latestSequence };
|
||||
}
|
||||
|
||||
public subscribe(sessionId: string, listener: (event: HarnessEvent) => void): () => void {
|
||||
const state = this.#state(sessionId);
|
||||
state.listeners.add(listener);
|
||||
let active = true;
|
||||
|
||||
return () => {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
#state(sessionId: string): SessionEvents {
|
||||
const existing = this.#sessions.get(sessionId);
|
||||
if (existing !== undefined) return existing;
|
||||
|
||||
const created: SessionEvents = {
|
||||
nextSequence: 1,
|
||||
events: [],
|
||||
listeners: new Set(),
|
||||
};
|
||||
this.#sessions.set(sessionId, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export {};
|
||||
export * from "./events/event-store.js";
|
||||
export * from "./events/in-memory-event-store.js";
|
||||
|
||||
Reference in New Issue
Block a user