From dbe8418cdd40ec07a26d46b225e4541ce01dcb54 Mon Sep 17 00:00:00 2001 From: GRayHook Date: Sun, 24 Aug 2025 22:51:36 +0700 Subject: [PATCH] use promise to cache requests Don't do N equal requests for N equal answers --- src/api.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++ src/components/Reactions.tsx | 46 +++++++++++---------------- 2 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 src/api.ts diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..756d0d7 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,76 @@ +import browser from 'webextension-polyfill'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +// Тип для ответа сервера, который мы ожидаем +type AllReactionsForIssue = { + [commentId: string]: { + [emoji: string]: string[]; + }; +}; + +// --- НАШ КЭШ --- +// Мы будем хранить Promise, а не сами данные. +let reactionsPromiseCache: Promise | null = null; + +/** + * Получает все реакции для указанной задачи. + * Использует кэш на уровне модуля, чтобы избежать повторных запросов на одной странице. + */ +export async function fetchReactionsForIssue(issueId: number): Promise { + // Если Promise уже есть в кэше, просто возвращаем его. + if (reactionsPromiseCache) { + console.log('Serving reactions from cache.'); + return reactionsPromiseCache; + } + + console.log('Fetching reactions from network...'); + + // Если кэш пуст, создаем новый Promise, СОХРАНЯЕМ ЕГО В КЭШ, и возвращаем. + reactionsPromiseCache = fetch(`${API_BASE_URL}/api/reactions/${issueId}`) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return response.json(); + }) + .catch(error => { + // В случае ошибки, очищаем кэш, чтобы можно было попробовать снова. + reactionsPromiseCache = null; + throw error; + }); + + return reactionsPromiseCache; +} + +/** + * Получает анонимный ID пользователя из хранилища. + * (Переносим эту логику сюда, чтобы весь код, связанный с API/Storage, был в одном месте). + */ +export async function getAnonymousUserId(): Promise { + const data = await browser.storage.local.get('userId'); + if (data.userId) return data.userId; + const newUserId = crypto.randomUUID(); + await browser.storage.local.set({ userId: newUserId }); + return newUserId; +} + +/** + * Отправляет запрос на добавление/удаление реакции. + */ +export async function toggleReactionApi( + issueId: number, + commentId: string, + emoji: string, + userId: string, + method: 'POST' | 'DELETE' +) { + return fetch(`${API_BASE_URL}/api/reactions`, { + method, + headers: { + 'Content-Type': 'application/json', + 'X-User-ID': userId, + }, + body: JSON.stringify({ issueId, commentId, emoji }), + }); +} diff --git a/src/components/Reactions.tsx b/src/components/Reactions.tsx index 2356b07..1aef7df 100644 --- a/src/components/Reactions.tsx +++ b/src/components/Reactions.tsx @@ -1,20 +1,10 @@ import React, { useState, useEffect, useRef } from 'react'; import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'; -import browser from 'webextension-polyfill'; - -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +import { fetchReactionsForIssue, getAnonymousUserId, toggleReactionApi } from '../api'; type CommentReactions = { [emoji: string]: string[] }; interface ReactionsProps { issueId: number; commentId: string; } -async function getAnonymousUserId(): Promise { - const data = await browser.storage.local.get('userId'); - if (data.userId) return data.userId; - const newUserId = crypto.randomUUID(); - await browser.storage.local.set({ userId: newUserId }); - return newUserId; -} - const Reactions: React.FC = ({ issueId, commentId }) => { const [reactions, setReactions] = useState({}); const [myUserId, setMyUserId] = useState(null); @@ -25,16 +15,17 @@ const Reactions: React.FC = ({ issueId, commentId }) => { useEffect(() => { const fetchData = async () => { try { - const userId = await getAnonymousUserId(); + const [userId, allReactions] = await Promise.all([ + getAnonymousUserId(), + fetchReactionsForIssue(issueId) + ]); + setMyUserId(userId); - const response = await fetch(`${API_BASE_URL}/api/reactions/${issueId}`); - if (!response.ok) throw new Error('Network response was not ok'); - const data = await response.json(); - if (data && data[commentId]) { - setReactions(data[commentId]); + if (allReactions && allReactions[commentId]) { + setReactions(allReactions[commentId]); } } catch (error) { - console.error("Failed to fetch reactions:", error); + console.error("Failed to fetch data:", error); } finally { setIsLoading(false); } @@ -52,22 +43,22 @@ const Reactions: React.FC = ({ issueId, commentId }) => { const handleReactionClick = async (emoji: string) => { if (!myUserId) return; - const currentReactions = reactions[emoji] || []; - const hasReacted = currentReactions.includes(myUserId); + const hasReacted = (reactions[emoji] || []).includes(myUserId); const method = hasReacted ? 'DELETE' : 'POST'; const previousReactions = { ...reactions }; + setReactions(prev => { - const newUsers = hasReacted ? (prev[emoji] || []).filter(id => id !== myUserId) : [...(prev[emoji] || []), myUserId]; + const users = prev[emoji] || []; + const newUsers = hasReacted ? users.filter(id => id !== myUserId) : [...users, myUserId]; const newReactionsForComment = { ...prev, [emoji]: newUsers }; - if (newReactionsForComment[emoji].length === 0) delete newReactionsForComment[emoji]; + if (newReactionsForComment[emoji].length === 0) { + delete newReactionsForComment[emoji]; + } return newReactionsForComment; }); + try { - const response = await fetch(`${API_BASE_URL}/api/reactions`, { - method, - headers: { 'Content-Type': 'application/json', 'X-User-ID': myUserId }, - body: JSON.stringify({ issueId, commentId, emoji }), - }); + const response = await toggleReactionApi(issueId, commentId, emoji, myUserId, method); if (!response.ok) throw new Error('Server returned an error'); } catch (error) { console.error("Failed to update reaction:", error); @@ -130,5 +121,4 @@ const styles: { [key: string]: React.CSSProperties } = { skeleton: { height: '26px', backgroundColor: '#eef0f2', borderRadius: '12px', animation: 'reactions-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite' }, addButtonSkeleton: { height: '26px', width: '42px', backgroundColor: 'transparent', border: '1px dashed #e0e0e0', borderRadius: '12px' }, }; - export default Reactions;