From e8866f15855e3465b70d9c7b036014c3406cfa3c Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 14 Mar 2025 02:51:41 +0200 Subject: [PATCH] add resources and update resources --- src/context/GlobalProvider.tsx | 6 +- src/hooks/useResources.tsx | 427 +++++++++++++++++++-------------- src/utils/queue.ts | 28 ++- 3 files changed, 269 insertions(+), 192 deletions(-) diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index c392296..9ec03f3 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -1,10 +1,12 @@ import React, { createContext, useContext, useMemo } from "react"; import { useAuth, UseAuthProps } from "../hooks/useAuth"; +import { useResources } from "../hooks/useResources"; // ✅ Define Global Context Type interface GlobalContextType { auth: ReturnType; +resources: ReturnType; } // ✅ Define Config Type for Hook Options @@ -23,10 +25,10 @@ const GlobalContext = createContext(null); export const GlobalProvider = ({ children, config }: GlobalProviderProps) => { // ✅ Call hooks and pass in options dynamically const auth = useAuth(config?.auth || {}); - + const resources = useResources() // ✅ Merge all hooks into a single `contextValue` - const contextValue = useMemo(() => ({ auth }), [auth]); + const contextValue = useMemo(() => ({ auth, resources }), [auth, resources]); return ( diff --git a/src/hooks/useResources.tsx b/src/hooks/useResources.tsx index d62e24e..2344e7a 100644 --- a/src/hooks/useResources.tsx +++ b/src/hooks/useResources.tsx @@ -1,207 +1,270 @@ -import React, { useCallback } from 'react' -import { QortalMetadata, QortalSearchParams } from '../types/interfaces/resources'; -import { useCacheStore } from '../state/cache'; -import { RequestQueueWithPromise } from '../utils/queue'; -import { base64ToUint8Array, uint8ArrayToObject } from '../utils/base64'; +import React, { useCallback } from "react"; +import { + QortalMetadata, + QortalSearchParams, +} from "../types/interfaces/resources"; +import { useCacheStore } from "../state/cache"; +import { RequestQueueWithPromise } from "../utils/queue"; +import { base64ToUint8Array, uint8ArrayToObject } from "../utils/base64"; export const requestQueueProductPublishes = new RequestQueueWithPromise(20); -export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(5); +export const requestQueueProductPublishesBackup = new RequestQueueWithPromise( + 5 +); + interface TemporaryResource { - qortalMetadata: QortalMetadata - data: any + qortalMetadata: QortalMetadata; + data: any; } export const useResources = () => { - const { setSearchCache, getSearchCache, getResourceCache, setResourceCache, addTemporaryResource, getTemporaryResources } = useCacheStore(); - const requestControllers = new Map(); + const { + setSearchCache, + getSearchCache, + getResourceCache, + setResourceCache, + addTemporaryResource + } = useCacheStore(); + const requestControllers = new Map(); - const getArbitraryResource = async (url: string, key: string): Promise => { - // ✅ Create or reuse an existing controller - let controller = requestControllers.get(key); - if (!controller) { - controller = new AbortController(); - requestControllers.set(key, controller); + const getArbitraryResource = async ( + url: string, + key: string + ): Promise => { + // ✅ Create or reuse an existing controller + let controller = requestControllers.get(key); + if (!controller) { + controller = new AbortController(); + requestControllers.set(key, controller); + } + + try { + const res = await fetch(url, { signal: controller.signal }); + return await res.text(); + } catch (error: any) { + if (error?.name === "AbortError") { + console.warn(`Request cancelled: ${key}`); + return "canceled"; // Return empty response on cancel + } else { + console.error(`Fetch error: ${key}`, error); } - + throw error; + } finally { + requestControllers.delete(key); // ✅ Cleanup controller after request + } + }; + + const cancelAllRequests = () => { + requestControllers.forEach((controller, key) => { + controller.abort(); + }); + requestControllers.clear(); + }; + + const fetchIndividualPublish = useCallback( + async (item: QortalMetadata) => { try { - const res = await fetch(url, { signal: controller.signal }); - return await res.text(); - } catch (error: any) { - if (error?.name === "AbortError") { - console.warn(`Request cancelled: ${key}`); - return "canceled"; // Return empty response on cancel - } else { - console.error(`Fetch error: ${key}`, error); + const key = `${item?.service}-${item?.name}-${item?.identifier}`; + + const cachedProduct = getResourceCache( + `${item?.service}-${item?.name}-${item?.identifier}` + ); + if (cachedProduct) return; + setResourceCache( + `${item?.service}-${item?.name}-${item?.identifier}`, + null + ); + let hasFailedToDownload = false; + let res: string | undefined = undefined; + try { + res = await requestQueueProductPublishes.enqueue( + (): Promise => { + return getArbitraryResource( + `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, + key + ); + } + ); + } catch (error) { + hasFailedToDownload = true; } - throw error; - } finally { - requestControllers.delete(key); // ✅ Cleanup controller after request - } - }; + if (res === "canceled") return false; - const cancelAllRequests = () => { - requestControllers.forEach((controller, key) => { - controller.abort(); - }); - requestControllers.clear(); - }; - + if (hasFailedToDownload) { + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 15000); + }); - const fetchIndividualPublish = useCallback( - async (item: QortalMetadata) => { try { - const key = `${item?.service}-${item?.name}-${item?.identifier}`; - - const cachedProduct = getResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`); - if (cachedProduct) return; - setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, null); - let hasFailedToDownload = false - let res: string | undefined = undefined - try { - res = await requestQueueProductPublishes.enqueue((): Promise => { - return getArbitraryResource(`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, key) - }); - } catch (error) { - hasFailedToDownload = true - } - if(res === 'canceled') return false - - if (hasFailedToDownload) { - await new Promise((res) => { - setTimeout(() => { - res(null) - }, 15000) - }) - - try { - res = await requestQueueProductPublishesBackup.enqueue((): Promise => { - return getArbitraryResource(`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, key) - }); - } catch (error) { - setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, false); - return false + res = await requestQueueProductPublishesBackup.enqueue( + (): Promise => { + return getArbitraryResource( + `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, + key + ); } - } - if (res) { - const toUint = base64ToUint8Array(res); - const toObject = uint8ArrayToObject(toUint); - const fullDataObject = { data: {...toObject}, qortalMetadata: item }; - setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, fullDataObject); - return fullDataObject - } - + ); } catch (error) { - return false + setResourceCache( + `${item?.service}-${item?.name}-${item?.identifier}`, + false + ); + return false; + } + } + if (res) { + const toUint = base64ToUint8Array(res); + const toObject = uint8ArrayToObject(toUint); + const fullDataObject = { + data: { ...toObject }, + qortalMetadata: item, + }; + setResourceCache( + `${item?.service}-${item?.name}-${item?.identifier}`, + fullDataObject + ); + return fullDataObject; + } + } catch (error) { + return false; } }, - [getResourceCache, setResourceCache] + [getResourceCache, setResourceCache] + ); + + const fetchDataFromResults = useCallback( + (responseData: QortalMetadata[]): void => { + for (const item of responseData) { + fetchIndividualPublish(item); + } + }, + [fetchIndividualPublish] + ); + + const fetchResources = useCallback( + async ( + params: QortalSearchParams, + listName: string, + cancelRequests?: boolean + ): Promise => { + if (cancelRequests) { + cancelAllRequests(); + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 250); + }); + } + const cacheKey = generateCacheKey(params); + const searchCache = getSearchCache(listName, cacheKey); + let responseData = []; + + if (searchCache) { + responseData = searchCache; + } else { + const response = await qortalRequest({ + action: "SEARCH_QDN_RESOURCES", + mode: "ALL", + limit: 20, + ...params, + }); + if (!response) throw new Error("Unable to fetch resources"); + responseData = response; + } + setSearchCache(listName, cacheKey, responseData); + fetchDataFromResults(responseData); + + return responseData; + }, + [getSearchCache, setSearchCache, fetchDataFromResults] + ); + + const addNewResources = useCallback( + (listName: string, resources: TemporaryResource[]) => { + addTemporaryResource( + listName, + resources.map((item) => item.qortalMetadata) ); + resources.forEach((temporaryResource) => { + setResourceCache( + `${temporaryResource?.qortalMetadata?.service}-${temporaryResource?.qortalMetadata?.name}-${temporaryResource?.qortalMetadata?.identifier}`, + temporaryResource.data + ); + }); + }, + [] + ); - const fetchDataFromResults = useCallback( - (responseData: QortalMetadata[]): void => { - for (const item of responseData) { - fetchIndividualPublish(item); - } - }, - [fetchIndividualPublish] - ); + const updateNewResources = useCallback( + (resources: TemporaryResource[]) => { - const fetchResources = useCallback( - async (params: QortalSearchParams, listName: string, cancelRequests?: boolean): Promise => { - if(cancelRequests){ - cancelAllRequests() - await new Promise((res)=> { - setTimeout(() => { - res(null) - }, 250); - }) - } - const cacheKey = generateCacheKey(params); - const searchCache = getSearchCache(listName, cacheKey); - let responseData = []; - - if (searchCache) { - responseData = searchCache; - } else { - const response = await qortalRequest({ - action: "SEARCH_QDN_RESOURCES", - mode: 'ALL', - limit: 20, - ...params, - }); - if (!response) throw new Error("Unable to fetch resources"); - responseData = response - } - setSearchCache(listName, cacheKey, responseData); - fetchDataFromResults(responseData); - - return responseData; - }, - [getSearchCache, setSearchCache, fetchDataFromResults] - ); - - - - const addNewResources = useCallback((listName:string, temporaryResources: TemporaryResource[])=> { - addTemporaryResource(listName, temporaryResources.map((item)=> item.qortalMetadata)) - }, []) + resources.forEach((temporaryResource) => { + setResourceCache( + `${temporaryResource?.qortalMetadata?.service}-${temporaryResource?.qortalMetadata?.name}-${temporaryResource?.qortalMetadata?.identifier}`, + temporaryResource.data + ); + }); + }, + [] + ); return { fetchResources, fetchIndividualPublish, - addNewResources - } -} - + addNewResources, + updateNewResources + }; +}; export const generateCacheKey = (params: QortalSearchParams): string => { - const { - identifier, - service, - query, - name, - names, - keywords, - title, - description, - prefix, - exactMatchNames, - minLevel, - nameListFilter, - followedOnly, - excludeBlocked, - before, - after, - limit, - offset, - reverse, - mode - } = params; - - const keyParts = [ - `catalog-${service}`, - `id-${identifier}`, - query && `q-${query}`, - name && `n-${name}`, - names && `ns-${names.join(",")}`, - keywords && `kw-${keywords.join(",")}`, - title && `t-${title}`, - description && `desc-${description}`, - prefix !== undefined && `p-${prefix}`, - exactMatchNames !== undefined && `ex-${exactMatchNames}`, - minLevel !== undefined && `ml-${minLevel}`, - nameListFilter && `nf-${nameListFilter}`, - followedOnly !== undefined && `fo-${followedOnly}`, - excludeBlocked !== undefined && `eb-${excludeBlocked}`, - before !== undefined && `b-${before}`, - after !== undefined && `a-${after}`, - limit !== undefined && `l-${limit}`, - offset !== undefined && `o-${offset}`, - reverse !== undefined && `r-${reverse}`, - mode !== undefined && `mo-${mode}`, - ] - .filter(Boolean) // Remove undefined or empty values - .join("_"); // Join into a string - - return keyParts; - }; - \ No newline at end of file + const { + identifier, + service, + query, + name, + names, + keywords, + title, + description, + prefix, + exactMatchNames, + minLevel, + nameListFilter, + followedOnly, + excludeBlocked, + before, + after, + limit, + offset, + reverse, + mode, + } = params; + + const keyParts = [ + `catalog-${service}`, + `id-${identifier}`, + query && `q-${query}`, + name && `n-${name}`, + names && `ns-${names.join(",")}`, + keywords && `kw-${keywords.join(",")}`, + title && `t-${title}`, + description && `desc-${description}`, + prefix !== undefined && `p-${prefix}`, + exactMatchNames !== undefined && `ex-${exactMatchNames}`, + minLevel !== undefined && `ml-${minLevel}`, + nameListFilter && `nf-${nameListFilter}`, + followedOnly !== undefined && `fo-${followedOnly}`, + excludeBlocked !== undefined && `eb-${excludeBlocked}`, + before !== undefined && `b-${before}`, + after !== undefined && `a-${after}`, + limit !== undefined && `l-${limit}`, + offset !== undefined && `o-${offset}`, + reverse !== undefined && `r-${reverse}`, + mode !== undefined && `mo-${mode}`, + ] + .filter(Boolean) // Remove undefined or empty values + .join("_"); // Join into a string + + return keyParts; +}; diff --git a/src/utils/queue.ts b/src/utils/queue.ts index b3aa003..3073663 100644 --- a/src/utils/queue.ts +++ b/src/utils/queue.ts @@ -64,23 +64,35 @@ export class RequestQueueWithPromise { } -export async function retryTransaction(fn, args, throwError, retries) { +export async function retryTransaction( + fn: (...args: any[]) => Promise, + args: any[], + throwError: boolean, + retries: number +): Promise { let attempt = 0; while (attempt < retries) { try { - return await fn(...args); - } catch (error) { - console.error(`Attempt ${attempt + 1} failed: ${error.message}`); + return await fn(...args); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`Attempt ${attempt + 1} failed: ${error.message}`); + } else { + console.error(`Attempt ${attempt + 1} failed: Unknown error`); + } + attempt++; if (attempt === retries) { console.error("Max retries reached. Skipping transaction."); - if(throwError){ - throw new Error(error?.message || "Unable to process transaction") + if (throwError) { + throw new Error(error instanceof Error ? error.message : "Unable to process transaction"); } else { - return null + return null; } } - await new Promise(res => setTimeout(res, 10000)); + await new Promise((res) => setTimeout(res, 10000)); } } + return null; } +