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,7 +1,10 @@
/**
* The socket protocol. Every message has a `type` discriminant, so each union
* narrows with a switch. The server sends these as JSON, so the client reads
* `Wire<ServerToClientMessage>`: its Dates arrive as strings.
* Defines the messages of the WebSocket between `WSProvider` in the client and
* `routes/ws.ts` in the server, as JSON objects told apart by `type`.
*
* The server's messages go out from `services/ws-service.ts`, and `WSProvider`
* handles them. Their Dates arrive as strings, so the client reads them as
* `Wire<ServerToClientMessage>` and deserializes what it keeps.
*
* @module
*/
@@ -13,117 +16,133 @@ import type { Notification } from "./notification.ts";
import type { Playlist } from "./playlist.ts";
import type { PublicUser } from "./user.ts";
// ── Client → Server ──────────────────────────────────────────────────────────
/** A keepalive; the other side answers {@link PongMessage}. Both directions. */
/**
* Sent by the server to every socket every 30 s. `WSProvider` answers
* {@link PongMessage}, and a socket that missed the previous ping is closed.
* The server also answers a client's ping, though the client never sends one.
*/
export interface PingMessage {
/** The discriminant. */
type: "ping";
}
/** The answer to a {@link PingMessage}. Both directions. */
/**
* Sent in answer to a {@link PingMessage}. The server records the client's to
* keep its socket open, and the client ignores the server's.
*/
export interface PongMessage {
/** The discriminant. */
type: "pong";
}
/** Upvotes a dump; answered by {@link VoteAckMessage}. */
/**
* Sent by `WSProvider` when the user upvotes a dump, after it has shown the
* vote. The server answers {@link VoteAckMessage}, or an {@link ErrorMessage}
* naming the dump, on which the client undoes the vote, as it does when
* neither comes within 5 s.
*/
export interface VoteCastMessage {
/** The discriminant. */
type: "vote_cast";
/** The dump's id. */
dumpId: string;
}
/** Withdraws an upvote; answered by {@link VoteAckMessage}. */
/** Sent by `WSProvider` to withdraw an upvote, as {@link VoteCastMessage}. */
export interface VoteRemoveMessage {
/** The discriminant. */
type: "vote_remove";
/** The dump's id. */
dumpId: string;
}
/** Likes a comment; answered by {@link CommentLikeAckMessage}. */
/**
* Sent by `WSProvider` when the user likes a comment, and handled as
* {@link VoteCastMessage} is, with {@link CommentLikeAckMessage} for the ack.
*/
export interface CommentLikeCastMessage {
/** The discriminant. */
type: "comment_like_cast";
/** The comment's id. */
commentId: string;
}
/** Withdraws a like; answered by {@link CommentLikeAckMessage}. */
/**
* Sent by `WSProvider` to withdraw a like, as {@link CommentLikeCastMessage}.
*/
export interface CommentLikeRemoveMessage {
/** The discriminant. */
type: "comment_like_remove";
/** The comment's id. */
commentId: string;
}
/** Posts a chat message; broadcast back as {@link ChatMessageMessage}. */
/**
* Sent by `WSProvider` when the user posts in the chatbox, which shows the
* message as pending meanwhile. The server broadcasts it as
* {@link ChatMessageMessage}, or answers a chat {@link ErrorMessage}.
*/
export interface ChatSendMessage {
/** The discriminant. */
type: "chat_send";
/** The message, in Markdown. */
body: string;
/** Id of the message being replied to, if this is a reply. */
/** Ignored if longer than 64 characters or naming no message. */
replyToId?: string;
/**
* Client-chosen id echoed on the broadcast (or on the error), so the
* sender can match the outcome to its pending message.
* Chosen by the client, and echoed on the broadcast or the error, so that
* it can match the outcome to its pending message. Ignored if longer than
* 64 characters.
*/
nonce?: string;
}
/**
* Sent while the user is composing (true) and when they stop (false). The
* server rebroadcasts the live set of typers to everyone as
* {@link ChatTypingUpdateMessage}.
* Sent by the chatbox while the user types, every 3 s, and once they pause.
* The server answers every signed-in socket with
* {@link ChatTypingUpdateMessage} when the user starts or stops, and forgets a
* typer it hasn't heard from in 6 s.
*/
export interface ChatTypingMessage {
/** The discriminant. */
type: "chat_typing";
/** Whether the user is composing. */
typing: boolean;
}
/**
* Tells the server whether this client currently has the chatbox open, so that
* mention notifications can be skipped for users who are already reading chat.
* Sent by `WSProvider` when the chatbox opens or closes, when the tab is
* hidden or shown, and after each `welcome`. The server notifies no one of a
* chat mention while one of their sockets says `open`.
*/
export interface ChatFocusMessage {
/** The discriminant. */
type: "chat_focus";
/** Whether the chatbox is open. */
/** True only while the chatbox is open in a visible tab. */
open: boolean;
}
/** Edits a chat message: the user's own, or any with `chat:moderate`. */
/**
* Sent by `WSProvider` when the user edits a chat message, their own or, with
* `chat:moderate`, anyone's. The server broadcasts
* {@link ChatMessageUpdatedMessage}, or answers a chat {@link ErrorMessage}.
*/
export interface ChatEditMessage {
/** The discriminant. */
type: "chat_edit";
/** The message's id. */
id: string;
/** The new text, in Markdown. */
body: string;
}
/** Deletes a chat message: the user's own, or any with `chat:moderate`. */
/**
* Sent by `WSProvider` when the user deletes a chat message, as
* {@link ChatEditMessage}. The server broadcasts
* {@link ChatMessageDeletedMessage}.
*/
export interface ChatDeleteMessage {
/** The discriminant. */
type: "chat_delete";
/** The message's id. */
id: string;
}
/** The user has seen the chat up to the message created at `at`. */
/**
* Sent by `WSProvider`, at most once a second, as the user reads the chat. The
* server moves their read marker to `at`, a message's ISO `createdAt`, and
* answers {@link ChatReadUpdateMessage}. It drops the message silently when it
* is malformed or over its rate limit.
*/
export interface ChatReadMessage {
/** The discriminant. */
type: "chat_read";
/** The creation time of the last message seen, as an ISO string. */
at: string;
}
/** Any message a client sends. */
/**
* Sent by `WSProvider`, and dispatched by the route's `handleMessage`, which
* checks each field's type before using it.
*/
export type ClientToServerMessage =
| PingMessage
| PongMessage
@@ -138,285 +157,300 @@ export type ClientToServerMessage =
| ChatDeleteMessage
| ChatReadMessage;
// ── Server → Client ──────────────────────────────────────────────────────────
/** A signed-in user with at least one socket open. */
/**
* Sent by the server in {@link WelcomeMessage},
* {@link PresenceUpdateMessage} and {@link ChatTypingUpdateMessage}, once per
* signed-in user whatever their number of sockets.
*/
export interface OnlineUser {
/** The user's id. */
userId: string;
/** The user's name. */
username: string;
/** Whether the user has an avatar. */
hasAvatar: boolean;
/** When the avatar last changed, to bust the client's cache. */
/**
* Set when the avatar changed while the user was connected, for the client
* to append to the avatar's URL so it doesn't show the cached one.
*/
avatarVersion?: number;
}
/** The first message on a new socket: the state the client starts from. */
/**
* Sent by the server to a socket once it opens, just after the
* {@link PresenceUpdateMessage} that announces it. `WSProvider` replaces its
* state with this one, dropping its pending votes and likes. After a
* reconnect, it also bumps `connectionEpoch`, so that pages refetch what they
* missed. The per-user fields are empty or zero on an anonymous socket.
*/
export interface WelcomeMessage {
/** The discriminant. */
type: "welcome";
/** Who is online. */
users: OnlineUser[];
/** The ids of the dumps the user upvoted; empty when signed out. */
myVotes: string[];
/** The ids of the comments the user likes; empty when signed out. */
myCommentLikes: string[];
/** How many notifications the user hasn't read. */
unreadNotificationCount: number;
/**
* The user's chat read marker (signed-in users only), as an ISO string.
*/
/** Absent on an anonymous socket. */
chatReadAt?: string;
/** How many chat messages came after the read marker. */
unreadChatCount: number;
}
/** Who is online, sent to everyone whenever it changes. */
/**
* Sent by the server to every socket when a socket opens or closes, and when
* a connected user's avatar changes.
*/
export interface PresenceUpdateMessage {
/** The discriminant. */
type: "presence_update";
/** Who is online. */
users: OnlineUser[];
}
/**
* A dump's vote count changed; sent to everyone, or only to the poster for a
* private dump.
* Sent by the server after a vote, to every socket, or to the poster's alone
* for a private dump. `WSProvider` updates the count, and when `voterId` is
* the user, it updates their votes too, so their other tabs follow.
*/
export interface VotesUpdateMessage {
/** The discriminant. */
type: "votes_update";
/** The dump's id. */
dumpId: string;
/** Its new vote count. */
voteCount: number;
/** Who voted. */
voterId: string;
/** Whether the vote was cast or withdrawn. */
action: "cast" | "remove";
}
/** Confirms the sender's own vote, with the count it led to. */
/**
* Sent by the server to the socket that sent a {@link VoteCastMessage} or
* {@link VoteRemoveMessage}, once the vote is stored. `WSProvider` confirms
* its optimistic vote with it.
*/
export interface VoteAckMessage {
/** The discriminant. */
type: "vote_ack";
/** The dump's id. */
dumpId: string;
/** Whether the vote was cast or withdrawn. */
action: "cast" | "remove";
/** The dump's new vote count. */
voteCount: number;
}
/** A comment's like count changed. */
/**
* Sent by the server after a like, as {@link VotesUpdateMessage} is after a
* vote: to the dump's poster alone when the comment is on a private dump.
*/
export interface CommentLikesUpdateMessage {
/** The discriminant. */
type: "comment_likes_update";
/** The comment's id. */
commentId: string;
/** Its new like count. */
likeCount: number;
/** Who liked it. */
likerId: string;
/** Whether the like was cast or withdrawn. */
action: "cast" | "remove";
}
/** Confirms the sender's own like, with the count it led to. */
/**
* Sent by the server to the socket that liked or unliked a comment, as
* {@link VoteAckMessage} is for a vote.
*/
export interface CommentLikeAckMessage {
/** The discriminant. */
type: "comment_like_ack";
/** The comment's id. */
commentId: string;
/** Whether the like was cast or withdrawn. */
action: "cast" | "remove";
/** The comment's new like count. */
likeCount: number;
}
/** A dump was posted. */
/**
* Sent by the server to every socket when a public dump is posted, or when a
* private one goes public. `WSProvider` puts it at the top of its recent
* dumps.
*/
export interface DumpCreatedMessage {
/** The discriminant. */
type: "dump_created";
/** The new dump. */
dump: Dump;
}
/** A dump changed. */
/**
* Sent by the server to every socket when a public dump changes. `WSProvider`
* refreshes it in place, without moving it up the feeds.
*/
export interface DumpUpdatedMessage {
/** The discriminant. */
type: "dump_updated";
/** The dump as it is now. */
dump: Dump;
}
/** A dump was deleted. */
/**
* Sent by the server to every socket when a dump is deleted, or when a public
* one goes private. `WSProvider` drops it from every list.
*/
export interface DumpDeletedMessage {
/** The discriminant. */
type: "dump_deleted";
/** The deleted dump's id. */
dumpId: string;
}
/** A playlist was created; sent to whoever may see it. */
/**
* Sent by the server when a playlist is created, to every socket if it is
* public, else to its owner's. `WSProvider` passes it on to the playlist pages
* as `lastPlaylistEvent`, as it does the three messages below.
*/
export interface PlaylistCreatedMessage {
/** The discriminant. */
type: "playlist_created";
/** The new playlist. */
playlist: Playlist;
}
/** A playlist changed; sent to whoever may see it, or could before. */
/**
* Sent by the server when a playlist changes, to every socket if it is public
* or was until this change, so that a playlist gone private leaves the others'
* views.
*/
export interface PlaylistUpdatedMessage {
/** The discriminant. */
type: "playlist_updated";
/** The playlist as it is now. */
playlist: Playlist;
}
/** A playlist was deleted; sent to whoever could see it. */
/** Sent by the server when a playlist is deleted, to those who could see it. */
export interface PlaylistDeletedMessage {
/** The discriminant. */
type: "playlist_deleted";
/** The deleted playlist's id. */
playlistId: string;
/** Its owner's id. */
userId: string;
}
/** A playlist's dumps changed; each recipient gets the ones they may see. */
/**
* Sent by the server when dumps are added to, removed from or reordered in a
* playlist, to those who can see it.
*/
export interface PlaylistDumpsUpdatedMessage {
/** The discriminant. */
type: "playlist_dumps_updated";
/** The playlist's id. */
playlistId: string;
/** The members this recipient can see, in order. */
/**
* Only the dumps this socket's user may see, in order: another user's
* private dump is left out.
*/
dumpIds: string[];
/** Every member, as Playlist.dumpCount counts them. */
/** Counts every dump, hidden ones included, as `Playlist.dumpCount` does. */
dumpCount: number;
}
/** A user's public profile changed. */
/**
* Sent by the server to every socket after a profile edit, a role change or an
* avatar upload. `WSProvider` refreshes the user's avatar wherever it shows.
*/
export interface UserUpdatedMessage {
/** The discriminant. */
type: "user_updated";
/** The user as they are now. */
user: PublicUser;
}
/** A comment was posted. */
/**
* Sent by the server to every socket when a comment is posted on a public
* dump. Comments on a private dump are never broadcast. `WSProvider` passes it
* on to the dump's page as `lastCommentEvent`, as it does the two messages
* below.
*/
export interface CommentCreatedMessage {
/** The discriminant. */
type: "comment_created";
/** The new comment. */
comment: Comment;
}
/** A comment was edited. */
/**
* Sent by the server to every socket when a comment on a public dump is edited.
*/
export interface CommentUpdatedMessage {
/** The discriminant. */
type: "comment_updated";
/** The comment as it is now. */
comment: Comment;
}
/** A comment was deleted. */
/**
* Sent by the server to every socket when a comment on a public dump is
* deleted.
*/
export interface CommentDeletedMessage {
/** The discriminant. */
type: "comment_deleted";
/** The deleted comment's id. */
commentId: string;
/** The id of the dump it was under. */
/** Lets a dump's page ignore the comments of other dumps. */
dumpId: string;
}
/** A notification for the recipient. */
/**
* Sent by the notification service to every socket of the notified user.
* `WSProvider` bumps the unread count and chimes.
*/
export interface NotificationCreatedMessage {
/** The discriminant. */
type: "notification_created";
/** The notification. */
notification: Notification;
}
/** An action the client sent failed. */
/**
* Sent by the server to the socket whose message failed. With `dumpId` or
* `commentId`, `WSProvider` undoes that optimistic vote or like at once rather
* than after its 5 s timeout. With `scope`, the chatbox shows it, on the
* pending message `nonce` names or as a notice. Any other error is ignored.
*/
export interface ErrorMessage {
/** The discriminant. */
type: "error";
/** What went wrong. */
/** In English: the chatbox shows it untranslated. */
message?: string;
/**
* The dump of a failed vote. With `commentId` and `action`, it gives the
* client the context of an optimistic-action failure, so it can revert the
* exact pending vote or like immediately instead of waiting for its ACK
* timeout.
*/
dumpId?: string;
/** The comment of a failed like. */
commentId?: string;
/** The failed vote's or like's action. */
action?: "cast" | "remove";
/** Set when a chat action failed, so the client can surface it in the chat. */
scope?: "chat";
/** Echo of the failed chat_send's nonce. */
/** Set only on the failure of a {@link ChatSendMessage} that had one. */
nonce?: string;
}
/** The session ended — the account was deleted or its token revoked. */
/**
* Sent by the server to every socket of a user whose account is being deleted,
* just before it closes them. `WSProvider` signs the tab out.
*/
export interface ForceLogoutMessage {
/** The discriminant. */
type: "force_logout";
}
/** A chat message was posted. */
/**
* Sent by the server to every signed-in socket, the sender's included, when a
* chat message is posted. `WSProvider` adds it to the live chat, counts it
* unread unless the user is reading the chat, and may chime.
*/
export interface ChatMessageMessage {
/** The discriminant. */
type: "chat_message";
/** The new message. */
message: ChatMessage;
/** The sender's nonce, so its pending copy can be retired. */
/**
* The sender's {@link ChatSendMessage} nonce, by which its client swaps the
* pending copy for this one.
*/
nonce?: string;
}
/** A chat message was edited. */
/**
* Sent by the server to every signed-in socket when a chat message is edited.
*/
export interface ChatMessageUpdatedMessage {
/** The discriminant. */
type: "chat_message_updated";
/** The message as it is now. */
message: ChatMessage;
}
/** A chat message was deleted. */
/**
* Sent by the server to every signed-in socket when a chat message is deleted.
* The client also keeps the id, to show the replies to it as replies to a
* deleted message.
*/
export interface ChatMessageDeletedMessage {
/** The discriminant. */
type: "chat_message_deleted";
/** The deleted message's id. */
id: string;
}
/**
* The current set of users composing a chat message. Reuses
* {@link OnlineUser} so the client can render the same avatars as the
* presence row.
* Sent by the server to every signed-in socket when a user starts or stops
* typing in the chat, with everyone typing. The chatbox shows their avatars.
*/
export interface ChatTypingUpdateMessage {
/** The discriminant. */
type: "chat_typing_update";
/** Who is composing. */
users: OnlineUser[];
}
/** The user's read marker moved (from any of their tabs or devices). */
/**
* Sent by the server to every socket of a user after their
* {@link ChatReadMessage}, so that all their tabs and devices update the chat's
* badge.
*/
export interface ChatReadUpdateMessage {
/** The discriminant. */
type: "chat_read_update";
/** The new marker, as an ISO string. */
readAt: string;
/** How many messages came after it. */
/** Counts only the messages of other users after `readAt`. */
unread: number;
}
/** Any message the server sends. */
/**
* Sent by `services/ws-service.ts` and the WebSocket route, and handled by
* `WSProvider`'s `onmessage`.
*/
export type ServerToClientMessage =
| PingMessage
| PongMessage