Skip to content

Commit

Permalink
Merge pull request #925 from tutors-sdk/development
Browse files Browse the repository at this point in the history
Layout menu + documentation updates
  • Loading branch information
edeleastar authored Jan 10, 2025
2 parents b93fd5e + 3f9b4f9 commit 85cf23e
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 92 deletions.
100 changes: 60 additions & 40 deletions src/lib/services/community/services/live.svelte.ts
Original file line number Diff line number Diff line change
@@ -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 = <PartySocket>{};

if (PUBLIC_party_kit_main_room !== "XXX") {
partyKitAll = new PartySocket({
host: partyKitServer,
Expand All @@ -17,87 +22,102 @@ if (PUBLIC_party_kit_main_room !== "XXX") {
}

export const liveService: LiveService = {
// Initialize reactive state using Svelte runes
listeningForCourse: rune<string>(""),
coursesOnline: rune<LoRecord[]>([]),
studentsOnline: rune<LoRecord[]>([]),
studentsOnlineByCourse: rune<LoRecord[][]>([]),

// Maps for efficient event lookup and updates
studentEventMap: new Map<string, LoRecord>(),
courseEventMap: new Map<string, LoRecord>(),

// Course-specific WebSocket connection - initialized on demand
partyKitCourse: <PartySocket>{},
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<LoRecord>();
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));
}
};
45 changes: 16 additions & 29 deletions src/lib/services/community/services/presence.svelte.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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: <PartySocket>{},
/** Course-specific PartyKit connection */
partyKitCourse: <PartySocket>{},
/** Currently monitored course ID */
listeningTo: "",
/** Reactive array of currently online students */
// Use Svelte's rune for reactive state management
studentsOnline: rune<LoRecord[]>([]),
/** Map of student events keyed by user ID */
studentEventMap: new Map<string, LoRecord>(),

/**
* 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,
Expand Down
88 changes: 73 additions & 15 deletions src/lib/services/community/types.svelte.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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<string, LoRecord>;
/** 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<string, LoRecord>;
/** Quick lookup for course activity by ID */
courseEventMap: Map<string, LoRecord>;
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;
}

Expand Down
5 changes: 4 additions & 1 deletion src/lib/ui/navigators/LayoutMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
</script>

{#snippet menuSelector()}
<Icon type="lightMode" tip="Open Theme Menu" />
<div class="flex items-center">
<Icon type="lightMode" tip="Open Theme Menu" />
<span class="ml-2 hidden text-sm font-bold md:block">Layout</span>
</div>
{/snippet}

{#snippet menuContent()}
Expand Down
Loading

0 comments on commit 85cf23e

Please sign in to comment.