From 19280e74f93f6a394a8d646184040279db3bde20 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Fri, 15 Nov 2024 10:59:26 -0700 Subject: [PATCH] fixing updating between server and client --- .../lecture/[lectureDay]/EditLecture.tsx | 39 +++---- .../[assignmentName]/EditAssignment.tsx | 37 +++---- .../[moduleName]/page/[pageName]/EditPage.tsx | 103 +++++++++--------- .../[moduleName]/quiz/[quizName]/EditQuiz.tsx | 57 +++------- .../utils/useAuthoritativeUpdates.tsx | 37 +++++++ 5 files changed, 133 insertions(+), 140 deletions(-) create mode 100644 nextjs/src/app/course/[courseName]/utils/useAuthoritativeUpdates.tsx diff --git a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx index c2666af..6b2e09d 100644 --- a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx +++ b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx @@ -15,6 +15,7 @@ import LectureButtons from "./LectureButtons"; import { useCourseContext } from "../../context/courseContext"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { Lecture } from "@/models/local/lecture"; +import { useAuthoritativeUpdates } from "../../utils/useAuthoritativeUpdates"; export default function EditLecture({ lectureDay }: { lectureDay: string }) { const { courseName } = useCourseContext(); @@ -27,24 +28,15 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) { .flatMap(({ lectures }) => lectures.map((lecture) => lecture)) .find((l) => l.date === lectureDay); - const startingText = getLectureTextOrDefault(lecture, lectureDay); - - const [text, setText] = useState(startingText); - const [updateMonacoKey, setUpdateMonacoKey] = useState(1); + const { clientIsAuthoritative, text, textUpdate, monacoKey } = + useAuthoritativeUpdates({ + serverUpdatedAt: serverDataUpdatedAt, + startingText: getLectureTextOrDefault(lecture, lectureDay), + }); const [error, setError] = useState(""); - const [clientDataUpdatedAt, setClientDataUpdatedAt] = - useState(serverDataUpdatedAt); - - const textUpdate = useCallback((t: string) => { - setText(t); - setClientDataUpdatedAt(Date.now()); - }, []); - useEffect(() => { const delay = 500; - const clientIsAuthoritative = serverDataUpdatedAt <= clientDataUpdatedAt; - console.log("client is authoritative", clientIsAuthoritative); const handler = setTimeout(() => { try { @@ -58,8 +50,7 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) { console.log( "client not authoritative, updating client with server data" ); - textUpdate(lectureToString(lecture)); - setUpdateMonacoKey((k) => k + 1); + textUpdate(lectureToString(lecture), true); } else { console.log( "client not authoritative, but no lecture on server, this is a bug" @@ -76,18 +67,22 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) { return () => { clearTimeout(handler); }; - }, [courseName, lecture, settings, text, textUpdate, updateLecture]); + }, [ + clientIsAuthoritative, + courseName, + lecture, + settings, + text, + textUpdate, + updateLecture, + ]); return (
- +
{error && error}
diff --git a/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx b/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx index b0bc08b..89db0d7 100644 --- a/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx +++ b/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx @@ -8,7 +8,7 @@ import { LocalAssignment, localAssignmentMarkdown, } from "@/models/local/assignment/localAssignment"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import AssignmentPreview from "./AssignmentPreview"; import { getModuleItemUrl } from "@/services/urlUtils"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; @@ -19,6 +19,7 @@ import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSu import { LocalCourseSettings } from "@/models/local/localCourseSettings"; import { useRouter } from "next/navigation"; import { AssignmentButtons } from "./AssignmentButtons"; +import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates"; export default function EditAssignment({ moduleName, @@ -34,31 +35,22 @@ export default function EditAssignment({ useAssignmentQuery(moduleName, assignmentName); const updateAssignment = useUpdateAssignmentMutation(); - const [assignmentText, setAssignmentText] = useState( - localAssignmentMarkdown.toMarkdown(assignment) - ); - - const [updateMonacoKey, setUpdateMonacoKey] = useState(1); - const [clientDataUpdatedAt, setClientDataUpdatedAt] = - useState(serverDataUpdatedAt); - - const textUpdate = useCallback((t: string) => { - setAssignmentText(t); - setClientDataUpdatedAt(Date.now()); - }, []); + const { clientIsAuthoritative, text, textUpdate, monacoKey } = + useAuthoritativeUpdates({ + serverUpdatedAt: serverDataUpdatedAt, + startingText: localAssignmentMarkdown.toMarkdown(assignment), + }); const [error, setError] = useState(""); const [showHelp, setShowHelp] = useState(false); useEffect(() => { const delay = 500; - const clientIsAuthoritative = serverDataUpdatedAt <= clientDataUpdatedAt; - console.log("client is authoritative", clientIsAuthoritative); const handler = setTimeout(() => { try { const updatedAssignment: LocalAssignment = - localAssignmentMarkdown.parseMarkdown(assignmentText); + localAssignmentMarkdown.parseMarkdown(text); if ( localAssignmentMarkdown.toMarkdown(assignment) !== localAssignmentMarkdown.toMarkdown(updatedAssignment) @@ -89,8 +81,7 @@ export default function EditAssignment({ console.log( "client not authoritative, updating client with server data" ); - textUpdate(localAssignmentMarkdown.toMarkdown(assignment)); - setUpdateMonacoKey((k) => k + 1); + textUpdate(localAssignmentMarkdown.toMarkdown(assignment), true); } } setError(""); @@ -105,10 +96,12 @@ export default function EditAssignment({ }, [ assignment, assignmentName, - assignmentText, + clientIsAuthoritative, courseName, moduleName, router, + text, + textUpdate, updateAssignment, ]); @@ -121,11 +114,7 @@ export default function EditAssignment({ )}
- +
{error && error}
diff --git a/nextjs/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx b/nextjs/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx index 46fa15b..0a846bb 100644 --- a/nextjs/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx +++ b/nextjs/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx @@ -14,6 +14,7 @@ import ClientOnly from "@/components/ClientOnly"; import { getModuleItemUrl } from "@/services/urlUtils"; import { useRouter } from "next/navigation"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; +import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates"; export default function EditPage({ moduleName, @@ -21,69 +22,61 @@ export default function EditPage({ }: { pageName: string; moduleName: string; -}) { - const [_, { dataUpdatedAt }] = usePageQuery(moduleName, pageName); - - return ( - - ); -} - -function InnerEditPage({ - moduleName, - pageName, -}: { - pageName: string; - moduleName: string; }) { const router = useRouter(); const { courseName } = useCourseContext(); - const [page, pageQuery] = usePageQuery(moduleName, pageName); - const updatePage = useUpdatePageMutation(); - const [pageText, setPageText] = useState( - localPageMarkdownUtils.toMarkdown(page) + const [page, { dataUpdatedAt }] = usePageQuery( + moduleName, + pageName ); + const updatePage = useUpdatePageMutation(); + + const { clientIsAuthoritative, text, textUpdate, monacoKey } = + useAuthoritativeUpdates({ + serverUpdatedAt: dataUpdatedAt, + startingText: localPageMarkdownUtils.toMarkdown(page), + }); + const [error, setError] = useState(""); const [settings] = useLocalCourseSettingsQuery(); - useEffect(() => { - console.log("page data updated on sever", pageQuery.dataUpdatedAt); - }, [pageQuery.dataUpdatedAt]); - useEffect(() => { const delay = 500; const handler = setTimeout(() => { try { - const updatedPage = localPageMarkdownUtils.parseMarkdown(pageText); + const updatedPage = localPageMarkdownUtils.parseMarkdown(text); if ( localPageMarkdownUtils.toMarkdown(page) !== localPageMarkdownUtils.toMarkdown(updatedPage) ) { - console.log("updating page"); - updatePage - .mutateAsync({ - page: updatedPage, - moduleName, - pageName: updatedPage.name, - previousModuleName: moduleName, - previousPageName: pageName, - courseName, - }) - .then(() => { - if (updatedPage.name !== pageName) - router.replace( - getModuleItemUrl( - courseName, - moduleName, - "page", - updatedPage.name - ) - ); - }); + if (clientIsAuthoritative) { + console.log("updating page"); + updatePage + .mutateAsync({ + page: updatedPage, + moduleName, + pageName: updatedPage.name, + previousModuleName: moduleName, + previousPageName: pageName, + courseName, + }) + .then(() => { + if (updatedPage.name !== pageName) + router.replace( + getModuleItemUrl( + courseName, + moduleName, + "page", + updatedPage.name + ) + ); + }); + } else { + console.log( + "client not authoritative, updating client with server data" + ); + textUpdate(localPageMarkdownUtils.toMarkdown(page), true); + } } setError(""); } catch (e: any) { @@ -94,13 +87,23 @@ function InnerEditPage({ return () => { clearTimeout(handler); }; - }, [courseName, moduleName, page, pageName, pageText, router, updatePage]); + }, [ + clientIsAuthoritative, + courseName, + moduleName, + page, + pageName, + router, + text, + textUpdate, + updatePage, + ]); return (
- +
{error && error}
diff --git a/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx b/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx index ec2cde3..0d0f48b 100644 --- a/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx +++ b/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx @@ -1,7 +1,7 @@ "use client"; import { MonacoEditor } from "@/components/editor/MonacoEditor"; import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import QuizPreview from "./QuizPreview"; import { QuizButtons } from "./QuizButton"; import ClientOnly from "@/components/ClientOnly"; @@ -13,6 +13,7 @@ import { useQuizQuery, useUpdateQuizMutation, } from "@/hooks/localCourse/quizHooks"; +import { useAuthoritativeUpdates } from "../../../../utils/useAuthoritativeUpdates"; const helpString = `QUESTION REFERENCE --- @@ -61,22 +62,6 @@ export default function EditQuiz({ }: { quizName: string; moduleName: string; -}) { - const [_, { dataUpdatedAt }] = useQuizQuery(moduleName, quizName); - return ( - - ); -} -export function InnerEditQuiz({ - moduleName, - quizName, -}: { - quizName: string; - moduleName: string; }) { const router = useRouter(); const { courseName } = useCourseContext(); @@ -85,34 +70,25 @@ export function InnerEditQuiz({ quizName ); const updateQuizMutation = useUpdateQuizMutation(); - const [quizText, setQuizText] = useState(quizMarkdownUtils.toMarkdown(quiz)); + const { clientIsAuthoritative, text, textUpdate, monacoKey } = + useAuthoritativeUpdates({ + serverUpdatedAt: serverDataUpdatedAt, + startingText: quizMarkdownUtils.toMarkdown(quiz), + }); - const [updateMonacoKey, setUpdateMonacoKey] = useState(1); - const [clientDataUpdatedAt, setClientDataUpdatedAt] = - useState(serverDataUpdatedAt); const [error, setError] = useState(""); const [showHelp, setShowHelp] = useState(false); - const textUpdate = useCallback((t: string) => { - setQuizText(t); - setClientDataUpdatedAt(Date.now()); - }, []); - useEffect(() => { const delay = 1000; - const clientIsAuthoritative = serverDataUpdatedAt <= clientDataUpdatedAt; - console.log("client is authoritative", clientIsAuthoritative); - const handler = setTimeout(async () => { try { if ( quizMarkdownUtils.toMarkdown(quiz) !== - quizMarkdownUtils.toMarkdown( - quizMarkdownUtils.parseMarkdown(quizText) - ) + quizMarkdownUtils.toMarkdown(quizMarkdownUtils.parseMarkdown(text)) ) { if (clientIsAuthoritative) { - const updatedQuiz = quizMarkdownUtils.parseMarkdown(quizText); + const updatedQuiz = quizMarkdownUtils.parseMarkdown(text); await updateQuizMutation .mutateAsync({ quiz: updatedQuiz, @@ -137,8 +113,7 @@ export function InnerEditQuiz({ console.log( "client not authoritative, updating client with server data" ); - textUpdate(quizMarkdownUtils.toMarkdown(quiz)); - setUpdateMonacoKey((k) => k + 1); + textUpdate(quizMarkdownUtils.toMarkdown(quiz), true); } } setError(""); @@ -151,19 +126,17 @@ export function InnerEditQuiz({ clearTimeout(handler); }; }, [ - clientDataUpdatedAt, + clientIsAuthoritative, courseName, moduleName, quiz, quizName, - quizText, router, - serverDataUpdatedAt, + text, textUpdate, updateQuizMutation, ]); - console.log("updateMonacoKey", updateMonacoKey); return (
@@ -173,11 +146,7 @@ export function InnerEditQuiz({ )}
- +
{error && error}
diff --git a/nextjs/src/app/course/[courseName]/utils/useAuthoritativeUpdates.tsx b/nextjs/src/app/course/[courseName]/utils/useAuthoritativeUpdates.tsx new file mode 100644 index 0000000..139b549 --- /dev/null +++ b/nextjs/src/app/course/[courseName]/utils/useAuthoritativeUpdates.tsx @@ -0,0 +1,37 @@ +"use client"; +import { useState, useMemo, useCallback } from "react"; + +export function useAuthoritativeUpdates({ + serverUpdatedAt, + startingText, +}: { + serverUpdatedAt: number; + startingText: string; +}) { + const [text, setText] = useState(startingText); + const [clientDataUpdatedAt, setClientDataUpdatedAt] = + useState(serverUpdatedAt); + const [updateMonacoKey, setUpdateMonacoKey] = useState(1); + + const clientIsAuthoritative = useMemo( + () => serverUpdatedAt <= clientDataUpdatedAt, + [clientDataUpdatedAt, serverUpdatedAt] + ); + + console.log("client is authoritative", clientIsAuthoritative); + const textUpdate = useCallback((t: string, updateMonaco: boolean = false) => { + setText(t); + setClientDataUpdatedAt(Date.now()); + if (updateMonaco) setUpdateMonacoKey((t) => t + 1); + }, []); + + return useMemo( + () => ({ + clientIsAuthoritative, + textUpdate, + text, + monacoKey: updateMonacoKey, + }), + [clientIsAuthoritative, text, textUpdate, updateMonacoKey] + ); +}