Skip to content

Commit 9c2b8e0

Browse files
Awakichclaude
andcommitted
add EntityCache<T> utility & apply to Project, Vacancy, Program repositories with event-driven invalidation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b66734f commit 9c2b8e0

11 files changed

Lines changed: 152 additions & 20 deletions

File tree

projects/social_platform/src/app/api/vacancy/use-cases/delete-vacancy.use-case.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
import { inject, Injectable } from "@angular/core";
44
import { VacancyRepositoryPort } from "../../../domain/vacancy/ports/vacancy.repository.port";
5-
import { catchError, map, Observable, of } from "rxjs";
5+
import { catchError, map, Observable, of, tap } from "rxjs";
66
import { fail, ok, Result } from "../../../domain/shared/result.type";
7+
import { EventBus } from "../../../domain/shared/event-bus";
8+
import { vacancyDelete } from "../../../domain/vacancy/events/vacancy-deleted.event";
79

810
@Injectable({ providedIn: "root" })
911
export class DeleteVacancyUseCase {
1012
private readonly vacancyRepositoryPort = inject(VacancyRepositoryPort);
13+
private readonly eventBus = inject(EventBus);
1114

1215
execute(
1316
vacancyId: number
1417
): Observable<Result<void, { kind: "delete_vacancy_error"; cause?: unknown }>> {
1518
return this.vacancyRepositoryPort.deleteVacancy(vacancyId).pipe(
19+
tap(() => this.eventBus.emit(vacancyDelete(vacancyId))),
1620
map(() => ok<void>(undefined)),
1721
catchError(error => of(fail({ kind: "delete_vacancy_error" as const, cause: error })))
1822
);

projects/social_platform/src/app/api/vacancy/use-cases/post-vacancy.use-case.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ import { inject, Injectable } from "@angular/core";
44
import { VacancyRepositoryPort } from "../../../domain/vacancy/ports/vacancy.repository.port";
55
import { CreateVacancyDto } from "../../project/dto/create-vacancy.model";
66
import { Vacancy } from "../../../domain/vacancy/vacancy.model";
7-
import { catchError, map, Observable, of } from "rxjs";
7+
import { catchError, map, Observable, of, tap } from "rxjs";
88
import { fail, ok, Result } from "../../../domain/shared/result.type";
9+
import { EventBus } from "../../../domain/shared/event-bus";
10+
import { vacancyCreated } from "../../../domain/vacancy/events/vacancy-created.event";
911

1012
@Injectable({ providedIn: "root" })
1113
export class PostVacancyUseCase {
1214
private readonly vacancyRepositoryPort = inject(VacancyRepositoryPort);
15+
private readonly eventBus = inject(EventBus);
1316

1417
execute(
1518
projectId: number,
1619
vacancy: CreateVacancyDto
1720
): Observable<Result<Vacancy, { kind: "post_vacancy_error"; cause?: unknown }>> {
1821
return this.vacancyRepositoryPort.postVacancy(projectId, vacancy).pipe(
22+
tap(() => this.eventBus.emit(vacancyCreated(projectId, vacancy))),
1923
map(createdVacancy => ok<Vacancy>(createdVacancy)),
2024
catchError(error => of(fail({ kind: "post_vacancy_error" as const, cause: error })))
2125
);

projects/social_platform/src/app/api/vacancy/use-cases/update-vacancy.use-case.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ import { inject, Injectable } from "@angular/core";
44
import { VacancyRepositoryPort } from "../../../domain/vacancy/ports/vacancy.repository.port";
55
import { CreateVacancyDto } from "../../project/dto/create-vacancy.model";
66
import { Vacancy } from "../../../domain/vacancy/vacancy.model";
7-
import { catchError, map, Observable, of } from "rxjs";
7+
import { catchError, map, Observable, of, tap } from "rxjs";
88
import { fail, ok, Result } from "../../../domain/shared/result.type";
9+
import { EventBus } from "../../../domain/shared/event-bus";
10+
import { vacancyUpdated } from "../../../domain/vacancy/events/vacancy-updated.event";
911

1012
@Injectable({ providedIn: "root" })
1113
export class UpdateVacancyUseCase {
1214
private readonly vacancyRepositoryPort = inject(VacancyRepositoryPort);
15+
private readonly eventBus = inject(EventBus);
1316

1417
execute(
1518
vacancyId: number,
1619
vacancy: Partial<Vacancy> | CreateVacancyDto
1720
): Observable<Result<Vacancy, { kind: "update_vacancy_error"; cause?: unknown }>> {
1821
return this.vacancyRepositoryPort.updateVacancy(vacancyId, vacancy).pipe(
22+
tap(() => this.eventBus.emit(vacancyUpdated(vacancyId, vacancy))),
1923
map(updatedVacancy => ok<Vacancy>(updatedVacancy)),
2024
catchError(error => of(fail({ kind: "update_vacancy_error" as const, cause: error })))
2125
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/** @format */
2+
3+
import { Observable, shareReplay } from "rxjs";
4+
5+
export class EntityCache<T> {
6+
private readonly store = new Map<number, Observable<T>>();
7+
8+
getOrFetch(id: number, factory: () => Observable<T>): Observable<T> {
9+
if (!this.store.has(id)) {
10+
const entity$ = factory().pipe(shareReplay(1));
11+
12+
this.store.set(id, entity$);
13+
}
14+
15+
return this.store.get(id)!;
16+
}
17+
18+
invalidate(id: number): void {
19+
this.store.delete(id);
20+
}
21+
22+
clear(): void {
23+
this.store.clear();
24+
}
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/** @format */
2+
3+
import { CreateVacancyDto } from "../../../api/project/dto/create-vacancy.model";
4+
import { DomainEvent } from "../../shared/domain-event";
5+
6+
export interface VacancyCreated extends DomainEvent {
7+
readonly type: "VacancyCreated";
8+
readonly payload: {
9+
readonly projectId: number;
10+
readonly vacancy: CreateVacancyDto;
11+
};
12+
}
13+
14+
export function vacancyCreated(projectId: number, vacancy: CreateVacancyDto): VacancyCreated {
15+
return {
16+
type: "VacancyCreated",
17+
payload: {
18+
projectId,
19+
vacancy,
20+
},
21+
occurredAt: new Date(),
22+
};
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/** @format */
2+
3+
import { DomainEvent } from "../../shared/domain-event";
4+
5+
export interface VacancyDelete extends DomainEvent {
6+
readonly type: "VacancyDelete";
7+
readonly payload: {
8+
readonly vacancyId: number;
9+
};
10+
}
11+
12+
export function vacancyDelete(vacancyId: number): VacancyDelete {
13+
return {
14+
type: "VacancyDelete",
15+
payload: {
16+
vacancyId,
17+
},
18+
occurredAt: new Date(),
19+
};
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/** @format */
2+
3+
import { CreateVacancyDto } from "../../../api/project/dto/create-vacancy.model";
4+
import { DomainEvent } from "../../shared/domain-event";
5+
import { Vacancy } from "../vacancy.model";
6+
7+
export interface VacancyUpdated extends DomainEvent {
8+
readonly type: "VacancyUpdated";
9+
readonly payload: {
10+
readonly vacancyId: number;
11+
readonly vacancy: Partial<Vacancy> | CreateVacancyDto;
12+
};
13+
}
14+
15+
export function vacancyUpdated(
16+
vacancyId: number,
17+
vacancy: Partial<Vacancy> | CreateVacancyDto
18+
): VacancyUpdated {
19+
return {
20+
type: "VacancyUpdated",
21+
payload: {
22+
vacancyId,
23+
vacancy,
24+
},
25+
occurredAt: new Date(),
26+
};
27+
}

projects/social_platform/src/app/infrastructure/repository/industry/industry.repository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { map, Observable, tap } from "rxjs";
66
import { Industry } from "../../../domain/industry/industry.model";
77
import { plainToInstance } from "class-transformer";
88
import { IndustryRepositoryPort } from "../../../domain/industry/ports/industry.repository.port";
9+
import { EntityCache } from "../../../domain/shared/entity-cache";
910

1011
@Injectable({ providedIn: "root" })
1112
export class IndustryRepository implements IndustryRepositoryPort {
1213
private readonly industryAdapter = inject(IndustryHttpAdapter);
14+
private readonly entityCache = new EntityCache<Industry>();
1315

1416
readonly industries = signal<Industry[]>([]);
1517

projects/social_platform/src/app/infrastructure/repository/program/program.repository.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import { ProjectAdditionalFields } from "../../../domain/project/project-additio
1212
import { Project } from "../../../domain/project/project.model";
1313
import { ProgramHttpAdapter } from "../../adapters/program/program-http.adapter";
1414
import { ProgramRepositoryPort } from "../../../domain/program/ports/program.repository.port";
15+
import { EntityCache } from "../../../domain/shared/entity-cache";
1516

1617
@Injectable({ providedIn: "root" })
1718
export class ProgramRepository implements ProgramRepositoryPort {
1819
private readonly programAdapter = inject(ProgramHttpAdapter);
20+
private readonly entityCache = new EntityCache<Program>();
1921

2022
getAll(skip: number, take: number, params?: HttpParams): Observable<ApiPagination<Program>> {
2123
return this.programAdapter.getAll(skip, take, params);
@@ -26,7 +28,7 @@ export class ProgramRepository implements ProgramRepositoryPort {
2628
}
2729

2830
getOne(programId: number): Observable<Program> {
29-
return this.programAdapter.getOne(programId);
31+
return this.entityCache.getOrFetch(programId, () => this.programAdapter.getOne(programId));
3032
}
3133

3234
create(program: ProgramCreate): Observable<Program> {

projects/social_platform/src/app/infrastructure/repository/project/project.repository.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/** @format */
22

3-
import { inject, Injectable, OnDestroy } from "@angular/core";
4-
import { BehaviorSubject, map, Observable, shareReplay, tap, Subject } from "rxjs";
5-
import { takeUntil } from "rxjs/operators";
3+
import { inject, Injectable } from "@angular/core";
4+
import { BehaviorSubject, map, Observable, tap } from "rxjs";
65
import { Project, ProjectCount } from "../../../domain/project/project.model";
76
import { ProjectHttpAdapter } from "../../adapters/project/project-http.adapter";
87
import { ApiPagination } from "../../../domain/other/api-pagination.model";
@@ -19,10 +18,11 @@ import { RemoveProjectCollaborator } from "../../../domain/project/events/remove
1918
import { SendVacancyResponse } from "../../../domain/vacancy/events/send-vacancy-response.event";
2019
import { AcceptVacancyResponse } from "../../../domain/vacancy/events/accept-vacancy-response.event";
2120
import { RejectVacancyResponse } from "../../../domain/vacancy/events/reject-vacancy-response.event";
21+
import { EntityCache } from "../../../domain/shared/entity-cache";
2222

2323
@Injectable({ providedIn: "root" })
2424
export class ProjectRepository implements ProjectRepositoryPort {
25-
private readonly cache = new Map<number, Observable<Project>>();
25+
private readonly entityCache = new EntityCache<Project>();
2626
readonly count$ = new BehaviorSubject<ProjectCount>({ my: 0, all: 0, subs: 0 });
2727

2828
private readonly projectAdapter = inject(ProjectHttpAdapter);
@@ -100,14 +100,9 @@ export class ProjectRepository implements ProjectRepositoryPort {
100100
}
101101

102102
getOne(id: number): Observable<Project> {
103-
if (!this.cache.has(id)) {
104-
const project$ = this.projectAdapter.fetchOne(id).pipe(
105-
map(dto => plainToInstance(Project, dto)),
106-
shareReplay(1)
107-
);
108-
this.cache.set(id, project$);
109-
}
110-
return this.cache.get(id)!;
103+
return this.entityCache.getOrFetch(id, () =>
104+
this.projectAdapter.fetchOne(id).pipe(map(dto => plainToInstance(Project, dto)))
105+
);
111106
}
112107

113108
refreshCount(): Observable<ProjectCount> {
@@ -139,6 +134,6 @@ export class ProjectRepository implements ProjectRepositoryPort {
139134
}
140135

141136
invalidate(id: number): void {
142-
this.cache.delete(id);
137+
this.entityCache.invalidate(id);
143138
}
144139
}

0 commit comments

Comments
 (0)