Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add and integrate match service #56

Merged
merged 28 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5b68ad9
Initialize Match Service
samuelim01 Sep 26, 2024
5da51fc
User: Remove extra fields
samuelim01 Oct 8, 2024
bff0504
Match: Implement /request routes
samuelim01 Oct 8, 2024
aaf1ce0
Match: Move DB logic into repository.ts
samuelim01 Oct 8, 2024
c3bd798
Match: Init message broker
samuelim01 Oct 8, 2024
164b5a4
Match: Add endpoints and consumers
samuelim01 Oct 9, 2024
0d3e85e
Small fixes
samuelim01 Oct 17, 2024
b7b84ea
Match: Use gateway
samuelim01 Oct 17, 2024
2cd617c
Add Match Service
samuelim01 Oct 17, 2024
248ea90
Integrate matching into frontend
McNaBry Oct 17, 2024
8004d5e
Add handling for timeout and cancellation of match
McNaBry Oct 17, 2024
10a5c44
Fix dependency issues
samuelim01 Oct 19, 2024
331fc33
Ensure logging for each match request
samuelim01 Oct 19, 2024
9f9dee0
Fix recursive call to closeDialog()
samuelim01 Oct 19, 2024
db84f12
Implement match frontend timer
samuelim01 Oct 19, 2024
7422aa9
Integrate navbar
samuelim01 Oct 19, 2024
ede8065
Minor code clean up
samuelim01 Oct 20, 2024
ea0e240
Reroute login to /matching
samuelim01 Oct 20, 2024
b59f678
Clean up match frontend code
McNaBry Oct 20, 2024
bf06529
Add loading state for start matching button when initiating a match
McNaBry Oct 20, 2024
d980262
Adjust height of matching component
McNaBry Oct 20, 2024
516214b
Extend healthcheck time for broker
samuelim01 Oct 20, 2024
2e005c6
Refactor match service: PUT /request
samuelim01 Oct 20, 2024
4b54aa1
Move match DB methods into repository.ts
samuelim01 Oct 20, 2024
bf2d6de
Add Match Service README
samuelim01 Oct 20, 2024
b0fe20f
Enable tests for match service
samuelim01 Oct 20, 2024
fd75363
Clean up code
samuelim01 Oct 20, 2024
899d47c
Fix bug with clearing topics input on match page
McNaBry Oct 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ USER_DB_LOCAL_URI=mongodb://user-db:27017/user
USER_DB_USERNAME=user
USER_DB_PASSWORD=password

# Match Service
MATCH_DB_CLOUD_URI=<FILL-THIS-IN>
MATCH_DB_LOCAL_URI=mongodb://match-db:27017/match
MATCH_DB_USERNAME=user
MATCH_DB_PASSWORD=password

# Secret for creating JWT signature
JWT_SECRET=you-can-replace-this-with-your-own-secret

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
service: [frontend, services/question, services/user]
service: [frontend, services/question, services/user, services/match]
steps:
- uses: actions/checkout@v4
- name: Use Node.js
Expand Down
18 changes: 17 additions & 1 deletion compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,20 @@ services:

user-db:
ports:
- 27018:27017
- 27018:27017

match:
command: npm run dev
ports:
- 8083:8083
volumes:
- /app/node_modules
- ./services/match:/app

match-db:
ports:
- 27019:27017

match-broker:
ports:
- 5672:5672
51 changes: 51 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ services:
- 8080:8080
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- question
- user
- match
networks:
- gateway-network

Expand Down Expand Up @@ -78,10 +82,55 @@ services:
- user-db-network
command: --quiet
restart: always

match:
container_name: match
image: match
build:
context: services/match
dockerfile: Dockerfile
environment:
DB_CLOUD_URI: ${MATCH_DB_CLOUD_URI}
DB_LOCAL_URI: ${MATCH_DB_LOCAL_URI}
DB_USERNAME: ${MATCH_DB_USERNAME}
DB_PASSWORD: ${MATCH_DB_PASSWORD}
depends_on:
match-broker:
condition: service_healthy
networks:
- gateway-network
- match-db-network
restart: always

