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 quiz = useCurrentQuiz();
const quizConfig = quiz?.config;
const { questions } = useQuestionsStore();
const { editQuizId } = useQuizStore();
const currentStep = useQuizStore((state) => state.currentStep);
const { isTestServer } = useDomainDefine();

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

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

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

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

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