import { Injectable } from '@nestjs/common'; import { IssueLinkType, IssueReadModel, StatusCategory, } from '@task-stream/api/tracker/domain'; import { FindIssuesRepository } from './ports/find-issues.repository'; /** A ticket another one waits for. */ export interface Blocker { id: string; key: string; title: string; } const FINISHED = new Set([ StatusCategory.DONE, StatusCategory.CANCELLED, ]); /** * What a ticket waits for: the tickets that block it and the ones it * depends on, while they are open. Links keep no status, so it is worked * out when reading; the lookups of one moment (a board, a page of a list) * go to the read model as one query, not one per ticket. */ @Injectable() export class BlockersService { private batch: { ids: Set; result: Promise>; } | null = null; constructor(private readonly findIssues: FindIssuesRepository) {} async blockedBy(issue: IssueReadModel): Promise { const ids = blockerIdsOf(issue); if (!ids.length) return []; const found = await this.load(ids); return ids .map((id) => found.get(id)) .filter( (blocker): blocker is IssueReadModel => !!blocker && !blocker.trashedAt && !FINISHED.has(blocker.statusCategory), ) .map(({ id, key, title }) => ({ id, key, title })); } private load(ids: string[]): Promise> { if (!this.batch) { const pending = new Set(); const result = new Promise>( (resolve, reject) => setImmediate(() => { this.batch = null; this.findIssues .findByIds([...pending]) .then((issues) => resolve(new Map(issues.map((i) => [i.id, i])))) .catch(reject); }), ); this.batch = { ids: pending, result }; } for (const id of ids) this.batch.ids.add(id); return this.batch.result; } } /** "X blocks this" and "this depends on X": X is what it waits for. */ export function blockerIdsOf(issue: IssueReadModel): string[] { return [ ...new Set( (issue.links ?? []) .filter( (link) => (link.type === IssueLinkType.BLOCKS && link.direction === 'INWARD') || (link.type === IssueLinkType.DEPENDS_ON && link.direction === 'OUTWARD'), ) .map((link) => link.issueId), ), ]; }