request queue dedupes requests by id

This commit is contained in:
nflnkr 2024-02-26 12:51:33 +03:00
parent 3f82b7d97a
commit 4f68ddfad5
6 changed files with 60 additions and 68 deletions

@ -35,7 +35,6 @@ export default function Main({ sidebar, header, footer, Page }: Props) {
const theme = useTheme(); const theme = useTheme();
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const quizConfig = quiz?.config; const quizConfig = quiz?.config;
const { questions } = useQuestionsStore();
const { editQuizId } = useQuizStore(); const { editQuizId } = useQuizStore();
const currentStep = useQuizStore((state) => state.currentStep); const currentStep = useQuizStore((state) => state.currentStep);
const { isTestServer } = useDomainDefine(); const { isTestServer } = useDomainDefine();

@ -380,15 +380,15 @@ export const findQuestionById = (quizId: number) => {
let found = null; let found = null;
questionStore questionStore
.getState() .getState()
["listQuestions"][quizId].some( [
(quiz: AnyTypedQuizQuestion, index: number) => { "listQuestions"
if (quiz.backendId === quizId) { ][quizId].some((quiz: AnyTypedQuizQuestion, index: number) => {
found = { quiz, index }; if (quiz.backendId === quizId) {
return true; found = { quiz, index };
} return true;
return false; }
}, return false;
); });
return found; return found;
}; };

