@daveyplate/supabase-swr-entities
Version:
An entity management library for Supabase and SWR
276 lines (219 loc) • 9.71 kB
text/typescript
import { useCallback, useEffect, useReducer, useRef, useState } from "react"
import Peer, { DataConnection } from "peerjs"
import { v4 } from "uuid"
import { useEntities } from "./use-entity-hooks"
export interface PeersResult {
peers: any[]
sendData: (data: any, connections?: DataConnection[]) => void
connections: import("peerjs").DataConnection[]
isOnline: (userId: string) => boolean
getPeer: (connection: import("peerjs").DataConnection) => any
getConnectionsForUser: (userId: string) => DataConnection[]
}
interface UsePeersOptions {
enabled?: boolean
onData?: (data: any, connection: DataConnection, peer: any) => void | null
room?: string | null
allowedUsers?: string[]
}
/**
* 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 = ["*"]
}: UsePeersOptions): PeersResult {
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<Peer>()
const connectionsRef = useRef<DataConnection[]>([])
const connectionAttempts = useRef<string[]>([])
const [dataQueue, setDataQueue] = useState<Record<string, any>[]>([])
const messageHistory = useRef<any[]>([])
const getPeer = useCallback((connection: DataConnection) => {
if (!enabled) return
return peers?.find((peer) => peer.id == connection?.peer)
}, [enabled, peers, connectionsRef.current])
// Data queue handler
useEffect(() => {
if (!peers) return
const newDataQueue: Record<string, any>[] = []
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: DataConnection, inbound = false) => {
connection?.removeAllListeners()
// Handle incoming data and store it in the data queue
onData && connection?.on("data", (data) => {
setDataQueue([...dataQueue, { data, connection }])
})
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?.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?.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?.id && deletePeer(peer.id)
connectionsRef.current.forEach((conn) => conn.close())
connectionsRef.current = []
connectionAttempts.current = []
setDataQueue([])
messageHistory.current = []
peer?.destroy()
}
window.addEventListener("beforeunload", deletePeerOnUnload)
return () => {
deletePeerOnUnload()
window.removeEventListener("beforeunload", deletePeerOnUnload)
}
}, [enabled, room, peer])
// Handle peer connections
useEffect(() => {
if (!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: DataConnection) => {
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?.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: any, connections?: DataConnection[]) => {
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?.user_id)) {
connection.send(data)
}
})
}, [enabled, getPeer, connectionsRef.current, JSON.stringify(allowedUsers)])
const sendMessageHistory = useCallback((connection: DataConnection) => {
if (!enabled) return
const connectionPeer = getPeer(connection)
if (allowedUsers.includes("*") || allowedUsers.includes(connectionPeer?.user_id)) {
messageHistory.current.forEach(data => {
connection.send(data)
})
}
}, [enabled, JSON.stringify(allowedUsers), getPeer])
const getConnectionsForUser = useCallback((userId: string) => {
if (!enabled) return []
return connectionsRef.current.filter((connection) => {
const connectionPeer = getPeer(connection)
return 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: string) => !!getConnectionsForUser(userId)?.length, [getConnectionsForUser])
return { peers, sendData, connections: connectionsRef.current, isOnline, getPeer, getConnectionsForUser }
}