/** * Live updates, votes, likes and the chat travel over one WebSocket between * `WSProvider` in the client and `routes/ws.ts` in the server. This module * types its messages, 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 when a dump is posted, to every socket if it is public * and to its poster's alone if it is private. It also goes to everyone but the * poster when a private dump goes public, since to them it is new. * `WSProvider` puts it at the top of its recent dumps. */ export interface DumpCreatedMessage { type: "dump_created"; dump: Dump; } /** * Sent by the server when a dump changes, to every socket if it is public and * to its poster's alone if it is private, as when it has just gone private. * `WSProvider` refreshes it in place, without moving it up the feeds. */ export interface DumpUpdatedMessage { type: "dump_updated"; dump: Dump; } /** * Sent by the server when a dump is deleted, to every socket if it was public * and to its poster's alone if it was private. It also goes to everyone but * the poster when a public dump goes private. `WSProvider` drops it from every * list. */ export interface DumpDeletedMessage { type: "dump_deleted"; dumpId: string; } /** * Sent by the server when the dumps related to some dumps changed: a note or * a comment gained or lost a link to a dump, or a dump that has such links * turned public. It goes to the sockets whose user may see both ends of a * link. The `Dump` page showing one of `dumpIds` fetches its related dumps * again. */ export interface DumpRelatedChangedMessage { type: "dump_related_changed"; /** * The dump whose links changed, then the dumps at their other end, without * those this socket's user may not see. */ dumpIds: string[]; } /** * Sent by the server when a playlist is created, to every socket if it is * public, else to its owner's. It also goes to everyone but the owner when a * private playlist goes public, since to them it is new. `WSProvider` passes * it on to the playlist pages as a `playlist` event, 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, * else to its owner's, as when it has just gone private. Its `dumpCount` and * `lastDumpAt` are the socket's user's, as in `playlist_created`. */ export interface PlaylistUpdatedMessage { type: "playlist_updated"; playlist: Playlist; } /** * Sent by the server when a playlist is deleted, to those who could see it. It * also goes to everyone but the owner when a public playlist goes private. */ 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, and when one of its dumps is deleted or * changes visibility, which changes what its viewers see of 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[]; /** * When the latest of them was added, the playlist's `lastDumpAt` for this * socket's user. It is missing when `dumpIds` is empty. */ lastDumpAt?: Date; } /** * Sent by the server to every socket after a profile edit, a rename, a role * change or an avatar upload. `WSProvider` refreshes the user's avatar * wherever it shows, and passes it on as a `user` event, which updates their * profile and their name on the lists. */ export interface UserUpdatedMessage { type: "user_updated"; user: PublicUser; } /** * Sent by the server when a comment is posted, to every socket if its dump is * public, else to the dump's poster's. `WSProvider` passes it on to the dump's * page and the dump lists as a `comment` event, as it does the two messages * below. */ export interface CommentCreatedMessage { type: "comment_created"; comment: Comment; } /** Sent by the server when a comment is edited, as when one is posted. */ export interface CommentUpdatedMessage { type: "comment_updated"; comment: Comment; } /** Sent by the server when a comment is deleted, as when one is posted. */ 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 notification service to every socket of a user who opened their * notifications, which marks them all read. `WSProvider` zeroes the bell's * count, in the other tabs and devices too. */ export interface NotificationsReadMessage { type: "notifications_read"; } /** * Sent by the category service to every socket after a category is created, * edited or deleted. `CategoriesProvider` fetches the categories again. */ export interface CategoriesChangedMessage { type: "categories_changed"; } /** * 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 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 the server to every socket of a user who followed or unfollowed a * user or a playlist, so that `FollowProvider` in their other tabs refetches * the follow status. */ export interface FollowsChangedMessage { type: "follows_changed"; } /** * 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 | DumpRelatedChangedMessage | PlaylistCreatedMessage | PlaylistUpdatedMessage | PlaylistDeletedMessage | PlaylistDumpsUpdatedMessage | UserUpdatedMessage | CommentCreatedMessage | CommentUpdatedMessage | CommentDeletedMessage | NotificationCreatedMessage | NotificationsReadMessage | CategoriesChangedMessage | ErrorMessage | ChatMessageMessage | ChatMessageUpdatedMessage | ChatMessageDeletedMessage | ChatTypingUpdateMessage | ChatReadUpdateMessage | FollowsChangedMessage;