updated usePublish

This commit is contained in:
PhilReact 2025-03-20 21:18:34 +02:00
parent f83ce67072
commit 770080b942
9 changed files with 267 additions and 64 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.7", "version": "1.0.9",
"description": "Qortal's core React library with global state, UI components, and utilities", "description": "Qortal's core React library with global state, UI components, and utilities",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.mjs", "module": "dist/index.mjs",

View File

@ -103,9 +103,8 @@ export const MemorizedComponent = ({
entityParams, entityParams,
retryAttempts = 2 retryAttempts = 2
}: PropsResourceListDisplay) => { }: PropsResourceListDisplay) => {
const { fetchResources } = useResources(retryAttempts);
const { filterOutDeletedResources } = useCacheStore(); const { filterOutDeletedResources } = useCacheStore();
const {identifierOperations} = useGlobal() const {identifierOperations, lists} = useGlobal()
const deletedResources = useCacheStore().deletedResources const deletedResources = useCacheStore().deletedResources
const memoizedParams = useMemo(() => JSON.stringify(search), [search]); const memoizedParams = useMemo(() => JSON.stringify(search), [search]);
const addList = useListStore().addList const addList = useListStore().addList
@ -119,9 +118,7 @@ export const MemorizedComponent = ({
const [isLoadingMore, setIsLoadingMore] = useState(false) const [isLoadingMore, setIsLoadingMore] = useState(false)
const initialized = useRef(false) const initialized = useRef(false)
const [generatedIdentifier, setGeneratedIdentifier] = useState("") const [generatedIdentifier, setGeneratedIdentifier] = useState("")
const prevGeneratedIdentifierRef = useRef('')
const stringifiedEntityParams = useMemo(()=> { const stringifiedEntityParams = useMemo(()=> {
@ -157,6 +154,7 @@ export const MemorizedComponent = ({
try { try {
if(!generatedIdentifier) return if(!generatedIdentifier) return
await new Promise((res)=> { await new Promise((res)=> {
setTimeout(() => { setTimeout(() => {
res(null) res(null)
@ -165,7 +163,7 @@ export const MemorizedComponent = ({
setIsLoading(true); setIsLoading(true);
const parsedParams = {...(JSON.parse(memoizedParams))}; const parsedParams = {...(JSON.parse(memoizedParams))};
parsedParams.identifier = generatedIdentifier 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
@ -176,16 +174,18 @@ export const MemorizedComponent = ({
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [memoizedParams, fetchResources, generatedIdentifier]); // Added dependencies for re-fetching }, [memoizedParams, lists.fetchResources, generatedIdentifier]); // Added dependencies for re-fetching
useEffect(() => { useEffect(() => {
if(initialized.current || !generatedIdentifier) return if(!generatedIdentifier) return
initialized.current = true
if(!isListExpired) { if(!isListExpired && !initialized.current) {
setIsLoading(false) setIsLoading(false)
return initialized.current = true
} return
}
sessionStorage.removeItem(`scroll-position-${listName}`); sessionStorage.removeItem(`scroll-position-${listName}`);
prevGeneratedIdentifierRef.current = generatedIdentifier
getResourceList(); getResourceList();
}, [getResourceList, isListExpired, generatedIdentifier]); // Runs when dependencies change }, [getResourceList, isListExpired, generatedIdentifier]); // Runs when dependencies change
@ -221,7 +221,7 @@ export const MemorizedComponent = ({
if(displayLimit){ if(displayLimit){
parsedParams.limit = 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 || []) addItems(listName, responseData || [])
} catch (error) { } catch (error) {
console.error("Failed to fetch resources:", error); console.error("Failed to fetch resources:", error);

View File

@ -5,17 +5,19 @@ import { useAppInfo } from "../hooks/useAppInfo";
import { IdentifierBuilder } from "../utils/encryption"; import { IdentifierBuilder } from "../utils/encryption";
import { useIdentifiers } from "../hooks/useIdentifiers"; import { useIdentifiers } from "../hooks/useIdentifiers";
import { objectToBase64 } from "../utils/base64"; import { objectToBase64 } from "../utils/base64";
import { base64ToObject } from "../utils/publish";
const utils = { const utils = {
objectToBase64 objectToBase64,
base64ToObject
} }
// ✅ Define Global Context Type // ✅ Define Global Context Type
interface GlobalContextType { interface GlobalContextType {
auth: ReturnType<typeof useAuth>; auth: ReturnType<typeof useAuth>;
resources: ReturnType<typeof useResources>; lists: ReturnType<typeof useResources>;
appInfo: ReturnType<typeof useAppInfo>; appInfo: ReturnType<typeof useAppInfo>;
identifierOperations: ReturnType<typeof useIdentifiers> identifierOperations: ReturnType<typeof useIdentifiers>
utils: typeof utils utils: typeof utils
@ -44,11 +46,11 @@ export const GlobalProvider = ({ children, config, identifierBuilder }: GlobalPr
// ✅ 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 appInfo = useAppInfo(config?.appName, config?.publicSalt) const appInfo = useAppInfo(config?.appName, config?.publicSalt)
const resources = useResources() const lists = useResources()
const identifierOperations = useIdentifiers(identifierBuilder, config?.publicSalt) const identifierOperations = useIdentifiers(identifierBuilder, config?.publicSalt)
// ✅ Merge all hooks into a single `contextValue` // ✅ 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 ( return (
<GlobalContext.Provider value={contextValue}> <GlobalContext.Provider value={contextValue}>
{children} {children}

View File

@ -10,7 +10,7 @@ export const useAppInfo = (appName?: string, publicSalt?: string) => {
const handleAppInfoSetup = useCallback(async (name: string, salt: 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({ setAppState({
appName: name, appName: name,
publicSalt: salt, publicSalt: salt,

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuthStore } from "../state/auth"; import { useAuthStore } from "../state/auth";
import { useAppStore } from "../state/app"; 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) => { 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) return buildSearchPrefix(appName, publicSalt, entityType, parentId, identifierBuilder)
}, [appName, publicSalt, 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(()=> { useEffect(()=> {
@ -35,6 +39,7 @@ export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string)
}, [stringifiedBuilder]) }, [stringifiedBuilder])
return { return {
buildIdentifier: buildIdentifierFunc, buildIdentifier: buildIdentifierFunc,
buildSearchPrefix: buildSearchPrefixFunc buildSearchPrefix: buildSearchPrefixFunc,
createSingleIdentifier
}; };
}; };

View File

@ -1,20 +1,31 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { usePublishStore } from "../state/publishes"; 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 { 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 = ( export const usePublish = (
maxFetchTries: number = 3, maxFetchTries: number = 3,
returnType: "PUBLIC_JSON" = "PUBLIC_JSON", returnType: "PUBLIC_JSON" = "PUBLIC_JSON",
metadata?: QortalGetMetadata metadata?: QortalGetMetadata
) => { ) => {
const {auth, appInfo} = useGlobal()
const username = auth?.name
const appNameHashed = appInfo?.appNameHashed
const hasFetched = useRef(false); const hasFetched = useRef(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState<null | string>(null);
const publish = usePublishStore().getPublish(metadata || null); const publish = usePublishStore().getPublish(metadata || null);
const setPublish = usePublishStore().setPublish; 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 fetchRawData = useCallback(async (item: QortalGetMetadata) => {
const url = `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`; const url = `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`;
const res = await fetch(url); const res = await fetch(url);
@ -22,8 +33,41 @@ export const usePublish = (
return base64ToObject(data); 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( const fetchPublish = useCallback(
async (metadataProp: QortalGetMetadata, returnTypeProp: "PUBLIC_JSON" = "PUBLIC_JSON") => { async (
metadataProp: QortalGetMetadata,
returnTypeProp: "PUBLIC_JSON" = "PUBLIC_JSON"
) => {
let resourceExists = null; let resourceExists = null;
let resource = null; let resource = null;
let error = null; let error = null;
@ -31,6 +75,31 @@ export const usePublish = (
if (metadata) { if (metadata) {
setIsLoading(true); 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 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, { const responseMetadata = await fetch(url, {
method: "GET", method: "GET",
@ -38,14 +107,33 @@ export const usePublish = (
"Content-Type": "application/json", "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(); const resMetadata = await responseMetadata.json();
if (resMetadata?.length === 0) { if (resMetadata?.length === 0) {
resourceExists = false; resourceExists = false;
setHasResource(false) if (metadata) {
setHasResource(false);
}
} else if (resMetadata[0]?.size === 32) {
resourceExists = false;
if (metadata) {
setHasResource(false);
}
} else { } else {
resourceExists = true resourceExists = true;
setHasResource(true) if (metadata) {
setHasResource(true);
}
const response = await retryTransaction( const response = await retryTransaction(
fetchRawData, fetchRawData,
[metadataProp], [metadataProp],
@ -91,10 +179,86 @@ export const usePublish = (
} }
}, [metadata, returnType]); }, [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) if (!metadata)
return { return {
fetchPublish, fetchPublish,
updatePublish,
deletePublish: deleteResource,
}; };
return { return {
isLoading, isLoading,
error, error,
@ -102,5 +266,7 @@ export const usePublish = (
hasResource, hasResource,
refetch: fetchPublish, refetch: fetchPublish,
fetchPublish, fetchPublish,
updatePublish,
deletePublish: deleteResource,
}; };
}; };

View File

@ -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 { return {
fetchResources, fetchResources,
fetchIndividualPublishJson,
addNewResources, addNewResources,
updateNewResources, updateNewResources,
deleteProduct, deleteResource,
}; };
}; };

View File

@ -2,3 +2,4 @@ import './index.css'
export { GlobalProvider, useGlobal } from "./context/GlobalProvider"; export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
export {usePublish} from "./hooks/usePublish" export {usePublish} from "./hooks/usePublish"
export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay" export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay"
export {QortalSearchParams} from './types/interfaces/resources'

View File

@ -1,35 +1,65 @@
import { create } from "zustand"; import { create } from "zustand";
import { IdentifierBuilder } from "../utils/encryption";
import { QortalGetMetadata } from "../types/interfaces/resources"; import { QortalGetMetadata } from "../types/interfaces/resources";
import { TemporaryResource } from "../hooks/useResources"; import { TemporaryResource } from "../hooks/useResources";
interface PublishCache {
data: TemporaryResource | null;
interface PublishState { expiry: number;
publishes: Record<string, TemporaryResource> ; }
getPublish: (qortalGetMetadata: QortalGetMetadata | null) => TemporaryResource | null; interface PublishState {
setPublish: (qortalGetMetadata: QortalGetMetadata, data: TemporaryResource) => void; 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) => ({ export const usePublishStore = create<PublishState>((set, get) => ({
publishes: {}, publishes: {},
getPublish: (qortalGetMetadata) => { publishExpiryDuration: 5 * 60 * 1000, // Default expiry: 5 minutes
if(qortalGetMetadata === null) return null
const cache = get().publishes[`${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`]; getPublish: (qortalGetMetadata, ignoreExpire = false) => {
if (cache) { if (!qortalGetMetadata) return null;
return cache
} const id = `${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`;
return null; const cache = get().publishes[id];
},
setPublish: (qortalGetMetadata, data) => if (cache) {
if (cache.expiry > Date.now() || ignoreExpire) {
if(cache?.data?.qortalMetadata?.size === 32) return null
return cache.data;
} else {
set((state) => { set((state) => {
return { const updatedPublishes = { ...state.publishes };
publishes: { delete updatedPublishes[id];
...state.publishes, return { publishes: updatedPublishes };
[`${qortalGetMetadata.service}-${qortalGetMetadata.name}-${qortalGetMetadata.identifier}`]: data, });
}, }
}; }
}), 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 };
});
},
})); }));