import { createContext, createSignal, onCleanup, useContext, type Component, type JSX } from 'solid-js';
import { SPACEDUST_SERVICE } from './api';
export interface LiveEvent {
receivedAt: string;
source: string;
sourceRecord: string;
subject: string;
}
interface LiveEventsContextValue {
events: () => LiveEvent[];
unread: () => number;
subscribe: (config: { subjects: string[]; sources: string[] } | null) => void;
clear: () => void;
markRead: () => void;
}
const LiveEventsContext = createContext<LiveEventsContextValue>();
export const LiveEventsProvider: Component<{ children: JSX.Element }> = (props) => {
const [events, setEvents] = createSignal<LiveEvent[]>([]);
const [unread, setUnread] = createSignal(0);
let socket: WebSocket | null = null;
let currentKey = '';
const clear = () => {
currentKey = '';
socket?.close();
socket = null;
setEvents([]);
setUnread(0);
};
const subscribe = (config: { subjects: string[]; sources: string[] } | null) => {
if (!config || config.subjects.length === 0 || config.sources.length === 0) {
clear();
return;
}
const key = JSON.stringify(config);
if (key === currentKey) {
return;
}
currentKey = key;
socket?.close();
setEvents([]);
setUnread(0);
const url = new URL(SPACEDUST_SERVICE);
for (const subject of config.subjects) {
url.searchParams.append('wantedSubjects', subject);
}
for (const source of config.sources) {
url.searchParams.append('wantedSources', source);
}
url.searchParams.set('instant', 'true');
socket = new WebSocket(url);
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as {
kind?: string;
link?: {
source: string;
source_record: string;
subject: string;
};
};
if (data.kind !== 'link' || !data.link) {
return;
}
const link = data.link;
setEvents((prev) =>
[
{
receivedAt: new Date().toISOString(),
source: link.source,
sourceRecord: link.source_record,
subject: link.subject,
},
...prev,
].slice(0, 20),
);
setUnread((count) => count + 1);
} catch {
// ignore malformed messages
}
};
};
const markRead = () => {
setUnread(0);
};
onCleanup(() => {
socket?.close();
});
return (
<LiveEventsContext.Provider value={{ events, unread, subscribe, clear, markRead }}>
{props.children}
</LiveEventsContext.Provider>
);
};
export const useLiveEvents = (): LiveEventsContextValue => {
const context = useContext(LiveEventsContext);
if (!context) {
throw new Error('useLiveEvents must be used within its provider');
}
return context;
};