v1 review pass: fixed some minor bugs
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { WSContext, type WSContextValue } from "./WSContext.ts";
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type VoteEvent, WSContext, type WSContextValue } from "./WSContext.ts";
|
||||
import { WS_URL } from "../config/api.ts";
|
||||
import type { Dump, OnlineUser } from "../model.ts";
|
||||
import type { Dump, OnlineUser, RawDump } from "../model.ts";
|
||||
import { deserializeDump } from "../model.ts";
|
||||
|
||||
interface WSProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -17,16 +25,21 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
const [myVotes, setMyVotes] = useState<Set<string>>(new Set());
|
||||
const [recentDumps, setRecentDumps] = useState<Dump[]>([]);
|
||||
const [deletedDumpIds, setDeletedDumpIds] = useState<Set<string>>(new Set());
|
||||
const [lastVoteEvent, setLastVoteEvent] = useState<VoteEvent | null>(null);
|
||||
|
||||
// Refs to avoid stale closures in event handlers
|
||||
const voteCountsRef = useRef(voteCounts);
|
||||
const myVotesRef = useRef(myVotes);
|
||||
voteCountsRef.current = voteCounts;
|
||||
myVotesRef.current = myVotes;
|
||||
useLayoutEffect(() => {
|
||||
voteCountsRef.current = voteCounts;
|
||||
myVotesRef.current = myVotes;
|
||||
});
|
||||
|
||||
const socketRef = useRef<WebSocket | null>(null);
|
||||
// Tracks pending optimistic votes: dumpId → revert timeout ID
|
||||
const pendingRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
const pendingRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let closed = false;
|
||||
@@ -36,7 +49,9 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
function connect() {
|
||||
if (closed) return;
|
||||
|
||||
const url = `${WS_URL}/ws${token ? `?token=${encodeURIComponent(token)}` : ""}`;
|
||||
const url = `${WS_URL}/ws${
|
||||
token ? `?token=${encodeURIComponent(token)}` : ""
|
||||
}`;
|
||||
const ws = new WebSocket(url);
|
||||
socketRef.current = ws;
|
||||
|
||||
@@ -67,13 +82,21 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
break;
|
||||
|
||||
case "votes_update": {
|
||||
const { dumpId, voteCount } = msg as { dumpId: string; voteCount: number };
|
||||
const { dumpId, voteCount, voterId, action } = msg as {
|
||||
dumpId: string;
|
||||
voteCount: number;
|
||||
voterId: string;
|
||||
action: "cast" | "remove";
|
||||
};
|
||||
setVoteCounts((prev) => ({ ...prev, [dumpId]: voteCount }));
|
||||
if (voterId && action) {
|
||||
setLastVoteEvent({ dumpId, voterId, action });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "dump_created": {
|
||||
const dump = msg.dump as Dump;
|
||||
const dump = deserializeDump(msg.dump as RawDump);
|
||||
setRecentDumps((prev) => [dump, ...prev]);
|
||||
break;
|
||||
}
|
||||
@@ -131,14 +154,14 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
|
||||
connect();
|
||||
|
||||
const pending = pendingRef.current;
|
||||
return () => {
|
||||
closed = true;
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
socketRef.current?.close();
|
||||
socketRef.current = null;
|
||||
// Clear all pending revert timeouts
|
||||
for (const t of pendingRef.current.values()) clearTimeout(t);
|
||||
pendingRef.current.clear();
|
||||
for (const t of pending.values()) clearTimeout(t);
|
||||
pending.clear();
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
@@ -148,13 +171,21 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
const prevVoted = myVotesRef.current.has(dumpId);
|
||||
if (prevVoted) return; // already voted
|
||||
|
||||
setMyVotes((prev) => { const n = new Set(prev); n.add(dumpId); return n; });
|
||||
setMyVotes((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.add(dumpId);
|
||||
return n;
|
||||
});
|
||||
setVoteCounts((prev) => ({ ...prev, [dumpId]: prevCount + 1 }));
|
||||
|
||||
// Schedule revert if no ack
|
||||
const timeout = setTimeout(() => {
|
||||
pendingRef.current.delete(dumpId);
|
||||
setMyVotes((prev) => { const n = new Set(prev); n.delete(dumpId); return n; });
|
||||
setMyVotes((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete(dumpId);
|
||||
return n;
|
||||
});
|
||||
setVoteCounts((prev) => ({ ...prev, [dumpId]: prevCount }));
|
||||
}, ACK_TIMEOUT);
|
||||
pendingRef.current.set(dumpId, timeout);
|
||||
@@ -168,13 +199,24 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
const prevVoted = myVotesRef.current.has(dumpId);
|
||||
if (!prevVoted) return; // not voted
|
||||
|
||||
setMyVotes((prev) => { const n = new Set(prev); n.delete(dumpId); return n; });
|
||||
setVoteCounts((prev) => ({ ...prev, [dumpId]: Math.max(0, prevCount - 1) }));
|
||||
setMyVotes((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete(dumpId);
|
||||
return n;
|
||||
});
|
||||
setVoteCounts((prev) => ({
|
||||
...prev,
|
||||
[dumpId]: Math.max(0, prevCount - 1),
|
||||
}));
|
||||
|
||||
// Schedule revert if no ack
|
||||
const timeout = setTimeout(() => {
|
||||
pendingRef.current.delete(dumpId);
|
||||
setMyVotes((prev) => { const n = new Set(prev); n.add(dumpId); return n; });
|
||||
setMyVotes((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.add(dumpId);
|
||||
return n;
|
||||
});
|
||||
setVoteCounts((prev) => ({ ...prev, [dumpId]: prevCount }));
|
||||
}, ACK_TIMEOUT);
|
||||
pendingRef.current.set(dumpId, timeout);
|
||||
@@ -188,6 +230,7 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
myVotes,
|
||||
recentDumps,
|
||||
deletedDumpIds,
|
||||
lastVoteEvent,
|
||||
castVote,
|
||||
removeVote,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user