/* eslint-disable @typescript-eslint/no-namespace */ import { RedmineTypes } from '../models/redmine-types'; import { CacheTTL, Injectable, Logger } from '@nestjs/common'; import { Issues } from '../couchdb-datasources/issues'; import { RedmineEventsGateway } from '../events/redmine-events.gateway'; import { RedmineIssuesCacheWriterService } from '../issue-cache-writer/redmine-issues-cache-writer.service'; import { RedmineDataLoader } from '../redmine-data-loader/redmine-data-loader'; import { MemoryCache } from '../utils/memory-cache'; import nano from 'nano'; import { UNLIMITED } from '../consts/consts'; import { GetParentsHint } from '../utils/get-parents-hint'; import { TreeIssuesStore } from '../utils/tree-issues-store'; import { FlatIssuesStore } from '../utils/flat-issues-store'; export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000; const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5; export namespace IssuesServiceNs { export type IssuesLoader = ( ids: number[], ) => Promise>; export type TreesAndQuery = { rootIds?: number[]; rootIssues?: RedmineTypes.Issue[]; query?: nano.MangoQuery; }; } @Injectable() export class IssuesService { private logger = new Logger(IssuesService.name); private memoryCache = new MemoryCache( ISSUE_MEMORY_CACHE_LIFETIME, ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL, ); constructor( private redmineDataLoader: RedmineDataLoader, private issues: Issues, private redmineIssuesCacheWriterService: RedmineIssuesCacheWriterService, private redmineEventsGateway: RedmineEventsGateway, ) {} async find(query: nano.MangoQuery): Promise { const issueDb = await this.issues.getDatasource(); const res = await issueDb.find(query); return res.docs; } @CacheTTL(60) async getIssues(ids: number[]): Promise { const issueDb = await this.issues.getDatasource(); try { const docs = await issueDb.find({ selector: { id: { $in: ids, }, }, limit: UNLIMITED, }); return docs.docs; } catch (ex) { return []; } } @CacheTTL(120) async getParents( issueId: number, count?: number, ): Promise { let index = 0; const res: RedmineTypes.Issue[] = []; let currentIssue = await this.getIssue(issueId); res.unshift(currentIssue); let parentIssueId = currentIssue.parent?.id || null; while ( parentIssueId !== null && currentIssue.id >= 0 && (typeof count === 'undefined' || count === null || index < count) ) { currentIssue = await this.getIssue(parentIssueId); res.unshift(currentIssue); parentIssueId = currentIssue.parent?.id || null; index++; } const parentsHint = GetParentsHint(res); this.logger.debug(`Parents for issue #${issueId} - ${parentsHint}`); return res; } async getIssue( issueId: number, force = false, ): Promise { const issueFromMemoryCache = this.getIssueFromMemoryCache(issueId); if (issueFromMemoryCache) { return issueFromMemoryCache; } const issueFromCache = await this.getIssueFromCache(issueId); if (issueFromCache) { this.memoryCache.set(issueId, issueFromCache); return issueFromCache; } if (force) { // force = true - прямо из redmine const issueFromRedmine = await this.redmineDataLoader.loadIssue(issueId); if (issueFromRedmine) { await this.redmineIssuesCacheWriterService.saveIssue(issueFromRedmine); this.memoryCache.set(issueId, issueFromRedmine); return issueFromRedmine; } else { return null; } } else { // force = false - через очередь this.redmineEventsGateway.addIssues([issueId]); const unknownIssue = { ...RedmineTypes.Unknown.issue }; this.memoryCache.set(issueId, unknownIssue); return unknownIssue; } } getIssueFromMemoryCache(issueId: number): RedmineTypes.Issue | null { const issue = this.memoryCache.get(issueId); if (issue) { this.logger.debug( `Get issue from memory cache: id = ${issueId}, subject = ${issue.subject}`, ); } return issue; } async getIssueFromRedmine( issueId: number, ): Promise { const issue = await this.redmineDataLoader.loadIssue(issueId); if (issue) { this.logger.debug( `Get issue from redmine: id = ${issueId}, subject = ${issue.subject}`, ); } return issue; } async getIssueFromCache(issueId: number): Promise { const issueDb = await this.issues.getDatasource(); try { const issue = (await issueDb.get(String(issueId))) as any; this.logger.debug( `Get issue from couchdb: id = ${issueId}, subject = ${issue.subject}`, ); return issue; } catch (ex) { return null; } } createDynamicIssuesLoader(): IssuesServiceNs.IssuesLoader { const fn = async ( ids: number[], ): Promise> => { const issues = await this.getIssues(ids); const res = {} as Record; for (let i = 0; i < issues.length; i++) { const issue = issues[i]; res[issue.id] = issue; } return res; }; return fn; } async getIssuesWithChildren( rootIssue: RedmineTypes.Issue, ): Promise { const treeIssuesStore = new TreeIssuesStore(); treeIssuesStore.setRootIssue(rootIssue); const loader = this.createDynamicIssuesLoader(); await treeIssuesStore.fillData(loader); return treeIssuesStore.getIssuesWithChildren(); } async getIssuesFromRoot( rootIssue: RedmineTypes.Issue, ): Promise { const treeStore = new TreeIssuesStore(); treeStore.setRootIssue(rootIssue); const loader = this.createDynamicIssuesLoader(); await treeStore.fillData(loader); return treeStore.getFlatStore().getIssues(); } async mergedTreesAndFind( query: IssuesServiceNs.TreesAndQuery, ): Promise { const flatStore = new FlatIssuesStore(); const loader = this.createDynamicIssuesLoader(); if (query.query) { const issues = await this.find(query.query); issues.forEach((issue) => flatStore.push(issue)); } const rootIssues = []; if (query.rootIds) { const issues = await this.getIssues(query.rootIds); rootIssues.push(...issues); } if (query.rootIssues) { rootIssues.push(...query.rootIssues); } for (let i = 0; i < rootIssues.length; i++) { const rootIssue = rootIssues[i]; const issues = await this.getIssuesFromRoot(rootIssue); issues.forEach((issue) => flatStore.push(issue)); } await flatStore.fillData(loader); return flatStore.getIssues(); } }