// Copyright 2017-2026 @pezkuwi/react-hooks authors & contributors // SPDX-License-Identifier: Apache-2.0 import '@pezkuwi/x-textencoder/shim'; import '@pezkuwi/x-textdecoder/shim'; import type { CallOptions } from './types.js'; import { useEffect, useMemo, useState } from 'react'; import { useIsMountedRef } from './useIsMountedRef.js'; interface Options extends CallOptions { transform?: (value: any) => T } export function normalizeMetadataLink (link?: string): string { if (!link) { return ''; } else if (link.toLowerCase().startsWith('http')) { return link; } // handle V0 CID const matchCidV0 = link.match(/Qm[A-Za-z0-9]{44}(?![A-Za-z0-9])/); if (matchCidV0 !== null) { return matchCidV0[0]; } // handle V1 CID const matchCidV1 = link.match(/[a-z0-9]{59}(?![A-Za-z0-9])/); if (matchCidV1 !== null) { return matchCidV1[0]; } return ''; } const cache = new Map(); async function fetchMetadata (metadataLinks: string[]): Promise> { const result = new Map(); const promises = metadataLinks.map((metadataLink) => { if (cache.has(metadataLink)) { result.set(metadataLink, cache.get(metadataLink)); return Promise.resolve(); } const fetchLink = metadataLink.startsWith('http') ? metadataLink : `https://ipfs.io/ipfs/${metadataLink}`; return fetch(fetchLink) .then(async (res) => { const response = res.status >= 200 && res.status < 300 ? await res.text() : null; cache.set(metadataLink, response); result.set(metadataLink, response); }); }); await Promise.allSettled(promises); return result; } function postProcessData (fetchedMetadata: Map, { transform }: Options = {}) { if (!transform) { return fetchedMetadata; } for (const [key, value] of fetchedMetadata.entries()) { fetchedMetadata.set(key, transform(value)); } return fetchedMetadata; } // FIXME This is generic, we cannot really use createNamedHook export function useMetadataFetch (rawLinks: string[] | undefined, options?: Options): Map | undefined { const mountedRef = useIsMountedRef(); const [value, setValue] = useState | undefined>(); const metadataLinks = useMemo(() => { if (!rawLinks) { return undefined; } return rawLinks .map((hash) => normalizeMetadataLink(hash)) .filter((hash) => !!hash); }, [rawLinks]); useEffect((): void => { if (mountedRef.current && metadataLinks) { fetchMetadata(metadataLinks) .then((fetchedMetadata) => setValue(postProcessData(fetchedMetadata, options))) // eslint-disable-next-line @typescript-eslint/no-empty-function .catch(() => { }); } }, [metadataLinks, options, mountedRef]); return value; }