Skip to content

Make BoardRepository board-scoped - #3

Merged
boubou666 merged 2 commits into
mainfrom
worktree-board-scoped-repository
Aug 4, 2026
Merged

boubou666 merged 2 commits into
mainfrom
worktree-board-scoped-repository

Conversation

@boubou666

@boubou666 boubou666 commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Step 1 of supporting multiple boards.

The repository held a single board under one key. It now addresses boards by id and can enumerate them — which is what a board picker needs, and what a server-backed implementation will answer with a query rather than a scan.

list()                      → [{ id, title, updatedAt }], newest first
load(id)                    → record | null
save(id, board, { title })  → boolean
rename(id, title)           → boolean
remove(id)                  → boolean
migrateLegacy({ toId })     → boolean

Boards are stored as an envelope — { id, title, updatedAt, board } — because a picker needs a label and an ordering, and neither belongs in the object graph. updatedAt comes from an injected now, so it stays deterministic in tests, matching how timing is handled everywhere else.

createApp gains boardId (defaulting to 'default'), namespace and now, replacing storageKey. Behaviour with a single board is unchanged, so this is inert until a board picker exists — which is the point of landing it separately.

Two deliberate behaviour changes

An empty board now restores as empty. Previously load() treated a board with no objects as "nothing saved" and re-seeded the starter board, so clearing your canvas was silently undone by a refresh. That was a bug; board-scoping forced the question. There is a test naming it explicitly.

Boards written by the single-board version are adopted. Anything under the old peerthink:board key migrates to default on first run and the old key is cleared, so the deployed site does not drop anyone's work. It refuses to overwrite an existing board, and leaves the old key alone if the copy cannot be written.

Tests

257 tests, 99.8% line coverage. src/platform/storage.js is at 100%, including every failure mode — unreadable storage, corrupt JSON, full quota, and storage that cannot be enumerated.

Summary by CodeRabbit

  • New Features

    • Added support for multiple saved boards with unique IDs and titles.
    • Boards can now be listed, loaded, renamed, and deleted.
    • Added automatic migration of existing board data.
    • Autosave now preserves changes for the selected board.
    • Unknown or newly selected boards open with a usable empty board.
  • Bug Fixes

    • Improved handling of malformed, missing, or unavailable saved data.
    • Added isolation between board storage areas to prevent unintended overwrites.

Step 1 of supporting multiple boards. The repository held a single board
under one key; it now addresses boards by id and can enumerate them, which
is what a board picker needs and what a server-backed implementation will
answer with a query.

  list()                        -> [{ id, title, updatedAt }], newest first
  load(id)                      -> record | null
  save(id, board, { title })    -> boolean
  rename(id, title)             -> boolean
  remove(id)                    -> boolean
  migrateLegacy({ toId })       -> boolean

Boards are stored as an envelope — id, title, updatedAt, board — because a
picker needs a label and an ordering, and neither belongs in the object
graph. updatedAt comes from an injected `now`, so it stays deterministic in
tests, matching how timing is handled everywhere else.

createApp gains `boardId` (defaulting to 'default'), `namespace` and `now`,
replacing `storageKey`. Behaviour with one board is unchanged, so this is
inert until a board picker exists.

Two deliberate behaviour changes:

- An empty board now restores as empty. Previously `load()` treated a board
  with no objects as "nothing saved" and re-seeded the starter board, so
  clearing your canvas was silently undone by a refresh.
- Boards written by the single-board version are adopted on first run and
  the old key is cleared, so the deployed site does not lose anyone's work.

257 tests, 99.8% line coverage.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd074b70-386f-466c-85bd-753227f4230f

📥 Commits

Reviewing files that changed from the base of the PR and between bf4e515 and 611530b.

