Files
fusion/packages/core/src/task-lane-cache.ts
gsxdsm cb57093d03 refactor: domain folder layout (types, API, core, engine) (#2398)
## Summary

Wave 17 organizes Fusion into **domain folders** (stacks on #2397).

### Layout
- **core/types/** — board, task, agents, settings, merge, workflow,
mesh, …
- **core/src/** — agents, ai, async-stores, workflows, tasks, config,
db, …
- **dashboard/app/api/** — client, tasks, agents, git, missions,
planning, …
- **engine/src/** — agents, auth, execution, merge, missions, overseer,
worktree, …

Root keepers retained for large entrypoints (`store.ts`, `executor.ts`,
`merger.ts`, …).

Public barrels (`@fusion/core`, `@fusion/engine`, `app/api.ts` → legacy)
stay stable.

## Test plan
- [x] `@fusion/core` typecheck
- [x] `@fusion/engine` typecheck (pre-existing playwright-core noise
only)
- [ ] CI merge gate

**Stack:** #2394 → #2397 → **this PR**
2026-08-03 00:20:53 -07:00

64 lines
2.1 KiB
TypeScript

import type { TaskMoveLanes } from "./workflows/workflow-lifecycle-traits.js";
export interface TaskLaneCacheOptions {
ttlMs?: number;
maxSize?: number;
now?: () => number;
}
interface TaskLaneCacheEntry {
lanes: TaskMoveLanes | undefined;
workflowId?: string;
at: number;
}
/*
FNXC:WorkflowEvents 2026-08-01-06:11:
PostgreSQL deployments can rewrite a task workflow selection from another node, so an in-process lane
answer must expire as well as being invalidated by local selection writes. Returning an old answer
would make a synchronous listener confidently choose the wrong lane; a cache miss is deliberately
"unknown" and lets that listener retain its existing fallback.
The clock is injectable so TTL coverage advances a deterministic fake clock instead of sleeping. The
cache never resolves an IR: update-event decoration must remain a synchronous Map lookup on hot paths.
*/
export class TaskLaneCache {
private readonly entries = new Map<string, TaskLaneCacheEntry>();
private readonly ttlMs: number;
private readonly maxSize: number;
private readonly now: () => number;
constructor({ ttlMs = 30_000, maxSize = 1_000, now = Date.now }: TaskLaneCacheOptions = {}) {
this.ttlMs = ttlMs;
this.maxSize = maxSize;
this.now = now;
}
set(taskId: string, lanes: TaskMoveLanes | undefined, options: { workflowId?: string } = {}): void {
this.entries.delete(taskId);
this.entries.set(taskId, { lanes, workflowId: options.workflowId, at: this.now() });
while (this.entries.size > this.maxSize) this.entries.delete(this.entries.keys().next().value!);
}
get(taskId: string): TaskMoveLanes | undefined {
const entry = this.entries.get(taskId);
if (!entry) return undefined;
if (this.now() - entry.at >= this.ttlMs) {
this.entries.delete(taskId);
return undefined;
}
// Refresh insertion order without changing the recorded cache time.
this.entries.delete(taskId);
this.entries.set(taskId, entry);
return entry.lanes;
}
invalidate(taskId: string): void {
this.entries.delete(taskId);
}
clear(): void {
this.entries.clear();
}
}