Skip to content

fix(metadata): lazy-load per-user metadata to stop recvq flood (#116)#212

Open
ValwareIRC wants to merge 2 commits into
mainfrom
fix/metadata-lazy-load
Open

fix(metadata): lazy-load per-user metadata to stop recvq flood (#116)#212
ValwareIRC wants to merge 2 commits into
mainfrom
fix/metadata-lazy-load

Conversation

@ValwareIRC
Copy link
Copy Markdown
Contributor

@ValwareIRC ValwareIRC commented May 13, 2026

Closes #116.

Root cause

On a 200+ user channel, joining the channel disconnects the client. Two hidden firehoses fire at JOIN time:

  1. NAMES handler (store/handlers/channels.ts) iterated every user and called metadataList(serverId, user), sending METADATA <nick> LIST once per user — ~200 lines in a burst.
  2. WHO handler (store/handlers/whois.ts) did the same loop again after WHO_END — another ~200 lines.

The connect-time METADATA * SUB (store/handlers/connection.ts) was also subscribing to 8 keys, telling the server to push 8 × N values for every user we share a channel with — the spec-blessed "metadata firehose".

UnrealIRCd's recvq protection then ate the connection.

Fix — fully lazy per-user metadata

Trigger What it does
Speak (CHANMSG / USERMSG) First time a non-self user is heard from this session, queue a METADATA LIST for them.
Nicklist visibility IntersectionObserver on each UserItem + 250 ms scroll-idle debounce. When the scroll comes to rest, fetch metadata for currently-visible nicks not yet requested. Mid-scroll visibility events don't fire fetches.
Profile modal open (Existing behavior, unchanged.)

The existing userMetadataRequested: Record<serverId, Set<nick>> store state deduplicates across all sources, so each unique nick is fetched at most once per session.

To bound bursts (e.g. scroll-stops on a wall of unfamiliar nicks), all METADATA LIST calls now go through a tiny drip queue — src/store/metadataLazyQueue.ts — that paces dispatches at one every 200 ms. FIFO, coalesces duplicates, idles when empty.

SUB narrowing

Kept a narrow SUB on display-name + avatar so the two user-visible bits still update live (rename / new avatar). Dropped url, website, status, location, color, bot. ~75% smaller firehose on JOIN, still nice live updates for the things people notice.

Test plan

  • npm run format, npm run fix:unsafe, npm run test (789 pass / 1 skipped), npm run build clean
  • New tests/store/metadataLazyQueue.test.ts covers: FIFO drip, coalesce duplicates, per-server isolation, idle-resume after queue empties
  • Manual: join a 200+ user channel on UnrealIRCd — no disconnect, members trickle in metadata as you scroll
  • Manual: someone speaks for the first time — their avatar / display-name appears within 200–400 ms
  • Manual: profile modal still loads complete metadata

Summary by CodeRabbit

  • Refactor

    • Optimized metadata loading to fetch on-demand instead of eagerly during connection
    • Member list now lazy-loads metadata as users scroll into view, improving initial load time and responsiveness
    • Metadata requests are batched and throttled for better performance
  • Tests

    • Added tests for metadata request queueing behavior

Review Change Stack

On a 200+ user channel, joining used to disconnect the client. Two
loops fired METADATA LIST for every member in NAMES and again on
WHO completion -- ~400 commands in a tight burst -- which trips
UnrealIRCd's recvq flood protection. The connect-time SUB was a
secondary firehose, asking the server to push 8 keys * N users on
every JOIN.

Switch to a fully lazy model:

- Narrow the SUB at connect from 8 keys to 2 (display-name + avatar)
  so live updates for the user-visible bits still come through but
  the server-side metadata firehose on JOIN is ~75% smaller.
- Remove the per-user METADATA LIST loops in the NAMES and WHO
  handlers. Lazy triggers below cover everyone the user actually
  sees.
- CHANMSG and USERMSG: lazy-fetch the sender's metadata the first
  time they speak. metadataList() already dedups and consults the
  localStorage cache, so this is one fetch per unique active speaker
  per session.
- MemberList: IntersectionObserver on each UserItem plus a 250 ms
  scroll-idle debounce. When the scroll comes to rest, fetch metadata
  for currently-visible nicks we haven't already requested. Mid-scroll
  visibility events don't fire fetches.
- All METADATA LIST calls now go through a tiny drip queue
  (src/store/metadataLazyQueue.ts) that paces them at one every
  200 ms. Bursty sources can never trip the server-side flood
  threshold again.

Fixes #116
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

Metadata requests are queued and paced at 200ms intervals. Lazy triggers occur in message handlers on first speak, MemberList scroll visibility, and connection setup now subscribes only to avatar and display-name. Eager batch fetches are removed from channel NAMES, WHO completion, and connection handlers.

Changes

Lazy Metadata Loading System

Layer / File(s) Summary
Metadata queue infrastructure and tests
src/store/metadataLazyQueue.ts, tests/store/metadataLazyQueue.test.ts
New drip-feed scheduler buffers and deduplicates (serverId, target) requests at 200ms intervals; interval starts on first enqueue and clears when queue drains; test suite validates dispatch ordering, coalescing behavior, and idle/resume cycles.
Store metadata action wiring
src/store/index.ts
metadataList action now routes through enqueueMetadataList instead of direct IRC client calls, centralizing all lazy dispatch logic.
Remove eager fetches and add lazy triggers
src/store/handlers/channels.ts, src/store/handlers/connection.ts, src/store/handlers/messages.ts, src/store/handlers/whois.ts
Connection handler narrows initial subscription to display-name and avatar only; NAMES and WHO_END handlers remove bulk metadata requests; CHANMSG and USERMSG handlers add per-sender lazy fetches on first message.
MemberList scroll-triggered visibility tracking
src/components/layout/MemberList.tsx
IntersectionObserver detects visible users in the scrollable member list; UserItem components register themselves via ref callback; debounced flush calls metadataList for on-screen members only.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MemberList as MemberList<br/>(UI)
  participant IntersectionObserver as IntersectionObserver
  participant MessageHandler as Message<br/>Handler
  participant Store
  participant LazyQueue as Lazy Queue
  participant IRC as IRC Client
  
  User->>MemberList: scroll member list
  MemberList->>IntersectionObserver: observe visible UserItems
  IntersectionObserver->>MemberList: intersection changed
  MemberList->>MemberList: debounce and collect usernames
  MemberList->>Store: metadataList(serverId, username)
  
  User->>MessageHandler: user speaks in channel
  MessageHandler->>Store: metadataList(serverId, sender)
  
  Store->>LazyQueue: enqueueMetadataList(client, serverId, target)
  LazyQueue->>LazyQueue: deduplicate and buffer
  LazyQueue->>LazyQueue: start 200ms tick interval
  
  loop every 200ms
    LazyQueue->>IRC: client.metadataList(serverId, target)
    LazyQueue->>LazyQueue: shift next queued entry
  end
  
  LazyQueue->>LazyQueue: clear timer when empty
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ObsidianIRC/ObsidianIRC#64: Adds IRCv3 METADATA support and renders user metadata (avatar, color, display-name, status) in the UI; this PR's lazy queue and visibility-driven fetching directly optimize how that metadata is requested to prevent recvq overflows.

Suggested reviewers

  • matheusfillipe

Poem

🐰 Hops through the list with careful sight,
Observing only what scrolls to light,
With queue-fed drips at steady pace,
No overflow in this kinder place!
Two hundred users now fit with grace.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% 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 accurately summarizes the main change: implementing lazy-loading of per-user metadata to prevent recvq floods.
Linked Issues check ✅ Passed All primary objectives from issue #116 are met: lazy metadata loading prevents recvq overflow, narrowed SUB reduces join-time firehose, drip queue paces requests, and tests validate the mechanism.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing recvq flood prevention through lazy metadata loading; no unrelated alterations detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/metadata-lazy-load

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown

Pages Preview
Preview URL: https://fix-metadata-lazy-load.obsidianirc.pages.dev

Automated deployment preview for the PR in the Cloudflare Pages.

ValwareIRC added a commit that referenced this pull request May 13, 2026
After PR #212 (lazy metadata) joining a 500-user channel still felt
slow: the member list ticked in one nick at a time and metadata
requests fired in dribs.

The cause was unrelated to metadata. Each 352 (RFC 1459 WHO_REPLY)
and 354 (WHOX RPL_WHOSPCRPL) was running a full store.setState that
mapped over every server, every channel, every user, and every
private chat -- once per nick. With 500 users that's 500 React
re-renders of MemberList, and the IntersectionObserver re-fires for
each item that scrolls into view, scheduling another lazy fetch
burst each tick.

Add a tiny batcher (src/store/whoReplyBatcher.ts) with idle + ceiling
timers, last-write-wins per nick, and per-(serverId,channel) buckets.
WHO_END flushes immediately so a channel renders the moment the
server says it is done. WHO_REPLY and WHOX_REPLY now push into the
buffer instead of running setState. PM-only fallbacks (no channel)
stay inline -- they are single lines, not bursts.

Tests cover idle flush, last-write-wins, explicit flush via WHO_END,
ceiling fallback for a trickling WHO, merge-into-existing-user, and
per-(server,channel) scoping. Full suite green (795 / 1 skipped),
build clean.
Comment thread src/store/handlers/channels.ts
matheusfillipe
matheusfillipe previously approved these changes May 21, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@src/store/handlers/channels.ts`:
- Line 408: There is trailing whitespace on the empty line in
src/store/handlers/channels.ts that fails the Biome formatter; remove the extra
spaces at the end of that line (e.g., after the closing block near the channel
handler/export) so the file contains no trailing whitespace and the CI
formatting check will pass.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2151a41a-adaa-41df-9ce6-d2ede408c112

📥 Commits

Reviewing files that changed from the base of the PR and between e153b25 and 9db12c9.

📒 Files selected for processing (8)
  • src/components/layout/MemberList.tsx
  • src/store/handlers/channels.ts
  • src/store/handlers/connection.ts
  • src/store/handlers/messages.ts
  • src/store/handlers/whois.ts
  • src/store/index.ts
  • src/store/metadataLazyQueue.ts
  • tests/store/metadataLazyQueue.test.ts

}
}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove trailing whitespace to unblock Biome format check.

Line 408 has trailing whitespace; this matches the reported formatting failure in CI and should be cleaned to restore pipeline green.

🤖 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 `@src/store/handlers/channels.ts` at line 408, There is trailing whitespace on
the empty line in src/store/handlers/channels.ts that fails the Biome formatter;
remove the extra spaces at the end of that line (e.g., after the closing block
near the channel handler/export) so the file contains no trailing whitespace and
the CI formatting check will pass.

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.

draft/metadata-2 breaks on networks with 200+ users (ObsdianIRC v0.2.0)

2 participants