molecule-core/canvas
Hongming Wang 20f76c4fdf fix(canvas/chat): stable IntersectionObserver + inflight guard for loadOlder
Self-review of the lazy-load PR caught three Important findings:

1. IO observer was re-armed on every messages change. The previous
   loadOlder useCallback depended on `messages`, so every live agent
   push recreated it → re-ran the IO useEffect → tore down + re-armed
   the observer. In a perf PR shipping to chat-heavy users, that's
   the wrong direction. Fix: refs for the captured state
   (oldestMessageRef, hasMoreRef), narrow loadOlder deps to
   [workspaceId], and gate the IO effect on `messages.length > 0`
   (boolean) instead of `messages` so it arms exactly once when data
   first lands and stays armed across appends.

2. loadingOlder setState race. Two IO callbacks dispatched in the
   same microtask (fast scroll, layout shift) could both pass the
   `if (loadingOlder)` guard before React committed setLoadingOlder.
   Fix: synchronous inflightRef set BEFORE any await, cleared in
   finally; loadingOlder state stays for the UI label only.

3. Retry-button onClick duplicated the mount-effect body. Single
   loadInitial() callback now serves both, eliminating the drift
   hazard.

Coverage:
- 4 new tests bring the file to 8/8 (was 4):
  - loadOlder fetches with limit=20 and before_ts=oldest.timestamp
  - inflight guard rejects three concurrent IO triggers while a
    deferred fetch is in flight (asserts call count stays at 2,
    not 5)
  - empty older response unmounts the sentinel (proxy for the
    anchor-clearing branch in loadOlder)
  - IO observer instance survives three subsequent prepends — same
    object reference both before and after, no churn
- Both behavioural tests verified to FAIL on the prior code
  (stashed ChatTab.tsx, ran them alone, confirmed both red), then
  PASS on this commit. Pinning real regressions, not tautologies.
- IntersectionObserver fake captures instances + exposes
  triggerIntersection() so the IO callback can be driven directly
  from jsdom (no real layout / scrolling needed).

Test: vitest run src/components/tabs/__tests__/ → 39 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:38:37 -07:00
..
e2e canvas/e2e: surface admin-orgs row + workspace body on failure 2026-05-03 14:01:50 -07:00
public chore: replace brand icon and add HANDOFF.md 2026-04-13 13:03:40 -07:00
src fix(canvas/chat): stable IntersectionObserver + inflight guard for loadOlder 2026-05-04 20:38:37 -07:00
.env.example fix(canvas): close 4 gaps in WS status indicator (env, toast, tests) 2026-04-14 08:26:38 +00:00
.gitignore feat(canvas): SaaS cross-origin — slug header + cookie credentials (Phase F) 2026-04-14 20:08:39 -07:00
components.json fix(canvas): restore text-white on saturated buttons + close zinc gaps 2026-05-03 02:04:20 -07:00
Dockerfile chore(canvas): upgrade node:20-alpine → node:22-alpine 2026-04-24 18:54:30 +00:00
next.config.ts fix(canvas,dotenv): review-driven hardening of fit gate + parser parity 2026-04-24 22:23:51 -07:00
package-lock.json fix(canvas): regenerate lockfile with cross-platform optional deps 2026-05-03 01:52:42 -07:00
package.json feat(canvas): warm-paper theme + Tailwind v4 migration 2026-05-03 01:43:55 -07:00
playwright.config.ts initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
playwright.staging.config.ts feat(e2e): canary + canvas Playwright workflows; delegation mechanics 2026-04-21 04:15:10 -07:00
postcss.config.js feat(canvas): warm-paper theme + Tailwind v4 migration 2026-05-03 01:43:55 -07:00
tsconfig.json initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
vitest.config.ts feat(canvas): vitest coverage instrumentation (#1815, no CI gate yet) 2026-04-26 23:44:07 -07:00