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 { useAuth, UseAuthProps } from "../hooks/useAuth";
import { useResources } from "../hooks/useResources";
// ✅ Define Global Context Type
interface GlobalContextType {
auth: ReturnType<typeof useAuth>;
resources: ReturnType<typeof useResources>;
}
// ✅ Define Config Type for Hook Options
@ -23,10 +25,10 @@ const GlobalContext = createContext<GlobalContextType | null>(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 (
<GlobalContext.Provider value={contextValue}>

View File

@ -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<string, AbortController>();
const {
setSearchCache,
getSearchCache,
getResourceCache,
setResourceCache,
addTemporaryResource
} = useCacheStore();
const requestControllers = new Map<string, AbortController>();
const getArbitraryResource = async (url: string, key: string): Promise<string> => {
// ✅ 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<string> => {
// ✅ 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<string> => {
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<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
res = await requestQueueProductPublishesBackup.enqueue(
(): Promise<string> => {
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<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(
(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<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, 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;
};
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;
};

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