UNPKG

@daveyplate/supabase-swr-entities

Version:

An entity management library for Supabase and SWR

209 lines (208 loc) 9.92 kB
import { useCallback, useEffect, useReducer, useRef, useState } from "react"; import Peer from "peerjs"; import { v4 } from "uuid"; import { useEntities } from "./use-entity-hooks"; /** * Peer Connections hook * @param {UsePeersOptions} options - The hook options * @param {boolean} [options.enabled=true] - Is the hook enabled * @param {(data: any, connection: import("peerjs").DataConnection, peer: Peer) => void} [options.onData=null] - The data handler * @param {string} [options.room=null] - The room to connect to * @param {string[]} [options.allowedUsers=["*"]] - The users allowed to send data to */ export function usePeers({ enabled = true, onData, room, allowedUsers = ["*"] }) { const [_, forceUpdate] = useReducer(x => x + 1, 0); const { entities: peers, createEntity: createPeer, updateEntity: updatePeer, deleteEntity: deletePeer, mutate: mutatePeers, } = useEntities(enabled ? 'peers' : null, { room }); const [peer, setPeer] = useState(); const connectionsRef = useRef([]); const connectionAttempts = useRef([]); const [dataQueue, setDataQueue] = useState([]); const messageHistory = useRef([]); const getPeer = useCallback((connection) => { if (!enabled) return; return peers === null || peers === void 0 ? void 0 : peers.find((peer) => peer.id == (connection === null || connection === void 0 ? void 0 : connection.peer)); }, [enabled, peers, connectionsRef.current]); // Data queue handler useEffect(() => { if (!peers) return; const newDataQueue = []; dataQueue.forEach(({ data, connection }) => { const peer = getPeer(connection); if (!peer) return newDataQueue.push({ data, connection }); if (!allowedUsers.includes("*") && !allowedUsers.includes(peer.user_id)) { console.error("Unauthorized data from: ", peer, connection); return connection.close(); } onData && onData(data, connection, peer); }); if (newDataQueue.length != dataQueue.length) { setDataQueue(newDataQueue); } }, [peers, getPeer, dataQueue, JSON.stringify(allowedUsers)]); const handleConnection = (connection, inbound = false) => { connection === null || connection === void 0 ? void 0 : connection.removeAllListeners(); // Handle incoming data and store it in the data queue onData && (connection === null || connection === void 0 ? void 0 : connection.on("data", (data) => { setDataQueue([...dataQueue, { data, connection }]); })); connection === null || connection === void 0 ? void 0 : connection.on("open", () => { console.log("connection opened", room); // Add the connection to the list connectionsRef.current = connectionsRef.current.filter((conn) => conn.peer != connection.peer); connectionsRef.current.push(connection); connectionAttempts.current = connectionAttempts.current.filter((id) => id != connection.peer); forceUpdate(); // Refresh the peers on new inbound connections inbound && mutatePeers(); sendMessageHistory(connection); }); connection === null || connection === void 0 ? void 0 : connection.on('close', () => { console.log("connection closed"); // Remove the connection from the list connectionsRef.current = connectionsRef.current.filter((conn) => conn.peer != connection.peer); connectionAttempts.current = connectionAttempts.current.filter((id) => id != connection.peer); forceUpdate(); }); connection === null || connection === void 0 ? void 0 : connection.on('error', (error) => { console.error("connection error", error); // Remove the connection from the list connectionsRef.current = connectionsRef.current.filter((conn) => conn.peer != connection.peer); connectionAttempts.current = connectionAttempts.current.filter((id) => id != connection.peer); forceUpdate(); }); // 10 second timeout for connection attempts setTimeout(() => { connectionAttempts.current = connectionAttempts.current.filter((id) => id != connection.peer); }, 10000); }; // Create Peer instance useEffect(() => { if (!enabled || !room) return; // Create a new Peer instance const newPeer = new Peer(v4()); newPeer.on('open', (id) => { console.log('Peer ID', id, 'Room', room); setPeer(newPeer); }); // newPeer.on('error', console.error) return () => { newPeer.removeAllListeners(); newPeer.on('open', () => newPeer.destroy()); }; }, [enabled, room]); // Clean up the peer on unload useEffect(() => { if (!enabled || !room || !peer) return; const deletePeerOnUnload = () => { (peer === null || peer === void 0 ? void 0 : peer.id) && deletePeer(peer.id); connectionsRef.current.forEach((conn) => conn.close()); connectionsRef.current = []; connectionAttempts.current = []; setDataQueue([]); messageHistory.current = []; peer === null || peer === void 0 ? void 0 : peer.destroy(); }; window.addEventListener("beforeunload", deletePeerOnUnload); return () => { deletePeerOnUnload(); window.removeEventListener("beforeunload", deletePeerOnUnload); }; }, [enabled, room, peer]); // Handle peer connections useEffect(() => { if (!(peer === null || peer === void 0 ? void 0 : peer.id) || !peers || !enabled || !room) return; // Create the peer entity if (!peers.find((p) => p.id == peer.id)) { createPeer({ id: peer.id, room }); } // Keep the peer entity current every 60 seconds const keepPeerCurrent = () => { const currentPeer = peers.find((p) => p.id == peer.id); if (currentPeer) { updatePeer(currentPeer.id, { updated_at: new Date() }); } else if (peer.id) { createPeer({ id: peer.id, room }); } }; const interval = setInterval(keepPeerCurrent, 60000); // Handle inbound connections const inboundConnection = (connection) => { handleConnection(connection, true); }; peer.on("connection", inboundConnection); // Connect to all peers peers.forEach((p) => { if (p.id == peer.id) return; if (connectionsRef.current.some((connection) => connection.peer == p.id)) return; if (connectionAttempts.current.includes(p.id)) return; if (!allowedUsers.includes("*") && !allowedUsers.includes(p === null || p === void 0 ? void 0 : p.user_id)) return; connectionAttempts.current.push(p.id); const conn = peer.connect(p.id); handleConnection(conn); }); connectionsRef.current.forEach((conn) => { handleConnection(conn); }); return () => { clearInterval(interval); peer.off("connection", inboundConnection); }; }, [enabled, room, peers, peer, onData, connectionsRef.current, JSON.stringify(allowedUsers)]); /** * Send data to connections * @param {any} data - The data to send * @param {import("peerjs").DataConnection[]} [connections] - Limit the connections to send to */ const sendData = useCallback((data, connections) => { if (!enabled) return; // Store the data in messageHistory for 10 seconds so we can send it on connection messageHistory.current.push(data); setTimeout(() => { messageHistory.current = messageHistory.current.filter((d) => d != data); }, 10000); connections = connections || connectionsRef.current; connections.forEach((connection) => { const peer = getPeer(connection); if (allowedUsers.includes("*") || allowedUsers.includes(peer === null || peer === void 0 ? void 0 : peer.user_id)) { connection.send(data); } }); }, [enabled, getPeer, connectionsRef.current, JSON.stringify(allowedUsers)]); const sendMessageHistory = useCallback((connection) => { if (!enabled) return; const connectionPeer = getPeer(connection); if (allowedUsers.includes("*") || allowedUsers.includes(connectionPeer === null || connectionPeer === void 0 ? void 0 : connectionPeer.user_id)) { messageHistory.current.forEach(data => { connection.send(data); }); } }, [enabled, JSON.stringify(allowedUsers), getPeer]); const getConnectionsForUser = useCallback((userId) => { if (!enabled) return []; return connectionsRef.current.filter((connection) => { const connectionPeer = getPeer(connection); return (connectionPeer === null || connectionPeer === void 0 ? void 0 : connectionPeer.user_id) == userId; }) || []; }, [enabled, getPeer, connectionsRef.current]); /** * Check if a user is online * @param {string} userId - The user ID * @returns {boolean} Is the user online */ const isOnline = useCallback((userId) => { var _a; return !!((_a = getConnectionsForUser(userId)) === null || _a === void 0 ? void 0 : _a.length); }, [getConnectionsForUser]); return { peers, sendData, connections: connectionsRef.current, isOnline, getPeer, getConnectionsForUser }; }