@memberjunction/a2aserver
Version:
MemberJunction Agent-To-Agent (A2A) Server Implementation
95 lines • 3.06 kB
JavaScript
import { LogStatus } from "@memberjunction/core";
/**
* Terminal task statuses — tasks in these states are eligible for cleanup
* after the retention window elapses.
*/
export const TERMINAL_STATUSES = new Set(['completed', 'cancelled', 'failed']);
/**
* Bounded in-memory task store with periodic terminal-state cleanup.
*
* Replaces the unbounded module-level `Map<string, Task>` that was the single
* largest leak found in the audit (R2-C11): tasks accumulated forever with
* their `messages[]` and `artifacts[]` arrays, growing memory roughly as
* `tasks × messages × artifact-bytes`. The store keeps the same `Map`-like
* surface (`set`/`get`/`delete`/`has`/`size`) so callers don't change, and
* adds a sweeper that drops tasks in terminal state older than `retentionMs`.
* Implements `IShutdownable` so the sweeper timer is cleared during graceful
* shutdown.
*/
export class TaskStore {
constructor(options = {}) {
this._tasks = new Map();
this._timer = null;
this._sweepIntervalMs = options.sweepIntervalMs ?? 5 * 60 * 1000;
this._retentionMs = options.retentionMs ?? 60 * 60 * 1000;
}
get ShutdownName() {
return 'A2AServer.TaskStore';
}
/**
* Begin the periodic sweep. Idempotent; calling twice is safe. Uses `unref()`
* so the sweep timer doesn't hold the process alive on its own.
*/
Start() {
if (this._timer)
return;
this._timer = setInterval(() => this.Sweep(), this._sweepIntervalMs);
if (this._timer && typeof this._timer.unref === 'function') {
this._timer.unref();
}
}
/**
* Stop the sweep timer and drop all tasks. Idempotent.
*/
Shutdown() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
this._tasks.clear();
}
/**
* Configured retention window in milliseconds.
*/
get RetentionMs() {
return this._retentionMs;
}
/**
* Drop terminal-state tasks that haven't been updated in `retentionMs`.
* Returns the count evicted (useful for tests and diagnostics).
*/
Sweep(now = Date.now()) {
const cutoff = now - this._retentionMs;
let evicted = 0;
for (const [id, task] of this._tasks) {
if (TERMINAL_STATUSES.has(task.status) && task.updated.getTime() <= cutoff) {
this._tasks.delete(id);
evicted++;
}
}
if (evicted > 0) {
LogStatus(`A2AServer.TaskStore swept ${evicted} terminal task(s)`);
}
return evicted;
}
set(id, task) {
this._tasks.set(id, task);
return this;
}
get(id) {
return this._tasks.get(id);
}
has(id) {
return this._tasks.has(id);
}
delete(id) {
return this._tasks.delete(id);
}
get size() {
return this._tasks.size;
}
values() {
return this._tasks.values();
}
}
//# sourceMappingURL=TaskStore.js.map