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:
190
shared/ws.ts
190
shared/ws.ts
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user