@four-leaf-studios/rl-socket-hook
Version:
A tiny React wrapper around a Rocket League WebSocket plugin (`ws://localhost:49122`). It provides:
88 lines (85 loc) • 3.46 kB
JavaScript
import { jsx } from 'react/jsx-runtime';
import { createContext, useRef, useEffect, useMemo, useContext, useSyncExternalStore, useCallback } from 'react';
import { useRocketLeagueSocket } from './useRocketLeagueSocket.js';
const RLContext = createContext(null);
const RLProvider = ({ url = "ws://localhost:49122", children }) => {
// 1) pull in every event & helpers from the single socket hook
const { events: allEvents, send, readyState, error, } = useRocketLeagueSocket(url);
// 2) Use a loose-typed ref to avoid union-vs-intersection complaints
const dataRef = useRef({});
const subsRef = useRef({});
// 3) Sync incoming allEvents into dataRef and notify subscribers
useEffect(() => {
// Force evt to be keyof PayloadStorage for type-narrowing
const keys = Object.keys(allEvents);
keys.forEach((evt) => {
const payload = allEvents[evt];
// write into our record (string index)
dataRef.current[evt] = payload;
// notify anyone subscribed to this key
(subsRef.current[evt] || []).forEach((cb) => cb());
});
}, [allEvents]);
// 4) Build the same Store API, casting when reading back
const store = useMemo(() => ({
getSnapshot: (eventName) =>
// cast from any back to the correct PayloadStorage type
dataRef.current[eventName],
subscribe: (eventName, callback) => {
const arr = (subsRef.current[eventName] ??= []);
arr.push(callback);
return () => {
subsRef.current[eventName] = arr.filter((c) => c !== callback);
};
},
send,
readyState,
error,
}), [send, readyState, error]);
return jsx(RLContext.Provider, { value: store, children: children });
};
/**
* Subscribe to the full payload of an event.
*/
function useEvent(eventName) {
const store = useContext(RLContext);
if (!store)
throw new Error("useEvent must be inside <RLProvider>");
return useSyncExternalStore((cb) => store.subscribe(eventName, cb), () => store.getSnapshot(eventName));
}
/** Tiny deep-equal for selector comparisons */
function deepEqual(a, b) {
if (Object.is(a, b))
return true;
if (typeof a !== "object" ||
a === null ||
typeof b !== "object" ||
b === null) {
return false;
}
const aKeys = Object.keys(a), bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length)
return false;
return aKeys.every((key) => deepEqual(a[key], b[key]));
}
/**
* Subscribe to a selected slice of the event payload. Re-renders only on actual changes.
*/
function useEventSelector(eventName, selector, isEqual = deepEqual) {
const store = useContext(RLContext);
const lastRef = useRef({});
const subscribe = useCallback((cb) => store.subscribe(eventName, cb), [eventName, store]);
const getSnapshot = useCallback(() => {
const raw = store.getSnapshot(eventName);
const next = selector(raw);
if (lastRef.current.val !== undefined &&
isEqual(next, lastRef.current.val)) {
return lastRef.current.val;
}
lastRef.current.val = next;
return next;
}, [eventName, selector, store, isEqual]);
return useSyncExternalStore(subscribe, getSnapshot);
}
export { RLContext, RLProvider, useEvent, useEventSelector };
//# sourceMappingURL=RLProvider.js.map