From f83ce6707289216847c356d45d9d2dfb22a9b262 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 19 Mar 2025 04:29:13 +0200 Subject: [PATCH] fixes --- .../ResourceList/ResourceListDisplay.tsx | 6 +- src/hooks/usePublish.tsx | 106 ++++++++++++++++++ src/hooks/useResources.tsx | 33 ++++-- src/index.ts | 1 + src/state/publishes.ts | 35 ++++++ src/types/interfaces/resources.ts | 6 + src/utils/publish.ts | 59 ++++++++++ 7 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 src/hooks/usePublish.tsx create mode 100644 src/state/publishes.ts create mode 100644 src/utils/publish.ts diff --git a/src/components/ResourceList/ResourceListDisplay.tsx b/src/components/ResourceList/ResourceListDisplay.tsx index 866cab8..c99e8f2 100644 --- a/src/components/ResourceList/ResourceListDisplay.tsx +++ b/src/components/ResourceList/ResourceListDisplay.tsx @@ -66,6 +66,7 @@ interface BaseProps { resourceCacheDuration?: number disablePagination?: boolean disableScrollTracker?: boolean + retryAttempts: number } // ✅ Restrict `direction` only when `disableVirtualization = false` @@ -99,9 +100,10 @@ export const MemorizedComponent = ({ resourceCacheDuration, disablePagination, disableScrollTracker, - entityParams + entityParams, + retryAttempts = 2 }: PropsResourceListDisplay) => { - const { fetchResources } = useResources(); + const { fetchResources } = useResources(retryAttempts); const { filterOutDeletedResources } = useCacheStore(); const {identifierOperations} = useGlobal() const deletedResources = useCacheStore().deletedResources diff --git a/src/hooks/usePublish.tsx b/src/hooks/usePublish.tsx new file mode 100644 index 0000000..2de21db --- /dev/null +++ b/src/hooks/usePublish.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { usePublishStore } from "../state/publishes"; +import { QortalGetMetadata } from "../types/interfaces/resources"; +import { base64ToObject, retryTransaction } from "../utils/publish"; + + +export const usePublish = ( + maxFetchTries: number = 3, + returnType: "PUBLIC_JSON" = "PUBLIC_JSON", + metadata?: QortalGetMetadata +) => { + const hasFetched = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const publish = usePublishStore().getPublish(metadata || null); + const setPublish = usePublishStore().setPublish; + const [hasResource, setHasResource] = useState(null) + const fetchRawData = useCallback(async (item: QortalGetMetadata) => { + const url = `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`; + const res = await fetch(url); + const data = await res.text(); + return base64ToObject(data); + }, []); + + const fetchPublish = useCallback( + async (metadataProp: QortalGetMetadata, returnTypeProp: "PUBLIC_JSON" = "PUBLIC_JSON") => { + let resourceExists = null; + let resource = null; + let error = null; + try { + if (metadata) { + setIsLoading(true); + } + const url = `/arbitrary/resources/search?mode=ALL&service=${metadataProp?.service}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${metadataProp?.name}&exactmatchnames=true&offset=0&identifier=${metadataProp?.identifier}`; + const responseMetadata = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (!responseMetadata?.ok) return false; + const resMetadata = await responseMetadata.json(); + if (resMetadata?.length === 0) { + resourceExists = false; + setHasResource(false) + } else { + resourceExists = true + setHasResource(true) + const response = await retryTransaction( + fetchRawData, + [metadataProp], + true, + maxFetchTries + ); + const fullData = { + qortalMetadata: resMetadata[0], + data: response, + }; + if (metadata) { + setPublish(resMetadata[0], fullData); + } + resource = { + qortalMetadata: resMetadata[0], + data: response, + }; + } + } catch (error: any) { + setError(error?.message); + if (!metadata) { + error = error?.message; + } + } finally { + if (metadata) { + setIsLoading(false); + } + } + return { + resourceExists, + resource, + error, + }; + }, + [metadata] + ); + + useEffect(() => { + if (hasFetched.current) return; + if (metadata?.identifier && metadata?.name && metadata?.service) { + hasFetched.current = true; + fetchPublish(metadata, returnType); + } + }, [metadata, returnType]); + + if (!metadata) + return { + fetchPublish, + }; + return { + isLoading, + error, + resource: publish || null, + hasResource, + refetch: fetchPublish, + fetchPublish, + }; +}; diff --git a/src/hooks/useResources.tsx b/src/hooks/useResources.tsx index 9586c57..5b07486 100644 --- a/src/hooks/useResources.tsx +++ b/src/hooks/useResources.tsx @@ -6,17 +6,18 @@ import { import { ListItem, useCacheStore } from "../state/cache"; import { RequestQueueWithPromise } from "../utils/queue"; import { base64ToUint8Array, uint8ArrayToObject } from "../utils/base64"; +import { retryTransaction } from "../utils/publish"; export const requestQueueProductPublishes = new RequestQueueWithPromise(20); export const requestQueueProductPublishesBackup = new RequestQueueWithPromise( - 5 + 10 ); -interface TemporaryResource { +export interface TemporaryResource { qortalMetadata: QortalMetadata; data: any; } -export const useResources = () => { +export const useResources = (retryAttempts: number = 2) => { const { setSearchCache, getSearchCache, @@ -120,18 +121,26 @@ export const useResources = () => { await new Promise((res) => { setTimeout(() => { res(null); - }, 15000); + }, 10000); }); try { - res = await requestQueueProductPublishesBackup.enqueue( - (): Promise => { - return getArbitraryResource( - `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, - key - ); - } - ); + const fetchRetries = async ()=> { + return await requestQueueProductPublishesBackup.enqueue( + (): Promise => { + return getArbitraryResource( + `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, + key + ); + } + ); + } + res = await retryTransaction( + fetchRetries, + [], + true, + retryAttempts + ); } catch (error) { setResourceCache( `${item?.service}-${item?.name}-${item?.identifier}`, diff --git a/src/index.ts b/src/index.ts index 61af36c..276ecef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ import './index.css' export { GlobalProvider, useGlobal } from "./context/GlobalProvider"; +export {usePublish} from "./hooks/usePublish" export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay" \ No newline at end of file diff --git a/src/state/publishes.ts b/src/state/publishes.ts new file mode 100644 index 0000000..9ed2816 --- /dev/null +++ b/src/state/publishes.ts @@ -0,0 +1,35 @@ +import { create } from "zustand"; +import { IdentifierBuilder } from "../utils/encryption"; +import { QortalGetMetadata } from "../types/interfaces/resources"; +import { TemporaryResource } from "../hooks/useResources"; + + + +interface PublishState { + publishes: Record ; + + getPublish: (qortalGetMetadata: QortalGetMetadata | null) => TemporaryResource | null; + setPublish: (qortalGetMetadata: QortalGetMetadata, data: TemporaryResource) => void; +} + +// ✅ Typed Zustand Store +export const usePublishStore = create((set, get) => ({ + publishes: {}, + getPublish: (qortalGetMetadata) => { + if(qortalGetMetadata === null) return null + const cache = get().publishes[`${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`]; + if (cache) { + return cache + } + return null; + }, + setPublish: (qortalGetMetadata, data) => + set((state) => { + return { + publishes: { + ...state.publishes, + [`${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`]: data, + }, + }; + }), +})); diff --git a/src/types/interfaces/resources.ts b/src/types/interfaces/resources.ts index c0ffa3f..833f859 100644 --- a/src/types/interfaces/resources.ts +++ b/src/types/interfaces/resources.ts @@ -70,6 +70,12 @@ export interface QortalMetadata { updated?: number } + export interface QortalGetMetadata { + name: string + identifier: string + service: Service + } + export interface QortalSearchParams { identifier: string; service: Service; diff --git a/src/utils/publish.ts b/src/utils/publish.ts new file mode 100644 index 0000000..dd8d4e3 --- /dev/null +++ b/src/utils/publish.ts @@ -0,0 +1,59 @@ +const MAX_RETRIES = 3; // Define your max retries constant + +export async function retryTransaction( + fn: (...args: any[]) => Promise, // Function that returns a promise + args: any[], // Arguments for the function + throwError: boolean, + retries: number = MAX_RETRIES +): Promise { + let attempt = 0; + + while (attempt < retries) { + try { + return await fn(...args); // Attempt to execute the function + } catch (error: any) { + console.error(`Attempt ${attempt + 1} failed: ${error.message}`); + attempt++; + + if (attempt === retries) { + console.error("Max retries reached. Skipping transaction."); + if (throwError) { + throw new Error(error?.message || "Unable to process transaction"); + } else { + return null; + } + } + + // Wait before retrying + await new Promise((res) => setTimeout(res, 10000)); + } + } + + return null; // This should never be reached, but added for type safety +} + +export function base64ToUint8Array(base64: string) { + const binaryString = atob(base64) + const len = binaryString.length + const bytes = new Uint8Array(len) + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return bytes +} + +export function uint8ArrayToObject(uint8Array: Uint8Array) { + // Decode the byte array using TextDecoder + const decoder = new TextDecoder() + const jsonString = decoder.decode(uint8Array) + // Convert the JSON string back into an object + return JSON.parse(jsonString) +} + + +export function base64ToObject(base64: string){ + const toUint = base64ToUint8Array(base64); + const toObject = uint8ArrayToObject(toUint); + + return toObject +} \ No newline at end of file