v3: code quality pass

This commit is contained in:
khannurien
2026-03-24 18:47:05 +00:00
parent cd4076343b
commit c293f3e706
39 changed files with 1464 additions and 1555 deletions

View File

@@ -18,13 +18,10 @@ import {
import { WS_URL } from "../config/api.ts";
import type {
Dump,
IncomingWSMessage,
Notification,
OnlineUser,
RawComment,
RawDump,
RawNotification,
RawPlaylist,
RawPublicUser,
OutgoingWSMessage,
} from "../model.ts";
import {
deserializeComment,
@@ -43,62 +40,18 @@ interface WSProviderProps {
const MAX_BACKOFF = 30_000;
const ACK_TIMEOUT = 5_000;
// ── Type guards for incoming WS messages ──────────────────────────────────────
function isOnlineUser(obj: unknown): obj is OnlineUser {
if (!obj || typeof obj !== "object") return false;
const o = obj as Record<string, unknown>;
return typeof o.userId === "string" &&
typeof o.username === "string" &&
typeof o.hasAvatar === "boolean";
}
function isOnlineUserArray(val: unknown): val is OnlineUser[] {
return Array.isArray(val) && val.every(isOnlineUser);
}
function isStringArray(val: unknown): val is string[] {
return Array.isArray(val) && val.every((x) => typeof x === "string");
}
function isVotesUpdatePayload(
msg: Record<string, unknown>,
): msg is {
dumpId: string;
voteCount: number;
voterId: string;
action: "cast" | "remove";
} {
return typeof msg.dumpId === "string" &&
typeof msg.voteCount === "number" &&
typeof msg.voterId === "string" &&
(msg.action === "cast" || msg.action === "remove");
}
function isVoteAckPayload(
msg: Record<string, unknown>,
): msg is { dumpId: string; action: "cast" | "remove"; voteCount: number } {
return typeof msg.dumpId === "string" &&
(msg.action === "cast" || msg.action === "remove") &&
typeof msg.voteCount === "number";
}
function isPlaylistDeletedPayload(
msg: Record<string, unknown>,
): msg is { playlistId: string; userId: string } {
return typeof msg.playlistId === "string" && typeof msg.userId === "string";
}
function isPlaylistDumpsUpdatedPayload(
msg: Record<string, unknown>,
): msg is { playlistId: string; dumpIds: string[] } {
return typeof msg.playlistId === "string" && isStringArray(msg.dumpIds);
}
function isCommentDeletedPayload(
msg: Record<string, unknown>,
): msg is { commentId: string; dumpId: string } {
return typeof msg.commentId === "string" && typeof msg.dumpId === "string";
// Minimal runtime check: verify the `type` field is a known string so we can
// safely cast to the discriminated union and let TypeScript narrow from there.
function parseWSMessage(data: string): IncomingWSMessage | null {
try {
const msg = JSON.parse(data);
if (!msg || typeof msg !== "object" || typeof msg.type !== "string") {
return null;
}
return msg as IncomingWSMessage;
} catch {
return null;
}
}
export function WSProvider({ children, token, userId }: WSProviderProps) {
@@ -155,39 +108,28 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
socketRef.current = ws;
ws.onmessage = (event) => {
let msg: Record<string, unknown>;
try {
msg = JSON.parse(event.data);
} catch {
return;
}
const msg = parseWSMessage(event.data);
if (!msg) return;
switch (msg.type) {
case "ping":
ws.send(JSON.stringify({ type: "pong" }));
break;
case "welcome": {
backoff = 500; // reset backoff on successful connect
if (!isOnlineUserArray(msg.users) || !isStringArray(msg.myVotes)) {
break;
}
setOnlineUsers(msg.users);
setMyVotes(new Set(msg.myVotes));
setUnreadNotificationCount(
typeof msg.unreadNotificationCount === "number"
? msg.unreadNotificationCount
: 0,
ws.send(
JSON.stringify({ type: "pong" } satisfies OutgoingWSMessage),
);
break;
}
case "welcome":
backoff = 500; // reset backoff on successful connect
setOnlineUsers(msg.users);
setMyVotes(new Set(msg.myVotes));
setUnreadNotificationCount(msg.unreadNotificationCount);
break;
case "presence_update":
if (isOnlineUserArray(msg.users)) setOnlineUsers(msg.users);
setOnlineUsers(msg.users);
break;
case "votes_update": {
if (!isVotesUpdatePayload(msg)) break;
const { dumpId, voteCount, voterId, action } = msg;
setVoteCounts((prev) => ({ ...prev, [dumpId]: voteCount }));
setLastVoteEvent({ dumpId, voterId, action });
@@ -205,15 +147,13 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}
case "dump_created": {
if (!msg.dump || typeof msg.dump !== "object") break;
const dump = deserializeDump(msg.dump as RawDump);
const dump = deserializeDump(msg.dump);
setRecentDumps((prev) => [dump, ...prev]);
break;
}
case "dump_updated": {
if (!msg.dump || typeof msg.dump !== "object") break;
const dump = deserializeDump(msg.dump as RawDump);
const dump = deserializeDump(msg.dump);
setLastDumpEvent(dump);
// Un-delete if this dump was previously removed from the feed
// (e.g. it was made private, and is now public again).
@@ -231,15 +171,13 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}
case "dump_deleted": {
if (typeof msg.dumpId !== "string") break;
const dumpId = msg.dumpId;
const { dumpId } = msg;
setDeletedDumpIds((prev) => new Set([...prev, dumpId]));
setRecentDumps((prev) => prev.filter((d) => d.id !== dumpId));
break;
}
case "vote_ack": {
if (!isVoteAckPayload(msg)) break;
const { dumpId, action, voteCount } = msg;
// Clear pending revert timeout
const timeout = pendingRef.current.get(dumpId);
@@ -261,8 +199,7 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
case "playlist_created":
case "playlist_updated": {
if (!msg.playlist || typeof msg.playlist !== "object") break;
const playlist = deserializePlaylist(msg.playlist as RawPlaylist);
const playlist = deserializePlaylist(msg.playlist);
setLastPlaylistEvent({
type: msg.type === "playlist_created" ? "created" : "updated",
playlistId: playlist.id,
@@ -272,7 +209,6 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}
case "playlist_deleted": {
if (!isPlaylistDeletedPayload(msg)) break;
const { playlistId, userId } = msg;
setDeletedPlaylistIds((prev) => new Set([...prev, playlistId]));
setLastPlaylistEvent({ type: "deleted", playlistId, userId });
@@ -280,7 +216,6 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}
case "playlist_dumps_updated": {
if (!isPlaylistDumpsUpdatedPayload(msg)) break;
const { playlistId, dumpIds } = msg;
setLastPlaylistEvent({
type: "dumps_updated",
@@ -291,15 +226,13 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}
case "user_updated": {
if (!msg.user || typeof msg.user !== "object") break;
const user = deserializePublicUser(msg.user as RawPublicUser);
const user = deserializePublicUser(msg.user);
setLastUserEvent({ user });
break;
}
case "comment_created": {
if (!msg.comment || typeof msg.comment !== "object") break;
const comment = deserializeComment(msg.comment as RawComment);
const comment = deserializeComment(msg.comment);
setLastCommentEvent({
type: "created",
dumpId: comment.dumpId,
@@ -309,15 +242,13 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}
case "comment_deleted": {
if (!isCommentDeletedPayload(msg)) break;
const { commentId, dumpId } = msg;
setLastCommentEvent({ type: "deleted", dumpId, commentId });
break;
}
case "comment_updated": {
if (!msg.comment || typeof msg.comment !== "object") break;
const comment = deserializeComment(msg.comment as RawComment);
const comment = deserializeComment(msg.comment);
setLastCommentEvent({
type: "updated",
dumpId: comment.dumpId,
@@ -327,12 +258,7 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}
case "notification_created": {
if (!msg.notification || typeof msg.notification !== "object") {
break;
}
const notification = deserializeNotification(
msg.notification as RawNotification,
);
const notification = deserializeNotification(msg.notification);
setLastNotification(notification);
setUnreadNotificationCount((prev) => prev + 1);
break;
@@ -396,7 +322,9 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}, ACK_TIMEOUT);
pendingRef.current.set(dumpId, timeout);
socketRef.current?.send(JSON.stringify({ type: "vote_cast", dumpId }));
socketRef.current?.send(
JSON.stringify({ type: "vote_cast", dumpId } satisfies OutgoingWSMessage),
);
}, []);
const removeVote = useCallback((dumpId: string) => {
@@ -427,7 +355,11 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
}, ACK_TIMEOUT);
pendingRef.current.set(dumpId, timeout);
socketRef.current?.send(JSON.stringify({ type: "vote_remove", dumpId }));
socketRef.current?.send(
JSON.stringify(
{ type: "vote_remove", dumpId } satisfies OutgoingWSMessage,
),
);
}, []);
const injectDump = useCallback((dump: Dump) => {