v3: shared/ is documented like api/ — deno doc --lint over api/ and shared/ goes from 385 errors to none — every module of shared/ opens with a /** … @module */ block; every export and every field of an exported interface has JSDoc, each request body naming the endpoint that takes it and each response its endpoint; the // comments that described declarations (the mention pattern, the permission union and table, four socket messages, the error message's optimistic-action context) are /** */ now, and the socket protocol's heading comment, which sat on no declaration, is its module doc. The lint wanted explicit types on MENTION_RE and DUMP_MAX_FILE_SIZE_BYTES. Along the way: the chat edit is allowed to moderators too, which the new ChatEditMessage doc says; UpdateUserRequest says that PATCH /api/users/me takes only its description and email; playlist-service's reorderPlaylist @throws is rewrapped. mod.ts and the README no longer call shared/ undocumented. No behaviour changes; 205 backend tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-27 21:13:07 +00:00
parent d62e4a9883
commit b85a71d86e
18 changed files with 599 additions and 59 deletions

View File

@@ -1,3 +1,11 @@
/**
* 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.
*
* @module
*/
import type { ChatMessage } from "./chat.ts";
import type { Comment } from "./comment.ts";
import type { Dump } from "./dump.ts";
@@ -5,83 +13,117 @@ import type { Notification } from "./notification.ts";
import type { Playlist } from "./playlist.ts";
import type { PublicUser } from "./user.ts";
/**
* 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.
*/
// ── Client → Server ──────────────────────────────────────────────────────────
/** A keepalive; the other side answers {@link PongMessage}. Both directions. */
export interface PingMessage {
/** The discriminant. */
type: "ping";
}
/** The answer to a {@link PingMessage}. Both directions. */
export interface PongMessage {
/** The discriminant. */
type: "pong";
}
/** Upvotes a dump; answered by {@link VoteAckMessage}. */
export interface VoteCastMessage {
/** The discriminant. */
type: "vote_cast";
/** The dump's id. */
dumpId: string;
}
/** Withdraws an upvote; answered by {@link VoteAckMessage}. */
export interface VoteRemoveMessage {
/** The discriminant. */
type: "vote_remove";
/** The dump's id. */
dumpId: string;
}
/** Likes a comment; answered by {@link CommentLikeAckMessage}. */
export interface CommentLikeCastMessage {
/** The discriminant. */
type: "comment_like_cast";
/** The comment's id. */
commentId: string;
}
/** Withdraws a like; answered by {@link CommentLikeAckMessage}. */
export interface CommentLikeRemoveMessage {
/** The discriminant. */
type: "comment_like_remove";
/** The comment's id. */
commentId: string;
}
/** Posts a chat message; broadcast back as {@link ChatMessageMessage}. */
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. */
replyToId?: string;
/** Client-chosen id echoed on the broadcast (or on the error), so the
* sender can match the outcome to its pending message. */
/**
* Client-chosen id echoed on the broadcast (or on the error), so the
* sender can match the outcome to its pending message.
*/
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 ChatTypingUpdate.
/**
* 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}.
*/
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.
/**
* 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.
*/
export interface ChatFocusMessage {
/** The discriminant. */
type: "chat_focus";
/** Whether the chatbox is open. */
open: boolean;
}
/** Edits a chat message: the user's own, or any with `chat:moderate`. */
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`. */
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`.
/** The user has seen the chat up to the message created at `at`. */
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. */
export type ClientToServerMessage =
| PingMessage
| PongMessage
@@ -98,93 +140,156 @@ export type ClientToServerMessage =
// ── Server → Client ──────────────────────────────────────────────────────────
/** A signed-in user with at least one socket open. */
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. */
avatarVersion?: number;
}
/** The first message on a new socket: the state the client starts from. */
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 and the unread count behind it (signed-in
* users only). */
/**
* The user's chat read marker (signed-in users only), as an ISO string.
*/
chatReadAt?: string;
/** How many chat messages came after the read marker. */
unreadChatCount: number;
}
/** Who is online, sent to everyone whenever it 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.
*/
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. */
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. */
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. */
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. */
export interface DumpCreatedMessage {
/** The discriminant. */
type: "dump_created";
/** The new dump. */
dump: Dump;
}
/** A dump changed. */
export interface DumpUpdatedMessage {
/** The discriminant. */
type: "dump_updated";
/** The dump as it is now. */
dump: Dump;
}
/** A dump was deleted. */
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. */
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. */
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. */
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. */
export interface PlaylistDumpsUpdatedMessage {
/** The discriminant. */
type: "playlist_dumps_updated";
/** The playlist's id. */
playlistId: string;
/** The members this recipient can see, in order. */
dumpIds: string[];
@@ -192,39 +297,64 @@ export interface PlaylistDumpsUpdatedMessage {
dumpCount: number;
}
/** A user's public profile changed. */
export interface UserUpdatedMessage {
/** The discriminant. */
type: "user_updated";
/** The user as they are now. */
user: PublicUser;
}
/** A comment was posted. */
export interface CommentCreatedMessage {
/** The discriminant. */
type: "comment_created";
/** The new comment. */
comment: Comment;
}
/** A comment was edited. */
export interface CommentUpdatedMessage {
/** The discriminant. */
type: "comment_updated";
/** The comment as it is now. */
comment: Comment;
}
/** A comment was deleted. */
export interface CommentDeletedMessage {
/** The discriminant. */
type: "comment_deleted";
/** The deleted comment's id. */
commentId: string;
/** The id of the dump it was under. */
dumpId: string;
}
/** A notification for the recipient. */
export interface NotificationCreatedMessage {
/** The discriminant. */
type: "notification_created";
/** The notification. */
notification: Notification;
}
/** An action the client sent failed. */
export interface ErrorMessage {
/** The discriminant. */
type: "error";
/** What went wrong. */
message?: string;
// Context for optimistic-action failures so the client can revert the exact
// pending vote/like immediately instead of waiting for its ACK timeout.
/**
* 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";
@@ -232,41 +362,61 @@ export interface ErrorMessage {
nonce?: string;
}
/** The session ended — the account was deleted or its token revoked. */
export interface ForceLogoutMessage {
/** The discriminant. */
type: "force_logout";
}
/** A chat message was posted. */
export interface ChatMessageMessage {
/** The discriminant. */
type: "chat_message";
/** The new message. */
message: ChatMessage;
/** The sender's nonce, so its pending copy can be retired. */
nonce?: string;
}
/** A chat message was edited. */
export interface ChatMessageUpdatedMessage {
/** The discriminant. */
type: "chat_message_updated";
/** The message as it is now. */
message: ChatMessage;
}
/** A chat message was deleted. */
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 OnlineUser so the
// client can render the same avatars as the presence row.
/**
* The current set of users composing a chat message. Reuses
* {@link OnlineUser} so the client can render the same avatars as the
* presence row.
*/
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).
/** The user's read marker moved (from any of their tabs or devices). */
export interface ChatReadUpdateMessage {
/** The discriminant. */
type: "chat_read_update";
/** The new marker, as an ISO string. */
readAt: string;
/** How many messages came after it. */
unread: number;
}
/** Any message the server sends. */
export type ServerToClientMessage =
| PingMessage
| PongMessage