Skip to content

Commit

Permalink
Set up SSE (Server sent events) for event signup counts (#776)
Browse files Browse the repository at this point in the history
* Create getActiveEvents method in Event service and its tests

Created the getActiveEvents to fetch present events

* Create getActiveReservationsCount method that uses previous created method that returns total count of reservations

Also did some documentation for methods that this method uses.

Created tests that are required

* Create /events/reservations/stream SSE endpoint

This endpoint creates a stream with keep-alive headers that continuously streams the number of active reservations.

* Update payload to follow SSE requirements, also now returns a JSON payload

* Why is my java files in here lol...removed NumbersApp.java

* Update doc for schema and methods involved

* Add generated routes
  • Loading branch information
jeffplays2005 authored Sep 15, 2024
1 parent f0ebbe0 commit 0a1f8c1
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 6 deletions.
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()
})
}
}

0 comments on commit 0a1f8c1

Please sign in to comment.