⛔ Files ignored due to path filters (4)
  • dist/assets/index-CGKfOYFM.js is excluded by !**/dist/**
  • dist/assets/index-CGKfOYFM.js.map is excluded by !**/dist/**, !**/*.map
  • dist/assets/index-DUTNQzWv.css is excluded by !**/dist/**
  • dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (5)
  • public/js/platform/storage.js
  • test/browser/main.test.js
  • test/helpers/browser.js
  • test/node/server.test.js
  • test/node/storage.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/browser/main.test.js
  • test/node/storage.test.js
  • public/js/platform/storage.js

📝 Walkthrough

Walkthrough

The application now stores versioned board records by namespace and board ID. Startup migrates legacy data and restores the selected record. Autosave persists the selected board ID. Tests cover storage operations, isolation, migration, polling, and dynamic server ports.

Changes

Multi-board persistence

Layer / File(s) Summary
Board repository and migration
public/js/platform/storage.js, test/node/storage.test.js
The repository stores versioned records by namespace and board ID. It supports listing, loading, saving, renaming, removing, validation, timestamps, legacy migration, and null-repository operations.
Application restoration and autosave
public/js/app.js, public/js/core/autosave.js, test/browser/app.test.js, test/browser/main.test.js, test/node/autosave.test.js
Startup migrates and restores the selected board record. Autosave persists the selected board ID. Tests cover titles, fallback behavior, board and namespace isolation, and migration.

Test execution support

Layer / File(s) Summary
Browser polling and dynamic server setup
test/helpers/browser.js, test/node/server.test.js
Browser tests gain polling with labeled timeouts. Server tests use an available dynamic port and validate the selected startup URL.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant createApp
  participant repository
  participant autosave
  participant localStorage
  createApp->>repository: migrateLegacy to boardId
  createApp->>repository: load boardId
  repository-->>createApp: board record and title
  createApp->>autosave: configure boardId
  autosave->>repository: save boardId and board
  repository->>localStorage: persist board record
Loading

Suggested reviewers: janacek

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: making BoardRepository support board-scoped persistence.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-board-scoped-repository

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
test/node/storage.test.js (1)

247-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that a failed copy leaves the legacy record in place.

This test proves the return value only. The property that matters is that no data is lost: if the copy cannot be written, peerthink:board must still hold the original board.

💚 Proposed addition
     test('reports failure when the copy cannot be written', () => {
       storage.data.set(LEGACY_KEY, JSON.stringify(legacy));
       storage.failOn = 'set';
       assert.equal(repo.migrateLegacy(), false);
+
+      storage.failOn = null;
+      assert.equal(storage.data.get(LEGACY_KEY), JSON.stringify(legacy), 'nothing is lost');
+      assert.equal(repo.load(DEFAULT_BOARD_ID), null);
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/node/storage.test.js` around lines 247 - 251, Update the test case for
migrateLegacy when storage.failOn is 'set' to also assert that storage.data
still contains the original legacy record under LEGACY_KEY after migration
fails, while preserving the existing false return-value assertion.
test/browser/app.test.js (1)

120-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the localStorage cleanup so a failed assertion does not leak keys.

Both tests write real keys into the page's localStorage, then remove them on the last line. If an assertion fails first, the removal never runs. peerthink:board:second-board or scratch:board:default then survives into later tests in the same page, and repository.list() in other tests sees an extra board.

withSecondApp already has a finally block for second.destroy(). Put the key removal in a try/finally in the test bodies, or extend withSecondApp to clear the keys it created.

♻️ Proposed change for the board-id test; apply the same shape at lines 142-157
   test('a board id keeps two boards in the same storage apart', async () => {
-    const result = await withSecondApp(
-      `storage: window.localStorage, boardId: 'second-board', autosaveDelay: 1`,
-      ...
-    );
-
-    assert.equal(result.boardId, 'second-board');
-    assert.equal(result.written, true);
-    assert.equal(result.leaked, false, "the page's own board is untouched");
-    assert.equal(result.listed, true, 'and it shows up in the board list');
-    await page.eval(`localStorage.removeItem('peerthink:board:second-board')`);
+    try {
+      const result = await withSecondApp(
+        `storage: window.localStorage, boardId: 'second-board', autosaveDelay: 1`,
+        ...
+      );
+
+      assert.equal(result.boardId, 'second-board');
+      assert.equal(result.written, true);
+      assert.equal(result.leaked, false, "the page's own board is untouched");
+      assert.equal(result.listed, true, 'and it shows up in the board list');
+    } finally {
+      await page.eval(`localStorage.removeItem('peerthink:board:second-board')`);
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/browser/app.test.js` around lines 120 - 157, Move localStorage cleanup
for both tests into guaranteed finally blocks: ensure
peerthink:board:second-board is removed in the board-id test and
scratch:board:default in the namespace test even when assertions fail. Preserve
the existing assertions and withSecondApp lifecycle while applying the same
cleanup shape to both test bodies.
test/browser/main.test.js (1)

6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the key constants instead of restating them.

public/js/platform/storage.js exports LEGACY_KEY. test/node/storage.test.js imports it. This file redeclares both LEGACY_KEY and the default board key as literals. If the namespace or the key layout changes, this suite keeps asserting the old strings and still passes.

KEY has no exported counterpart today. Build it from the exported pieces so it tracks the source.

♻️ Proposed change
-const LEGACY_KEY = 'peerthink:board';
-const KEY = 'peerthink:board:default';
+import { DEFAULT_BOARD_ID, LEGACY_KEY } from '../../public/js/platform/storage.js';
+
+const KEY = `peerthink:board:${DEFAULT_BOARD_ID}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/browser/main.test.js` around lines 6 - 11, Update the key declarations
in the browser storage tests to import LEGACY_KEY from
public/js/platform/storage.js and construct the default board KEY from exported
storage-key pieces rather than hardcoded literals. Preserve the existing record
helper and ensure both constants stay synchronized with the production namespace
and key layout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@public/js/platform/storage.js`:
- Around line 115-133: Update migrateLegacy to adopt and remove LEGACY_KEY only
when the repository is using the default namespace; non-default namespaces must
return without consuming the shared legacy board. Preserve the existing
migration, save, and cleanup behavior for the owning namespace.
- Around line 70-76: Update storage.list() to derive each returned record’s id
from the storage key used by keyFor(id), rather than trusting the parsed
record.id. Ensure updatedAt has a valid fallback before the sort comparator
runs, while preserving the existing filtering and projected result shape.

In `@test/browser/main.test.js`:
- Around line 68-76: Replace the fixed page.sleep(600) in the “it saves under
the board id, not a single global key” test with the existing
app.autosave.flush() readiness call before asserting localStorage contents.
Preserve the board ID assertion and both storage-key assertions.

---

Nitpick comments:
In `@test/browser/app.test.js`:
- Around line 120-157: Move localStorage cleanup for both tests into guaranteed
finally blocks: ensure peerthink:board:second-board is removed in the board-id
test and scratch:board:default in the namespace test even when assertions fail.
Preserve the existing assertions and withSecondApp lifecycle while applying the
same cleanup shape to both test bodies.

In `@test/browser/main.test.js`:
- Around line 6-11: Update the key declarations in the browser storage tests to
import LEGACY_KEY from public/js/platform/storage.js and construct the default
board KEY from exported storage-key pieces rather than hardcoded literals.
Preserve the existing record helper and ensure both constants stay synchronized
with the production namespace and key layout.

In `@test/node/storage.test.js`:
- Around line 247-251: Update the test case for migrateLegacy when
storage.failOn is 'set' to also assert that storage.data still contains the
original legacy record under LEGACY_KEY after migration fails, while preserving
the existing false return-value assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d12eca1b-9ed2-4e61-b03d-79596c92edf1

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf01de and bf4e515.

📒 Files selected for processing (7)
  • public/js/app.js
  • public/js/core/autosave.js
  • public/js/platform/storage.js
  • test/browser/app.test.js
  • test/browser/main.test.js
  • test/node/autosave.test.js
  • test/node/storage.test.js

Comment thread public/js/platform/storage.js
Comment thread public/js/platform/storage.js
Comment thread test/browser/main.test.js
list() took id and updatedAt from the parsed record, but read() only
validated `board`. A record written by an older version or edited by hand
could carry a stale id — producing a picker entry whose load() returns null
— or no updatedAt at all, which makes the comparator return NaN and
scrambles the order of the whole list rather than just that row. The key is
the record's real address, so it is now the source of the id, and both
metadata fields have a floor.

migrateLegacy read and deleted LEGACY_KEY regardless of namespace. A
repository on `scratch` sharing one Web Storage would adopt the default
workspace's legacy board and then delete it, so the workspace that actually
owned it had nothing left to migrate. Adoption is now limited to the owning
namespace, with an explicit `legacyKey` for callers that mean it.

Two browser tests slept 600ms past a 400ms debounce, which is generous on a
laptop and a coin flip on a loaded runner. They now poll a real condition
via page.waitFor; the third asserts that nothing was written, where there is
no positive condition to wait for, so it flushes the autosave instead.

Also took the fixed port out of the server spawn test — a leftover process
from an earlier run made it fail with a ten-second timeout that looked
nothing like the actual cause.

261 tests, 99.8% line coverage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants