v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* The socket's provider: one connection per session, and the live state it
|
||||
* feeds — presence, votes, likes, dumps, playlists, comments, users,
|
||||
* notifications and the chat.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
@@ -51,40 +59,70 @@ import { setTitleBadge } from "../../hooks/shared/useDocumentTitle.ts";
|
||||
import { playChime } from "../../utils/shared/sound.ts";
|
||||
import { mentionsUser } from "../../utils/chat/text.ts";
|
||||
|
||||
interface WSProviderProps {
|
||||
/** The props of {@link WSProvider}. */
|
||||
export interface WSProviderProps {
|
||||
/** The app, which reads the live state. */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/** The longest wait between reconnection attempts, in ms. */
|
||||
const MAX_BACKOFF = 30_000;
|
||||
/**
|
||||
* How long an optimistic vote or like waits for its acknowledgement before it
|
||||
* is rolled back, in ms.
|
||||
*/
|
||||
const ACK_TIMEOUT = 5_000;
|
||||
/**
|
||||
* How long a connection attempt may take before the socket is shown as down, in
|
||||
* ms.
|
||||
*/
|
||||
const CONNECT_TIMEOUT = 2_500;
|
||||
// Cap the live "new dumps" buffer so a long-lived session can't grow it without
|
||||
// bound. Feeds merge these on top of their own paginated list, so a generous
|
||||
// cap is plenty — older live arrivals are reachable via normal pagination.
|
||||
/**
|
||||
* Cap on the live "new dumps" buffer, so a long-lived session can't grow it
|
||||
* without bound. Feeds merge these on top of their own paginated list, so a
|
||||
* generous cap is plenty — older live arrivals are reachable via normal
|
||||
* pagination.
|
||||
*/
|
||||
const MAX_RECENT_DUMPS = 100;
|
||||
// Cap the live chat buffer so a long-lived session can't grow it without bound.
|
||||
// Older messages remain reachable via the /api/chat history endpoint.
|
||||
/**
|
||||
* Cap on the live chat buffer, so a long-lived session can't grow it without
|
||||
* bound. Older messages remain reachable via the /api/chat history endpoint.
|
||||
*/
|
||||
const MAX_CHAT_MESSAGES = 200;
|
||||
// How long a sent chat message may go without its echo before it's shown as
|
||||
// failed, and how often the read marker may go out.
|
||||
/**
|
||||
* How long a sent chat message may go without its echo before it's shown as
|
||||
* failed, in ms.
|
||||
*/
|
||||
const CHAT_SEND_TIMEOUT = 10_000;
|
||||
/** How often the read marker may go out, in ms. */
|
||||
const CHAT_READ_THROTTLE = 1_000;
|
||||
// A failed send whose text turns up in the resynced history within this long
|
||||
// of being sent did reach the server — only its echo was lost.
|
||||
/**
|
||||
* A failed send whose text turns up in the resynced history within this long of
|
||||
* being sent did reach the server — only its echo was lost.
|
||||
*/
|
||||
const CHAT_ECHO_WINDOW = 60_000;
|
||||
|
||||
/** An optimistic vote awaiting its acknowledgement. */
|
||||
interface PendingVote {
|
||||
/** The timer that rolls it back. */
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
/** Undoes it. */
|
||||
rollback: () => void;
|
||||
}
|
||||
|
||||
/** An optimistic like awaiting its acknowledgement. */
|
||||
interface PendingLike {
|
||||
/** The timer that rolls it back. */
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
/** Undoes it. */
|
||||
rollback: () => void;
|
||||
}
|
||||
|
||||
// Minimal runtime check: verify the `type` field is a known string so we can
|
||||
// safely cast to the discriminated union and let TypeScript narrow from there.
|
||||
/**
|
||||
* Returns `data` as a server message, or null when it isn't one. The runtime
|
||||
* check is minimal: the `type` field must be a string, so the cast to the
|
||||
* discriminated union is safe and TypeScript narrows from there.
|
||||
*/
|
||||
function parseWSMessage(data: string): Wire<ServerToClientMessage> | null {
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
@@ -97,7 +135,16 @@ function parseWSMessage(data: string): Wire<ServerToClientMessage> | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function WSProvider({ children }: WSProviderProps) {
|
||||
/**
|
||||
* Provides {@link WSContext}, {@link WSChatContext}, {@link WSPresenceContext}
|
||||
* and {@link AvatarOverrideContext}. It connects with the session's token and
|
||||
* reconnects with a growing backoff; each reconnect bumps `connectionEpoch` so
|
||||
* pages backfill what they missed. Votes, likes and chat messages apply at once
|
||||
* and are confirmed or rolled back by the server's answer. A new notification
|
||||
* or chat message chimes, and the unread count goes in the tab title. A
|
||||
* `force_logout` signs the user out; a change of session clears the live state.
|
||||
*/
|
||||
export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
const { token, user, logout } = useAuth();
|
||||
const userId = user?.id ?? null;
|
||||
const [wsStatus, setWSStatus] = useState<
|
||||
|
||||
Reference in New Issue
Block a user