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

Set up SSE (Server sent events) for event signup counts #776

19 changes: 19 additions & 0 deletions client/src/models/__generated__/schema.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions server/src/data-layer/models/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,13 @@ export interface Event {
*/
location: string
/**
* The start date of the event.
* The signup period start date.
* Note that this date is in UTC time.
* Use the same start and end day to show that its a 1 day event.
* Use the same start and end date to indicate a 1 day signup period.
*/
start_date: Timestamp
/**
* The end date of the event.
* The signup period end date.
* Note that this date is in UTC time.
*/
end_date: Timestamp
Expand Down
34 changes: 34 additions & 0 deletions server/src/data-layer/services/EventService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "data-layer/adapters/DateUtils"
import { Event, EventReservation } from "data-layer/models/firebase"
import FirestoreCollections from "data-layer/adapters/FirestoreCollections"
import { Timestamp } from "firebase-admin/firestore"

const eventService = new EventService()

Expand All @@ -26,6 +27,13 @@ const event2: Event = {
start_date: startDate,
end_date: endDate
}
const now = new Date(Date.now())
const futureEvent: Event = {
title: "Scheduled event",
location: "Future event",
start_date: Timestamp.fromDate(new Date(now.getUTCFullYear() + 1, 1, 1)),
end_date: Timestamp.fromDate(new Date(now.getUTCFullYear() + 1, 1, 1))
}

const reservation1: EventReservation = {
first_name: "John",
Expand Down Expand Up @@ -73,6 +81,19 @@ describe("EventService integration tests", () => {
}).toEqual(event1)
})

it("Should be able to get current existing events", async () => {
// Create past events
await eventService.createEvent(event1)
await eventService.createEvent(event2)
// Create a future event
const newEvent = await eventService.createEvent(futureEvent)

const futureEvents = await eventService.getActiveEvents()

expect(futureEvents.length).toBe(1)
expect(futureEvents).toEqual([{ ...futureEvent, id: newEvent.id }])
})

it("Should be able to update an event", async () => {
const newEvent = await eventService.createEvent(event1)

Expand Down Expand Up @@ -178,6 +199,19 @@ describe("EventService integration tests", () => {
expect(fetchedReservation).toEqual(reservation1)
})

it("Should get the total count of active event reservations", async () => {
// An older event shouldn't be counted.
const oldEvent = await eventService.createEvent(event1)
await eventService.addReservation(oldEvent.id, reservation1)
// Should only count reservations for future events
const newEvent = await eventService.createEvent(futureEvent)
await eventService.addReservation(newEvent.id, reservation1)
await eventService.addReservation(newEvent.id, reservation2)

const count = await eventService.getActiveReservationsCount()
expect(count).toBe(2)
})

it("Should get all event reservations", async () => {
const newEvent = await eventService.createEvent(event1)
await eventService.addReservation(newEvent.id, reservation1)
Expand Down
44 changes: 42 additions & 2 deletions server/src/data-layer/services/EventService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import FirestoreCollections from "data-layer/adapters/FirestoreCollections"
import FirestoreSubcollections from "data-layer/adapters/FirestoreSubcollections"
import { DocumentDataWithUid } from "data-layer/models/common"
import { Event, EventReservation } from "data-layer/models/firebase"

class EventService {
Expand All @@ -25,6 +26,24 @@ class EventService {
return result.data()
}

/**
* Fetches all events that have a end_date in the future.
* Note that "active" means any event that haven't ended yet.
*
* @returns a list of events that have a end_date that is later to the current date.
*/
public async getActiveEvents(): Promise<DocumentDataWithUid<Event>[]> {
const now = new Date(Date.now())

const result = await FirestoreCollections.events
.where("end_date", ">", now) // Only get events that have not ended
.get()

return result.docs.map((doc) => {
return { ...(doc.data() as Event), id: doc.id }
})
}

/**
* Updates an existing event document by ID with new Event data.
*
Expand Down Expand Up @@ -72,7 +91,10 @@ class EventService {
* @param reservationId the ID of the reservation document
* @returns the reservation document
*/
public async getReservationById(eventId: string, reservationId: string) {
public async getReservationById(
eventId: string,
reservationId: string
): Promise<EventReservation> {
const result = await FirestoreSubcollections.reservations(eventId)
.doc(reservationId)
.get()
Expand All @@ -85,11 +107,29 @@ class EventService {
* @param eventId the ID of the event document
* @returns an array of all the event reservation documents
*/
public async getAllReservations(eventId: string) {
public async getAllReservations(
eventId: string
): Promise<EventReservation[]> {
const result = await FirestoreSubcollections.reservations(eventId).get()
return result.docs.map((doc) => doc.data())
}

/**
* Used for the SSE feature to display the total number of active event reservations.
* @returns the total number of active event reservations
*/
public async getActiveReservationsCount(): Promise<number> {
const currentEvents = await this.getActiveEvents()
let total = 0
await Promise.all(
currentEvents.map(async (event) => {
const eventReservations = await this.getAllReservations(event.id)
total += eventReservations.length
})
)
return total
}

/**
* Updates an existing reservation document by ID with new EventReservation data.
*
Expand Down
30 changes: 30 additions & 0 deletions server/src/middleware/__generated__/routes.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions server/src/middleware/__generated__/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 48 additions & 1 deletion server/src/service-layer/controllers/EventController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import EventService from "data-layer/services/EventService"
import { EventSignupBody } from "service-layer/request-models/EventRequests"
import { EventSignupResponse } from "service-layer/response-models/EventResponse"
import { Body, Controller, Post, Route, SuccessResponse } from "tsoa"
import {
Get,
Body,
Controller,
Post,
Route,
Request,
SuccessResponse
} from "tsoa"
import express from "express"

@Route("events")
export class EventController extends Controller {
Expand Down Expand Up @@ -58,4 +67,42 @@ export class EventController extends Controller {
return { error: "Failed to sign up for event." }
}
}

/**
* Streams the signup count for active events signups.
* Note that when testing this on swagger, the connection will remain open.
*/
@Get("/reservations/stream")
public async streamSignupCounts(
@Request() req: express.Request
): Promise<void> {
// Set the required headers for SSE
req.res.setHeader("Cache-Control", "no-cache")
req.res.setHeader("Content-Type", "text/event-stream")
req.res.setHeader("Access-Control-Allow-Origin", "*")
req.res.setHeader("Connection", "keep-alive")
req.res.flushHeaders()
const eventService = new EventService()

const signupCount = await eventService.getActiveReservationsCount() // Fetch the current signup count
req.res.write(
`data: ${JSON.stringify({ reservation_count: signupCount })}\n\n`
)

// Create something that updates every 5 seconds
const interValID = setInterval(async () => {
const signupCount = await eventService.getActiveReservationsCount() // Fetch the current signup count
// NOTE: We use double new line because SSE requires this to indicate we're ready for the next event
// We also need the data: to indicate data payload
req.res.write(
`data: ${JSON.stringify({ reservation_count: signupCount })}\n\n`
) // res.write() instead of res.send()
}, 5000)

// If the connection drops, stop sending events
req.res?.on("close", () => {
clearInterval(interValID) // Clear the loop
req.res?.end()
})
}
}
Loading