add resources and update resources

This commit is contained in:
PhilReact 2025-03-14 02:51:41 +02:00
parent ae5111618f
commit e8866f1585
3 changed files with 269 additions and 192 deletions

View File

@ -1,10 +1,12 @@
import React, { createContext, useContext, useMemo } from "react"; import React, { createContext, useContext, useMemo } from "react";
import { useAuth, UseAuthProps } from "../hooks/useAuth"; import { useAuth, UseAuthProps } from "../hooks/useAuth";
import { useResources } from "../hooks/useResources";
// ✅ Define Global Context Type // ✅ Define Global Context Type
interface GlobalContextType { interface GlobalContextType {
auth: ReturnType<typeof useAuth>; auth: ReturnType<typeof useAuth>;
resources: ReturnType<typeof useResources>;
} }
// ✅ Define Config Type for Hook Options // ✅ Define Config Type for Hook Options
@ -23,10 +25,10 @@ const GlobalContext = createContext<GlobalContextType | null>(null);
export const GlobalProvider = ({ children, config }: GlobalProviderProps) => { export const GlobalProvider = ({ children, config }: GlobalProviderProps) => {
// ✅ Call hooks and pass in options dynamically // ✅ Call hooks and pass in options dynamically
const auth = useAuth(config?.auth || {}); const auth = useAuth(config?.auth || {});
const resources = useResources()
// ✅ Merge all hooks into a single `contextValue` // ✅ Merge all hooks into a single `contextValue`
const contextValue = useMemo(() => ({ auth }), [auth]); const contextValue = useMemo(() => ({ auth, resources }), [auth, resources]);
return ( return (
<GlobalContext.Provider value={contextValue}> <GlobalContext.Provider value={contextValue}>

View File

@ -1,207 +1,270 @@
import React, { useCallback } from 'react' import React, { useCallback } from "react";
import { QortalMetadata, QortalSearchParams } from '../types/interfaces/resources'; import {
import { useCacheStore } from '../state/cache'; QortalMetadata,
import { RequestQueueWithPromise } from '../utils/queue'; QortalSearchParams,
import { base64ToUint8Array, uint8ArrayToObject } from '../utils/base64'; } 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 requestQueueProductPublishes = new RequestQueueWithPromise(20);
export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(5); export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(
5
);
interface TemporaryResource { interface TemporaryResource {
qortalMetadata: QortalMetadata qortalMetadata: QortalMetadata;
data: any data: any;
} }
export const useResources = () => { export const useResources = () => {
const { setSearchCache, getSearchCache, getResourceCache, setResourceCache, addTemporaryResource, getTemporaryResources } = useCacheStore(); const {
const requestControllers = new Map<string, AbortController>(); setSearchCache,
getSearchCache,
getResourceCache,
setResourceCache,
addTemporaryResource
} = useCacheStore();
const requestControllers = new Map<string, AbortController>();
const getArbitraryResource = async (url: string, key: string): Promise<string> => { const getArbitraryResource = async (
// ✅ Create or reuse an existing controller url: string,
let controller = requestControllers.get(key); key: string
if (!controller) { ): Promise<string> => {
controller = new AbortController(); // ✅ Create or reuse an existing controller
requestControllers.set(key, 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 { try {
const res = await fetch(url, { signal: controller.signal }); const key = `${item?.service}-${item?.name}-${item?.identifier}`;
return await res.text();
} catch (error: any) { const cachedProduct = getResourceCache(
if (error?.name === "AbortError") { `${item?.service}-${item?.name}-${item?.identifier}`
console.warn(`Request cancelled: ${key}`); );
return "canceled"; // Return empty response on cancel if (cachedProduct) return;
} else { setResourceCache(
console.error(`Fetch error: ${key}`, error); `${item?.service}-${item?.name}-${item?.identifier}`,
null
);
let hasFailedToDownload = false;
let res: string | undefined = undefined;
try {
res = await requestQueueProductPublishes.enqueue(
(): Promise<string> => {
return getArbitraryResource(
`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`,
key
);
}
);
} catch (error) {
hasFailedToDownload = true;
} }
throw error; if (res === "canceled") return false;
} finally {
requestControllers.delete(key); // ✅ Cleanup controller after request
}
};
const cancelAllRequests = () => { if (hasFailedToDownload) {
requestControllers.forEach((controller, key) => { await new Promise((res) => {
controller.abort(); setTimeout(() => {
}); res(null);
requestControllers.clear(); }, 15000);
}; });
const fetchIndividualPublish = useCallback(
async (item: QortalMetadata) => {
try { try {
const key = `${item?.service}-${item?.name}-${item?.identifier}`; res = await requestQueueProductPublishesBackup.enqueue(
(): Promise<string> => {
const cachedProduct = getResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`); return getArbitraryResource(
if (cachedProduct) return; `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`,
setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, null); key
let hasFailedToDownload = false );
let res: string | undefined = undefined
try {
res = await requestQueueProductPublishes.enqueue((): Promise<string> => {
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<string> => {
return getArbitraryResource(`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, key)
});
} catch (error) {
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) { } 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<QortalMetadata[]> => {
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( const updateNewResources = useCallback(
(responseData: QortalMetadata[]): void => { (resources: TemporaryResource[]) => {
for (const item of responseData) {
fetchIndividualPublish(item);
}
},
[fetchIndividualPublish]
);
const fetchResources = useCallback( resources.forEach((temporaryResource) => {
async (params: QortalSearchParams, listName: string, cancelRequests?: boolean): Promise<QortalMetadata[]> => { setResourceCache(
if(cancelRequests){ `${temporaryResource?.qortalMetadata?.service}-${temporaryResource?.qortalMetadata?.name}-${temporaryResource?.qortalMetadata?.identifier}`,
cancelAllRequests() temporaryResource.data
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))
}, [])
return { return {
fetchResources, fetchResources,
fetchIndividualPublish, fetchIndividualPublish,
addNewResources addNewResources,
} updateNewResources
} };
};
export const generateCacheKey = (params: QortalSearchParams): string => { export const generateCacheKey = (params: QortalSearchParams): string => {
const { const {
identifier, identifier,
service, service,
query, query,
name, name,
names, names,
keywords, keywords,
title, title,
description, description,
prefix, prefix,
exactMatchNames, exactMatchNames,
minLevel, minLevel,
nameListFilter, nameListFilter,
followedOnly, followedOnly,
excludeBlocked, excludeBlocked,
before, before,
after, after,
limit, limit,
offset, offset,
reverse, reverse,
mode mode,
} = params; } = params;
const keyParts = [ const keyParts = [
`catalog-${service}`, `catalog-${service}`,
`id-${identifier}`, `id-${identifier}`,
query && `q-${query}`, query && `q-${query}`,
name && `n-${name}`, name && `n-${name}`,
names && `ns-${names.join(",")}`, names && `ns-${names.join(",")}`,
keywords && `kw-${keywords.join(",")}`, keywords && `kw-${keywords.join(",")}`,
title && `t-${title}`, title && `t-${title}`,
description && `desc-${description}`, description && `desc-${description}`,
prefix !== undefined && `p-${prefix}`, prefix !== undefined && `p-${prefix}`,
exactMatchNames !== undefined && `ex-${exactMatchNames}`, exactMatchNames !== undefined && `ex-${exactMatchNames}`,
minLevel !== undefined && `ml-${minLevel}`, minLevel !== undefined && `ml-${minLevel}`,
nameListFilter && `nf-${nameListFilter}`, nameListFilter && `nf-${nameListFilter}`,
followedOnly !== undefined && `fo-${followedOnly}`, followedOnly !== undefined && `fo-${followedOnly}`,
excludeBlocked !== undefined && `eb-${excludeBlocked}`, excludeBlocked !== undefined && `eb-${excludeBlocked}`,
before !== undefined && `b-${before}`, before !== undefined && `b-${before}`,
after !== undefined && `a-${after}`, after !== undefined && `a-${after}`,
limit !== undefined && `l-${limit}`, limit !== undefined && `l-${limit}`,
offset !== undefined && `o-${offset}`, offset !== undefined && `o-${offset}`,
reverse !== undefined && `r-${reverse}`, reverse !== undefined && `r-${reverse}`,
mode !== undefined && `mo-${mode}`, mode !== undefined && `mo-${mode}`,
] ]
.filter(Boolean) // Remove undefined or empty values .filter(Boolean) // Remove undefined or empty values
.join("_"); // Join into a string .join("_"); // Join into a string
return keyParts; return keyParts;
}; };

View File

@ -64,23 +64,35 @@ export class RequestQueueWithPromise<T = any> {
} }
export async function retryTransaction(fn, args, throwError, retries) { export async function retryTransaction<T>(
fn: (...args: any[]) => Promise<T>,
args: any[],
throwError: boolean,
retries: number
): Promise<T | null> {
let attempt = 0; let attempt = 0;
while (attempt < retries) { while (attempt < retries) {
try { try {
return await fn(...args); return await fn(...args);
} catch (error) { } catch (error: unknown) {
console.error(`Attempt ${attempt + 1} failed: ${error.message}`); if (error instanceof Error) {
console.error(`Attempt ${attempt + 1} failed: ${error.message}`);
} else {
console.error(`Attempt ${attempt + 1} failed: Unknown error`);
}
attempt++; attempt++;
if (attempt === retries) { if (attempt === retries) {
console.error("Max retries reached. Skipping transaction."); console.error("Max retries reached. Skipping transaction.");
if(throwError){ if (throwError) {
throw new Error(error?.message || "Unable to process transaction") throw new Error(error instanceof Error ? error.message : "Unable to process transaction");
} else { } else {
return null return null;
} }
} }
await new Promise(res => setTimeout(res, 10000)); await new Promise((res) => setTimeout(res, 10000));
} }
} }
return null;
} }