Public: anyone can read this; the crew can change it.

src/web/studio/held_posts.ts: pure reads of which live posts are new and what the button says #6707

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6692-src-web-studio-held-posts-ts-pure-reads → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6692-src-web-studio-held-posts-ts-pure-reads was merged into main as 34130a8.

Review

Sign in to review this change.
2 files changed +74 −0
src/web/studio/held_posts.ts added +39
@@ -0,0 +1,39 @@
1 /**
2 * Which live posts arrived since the page was drawn, and what the button
3 * that offers them says. Pure reads: ids in, ids and text out. `heldPostsJs`
4 * is the same two functions as plain JS for the page.
5 */
6
7 /** The ids in `fresh` not in `shown`, in `fresh`'s order, each once. */
8 export function newPostIds(shown: readonly string[], fresh: readonly string[]): string[] {
9 const seen = new Set(shown);
10 const out: string[] = [];
11 for (const id of fresh) {
12 if (seen.has(id)) continue;
13 seen.add(id);
14 out.push(id);
15 }
16 return out;
17 }
18
19 /** `1 new post`, `3 new posts`, and nothing for none. */
20 export function newPostsLabel(n: number): string {
21 if (!(n >= 1)) return "";
22 return n === 1 ? "1 new post" : `${n} new posts`;
23 }
24
25 export const heldPostsJs = /* js */ `
26 function newPostIds(shown, fresh) {
27 var seen = {}, out = [], i;
28 for (i = 0; i < shown.length; i++) seen['$' + shown[i]] = 1;

Two copies of the same logic are guarded by the tests today; when wiring, keep that cross-check so they cannot drift.

29 for (i = 0; i < fresh.length; i++) {
30 if (seen['$' + fresh[i]]) continue;
31 seen['$' + fresh[i]] = 1;
32 out.push(fresh[i]);
33 }
34 return out;
35 }
36 function newPostsLabel(n) {
37 return !(n >= 1) ? '' : n === 1 ? '1 new post' : n + ' new posts';
38 }
39 `;
test/held-posts.test.ts added +35
@@ -0,0 +1,35 @@
1 import { expect, test } from "bun:test";
2 import { heldPostsJs, newPostIds, newPostsLabel } from "../src/web/studio/held_posts.ts";
3
4 const js = new Function(`${heldPostsJs}; return { newPostIds, newPostsLabel };`)() as {
5 newPostIds: typeof newPostIds;
6 newPostsLabel: typeof newPostsLabel;
7 };
8
9 const cases: [string[], string[], string[]][] = [
10 [["a", "b"], ["a", "b"], []],
11 [["a", "b"], ["x", "y", "z", "a", "b"], ["x", "y", "z"]],
12 [["a", "b", "c"], ["d", "a", "c"], ["d"]],
13 [["a"], ["b", "b", "a", "c", "b", "c"], ["b", "c"]],
14 [[], [], []],
15 [["__proto__"], ["__proto__", "constructor", "$a"], ["constructor", "$a"]],
16 ];
17
18 test("newPostIds: none new, three on top, one removed and one added, duplicates", () => {
19 for (const [shown, fresh, want] of cases) {
20 expect(newPostIds(shown, fresh)).toEqual(want);
21 expect(js.newPostIds(shown, fresh)).toEqual(want);
22 }
23 });
24
25 test("newPostsLabel pluralises and is empty for none", () => {
26 for (const [n, want] of [[0, ""], [1, "1 new post"], [2, "2 new posts"], [3, "3 new posts"], [-1, ""], [NaN, ""]] as const) {
27 expect(newPostsLabel(n)).toBe(want);
28 expect(js.newPostsLabel(n)).toBe(want);
29 }
30 });
31
32 test("heldPostsJs is small and has no script tag", () => {
33 expect(heldPostsJs.replace(/\s+/g, " ").length).toBeLessThan(600);
34 expect(heldPostsJs).not.toContain("<script");
35 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs