fixing updating between server and client

This commit is contained in:
2024-11-15 10:59:26 -07:00
parent 9f3360261e
commit 19280e74f9
5 changed files with 133 additions and 140 deletions

View File

@@ -15,6 +15,7 @@ import LectureButtons from "./LectureButtons";
import { useCourseContext } from "../../context/courseContext"; import { useCourseContext } from "../../context/courseContext";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { Lecture } from "@/models/local/lecture"; import { Lecture } from "@/models/local/lecture";
import { useAuthoritativeUpdates } from "../../utils/useAuthoritativeUpdates";
export default function EditLecture({ lectureDay }: { lectureDay: string }) { export default function EditLecture({ lectureDay }: { lectureDay: string }) {
const { courseName } = useCourseContext(); const { courseName } = useCourseContext();
@@ -27,24 +28,15 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) {
.flatMap(({ lectures }) => lectures.map((lecture) => lecture)) .flatMap(({ lectures }) => lectures.map((lecture) => lecture))
.find((l) => l.date === lectureDay); .find((l) => l.date === lectureDay);
const startingText = getLectureTextOrDefault(lecture, lectureDay); const { clientIsAuthoritative, text, textUpdate, monacoKey } =
useAuthoritativeUpdates({
const [text, setText] = useState(startingText); serverUpdatedAt: serverDataUpdatedAt,
const [updateMonacoKey, setUpdateMonacoKey] = useState(1); startingText: getLectureTextOrDefault(lecture, lectureDay),
});
const [error, setError] = useState(""); const [error, setError] = useState("");
const [clientDataUpdatedAt, setClientDataUpdatedAt] =
useState(serverDataUpdatedAt);
const textUpdate = useCallback((t: string) => {
setText(t);
setClientDataUpdatedAt(Date.now());
}, []);
useEffect(() => { useEffect(() => {
const delay = 500; const delay = 500;
const clientIsAuthoritative = serverDataUpdatedAt <= clientDataUpdatedAt;
console.log("client is authoritative", clientIsAuthoritative);
const handler = setTimeout(() => { const handler = setTimeout(() => {
try { try {
@@ -58,8 +50,7 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) {
console.log( console.log(
"client not authoritative, updating client with server data" "client not authoritative, updating client with server data"
); );
textUpdate(lectureToString(lecture)); textUpdate(lectureToString(lecture), true);
setUpdateMonacoKey((k) => k + 1);
} else { } else {
console.log( console.log(
"client not authoritative, but no lecture on server, this is a bug" "client not authoritative, but no lecture on server, this is a bug"
@@ -76,18 +67,22 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) {
return () => { return () => {
clearTimeout(handler); clearTimeout(handler);
}; };
}, [courseName, lecture, settings, text, textUpdate, updateLecture]); }, [
clientIsAuthoritative,
courseName,
lecture,
settings,
text,
textUpdate,
updateLecture,
]);
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<EditLectureTitle lectureDay={lectureDay} /> <EditLectureTitle lectureDay={lectureDay} />
<div className="sm:columns-2 min-h-0 flex-1"> <div className="sm:columns-2 min-h-0 flex-1">
<div className="flex-1 h-full"> <div className="flex-1 h-full">
<MonacoEditor <MonacoEditor key={monacoKey} value={text} onChange={textUpdate} />
key={updateMonacoKey}
value={text}
onChange={textUpdate}
/>
</div> </div>
<div className="h-full sm:block none overflow-auto"> <div className="h-full sm:block none overflow-auto">
<div className="text-red-300">{error && error}</div> <div className="text-red-300">{error && error}</div>

View File

@@ -8,7 +8,7 @@ import {
LocalAssignment, LocalAssignment,
localAssignmentMarkdown, localAssignmentMarkdown,
} from "@/models/local/assignment/localAssignment"; } from "@/models/local/assignment/localAssignment";
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AssignmentPreview from "./AssignmentPreview"; import AssignmentPreview from "./AssignmentPreview";
import { getModuleItemUrl } from "@/services/urlUtils"; import { getModuleItemUrl } from "@/services/urlUtils";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; 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 { LocalCourseSettings } from "@/models/local/localCourseSettings";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { AssignmentButtons } from "./AssignmentButtons"; import { AssignmentButtons } from "./AssignmentButtons";
import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates";
export default function EditAssignment({ export default function EditAssignment({
moduleName, moduleName,
@@ -34,31 +35,22 @@ export default function EditAssignment({
useAssignmentQuery(moduleName, assignmentName); useAssignmentQuery(moduleName, assignmentName);
const updateAssignment = useUpdateAssignmentMutation(); const updateAssignment = useUpdateAssignmentMutation();
const [assignmentText, setAssignmentText] = useState( const { clientIsAuthoritative, text, textUpdate, monacoKey } =
localAssignmentMarkdown.toMarkdown(assignment) useAuthoritativeUpdates({
); serverUpdatedAt: serverDataUpdatedAt,
startingText: localAssignmentMarkdown.toMarkdown(assignment),
const [updateMonacoKey, setUpdateMonacoKey] = useState(1); });
const [clientDataUpdatedAt, setClientDataUpdatedAt] =
useState(serverDataUpdatedAt);
const textUpdate = useCallback((t: string) => {
setAssignmentText(t);
setClientDataUpdatedAt(Date.now());
}, []);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [showHelp, setShowHelp] = useState(false); const [showHelp, setShowHelp] = useState(false);
useEffect(() => { useEffect(() => {
const delay = 500; const delay = 500;
const clientIsAuthoritative = serverDataUpdatedAt <= clientDataUpdatedAt;
console.log("client is authoritative", clientIsAuthoritative);
const handler = setTimeout(() => { const handler = setTimeout(() => {
try { try {
const updatedAssignment: LocalAssignment = const updatedAssignment: LocalAssignment =
localAssignmentMarkdown.parseMarkdown(assignmentText); localAssignmentMarkdown.parseMarkdown(text);
if ( if (
localAssignmentMarkdown.toMarkdown(assignment) !== localAssignmentMarkdown.toMarkdown(assignment) !==
localAssignmentMarkdown.toMarkdown(updatedAssignment) localAssignmentMarkdown.toMarkdown(updatedAssignment)
@@ -89,8 +81,7 @@ export default function EditAssignment({
console.log( console.log(
"client not authoritative, updating client with server data" "client not authoritative, updating client with server data"
); );
textUpdate(localAssignmentMarkdown.toMarkdown(assignment)); textUpdate(localAssignmentMarkdown.toMarkdown(assignment), true);
setUpdateMonacoKey((k) => k + 1);
} }
} }
setError(""); setError("");
@@ -105,10 +96,12 @@ export default function EditAssignment({
}, [ }, [
assignment, assignment,
assignmentName, assignmentName,
assignmentText, clientIsAuthoritative,
courseName, courseName,
moduleName, moduleName,
router, router,
text,
textUpdate,
updateAssignment, updateAssignment,
]); ]);
@@ -121,11 +114,7 @@ export default function EditAssignment({
</pre> </pre>
)} )}
<div className="flex-1 h-full"> <div className="flex-1 h-full">
<MonacoEditor <MonacoEditor key={monacoKey} value={text} onChange={textUpdate} />
key={updateMonacoKey}
value={assignmentText}
onChange={textUpdate}
/>
</div> </div>
<div className="flex-1 h-full"> <div className="flex-1 h-full">
<div className="text-red-300">{error && error}</div> <div className="text-red-300">{error && error}</div>

View File

@@ -14,6 +14,7 @@ import ClientOnly from "@/components/ClientOnly";
import { getModuleItemUrl } from "@/services/urlUtils"; import { getModuleItemUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates";
export default function EditPage({ export default function EditPage({
moduleName, moduleName,
@@ -21,48 +22,34 @@ export default function EditPage({
}: { }: {
pageName: string; pageName: string;
moduleName: string; moduleName: string;
}) {
const [_, { dataUpdatedAt }] = usePageQuery(moduleName, pageName);
return (
<InnerEditPage
key={dataUpdatedAt}
pageName={pageName}
moduleName={moduleName}
></InnerEditPage>
);
}
function InnerEditPage({
moduleName,
pageName,
}: {
pageName: string;
moduleName: string;
}) { }) {
const router = useRouter(); const router = useRouter();
const { courseName } = useCourseContext(); const { courseName } = useCourseContext();
const [page, pageQuery] = usePageQuery(moduleName, pageName); const [page, { dataUpdatedAt }] = usePageQuery(
const updatePage = useUpdatePageMutation(); moduleName,
const [pageText, setPageText] = useState( pageName
localPageMarkdownUtils.toMarkdown(page)
); );
const updatePage = useUpdatePageMutation();
const { clientIsAuthoritative, text, textUpdate, monacoKey } =
useAuthoritativeUpdates({
serverUpdatedAt: dataUpdatedAt,
startingText: localPageMarkdownUtils.toMarkdown(page),
});
const [error, setError] = useState(""); const [error, setError] = useState("");
const [settings] = useLocalCourseSettingsQuery(); const [settings] = useLocalCourseSettingsQuery();
useEffect(() => {
console.log("page data updated on sever", pageQuery.dataUpdatedAt);
}, [pageQuery.dataUpdatedAt]);
useEffect(() => { useEffect(() => {
const delay = 500; const delay = 500;
const handler = setTimeout(() => { const handler = setTimeout(() => {
try { try {
const updatedPage = localPageMarkdownUtils.parseMarkdown(pageText); const updatedPage = localPageMarkdownUtils.parseMarkdown(text);
if ( if (
localPageMarkdownUtils.toMarkdown(page) !== localPageMarkdownUtils.toMarkdown(page) !==
localPageMarkdownUtils.toMarkdown(updatedPage) localPageMarkdownUtils.toMarkdown(updatedPage)
) { ) {
if (clientIsAuthoritative) {
console.log("updating page"); console.log("updating page");
updatePage updatePage
.mutateAsync({ .mutateAsync({
@@ -84,6 +71,12 @@ function InnerEditPage({
) )
); );
}); });
} else {
console.log(
"client not authoritative, updating client with server data"
);
textUpdate(localPageMarkdownUtils.toMarkdown(page), true);
}
} }
setError(""); setError("");
} catch (e: any) { } catch (e: any) {
@@ -94,13 +87,23 @@ function InnerEditPage({
return () => { return () => {
clearTimeout(handler); clearTimeout(handler);
}; };
}, [courseName, moduleName, page, pageName, pageText, router, updatePage]); }, [
clientIsAuthoritative,
courseName,
moduleName,
page,
pageName,
router,
text,
textUpdate,
updatePage,
]);
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="columns-2 min-h-0 flex-1"> <div className="columns-2 min-h-0 flex-1">
<div className="flex-1 h-full"> <div className="flex-1 h-full">
<MonacoEditor value={pageText} onChange={setPageText} /> <MonacoEditor key={monacoKey} value={text} onChange={textUpdate} />
</div> </div>
<div className="h-full"> <div className="h-full">
<div className="text-red-300">{error && error}</div> <div className="text-red-300">{error && error}</div>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { MonacoEditor } from "@/components/editor/MonacoEditor"; import { MonacoEditor } from "@/components/editor/MonacoEditor";
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import QuizPreview from "./QuizPreview"; import QuizPreview from "./QuizPreview";
import { QuizButtons } from "./QuizButton"; import { QuizButtons } from "./QuizButton";
import ClientOnly from "@/components/ClientOnly"; import ClientOnly from "@/components/ClientOnly";
@@ -13,6 +13,7 @@ import {
useQuizQuery, useQuizQuery,
useUpdateQuizMutation, useUpdateQuizMutation,
} from "@/hooks/localCourse/quizHooks"; } from "@/hooks/localCourse/quizHooks";
import { useAuthoritativeUpdates } from "../../../../utils/useAuthoritativeUpdates";
const helpString = `QUESTION REFERENCE const helpString = `QUESTION REFERENCE
--- ---
@@ -61,22 +62,6 @@ export default function EditQuiz({
}: { }: {
quizName: string; quizName: string;
moduleName: string; moduleName: string;
}) {
const [_, { dataUpdatedAt }] = useQuizQuery(moduleName, quizName);
return (
<InnerEditQuiz
key={dataUpdatedAt}
quizName={quizName}
moduleName={moduleName}
/>
);
}
export function InnerEditQuiz({
moduleName,
quizName,
}: {
quizName: string;
moduleName: string;
}) { }) {
const router = useRouter(); const router = useRouter();
const { courseName } = useCourseContext(); const { courseName } = useCourseContext();
@@ -85,34 +70,25 @@ export function InnerEditQuiz({
quizName quizName
); );
const updateQuizMutation = useUpdateQuizMutation(); 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 [error, setError] = useState("");
const [showHelp, setShowHelp] = useState(false); const [showHelp, setShowHelp] = useState(false);
const textUpdate = useCallback((t: string) => {
setQuizText(t);
setClientDataUpdatedAt(Date.now());
}, []);
useEffect(() => { useEffect(() => {
const delay = 1000; const delay = 1000;
const clientIsAuthoritative = serverDataUpdatedAt <= clientDataUpdatedAt;
console.log("client is authoritative", clientIsAuthoritative);
const handler = setTimeout(async () => { const handler = setTimeout(async () => {
try { try {
if ( if (
quizMarkdownUtils.toMarkdown(quiz) !== quizMarkdownUtils.toMarkdown(quiz) !==
quizMarkdownUtils.toMarkdown( quizMarkdownUtils.toMarkdown(quizMarkdownUtils.parseMarkdown(text))
quizMarkdownUtils.parseMarkdown(quizText)
)
) { ) {
if (clientIsAuthoritative) { if (clientIsAuthoritative) {
const updatedQuiz = quizMarkdownUtils.parseMarkdown(quizText); const updatedQuiz = quizMarkdownUtils.parseMarkdown(text);
await updateQuizMutation await updateQuizMutation
.mutateAsync({ .mutateAsync({
quiz: updatedQuiz, quiz: updatedQuiz,
@@ -137,8 +113,7 @@ export function InnerEditQuiz({
console.log( console.log(
"client not authoritative, updating client with server data" "client not authoritative, updating client with server data"
); );
textUpdate(quizMarkdownUtils.toMarkdown(quiz)); textUpdate(quizMarkdownUtils.toMarkdown(quiz), true);
setUpdateMonacoKey((k) => k + 1);
} }
} }
setError(""); setError("");
@@ -151,19 +126,17 @@ export function InnerEditQuiz({
clearTimeout(handler); clearTimeout(handler);
}; };
}, [ }, [
clientDataUpdatedAt, clientIsAuthoritative,
courseName, courseName,
moduleName, moduleName,
quiz, quiz,
quizName, quizName,
quizText,
router, router,
serverDataUpdatedAt, text,
textUpdate, textUpdate,
updateQuizMutation, updateQuizMutation,
]); ]);
console.log("updateMonacoKey", updateMonacoKey);
return ( return (
<div className="h-full flex flex-col align-middle px-1"> <div className="h-full flex flex-col align-middle px-1">
<div className={"min-h-96 h-full flex flex-row w-full"}> <div className={"min-h-96 h-full flex flex-row w-full"}>
@@ -173,11 +146,7 @@ export function InnerEditQuiz({
</pre> </pre>
)} )}
<div className="flex-1 h-full"> <div className="flex-1 h-full">
<MonacoEditor <MonacoEditor key={monacoKey} value={text} onChange={textUpdate} />
key={updateMonacoKey}
value={quizText}
onChange={textUpdate}
/>
</div> </div>
<div className="flex-1 h-full"> <div className="flex-1 h-full">
<div className="text-red-300">{error && error}</div> <div className="text-red-300">{error && error}</div>

View File

@@ -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]
);
}