Files
gerbeur/shared/ws.ts

525 lines
15 KiB
TypeScript

/**
* 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<ServerToClientMessage>` 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` is
* 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[];
}
/**
* 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;