From 6fbffcbb501684ae73411e2b17e22decca946005 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 1 Apr 2025 20:05:07 +0300 Subject: [PATCH] added useResource status hook --- src/hooks/useResourceStatus.tsx | 187 ++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/state/publishes.ts | 45 +++++++- 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useResourceStatus.tsx diff --git a/src/hooks/useResourceStatus.tsx b/src/hooks/useResourceStatus.tsx new file mode 100644 index 0000000..36bc45a --- /dev/null +++ b/src/hooks/useResourceStatus.tsx @@ -0,0 +1,187 @@ +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { usePublishStore } from "../state/publishes"; +import { QortalGetMetadata } from "../types/interfaces/resources"; + +interface PropsUseResourceStatus { + resource: QortalGetMetadata; + retryAttempts?: number; +} +export const useResourceStatus = ({ + resource, + retryAttempts = 15, +}: PropsUseResourceStatus) => { + const resourceId = `${resource.service}-${resource.name}-${resource.identifier}`; + const status = usePublishStore((state)=> state.getResourceStatus(resourceId)) || null + const intervalRef = useRef(null) + const timeoutRef = useRef(null) + const setResourceStatus = usePublishStore((state) => state.setResourceStatus); + const downloadResource = useCallback( + ({ service, name, identifier }: QortalGetMetadata, build?: boolean) => { + try { + setResourceStatus( + { service, name, identifier }, + { + "status": "SEARCHING", + "localChunkCount": 0, + "totalChunkCount": 0, + "percentLoaded": 0 + } + ); + let isCalling = false; + let percentLoaded = 0; + let timer = 24; + let tries = 0; + let calledFirstTime = false; + const callFunction = async () => { + if (isCalling) return; + isCalling = true; + + let res; + + if (!build) { + const urlFirstTime = `/arbitrary/resource/status/${service}/${name}/${identifier}`; + const resCall = await fetch(urlFirstTime, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + res = await resCall.json(); + setResourceStatus( + { service, name, identifier }, + { + ...res + } + ); + if (tries > retryAttempts) { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setResourceStatus( + { service, name, identifier }, + { + ...res, + status: "FAILED_TO_DOWNLOAD", + } + ); + + return; + } + tries = tries + 1; + } + + if (build || (calledFirstTime === false && res?.status !== "READY")) { + const url = `/arbitrary/resource/properties/${service}/${name}/${identifier}?build=true`; + const resCall = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + res = await resCall.json(); + } + calledFirstTime = true; + isCalling = false; + + if (res.localChunkCount) { + if (res.percentLoaded) { + if ( + res.percentLoaded === percentLoaded && + res.percentLoaded !== 100 + ) { + timer = timer - 5; + } else { + timer = 24; + } + + if (timer < 0) { + timer = 24; + isCalling = true; + + setResourceStatus( + { service, name, identifier }, + { + ...res, + status: "REFETCHING", + } + ); + + timeoutRef.current = setTimeout(() => { + isCalling = false; + downloadResource({ name, service, identifier }, true); + }, 25000); + + return; + } + + percentLoaded = res.percentLoaded; + } + + setResourceStatus( + { service, name, identifier }, + { + ...res, + } + ); + } + + // Check if progress is 100% and clear interval if true + if (res?.status === "READY") { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setResourceStatus({service, name, identifier}, { + ...res, + }) + } + if (res?.status === "DOWNLOADED") { + const url = `/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`; + const resCall = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + res = await resCall.json(); + } + }; + callFunction(); + intervalRef.current = setInterval(async () => { + callFunction(); + }, 5000); + } catch (error) { + console.error("Error during resource fetch:", error); + } + }, + [retryAttempts] + ); + useEffect(() => { + if (resource?.identifier && resource?.name && resource?.service) { + downloadResource({ + service: resource?.service, + name: resource?.name, + identifier: resource?.identifier, + }); + } + return ()=> { + if(intervalRef.current){ + clearInterval(intervalRef.current) + } + if(timeoutRef.current){ + clearTimeout(timeoutRef.current) + } + } + }, [ + resource?.identifier, + resource?.name, + resource?.service, + downloadResource, + ]); + return !status ? null : {...(status || {}), isReady: status?.status === 'READY'}; +}; diff --git a/src/index.ts b/src/index.ts index 98b1c72..77fcdb5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export { useResourceStatus } from './hooks/useResourceStatus'; import './index.css' export { EnumCollisionStrength } from './utils/encryption'; export { showLoading, dismissToast, showError, showSuccess } from './utils/toast'; diff --git a/src/state/publishes.ts b/src/state/publishes.ts index 8c06cd8..79679cc 100644 --- a/src/state/publishes.ts +++ b/src/state/publishes.ts @@ -7,16 +7,41 @@ interface PublishCache { expiry: number; } +export type Status = +| 'PUBLISHED' +| 'NOT_PUBLISHED' +| 'DOWNLOADING' +| 'DOWNLOADED' +| 'BUILDING' +| 'READY' +| 'MISSING_DATA' +| 'BUILD_FAILED' +| 'UNSUPPORTED' +| 'BLOCKED' +| 'FAILED_TO_DOWNLOAD' +| 'REFETCHING' +| 'SEARCHING' + +export interface ResourceStatus { + status: Status + localChunkCount: number + totalChunkCount: number + percentLoaded: number + +} interface PublishState { publishes: Record; - + resourceStatus: Record; + setResourceStatus: (qortalGetMetadata: QortalGetMetadata, data: ResourceStatus | null) => void; getPublish: (qortalGetMetadata: QortalGetMetadata | null, ignoreExpire?: boolean) => Resource | null; + getResourceStatus: (resourceId: string) => ResourceStatus | null; setPublish: (qortalGetMetadata: QortalGetMetadata, data: Resource | null, customExpiry?: number) => void; clearExpiredPublishes: () => void; publishExpiryDuration: number; // Default expiry duration } export const usePublishStore = create((set, get) => ({ + resourceStatus: {}, publishes: {}, publishExpiryDuration: 5 * 60 * 1000, // Default expiry: 5 minutes @@ -52,7 +77,23 @@ export const usePublishStore = create((set, get) => ({ }, })); }, - + setResourceStatus: (qortalGetMetadata, data) => { + const id = `${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`; + const existingData = get().resourceStatus[id] || {}; + set((state) => ({ + resourceStatus: { + ...state.resourceStatus, + [id]: !data ? null : { + ...existingData, + ...data + }, + }, + })); + }, + getResourceStatus: (resourceId) => { + const status = get().resourceStatus[resourceId]; + return status || null; + }, clearExpiredPublishes: () => { set((state) => { const now = Date.now();