UNPKG

@firtoz/socka

Version:

Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects

220 lines (217 loc) • 6.46 kB
import { SockaSession } from '../chunk-THFUHQJ3.js'; import '../chunk-YMT4HAH7.js'; import '../chunk-IFIGKR3W.js'; import { createContext, useRef, useState, useEffect, useMemo, useContext } from 'react'; import { jsx } from 'react/jsx-runtime'; function useSocka(options, deps) { const { onOpen, onClose, onReconnecting, onReconnected, ...restOptions } = options; const onOpenRef = useRef(onOpen); onOpenRef.current = onOpen; const onCloseRef = useRef(onClose); onCloseRef.current = onClose; const onReconnectingRef = useRef(onReconnecting); onReconnectingRef.current = onReconnecting; const onReconnectedRef = useRef(onReconnected); onReconnectedRef.current = onReconnected; const [ready, setReady] = useState(false); const [status, setStatus] = useState( () => options.autoConnect === false ? "idle" : "connecting" ); const [reconnectAttempt, setReconnectAttempt] = useState(0); const sessionRef = useRef(null); useEffect(() => { let cancelled = false; setReady(false); setReconnectAttempt(0); const session = new SockaSession({ ...restOptions, onOpen: (event) => { if (!cancelled) { setReady(true); } onOpenRef.current?.(event); }, onClose: (event) => { if (!cancelled) { setReady(false); } onCloseRef.current?.(event); }, onReconnecting: (info) => { setReconnectAttempt(info.attempt); onReconnectingRef.current?.(info); }, onReconnected: (info) => { setReconnectAttempt(0); onReconnectedRef.current?.(info); } }); const unsubStatus = session.onStatusChange((s) => { if (!cancelled) setStatus(s); }); sessionRef.current = session; void session.client.connect().then( () => { if (!cancelled) { setReady(true); } }, () => { } ); return () => { cancelled = true; unsubStatus(); sessionRef.current = null; session.rejectAllPending(new Error("WebSocket closed")); session.close(); }; }, deps); return { ready, sessionRef, status, reconnecting: status === "reconnecting", reconnectAttempt }; } // src/react/useSockaSession.ts function createSockaSendProxyFromSession(contract, sessionRef) { const proxy = {}; for (const name of Object.keys(contract.calls)) { proxy[name] = (...args) => { const session = sessionRef.current; if (!session) { return Promise.reject( new Error("socka: session ref is null; cannot send") ); } const fn = session.send[name]; return fn.apply(session.send, args); }; } return proxy; } function useSockaSession(contract, options, deps) { const { pushHandlers, ...sockaOpts } = options; const { ready, sessionRef, status, reconnecting, reconnectAttempt } = useSocka( { ...sockaOpts, contract, pushHandlers }, deps ); const send = useMemo( () => createSockaSendProxyFromSession(contract, sessionRef), [contract, sessionRef] ); return { ready, send, sessionRef, status, reconnecting, reconnectAttempt }; } function useSockaPresence(sessionRef, ready, options, deps) { const [users, setUsers] = useState([]); const [selfUserId, setSelfUserId] = useState(); const [loading, setLoading] = useState(true); const optionsRef = useRef(options); optionsRef.current = options; useEffect(() => { if (!ready) { setUsers([]); setSelfUserId(void 0); setLoading(true); return; } const s = sessionRef.current; if (!s) { setLoading(false); return; } const o = optionsRef.current; let cancelled = false; void (async () => { setLoading(true); try { const snap = await o.snapshot(); if (cancelled) return; setSelfUserId(snap.selfUserId); setUsers(o.sortUsers ? [...snap.users].sort(o.sortUsers) : snap.users); } finally { if (!cancelled) setLoading(false); } })(); const onJoin = (p) => { const cur = optionsRef.current; const u = cur.mapJoinUser(p); setUsers((prev) => { const next = prev.filter((x) => x.userId !== u.userId); const merged = [...next, u]; return cur.sortUsers ? merged.sort(cur.sortUsers) : merged; }); }; const onLeave = (p) => { const id = optionsRef.current.mapLeaveUserId(p); setUsers((prev) => prev.filter((x) => x.userId !== id)); }; s.subscribe.on(o.joinPush, onJoin); s.subscribe.on(o.leavePush, onLeave); return () => { cancelled = true; s.subscribe.off(o.joinPush, onJoin); s.subscribe.off(o.leavePush, onLeave); }; }, [ready, sessionRef, ...deps]); return { users, selfUserId, loading }; } var SockaSessionContext = createContext(null); function contextMatchesContract(ctx, contract) { return ctx.contract === contract; } function SockaSessionProvider(props) { const { contract, deps, children, ...sessionOptions } = props; const value = useSockaSession(contract, sessionOptions, deps); const merged = { contract, ready: value.ready, sessionRef: value.sessionRef, status: value.status, reconnecting: value.reconnecting, reconnectAttempt: value.reconnectAttempt }; return /* @__PURE__ */ jsx(SockaSessionContext.Provider, { value: merged, children }); } SockaSessionProvider.displayName = "SockaSessionProvider"; function useSockaSessionContext(contract) { const ctx = useContext(SockaSessionContext); if (ctx === null) { throw new Error( "useSockaSessionContext must be used within a SockaSessionProvider" ); } if (!contextMatchesContract(ctx, contract)) { throw new Error( "useSockaSessionContext: `contract` must be the same reference as SockaSessionProvider's `contract`" ); } const send = useMemo( () => createSockaSendProxyFromSession(contract, ctx.sessionRef), [contract, ctx.sessionRef] ); return { ready: ctx.ready, send, sessionRef: ctx.sessionRef, status: ctx.status, reconnecting: ctx.reconnecting, reconnectAttempt: ctx.reconnectAttempt }; } export { SockaSessionProvider, createSockaSendProxyFromSession, useSocka, useSockaPresence, useSockaSession, useSockaSessionContext }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map