A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use WebSocket for live feed and now playing

Add VITE_WS_URL and WS_URL constant. Remove polling refetchInterval from
feed and now-playing queries and update React Query cache via WebSocket
messages. Implement ping/pong heartbeat and proper socket cleanup on
unmount.

+77 -5
+2 -1
apps/web/.env.example
··· 1 - VITE_API_URL=http://localhost:3004 1 + VITE_API_URL=http://localhost:3004 2 + VITE_WS_URL=ws://localhost:8002
+1
apps/web/src/consts.ts
··· 1 1 export const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"; 2 + export const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8002"; 2 3 3 4 export const AUDIO_EXTENSIONS = [ 4 5 "mp3",
-1
apps/web/src/hooks/useFeed.tsx
··· 9 9 client.get("/xrpc/app.rocksky.scrobble.getScrobbles", { 10 10 params: { limit }, 11 11 }), 12 - refetchInterval: 7000, 13 12 select: (res) => res.data.scrobbles || [], 14 13 }); 15 14
-1
apps/web/src/hooks/useNowPlaying.tsx
··· 24 24 "/xrpc/app.rocksky.feed.getNowPlayings", 25 25 { params: { size: 7 } }, 26 26 ), 27 - refetchInterval: 6000, 28 27 select: (res) => res.data.nowPlayings || [], 29 28 });
+37 -1
apps/web/src/pages/home/feed/Feed.tsx
··· 11 11 import Handle from "../../../components/Handle"; 12 12 import SongCover from "../../../components/SongCover"; 13 13 import { useFeedQuery } from "../../../hooks/useFeed"; 14 + import { useEffect, useRef } from "react"; 15 + import { WS_URL } from "../../../consts"; 16 + import { useQueryClient } from "@tanstack/react-query"; 14 17 15 18 dayjs.extend(relativeTime); 16 19 ··· 28 31 `; 29 32 30 33 function Feed() { 34 + const queryClient = useQueryClient(); 35 + const socketRef = useRef<WebSocket | null>(null); 36 + const heartbeatInterval = useRef<number | null>(null); 31 37 const { data, isLoading } = useFeedQuery(); 32 - console.log(data); 38 + 39 + useEffect(() => { 40 + const ws = new WebSocket(`${WS_URL.replace("http", "ws")}/ws`); 41 + socketRef.current = ws; 42 + 43 + ws.onopen = () => { 44 + heartbeatInterval.current = window.setInterval(() => { 45 + ws.send("ping"); 46 + }, 3000); 47 + }; 48 + 49 + ws.onmessage = (event) => { 50 + if (event.data === "pong") { 51 + return; 52 + } 53 + 54 + const message = JSON.parse(event.data); 55 + queryClient.setQueryData(["feed"], message.scrobbles); 56 + }; 57 + 58 + return () => { 59 + if (ws) { 60 + if (heartbeatInterval.current) { 61 + clearInterval(heartbeatInterval.current); 62 + } 63 + ws.close(); 64 + } 65 + console.log(">> WebSocket connection closed"); 66 + }; 67 + }, []); 68 + 33 69 return ( 34 70 <Container> 35 71 <HeadingMedium
+37 -1
apps/web/src/pages/home/nowplayings/NowPlayings.tsx
··· 7 7 import dayjs from "dayjs"; 8 8 import relativeTime from "dayjs/plugin/relativeTime"; 9 9 import utc from "dayjs/plugin/utc"; 10 - import { useEffect, useState } from "react"; 10 + import { useEffect, useRef, useState } from "react"; 11 11 import { useNowPlayingsQuery } from "../../../hooks/useNowPlaying"; 12 12 import styles from "./styles"; 13 + import { WS_URL } from "../../../consts"; 14 + import { useQueryClient } from "@tanstack/react-query"; 13 15 14 16 dayjs.extend(relativeTime); 15 17 dayjs.extend(utc); ··· 88 90 `; 89 91 90 92 function NowPlayings() { 93 + const queryClient = useQueryClient(); 91 94 const [isOpen, setIsOpen] = useState(false); 95 + const socketRef = useRef<WebSocket | null>(null); 96 + const heartbeatInterval = useRef<number | null>(null); 92 97 const { data: nowPlayings, isLoading } = useNowPlayingsQuery(); 93 98 const [currentlyPlaying, setCurrentlyPlaying] = useState<{ 94 99 id: string; ··· 106 111 } | null>(null); 107 112 const [currentIndex, setCurrentIndex] = useState(0); 108 113 const [progress, setProgress] = useState(0); 114 + 115 + useEffect(() => { 116 + const ws = new WebSocket(`${WS_URL.replace("http", "ws")}/ws`); 117 + socketRef.current = ws; 118 + 119 + ws.onopen = () => { 120 + heartbeatInterval.current = window.setInterval(() => { 121 + ws.send("ping"); 122 + }, 3000); 123 + }; 124 + 125 + ws.onmessage = (event) => { 126 + if (event.data === "pong") { 127 + return; 128 + } 129 + 130 + const message = JSON.parse(event.data); 131 + queryClient.setQueryData(["now-playings"], message.nowPlayings); 132 + queryClient.setQueryData(["scrobblesChart"], message.scrobblesChart); 133 + }; 134 + 135 + return () => { 136 + if (ws) { 137 + if (heartbeatInterval.current) { 138 + clearInterval(heartbeatInterval.current); 139 + } 140 + ws.close(); 141 + } 142 + console.log(">> WebSocket connection closed"); 143 + }; 144 + }, []); 109 145 110 146 const onNext = () => { 111 147 const nextIndex = currentIndex + 1;