v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-29 14:31:14 +00:00
parent 42e0356d21
commit 0d4e344d77
287 changed files with 5679 additions and 5135 deletions

View File

@@ -1,8 +1,7 @@
/**
* The socket contexts, split three ways by how often each part changes, so a
* consumer re-renders only with what it reads: typing and presence update every
* few seconds while people are around, chat with every message, and a feed has
* no use for either. WSProvider provides all three.
* Declares the three contexts `WSProvider` fills from the WebSocket, and the
* types they carry. They are split by how often each part changes, so that a
* feed doesn't re-render with every chat message or presence change.
*
* @module
*/
@@ -16,225 +15,266 @@ import type { OnlineUser } from "../../../shared/ws.ts";
import type { Playlist } from "../../../shared/playlist.ts";
import type { PublicUser } from "../../../shared/user.ts";
/** A vote someone cast or withdrew. */
/**
* Set as `lastVoteEvent` by `WSProvider` on a `votes_update`. The profile
* pages read it to add or drop a dump from a user's upvoted list.
*/
export interface VoteEvent {
/** The dump's id. */
dumpId: string;
/** Who voted. */
voterId: string;
/** Whether the vote was cast or withdrawn. */
action: "cast" | "remove";
}
/** A like someone cast or withdrew. */
/** Set as `lastLikeEvent` by `WSProvider` on a `comment_likes_update`. */
export interface LikeEvent {
/** The comment's id. */
commentId: string;
/** Who liked it. */
likerId: string;
/** Whether the like was cast or withdrawn. */
action: "cast" | "remove";
}
/** A playlist created, changed, deleted, or whose dumps changed. */
/**
* Set as `lastPlaylistEvent` by `WSProvider` on each `playlist_*` message.
* The pages that show playlists apply it, some through `usePlaylistListSync`.
* Which fields are set depends on `type`.
*/
export interface PlaylistEvent {
/** What happened. */
type: "created" | "updated" | "deleted" | "dumps_updated";
/** The playlist's id. */
playlistId: string;
/** The playlist as it is now, when created or updated. */
/** Set when created or updated. */
playlist?: Playlist;
/** The owner's id, when deleted. */
/** The owner's id, set when deleted. */
userId?: string;
/** The dumps the user can see in it, in order, when its dumps changed. */
/**
* Set when the dumps changed. It lists only the dumps this user may see, in
* order.
*/
dumpIds?: string[];
/** How many dumps it holds, when its dumps changed. */
/** Set when the dumps changed. It counts private dumps too. */
dumpCount?: number;
}
/** A comment posted, edited or deleted. */
/**
* Set as `lastCommentEvent` by `WSProvider` on each `comment_*` message. The
* `Dump` page applies it to its thread, and `useDumpListSync` to its comment
* counts.
*/
export interface CommentEvent {
/** What happened. */
type: "created" | "deleted" | "updated";
/** The id of the dump it is under. */
dumpId: string;
/** The comment as it is now, when posted or edited. */
/** Set when created or updated. */
comment?: Comment;
/** The comment's id, when deleted. */
/** Set when deleted. */
commentId?: string;
}
/** A user whose public profile changed. */
/**
* Set as `lastUserEvent` by `WSProvider` on a `user_updated`, which the
* server sends on any change to a profile. `UserPublicProfile` applies it.
*/
export interface UserEvent {
/** The user as they are now. */
user: PublicUser;
}
/** The replied-to message, as shown in the reply bar and on a pending send. */
/**
* Chosen in `ChatModal` for the reply bar, and kept on the pending message so
* it shows the reply before the server's echo. Only `id` goes to the server.
*/
export interface ChatReplyTarget {
/** The message's id. */
id: string;
/** Its author's name. */
author: string;
/** Its text. */
body: string;
}
/**
* A message this client sent that the server hasn't echoed back yet — shown in
* the list as "sending", or "failed" with a way to retry or discard it.
* Held by `WSProvider` for a chat message this client sent and the server
* hasn't echoed back yet. `ChatModal` shows it, as `PendingItem`, after the
* messages the server has.
*/
export interface PendingChatMessage {
/** The id the echo will carry back. */
/**
* Sent with `chat_send` and returned with the echo or the error. A retry
* gets a new one, so a late echo of the first attempt can't retire it.
*/
nonce: string;
/** The message. */
body: string;
/** The message it replies to. */
replyTo?: ChatReplyTarget;
/** When it was sent. */
/** When this client sent it, to find it in the history after a reconnect. */
createdAt: Date;
/** Whether it is on its way or failed. */
/**
* `failed` when the server refused it, the socket was down or closed, or no
* echo came in 10 s. A failed message may still have reached the server.
*/
status: "sending" | "failed";
/** Why it failed. */
error?: string;
}
/** A failed chat edit or delete, surfaced in the chat until dismissed. */
/**
* Set as `chatError` by `WSProvider` when the server refuses a chat edit or
* delete. `ChatModal` shows it for a few seconds.
*/
export interface ChatError {
/** Distinguishes one error from the next. */
/** Tells apart two errors with the same message. */
id: number;
/** What went wrong. */
message: string;
}
/**
* What {@link WSContext} holds: the connection, votes, likes, and the live
* dump, playlist, comment, user and notification events.
* Provided by `WSProvider` through {@link WSContext}, read with `useWS`. The
* `last*Event` fields hold only the latest event of their kind: a consumer
* reacts in an effect when one changes, and an event that another replaces
* within the same render is missed. After a reconnect, `connectionEpoch` tells
* pages to fetch what they missed.
*/
export interface WSContextValue {
/** The connection's state. */
wsStatus: "connecting" | "connected" | "disconnected";
/** Why the socket is down, while it is. */
/** Set while the socket is down, for the banner that says so. */
wsErrorMessage: string | null;
/** Live vote counts, by dump id. */
/**
* Counts known to the socket, by dump id: pushed by the server, or changed
* optimistically. A missing dump shows its fetched `voteCount`.
*/
voteCounts: Record<string, number>;
/** The ids of the dumps the user upvoted. */
/** Reset from the server's `welcome` on every connection. */
myVotes: Set<string>;
/** Live like counts, by comment id. */
/** As `voteCounts`, for comments. */
likeCounts: Record<string, number>;
/** The ids of the comments the user likes. */
/** As `myVotes`, for comments. */
myLikes: Set<string>;
/** Dumps posted (or made public) this session, newest first (capped). */
/**
* Dumps that arrived this session, newest first and at most 100: the ones
* the server announced, which are public, and the ones this user posted,
* which may be private. `Index` puts them above the feed it fetched.
*/
recentDumps: Dump[];
/** Ids of dumps deleted this session. */
/**
* Dumps deleted or made private this session, for the lists to filter out.
* A dump made public again leaves it.
*/
deletedDumpIds: Set<string>;
/** The latest vote. */
lastVoteEvent: VoteEvent | null;
/** The latest like. */
/** Read by no consumer at the moment. */
lastLikeEvent: LikeEvent | null;
/** The latest edited dump. */
/** The dump as it is after its latest edit. */
lastDumpEvent: Dump | null;
/** The latest playlist change. */
lastPlaylistEvent: PlaylistEvent | null;
/** Ids of playlists deleted this session. */
deletedPlaylistIds: Set<string>;
/** The latest comment change. */
lastCommentEvent: CommentEvent | null;
/** The latest profile change. */
lastUserEvent: UserEvent | null;
/** How many notifications the user hasn't read. */
/** Read from the `welcome`, then counted up by each new notification. */
unreadNotificationCount: number;
/** The latest notification received. */
/** Read by the bell, which rings, and by the notifications page. */
lastNotification: Notification | null;
/** Increments on each WS reconnect so pages can backfill missed events. */
/**
* Incremented on each reconnect, never on the first connection, so pages
* fetch again what they missed while the socket was down.
*/
connectionEpoch: number;
/**
* Upvotes a dump, optimistically. `shownCount`: the count on screen, used
* until a live one arrives.
* Upvotes a dump at once and sends `vote_cast`. The server's answer confirms
* or reverts it, and so does a timeout of 5 s, which covers a closed socket.
* `shownCount` is the count on screen, used when `voteCounts` has none yet.
* Does nothing when the user already voted.
*/
castVote: (dumpId: string, shownCount?: number) => void;
/** Withdraws an upvote, optimistically; `shownCount` as for `castVote`. */
/** Withdraws an upvote, as {@link WSContextValue.castVote} casts one. */
removeVote: (dumpId: string, shownCount?: number) => void;
/** Likes a comment, optimistically; `shownCount` as for `castVote`. */
/** Likes a comment, as {@link WSContextValue.castVote} upvotes a dump. */
castCommentLike: (commentId: string, shownCount?: number) => void;
/** Withdraws a like, optimistically; `shownCount` as for `castVote`. */
/** Withdraws a like, as {@link WSContextValue.castVote} casts a vote. */
removeCommentLike: (commentId: string, shownCount?: number) => void;
/**
* Adds a dump this client just posted to `recentDumps`, without waiting for
* its broadcast.
* Adds a dump this user just posted to `recentDumps`, without waiting for
* its broadcast, which a private dump never gets. `DumpCreateModal` calls it.
*/
injectDump: (dump: Dump) => void;
/** Sets the unread notification count to zero. */
/**
* Zeroes the bell's count, once the notifications page has marked them read
* on the server.
*/
clearUnreadNotifications: () => void;
}
/** The chat room: its live stream, unread state and what to send into it. */
/**
* Provided by `WSProvider` through {@link WSChatContext}, read with
* `useWSChat`. The socket sends chat messages only to signed-in users, so for
* a visitor it stays empty.
*/
export interface WSChatContextValue {
/** Live buffer of chat messages received this session (capped). */
/**
* The messages received this session, at most 200, for `ChatModal` to merge
* with the history it fetches. A reconnect empties it while the chat is
* closed, since `ChatModal` fetches everything again on open.
*/
chatMessages: ChatMessage[];
/**
* Ids of chat messages deleted live this session, so fetched history can
* filter them out.
* Messages deleted this session, which `ChatModal` filters out of its
* fetched history.
*/
deletedChatIds: Set<string>;
/**
* Messages from others since the read marker — kept by the server, so it
* survives reloads and agrees across tabs and devices.
* Messages from others after the read marker. The server keeps the marker,
* so the count agrees across tabs and devices.
*/
unreadChatCount: number;
/**
* The server's read marker: created_at of the newest message the user has
* seen. Null until the socket's welcome.
* The time of the newest message the user has seen, which the server keeps.
* Null until the `welcome`, and for a visitor.
*/
chatReadAt: Date | null;
/** Messages sent and not yet echoed back. */
pendingChatMessages: PendingChatMessage[];
/** The latest failed edit or delete, until dismissed. */
/** Only for edits and deletes. A failed send shows on its pending message. */
chatError: ChatError | null;
/**
* Users currently composing a chat message (excludes nobody — the chatbox
* filters out the current user itself). Shaped like presence for avatars.
*/
/** Includes the user when they are typing: `ChatModal` filters them out. */
typingUsers: OnlineUser[];
/** Sends a message, replying to `replyTo` if given. */
/** Trims `body` and sends it as a pending message. Ignores a blank body. */
sendChatMessage: (body: string, replyTo?: ChatReplyTarget) => void;
/** Resends the failed message `nonce`. */
retryChatMessage: (nonce: string) => void;
/** Drops the failed message `nonce`. */
discardChatMessage: (nonce: string) => void;
/** Edits a message. */
/**
* Sends the edit if the socket is open, else drops it. It shows once the
* server broadcasts it, or fails as `chatError`.
*/
editChatMessage: (id: string, body: string) => void;
/** Deletes a message. */
/** Sends the deletion if the socket is open, as `editChatMessage` does. */
deleteChatMessage: (id: string) => void;
/** Clears `chatError`. */
dismissChatError: () => void;
/** Moves the read marker up to the message created at `at`. */
/**
* Moves the read marker up to `at`, the time of the newest message seen. It
* zeroes `unreadChatCount` at once and sends the marker at most once a
* second. A marker set offline goes out with the next `welcome`.
*/
markChatRead: (at: Date) => void;
/**
* Folds an authoritative fetch of the stream between `from` and `to` into the
* live buffer after a reconnect: buffered messages in that range which the
* server no longer has were deleted while the socket was down.
* Replaces the live buffer's messages between `from` and `to` with
* `messages`, which `ChatModal` fetched after a reconnect. A message missing
* from them was deleted while the socket was down. A failed send found among
* them did reach the server, and leaves the pending list.
*/
reconcileChat: (messages: ChatMessage[], from: Date, to: Date) => void;
/**
* Informs the server whether the user is currently composing a message, so it
* can show/hide the "is typing" indicator to other clients.
* Tells the server whether the user is typing, when the socket is open.
* `ChatModal` resends `true` every few seconds while typing continues, since
* the server forgets a typer after 6 s.
*/
setChatTyping: (typing: boolean) => void;
/**
* Informs the server whether the chatbox is open (suppresses redundant
* mention notifications while the user is reading chat).
* Tells the server whether the user is reading the chat: open, in a visible
* tab. The server sends no notification for a mention the user reads live,
* and this client doesn't count the message unread.
*/
setChatOpen: (open: boolean) => void;
}
/** Who's connected. */
/**
* Provided by `WSProvider` through {@link WSPresenceContext}, read with
* `useWSPresence`.
*/
export interface WSPresenceContextValue {
/** The signed-in users with a socket open. */
/** The signed-in users with a socket open, each once. */
onlineUsers: OnlineUser[];
}
/** What a consumer outside any provider gets. */
/** Given to a consumer outside `WSProvider`, as if it were still connecting. */
export const defaultWSValue: WSContextValue = {
wsStatus: "connecting",
wsErrorMessage: null,
@@ -262,7 +302,7 @@ export const defaultWSValue: WSContextValue = {
clearUnreadNotifications: () => {},
};
/** What a consumer outside any provider gets. */
/** Given to a consumer outside `WSProvider`: an empty chat. */
export const defaultWSChatValue: WSChatContextValue = {
chatMessages: [],
deletedChatIds: new Set(),
@@ -283,22 +323,25 @@ export const defaultWSChatValue: WSChatContextValue = {
setChatOpen: () => {},
};
/** What a consumer outside any provider gets. */
/** Given to a consumer outside `WSProvider`: nobody is online. */
export const defaultWSPresenceValue: WSPresenceContextValue = {
onlineUsers: [],
};
/** The socket context: connection, votes, likes and live events. */
/**
* Holds the connection, the votes and likes, and the latest live event of
* each kind. Read it with `useWS`.
*/
export const WSContext: React.Context<WSContextValue> = createContext<
WSContextValue
>(defaultWSValue);
/** The chat context. */
/** Holds the chat's live stream and read state. Read it with `useWSChat`. */
export const WSChatContext: React.Context<WSChatContextValue> = createContext<
WSChatContextValue
>(
defaultWSChatValue,
);
/** The presence context. */
/** Holds who is online. Read it with `useWSPresence`. */
export const WSPresenceContext: React.Context<WSPresenceContextValue> =
createContext<
WSPresenceContextValue