diff --git a/.eslintrc.json b/.eslintrc.json index 607d175..003944d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,6 +9,7 @@ "plugin:@typescript-eslint/stylistic-type-checked", "plugin:import/recommended", "plugin:import/typescript", + "plugin:@tanstack/eslint-plugin-query/recommended", "prettier" ], "parser": "@typescript-eslint/parser", diff --git a/package.json b/package.json index aec1450..ab0418d 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,16 @@ }, "dependencies": { "@gitkraken/provider-apis": "0.19.1", + "@tanstack/query-async-storage-persister": "5.32.0", + "@tanstack/react-query": "5.32.0", + "@tanstack/react-query-persist-client": "5.32.0", "react": "18.2.0", "react-dom": "18.2.0", "webextension-polyfill": "0.10.0" }, "devDependencies": { "@playwright/test": "1.35.1", + "@tanstack/eslint-plugin-query": "5.28.11", "@types/chrome": "0.0.246", "@types/react-dom": "18.2.23", "@types/webextension-polyfill": "0.10.1", diff --git a/src/gkApi.ts b/src/gkApi.ts index fb06379..dc2f5cd 100644 --- a/src/gkApi.ts +++ b/src/gkApi.ts @@ -1,6 +1,6 @@ import { cookies, storage } from 'webextension-polyfill'; import { checkOrigins } from './permissions-helper'; -import { DefaultCacheTimeMinutes, sessionCachedFetch, updateExtensionIcon } from './shared'; +import { updateExtensionIcon } from './shared'; import type { Provider, ProviderConnection, ProviderToken, PullRequestDraftCounts, User } from './types'; declare const MODE: 'production' | 'development' | 'none'; @@ -14,7 +14,7 @@ const onLoggedOut = () => { void storage.session.clear(); }; -const getAccessToken = async () => { +export const getAccessToken = async () => { // Check if the user has granted permission to GitKraken.dev if (!(await checkOrigins(['gitkraken.dev']))) { // If not, just assume we're logged out @@ -48,26 +48,18 @@ export const fetchUser = async () => { return null; } - // Since the user object is unlikely to change, we can cache it for much longer than other data - const user = await sessionCachedFetch('user', 60 * 12 /* 12 hours */, async () => { - const res = await fetch(`${gkApiUrl}/user`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!res.ok) { - return null; - } - - return res.json() as Promise; + const res = await fetch(`${gkApiUrl}/user`, { + headers: { + Authorization: `Bearer ${token}`, + }, }); - if (!user) { + if (!res.ok) { onLoggedOut(); return null; } + const user = (await res.json()) as User; void updateExtensionIcon(true); return user; }; @@ -105,20 +97,18 @@ export const fetchProviderConnections = async () => { return null; } - return sessionCachedFetch('providerConnections', DefaultCacheTimeMinutes, async () => { - const res = await fetch(`${gkApiUrl}/v1/provider-tokens/`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const res = await fetch(`${gkApiUrl}/v1/provider-tokens/`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); - if (!res.ok) { - return null; - } + if (!res.ok) { + return null; + } - const payload = await res.json(); - return payload.data as ProviderConnection[]; - }); + const payload = await res.json(); + return payload.data as ProviderConnection[]; }; export const refreshProviderToken = async (provider: Provider) => { diff --git a/src/popup/components/FocusView.tsx b/src/popup/components/FocusView.tsx index a04fe2b..6448f12 100644 --- a/src/popup/components/FocusView.tsx +++ b/src/popup/components/FocusView.tsx @@ -1,26 +1,28 @@ -import type { PullRequestBucket } from '@gitkraken/provider-apis'; import { GitProviderUtils } from '@gitkraken/provider-apis'; -import React, { useEffect, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import React, { useEffect, useMemo, useState } from 'react'; import { storage } from 'webextension-polyfill'; import { openGitKrakenDeepLink } from '../../deepLink'; -import { fetchDraftCounts, fetchProviderConnections } from '../../gkApi'; -import { fetchFocusViewData, ProviderMeta } from '../../providers'; -import { DefaultCacheTimeMinutes, GKDotDevUrl, sessionCachedFetch } from '../../shared'; +import { ProviderMeta } from '../../providers'; +import { GKDotDevUrl } from '../../shared'; import type { - FocusViewData, FocusViewSupportedProvider, GitPullRequestWithUniqueID, PullRequestBucketWithUniqueIDs, } from '../../types'; +import { useFocusViewConnectedProviders, useFocusViewDataQuery, usePullRequestDraftCountsQuery } from '../hooks'; import { ConnectAProvider } from './ConnectAProvider'; type PullRequestRowProps = { + userId: string; pullRequest: GitPullRequestWithUniqueID; provider: FocusViewSupportedProvider; draftCount?: number; }; -const PullRequestRow = ({ pullRequest, provider, draftCount = 0 }: PullRequestRowProps) => { +const PullRequestRow = ({ userId, pullRequest, provider, draftCount = 0 }: PullRequestRowProps) => { + const queryClient = useQueryClient(); + return ( <>
@@ -33,9 +35,8 @@ const PullRequestRow = ({ pullRequest, provider, draftCount = 0 }: PullRequestRo target="_blank" onClick={() => { // Since there is a decent chance that the PR will be acted upon after the user clicks on it, - // invalidate the cache so that the PR shows up in the appropriate bucket (or not at all) the - // next time the popup is opened. - void storage.session.remove('focusViewData'); + // mark the focus view data as stale so that it will be refetched when the user returns. + void queryClient.invalidateQueries({ queryKey: [userId, 'focusViewData', provider] }); }} title={`View pull request on ${ProviderMeta[provider].name}`} > @@ -72,12 +73,13 @@ const PullRequestRow = ({ pullRequest, provider, draftCount = 0 }: PullRequestRo }; type BucketProps = { + userId: string; bucket: PullRequestBucketWithUniqueIDs; provider: FocusViewSupportedProvider; - prDraftCountsByEntityID: Record; + prDraftCountsByEntityID?: Record; }; -const Bucket = ({ bucket, provider, prDraftCountsByEntityID }: BucketProps) => { +const Bucket = ({ userId, bucket, provider, prDraftCountsByEntityID }: BucketProps) => { return (
@@ -87,118 +89,67 @@ const Bucket = ({ bucket, provider, prDraftCountsByEntityID }: BucketProps) => { {bucket.pullRequests.map(pullRequest => ( ))}
); }; -export const FocusView = () => { - const [connectedProviders, setConnectedProviders] = useState([]); - const [selectedProvider, setSelectedProvider] = useState(); - const [prDraftCountsByEntityID, setPRDraftCountsByEntityID] = useState< - Record - >({}); - const [pullRequestBuckets, setPullRequestBuckets] = useState(); - const [isFirstLoad, setIsFirstLoad] = useState(true); - const [isLoadingPullRequests, setIsLoadingPullRequests] = useState(true); +export const FocusView = ({ userId }: { userId: string }) => { + const [selectedProvider, setSelectedProvider] = useState(); const [filterString, setFilterString] = useState(''); - useEffect(() => { - const loadData = async () => { - const [providerConnections, { focusViewSelectedProvider: savedSelectedProvider }] = await Promise.all([ - fetchProviderConnections(), - storage.local.get('focusViewSelectedProvider'), - ]); + const connectedProviders = useFocusViewConnectedProviders(userId); + const focusViewDataQuery = useFocusViewDataQuery(userId, selectedProvider); + const prDraftCountsQuery = usePullRequestDraftCountsQuery( + userId, + selectedProvider, + focusViewDataQuery.data?.pullRequests, + ); - const supportedProviders = (providerConnections || []) - .filter( - connection => - (connection.provider === 'github' || - connection.provider === 'gitlab' || - connection.provider === 'bitbucket' || - connection.provider === 'azure') && - !connection.domain, - ) - .map(connection => connection.provider as FocusViewSupportedProvider); + // This effect sets which provider is selected after the provider connections are loaded/changed + useEffect(() => { + const selectInitialProvider = async () => { + if (!connectedProviders) { + return; + } - setConnectedProviders(supportedProviders); + if (connectedProviders && connectedProviders.length > 0) { + const { focusViewSelectedProvider } = await storage.local.get('focusViewSelectedProvider'); - if (supportedProviders && supportedProviders.length > 0) { const providerToSelect = - savedSelectedProvider && supportedProviders.includes(savedSelectedProvider) - ? (savedSelectedProvider as FocusViewSupportedProvider) - : supportedProviders[0]; + focusViewSelectedProvider && connectedProviders.includes(focusViewSelectedProvider) + ? (focusViewSelectedProvider as FocusViewSupportedProvider) + : connectedProviders[0]; setSelectedProvider(providerToSelect); void storage.local.set({ focusViewSelectedProvider: providerToSelect }); } else { - setIsLoadingPullRequests(false); - setIsFirstLoad(false); - // Clear the cache so that if the user connects a provider, we'll fetch it the next - // time the popup is opened. - void storage.session.remove('providerConnections'); + setSelectedProvider(null); + void storage.local.remove('focusViewSelectedProvider'); } }; - void loadData(); - }, []); - - useEffect(() => { - const loadData = async () => { - if (!selectedProvider) { - return; - } - - setIsLoadingPullRequests(true); + void selectInitialProvider(); + }, [connectedProviders]); - let focusViewData: FocusViewData | null = null; - try { - focusViewData = await sessionCachedFetch('focusViewData', DefaultCacheTimeMinutes, () => - fetchFocusViewData(selectedProvider), - ); - } catch (e) { - // If there was an error, fall through to the next if block to at least end the loading state. - } - - if (!focusViewData) { - setPullRequestBuckets([]); - setIsLoadingPullRequests(false); - setIsFirstLoad(false); - return; - } - - const bucketsMap = GitProviderUtils.groupPullRequestsIntoBuckets( - focusViewData.pullRequests, - focusViewData.providerUser, - ); - const buckets = Object.values(bucketsMap) - .filter(bucket => bucket.pullRequests.length) - .sort((a, b) => a.priority - b.priority); + const pullRequestBuckets = useMemo(() => { + if (!focusViewDataQuery.data) { + return null; + } - setPullRequestBuckets(buckets); - setIsLoadingPullRequests(false); - setIsFirstLoad(false); - - if (selectedProvider === 'github' && focusViewData.pullRequests.length) { - const draftCounts = await sessionCachedFetch('focusViewDraftCounts', DefaultCacheTimeMinutes, () => { - if (!focusViewData) { - return null; - } - const prUniqueIds = focusViewData.pullRequests.map(pr => pr.uniqueId); - return fetchDraftCounts(prUniqueIds); - }); - if (draftCounts) { - setPRDraftCountsByEntityID(draftCounts.counts); - } - } - }; - - void loadData(); - }, [selectedProvider]); + const bucketsMap = GitProviderUtils.groupPullRequestsIntoBuckets( + focusViewDataQuery.data.pullRequests, + focusViewDataQuery.data.providerUser, + ); + return Object.values(bucketsMap) + .filter(bucket => bucket.pullRequests.length) + .sort((a, b) => a.priority - b.priority); + }, [focusViewDataQuery.data]); const lowercaseFilterString = filterString.toLowerCase().trim(); const filteredBuckets = lowercaseFilterString @@ -213,13 +164,12 @@ export const FocusView = () => { : pullRequestBuckets; const onProviderChange = (e: React.ChangeEvent) => { - void storage.session.remove(['focusViewData', 'focusViewDraftCounts']); void storage.local.set({ focusViewSelectedProvider: e.target.value }); setSelectedProvider(e.target.value as FocusViewSupportedProvider); setFilterString(''); }; - if (isFirstLoad) { + if (selectedProvider === undefined) { return (
@@ -237,7 +187,7 @@ export const FocusView = () => {
setFilterString(e.target.value)} placeholder="Search for pull requests" value={filterString} @@ -247,14 +197,14 @@ export const FocusView = () => { )}
)} - {selectedProvider && connectedProviders.length > 1 && ( + {selectedProvider && connectedProviders && connectedProviders.length > 1 && (
PRs:
)} - {isLoadingPullRequests ? ( + {focusViewDataQuery.isLoading ? (
@@ -273,9 +223,10 @@ export const FocusView = () => { {filteredBuckets?.map(bucket => ( ))}
diff --git a/src/popup/components/Popup.tsx b/src/popup/components/Popup.tsx index 1ff2397..7ff63d4 100644 --- a/src/popup/components/Popup.tsx +++ b/src/popup/components/Popup.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; import { runtime } from 'webextension-polyfill'; -import { fetchUser } from '../../gkApi'; +import { getAccessToken } from '../../gkApi'; import type { PermissionsRequest } from '../../permissions-helper'; import { PopupInitMessage } from '../../shared'; -import type { User } from '../../types'; +import { useUserQuery } from '../hooks'; import { RequestPermissionsBanner } from './RequestPermissionsBanner'; import { SignedIn } from './SignedIn'; import { SignedOut } from './SignedOut'; @@ -14,26 +14,27 @@ const syncWithBackground = async () => { export const Popup = () => { const [permissionsRequest, setPermissionsRequest] = useState(undefined); - const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [isCheckingPermissions, setIsCheckingPermissions] = useState(true); + const [token, setToken] = useState(undefined); + + const userQuery = useUserQuery(token); useEffect(() => { - const loadData = async () => { + const checkPermissions = async () => { const newPermissionsRequest = await syncWithBackground(); setPermissionsRequest(newPermissionsRequest); + setIsCheckingPermissions(false); - if (!permissionsRequest?.hasRequired) { - const fetchedUser = await fetchUser(); - setUser(fetchedUser); + if (!newPermissionsRequest?.hasRequired) { + const accessToken = await getAccessToken(); + setToken(accessToken); } - - setIsLoading(false); }; - void loadData(); + void checkPermissions(); }, []); - if (isLoading) { + if (isCheckingPermissions || (!permissionsRequest?.hasRequired && userQuery.isLoading)) { return (
@@ -49,8 +50,8 @@ export const Popup = () => { ); } - if (user) { - return ; + if (userQuery.data) { + return ; } return ; diff --git a/src/popup/components/SignedIn.tsx b/src/popup/components/SignedIn.tsx index bb03411..2140ff2 100644 --- a/src/popup/components/SignedIn.tsx +++ b/src/popup/components/SignedIn.tsx @@ -31,7 +31,7 @@ export const SignedIn = ({ permissionsRequest, user }: { permissionsRequest?: Pe
{permissionsRequest && }
- +
diff --git a/src/popup/hooks.ts b/src/popup/hooks.ts new file mode 100644 index 0000000..aacbc2c --- /dev/null +++ b/src/popup/hooks.ts @@ -0,0 +1,84 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { fetchDraftCounts, fetchProviderConnections, fetchUser } from '../gkApi'; +import { fetchFocusViewData } from '../providers'; +import type { FocusViewSupportedProvider } from '../types'; + +export const useUserQuery = (token: string | undefined) => { + return useQuery({ + enabled: Boolean(token), + queryKey: [token, 'user'], + queryFn: fetchUser, + // User info is rarely updated, so we can cache it for a long time + staleTime: 1000 * 60 * 60 * 24, // 24 hours + }); +}; + +export const useProviderConnectionsQuery = (userId: string) => { + return useQuery({ + queryKey: [userId, 'providerConnections'], + queryFn: fetchProviderConnections, + }); +}; + +export const useFocusViewConnectedProviders = (userId: string) => { + const providerConnectionsQuery = useProviderConnectionsQuery(userId); + + return useMemo(() => { + if (!providerConnectionsQuery.data) { + return null; + } + + return providerConnectionsQuery.data + .filter( + connection => + (connection.provider === 'github' || + connection.provider === 'gitlab' || + connection.provider === 'bitbucket' || + connection.provider === 'azure') && + !connection.domain, + ) + .map(connection => connection.provider as FocusViewSupportedProvider); + }, [providerConnectionsQuery.data]); +}; + +export const useFocusViewDataQuery = ( + userId: string, + selectedProvider: FocusViewSupportedProvider | null | undefined, +) => { + return useQuery({ + enabled: Boolean(selectedProvider), + queryKey: [userId, 'focusViewData', selectedProvider], + queryFn: async () => { + if (!selectedProvider) { + return null; + } + + return fetchFocusViewData(selectedProvider); + }, + // Focus view data is expensive, so we increase the stale time, and manually + // mark the data as stale if the user clicks on a PR link, which indicates + // that the user intends to take an action on the PR. + staleTime: 1000 * 60 * 10, // 10 minutes + }); +}; + +export const usePullRequestDraftCountsQuery = ( + userId: string, + selectedProvider: FocusViewSupportedProvider | null | undefined, + pullRequests: { uniqueId: string }[] | undefined, +) => { + let prUniqueIds: string[] = []; + if (selectedProvider === 'github' && pullRequests?.length) { + prUniqueIds = pullRequests.map(pr => pr.uniqueId); + } + + return useQuery({ + enabled: selectedProvider === 'github' && prUniqueIds.length > 0, + queryKey: [userId, 'focusViewData', selectedProvider, 'draftCounts', prUniqueIds], + queryFn: async () => { + const draftCounts = await fetchDraftCounts(prUniqueIds); + return draftCounts?.counts; + }, + }); +}; diff --git a/src/popup/main.tsx b/src/popup/main.tsx index 75908af..cafe56f 100644 --- a/src/popup/main.tsx +++ b/src/popup/main.tsx @@ -1,13 +1,19 @@ // Note: This code runs every time the extension popup is opened. +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { Popup } from './components/Popup'; +import { asyncStoragePersister, queryClient } from './queryClient'; function main() { const mainEl = document.getElementById('popup-container')!; const root = createRoot(mainEl); - root.render(); + root.render( + + + , + ); } main(); diff --git a/src/popup/queryClient.ts b/src/popup/queryClient.ts new file mode 100644 index 0000000..71fa233 --- /dev/null +++ b/src/popup/queryClient.ts @@ -0,0 +1,28 @@ +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; +import { QueryClient } from '@tanstack/react-query'; +import { storage } from 'webextension-polyfill'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + staleTime: 1000 * 10, // 10 seconds + }, + }, +}); + +// This implements a persister for react-query that uses the browser extension's storage API +export const asyncStoragePersister = createAsyncStoragePersister({ + storage: { + getItem: async key => { + const data = await storage.session.get(key); + return data[key] as string | null | undefined; + }, + setItem: async (key, value) => { + await storage.session.set({ [key]: value }); + }, + removeItem: async key => { + await storage.session.remove(key); + }, + }, +}); diff --git a/src/shared.ts b/src/shared.ts index f2721f7..34ca35b 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,13 +1,6 @@ -import { action, storage } from 'webextension-polyfill'; -import { debug } from './debug'; +import { action } from 'webextension-polyfill'; import { fetchProviderConnections } from './gkApi'; -import type { - CacheContext, - CachedFetchResponse, - EnterpriseProviderConnection, - ProviderConnection, - SessionCacheKey, -} from './types'; +import type { CacheContext, EnterpriseProviderConnection, ProviderConnection } from './types'; declare const MODE: 'production' | 'development' | 'none'; @@ -97,29 +90,3 @@ export async function getEnterpriseConnections(context: CacheContext) { return enterpriseConnections; }); } - -export const DefaultCacheTimeMinutes = 30; - -export const sessionCachedFetch = async ( - key: SessionCacheKey, - cacheTimeMinutes: number, - fetchFn: () => Promise | T, -) => { - const sessionStorage = await storage.session.get(key); - const data = sessionStorage[key] as CachedFetchResponse | undefined; - if (data && data.timestamp > Date.now() - cacheTimeMinutes * 60 * 1000) { - debug('Cache hit:', key); - return data.data; - } else if (data) { - debug('Cache stale:', key); - } else { - debug('Cache miss:', key); - } - - const newData = await fetchFn(); - if (newData) { - await storage.session.set({ [key]: { data: newData, timestamp: Date.now() } }); - } - - return newData; -}; diff --git a/src/types.ts b/src/types.ts index 0a760f2..1541632 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import type { Account, GitPullRequest, PullRequestBucket } from '@gitkraken/provider-apis'; export interface User { + id: string; email: string; name?: string; proAccessState?: { @@ -57,10 +58,3 @@ export type EnterpriseProviderConnection = ProviderConnection & Required = { - data: T; - timestamp: number; -}; diff --git a/yarn.lock b/yarn.lock index 6ef0444..cc9a253 100644 --- a/yarn.lock +++ b/yarn.lock @@ -253,7 +253,7 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.13.tgz#031f69b1f4cf62a18c38d502458c0b8b02625461" integrity sha512-iVl6lehAfJS+VmpF3exKpNQ8b0eucf5VWfzR8S7xFve64NBNz2jPUgx1X93/kfnkfgP737O+i1k54SVQS7uVZA== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== @@ -400,6 +400,46 @@ optionalDependencies: fsevents "2.3.2" +"@tanstack/eslint-plugin-query@5.28.11": + version "5.28.11" + resolved "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.28.11.tgz#0a46f2b301b706d993fc4bc25fef60c1fb639101" + integrity sha512-bODGLeG4WCGmHVKCh3bH1KLfq7xdi1jsRjTESV6ifCw1mZ0m2fBMxAjK42KjbhJwcvNdTlYHI+YY/aZWBk4Niw== + dependencies: + "@typescript-eslint/utils" "^6.20.0" + +"@tanstack/query-async-storage-persister@5.32.0": + version "5.32.0" + resolved "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.32.0.tgz#e95201b2508f5bc8da450a6723003b7074ab7271" + integrity sha512-wPtiXYz3twJ5bNmR1RNDlUW+LV9U1sG9pla4CJgEyP6H2gszLAFjDoCtx7SdifMnORemTVlypQHoa5my4oVCTw== + dependencies: + "@tanstack/query-persist-client-core" "5.32.0" + +"@tanstack/query-core@5.32.0": + version "5.32.0" + resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.32.0.tgz#e097ec2b394a2f64de33c98cd8baf3525c99641a" + integrity sha512-Z3flEgCat55DRXU5UMwYU1U+DgFZKA3iufyOKs+II7iRAo0uXkeU7PH5e6sOH1CGEag0IpKmZxlUFpCg6roSKw== + +"@tanstack/query-persist-client-core@5.32.0": + version "5.32.0" + resolved "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.32.0.tgz#4cda309bdd1aba56f0958fc918a062f682c60625" + integrity sha512-+DG5g+ife4rxcMmvtY+AedG4f++V4sxUVSmXKEo2SqIB12iaE+OwQ1CiHFbvlcCr/q3MaCOO5p5uBTonkqBtCA== + dependencies: + "@tanstack/query-core" "5.32.0" + +"@tanstack/react-query-persist-client@5.32.0": + version "5.32.0" + resolved "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.32.0.tgz#4da58a858ca4ab99d7c2a1de71bf44d91d0bb3c4" + integrity sha512-+RvDOuoj4axgok+XrM54NFThum0uW2opFv2j0TKiLILOT0GDkfxHgJz/R+e0IzYCmlGkgjiqh4W0uupCLAEb6g== + dependencies: + "@tanstack/query-persist-client-core" "5.32.0" + +"@tanstack/react-query@5.32.0": + version "5.32.0" + resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.32.0.tgz#52d441e7ad2a0098dc426f3834f68150c13f265b" + integrity sha512-+E3UudQtarnx9A6xhpgMZapyF+aJfNBGFMgI459FnduEZqT/9KhOWnMOneZahLRt52yzskSA0AuOyLkXHK0yBA== + dependencies: + "@tanstack/query-core" "5.32.0" + "@types/chrome@0.0.246": version "0.0.246" resolved "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.246.tgz#412ee1b162a67d1cfebe413887b9aa0c28998ef1" @@ -459,6 +499,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -514,6 +559,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + "@types/webextension-polyfill@0.10.1": version "0.10.1" resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.1.tgz#63698f0ef78a069d2d307be3caaee5e70c12e09d" @@ -557,6 +607,14 @@ "@typescript-eslint/types" "6.0.0" "@typescript-eslint/visitor-keys" "6.0.0" +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/type-utils@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.0.0.tgz#0478d8a94f05e51da2877cc0500f1b3c27ac7e18" @@ -572,6 +630,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.0.0.tgz#19795f515f8decbec749c448b0b5fc76d82445a1" integrity sha512-Zk9KDggyZM6tj0AJWYYKgF0yQyrcnievdhG0g5FqyU3Y2DRxJn4yWY21sJC0QKBckbsdKKjYDV2yVrrEvuTgxg== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + "@typescript-eslint/typescript-estree@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.0.0.tgz#1e09aab7320e404fb9f83027ea568ac24e372f81" @@ -585,6 +648,20 @@ semver "^7.5.0" ts-api-utils "^1.0.1" +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/utils@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.0.0.tgz#27a16d0d8f2719274a39417b9782f7daa3802db0" @@ -599,6 +676,19 @@ eslint-scope "^5.1.1" semver "^7.5.0" +"@typescript-eslint/utils@^6.20.0": + version "6.21.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" + "@typescript-eslint/visitor-keys@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.0.0.tgz#0b49026049fbd096d2c00c5e784866bc69532a31" @@ -607,6 +697,14 @@ "@typescript-eslint/types" "6.0.0" eslint-visitor-keys "^3.4.1" +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -1674,6 +1772,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -3532,6 +3637,13 @@ min-indent@^1.0.1: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -4191,6 +4303,13 @@ semver@^7.3.2, semver@^7.3.4, semver@^7.3.8, semver@^7.5.0: dependencies: lru-cache "^6.0.0" +semver@^7.5.4: + version "7.6.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + serialize-javascript@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"