mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +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 { 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}>
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user