socket.io-react-hooks-advanced
Version:
A modular and extensible React + Socket.IO hook library designed for real-world applications. Supports namespaced sockets, reconnection strategies, offline queues, latency monitoring, middleware, encryption, and more.
204 lines (203 loc) • 7.98 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
import { io } from "socket.io-client";
import { SocketContext } from "./SocketContext";
import { MiddlewareManager } from "../middleware";
import { clearQueue, loadQueue, saveQueue } from "../utils/storageQueue";
export const SocketProvider = ({ url, getToken, onUnauthorized, maxRetries = 5, initialDelayMs = 1000, maxDelayMs = 30000, backoffFactor = 2, onRetry, onGiveUp, maxQueueSize = 100, onQueueOverflow, children, persistQueue, queueKey, queueTTL, extraHeaders }) => {
const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
const [authToken, setAuthToken] = useState();
const [latency, setLatency] = useState();
const latencyRef = useRef([]);
const latencyCallbacks = useRef([]);
const pingInterval = useRef(null);
const retryCountRef = useRef(0);
const retryTimeoutRef = useRef(null);
const manualDisconnectRef = useRef(false);
const queueRef = useRef([]);
const flushCountRef = useRef(0);
const middlewareManager = useRef(new MiddlewareManager());
const fetchToken = useCallback(async () => {
if (!getToken)
return;
const token = await getToken();
setAuthToken(token);
}, [getToken]);
useEffect(() => {
fetchToken();
}, [fetchToken]);
useEffect(() => () => {
if (retryTimeoutRef.current)
clearTimeout(retryTimeoutRef.current);
}, []);
useEffect(() => {
manualDisconnectRef.current = false;
const socketInstance = io(url, {
auth: {
...(authToken ? { token: authToken } : {}),
...(extraHeaders || {}),
},
transports: ["websocket", "polling"],
autoConnect: false,
});
setSocket(socketInstance);
// Override emit/on with middleware and offline queue support
const rawEmit = socketInstance.emit.bind(socketInstance);
socketInstance.emit = (event, data, ...args) => {
const ack = typeof args[0] === "function" ? args[0] : undefined;
middlewareManager.current.runEmit(event, data, (ev, dt) => {
if (socketInstance.connected) {
rawEmit(ev, dt, ack);
}
else {
if (queueRef.current.length >= maxQueueSize) {
const dropped = queueRef.current.shift();
onQueueOverflow === null || onQueueOverflow === void 0 ? void 0 : onQueueOverflow(dropped);
}
queueRef.current.push({ event: ev, data: dt, ack, timestamp: Date.now() });
}
});
return socketInstance;
};
const rawOn = socketInstance.on.bind(socketInstance);
socketInstance.on = (event, handler) => {
const wrappedHandler = (data) => {
middlewareManager.current.runOn(event, data, handler);
};
return rawOn(event, wrappedHandler);
};
const connectWithRetry = () => {
if (manualDisconnectRef.current)
return;
socketInstance.connect();
};
socketInstance.on("connect", () => {
setConnected(true);
retryCountRef.current = 0;
pingInterval.current = setInterval(() => {
const start = Date.now();
socketInstance.emit("ping-latency", () => {
const ms = Date.now() - start;
setLatency(ms);
latencyRef.current.push(ms);
latencyCallbacks.current.forEach(cb => cb(ms));
});
}, 5000); // every 5 sec
// Flush offline queue
while (queueRef.current.length > 0) {
const { event, data, ack } = queueRef.current.shift();
socketInstance.emit(event, data, ack);
}
flushCountRef.current++;
clearQueue(queueKey); // clear persisted queue after flush
});
socketInstance.on("disconnect", (reason) => {
setConnected(false);
if (manualDisconnectRef.current)
return;
if (retryCountRef.current >= maxRetries) {
onGiveUp === null || onGiveUp === void 0 ? void 0 : onGiveUp();
return;
}
const delay = Math.min(initialDelayMs * Math.pow(backoffFactor, retryCountRef.current), maxDelayMs);
onRetry === null || onRetry === void 0 ? void 0 : onRetry(retryCountRef.current + 1);
retryTimeoutRef.current = setTimeout(() => {
retryCountRef.current++;
socketInstance.connect();
}, delay);
});
socketInstance.on("unauthorized", async () => {
if (!onUnauthorized) {
socketInstance.disconnect();
return;
}
try {
const newToken = await onUnauthorized();
setAuthToken(newToken);
}
catch (err) {
socketInstance.disconnect();
}
});
if (persistQueue) {
const restored = loadQueue(queueKey, queueTTL);
queueRef.current.push(...restored);
}
connectWithRetry();
return () => {
manualDisconnectRef.current = true;
if (retryTimeoutRef.current)
clearTimeout(retryTimeoutRef.current);
if (pingInterval.current)
clearInterval(pingInterval.current);
socketInstance.disconnect();
};
}, [
url,
authToken,
maxRetries,
initialDelayMs,
maxDelayMs,
backoffFactor,
onRetry,
onGiveUp,
onUnauthorized,
maxQueueSize,
onQueueOverflow,
]);
const addEmitMiddleware = useCallback((middleware) => {
const id = crypto.randomUUID();
middlewareManager.current.addMiddleware({ id, emit: middleware });
return id;
}, []);
const addOnMiddleware = useCallback((middleware) => {
const id = crypto.randomUUID();
middlewareManager.current.addMiddleware({ id, on: middleware });
return id;
}, []);
const removeMiddleware = useCallback((id) => {
middlewareManager.current.removeMiddleware(id);
}, []);
const emitWithQueue = useCallback((event, data, ack) => {
if (socket === null || socket === void 0 ? void 0 : socket.connected) {
socket.emit(event, data, ack);
}
else {
if (queueRef.current.length >= maxQueueSize) {
const dropped = queueRef.current.shift();
onQueueOverflow === null || onQueueOverflow === void 0 ? void 0 : onQueueOverflow(dropped);
}
const queued = { event, data, ack, timestamp: Date.now() };
queueRef.current.push(queued);
if (persistQueue)
saveQueue(queueRef.current, queueKey);
}
}, [socket, maxQueueSize, onQueueOverflow]);
const onLatencyUpdate = (cb) => {
latencyCallbacks.current.push(cb);
};
const value = useMemo(() => ({
socket,
connected,
setAuthToken,
addEmitMiddleware,
addOnMiddleware,
removeMiddleware,
emitWithQueue,
queueLength: queueRef.current.length,
flushCount: flushCountRef.current,
latency,
latencyHistory: latencyRef.current,
onLatencyUpdate,
}), [
socket,
connected,
setAuthToken,
addEmitMiddleware,
addOnMiddleware,
removeMiddleware,
emitWithQueue,
]);
return (_jsx(SocketContext.Provider, { value: value, children: children }));
};