v3: every doc opens with the role, then the mechanism, then the details — the docs were written bottom-up: what a thing does first ("Declares the three contexts WSProvider fills…"), details that only make sense once its purpose is known next, and the reason it exists last ("…so that a feed doesn't re-render"), so a reader had nothing to hang the first sentences on. The README's rule caused it, since a module doc "opened with what the module does"; it now opens with the module's role in the app, and the value-side phrasing of a type ("Returned to Y by X") comes once the role is said, with WSContext as the before and after. WSContext.ts is rewritten that way throughout, and so are the docs elsewhere that buried their purpose in a trailing "so that" (the migrations, og, image-proxy, public-address, useFading, useFeedCache, the drafts), opened on a generic verb that hid it ("Holds", "Declares", "Loads", "Defines", "Types": the contexts and providers, the shared types, the models, the hooks), or were label fragments ("The page of a dump…", "A row of tabs."). A function whose verb is its action ("Stores session"), a props or result type, and a protocol message ("Sent by X when Y") already say their role and are left as they are. Only comments changed, and the catalogs only move their references; deno fmt, deno lint and doc:lint are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Declares the context through which `Avatar` learns of avatars changed since
|
||||
* the page loaded. `WSProvider` fills it from `user_updated` messages.
|
||||
* Shows a user's new avatar everywhere as soon as they change it, without a
|
||||
* reload. `WSProvider` fills this context from `user_updated` messages, and
|
||||
* `Avatar` reads it.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -21,8 +22,9 @@ export interface AvatarOverride {
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds an {@link AvatarOverride} by user id. It is apart from `WSContext`, so
|
||||
* the many avatars of a page don't re-render on every vote or comment.
|
||||
* Shows changed avatars without a reload, as an {@link AvatarOverride} by user
|
||||
* id. It is apart from `WSContext`, so the many avatars of a page don't
|
||||
* re-render on every vote or comment.
|
||||
*/
|
||||
export const AvatarOverrideContext: React.Context<
|
||||
Record<string, AvatarOverride>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* Declares the three contexts `WSProvider` fills from the WebSocket, and the
|
||||
* types they carry. They are split by how often each part changes, so that a
|
||||
* feed doesn't re-render with every chat message or presence change.
|
||||
* Live updates from the server reach the pages through the three contexts
|
||||
* declared here: votes and likes, dumps, playlists, comments, profiles and
|
||||
* notifications in one, the chat in another, who is online in the third. They
|
||||
* are split so that a feed doesn't re-render with every chat message or
|
||||
* presence change. `WSProvider` fills them from its WebSocket, and components
|
||||
* read them with `useWS`, `useWSChat` and `useWSPresence`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -16,8 +19,9 @@ import type { Playlist } from "../../../shared/playlist.ts";
|
||||
import type { PublicUser } from "../../../shared/user.ts";
|
||||
|
||||
/**
|
||||
* Set as `lastVoteEvent` by `WSProvider` on a `votes_update`. The profile
|
||||
* pages read it to add or drop a dump from a user's upvoted list.
|
||||
* Keeps a user's upvoted list live, on their profile and on `UserUpvoted`.
|
||||
* `WSProvider` sets it as `lastVoteEvent` on each `votes_update`, and
|
||||
* `useUpvotedList` adds or drops the dump.
|
||||
*/
|
||||
export interface VoteEvent {
|
||||
dumpId: string;
|
||||
@@ -26,9 +30,11 @@ export interface VoteEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set as `lastPlaylistEvent` by `WSProvider` on each `playlist_*` message.
|
||||
* The pages that show playlists apply it, some through `usePlaylistListSync`.
|
||||
* Which fields are set depends on `type`.
|
||||
* Keeps the playlists on screen up to date, in lists, on a playlist's page and
|
||||
* on a dump's page. `WSProvider` sets it as `lastPlaylistEvent` on each
|
||||
* `playlist_*` message; lists apply it through `usePlaylistListSync`, a
|
||||
* playlist's page through `usePlaylist`. Which fields are set depends on
|
||||
* `type`.
|
||||
*/
|
||||
export interface PlaylistEvent {
|
||||
type: "created" | "updated" | "deleted" | "dumps_updated";
|
||||
@@ -47,9 +53,9 @@ export interface PlaylistEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set as `lastCommentEvent` by `WSProvider` on each `comment_*` message. The
|
||||
* `Dump` page applies it to its thread, and `useDumpListSync` to its comment
|
||||
* counts.
|
||||
* Keeps comment threads and comment counts live. `WSProvider` sets it as
|
||||
* `lastCommentEvent` on each `comment_*` message; the `Dump` page applies it
|
||||
* to its thread, and `useDumpListSync` to the counts on dump cards.
|
||||
*/
|
||||
export interface CommentEvent {
|
||||
type: "created" | "deleted" | "updated";
|
||||
@@ -61,16 +67,18 @@ export interface CommentEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set as `lastUserEvent` by `WSProvider` on a `user_updated`, which the
|
||||
* server sends on any change to a profile. `useProfile` applies it.
|
||||
* Keeps a profile page live while its user edits their profile. `WSProvider`
|
||||
* sets it as `lastUserEvent` on each `user_updated`, which the server sends on
|
||||
* any change to a profile, and `useProfile` applies it.
|
||||
*/
|
||||
export interface UserEvent {
|
||||
user: PublicUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chosen in `ChatModal` for the reply bar, and kept on the pending message so
|
||||
* it shows the reply before the server's echo. Only `id` goes to the server.
|
||||
* Quotes the message a chat message replies to. `ChatModal` shows it in the
|
||||
* reply bar once the user picks it, and it stays on the pending message so
|
||||
* the quote shows before the server's echo. Only `id` goes to the server.
|
||||
*/
|
||||
export interface ChatReplyTarget {
|
||||
id: string;
|
||||
@@ -79,9 +87,9 @@ export interface ChatReplyTarget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Held by `WSProvider` for a chat message this client sent and the server
|
||||
* hasn't echoed back yet. `ChatModal` shows it, as `PendingItem`, after the
|
||||
* messages the server has.
|
||||
* Shows a chat message the moment the user sends it, and lets them retry it if
|
||||
* it fails. `WSProvider` holds it until the server echoes the message back, and
|
||||
* `ChatModal` shows it, as `PendingItem`, after the messages the server has.
|
||||
*/
|
||||
export interface PendingChatMessage {
|
||||
/**
|
||||
@@ -102,8 +110,8 @@ export interface PendingChatMessage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set as `chatError` by `WSProvider` when the server refuses a chat edit or
|
||||
* delete. `ChatModal` shows it for a few seconds.
|
||||
* Tells the user the server refused their chat edit or delete. `WSProvider`
|
||||
* sets it as `chatError`, and `ChatModal` shows it for a few seconds.
|
||||
*/
|
||||
export interface ChatError {
|
||||
/** Tells apart two errors with the same message. */
|
||||
@@ -112,60 +120,75 @@ export interface ChatError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided by `WSProvider` through {@link WSContext}, read with `useWS`. The
|
||||
* `last*Event` fields hold only the latest event of their kind: a consumer
|
||||
* reacts in an effect when one changes, and an event that another replaces
|
||||
* within the same render is missed. After a reconnect, `connectionEpoch` tells
|
||||
* pages to fetch what they missed.
|
||||
* Carries everything live but the chat and presence, for pages to lay over
|
||||
* what they fetched. `WSProvider` provides it through {@link WSContext}, and
|
||||
* components read it with `useWS`. Each `last*Event` field holds only the
|
||||
* latest event of its kind: a consumer reacts in an effect when one changes,
|
||||
* and an event that another replaces within the same render is missed. After a
|
||||
* reconnect, `connectionEpoch` tells pages to fetch what they missed.
|
||||
*/
|
||||
export interface WSContextValue {
|
||||
wsStatus: "connecting" | "connected" | "disconnected";
|
||||
/** Set while the socket is down, for the banner that says so. */
|
||||
wsErrorMessage: string | null;
|
||||
/**
|
||||
* Counts known to the socket, by dump id: pushed by the server, or changed
|
||||
* optimistically. A missing dump shows its fetched `voteCount`.
|
||||
* Shows vote counts live. It holds, by dump id, the counts the server pushed
|
||||
* or this tab changed optimistically; a dump missing from it shows its
|
||||
* fetched `voteCount`.
|
||||
*/
|
||||
voteCounts: Record<string, number>;
|
||||
/** Reset from the server's `welcome` on every connection. */
|
||||
/**
|
||||
* Marks the dumps this user upvoted, for their vote buttons. The server's
|
||||
* `welcome` resets it on every connection.
|
||||
*/
|
||||
myVotes: Set<string>;
|
||||
/** As `voteCounts`, for comments. */
|
||||
likeCounts: Record<string, number>;
|
||||
/** As `myVotes`, for comments. */
|
||||
myLikes: Set<string>;
|
||||
/**
|
||||
* Dumps that arrived this session, newest first and at most 100: the ones
|
||||
* the server announced, which are public, and the ones this user posted,
|
||||
* which may be private. `Index` puts them above the feed it fetched.
|
||||
* Shows new dumps on the index as they are posted, above the feed `Index`
|
||||
* fetched. It holds, newest first and at most 100, the ones the server
|
||||
* announced this session, which are public, and the ones this user posted,
|
||||
* which may be private.
|
||||
*/
|
||||
recentDumps: Dump[];
|
||||
/**
|
||||
* It holds the dumps deleted this session, or made private by someone else,
|
||||
* for the lists to filter out. A dump made public again leaves it.
|
||||
* Takes deleted dumps off every list at once. It holds the dumps deleted this
|
||||
* session, or made private by someone else; a dump made public again leaves
|
||||
* it.
|
||||
*/
|
||||
deletedDumpIds: Set<string>;
|
||||
lastVoteEvent: VoteEvent | null;
|
||||
/**
|
||||
* It holds the dump as its latest `dump_created` or `dump_updated` has it,
|
||||
* for the lists to update it in place, or put it back where it was.
|
||||
* Keeps a dump up to date in every list that shows it. It holds the dump as
|
||||
* its latest `dump_created` or `dump_updated` has it, and a list updates it
|
||||
* in place, or puts it back where it was.
|
||||
*/
|
||||
lastDumpEvent: Dump | null;
|
||||
lastPlaylistEvent: PlaylistEvent | null;
|
||||
deletedPlaylistIds: Set<string>;
|
||||
lastCommentEvent: CommentEvent | null;
|
||||
lastUserEvent: UserEvent | null;
|
||||
/** Read from the `welcome`, then counted up by each new notification. */
|
||||
/**
|
||||
* Drives the bell's badge. It comes from the `welcome`, then each new
|
||||
* notification counts it up.
|
||||
*/
|
||||
unreadNotificationCount: number;
|
||||
/** Read by the bell, which rings, and by the notifications page. */
|
||||
/**
|
||||
* Makes the bell ring, and puts the notification on the notifications page
|
||||
* as it arrives.
|
||||
*/
|
||||
lastNotification: Notification | null;
|
||||
/**
|
||||
* Incremented on each reconnect, never on the first connection, so pages
|
||||
* fetch again what they missed while the socket was down.
|
||||
* Tells pages to fetch again what they missed while the socket was down. It
|
||||
* goes up on each reconnect, never on the first connection.
|
||||
*/
|
||||
connectionEpoch: number;
|
||||
/**
|
||||
* Incremented when the user follows or unfollows something in another tab
|
||||
* (or this one), so `FollowProvider` fetches the follow status again.
|
||||
* Keeps the Follow buttons right when the user follows or unfollows in
|
||||
* another tab. It goes up on every follow change, in any tab, and
|
||||
* `FollowProvider` fetches the follow status again.
|
||||
*/
|
||||
followsVersion: number;
|
||||
/**
|
||||
@@ -182,8 +205,9 @@ export interface WSContextValue {
|
||||
/** Withdraws a like, as {@link WSContextValue.castVote} casts a vote. */
|
||||
removeCommentLike: (commentId: string, shownCount?: number) => void;
|
||||
/**
|
||||
* Adds a dump this user just posted to `recentDumps`, without waiting for
|
||||
* its broadcast, which a private dump never gets. `DumpCreateModal` calls it.
|
||||
* Shows a dump this user just posted on the index without waiting for its
|
||||
* broadcast, which a private dump never gets. `DumpCreateModal` calls it,
|
||||
* and it adds the dump to `recentDumps`.
|
||||
*/
|
||||
injectDump: (dump: Dump) => void;
|
||||
/**
|
||||
@@ -194,36 +218,43 @@ export interface WSContextValue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided by `WSProvider` through {@link WSChatContext}, read with
|
||||
* `useWSChat`. The socket sends chat messages only to signed-in users, so for
|
||||
* a visitor it stays empty.
|
||||
* Carries the chat's live side, which `ChatModal` lays over the history it
|
||||
* fetches and the chat buttons show as a badge. `WSProvider` provides it
|
||||
* through {@link WSChatContext}, and components read it with `useWSChat`. The
|
||||
* socket sends chat only to signed-in users, so for a visitor it stays empty.
|
||||
*/
|
||||
export interface WSChatContextValue {
|
||||
/**
|
||||
* The messages received this session, at most 200, for `ChatModal` to merge
|
||||
* with the history it fetches. A reconnect empties it while the chat is
|
||||
* closed, since `ChatModal` fetches everything again on open.
|
||||
* Shows chat messages as they arrive. It holds the ones received this
|
||||
* session, at most 200, which `ChatModal` merges with the history it
|
||||
* fetches. A reconnect empties it while the chat is closed, since `ChatModal`
|
||||
* fetches everything again on open.
|
||||
*/
|
||||
chatMessages: ChatMessage[];
|
||||
/**
|
||||
* Messages deleted this session, which `ChatModal` filters out of its
|
||||
* fetched history.
|
||||
* Takes the messages deleted this session out of the history `ChatModal`
|
||||
* fetched.
|
||||
*/
|
||||
deletedChatIds: Set<string>;
|
||||
/**
|
||||
* Messages from others after the read marker. The server keeps the marker,
|
||||
* so the count agrees across tabs and devices.
|
||||
* Drives the chat buttons' unread badge. It counts the messages from others
|
||||
* after the read marker, which the server keeps, so the count agrees across
|
||||
* tabs and devices.
|
||||
*/
|
||||
unreadChatCount: number;
|
||||
/**
|
||||
* The time of the newest message the user has seen, which the server keeps.
|
||||
* Null until the `welcome`, and for a visitor.
|
||||
* Places the "new" divider when the chat opens. It holds the time of the
|
||||
* newest message the user has seen, which the server keeps, and is null
|
||||
* until the `welcome`, and for a visitor.
|
||||
*/
|
||||
chatReadAt: Date | null;
|
||||
pendingChatMessages: PendingChatMessage[];
|
||||
/** Only for edits and deletes. A failed send shows on its pending message. */
|
||||
chatError: ChatError | null;
|
||||
/** Includes the user when they are typing: `ChatModal` filters them out. */
|
||||
/**
|
||||
* Shows who is typing. It includes the user themselves, whom `ChatModal`
|
||||
* filters out.
|
||||
*/
|
||||
typingUsers: OnlineUser[];
|
||||
/** Trims `body` and sends it as a pending message. Ignores a blank body. */
|
||||
sendChatMessage: (body: string, replyTo?: ChatReplyTarget) => void;
|
||||
@@ -244,10 +275,10 @@ export interface WSChatContextValue {
|
||||
*/
|
||||
markChatRead: (at: Date) => void;
|
||||
/**
|
||||
* Replaces the live buffer's messages between `from` and `to` with
|
||||
* `messages`, which `ChatModal` fetched after a reconnect. A message missing
|
||||
* from them was deleted while the socket was down. A failed send found among
|
||||
* them did reach the server, and leaves the pending list.
|
||||
* Repairs the live buffer after a reconnect. It replaces its messages between
|
||||
* `from` and `to` with `messages`, which `ChatModal` fetched. A message
|
||||
* missing from them was deleted while the socket was down. A failed send
|
||||
* found among them did reach the server, and leaves the pending list.
|
||||
*/
|
||||
reconcileChat: (messages: ChatMessage[], from: Date, to: Date) => void;
|
||||
/**
|
||||
@@ -257,23 +288,27 @@ export interface WSChatContextValue {
|
||||
*/
|
||||
setChatTyping: (typing: boolean) => void;
|
||||
/**
|
||||
* Tells the server whether the user is reading the chat: open, in a visible
|
||||
* tab. The server sends no notification for a mention the user reads live,
|
||||
* and this client doesn't count the message unread.
|
||||
* Spares the user a notification for a mention they read live. It tells the
|
||||
* server whether the chat is open in a visible tab; while it is, the server
|
||||
* sends no notification for a mention, and this client doesn't count the
|
||||
* message unread.
|
||||
*/
|
||||
setChatOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided by `WSProvider` through {@link WSPresenceContext}, read with
|
||||
* `useWSPresence`.
|
||||
* Carries who is online, for `PresenceRow`. `WSProvider` provides it through
|
||||
* {@link WSPresenceContext}, and components read it with `useWSPresence`.
|
||||
*/
|
||||
export interface WSPresenceContextValue {
|
||||
/** The signed-in users with a socket open, each once. */
|
||||
onlineUsers: OnlineUser[];
|
||||
}
|
||||
|
||||
/** Given to a consumer outside `WSProvider`, as if it were still connecting. */
|
||||
/**
|
||||
* Lets a component render outside `WSProvider`, as if the socket were
|
||||
* connecting.
|
||||
*/
|
||||
export const defaultWSValue: WSContextValue = {
|
||||
wsStatus: "connecting",
|
||||
wsErrorMessage: null,
|
||||
@@ -301,7 +336,7 @@ export const defaultWSValue: WSContextValue = {
|
||||
clearUnreadNotifications: () => {},
|
||||
};
|
||||
|
||||
/** Given to a consumer outside `WSProvider`: an empty chat. */
|
||||
/** Lets a component render outside `WSProvider`, with an empty chat. */
|
||||
export const defaultWSChatValue: WSChatContextValue = {
|
||||
chatMessages: [],
|
||||
deletedChatIds: new Set(),
|
||||
@@ -322,25 +357,25 @@ export const defaultWSChatValue: WSChatContextValue = {
|
||||
setChatOpen: () => {},
|
||||
};
|
||||
|
||||
/** Given to a consumer outside `WSProvider`: nobody is online. */
|
||||
/** Lets a component render outside `WSProvider`, with nobody online. */
|
||||
export const defaultWSPresenceValue: WSPresenceContextValue = {
|
||||
onlineUsers: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds the connection, the votes and likes, and the latest live event of
|
||||
* each kind. Read it with `useWS`.
|
||||
* Carries the live state pages lay over what they fetched: the connection, the
|
||||
* votes and likes, and the latest event of each kind. Read it with `useWS`.
|
||||
*/
|
||||
export const WSContext: React.Context<WSContextValue> = createContext<
|
||||
WSContextValue
|
||||
>(defaultWSValue);
|
||||
/** Holds the chat's live stream and read state. Read it with `useWSChat`. */
|
||||
/** Carries the chat's live stream and read state. Read it with `useWSChat`. */
|
||||
export const WSChatContext: React.Context<WSChatContextValue> = createContext<
|
||||
WSChatContextValue
|
||||
>(
|
||||
defaultWSChatValue,
|
||||
);
|
||||
/** Holds who is online. Read it with `useWSPresence`. */
|
||||
/** Carries who is online. Read it with `useWSPresence`. */
|
||||
export const WSPresenceContext: React.Context<WSPresenceContextValue> =
|
||||
createContext<
|
||||
WSPresenceContextValue
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Holds the app's one WebSocket to `/ws` and turns its messages into the state
|
||||
* of `WSContext`, `WSChatContext`, `WSPresenceContext` and
|
||||
* `AvatarOverrideContext`. `App` mounts it around every page. The protocol is
|
||||
* `shared/ws.ts`, and the server's side is `api/routes/ws.ts`.
|
||||
* Brings the server's live updates into the app. It holds the one WebSocket to
|
||||
* `/ws` and turns its messages into the state of `WSContext`, `WSChatContext`,
|
||||
* `WSPresenceContext` and `AvatarOverrideContext`. `App` mounts it around every
|
||||
* page. The protocol is `shared/ws.ts`, and the server's side is
|
||||
* `api/routes/ws.ts`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -94,9 +95,9 @@ const CHAT_READ_THROTTLE = 1_000;
|
||||
const CHAT_ECHO_WINDOW = 60_000;
|
||||
|
||||
/**
|
||||
* Held by {@link useOptimisticToggles} for an item whose toggles the server
|
||||
* hasn't all acknowledged yet. `count` and `on` are as before the first of
|
||||
* them, for a failure to restore.
|
||||
* Lets a failed toggle restore what was shown before the first click the server
|
||||
* hasn't acknowledged. {@link useOptimisticToggles} holds one for each such
|
||||
* item; `count` and `on` are as they were before that click.
|
||||
*/
|
||||
interface PendingToggle {
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
Reference in New Issue
Block a user