mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-15 09:51:21 +00:00
add resources and update resources
This commit is contained in:
parent
ae5111618f
commit
e8866f1585
@ -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}>
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user