From 336f7b44fb6ff83eec952a7e76b942bffb13a69d Mon Sep 17 00:00:00 2001 From: bh0fer Date: Tue, 21 Jul 2026 17:02:16 +0000 Subject: [PATCH 1/4] feature: handle streamable doc update policy in student groups --- .../migration.sql | 2 ++ prisma/schema.prisma | 1 + prisma/view-migrations/tsconfig.json | 8 +++----- src/models/StudentGroup.ts | 2 +- src/routes/event-handlers/joinRoom.handler.ts | 9 +++++---- .../event-handlers/streamUpdate.handler.ts | 17 ++++++++++++++--- src/routes/socketEventTypes.ts | 5 +++-- 7 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql diff --git a/prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql b/prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql new file mode 100644 index 0000000..5b00fa6 --- /dev/null +++ b/prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "student_groups" ADD COLUMN "can_stream_updates" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9d582e3..ea823c9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -139,6 +139,7 @@ model StudentGroup { parentId String? @map("parent_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + canStreamUpdates Boolean @default(false) @map("can_stream_updates") rootGroupPermissions RootGroupPermission[] @relation("root_group_to_student_group_permission") parent StudentGroup? @relation("parent_student_group", fields: [parentId], references: [id], onDelete: Cascade) children StudentGroup[] @relation("parent_student_group") diff --git a/prisma/view-migrations/tsconfig.json b/prisma/view-migrations/tsconfig.json index 7834ebc..b70b2f9 100644 --- a/prisma/view-migrations/tsconfig.json +++ b/prisma/view-migrations/tsconfig.json @@ -4,12 +4,10 @@ "target": "esnext", "module": "nodenext", "outDir": "../../dist/view-migrations", - "rootDir": "./", "esModuleInterop": true, "skipLibCheck": true, - "baseUrl": "../../", - "paths": { - "*": ["node_modules/*"] - } + "types": [ + "node" + ], } } \ No newline at end of file diff --git a/src/models/StudentGroup.ts b/src/models/StudentGroup.ts index cd57d52..6ed0c89 100644 --- a/src/models/StudentGroup.ts +++ b/src/models/StudentGroup.ts @@ -6,7 +6,7 @@ import { hasElevatedAccess, Role } from './User.js'; const getData = createDataExtractor( ['description', 'name'], - ['parentId'] + ['parentId', 'canStreamUpdates'] ); export type ApiStudentGroup = DbStudentGroup & { userIds: string[]; adminIds: string[] }; diff --git a/src/routes/event-handlers/joinRoom.handler.ts b/src/routes/event-handlers/joinRoom.handler.ts index f5aed86..a89775f 100644 --- a/src/routes/event-handlers/joinRoom.handler.ts +++ b/src/routes/event-handlers/joinRoom.handler.ts @@ -7,6 +7,7 @@ import onStreamUpdate from './streamUpdate.handler.js'; import DocumentRoot from '../../models/DocumentRoot.js'; import { highestAccess, RWAccess } from '../../helpers/accessPolicy.js'; import { Role } from '../../models/User.js'; +import Logger from '../../utils/logger.js'; type SocketType = Socket; const isDocumentRoot = (roomId: string) => { @@ -44,9 +45,9 @@ const joinRoom = (socket: SocketType, roomId: string, joinStreamGroup: boolean) const onJoinRoom: (user: User, socket: SocketType) => ClientToServerEvents[IoClientEvent.JOIN_ROOM] = (user, socket) => (roomId: string, callback: (joined: boolean) => void) => { if (user.role === Role.ADMIN) { - return isDocumentRoot(roomId) - .then((docRoot) => { - joinRoom(socket, roomId, !!docRoot); + return Promise.all([isDocumentRoot(roomId), StudentGroup.findModel(user, roomId)]) + .then(([docRoot, group]) => { + joinRoom(socket, roomId, !!docRoot || !!(group && group.canStreamUpdates)); callback(true); }) .catch(() => { @@ -55,7 +56,7 @@ const onJoinRoom: (user: User, socket: SocketType) => ClientToServerEvents[IoCli } StudentGroup.findModel(user, roomId).then((group) => { if (group) { - socket.join(roomId); + joinRoom(socket, roomId, group.canStreamUpdates); callback(true); } else { if (user.role === Role.TEACHER) { diff --git a/src/routes/event-handlers/streamUpdate.handler.ts b/src/routes/event-handlers/streamUpdate.handler.ts index afefcbb..1a0e373 100644 --- a/src/routes/event-handlers/streamUpdate.handler.ts +++ b/src/routes/event-handlers/streamUpdate.handler.ts @@ -1,4 +1,10 @@ -import { ClientToServerEvents, IoClientEvent, IoEvent, ServerToClientEvents } from '../socketEventTypes.js'; +import { + ChangedDocument, + ClientToServerEvents, + IoClientEvent, + IoEvent, + ServerToClientEvents +} from '../socketEventTypes.js'; import type { DefaultEventsMap, Socket } from 'socket.io'; const onStreamUpdate: ( @@ -8,11 +14,16 @@ const onStreamUpdate: ( if (roomId !== payload.roomId) { return; } - socket.to(payload.roomId).emit(IoEvent.CHANGED_DOCUMENT, { + const pkg: ChangedDocument = { data: payload.data, id: payload.id, updatedAt: payload.updatedAt - }); + }; + if (payload.meta) { + pkg.meta = payload.meta; + } + + socket.to(payload.roomId).emit(IoEvent.CHANGED_DOCUMENT, pkg); }; export default onStreamUpdate; diff --git a/src/routes/socketEventTypes.ts b/src/routes/socketEventTypes.ts index 41b568e..7cfbdf2 100644 --- a/src/routes/socketEventTypes.ts +++ b/src/routes/socketEventTypes.ts @@ -47,13 +47,14 @@ export interface ChangedRecord { record: TypeRecordMap[T]; } -export interface ChangedDocument { +export interface ChangedDocument { id: string; data: Prisma.JsonValue; updatedAt: Date; + meta?: T; } -export interface StreamedDynamicDocument extends ChangedDocument { +export interface StreamedDynamicDocument extends ChangedDocument { roomId: string; } From a0d125886372e32ab241957f26f0c18d86354e6d Mon Sep 17 00:00:00 2001 From: bh0fer Date: Wed, 22 Jul 2026 12:28:28 +0000 Subject: [PATCH 2/4] move stream prop to student group --- .../migration.sql | 2 - .../migration.sql | 3 ++ prisma/schema.prisma | 3 +- src/controllers/studentGroups.ts | 11 +++-- src/models/StudentGroup.ts | 46 +++++++++++++++++-- src/routes/event-handlers/joinRoom.handler.ts | 8 ++-- .../event-handlers/streamUpdate.handler.ts | 21 +++++++++ src/routes/socketEvents.ts | 2 + src/server.ts | 2 + 9 files changed, 83 insertions(+), 15 deletions(-) delete mode 100644 prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql create mode 100644 prisma/migrations/20260722070708_add_presentation_flag_to_student_group/migration.sql diff --git a/prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql b/prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql deleted file mode 100644 index 5b00fa6..0000000 --- a/prisma/migrations/20260721161522_add_can_stream_updates_flag_to_groups/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "student_groups" ADD COLUMN "can_stream_updates" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20260722070708_add_presentation_flag_to_student_group/migration.sql b/prisma/migrations/20260722070708_add_presentation_flag_to_student_group/migration.sql new file mode 100644 index 0000000..efabb7f --- /dev/null +++ b/prisma/migrations/20260722070708_add_presentation_flag_to_student_group/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "student_groups" ADD COLUMN "can_present" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "presented_document" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ea823c9..a441c63 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -139,7 +139,8 @@ model StudentGroup { parentId String? @map("parent_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") - canStreamUpdates Boolean @default(false) @map("can_stream_updates") + canPresent Boolean @default(false) @map("can_present") + presentedDocument Json? @map("presented_document") rootGroupPermissions RootGroupPermission[] @relation("root_group_to_student_group_permission") parent StudentGroup? @relation("parent_student_group", fields: [parentId], references: [id], onDelete: Cascade) children StudentGroup[] @relation("parent_student_group") diff --git a/src/controllers/studentGroups.ts b/src/controllers/studentGroups.ts index ff695a6..6d74037 100644 --- a/src/controllers/studentGroups.ts +++ b/src/controllers/studentGroups.ts @@ -2,6 +2,7 @@ import { StudentGroup as DbStudentGroup } from '../../prisma/generated/client.js import { RequestHandler } from 'express'; import StudentGroup from '../models/StudentGroup.js'; import { IoEvent, RecordType } from '../routes/socketEventTypes.js'; +import { JsonObject } from '@prisma/client/runtime/client'; export const find: RequestHandler<{ id: string }> = async (req, res, next) => { const group = await StudentGroup.findModel((req as any).user!, req.params.id); @@ -14,11 +15,11 @@ export const create: RequestHandler = async (req, res, res.status(200).json(model); }; -export const update: RequestHandler<{ id: string }, any, { data: DbStudentGroup }> = async ( - req, - res, - next -) => { +export const update: RequestHandler< + { id: string }, + any, + { data: Partial & { presentedDocument: JsonObject }> } +> = async (req, res, next) => { const model = await StudentGroup.updateModel((req as any).user!, req.params.id, req.body.data); if (!model) { return res.status(404).json({ message: 'Student group not found' }); diff --git a/src/models/StudentGroup.ts b/src/models/StudentGroup.ts index 6ed0c89..1c3eca3 100644 --- a/src/models/StudentGroup.ts +++ b/src/models/StudentGroup.ts @@ -3,14 +3,34 @@ import prisma from '../prisma.js'; import { HTTP403Error, HTTP404Error } from '../utils/errors/Errors.js'; import { createDataExtractor } from '../helpers/dataExtractor.js'; import { hasElevatedAccess, Role } from './User.js'; +import { JsonObject } from '@prisma/client/runtime/client'; +import Logger from '../utils/logger.js'; const getData = createDataExtractor( ['description', 'name'], - ['parentId', 'canStreamUpdates'] + ['parentId', 'canPresent', 'presentedDocument'] ); export type ApiStudentGroup = DbStudentGroup & { userIds: string[]; adminIds: string[] }; +export const StreamableGroupUserCacheStore = new Map>(); + +export const initializeStreamableGroupUserCache = async () => { + const all = await prisma.studentGroup.findMany({ + include: { users: true } + }); + const groupUsers = all + .map((group) => asApiRecord(group)!) + .map((g) => [g.id, [...g.userIds, ...g.adminIds]]) as [string, string[]][]; + StreamableGroupUserCacheStore.clear(); + for (const [groupId, userIds] of groupUsers) { + StreamableGroupUserCacheStore.set(groupId, new Set(userIds)); + } + Logger.info( + `☄️ Initialized StreamableGroupUserCacheStore with ${StreamableGroupUserCacheStore.size} groups` + ); +}; + function asApiRecord( record: DbStudentGroup & { users: { userId: string; isAdmin: boolean }[] } ): ApiStudentGroup; @@ -65,13 +85,18 @@ function StudentGroup(db: PrismaClient['studentGroup']) { return asApiRecord(model); }, - async updateModel(actor: User, id: string, data: Partial): Promise { + async updateModel( + actor: User, + id: string, + data: Partial & { presentedDocument: JsonObject }> + ): Promise { const record = await this.findModel(actor, id); if (!hasAdminAccess(actor, record)) { throw new HTTP403Error('Not authorized'); } /** remove fields not updatable*/ const sanitized = getData(data, false, hasElevatedAccess(actor.role)); + const parentId = typeof sanitized.parentId === 'string' ? sanitized.parentId : sanitized.parentId?.set; if (parentId && actor.role !== Role.ADMIN) { @@ -87,7 +112,18 @@ function StudentGroup(db: PrismaClient['studentGroup']) { data: sanitized, include: { users: { select: { userId: true, isAdmin: true } } } }); - return asApiRecord(result); + const resultApi = asApiRecord(result); + if (resultApi.canPresent !== record.canPresent) { + if (record.canPresent) { + StreamableGroupUserCacheStore.set( + id, + new Set(resultApi.userIds.concat(resultApi.adminIds)) + ); + } else { + StreamableGroupUserCacheStore.delete(id); + } + } + return resultApi; }, async setAdminRole( @@ -143,6 +179,7 @@ function StudentGroup(db: PrismaClient['studentGroup']) { }, include: { users: { select: { userId: true, isAdmin: true } } } }); + StreamableGroupUserCacheStore.get(id)?.add(userId); return asApiRecord(result)!; }, @@ -160,6 +197,7 @@ function StudentGroup(db: PrismaClient['studentGroup']) { data: { users: { delete: { id: { userId: userId, studentGroupId: record.id } } } }, include: { users: { select: { userId: true, isAdmin: true } } } }); + StreamableGroupUserCacheStore.get(id)?.delete(userId); return asApiRecord(result)!; }, @@ -204,6 +242,7 @@ function StudentGroup(db: PrismaClient['studentGroup']) { }, include: { users: { select: { userId: true, isAdmin: true } } } }); + StreamableGroupUserCacheStore.set(model.id, new Set([actor.id])); return asApiRecord(model)!; }, @@ -212,6 +251,7 @@ function StudentGroup(db: PrismaClient['studentGroup']) { if (!hasAdminAccess(actor, record)) { throw new HTTP403Error('Not authorized'); } + StreamableGroupUserCacheStore.delete(id); return db.delete({ where: { id: id } }); } }); diff --git a/src/routes/event-handlers/joinRoom.handler.ts b/src/routes/event-handlers/joinRoom.handler.ts index a89775f..92f9d36 100644 --- a/src/routes/event-handlers/joinRoom.handler.ts +++ b/src/routes/event-handlers/joinRoom.handler.ts @@ -3,7 +3,7 @@ import { ClientToServerEvents, IoClientEvent, ServerToClientEvents } from '../so import type { DefaultEventsMap, Socket } from 'socket.io'; import prisma from '../../prisma.js'; import StudentGroup from '../../models/StudentGroup.js'; -import onStreamUpdate from './streamUpdate.handler.js'; +import onStreamUpdate, { onStreamDynamicRoomUpdate } from './streamUpdate.handler.js'; import DocumentRoot from '../../models/DocumentRoot.js'; import { highestAccess, RWAccess } from '../../helpers/accessPolicy.js'; import { Role } from '../../models/User.js'; @@ -38,7 +38,7 @@ const findStudentGroup = (userId: string, roomId: string) => { const joinRoom = (socket: SocketType, roomId: string, joinStreamGroup: boolean) => { socket.join(roomId); if (joinStreamGroup) { - socket.on(IoClientEvent.STREAM_UPDATE, onStreamUpdate(roomId, socket)); + socket.on(IoClientEvent.STREAM_UPDATE, onStreamDynamicRoomUpdate(roomId, socket)); } }; @@ -47,7 +47,7 @@ const onJoinRoom: (user: User, socket: SocketType) => ClientToServerEvents[IoCli if (user.role === Role.ADMIN) { return Promise.all([isDocumentRoot(roomId), StudentGroup.findModel(user, roomId)]) .then(([docRoot, group]) => { - joinRoom(socket, roomId, !!docRoot || !!(group && group.canStreamUpdates)); + joinRoom(socket, roomId, !!docRoot || !!(group && group.canPresent)); callback(true); }) .catch(() => { @@ -56,7 +56,7 @@ const onJoinRoom: (user: User, socket: SocketType) => ClientToServerEvents[IoCli } StudentGroup.findModel(user, roomId).then((group) => { if (group) { - joinRoom(socket, roomId, group.canStreamUpdates); + joinRoom(socket, roomId, group.canPresent); callback(true); } else { if (user.role === Role.TEACHER) { diff --git a/src/routes/event-handlers/streamUpdate.handler.ts b/src/routes/event-handlers/streamUpdate.handler.ts index 1a0e373..c8bc334 100644 --- a/src/routes/event-handlers/streamUpdate.handler.ts +++ b/src/routes/event-handlers/streamUpdate.handler.ts @@ -5,9 +5,30 @@ import { IoEvent, ServerToClientEvents } from '../socketEventTypes.js'; +import { User } from '../../../prisma/generated/client.js'; import type { DefaultEventsMap, Socket } from 'socket.io'; +import { StreamableGroupUserCacheStore } from '../../models/StudentGroup.js'; const onStreamUpdate: ( + user: User, + socket: Socket +) => ClientToServerEvents[IoClientEvent.STREAM_UPDATE] = (user, socket) => (payload) => { + if (!StreamableGroupUserCacheStore.get(payload.roomId)?.has(user.id)) { + return; + } + const pkg: ChangedDocument = { + data: payload.data, + id: payload.id, + updatedAt: payload.updatedAt + }; + if (payload.meta) { + pkg.meta = payload.meta; + } + + socket.to(payload.roomId).emit(IoEvent.CHANGED_DOCUMENT, pkg); +}; + +export const onStreamDynamicRoomUpdate: ( roomId: string, socket: Socket ) => ClientToServerEvents[IoClientEvent.STREAM_UPDATE] = (roomId, socket) => (payload) => { diff --git a/src/routes/socketEvents.ts b/src/routes/socketEvents.ts index 6e7ab76..ce1c2d1 100644 --- a/src/routes/socketEvents.ts +++ b/src/routes/socketEvents.ts @@ -10,6 +10,7 @@ import onAction from './event-handlers/action.handler.js'; import onJoinRoom from './event-handlers/joinRoom.handler.js'; import onLeaveRoom from './event-handlers/leaveRoom.handler.js'; import { auth } from '../auth.js'; +import onStreamUpdate from './event-handlers/streamUpdate.handler.js'; export enum IoRoom { ADMIN = 'admin', @@ -44,6 +45,7 @@ const EventRouter = (io: Server) => socket.join(IoRoom.ALL); socket.on(IoClientEvent.JOIN_ROOM, onJoinRoom(user, socket)); socket.on(IoClientEvent.LEAVE_ROOM, onLeaveRoom(user, socket)); + socket.on(IoClientEvent.STREAM_UPDATE, onStreamUpdate(user, socket)); const groups = await StudentGroup.all(user); const groupIds = groups.map((group) => group.id); if (groupIds.length > 0) { diff --git a/src/server.ts b/src/server.ts index d959592..6058194 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,7 @@ import http from 'http'; import * as Sentry from '@sentry/node'; import Logger from './utils/logger.js'; import dotenv from 'dotenv'; +import { initializeStreamableGroupUserCache } from './models/StudentGroup.js'; dotenv.config(); const PORT = process.env.PORT || 3002; @@ -12,6 +13,7 @@ const server = http.createServer(app); initializeSocketIo(server); configure(app); +initializeStreamableGroupUserCache(); if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) { Sentry.setupExpressErrorHandler(app); From fdc4c2437b270c88a818060057384dc3921461e3 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Wed, 22 Jul 2026 15:58:51 +0000 Subject: [PATCH 3/4] fix cache invariants --- src/models/StudentGroup.ts | 29 ++++++++++++++--------------- src/server.ts | 27 +++++++++++++++++---------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/models/StudentGroup.ts b/src/models/StudentGroup.ts index 1c3eca3..a1d1629 100644 --- a/src/models/StudentGroup.ts +++ b/src/models/StudentGroup.ts @@ -15,16 +15,21 @@ export type ApiStudentGroup = DbStudentGroup & { userIds: string[]; adminIds: st export const StreamableGroupUserCacheStore = new Map>(); +const setStreamableGroupUsers = (group: ApiStudentGroup) => { + if (!group.canPresent) { + StreamableGroupUserCacheStore.delete(group.id); + return; + } + StreamableGroupUserCacheStore.set(group.id, new Set(group.userIds.concat(group.adminIds))); +}; + export const initializeStreamableGroupUserCache = async () => { const all = await prisma.studentGroup.findMany({ include: { users: true } }); - const groupUsers = all - .map((group) => asApiRecord(group)!) - .map((g) => [g.id, [...g.userIds, ...g.adminIds]]) as [string, string[]][]; StreamableGroupUserCacheStore.clear(); - for (const [groupId, userIds] of groupUsers) { - StreamableGroupUserCacheStore.set(groupId, new Set(userIds)); + for (const group of all.map((record) => asApiRecord(record)!)) { + setStreamableGroupUsers(group); } Logger.info( `☄️ Initialized StreamableGroupUserCacheStore with ${StreamableGroupUserCacheStore.size} groups` @@ -114,14 +119,7 @@ function StudentGroup(db: PrismaClient['studentGroup']) { }); const resultApi = asApiRecord(result); if (resultApi.canPresent !== record.canPresent) { - if (record.canPresent) { - StreamableGroupUserCacheStore.set( - id, - new Set(resultApi.userIds.concat(resultApi.adminIds)) - ); - } else { - StreamableGroupUserCacheStore.delete(id); - } + setStreamableGroupUsers(resultApi); } return resultApi; }, @@ -242,8 +240,9 @@ function StudentGroup(db: PrismaClient['studentGroup']) { }, include: { users: { select: { userId: true, isAdmin: true } } } }); - StreamableGroupUserCacheStore.set(model.id, new Set([actor.id])); - return asApiRecord(model)!; + const result = asApiRecord(model)!; + setStreamableGroupUsers(result); + return result; }, async deleteModel(actor: User, id: string): Promise { diff --git a/src/server.ts b/src/server.ts index 6058194..cb93198 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,17 +9,24 @@ dotenv.config(); const PORT = process.env.PORT || 3002; -const server = http.createServer(app); -initializeSocketIo(server); +const start = async () => { + const server = http.createServer(app); + initializeSocketIo(server); -configure(app); -initializeStreamableGroupUserCache(); + configure(app); + await initializeStreamableGroupUserCache(); -if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) { - Sentry.setupExpressErrorHandler(app); -} + if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) { + Sentry.setupExpressErrorHandler(app); + } -server.listen(PORT || 3002, () => { - Logger.info(`application is running at: http://localhost:${PORT}`); - Logger.info('Press Ctrl+C to quit.'); + server.listen(PORT || 3002, () => { + Logger.info(`application is running at: http://localhost:${PORT}`); + Logger.info('Press Ctrl+C to quit.'); + }); +}; + +start().catch((error) => { + Logger.error('Failed to initialize server', error); + process.exit(1); }); From cb47b3b69beea39c9250a801d3db3702f11a4b2e Mon Sep 17 00:00:00 2001 From: bh0fer Date: Wed, 22 Jul 2026 16:13:02 +0000 Subject: [PATCH 4/4] recreate cache all 45' --- src/models/StudentGroup.ts | 8 +++++++- src/server.ts | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/models/StudentGroup.ts b/src/models/StudentGroup.ts index a1d1629..cce2da1 100644 --- a/src/models/StudentGroup.ts +++ b/src/models/StudentGroup.ts @@ -23,7 +23,12 @@ const setStreamableGroupUsers = (group: ApiStudentGroup) => { StreamableGroupUserCacheStore.set(group.id, new Set(group.userIds.concat(group.adminIds))); }; -export const initializeStreamableGroupUserCache = async () => { +let lastCacheRecreation: number | null = null; +const MS_IN_45_MINUTES = 1000 * 60 * 45; +export const recreateStreamableGroupUserCache = async () => { + if (lastCacheRecreation && Date.now() - lastCacheRecreation < MS_IN_45_MINUTES) { + return; + } const all = await prisma.studentGroup.findMany({ include: { users: true } }); @@ -34,6 +39,7 @@ export const initializeStreamableGroupUserCache = async () => { Logger.info( `☄️ Initialized StreamableGroupUserCacheStore with ${StreamableGroupUserCacheStore.size} groups` ); + lastCacheRecreation = Date.now(); }; function asApiRecord( diff --git a/src/server.ts b/src/server.ts index cb93198..e68301d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,7 @@ import http from 'http'; import * as Sentry from '@sentry/node'; import Logger from './utils/logger.js'; import dotenv from 'dotenv'; -import { initializeStreamableGroupUserCache } from './models/StudentGroup.js'; +import { recreateStreamableGroupUserCache } from './models/StudentGroup.js'; dotenv.config(); const PORT = process.env.PORT || 3002; @@ -14,7 +14,7 @@ const start = async () => { initializeSocketIo(server); configure(app); - await initializeStreamableGroupUserCache(); + await recreateStreamableGroupUserCache(); if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) { Sentry.setupExpressErrorHandler(app);