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",
"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",

View File

@ -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);

View File

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

View File

@ -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,

View File

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

View File

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

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

View File

@ -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'

View File

@ -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 };
});
},
}));