import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { MobulaClient } from '@mobula/sdk';
interface PulseToken {
address?: string;
chainId?: string;
name?: string;
symbol?: string;
logo?: string;
marketCap?: number;
holders_count?: number;
price_change_24h?: number;
created_at?: string;
[key: string]: unknown;
}
type ViewName = 'new' | 'bonding' | 'bonded';
export function usePulseStream(apiKey: string, chainIds: string[] = ['solana:solana']) {
const clientRef = useRef<MobulaClient | null>(null);
const subIdRef = useRef<string | null>(null);
const [tokens, setTokens] = useState<Record<ViewName, PulseToken[]>>({
new: [], bonding: [], bonded: [],
});
const [loading, setLoading] = useState(true);
const getClient = useCallback(() => {
if (!clientRef.current) {
clientRef.current = new MobulaClient({
restUrl: 'https://pulse-v2-api.mobula.io',
apiKey,
});
}
return clientRef.current;
}, [apiKey]);
const payload = useMemo(() => ({
assetMode: true,
compressed: false,
views: [
{ name: 'new', model: 'new' as const, chainId: chainIds, limit: 50 },
{ name: 'bonding', model: 'bonding' as const, chainId: chainIds, limit: 50 },
{ name: 'bonded', model: 'bonded' as const, chainId: chainIds, limit: 50 },
],
}), [chainIds]);
useEffect(() => {
const client = getClient();
// 1. REST for fast initial load
client.fetchPulseV2(payload).then((data) => {
setTokens({
new: Array.isArray(data.new?.data) ? data.new.data : [],
bonding: Array.isArray(data.bonding?.data) ? data.bonding.data : [],
bonded: Array.isArray(data.bonded?.data) ? data.bonded.data : [],
});
setLoading(false);
});
// 2. WebSocket for live updates
subIdRef.current = client.streams.subscribe('pulse-v2', payload, (msg: any) => {
if (msg.type === 'init') {
setTokens({
new: Array.isArray(msg.payload.new?.data) ? msg.payload.new.data : [],
bonding: Array.isArray(msg.payload.bonding?.data) ? msg.payload.bonding.data : [],
bonded: Array.isArray(msg.payload.bonded?.data) ? msg.payload.bonded.data : [],
});
setLoading(false);
}
if (msg.type === 'update-token' || msg.type === 'new-token') {
const { viewName, token } = msg.payload;
setTokens((prev) => {
const view = prev[viewName as ViewName] || [];
const idx = view.findIndex((t) => t.address === token.address);
const updated = idx >= 0
? view.map((t, i) => (i === idx ? { ...t, ...token } : t))
: [token, ...view].slice(0, 50);
return { ...prev, [viewName]: updated };
});
}
});
return () => {
if (subIdRef.current) {
client.streams.unsubscribe('pulse-v2', subIdRef.current);
subIdRef.current = null;
}
};
}, [getClient, payload]);
return { tokens, loading };
}