From 0f66dc670854b22b4dd1d7084e114ed02b40986a Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sat, 14 Sep 2024 23:54:19 +1200 Subject: [PATCH 1/7] Create getActiveEvents method in Event service and its tests Created the getActiveEvents to fetch present events --- NumbersApp.java | 54 +++++++++++++++++++ .../data-layer/services/EventService.test.ts | 23 ++++++++ .../src/data-layer/services/EventService.ts | 16 ++++++ 3 files changed, 93 insertions(+) create mode 100644 NumbersApp.java diff --git a/NumbersApp.java b/NumbersApp.java new file mode 100644 index 000000000..78a205c5d --- /dev/null +++ b/NumbersApp.java @@ -0,0 +1,54 @@ +import java.awt.*; +import javax.swing.*; +import java.awt.event.*; +import java.awt.FlowLayout; +import javax.swing.event.*; +import java.util.ArrayList; +import java.util.Arrays; + +public class NumbersApp extends JFrame { + private JTextField factorField; + private JList list; + private JButton addButton; + private Integer[] numbers = new Integer[]{Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)}; + public NumbersApp() { + factorField = new JTextField("3", 10); + list = new JList(numbers); + addButton = new JButton("Add"); + JPanel commandpanel = new JPanel(); + commandpanel.add(list); + commandpanel.add(factorField); + commandpanel.add(addButton); + getContentPane().add(commandpanel, BorderLayout.NORTH); + addButton.addActionListener( new AddFactorListener() ); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(600, 200); + setVisible(true); + } + //complete this + // We create a AddFactorListener class that implements ActionListener + class AddFactorListener implements ActionListener { + // This actionPerformed method runs when the button clicks + public void actionPerformed(ActionEvent e) { + // jijiku use try n catch because if not an integer, we dont do anything + try { + // parse the text field as an integer + int factor = Integer.parseInt(factorField.getText()); + // loop all numbers n try to add the factor + for(int i = 0; i < numbers.length; i++){ + numbers[i] += factor; + } + // update the java thingy + list.updateUI(); + } catch (NumberFormatException ex) { + // if not an integer, we set the text field to 3 + } + } + } + + public static void main(String[] args) { + javax.swing.SwingUtilities.invokeLater(new Runnable() { + public void run() {new NumbersApp();} + }); + } +} diff --git a/server/src/data-layer/services/EventService.test.ts b/server/src/data-layer/services/EventService.test.ts index 6842daed0..0a08db7dd 100644 --- a/server/src/data-layer/services/EventService.test.ts +++ b/server/src/data-layer/services/EventService.test.ts @@ -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() @@ -73,6 +74,28 @@ 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 now = new Date(Date.now()) + const scheduledEvent: 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)) + } + await eventService.createEvent(scheduledEvent) + + const futureEvents = await eventService.getActiveEvents() + + expect(futureEvents.length).toBe(1) + expect({ + ...futureEvents[0] + }).toEqual(scheduledEvent) + }) + it("Should be able to update an event", async () => { const newEvent = await eventService.createEvent(event1) diff --git a/server/src/data-layer/services/EventService.ts b/server/src/data-layer/services/EventService.ts index c0af72859..1e075de32 100644 --- a/server/src/data-layer/services/EventService.ts +++ b/server/src/data-layer/services/EventService.ts @@ -25,6 +25,22 @@ class EventService { return result.data() } + /** + * Fetches all events that have a start date in the future. + * Note that "active" means any event that is still upcoming, or "ready" to signup. + * + * @returns a list of events that have a start_date that is later to the current date. + */ + public async getActiveEvents(): Promise { + const now = new Date(Date.now()) + + const result = await FirestoreCollections.events + .where("start_date", ">=", now) + .get() + + return result.docs.map((doc) => doc.data() as Event) + } + /** * Updates an existing event document by ID with new Event data. * From bd655d035e921c4f90fb3eb02a78367492a6f9a6 Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sun, 15 Sep 2024 00:16:15 +1200 Subject: [PATCH 2/7] 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 --- .../data-layer/services/EventService.test.ts | 33 ++++++++++++------- .../src/data-layer/services/EventService.ts | 32 +++++++++++++++--- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/server/src/data-layer/services/EventService.test.ts b/server/src/data-layer/services/EventService.test.ts index 0a08db7dd..7b8a9d972 100644 --- a/server/src/data-layer/services/EventService.test.ts +++ b/server/src/data-layer/services/EventService.test.ts @@ -27,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", @@ -79,21 +86,12 @@ describe("EventService integration tests", () => { await eventService.createEvent(event1) await eventService.createEvent(event2) // Create a future event - const now = new Date(Date.now()) - const scheduledEvent: 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)) - } - await eventService.createEvent(scheduledEvent) + const newEvent = await eventService.createEvent(futureEvent) const futureEvents = await eventService.getActiveEvents() expect(futureEvents.length).toBe(1) - expect({ - ...futureEvents[0] - }).toEqual(scheduledEvent) + expect(futureEvents).toEqual([{ ...futureEvent, id: newEvent.id }]) }) it("Should be able to update an event", async () => { @@ -201,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) diff --git a/server/src/data-layer/services/EventService.ts b/server/src/data-layer/services/EventService.ts index 1e075de32..f005e35c8 100644 --- a/server/src/data-layer/services/EventService.ts +++ b/server/src/data-layer/services/EventService.ts @@ -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 { @@ -31,14 +32,16 @@ class EventService { * * @returns a list of events that have a start_date that is later to the current date. */ - public async getActiveEvents(): Promise { + public async getActiveEvents(): Promise[]> { const now = new Date(Date.now()) const result = await FirestoreCollections.events .where("start_date", ">=", now) .get() - return result.docs.map((doc) => doc.data() as Event) + return result.docs.map((doc) => { + return { ...(doc.data() as Event), id: doc.id } + }) } /** @@ -88,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 { const result = await FirestoreSubcollections.reservations(eventId) .doc(reservationId) .get() @@ -101,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 { 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 { + 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. * From 3f3771ce6b3c48e0da4fe74717aa2f231dcc4a6d Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sun, 15 Sep 2024 13:01:10 +1200 Subject: [PATCH 3/7] Create /events/reservations/stream SSE endpoint This endpoint creates a stream with keep-alive headers that continuously streams the number of active reservations. --- client/src/models/__generated__/schema.d.ts | 11 ++++++ server/src/middleware/__generated__/routes.ts | 30 ++++++++++++++++ .../src/middleware/__generated__/swagger.json | 12 +++++++ .../controllers/EventController.ts | 36 ++++++++++++++++++- 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/client/src/models/__generated__/schema.d.ts b/client/src/models/__generated__/schema.d.ts index 9b305014b..a00a9d28e 100644 --- a/client/src/models/__generated__/schema.d.ts +++ b/client/src/models/__generated__/schema.d.ts @@ -56,6 +56,9 @@ export interface paths { /** @description Signs up for an event */ post: operations["EventSignup"]; }; + "/events/reservations/stream": { + get: operations["StreamSignupCounts"]; + }; "/bookings": { /** @description Fetches all bookings for a user based on their UID. */ get: operations["GetAllBookings"]; @@ -847,6 +850,14 @@ export interface operations { }; }; }; + StreamSignupCounts: { + responses: { + /** @description No content */ + 204: { + content: never; + }; + }; + }; /** @description Fetches all bookings for a user based on their UID. */ GetAllBookings: { responses: { diff --git a/server/src/middleware/__generated__/routes.ts b/server/src/middleware/__generated__/routes.ts index d46b6ddd2..9c862ffc8 100644 --- a/server/src/middleware/__generated__/routes.ts +++ b/server/src/middleware/__generated__/routes.ts @@ -879,6 +879,36 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/events/reservations/stream', + ...(fetchMiddlewares(EventController)), + ...(fetchMiddlewares(EventController.prototype.streamSignupCounts)), + + function EventController_streamSignupCounts(request: ExRequest, response: ExResponse, next: any) { + const args: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new EventController(); + + templateService.apiHandler({ + methodName: 'streamSignupCounts', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/bookings', authenticateMiddleware([{"jwt":["member"]}]), ...(fetchMiddlewares(BookingController)), diff --git a/server/src/middleware/__generated__/swagger.json b/server/src/middleware/__generated__/swagger.json index 353b3488c..844cb1bd4 100644 --- a/server/src/middleware/__generated__/swagger.json +++ b/server/src/middleware/__generated__/swagger.json @@ -1746,6 +1746,18 @@ } } }, + "/events/reservations/stream": { + "get": { + "operationId": "StreamSignupCounts", + "responses": { + "204": { + "description": "No content" + } + }, + "security": [], + "parameters": [] + } + }, "/bookings": { "get": { "operationId": "GetAllBookings", diff --git a/server/src/service-layer/controllers/EventController.ts b/server/src/service-layer/controllers/EventController.ts index d753f470c..3b27a1b21 100644 --- a/server/src/service-layer/controllers/EventController.ts +++ b/server/src/service-layer/controllers/EventController.ts @@ -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 { @@ -58,4 +67,29 @@ export class EventController extends Controller { return { error: "Failed to sign up for event." } } } + + @Get("/reservations/stream") + public async streamSignupCounts( + @Request() req: express.Request + ): Promise { + // 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() + // Create something that updates every second + const interValID = setInterval(async () => { + const signupCount = await eventService.getActiveReservationsCount() // Fetch the current signup count + req.res?.write(`${signupCount}\n`) // res.write() instead of res.send() + }, 1000) + + // If the connection drops, stop sending events + req.res?.on("close", () => { + clearInterval(interValID) // Clear the loop + req.res?.end() + }) + } } From 771ed507754fdbf97a1fcbafbd33b4c53e764fea Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sun, 15 Sep 2024 13:16:06 +1200 Subject: [PATCH 4/7] Update payload to follow SSE requirements, also now returns a JSON payload --- client/src/models/__generated__/schema.d.ts | 8 +++++++ .../src/middleware/__generated__/swagger.json | 1 + .../controllers/EventController.ts | 21 +++++++++++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/client/src/models/__generated__/schema.d.ts b/client/src/models/__generated__/schema.d.ts index a00a9d28e..31d66734a 100644 --- a/client/src/models/__generated__/schema.d.ts +++ b/client/src/models/__generated__/schema.d.ts @@ -57,6 +57,10 @@ export interface paths { post: operations["EventSignup"]; }; "/events/reservations/stream": { + /** + * @description Streams the current signup count for events. + * Note that when testing this on swagger, the connection will remain open. + */ get: operations["StreamSignupCounts"]; }; "/bookings": { @@ -850,6 +854,10 @@ export interface operations { }; }; }; + /** + * @description Streams the current signup count for events. + * Note that when testing this on swagger, the connection will remain open. + */ StreamSignupCounts: { responses: { /** @description No content */ diff --git a/server/src/middleware/__generated__/swagger.json b/server/src/middleware/__generated__/swagger.json index 844cb1bd4..4272529dd 100644 --- a/server/src/middleware/__generated__/swagger.json +++ b/server/src/middleware/__generated__/swagger.json @@ -1754,6 +1754,7 @@ "description": "No content" } }, + "description": "Streams the current signup count for events.\nNote that when testing this on swagger, the connection will remain open.", "security": [], "parameters": [] } diff --git a/server/src/service-layer/controllers/EventController.ts b/server/src/service-layer/controllers/EventController.ts index 3b27a1b21..72b8ab635 100644 --- a/server/src/service-layer/controllers/EventController.ts +++ b/server/src/service-layer/controllers/EventController.ts @@ -68,6 +68,10 @@ export class EventController extends Controller { } } + /** + * Streams the current signup count for events. + * Note that when testing this on swagger, the connection will remain open. + */ @Get("/reservations/stream") public async streamSignupCounts( @Request() req: express.Request @@ -78,13 +82,22 @@ export class EventController extends Controller { req.res.setHeader("Access-Control-Allow-Origin", "*") req.res.setHeader("Connection", "keep-alive") req.res.flushHeaders() - const eventService = new EventService() - // Create something that updates every second + + 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 - req.res?.write(`${signupCount}\n`) // res.write() instead of res.send() - }, 1000) + // 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", () => { From 83ed32ee059ff8ff2d2c5c39b6a513a839cecec3 Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sun, 15 Sep 2024 13:17:08 +1200 Subject: [PATCH 5/7] Why is my java files in here lol...removed NumbersApp.java --- NumbersApp.java | 54 ------------------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 NumbersApp.java diff --git a/NumbersApp.java b/NumbersApp.java deleted file mode 100644 index 78a205c5d..000000000 --- a/NumbersApp.java +++ /dev/null @@ -1,54 +0,0 @@ -import java.awt.*; -import javax.swing.*; -import java.awt.event.*; -import java.awt.FlowLayout; -import javax.swing.event.*; -import java.util.ArrayList; -import java.util.Arrays; - -public class NumbersApp extends JFrame { - private JTextField factorField; - private JList list; - private JButton addButton; - private Integer[] numbers = new Integer[]{Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)}; - public NumbersApp() { - factorField = new JTextField("3", 10); - list = new JList(numbers); - addButton = new JButton("Add"); - JPanel commandpanel = new JPanel(); - commandpanel.add(list); - commandpanel.add(factorField); - commandpanel.add(addButton); - getContentPane().add(commandpanel, BorderLayout.NORTH); - addButton.addActionListener( new AddFactorListener() ); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setSize(600, 200); - setVisible(true); - } - //complete this - // We create a AddFactorListener class that implements ActionListener - class AddFactorListener implements ActionListener { - // This actionPerformed method runs when the button clicks - public void actionPerformed(ActionEvent e) { - // jijiku use try n catch because if not an integer, we dont do anything - try { - // parse the text field as an integer - int factor = Integer.parseInt(factorField.getText()); - // loop all numbers n try to add the factor - for(int i = 0; i < numbers.length; i++){ - numbers[i] += factor; - } - // update the java thingy - list.updateUI(); - } catch (NumberFormatException ex) { - // if not an integer, we set the text field to 3 - } - } - } - - public static void main(String[] args) { - javax.swing.SwingUtilities.invokeLater(new Runnable() { - public void run() {new NumbersApp();} - }); - } -} From 7d6c49f67d416096614b1c7b34cb6e8242e55bd3 Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sun, 15 Sep 2024 14:57:40 +1200 Subject: [PATCH 6/7] Update doc for schema and methods involved --- server/src/data-layer/models/firebase.ts | 6 +++--- server/src/data-layer/services/EventService.ts | 8 ++++---- server/src/service-layer/controllers/EventController.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/data-layer/models/firebase.ts b/server/src/data-layer/models/firebase.ts index f6145bd3a..a5822667c 100644 --- a/server/src/data-layer/models/firebase.ts +++ b/server/src/data-layer/models/firebase.ts @@ -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 diff --git a/server/src/data-layer/services/EventService.ts b/server/src/data-layer/services/EventService.ts index f005e35c8..6370b97a6 100644 --- a/server/src/data-layer/services/EventService.ts +++ b/server/src/data-layer/services/EventService.ts @@ -27,16 +27,16 @@ class EventService { } /** - * Fetches all events that have a start date in the future. - * Note that "active" means any event that is still upcoming, or "ready" to signup. + * 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 start_date that is later to the current date. + * @returns a list of events that have a end_date that is later to the current date. */ public async getActiveEvents(): Promise[]> { const now = new Date(Date.now()) const result = await FirestoreCollections.events - .where("start_date", ">=", now) + .where("end_date", ">", now) // Only get events that have not ended .get() return result.docs.map((doc) => { diff --git a/server/src/service-layer/controllers/EventController.ts b/server/src/service-layer/controllers/EventController.ts index 72b8ab635..81d3a2564 100644 --- a/server/src/service-layer/controllers/EventController.ts +++ b/server/src/service-layer/controllers/EventController.ts @@ -69,7 +69,7 @@ export class EventController extends Controller { } /** - * Streams the current signup count for events. + * Streams the signup count for active events signups. * Note that when testing this on swagger, the connection will remain open. */ @Get("/reservations/stream") From fd12ae5eede260d8b705455539366b64e3d029dc Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sun, 15 Sep 2024 14:59:04 +1200 Subject: [PATCH 7/7] Add generated routes --- client/src/models/__generated__/schema.d.ts | 4 ++-- server/src/middleware/__generated__/swagger.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/models/__generated__/schema.d.ts b/client/src/models/__generated__/schema.d.ts index 31d66734a..2859e8499 100644 --- a/client/src/models/__generated__/schema.d.ts +++ b/client/src/models/__generated__/schema.d.ts @@ -58,7 +58,7 @@ export interface paths { }; "/events/reservations/stream": { /** - * @description Streams the current signup count for events. + * @description Streams the signup count for active events signups. * Note that when testing this on swagger, the connection will remain open. */ get: operations["StreamSignupCounts"]; @@ -855,7 +855,7 @@ export interface operations { }; }; /** - * @description Streams the current signup count for events. + * @description Streams the signup count for active events signups. * Note that when testing this on swagger, the connection will remain open. */ StreamSignupCounts: { diff --git a/server/src/middleware/__generated__/swagger.json b/server/src/middleware/__generated__/swagger.json index 4272529dd..dc1908ec9 100644 --- a/server/src/middleware/__generated__/swagger.json +++ b/server/src/middleware/__generated__/swagger.json @@ -1754,7 +1754,7 @@ "description": "No content" } }, - "description": "Streams the current signup count for events.\nNote that when testing this on swagger, the connection will remain open.", + "description": "Streams the signup count for active events signups.\nNote that when testing this on swagger, the connection will remain open.", "security": [], "parameters": [] }