jevstrudel.git / worker / src / activity.ts

What is happening on the site, newest first: the jev panel's activity tab (website/src/jev/ActivityTab.jsx), from GET /jev/data/activity (data.ts). Every source is a bounded query, newest first, from before a cursor; the merge takes the newest limit of them all and answers the cursor for the next page (next, the oldest item's time), so paging never skips or repeats more than items sharing one millisecond.

revision a listener published a song or its next revision, with Jev's verdict as it stands (pending, fine, spam, abuse) scored the art critic scored a fine revision comment a comment on a song, with its verdict pitch a song idea, with its verdict take someone played a song as Jev arranged it (a stored performance): who (null when signed out), the song, how many sections it played and how it ended, and the take's id, for its replay link

Held and pending text is not in the feed: an item says Jev held it and why, and the data tab has its text; the feed carries the body of fine comments and pitches only.

21export const ACTIVITY_MAX = 50;
22export const ACTIVITY_DEFAULT = 30;
24type Author = { id: string; name: string };
25export type Activity =
26  | { kind: 'revision'; at: number; song: string; rev: number; title: string; author: Author; screen: string; art: number | null }
27  | { kind: 'scored'; at: number; song: string; rev: number; title: string; author: Author; art: number }
28  | {
29      kind: 'comment';
30      at: number;
31      id: string;
32      author: Author;
33      on: { site: string } | { listener: string; title: string | null };
34      screen: string;
35      body: string | null;
36    }
37  | { kind: 'pitch'; at: number; id: string; author: Author; screen: string; body: string | null }
38  | {
39      kind: 'take';
40      at: number;
41      id: string;
42      song: string;
43      // a listener song's newest title (a site song's the page has)
44      title: string | null;
45      player: Author | null;
46      sections: number;
47      ended: string;
48    };
49
50type Row = Record<string, unknown>;
51const author = (r: Row): Author => ({ id: r.author_id as string, name: r.author as string });

The newest limit of every source, before before.

54export function merge(sources: Activity[][], limit: number): { items: Activity[]; next: number | null } {
55  const items = sources
56    .flat()
57    .sort((a, b) => b.at - a.at)
58    .slice(0, limit);
59  return { items, next: items.length === limit ? items[items.length - 1].at : null };
60}
62export async function activity(db: D1Database, before: number, limit: number) {
63  const [revisions, scored, comments, pitches, played] = (await db.batch([
64    db
65      .prepare(
66        `SELECT r.song, r.rev, r.title, r.created_at AS at, r.screen, r.art, u.id AS author_id, u.display_name AS author
67         FROM song_revisions r JOIN listener_songs s ON s.id = r.song JOIN users u ON u.id = s.user_id
68         WHERE r.created_at < ? ORDER BY r.created_at DESC LIMIT ?`,
69      )
70      .bind(before, limit),
71    db
72      .prepare(
73        `SELECT r.song, r.rev, r.title, r.scored_at AS at, r.art, u.id AS author_id, u.display_name AS author
74         FROM song_revisions r JOIN listener_songs s ON s.id = r.song JOIN users u ON u.id = s.user_id
75         WHERE r.scored_at IS NOT NULL AND r.scored_at < ? ORDER BY r.scored_at DESC LIMIT ?`,
76      )
77      .bind(before, limit),
78    db
79      .prepare(
80        `SELECT c.id, c.site_song, c.listener_song, c.body, c.created_at AS at, c.screen,
81           u.id AS author_id, u.display_name AS author,
82           (SELECT title FROM song_revisions r WHERE r.song = c.listener_song AND r.screen = 'fine'
83              ORDER BY r.rev DESC LIMIT 1) AS listener_title
84         FROM comments c JOIN users u ON u.id = c.user_id
85         WHERE c.created_at < ? ORDER BY c.created_at DESC LIMIT ?`,
86      )
87      .bind(before, limit),
88    db
89      .prepare(
90        `SELECT p.id, p.body, p.created_at AS at, p.screen, u.id AS author_id, u.display_name AS author
91         FROM pitches p JOIN users u ON u.id = p.user_id
92         WHERE p.created_at < ? ORDER BY p.created_at DESC LIMIT ?`,
93      )
94      .bind(before, limit),
95    db
96      .prepare(
97        `SELECT p.id, p.song, p.recorded_at AS at, p.ended, u.id AS author_id, u.display_name AS author,
98           (SELECT count(*) FROM performance_segments s WHERE s.performance = p.id AND s.jev = coalesce(p.form_jev, 0)) AS sections,
99           (SELECT r.title FROM song_revisions r WHERE p.song = 'listener:' || r.song AND r.screen = 'fine' ORDER BY r.rev DESC LIMIT 1) AS title
100         FROM performances p LEFT JOIN users u ON u.id = p.user_id
101         WHERE p.recorded_at < ? ORDER BY p.recorded_at DESC LIMIT ?`,
102      )
103      .bind(before, limit),
104  ])) as D1Result<Row>[];
105
106  const sources: Activity[][] = [
107    revisions.results.map((r) => ({
108      kind: 'revision' as const,
109      at: r.at as number,
110      song: r.song as string,
111      rev: r.rev as number,
112      title: r.title as string,
113      author: author(r),
114      screen: r.screen as string,
115      art: (r.art as number | null) ?? null,
116    })),
117    scored.results.map((r) => ({
118      kind: 'scored' as const,
119      at: r.at as number,
120      song: r.song as string,
121      rev: r.rev as number,
122      title: r.title as string,
123      author: author(r),
124      art: r.art as number,
125    })),
126    comments.results.map((r) => ({
127      kind: 'comment' as const,
128      at: r.at as number,
129      id: r.id as string,
130      author: author(r),
131      on: r.site_song
132        ? { site: r.site_song as string }
133        : { listener: r.listener_song as string, title: (r.listener_title as string | null) ?? null },
134      screen: r.screen as string,
135      body: r.screen === 'fine' ? (r.body as string) : null,
136    })),
137    pitches.results.map((r) => ({
138      kind: 'pitch' as const,
139      at: r.at as number,
140      id: r.id as string,
141      author: author(r),
142      screen: r.screen as string,
143      body: r.screen === 'fine' ? (r.body as string) : null,
144    })),
145    played.results.map((r) => ({
146      kind: 'take' as const,
147      at: r.at as number,
148      id: r.id as string,
149      song: r.song as string,
150      title: (r.title as string | null) ?? null,
151      player: typeof r.author_id === 'string' ? author(r) : null,
152      sections: r.sections as number,
153      ended: r.ended as string,
154    })),
155  ];
156  return merge(sources, limit);
157}