/** * 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` and deserializes what it keeps. * * @module */ import type { ChatMessage } from "./chat.ts"; import type { Comment } from "./comment.ts"; import type { Dump } from "./dump.ts"; import type { Notification } from "./notification.ts"; import type { Playlist } from "./playlist.ts"; import type { PublicUser } from "./user.ts"; /** * 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 { type: "ping"; } /** * 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 { type: "pong"; } /** * 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 { type: "vote_cast"; dumpId: string; } /** Sent by `WSProvider` to withdraw an upvote, as {@link VoteCastMessage}. */ export interface VoteRemoveMessage { type: "vote_remove"; dumpId: string; } /** * Sent by `WSProvider` when the user likes a comment, and handled as * {@link VoteCastMessage} is, with {@link CommentLikeAckMessage} for the ack. */ export interface CommentLikeCastMessage { type: "comment_like_cast"; commentId: string; } /** * Sent by `WSProvider` to withdraw a like, as {@link CommentLikeCastMessage}. */ export interface CommentLikeRemoveMessage { type: "comment_like_remove"; commentId: string; } /** * 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 { type: "chat_send"; body: string; /** Ignored if longer than 64 characters or naming no message. */ replyToId?: string; /** * 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 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 { type: "chat_typing"; typing: boolean; } /** * 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 { type: "chat_focus"; /** True only while the chatbox is open in a visible tab. */ open: boolean; } /** * 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 { type: "chat_edit"; id: string; body: string; } /** * Sent by `WSProvider` when the user deletes a chat message, as * {@link ChatEditMessage}. The server broadcasts * {@link ChatMessageDeletedMessage}. */ export interface ChatDeleteMessage { type: "chat_delete"; id: string; } /** * 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 { type: "chat_read"; at: string; } /** * Sent by `WSProvider`, and dispatched by the route's `handleMessage`, which * checks each field's type before using it. */ export type ClientToServerMessage = | PingMessage | PongMessage | VoteCastMessage | VoteRemoveMessage | CommentLikeCastMessage | CommentLikeRemoveMessage | ChatSendMessage | ChatTypingMessage | ChatFocusMessage | ChatEditMessage | ChatDeleteMessage | ChatReadMessage; /** * 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 { userId: string; username: string; hasAvatar: boolean; /** * 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; } /** * 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 { type: "welcome"; users: OnlineUser[]; myVotes: string[]; myCommentLikes: string[]; unreadNotificationCount: number; /** Absent on an anonymous socket. */ chatReadAt?: string; unreadChatCount: number; } /** * Sent by the server to every socket when a socket opens or closes, and when * a connected user's avatar changes. */ export interface PresenceUpdateMessage { type: "presence_update"; users: OnlineUser[]; } /** * 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 { type: "votes_update"; dumpId: string; voteCount: number; voterId: string; action: "cast" | "remove"; } /** * 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 { type: "vote_ack"; dumpId: string; action: "cast" | "remove"; voteCount: number; } /** * 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 { type: "comment_likes_update"; commentId: string; likeCount: number; likerId: string; action: "cast" | "remove"; } /** * Sent by the server to the socket that liked or unliked a comment, as * {@link VoteAckMessage} is for a vote. */ export interface CommentLikeAckMessage { type: "comment_like_ack"; commentId: string; action: "cast" | "remove"; likeCount: number; } /** * 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 { type: "dump_created"; dump: Dump; } /** * 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 { type: "dump_updated"; dump: Dump; } /** * 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 { type: "dump_deleted"; dumpId: string; } /** * 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 { type: "playlist_created"; playlist: Playlist; } /** * 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 { type: "playlist_updated"; playlist: Playlist; } /** Sent by the server when a playlist is deleted, to those who could see it. */ export interface PlaylistDeletedMessage { type: "playlist_deleted"; playlistId: string; userId: string; } /** * 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 { type: "playlist_dumps_updated"; playlistId: string; /** * Only the dumps this socket's user may see, in order: another user's * private dump is left out. */ dumpIds: string[]; /** Counts every dump, hidden ones included, as `Playlist.dumpCount` does. */ dumpCount: number; } /** * 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 { type: "user_updated"; user: PublicUser; } /** * 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 { type: "comment_created"; comment: Comment; } /** * Sent by the server to every socket when a comment on a public dump is edited. */ export interface CommentUpdatedMessage { type: "comment_updated"; comment: Comment; } /** * Sent by the server to every socket when a comment on a public dump is * deleted. */ export interface CommentDeletedMessage { type: "comment_deleted"; commentId: string; /** Lets a dump's page ignore the comments of other dumps. */ dumpId: string; } /** * Sent by the notification service to every socket of the notified user. * `WSProvider` bumps the unread count and chimes. */ export interface NotificationCreatedMessage { type: "notification_created"; notification: Notification; } /** * 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 { type: "error"; /** In English: the chatbox shows it untranslated. */ message?: string; dumpId?: string; commentId?: string; action?: "cast" | "remove"; scope?: "chat"; /** Set only on the failure of a {@link ChatSendMessage} that had one. */ nonce?: string; } /** * 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 { type: "force_logout"; } /** * 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 { type: "chat_message"; message: ChatMessage; /** * The sender's {@link ChatSendMessage} nonce, by which its client swaps the * pending copy for this one. */ nonce?: string; } /** * Sent by the server to every signed-in socket when a chat message is edited. */ export interface ChatMessageUpdatedMessage { type: "chat_message_updated"; message: ChatMessage; } /** * 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 { type: "chat_message_deleted"; id: string; } /** * 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 { type: "chat_typing_update"; users: OnlineUser[]; } /** * 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 { type: "chat_read_update"; readAt: string; /** Counts only the messages of other users after `readAt`. */ unread: number; } /** * Sent by `services/ws-service.ts` and the WebSocket route, and handled by * `WSProvider`'s `onmessage`. */ export type ServerToClientMessage = | PingMessage | PongMessage | WelcomeMessage | PresenceUpdateMessage | VotesUpdateMessage | VoteAckMessage | CommentLikesUpdateMessage | CommentLikeAckMessage | DumpCreatedMessage | DumpUpdatedMessage | DumpDeletedMessage | PlaylistCreatedMessage | PlaylistUpdatedMessage | PlaylistDeletedMessage | PlaylistDumpsUpdatedMessage | UserUpdatedMessage | CommentCreatedMessage | CommentUpdatedMessage | CommentDeletedMessage | NotificationCreatedMessage | ErrorMessage | ForceLogoutMessage | ChatMessageMessage | ChatMessageUpdatedMessage | ChatMessageDeletedMessage | ChatTypingUpdateMessage | ChatReadUpdateMessage;