match-db:
container_name: match-db
image: mongo:7.0.14
environment:
MONGO_INITDB_ROOT_USERNAME: ${MATCH_DB_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MATCH_DB_PASSWORD}
volumes:
- match-db:/data/db
networks:
- match-db-network
restart: always

match-broker:
container_name: match-broker
hostname: match-broker
image: rabbitmq:4.0.2
networks:
- match-db-network
healthcheck:
test: rabbitmq-diagnostics check_port_connectivity
interval: 30s
timeout: 30s
retries: 10
start_period: 30s

volumes:
question-db:
user-db:
match-db:

networks:
gateway-network:
Expand All @@ -90,3 +139,5 @@ networks:
driver: bridge
user-db-network:
driver: bridge
match-db-network:
driver: bridge
50 changes: 50 additions & 0 deletions frontend/src/_services/match.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
import { MatchRequest, MatchResponse } from '../app/matching/match.model';

@Injectable({
providedIn: 'root',
})
export class MatchService extends ApiService {
protected apiPath = 'match/request';

private httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};

constructor(private http: HttpClient) {
super();
}

/**
* Creates a match request with the provided details. The match request will
* be valid for one minute.
*/
createMatchRequest(matchRequest: MatchRequest) {
return this.http.post<MatchResponse>(this.apiUrl, matchRequest, this.httpOptions);
}

/**
* Retrieves the match request and its current status
*/
retrieveMatchRequest(id: string) {
return this.http.get<MatchResponse>(this.apiUrl + '/' + id);
}

/**
* Refreshes the match request, effectively resetting its validity to one minute.
*/
updateMatchRequest(id: string) {
return this.http.put<MatchResponse>(this.apiUrl + '/' + id, {}, this.httpOptions);
}

/**
* Deletes the match request
*/
deleteMatchRequest(id: string) {
return this.http.delete<MatchResponse>(this.apiUrl + '/' + id);
}
}
6 changes: 2 additions & 4 deletions frontend/src/app/account/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class LoginComponent {
) {
//redirect to home if already logged in
if (this.authenticationService.userValue) {
this.router.navigate(['/']);
this.router.navigate(['/matching']);
}
}

