From 4cf8e6726dd0d8f454642d94f6495bd8d4be0779 Mon Sep 17 00:00:00 2001 From: Eamonn de Leastar Date: Thu, 9 Jan 2025 17:05:36 +0000 Subject: [PATCH 1/2] documentation for live and presence --- .../community/services/live.svelte.ts | 100 +++++++++++------- .../community/services/presence.svelte.ts | 45 +++----- src/lib/services/community/types.svelte.ts | 88 ++++++++++++--- 3 files changed, 149 insertions(+), 84 deletions(-) diff --git a/src/lib/services/community/services/live.svelte.ts b/src/lib/services/community/services/live.svelte.ts index 585a6f10..a456c355 100644 --- a/src/lib/services/community/services/live.svelte.ts +++ b/src/lib/services/community/services/live.svelte.ts @@ -1,14 +1,19 @@ +/** + * PartyKit-based implementation of LiveService + * Uses WebSocket connections to track both course-specific and platform-wide activity + */ + import PartySocket from "partysocket"; import { PUBLIC_party_kit_main_room } from "$env/static/public"; import { refreshLoRecord } from "./presence.svelte"; import { rune } from "$lib/runes.svelte"; import { LoRecord, type LiveService } from "../types.svelte"; -/** PartyKit server URL from environment */ +// Server URL from environment variables const partyKitServer = PUBLIC_party_kit_main_room; +// Initialize global WebSocket connection if server URL is configured let partyKitAll = {}; - if (PUBLIC_party_kit_main_room !== "XXX") { partyKitAll = new PartySocket({ host: partyKitServer, @@ -17,87 +22,102 @@ if (PUBLIC_party_kit_main_room !== "XXX") { } export const liveService: LiveService = { + // Initialize reactive state using Svelte runes listeningForCourse: rune(""), coursesOnline: rune([]), studentsOnline: rune([]), studentsOnlineByCourse: rune([]), + // Maps for efficient event lookup and updates studentEventMap: new Map(), courseEventMap: new Map(), + // Course-specific WebSocket connection - initialized on demand partyKitCourse: {}, listeningAll: false, - groupedStudentListener(event: any) { - const courseArray = this.studentsOnlineByCourse.value.find((lo: LoRecord[]) => lo[0].courseId === event.courseId); + groupedStudentListener(event: MessageEvent) { + // Parse incoming WebSocket data + const data = JSON.parse(event.data); + + // Find existing course group or create new one + const courseArray = this.studentsOnlineByCourse.value.find((lo: LoRecord[]) => lo[0].courseId === data.courseId); if (!courseArray) { + // First student in this course const studentArray = new Array(); - studentArray.push(new LoRecord(event)); + studentArray.push(new LoRecord(data)); this.studentsOnlineByCourse.value.push(studentArray); } else { - const loStudent = courseArray.find((lo: LoRecord) => lo.user?.id === event.user.id); + // Course exists, find or add student + const loStudent = courseArray.find((lo: LoRecord) => lo.user?.id === data.user.id); if (!loStudent) { - courseArray.push(new LoRecord(event)); + courseArray.push(new LoRecord(data)); } else { - refreshLoRecord(loStudent, event); + refreshLoRecord(loStudent, data); } } }, - studentListener(event: any) { - const studentEvent = this.studentEventMap.get(event.user.id); + studentListener(event: MessageEvent) { + // Parse and extract student data + const data = JSON.parse(event.data); + const studentEvent = this.studentEventMap.get(data.user.id); + if (!studentEvent) { - const latestLo = new LoRecord(event); + // First time seeing this student + const latestLo = new LoRecord(data); this.studentsOnline.value.push(latestLo); - this.studentEventMap.set(event.user.id, latestLo); + this.studentEventMap.set(data.user.id, latestLo); } else { - refreshLoRecord(studentEvent, event); + // Update existing student record + refreshLoRecord(studentEvent, data); } }, - courseListener(event: any) { - const courseEvent = this.courseEventMap.get(event.courseId); + courseListener(event: MessageEvent) { + // Parse and extract course data + const data = JSON.parse(event.data); + const courseEvent = this.courseEventMap.get(data.courseId); + if (!courseEvent) { - const latestLo = new LoRecord(event); + // First activity in this course + const latestLo = new LoRecord(data); this.coursesOnline.value.push(latestLo); - this.courseEventMap.set(event.courseId, latestLo); + this.courseEventMap.set(data.courseId, latestLo); } else { - refreshLoRecord(courseEvent, event); + // Update existing course activity + refreshLoRecord(courseEvent, data); } }, - partyKitListener(event: any) { - try { - const nextCourseEvent = JSON.parse(event.data); - this.courseListener(nextCourseEvent); - this.studentListener(nextCourseEvent); - this.groupedStudentListener(nextCourseEvent); - } catch (e) { - console.log(e); - } + + partyKitListener(event: MessageEvent) { + // Parse message once and distribute to specific handlers + this.groupedStudentListener(event); + this.courseListener(event); + this.studentListener(event); }, startGlobalPresenceService() { + // Only set up global listener once if (!this.listeningAll) { + partyKitAll.addEventListener("message", this.partyKitListener.bind(this)); this.listeningAll = true; - partyKitAll.addEventListener("message", (event) => { - this.partyKitListener(event); - }); } }, startCoursePresenceListener(courseId: string) { - const partyKitCourse = new PartySocket({ + // Reset state for new course + this.listeningForCourse.value = courseId; + this.studentsOnline.value = []; + this.studentEventMap.clear(); + + // Create new WebSocket connection for course + this.partyKitCourse = new PartySocket({ host: partyKitServer, room: courseId }); - partyKitCourse.addEventListener("message", (event) => { - try { - const nextCourseEvent = JSON.parse(event.data); - this.listeningForCourse.value = nextCourseEvent.courseTitle; - this.groupedStudentListener(nextCourseEvent); - } catch (e) { - console.log(e); - } - }); + + // Bind message handler with correct 'this' context + this.partyKitCourse.addEventListener("message", this.studentListener.bind(this)); } }; diff --git a/src/lib/services/community/services/presence.svelte.ts b/src/lib/services/community/services/presence.svelte.ts index 47a4c030..1aa56e84 100644 --- a/src/lib/services/community/services/presence.svelte.ts +++ b/src/lib/services/community/services/presence.svelte.ts @@ -1,6 +1,6 @@ /** - * Real-time presence service using PartyKit for user activity tracking. - * Manages WebSocket connections for both global and course-specific events. + * PartyKit-based implementation of the presence service + * Uses WebSocket connections to track and broadcast user activity */ import PartySocket from "partysocket"; @@ -11,74 +11,61 @@ import { rune, tutorsId } from "$lib/runes.svelte"; import { LoRecord, type LoUser, type PresenceService } from "../types.svelte"; import type { TutorsId } from "$lib/services/connect"; -/** PartyKit server URL from environment */ +// Server URL from environment variables const partyKitServer = PUBLIC_party_kit_main_room; export const presenceService: PresenceService = { - /** Global PartyKit connection for all course events */ + // Initialize empty WebSocket connections - will be established on demand partyKitAll: {}, - /** Course-specific PartyKit connection */ partyKitCourse: {}, - /** Currently monitored course ID */ listeningTo: "", - /** Reactive array of currently online students */ + // Use Svelte's rune for reactive state management studentsOnline: rune([]), - /** Map of student events keyed by user ID */ studentEventMap: new Map(), - /** - * Handles incoming student activity events - * Updates presence data for the current course - * @param event - WebSocket message event containing student activity - */ - studentListener(event: any) { + studentListener(event: MessageEvent) { + // Parse JSON data from WebSocket message const nextCourseEvent = JSON.parse(event.data); + // Only process events for current course and from other users if (nextCourseEvent.courseId === this.listeningTo && nextCourseEvent.user.id !== tutorsId.value?.login) { const studentEvent = this.studentEventMap.get(nextCourseEvent.user.id); if (!studentEvent) { + // First time seeing this student - add to online list const latestLo = new LoRecord(nextCourseEvent); this.studentsOnline.value.push(latestLo); this.studentEventMap.set(nextCourseEvent.user.id, latestLo); } else { + // Update existing student's activity refreshLoRecord(studentEvent, nextCourseEvent); } } }, - /** - * Establishes connection to global course activity room - */ connectToAllCourseAccess(): void { + // Create WebSocket connection to global activity room this.partyKitAll = new PartySocket({ host: partyKitServer, room: "tutors-all-course-access" }); }, - /** - * Starts monitoring presence for a specific course - * Clears previous state and establishes new WebSocket connection - * @param courseId - Course to monitor - */ startPresenceListener(courseId: string) { + // Reset state before starting new listener this.studentsOnline.value = []; this.studentEventMap.clear(); this.listeningTo = courseId; + + // Create course-specific WebSocket connection this.partyKitCourse = new PartySocket({ host: partyKitServer, room: courseId }); + // Bind event handler with correct 'this' context this.partyKitCourse.addEventListener("message", this.studentListener.bind(this)); }, - /** - * Broadcasts a learning object interaction event - * Sends to both global and course-specific channels - * @param course - Current course - * @param lo - Learning object being interacted with - * @param student - Student performing the interaction - */ sendLoEvent(course: Course, lo: Lo, student: TutorsId) { + // Construct event data const loRecord: LoRecord = { courseId: course.courseId, courseUrl: course.courseUrl, diff --git a/src/lib/services/community/types.svelte.ts b/src/lib/services/community/types.svelte.ts index 8a6bede5..704f5931 100644 --- a/src/lib/services/community/types.svelte.ts +++ b/src/lib/services/community/types.svelte.ts @@ -1,6 +1,6 @@ import type { TutorsId } from "$lib/services/connect"; import type { Course, IconType, Lo } from "$lib/services/base/lo-types"; - +import PartySocket from "partysocket"; /** * Minimal user information for learning object interactions */ @@ -33,43 +33,101 @@ export class LoRecord { /** * Service for managing real-time user presence and interactions + * Tracks student activity and broadcasts learning object interactions */ export interface PresenceService { - /** Currently online students */ - studentsOnline: any; - /** Global PartyKit connection */ - partyKitAll: any; - /** Course-specific PartyKit connection */ - partyKitCourse: any; - /** Map of student events */ + /** Currently online students in the active course */ + studentsOnline: { value: LoRecord[] }; + /** Connection for all course events across the platform */ + partyKitAll: PartySocket; + /** Connection for events specific to the current course */ + partyKitCourse: PartySocket; + /** Lookup table for quick access to student events */ studentEventMap: Map; - /** Currently monitored course ID */ + /** ID of the course currently being monitored */ listeningTo: string; - studentListener(event: any): void; + /** + * Process incoming student activity messages + * @param event - WebSocket message with student activity data + */ + studentListener(event: MessageEvent): void; + + /** + * Notify other users about learning object interaction + * @param course - Course being accessed + * @param lo - Learning object being viewed + * @param student - Student viewing the content + */ sendLoEvent(course: Course, lo: Lo, student: TutorsId): void; + + /** + * Begin monitoring platform-wide course activity + */ connectToAllCourseAccess(): void; + + /** + * Begin monitoring activity in a specific course + * @param courseId - Identifier of course to monitor + */ startPresenceListener(courseId: string): void; } /** * Service for managing real-time course interactions and student presence + * Provides course-wide and platform-wide activity monitoring */ export interface LiveService { + /** Currently monitored course identifier */ listeningForCourse: { value: string }; + /** List of courses with active users */ coursesOnline: { value: LoRecord[] }; + /** List of all active students in current course */ studentsOnline: { value: LoRecord[] }; + /** Active students grouped by their courses */ studentsOnlineByCourse: { value: LoRecord[][] }; + /** Quick lookup for student activity by ID */ studentEventMap: Map; + /** Quick lookup for course activity by ID */ courseEventMap: Map; - partyKitCourse: any; + /** Connection for course-specific events */ + partyKitCourse: PartySocket; + /** Flag indicating if global monitoring is active */ listeningAll: boolean; - groupedStudentListener(event: any): void; - studentListener(event: any): void; - courseListener(event: any): void; - partyKitListener(event: any): void; + /** + * Process student activity for course grouping + * @param event - WebSocket message containing student activity + */ + groupedStudentListener(event: MessageEvent): void; + + /** + * Process individual student activity + * @param event - WebSocket message containing student update + */ + studentListener(event: MessageEvent): void; + + /** + * Process course-level activity + * @param event - WebSocket message containing course update + */ + courseListener(event: MessageEvent): void; + + /** + * Handle incoming WebSocket messages + * @param event - WebSocket message to process + */ + partyKitListener(event: MessageEvent): void; + + /** + * Begin monitoring platform-wide activity + */ startGlobalPresenceService(): void; + + /** + * Begin monitoring activity in a specific course + * @param courseId - Course to monitor + */ startCoursePresenceListener(courseId: string): void; } From c2de68f4352588d9e218cf59ee336eb8f93b0cb1 Mon Sep 17 00:00:00 2001 From: emmaroche <20088680@mail.wit.ie> Date: Fri, 10 Jan 2025 08:11:04 +0000 Subject: [PATCH 2/2] Add layout text beside layout menu icon --- src/lib/ui/navigators/LayoutMenu.svelte | 5 ++++- src/lib/ui/navigators/MainNavigator.svelte | 17 ++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/lib/ui/navigators/LayoutMenu.svelte b/src/lib/ui/navigators/LayoutMenu.svelte index c00b270e..aaf5bd52 100644 --- a/src/lib/ui/navigators/LayoutMenu.svelte +++ b/src/lib/ui/navigators/LayoutMenu.svelte @@ -37,7 +37,10 @@ {#snippet menuSelector()} - +
+ + +
{/snippet} {#snippet menuContent()} diff --git a/src/lib/ui/navigators/MainNavigator.svelte b/src/lib/ui/navigators/MainNavigator.svelte index b53fa03a..5e18ca2e 100644 --- a/src/lib/ui/navigators/MainNavigator.svelte +++ b/src/lib/ui/navigators/MainNavigator.svelte @@ -35,14 +35,17 @@ {/snippet} {#snippet trail()} -