pinkmine/libs/event-emitter/src/issues/issues.service.ts
Pavel Gnedov 9bffaec6cf Добавлены дополнительные ендпоинты для загрузки данных из кеша
* добавлен ендпоинт find-from-root - для рекурсивной загрузки подзадач от корневой задачи
* добавлен ендпоинт find-from-merged-trees-and-query - для гибридной загрузки задач от корневых + с помощью find запроса для couchdb
2024-04-05 18:56:40 +07:00

225 lines
6.9 KiB
TypeScript

/* 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<Record<number, RedmineTypes.Issue | null>>;
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<number, RedmineTypes.Issue>(
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<RedmineTypes.Issue[]> {
const issueDb = await this.issues.getDatasource();
const res = await issueDb.find(query);
return res.docs;
}
@CacheTTL(60)
async getIssues(ids: number[]): Promise<RedmineTypes.Issue[]> {
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<RedmineTypes.Issue[]> {
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<RedmineTypes.Issue | null> {
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<RedmineTypes.Issue | null> {
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<RedmineTypes.Issue | null> {
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<Record<number, RedmineTypes.Issue>> => {
const issues = await this.getIssues(ids);
const res = {} as Record<number, RedmineTypes.Issue | null>;
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<RedmineTypes.Issue[]> {
const treeIssuesStore = new TreeIssuesStore();
treeIssuesStore.setRootIssue(rootIssue);
const loader = this.createDynamicIssuesLoader();
await treeIssuesStore.fillData(loader);
return treeIssuesStore.getIssuesWithChildren();
}
async getIssuesFromRoot(
rootIssue: RedmineTypes.Issue,
): Promise<RedmineTypes.Issue[]> {
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<RedmineTypes.Issue[]> {
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();
}
}