Expand All @@ -44,9 +44,7 @@ export class LoginComponent {
// authenticationService returns an observable that we can subscribe to
this.authenticationService.login(this.userForm.username, this.userForm.password).subscribe({
next: () => {
// get return url from route parameters or default to '/'
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.router.navigate([returnUrl]);
this.router.navigate(['/matching']);
},
error: error => {
this.isProcessingLogin = false;
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/app/account/register.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class RegisterComponent {
) {
// redirect to home if already logged in
if (this.authenticationService.userValue) {
this.router.navigate(['/']);
this.router.navigate(['/matching']);
}
}

Expand Down Expand Up @@ -99,9 +99,7 @@ export class RegisterComponent {
.pipe()
.subscribe({
next: () => {
// get return url from route parameters or default to '/'
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.router.navigate([returnUrl]);
this.router.navigate(['/matching']);
},
// error handling for registration because we assume there will be no errors with auto login
error: error => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
@if (isFindingMatch) {
<p-progressSpinner styleClass="w-2rem h-2rem mt-0" strokeWidth="6" />
<h2 class="mt-0 mb-0">Finding a Match...</h2>
<div class="flex gap-2 align-items-center">
<i class="pi pi-stopwatch"></i>
<p class="m-0">Time Left: {{ matchTimeLeft }}</p>
</div>
} @else {
<i class="pi pi-check text-4xl text-green-300"></i>
<h2 class="mt-0 mb-0">Match Found!</h2>
Expand Down
99 changes: 88 additions & 11 deletions frontend/src/app/matching/finding-match/finding-match.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { ButtonModule } from 'primeng/button';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { ChipModule } from 'primeng/chip';
import { CommonModule } from '@angular/common';
import { catchError, Observable, of, Subscription, switchMap, takeUntil, tap, timer } from 'rxjs';
import { MessageService } from 'primeng/api';
import { MatchService } from '../../../_services/match.service';
import { MatchResponse, MatchStatus } from '../match.model';

@Component({
selector: 'app-finding-match',
Expand All @@ -15,36 +19,109 @@ import { CommonModule } from '@angular/common';
})
export class FindingMatchComponent {
@Input() userCriteria!: UserCriteria;
@Input() matchId!: string;
@Input() isVisible = false;

@Output() dialogClose = new EventEmitter<void>();
@Output() matchFailed = new EventEmitter<void>();
@Output() matchSuccess = new EventEmitter<void>();

isFindingMatch = true;
protected isFindingMatch = true;
protected matchTimeLeft = 0;
protected matchTimeInterval!: NodeJS.Timeout;
protected matchPoll!: Subscription;
protected stopPolling$ = new EventEmitter();

closeDialog() {
this.dialogClose.emit();
}
constructor(
private matchService: MatchService,
private messageService: MessageService,
) {}

onMatchFailed() {
this.stopTimer();
this.matchFailed.emit();
}

onMatchSuccess() {
this.stopTimer();
this.isFindingMatch = false;
this.matchSuccess.emit();
// Possible to handle routing to workspace here.
}

onDialogShow() {
// Simulate request to API and subsequent success/failure.
setTimeout(() => {
if (this.isVisible) {
// Toggle to simulate different situations.
// this.onMatchFailed();
this.onMatchSuccess();
this.startTimer(60);
this.matchPoll = this.startPolling(5000).pipe(tap(), takeUntil(this.stopPolling$)).subscribe();
}

startPolling(interval: number): Observable<MatchResponse | null> {
return timer(0, interval).pipe(switchMap(() => this.requestData()));
}

requestData() {
return this.matchService.retrieveMatchRequest(this.matchId).pipe(
tap((response: MatchResponse) => {
console.log(response);
const status: MatchStatus = response.data.status || MatchStatus.PENDING;
switch (status) {
case MatchStatus.MATCH_FOUND:
this.onMatchSuccess();
break;
case MatchStatus.TIME_OUT:
this.stopPolling$.next(false);
this.onMatchFailed();
break;
// TODO: Add case for MatchStatus.COLLAB_CREATED
}
}),
catchError(() => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: `Something went wrong while matching.`,
life: 3000,
});
this.closeDialog();
return of(null);
}),
);
}

closeDialog() {
this.stopTimer();
this.matchPoll.unsubscribe();
this.matchService.deleteMatchRequest(this.matchId).subscribe({
next: response => {
console.log(response);
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: `Something went wrong while cancelling your match.`,
life: 3000,
});
},
complete: () => {
this.dialogClose.emit();
},
});
}

startTimer(time: number) {
this.matchTimeLeft = time;
this.matchTimeInterval = setInterval(() => {
if (this.matchTimeLeft > 0) {
this.matchTimeLeft--;
} else {
this.stopTimer();
}
}, 3000);
}, 1000);
}

stopTimer() {
if (this.matchTimeInterval) {
clearInterval(this.matchTimeInterval);
}
}
}
35 changes: 35 additions & 0 deletions frontend/src/app/matching/match.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Difficulty } from './user-criteria.model';

export interface MatchRequest {
topics: string[];
difficulty: Difficulty;
}

export enum MatchStatus {
PENDING = 'PENDING',
TIME_OUT = 'TIME_OUT',
MATCH_FOUND = 'MATCH_FOUND',
COLLAB_CREATED = 'COLLAB_CREATED',
}

export interface MatchRequestStatus {
_id: string;
userId: string;
username: string;
createdAt: Date;
updatedAt: Date;
topics: string[];
difficulty: Difficulty;
status?: MatchStatus;
pairId?: string;
collabId?: string;
}

export interface BaseResponse {
status: string;
message: string;
}

export interface MatchResponse extends BaseResponse {
data: MatchRequestStatus;
}
2 changes: 1 addition & 1 deletion frontend/src/app/matching/matching.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
min-height: calc(100vh - 160px);
padding: 1rem;
}

Expand Down
Loading