@ -13,21 +13,14 @@ import {
UntypedQuizQuestion, UntypedQuizQuestion,
createQuestionVariant, createQuestionVariant,
} from "@model/questionTypes/shared"; } from "@model/questionTypes/shared";
import { defaultQuestionByType } from "../../constants/default";
import { produce } from "immer"; import { produce } from "immer";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { defaultQuestionByType } from "../../constants/default";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue";
import { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { QuestionsStore, useQuestionsStore } from "./store";
import { useUiTools } from "../uiTools/store";
import { withErrorBoundary } from "react-error-boundary";
import { QuizQuestionResult } from "@model/questionTypes/result";
import { replaceEmptyLinesToSpace } from "../../utils/replaceEmptyLinesToSpace"; import { replaceEmptyLinesToSpace } from "../../utils/replaceEmptyLinesToSpace";
import { useQuizPreviewStore } from "@root/quizPreview"; import { RequestQueue } from "../../utils/requestQueue";
import { useQuizStore } from "@root/quizes/store"; import { QuestionsStore, useQuestionsStore } from "./store";
export const setQuestions = (questions: RawQuestion[] | null | undefined) => export const setQuestions = (questions: RawQuestion[] | null | undefined) =>
setProducedState( setProducedState(
@ -244,9 +237,7 @@ export const cancelQuestionDeletion = (questionId: string) =>
}, },
); );
const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue(); const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = async <T = AnyTypedQuizQuestion>( export const updateQuestion = async <T = AnyTypedQuizQuestion>(
questionId: string, questionId: string,
@ -275,8 +266,6 @@ export const updateQuestion = async <T = AnyTypedQuizQuestion>(
}, },
); );
// clearTimeout(requestTimeoutId);
const request = async () => { const request = async () => {
const q = const q =
useQuestionsStore.getState().questions.find((q) => q.id === questionId) || useQuestionsStore.getState().questions.find((q) => q.id === questionId) ||
@ -321,9 +310,7 @@ export const updateQuestion = async <T = AnyTypedQuizQuestion>(
return; return;
} }
// requestTimeoutId = setTimeout(() => { requestQueue.enqueue(`updateQuestion-${questionId}`, request);
requestQueue.enqueue(request);
// }, REQUEST_DEBOUNCE);
}; };
export const addQuestionVariant = (questionId: string) => { export const addQuestionVariant = (questionId: string) => {
@ -453,7 +440,7 @@ export const createTypedQuestion = async (
questionId: string, questionId: string,
type: QuestionType, type: QuestionType,
) => ) =>
requestQueue.enqueue(async () => { requestQueue.enqueue(`createTypedQuestion-${questionId}`, async () => {
const questions = useQuestionsStore.getState().questions; const questions = useQuestionsStore.getState().questions;
const question = questions.find((q) => q.id === questionId); const question = questions.find((q) => q.id === questionId);
if (!question) return; if (!question) return;
@ -501,7 +488,7 @@ export const createTypedQuestion = async (
}); });
export const deleteQuestion = async (questionId: string) => export const deleteQuestion = async (questionId: string) =>
requestQueue.enqueue(async () => { requestQueue.enqueue(`deleteQuestion-${questionId}`, async () => {
const question = useQuestionsStore const question = useQuestionsStore
.getState() .getState()
.questions.find((q) => q.id === questionId); .questions.find((q) => q.id === questionId);
@ -525,7 +512,7 @@ export const deleteQuestion = async (questionId: string) =>
}); });
export const copyQuestion = async (questionId: string, quizId: number) => export const copyQuestion = async (questionId: string, quizId: number) =>
requestQueue.enqueue(async () => { requestQueue.enqueue(`copyQuestion-${quizId}-${questionId}`, async () => {
const question = useQuestionsStore const question = useQuestionsStore
.getState() .getState()
.questions.find((q) => q.id === questionId); .questions.find((q) => q.id === questionId);
@ -585,7 +572,7 @@ export const copyQuestion = async (questionId: string, quizId: number) =>
} }
}); });
function setProducedState<A extends string | { type: unknown }>( function setProducedState<A extends string | { type: string }>(
recipe: (state: QuestionsStore) => void, recipe: (state: QuestionsStore) => void,
action?: A, action?: A,
) { ) {
@ -635,7 +622,7 @@ export const createResult = async (
quizId: number | null | undefined, quizId: number | null | undefined,
parentContentId?: string, parentContentId?: string,
) => ) =>
requestQueue.enqueue(async () => { requestQueue.enqueue(`createResult-${quizId}`, async () => {
if (!quizId || !parentContentId) { if (!quizId || !parentContentId) {
console.error( console.error(
"Нет данных для создания результата. quizId: ", "Нет данных для создания результата. quizId: ",

@ -3,15 +3,14 @@ import { devlog, getMessageFromFetchError } from "@frontend/kitui";
import { quizToEditQuizRequest } from "@model/quiz/edit"; import { quizToEditQuizRequest } from "@model/quiz/edit";
import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz"; import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz";
import { QuizConfig, maxQuizSetupSteps } from "@model/quizSettings"; import { QuizConfig, maxQuizSetupSteps } from "@model/quizSettings";
import { createUntypedQuestion, updateQuestion } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { produce } from "immer"; import { produce } from "immer";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { NavigateFunction } from "react-router-dom"; import { NavigateFunction } from "react-router-dom";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue"; import { RequestQueue } from "../../utils/requestQueue";
import { QuizStore, useQuizStore } from "./store"; import { QuizStore, useQuizStore } from "./store";
import { createUntypedQuestion, updateQuestion } from "@root/questions/actions";
import { useCurrentQuiz } from "./hooks";
import { useQuestionsStore } from "@root/questions/store";
export const setEditQuizId = (quizId: number | null) => export const setEditQuizId = (quizId: number | null) =>
setProducedState( setProducedState(
@ -157,7 +156,7 @@ export const updateQuiz = (
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
requestTimeoutId = setTimeout(async () => { requestTimeoutId = setTimeout(async () => {
requestQueue requestQueue
.enqueue(async () => { .enqueue(`updateQuiz-${quizId}`, async () => {
const quiz = useQuizStore const quiz = useQuizStore
.getState() .getState()
.quizes.find((q) => q.id === quizId); .quizes.find((q) => q.id === quizId);
@ -178,7 +177,7 @@ export const updateQuiz = (
}; };
export const createQuiz = async (navigate: NavigateFunction) => export const createQuiz = async (navigate: NavigateFunction) =>
requestQueue.enqueue(async () => { requestQueue.enqueue("createQuiz", async () => {
try { try {
const rawQuiz = await quizApi.create(); const rawQuiz = await quizApi.create();
const quiz = rawQuizToQuiz(rawQuiz); const quiz = rawQuizToQuiz(rawQuiz);
@ -196,7 +195,7 @@ export const createQuiz = async (navigate: NavigateFunction) =>
}); });
export const deleteQuiz = async (quizId: string) => export const deleteQuiz = async (quizId: string) =>
requestQueue.enqueue(async () => { requestQueue.enqueue(`deleteQuiz-${quizId}`, async () => {
const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId); const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId);
if (!quiz) return; if (!quiz) return;

@ -33,7 +33,7 @@ const removeResult = (resultId: string) =>
}); });
export const deleteResult = async (resultId: number) => export const deleteResult = async (resultId: number) =>
requestQueue.enqueue(async () => { requestQueue.enqueue(`deleteResult-${resultId}`, async () => {
const result = useResultStore const result = useResultStore
.getState() .getState()
.results.find((r) => r.id === resultId); .results.find((r) => r.id === resultId);
@ -54,32 +54,35 @@ export const obsolescenceResult = async (
resultId: string, resultId: string,
editQuizId: number, editQuizId: number,
) => ) =>
requestQueue.enqueue(async () => { requestQueue.enqueue(
const result = useResultStore `obsolescenceResult-${resultId}-${editQuizId}`,
.getState() async () => {
.results.find((r) => r.id === resultId); const result = useResultStore
if (!result) return; .getState()
if (result.new === false) return; .results.find((r) => r.id === resultId);
let lossDebouncer: null | ReturnType<typeof setTimeout> = null; if (!result) return;
let lossId: string[] = [] as string[]; if (result.new === false) return;
if (!lossId.includes(resultId)) lossId.push(resultId); let lossDebouncer: null | ReturnType<typeof setTimeout> = null;
if (typeof lossDebouncer === "number") clearTimeout(lossDebouncer); let lossId: string[] = [] as string[];
lossDebouncer = setTimeout(async () => { if (!lossId.includes(resultId)) lossId.push(resultId);
//стреляем на лишение новизны if (typeof lossDebouncer === "number") clearTimeout(lossDebouncer);
try { lossDebouncer = setTimeout(async () => {
await resultApi.obsolescence(lossId); //стреляем на лишение новизны
//сбрасываем массив try {
lossId = []; await resultApi.obsolescence(lossId);
} catch (error) { //сбрасываем массив
devlog("Error", error); lossId = [];
} catch (error) {
devlog("Error", error);
const message = getMessageFromFetchError(error) ?? ""; const message = getMessageFromFetchError(error) ?? "";
enqueueSnackbar(`Ошибка. ${message}`); enqueueSnackbar(`Ошибка. ${message}`);
} }
}, 3000); }, 3000);
const resultList = await resultApi.getList(editQuizId); const resultList = await resultApi.getList(editQuizId);
setResults(resultList); setResults(resultList);
}); },
);
export const answerResultListExport = async ( export const answerResultListExport = async (
editQuizId: number, editQuizId: number,
@ -111,7 +114,7 @@ export const answerResultListExport = async (
download(); download();
}; };
function setProducedState<A extends string | { type: unknown }>( function setProducedState<A extends string | { type: string }>(
recipe: (state: ResultStore) => void, recipe: (state: ResultStore) => void,
action?: A, action?: A,
) { ) {

@ -1,14 +1,15 @@
export class RequestQueue<T = unknown> { export class RequestQueue<IdType, T = unknown> {
private pendingPromise = false; private pendingPromise = false;
private items: Array<{ private items: Array<{
id: IdType;
action: () => Promise<T>; action: () => Promise<T>;
resolve: (value: T) => void; resolve: (value: T) => void;
reject: (reason?: any) => void; reject: (reason?: any) => void;
}> = []; }> = [];
enqueue(action: () => Promise<T>) { enqueue(id: IdType, action: () => Promise<T>) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.items.push({ action, resolve, reject }); this.items.push({ action, resolve, reject, id });
this.dequeue(); this.dequeue();
}); });
} }
@ -19,6 +20,9 @@ export class RequestQueue<T = unknown> {
const item = this.items.shift(); const item = this.items.shift();
if (!item) return; if (!item) return;
// remove tasks with same id since they are outdated
this.items = this.items.filter((i) => i.id !== item.id);
try { try {
this.pendingPromise = true; this.pendingPromise = true;
const payload = await item.action(); const payload = await item.action();