Skip to content

refactor: replace 5s polling with Firestore onSnapshot in mentor course#125

Merged
TakalaWang merged 1 commit into
mainfrom
refactor/onsnapshot-mentor-course
Apr 8, 2026
Merged

refactor: replace 5s polling with Firestore onSnapshot in mentor course#125
TakalaWang merged 1 commit into
mainfrom
refactor/onsnapshot-mentor-course

Conversation

@TakalaWang
Copy link
Copy Markdown
Collaborator

Summary

  • Mentor course page and topics tab were polling every 5s via startVisibilityPolling to pick up collaborator edits. Replaced with Firestore onSnapshot subscriptions so changes propagate in real time without the wasteful interval traffic.
  • Added four subscribe* helpers in mentora-api (course / topics / assignments / questionnaires) modeled on the existing subscribeToConversation pattern, exposed via coursesSubscribe.get and {topics,assignments,questionnaires}Subscribe.listForCourse on MentoraAPI.
  • Refactored MentorCourse.svelte and CourseTopics.svelte to use the subscriptions, derive UI state via \$derived / \$effect, and drop manual loadCourseData() / loadData() reloads after CRUD (subscription auto-fires). PostHog instrumentation added in #upstream is preserved.
  • Deleted unused apps/mentora/src/lib/features/polling/visibility.ts.

Test plan

  • Open mentor course page → confirm course title, announcements, and topics load
  • In another tab/browser, edit a course announcement → original tab reflects within ~1s
  • Add / rename / delete a topic → no manual refresh needed
  • Drag-reorder topics → order persists and other tabs update
  • Add / edit / delete an assignment or questionnaire → reflects everywhere live
  • Verify PostHog topic_created, topic_updated, topic_deleted, topics_reordered, assignment_*, assignments_reordered events still fire
  • DevTools → Network: confirm no more periodic 5s polling requests on mentor course pages

🤖 Generated with Claude Code

The mentor course page (MentorCourse.svelte) and topics tab
(CourseTopics.svelte) were polling every 5s via startVisibilityPolling
to pick up changes from collaborators. Replace with Firestore
onSnapshot subscriptions so changes propagate in real time without
the wasteful interval traffic.

- mentora-api: add subscribeToCourse, subscribeToCourseTopics,
  subscribeToCourseAssignments, subscribeToCourseQuestionnaires,
  exposed via coursesSubscribe.get and {topics,assignments,
  questionnaires}Subscribe.listForCourse on MentoraAPI.
- MentorCourse.svelte: derive courseTitle/announcements from
  reactive courseState; subscribe in $effect with authReady wait;
  drop manual loadCourseData() reloads after CRUD (subscription
  fires automatically).
- CourseTopics.svelte: three subscriptions (topics + assignments +
  questionnaires) combined via $effect into the local topics state;
  drop manual loadData() reloads; preserve PostHog instrumentation.
- Delete unused apps/mentora/src/lib/features/polling/visibility.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 8, 2026 16:23
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 8, 2026

⚠️ No Changeset found

Latest commit: 085f3e5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying mentora-app with  Cloudflare Pages  Cloudflare Pages

Latest commit: 085f3e5
Status: ✅  Deploy successful!
Preview URL: https://8b70363a.mentora-app.pages.dev
Branch Preview URL: https://refactor-onsnapshot-mentor-c.mentora-app.pages.dev

View logs

@TakalaWang TakalaWang merged commit 7104b34 into main Apr 8, 2026
6 checks passed
@TakalaWang TakalaWang deleted the refactor/onsnapshot-mentor-course branch April 8, 2026 16:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the mentor course experience to use Firestore real-time subscriptions (onSnapshot) instead of 5s visibility-based polling, enabling near-instant collaborator updates and reducing redundant network traffic.

Changes:

  • Added new Firestore subscription helpers in mentora-api for course, topics, assignments, and questionnaires, and exposed them on MentoraAPI.
  • Updated mentor course UI (course page + topics tab) to derive state from reactive subscription-backed ReactiveState containers and removed manual reload/polling flows.
  • Removed the now-unused visibility polling utility.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/mentora-api/src/lib/api/topics.ts Adds subscribeToCourseTopics using Firestore onSnapshot with course filter + order.
packages/mentora-api/src/lib/api/questionnaires.ts Adds subscribeToCourseQuestionnaires subscription helper.
packages/mentora-api/src/lib/api/courses.ts Adds subscribeToCourse for real-time updates of a single course doc.
packages/mentora-api/src/lib/api/assignments.ts Adds subscribeToCourseAssignments subscription helper.
packages/mentora-api/src/lib/api/api.svelte.ts Exposes new subscription helpers on MentoraAPI (coursesSubscribe.get, topics/assignments/questionnairesSubscribe.listForCourse).
apps/mentora/src/routes/courses/[id]/MentorCourse.svelte Replaces polling-based loading with a course subscription and derives UI fields from the subscribed state.
apps/mentora/src/lib/features/polling/visibility.ts Deletes unused visibility polling helper.
apps/mentora/src/lib/components/course/mentor/CourseTopics.svelte Replaces polling + manual reloads with live subscriptions and derives UI topics from subscribed topic/assignment/questionnaire data.

