This commit is contained in:
PhilReact 2025-03-19 04:29:13 +02:00
parent 5e05ecff28
commit f83ce67072
7 changed files with 232 additions and 14 deletions

View File

@ -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

106
src/hooks/usePublish.tsx Normal file
View File

@ -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<boolean | null>(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,
};
};

View File

@ -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<string> => {
return getArbitraryResource(
`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`,
key
);
}
);
const fetchRetries = async ()=> {
return await requestQueueProductPublishesBackup.enqueue(
(): Promise<string> => {
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}`,

View File

@ -1,3 +1,4 @@
import './index.css'
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
export {usePublish} from "./hooks/usePublish"
export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay"

35
src/state/publishes.ts Normal file
View File

@ -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<string, TemporaryResource> ;
getPublish: (qortalGetMetadata: QortalGetMetadata | null) => TemporaryResource | null;
setPublish: (qortalGetMetadata: QortalGetMetadata, data: TemporaryResource) => void;
}
// ✅ Typed Zustand Store
export const usePublishStore = create<PublishState>((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,
},
};
}),
}));

View File

@ -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;

59
src/utils/publish.ts Normal file
View File

@ -0,0 +1,59 @@
const MAX_RETRIES = 3; // Define your max retries constant
export async function retryTransaction<T>(
fn: (...args: any[]) => Promise<T>, // Function that returns a promise
args: any[], // Arguments for the function
throwError: boolean,
retries: number = MAX_RETRIES
): Promise<T | null> {
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
}