Skip to content
61 linesCodeBlameRaw
1/**
2 * What a message may hold and how pages of them are read. Pure, so it is
3 * tested apart from the service.
4 */
5
6/** The longest message body, in characters. */
7export const MAX_BODY = 40_000;
8
9/** Messages on one page when none is asked for, and the most on one page. */
10export const DEFAULT_PAGE = 50;
11export const MAX_PAGE = 200;
12
13/** How long "is typing" shows after a person's keystroke. */
14export const PERSON_TYPING_MS = 5_000;
15
16/** A body as kept, or why it cannot be: something to say, and not too much. */
17export function messageBody(body: unknown, hasCard = false): { ok: true; body: string } | { ok: false; message: string } {
18 const text = typeof body === "string" ? body.replace(/\s+$/, "") : "";
19 if (!text.trim() && !hasCard) return { ok: false, message: "A message needs something in it." };
20 if (text.length > MAX_BODY) return { ok: false, message: `A message is at most ${MAX_BODY.toLocaleString("en-US")} characters.` };
21 return { ok: true, body: text };
22}
23
24/** How many messages a page holds: the default, or what was asked within 1 to the most. */
25export function pageSize(limit: unknown): number {
26 const n = typeof limit === "number" && Number.isFinite(limit) ? Math.floor(limit) : DEFAULT_PAGE;
27 return Math.min(MAX_PAGE, Math.max(1, n));
28}
29
30/**
31 * A page read one past its size, newest first: the page, and the cursor
32 * for the next one back (its oldest id), or null when this is the start.
33 */
34export function pageOf<T extends { id: string }>(rows: T[], size: number): { rows: T[]; older: string | null } {
35 if (rows.length <= size) return { rows, older: null };
36 const page = rows.slice(0, size);
37 return { rows: page, older: page[page.length - 1].id };
38}
39
40/**
41 * Whether a frame from a client's socket says it is typing in `channelId`:
42 * `{"type":"typing"}`, optionally naming the channel. A frame naming
43 * another channel is ignored.
44 */
45export function isTypingFrame(frame: unknown, channelId: string): boolean {
46 if (typeof frame !== "string" || frame.length > 1024) return false;
47 let said: unknown;
48 try {
49 said = JSON.parse(frame);
50 } catch {
51 return false;
52 }
53 if (!said || typeof said !== "object") return false;
54 const { type, channel_id } = said as { type?: unknown; channel_id?: unknown };
55 return type === "typing" && (channel_id === undefined || channel_id === null || channel_id === channelId);
56}
57
58/** The UTC day a time falls on, as the meter keys it. */
59export function meterDay(iso: string): string {
60 return iso.slice(0, 10);
61}