Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/domain/landing-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
* Someone who already has boards gets their boards; everyone else
* gets the demo, when there is one.
*/
/**
* How many of the visitor's boards the landing needs to know about to
* choose: two tells "one" from "several". A decision threshold, not a
* tunable limit.
*/
export const LANDING_BOARDS_NEEDED = 2;

export type LandingSecondary =
| { kind: "demo" }
| { kind: "myBoard"; slug: string }
Expand Down
11 changes: 6 additions & 5 deletions src/server/landing-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { cookies } from "next/headers";
import { getConfig } from "../config";
import { getPolicy } from "../policy/server";
import { getBoardRepository } from "./board-repository";
import { listMyBoards } from "./my-boards";
import { LANDING_BOARDS_NEEDED } from "../domain/landing-actions";
import { liveBoardSlugs } from "./my-boards";
import { getUserRepository } from "./user-repository";
import { resolveUserSession, USER_COOKIE_NAME } from "./user-session";

/**
* The signed-in visitor's own board slugs for the landing (PIN-264),
* newest activity first, as My boards lists them. Null when sign-in
* The signed-in visitor's own live board slugs for the landing
* (PIN-264), at most LANDING_BOARDS_NEEDED: only the claim checks,
* never the My boards library reads. Null when sign-in
* is off, there is no session cookie (no read at all for anonymous
* visitors), the session is not valid, or anything fails: the
* landing then shows its usual demo button. The slugs only go back
Expand All @@ -25,8 +27,7 @@ export async function viewerBoardSlugs(): Promise<string[] | null> {
const users = getUserRepository();
const user = await resolveUserSession(cookieValue, { users, sessionSecret: cfg.sessionSecret, policy: getPolicy() });
if (!user) return null;
const cards = await listMyBoards(user, { boards: getBoardRepository(), users, plusMode: cfg.plusMode });
return cards.map((card) => card.slug);
return await liveBoardSlugs(user, { boards: getBoardRepository(), users }, LANDING_BOARDS_NEEDED);
} catch {
return null;
}
Expand Down
43 changes: 42 additions & 1 deletion src/server/my-boards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { InMemoryBoardRepository } from "../domain/board-repository";
import type { UserProfile } from "../domain/user";
import { InMemoryUserRepository } from "../domain/user-repository";
import { resolvePolicy } from "../policy/policy";
import { listMyBoards, removeMyBoard } from "./my-boards";
import { listMyBoards, liveBoardSlugs, removeMyBoard } from "./my-boards";
import { findClaimedParticipant, linkParticipantToUser } from "./user-session";

const USER: UserProfile = { uid: "synthetic-alice", createdAt: 1, updatedAt: 1 };
Expand Down Expand Up @@ -265,3 +265,44 @@ describe("removeMyBoard (PIN-166)", () => {
expect(await users.getUserBoard(USER.uid, OTHER_SLUG)).not.toBeNull();
});
});

describe("liveBoardSlugs (PIN-264)", () => {
async function setup() {
const boards = new InMemoryBoardRepository();
const users = new InMemoryUserRepository();
await users.putUser(USER);
for (const [slug, pid, owner] of [["b1", "p1", USER.uid], ["stale", "ps", "synthetic-bob"], ["b2", "p2", USER.uid], ["b3", "p3", USER.uid]] as const) {
await boards.createBoard(board(slug, 1));
await boards.putParticipant(participant(pid, slug, owner));
}
for (const [boardId, participantId, at] of [["b1", "p1", 10], ["gone", "pg", 15], ["stale", "ps", 20], ["b2", "p2", 30], ["b3", "p3", 40]] as const) {
await users.putUserBoard(USER.uid, { boardId, participantId, claimedAt: at, updatedAt: at });
}
return { boards, users };
}

it("keeps the same claim checks as My boards and stops at the limit", async () => {
const deps = await setup();
const all = await liveBoardSlugs(USER, deps, 10);
expect([...all].sort()).toEqual(["b1", "b2", "b3"]);
expect(await liveBoardSlugs(USER, deps, 2)).toHaveLength(2);
expect(await liveBoardSlugs(USER, deps, 0)).toEqual([]);
// Same set as the full library, never a gone or re-claimed board.
const cards = await listMyBoards(USER, deps);
expect([...all].sort()).toEqual(cards.map((c) => c.slug).sort());
});

it("never reads a board's elements, pins, scenes or events", async () => {
const deps = await setup();
const touched: string[] = [];
for (const name of ["listElementsByOwner", "listPins", "listScenes", "listEvents"] as const) {
const original = deps.boards[name].bind(deps.boards) as (...args: unknown[]) => unknown;
(deps.boards as unknown as Record<string, unknown>)[name] = (...args: unknown[]) => {
touched.push(name);
return original(...args);
};
}
await liveBoardSlugs(USER, deps, 2);
expect(touched).toEqual([]);
});
});
26 changes: 26 additions & 0 deletions src/server/my-boards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ export interface MyBoardsDeps {
plusMode?: PlusMode;
}

/**
* The user's live board slugs, at most `limit` (PIN-264, Bugbot on
* #242): the same claim checks as listMyBoards (the board exists and
* the participant still carries this user's claim) without the
* per-board elements, pins, scenes and events reads. It stops as soon
* as it has `limit`. The landing only needs to tell none, one (and
* its slug) and several apart.
*/
export async function liveBoardSlugs(
user: UserProfile,
deps: Pick<MyBoardsDeps, "boards" | "users">,
limit: number,
): Promise<string[]> {
const slugs: string[] = [];
if (limit <= 0) return slugs;
for (const entry of await deps.users.listUserBoards(user.uid)) {
const board = await deps.boards.getBoard(entry.boardId);
if (!board) continue;
const participant = await deps.boards.getParticipant(entry.boardId, entry.participantId);
if (!participant || participant.userId !== user.uid) continue;
slugs.push(board.slug);
if (slugs.length >= limit) break;
}
return slugs;
}

export async function listMyBoards(
user: UserProfile,
deps: MyBoardsDeps,
Expand Down
Loading