import { Injectable } from '@nestjs/common'; import { DEFAULT_PROJECT_SETTINGS, IssueReadModel, ProjectWorkflow, } from '@task-stream/api/tracker/domain'; import { FindProjectsRepository } from './ports/find-projects.repository'; /** How long a project's workflow is reused (a board asks once per ticket). */ const TTL_MS = 2_000; /** * The workflow of each project, for presenting tickets (e.g. which statuses a * ticket can move to). Commands always load the aggregate instead. */ @Injectable() export class WorkflowsService { private readonly cache = new Map< string, { at: number; workflow: Promise } >(); constructor(private readonly projects: FindProjectsRepository) {} of(projectId: string): Promise { const now = Date.now(); const hit = this.cache.get(projectId); if (hit && now - hit.at < TTL_MS) return hit.workflow; for (const [id, entry] of this.cache) if (now - entry.at >= TTL_MS) this.cache.delete(id); const workflow = this.projects .findById(projectId) .then( (project) => new ProjectWorkflow(project?.settings ?? DEFAULT_PROJECT_SETTINGS), ); this.cache.set(projectId, { at: now, workflow }); return workflow; } /** Active statuses the ticket can move to next. */ async allowedTransitions(issue: IssueReadModel): Promise { return (await this.of(issue.projectId)).allowedFrom(issue.status); } }