mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +00:00
updated usePublish
This commit is contained in:
parent
f83ce67072
commit
770080b942
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.9",
|
||||
"description": "Qortal's core React library with global state, UI components, and utilities",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
|
@ -103,9 +103,8 @@ export const MemorizedComponent = ({
|
||||
entityParams,
|
||||
retryAttempts = 2
|
||||
}: PropsResourceListDisplay) => {
|
||||
const { fetchResources } = useResources(retryAttempts);
|
||||
const { filterOutDeletedResources } = useCacheStore();
|
||||
const {identifierOperations} = useGlobal()
|
||||
const {identifierOperations, lists} = useGlobal()
|
||||
const deletedResources = useCacheStore().deletedResources
|
||||
const memoizedParams = useMemo(() => JSON.stringify(search), [search]);
|
||||
const addList = useListStore().addList
|
||||
@ -119,9 +118,7 @@ export const MemorizedComponent = ({
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
const initialized = useRef(false)
|
||||
const [generatedIdentifier, setGeneratedIdentifier] = useState("")
|
||||
|
||||
|
||||
|
||||
const prevGeneratedIdentifierRef = useRef('')
|
||||
|
||||
|
||||
const stringifiedEntityParams = useMemo(()=> {
|
||||
@ -157,6 +154,7 @@ export const MemorizedComponent = ({
|
||||
try {
|
||||
|
||||
if(!generatedIdentifier) return
|
||||
|
||||
await new Promise((res)=> {
|
||||
setTimeout(() => {
|
||||
res(null)
|
||||
@ -165,27 +163,29 @@ export const MemorizedComponent = ({
|
||||
setIsLoading(true);
|
||||
const parsedParams = {...(JSON.parse(memoizedParams))};
|
||||
parsedParams.identifier = generatedIdentifier
|
||||
const responseData = await fetchResources(parsedParams, listName, true); // Awaiting the async function
|
||||
const responseData = await lists.fetchResources(parsedParams, listName, true); // Awaiting the async function
|
||||
|
||||
|
||||
|
||||
addList(listName, responseData || []);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch resources:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [memoizedParams, fetchResources, generatedIdentifier]); // Added dependencies for re-fetching
|
||||
}, [memoizedParams, lists.fetchResources, generatedIdentifier]); // Added dependencies for re-fetching
|
||||
useEffect(() => {
|
||||
if(initialized.current || !generatedIdentifier) return
|
||||
initialized.current = true
|
||||
if(!isListExpired) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if(!generatedIdentifier) return
|
||||
|
||||
if(!isListExpired && !initialized.current) {
|
||||
setIsLoading(false)
|
||||
initialized.current = true
|
||||
return
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(`scroll-position-${listName}`);
|
||||
prevGeneratedIdentifierRef.current = generatedIdentifier
|
||||
getResourceList();
|
||||
}, [getResourceList, isListExpired, generatedIdentifier]); // Runs when dependencies change
|
||||
|
||||
@ -221,7 +221,7 @@ export const MemorizedComponent = ({
|
||||
if(displayLimit){
|
||||
parsedParams.limit = displayLimit
|
||||
}
|
||||
const responseData = await fetchResources(parsedParams, listName); // Awaiting the async function
|
||||
const responseData = await lists.fetchResources(parsedParams, listName); // Awaiting the async function
|
||||
addItems(listName, responseData || [])
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch resources:", error);
|
||||
|
@ -5,17 +5,19 @@ import { useAppInfo } from "../hooks/useAppInfo";
|
||||
import { IdentifierBuilder } from "../utils/encryption";
|
||||
import { useIdentifiers } from "../hooks/useIdentifiers";
|
||||
import { objectToBase64 } from "../utils/base64";
|
||||
import { base64ToObject } from "../utils/publish";
|
||||
|
||||
|
||||
const utils = {
|
||||
objectToBase64
|
||||
objectToBase64,
|
||||
base64ToObject
|
||||
}
|
||||
|
||||
|
||||
// ✅ Define Global Context Type
|
||||
interface GlobalContextType {
|
||||
auth: ReturnType<typeof useAuth>;
|
||||
resources: ReturnType<typeof useResources>;
|
||||
lists: ReturnType<typeof useResources>;
|
||||
appInfo: ReturnType<typeof useAppInfo>;
|
||||
identifierOperations: ReturnType<typeof useIdentifiers>
|
||||
utils: typeof utils
|
||||
@ -44,11 +46,11 @@ export const GlobalProvider = ({ children, config, identifierBuilder }: GlobalPr
|
||||
// ✅ Call hooks and pass in options dynamically
|
||||
const auth = useAuth(config?.auth || {});
|
||||
const appInfo = useAppInfo(config?.appName, config?.publicSalt)
|
||||
const resources = useResources()
|
||||
const lists = useResources()
|
||||
const identifierOperations = useIdentifiers(identifierBuilder, config?.publicSalt)
|
||||
|
||||
// ✅ Merge all hooks into a single `contextValue`
|
||||
const contextValue = useMemo(() => ({ auth, resources, appInfo, identifierOperations, utils }), [auth, resources, appInfo, identifierOperations]);
|
||||
const contextValue = useMemo(() => ({ auth, lists, appInfo, identifierOperations, utils }), [auth, lists, appInfo, identifierOperations]);
|
||||
return (
|
||||
<GlobalContext.Provider value={contextValue}>
|
||||
{children}
|
||||
|
@ -10,7 +10,7 @@ export const useAppInfo = (appName?: string, publicSalt?: string) => {
|
||||
|
||||
|
||||
const handleAppInfoSetup = useCallback(async (name: string, salt: string)=> {
|
||||
const appNameHashed = await hashWord(name, EnumCollisionStrength.LOW, salt)
|
||||
const appNameHashed = await hashWord(name, EnumCollisionStrength.HIGH, salt)
|
||||
setAppState({
|
||||
appName: name,
|
||||
publicSalt: salt,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useAuthStore } from "../state/auth";
|
||||
import { useAppStore } from "../state/app";
|
||||
import { buildIdentifier, buildSearchPrefix, IdentifierBuilder } from "../utils/encryption";
|
||||
import { buildIdentifier, buildSearchPrefix, EnumCollisionStrength, hashWord, IdentifierBuilder } from "../utils/encryption";
|
||||
|
||||
|
||||
export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string) => {
|
||||
@ -25,7 +25,11 @@ export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string)
|
||||
return buildSearchPrefix(appName, publicSalt, entityType, parentId, identifierBuilder)
|
||||
}, [appName, publicSalt, identifierBuilder])
|
||||
|
||||
|
||||
const createSingleIdentifier = useCallback(async ( partialIdentifier: string)=> {
|
||||
if(!partialIdentifier || !appName || !publicSalt) return null
|
||||
const appNameHashed = await hashWord(appName, EnumCollisionStrength.HIGH, publicSalt)
|
||||
return appNameHashed + '_' + partialIdentifier
|
||||
}, [appName, publicSalt])
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
@ -35,6 +39,7 @@ export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string)
|
||||
}, [stringifiedBuilder])
|
||||
return {
|
||||
buildIdentifier: buildIdentifierFunc,
|
||||
buildSearchPrefix: buildSearchPrefixFunc
|
||||
buildSearchPrefix: buildSearchPrefixFunc,
|
||||
createSingleIdentifier
|
||||
};
|
||||
};
|
||||
|
@ -1,20 +1,31 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { usePublishStore } from "../state/publishes";
|
||||
import { QortalGetMetadata } from "../types/interfaces/resources";
|
||||
import { QortalGetMetadata, QortalMetadata } from "../types/interfaces/resources";
|
||||
import { base64ToObject, retryTransaction } from "../utils/publish";
|
||||
import { useGlobal } from "../context/GlobalProvider";
|
||||
|
||||
|
||||
const STORAGE_EXPIRY_DURATION = 5 * 60 * 1000;
|
||||
interface StoredPublish {
|
||||
qortalMetadata: QortalMetadata;
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
export const usePublish = (
|
||||
maxFetchTries: number = 3,
|
||||
returnType: "PUBLIC_JSON" = "PUBLIC_JSON",
|
||||
metadata?: QortalGetMetadata
|
||||
) => {
|
||||
const {auth, appInfo} = useGlobal()
|
||||
const username = auth?.name
|
||||
const appNameHashed = appInfo?.appNameHashed
|
||||
const hasFetched = useRef(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
const publish = usePublishStore().getPublish(metadata || null);
|
||||
const setPublish = usePublishStore().setPublish;
|
||||
const [hasResource, setHasResource] = useState<boolean | null>(null)
|
||||
const getPublish = usePublishStore().getPublish;
|
||||
|
||||
const [hasResource, setHasResource] = useState<boolean | null>(null);
|
||||
const fetchRawData = useCallback(async (item: QortalGetMetadata) => {
|
||||
const url = `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`;
|
||||
const res = await fetch(url);
|
||||
@ -22,8 +33,41 @@ export const usePublish = (
|
||||
return base64ToObject(data);
|
||||
}, []);
|
||||
|
||||
const getStorageKey = useCallback(() => {
|
||||
if (!username || !appNameHashed) return null;
|
||||
return `qortal_publish_${username}_${appNameHashed}`;
|
||||
}, [username, appNameHashed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!username || !appNameHashed) return;
|
||||
|
||||
const storageKey = getStorageKey();
|
||||
if (!storageKey) return;
|
||||
|
||||
const storedData: StoredPublish[] = JSON.parse(localStorage.getItem(storageKey) || "[]");
|
||||
|
||||
if (Array.isArray(storedData) && storedData.length > 0) {
|
||||
const now = Date.now();
|
||||
const validPublishes = storedData.filter((item) => now - item.timestamp < STORAGE_EXPIRY_DURATION);
|
||||
|
||||
// ✅ Re-populate the Zustand store only with recent publishes
|
||||
validPublishes.forEach((publishData) => {
|
||||
setPublish(publishData.qortalMetadata, {
|
||||
qortalMetadata: publishData.qortalMetadata,
|
||||
data: publishData.data
|
||||
}, Date.now() - publishData.timestamp);
|
||||
});
|
||||
|
||||
// ✅ Re-store only valid (non-expired) publishes
|
||||
localStorage.setItem(storageKey, JSON.stringify(validPublishes));
|
||||
}
|
||||
}, [username, appNameHashed, getStorageKey, setPublish]);
|
||||
|
||||
const fetchPublish = useCallback(
|
||||
async (metadataProp: QortalGetMetadata, returnTypeProp: "PUBLIC_JSON" = "PUBLIC_JSON") => {
|
||||
async (
|
||||
metadataProp: QortalGetMetadata,
|
||||
returnTypeProp: "PUBLIC_JSON" = "PUBLIC_JSON"
|
||||
) => {
|
||||
let resourceExists = null;
|
||||
let resource = null;
|
||||
let error = null;
|
||||
@ -31,6 +75,31 @@ export const usePublish = (
|
||||
if (metadata) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
const hasCache = getPublish(metadataProp)
|
||||
|
||||
if(hasCache){
|
||||
if(hasCache?.qortalMetadata.size === 32){
|
||||
if(metadata){
|
||||
setHasResource(false)
|
||||
setError(null)
|
||||
|
||||
}
|
||||
return {
|
||||
resource: null,
|
||||
error: null,
|
||||
resourceExists: false
|
||||
}
|
||||
}
|
||||
if(metadata){
|
||||
setHasResource(true)
|
||||
setError(null)
|
||||
}
|
||||
return {
|
||||
resource: hasCache,
|
||||
error: null,
|
||||
resourceExists: true
|
||||
}
|
||||
}
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=${metadataProp?.service}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${metadataProp?.name}&exactmatchnames=true&offset=0&identifier=${metadataProp?.identifier}`;
|
||||
const responseMetadata = await fetch(url, {
|
||||
method: "GET",
|
||||
@ -38,14 +107,33 @@ export const usePublish = (
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!responseMetadata?.ok) return false;
|
||||
if (!responseMetadata?.ok) {
|
||||
if (metadata) {
|
||||
setError("Invalid search params");
|
||||
}
|
||||
return {
|
||||
resourceExists,
|
||||
resource,
|
||||
error: "Invalid search params",
|
||||
};
|
||||
}
|
||||
const resMetadata = await responseMetadata.json();
|
||||
if (resMetadata?.length === 0) {
|
||||
resourceExists = false;
|
||||
setHasResource(false)
|
||||
if (metadata) {
|
||||
setHasResource(false);
|
||||
}
|
||||
} else if (resMetadata[0]?.size === 32) {
|
||||
resourceExists = false;
|
||||
if (metadata) {
|
||||
setHasResource(false);
|
||||
}
|
||||
} else {
|
||||
resourceExists = true
|
||||
setHasResource(true)
|
||||
resourceExists = true;
|
||||
if (metadata) {
|
||||
setHasResource(true);
|
||||
}
|
||||
|
||||
const response = await retryTransaction(
|
||||
fetchRawData,
|
||||
[metadataProp],
|
||||
@ -91,10 +179,86 @@ export const usePublish = (
|
||||
}
|
||||
}, [metadata, returnType]);
|
||||
|
||||
|
||||
const deleteResource = useCallback(async (publish: QortalGetMetadata) => {
|
||||
const res = await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
service: publish.service,
|
||||
identifier: publish.identifier,
|
||||
base64: "RA==",
|
||||
});
|
||||
|
||||
if (res?.signature) {
|
||||
const storageKey = getStorageKey();
|
||||
if (storageKey) {
|
||||
const existingPublishes = JSON.parse(localStorage.getItem(storageKey) || "[]");
|
||||
|
||||
// Remove any previous entries for the same identifier
|
||||
const updatedPublishes = existingPublishes.filter(
|
||||
(item: StoredPublish) => item.qortalMetadata.identifier !== publish.identifier && item.qortalMetadata.service !== publish.service && item.qortalMetadata.name !== publish.name
|
||||
);
|
||||
|
||||
// Add the new one with timestamp
|
||||
updatedPublishes.push({ qortalMetadata: {
|
||||
...publish,
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
size: 32
|
||||
}, data: "RA==", timestamp: Date.now() });
|
||||
|
||||
// Save back to storage
|
||||
localStorage.setItem(storageKey, JSON.stringify(updatedPublishes));
|
||||
}
|
||||
setPublish(publish, null);
|
||||
setError(null)
|
||||
setIsLoading(false)
|
||||
setHasResource(false)
|
||||
return true;
|
||||
}
|
||||
}, [getStorageKey]);
|
||||
|
||||
|
||||
const updatePublish = useCallback(async (publish: QortalGetMetadata, data: any) => {
|
||||
setError(null)
|
||||
setIsLoading(false)
|
||||
setHasResource(true)
|
||||
setPublish(publish, {qortalMetadata: {
|
||||
...publish,
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
size: 100
|
||||
}, data});
|
||||
|
||||
const storageKey = getStorageKey();
|
||||
if (storageKey) {
|
||||
const existingPublishes = JSON.parse(localStorage.getItem(storageKey) || "[]");
|
||||
|
||||
// Remove any previous entries for the same identifier
|
||||
const updatedPublishes = existingPublishes.filter(
|
||||
(item: StoredPublish) => item.qortalMetadata.identifier !== publish.identifier && item.qortalMetadata.service !== publish.service && item.qortalMetadata.name !== publish.name
|
||||
);
|
||||
|
||||
// Add the new one with timestamp
|
||||
updatedPublishes.push({ qortalMetadata: {
|
||||
...publish,
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
size: 100
|
||||
}, data, timestamp: Date.now() });
|
||||
|
||||
// Save back to storage
|
||||
localStorage.setItem(storageKey, JSON.stringify(updatedPublishes));
|
||||
}
|
||||
|
||||
}, [getStorageKey, setPublish]);
|
||||
|
||||
if (!metadata)
|
||||
return {
|
||||
fetchPublish,
|
||||
updatePublish,
|
||||
deletePublish: deleteResource,
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
@ -102,5 +266,7 @@ export const usePublish = (
|
||||
hasResource,
|
||||
refetch: fetchPublish,
|
||||
fetchPublish,
|
||||
updatePublish,
|
||||
deletePublish: deleteResource,
|
||||
};
|
||||
};
|
||||
|
@ -259,7 +259,7 @@ export const useResources = (retryAttempts: number = 2) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteProduct = useCallback(async (resourcesToDelete: QortalMetadata[]) => {
|
||||
const deleteResource = useCallback(async (resourcesToDelete: QortalMetadata[]) => {
|
||||
|
||||
|
||||
|
||||
@ -288,10 +288,9 @@ export const useResources = (retryAttempts: number = 2) => {
|
||||
|
||||
return {
|
||||
fetchResources,
|
||||
fetchIndividualPublishJson,
|
||||
addNewResources,
|
||||
updateNewResources,
|
||||
deleteProduct,
|
||||
deleteResource,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import './index.css'
|
||||
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
|
||||
export {usePublish} from "./hooks/usePublish"
|
||||
export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay"
|
||||
export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay"
|
||||
export {QortalSearchParams} from './types/interfaces/resources'
|
||||
|
@ -1,35 +1,65 @@
|
||||
import { create } from "zustand";
|
||||
import { IdentifierBuilder } from "../utils/encryption";
|
||||
import { QortalGetMetadata } from "../types/interfaces/resources";
|
||||
import { TemporaryResource } from "../hooks/useResources";
|
||||
|
||||
|
||||
|
||||
interface PublishState {
|
||||
publishes: Record<string, TemporaryResource> ;
|
||||
|
||||
getPublish: (qortalGetMetadata: QortalGetMetadata | null) => TemporaryResource | null;
|
||||
setPublish: (qortalGetMetadata: QortalGetMetadata, data: TemporaryResource) => void;
|
||||
interface PublishCache {
|
||||
data: TemporaryResource | null;
|
||||
expiry: number;
|
||||
}
|
||||
|
||||
interface PublishState {
|
||||
publishes: Record<string, PublishCache>;
|
||||
|
||||
getPublish: (qortalGetMetadata: QortalGetMetadata | null, ignoreExpire?: boolean) => TemporaryResource | null;
|
||||
setPublish: (qortalGetMetadata: QortalGetMetadata, data: TemporaryResource | null, customExpiry?: number) => void;
|
||||
clearExpiredPublishes: () => void;
|
||||
publishExpiryDuration: number; // Default expiry duration
|
||||
}
|
||||
|
||||
// ✅ Typed Zustand Store
|
||||
export const usePublishStore = create<PublishState>((set, get) => ({
|
||||
publishes: {},
|
||||
getPublish: (qortalGetMetadata) => {
|
||||
if(qortalGetMetadata === null) return null
|
||||
const cache = get().publishes[`${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`];
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
return null;
|
||||
},
|
||||
setPublish: (qortalGetMetadata, data) =>
|
||||
publishes: {},
|
||||
publishExpiryDuration: 5 * 60 * 1000, // Default expiry: 5 minutes
|
||||
|
||||
getPublish: (qortalGetMetadata, ignoreExpire = false) => {
|
||||
if (!qortalGetMetadata) return null;
|
||||
|
||||
const id = `${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`;
|
||||
const cache = get().publishes[id];
|
||||
|
||||
if (cache) {
|
||||
if (cache.expiry > Date.now() || ignoreExpire) {
|
||||
if(cache?.data?.qortalMetadata?.size === 32) return null
|
||||
return cache.data;
|
||||
} else {
|
||||
set((state) => {
|
||||
return {
|
||||
publishes: {
|
||||
...state.publishes,
|
||||
[`${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`]: data,
|
||||
},
|
||||
};
|
||||
}),
|
||||
const updatedPublishes = { ...state.publishes };
|
||||
delete updatedPublishes[id];
|
||||
return { publishes: updatedPublishes };
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
setPublish: (qortalGetMetadata, data, customExpiry) => {
|
||||
const id = `${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`;
|
||||
const expiry = Date.now() + (customExpiry || get().publishExpiryDuration);
|
||||
|
||||
set((state) => ({
|
||||
publishes: {
|
||||
...state.publishes,
|
||||
[id]: { data, expiry },
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
clearExpiredPublishes: () => {
|
||||
set((state) => {
|
||||
const now = Date.now();
|
||||
const updatedPublishes = Object.fromEntries(
|
||||
Object.entries(state.publishes).filter(([_, cache]) => cache.expiry > now)
|
||||
);
|
||||
return { publishes: updatedPublishes };
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
Loading…
x
Reference in New Issue
Block a user