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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6692-src-web-studio-held-posts-ts-pure-reads → main 1 commit fork 1 thread · 1 unresolved
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; | |
| 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 | }); |
new above
offline
Two copies of the same logic are guarded by the tests today; when wiring, keep that cross-check so they cannot drift.