Files
gerbeur/shared/dump.ts
khannurien c3594e4c27 v4: what the server and the client both state is declared once — several rules were written out in two or more files, some already drifting apart. A link's site name (its host without www.) was computed in five places, now shared/url.ts's siteHost; dumpFileVersion, which the frontend and the server had to keep equal by hand, is one function in shared/dump.ts; the Markdown flattening of the chat's reply previews and the digest's quotes is shared/markdown.ts's markdownToPlainText, so the digest also keeps an image's alt text; an attachment's URL and how to find one in a text are shared/attachment.ts, which the editor, Markdown and attachment-service read, its id pattern the new UUID_RE that the file and thumbnail routes check ids with. On the server, the Bandcamp, YouTube playlist and link preview caches use one TtlCache (api/lib/ttl-cache.ts) instead of three copies of the same expiry and eviction; the link previews' theme-color and the emails' theme tokens read colours with one parser (api/lib/css-color.ts), and a transparent theme-color now gives no tint; the WebSocket route sends through ws-service's send, the welcome included, so it is type-checked; fetch.ts strips an IPv6 literal's brackets with public-address's bareHostname; and a category's slug is checked by the same validateSlug as a dump's, with its own length. The migrations keep their frozen copies, as they document. 272 backend tests and 677 frontend tests pass, and deno check, tsc, eslint and doc:lint are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:16:52 +00:00

157 lines
4.8 KiB
TypeScript

/**
* A dump is what users post: a link or a file, with a title and a note. This
* module types it as the API sends it, with its link preview and the bodies
* that create and edit it. The backend's `dump-service.ts` produces these types
* and the frontend's dump pages and create form consume them.
*
* @module
*/
/**
* Built by a provider of `rich-content-service.ts` when a link is posted or
* previewed, and stored with the dump as JSON. The frontend's
* `RichContentCard` and player read it.
*/
export interface RichContent {
/**
* The provider that read the page: `youtube`, `bandcamp`, `soundcloud` or
* `generic`. The player picks how to play the link by it.
*/
type: string;
url: string;
siteName?: string;
title?: string;
description?: string;
/**
* A real preview image (og:image, a large content image, a video still).
* Never a favicon — an absent value means "this page offers no artwork",
* which is what makes the generated placeholder possible.
*/
thumbnailUrl?: string;
/** Used as the placeholder's glyph. */
faviconUrl?: string;
/** The page's declared brand color, normalized to `#rrggbb`. */
accentColor?: string;
/** Set for a YouTube link only. */
videoId?: string;
embedUrl?: string;
}
/**
* Returned by every endpoint that answers dumps, and pushed to clients by the
* `dump_created` and `dump_updated` socket messages. A URL dump has `url` and
* `richContent`, a file dump the `file…` fields. A private dump reaches its
* poster only.
*/
export interface Dump {
id: string;
kind: "url" | "file";
title: string;
/**
* Names the dump in its page's path, `dumpPath` (`shared/paths.ts`) building
* it with {@link ownerUsername}. Made from the title on creation, it changes
* only when edited, and is unique among its poster's dumps.
*/
slug: string;
/** The poster's note on the dump, in Markdown. */
comment?: string;
userId: string;
/** The poster's current name, which the dump's path starts with. */
ownerUsername: string;
createdAt: Date;
updatedAt?: Date;
url?: string;
richContent?: RichContent;
fileName?: string;
fileMime?: string;
fileSize?: number;
voteCount: number;
/** Deleted comments are not counted. */
commentCount: number;
isPrivate: boolean;
/** Set when the poster uploaded a custom thumbnail. */
thumbnailMime?: string;
categoryIds?: string[];
}
/**
* Returned by `GET /api/dumps?url=` for each dump already posted with the
* link being typed, and shown by the create form's `DuplicateNotice`. It holds
* only what the notice displays and links to.
*/
export interface DumpUrlMatch {
id: string;
slug: string;
title: string;
/** The poster's username. */
ownerUsername: string;
createdAt: Date;
voteCount: number;
commentCount: number;
}
/**
* Returned by `GET /api/remote/preview` to the create form, which shows it before the
* link is posted.
*/
export interface LinkPreview {
/**
* False when the page couldn't be fetched. `richContent` is then a stub
* holding the host name, and this flag is how the form tells a dead link
* from a page with no preview.
*/
reached: boolean;
/**
* Null only for a link that doesn't parse, which the route rejects before
* fetching anyway.
*/
richContent: RichContent | null;
}
/**
* Passed by the create form to `POST /api/dumps` as a JSON body, to post a
* link. A file is posted as a multipart form instead.
*/
export interface CreateUrlDumpRequest {
url: string;
/** Replaces the title read from the page, when the poster edited it. */
title?: string;
comment?: string;
isPrivate?: boolean;
categoryIds?: string[];
}
/**
* Passed to `PUT /api/dumps/:dumpId` by the edit page, and by the dump page's
* inline title edit. A field left out stays as it is.
*/
export interface UpdateDumpRequest {
/** Ignored for a file dump. A changed link has its preview fetched again. */
url?: string;
title?: string;
/**
* A new slug, which the edit page's slug field sends. A title change never
* changes the slug by itself.
*/
slug?: string;
/** `null` removes the note. */
comment?: string | null;
isPrivate?: boolean;
/** Replaces the current categories. */
categoryIds?: string[];
}
/**
* Returns a file dump's version, which the file and thumbnail URLs carry as
* `?v=`: the frontend's `dumpFileUrl` and `dumpThumbnailUrl`, and the digest's
* thumbnails. It changes whenever the file may have, so the file and thumbnail
* routes cache a URL naming the current one for good and revalidate any other.
* Without its dates, a dump gets a version no route serves as current.
*/
export function dumpFileVersion(
dump: { fileSize?: number; createdAt?: Date; updatedAt?: Date },
): string {
const at = dump.updatedAt ?? dump.createdAt;
return `${dump.fileSize ?? 0}-${at ? at.getTime() : 0}`;
}