Comment on lines +78 to +98
$effect(() => {
const id = courseId;
if (!id) {
courseState.cleanup();
return;
}

let disposed = false;

(async () => {
if (!api.isAuthenticated) {
await api.authReady;
}
if (disposed || courseId !== id) return;
api.coursesSubscribe.get(id, courseState);
})();

return () => {
disposed = true;
courseState.cleanup();
};
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

courseState.cleanup() only unsubscribes; it does not clear the last loaded course value/error. When navigating between course IDs within the same route instance, the previous course title/announcements can remain visible until the first snapshot arrives. Consider resetting courseState (e.g., set(null) + setError(null)) before (re)subscribing, or enhancing cleanup() to optionally clear value/error/loading.

Copilot uses AI. Check for mistakes.
Comment on lines 120 to 135
if (id) {
const now = Date.now();
let newAnnouncements = [...currentAnnouncements];
// Edit
newAnnouncements = newAnnouncements.map((a) =>
// Edit existing announcement
const newAnnouncements = currentAnnouncements.map((a) =>
a.id === id
? { ...a, content: formattedContent, updatedAt: now }
: a,
);

const res = await api.courses.update(courseId, {
await api.courses.update(courseId, {
announcements: newAnnouncements,
});

if (res.success) {
loadCourseData();
}
} else {
// Create
const res = await api.courses.createAnnouncement(
courseId,
formattedContent,
);
if (res.success) {
loadCourseData();
}
// Create new announcement via backend
await api.courses.createAnnouncement(courseId, formattedContent);
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API methods here return an APIResult (success=false) rather than throwing; awaiting without checking the result means failed updates/creates will be silently ignored and the UI modal will still close. Capture the returned APIResult for both update and createAnnouncement, and surface an error (and/or keep local state unchanged) when success is false.

Copilot uses AI. Check for mistakes.
Comment on lines 138 to 148
async function handleDeleteAnnouncement(id: string | number) {
if (!courseId || !fullCourse) return;

const currentAnnouncements = fullCourse.announcements || [];
const newAnnouncements = currentAnnouncements.filter(
(a) => String(a.id) !== String(id),
);

const res = await api.courses.update(courseId, {
await api.courses.update(courseId, {
announcements: newAnnouncements,
});
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as save: api.courses.update returns APIResult and won't throw on failure, but the result is ignored. Please check res.success (and display an error / avoid implying deletion succeeded) so failed deletes don't silently desync the UI.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to 103
$effect(() => {
const id = courseId;
if (!id) {
topicsState.cleanup();
assignmentsState.cleanup();
questionnairesState.cleanup();
return;
}

let disposed = false;

(async () => {
if (!api.isAuthenticated) {
await api.authReady;
}
if (disposed || courseId !== id) return;
api.topicsSubscribe.listForCourse(id, topicsState);
api.assignmentsSubscribe.listForCourse(id, assignmentsState);
api.questionnairesSubscribe.listForCourse(id, questionnairesState);
})();

return () => {
stopPolling?.();
stopPolling = null;
disposed = true;
topicsState.cleanup();
assignmentsState.cleanup();
questionnairesState.cleanup();
};
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topicsState/assignmentsState/questionnairesState.cleanup() only unsubscribes and leaves the previous .value intact. If courseId changes (or becomes falsy), the UI can continue rendering the previous course’s topics until the new snapshots arrive. Consider clearing the state values (and resetting topics to []) when courseId changes / on cleanup.

Copilot uses AI. Check for mistakes.
Comment on lines 297 to 300
} catch (e) {
console.error(e);
errorMessage = m.mentor_assignment_save_failed();
await loadData();
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calls inside this try/catch return APIResult objects and typically won’t throw on request failure (success=false). As written, failures will bypass the catch, but the UI has already been optimistically updated and analytics will still be captured. Capture and check the APIResult(s); on failure, show an error and revert the optimistic title change (or re-sync from the subscribed state).

Copilot uses AI. Check for mistakes.
Comment on lines 472 to 477
},
);
showAssignmentModal = false;
await loadData();
} catch (e) {
console.error(e);
errorMessage = m.mentor_assignment_save_failed();
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this flow, several awaited api.* calls (topics.update / questionnaires.update / assignments.update) return APIResult rather than throwing. That means errors won’t be caught here, but PostHog events will still fire and the modal will still close. Please check each APIResult.success and only capture analytics / close the modal on success; otherwise surface an error and keep the editor open so the user can retry.

Copilot uses AI. Check for mistakes.
Comment on lines 521 to 526
assignment_id: assignmentId,
assignment_type: assignment.type,
});
await loadData();
} catch (e) {
console.error(e);
errorMessage = m.mentor_assignment_delete_failed();
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same APIResult vs throw issue here: api.topics.update / api.questionnaires.delete / api.assignments.delete return {success:false} on failure, so the catch may never run and the delete analytics event can be emitted even when deletion fails. Capture and validate the APIResult(s) before capturing PostHog and before leaving the UI in a deleted state.

Copilot uses AI. Check for mistakes.
Comment on lines 308 to 313
topic_id: topicId,
assignment_count: topic?.assignments.length ?? 0,
});
await loadData();
}

async function askDeleteTopic(topicId: string) {
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api.topics.delete returns an APIResult (success=false) on failure, so this code can still capture a "topic_deleted" PostHog event even if the delete did not succeed. Please check the delete result and only emit analytics (and update UI) when success is true; otherwise surface an error.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants