@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
JavaScript
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