Skip to content

fix: stop relation metadata requests on grid scroll - #491

Merged
appflowy merged 2 commits into
mainfrom
fix/relation-header-metadata-requests
Aug 20, 2026
Merged

fix: stop relation metadata requests on grid scroll#491
appflowy merged 2 commits into
mainfrom
fix/relation-header-metadata-requests

Conversation

@appflowy

@appflowy appflowy commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

Scrolling a virtualized database grid unmounts and remounts its normal and sticky relation headers. For a legacy relation missing from both the workspace database catalog and the currently materialized (lazy) folder outline, each header instance fell back to GET /view/:id?depth=1 for the relation view and its optional parent. The existing generic view cache was only short-lived and did not cache failures; when the parent was inaccessible, the successful child metadata was discarded and the next remount repeated the sequence.

This PR moves that fallback into the app-wide folder metadata path:

  • adds a flat, user/session/workspace-scoped view metadata index populated from accepted root/lazy/navigation responses, granular folder events, and successful by-ID lookups;
  • preserves the existing lazy outline and checks global metadata before issuing one shared by-ID request;
  • adds a metadataOnly lookup mode for relation labels while keeping the existing depth-1 loadViewMeta behavior unchanged for navigation and database tabs;
  • shares catalog/candidate work and a single workspace event binding across normal, sticky, and virtualized header instances;
  • keeps a valid relation child visible when its optional parent is missing or inaccessible;
  • fences stale workspace, session, permission, and overlapping server/event results so older responses cannot repopulate global state;
  • invalidates both the flat metadata index and the older short-lived view cache on authoritative access/removal changes;
  • ignores self-parent database-row folder notifications so row scrolling does not churn the global catalog or trash state.

The normal catalog path performs no folder-by-ID request. A legacy miss performs at most one shared child lookup and one optional parent lookup; later header remounts use the global metadata result.

Testing

  • 177 focused cache/catalog/workspace/relation integration tests
  • 64 broader relation/rollup component tests
  • Vercel React best-practices audit: no actionable findings
  • pnpm lint (TypeScript + repository-wide ESLint)
  • pnpm build
  • git diff --check
  • Live Chrome check: 12 forced relation-header unmount/remount passes after the former 5-second cache window produced zero new /view/:id?depth=1, /database?offset, or /trash requests

Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the production-backed flow in Chrome.

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (not applicable: bug fix).
  • I've verified that the global metadata resolver preserves existing non-relation loadViewMeta behavior.

Summary by Sourcery

Centralize relation view metadata resolution and invalidation to eliminate redundant requests during virtualized grid scrolling.

Bug Fixes:

  • Prevent repeated relation metadata requests when virtualized grid headers unmount and remount.
  • Preserve valid relation child metadata when its optional parent is unavailable.

Enhancements:

  • Add user, session, and workspace-scoped metadata indexing with shared lookups, negative caching, and stale-response protection.
  • Coordinate relation candidate loading and workspace event subscriptions across header instances.
  • Improve catalog and metadata invalidation for permission, sharing, removal, and folder events, including row-document exclusions.
  • Maintain existing depth-1 view metadata behavior for navigation and database consumers.

CI:

  • Pin Playwright CI to Bun 1.3.14 pending resolution of an upstream Bun 1.4.0 package corruption regression.

Tests:

  • Add extensive cache, catalog, workspace revalidation, relation, and component coverage for metadata sharing, invalidation, and stale-request handling.

@sourcery-ai

sourcery-ai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a scoped external store for relation display metadata, wires it into relation headers via useRelationData, and coordinates cache invalidation with the workspace database catalog so metadata requests are shared, bounded, and properly refreshed on invalidation events and session changes.

Sequence diagram for shared relation display metadata loading

sequenceDiagram
  actor RelationHeader
  participant useRelationData
  participant WorkspaceDatabaseCatalog as getWorkspaceDatabaseCatalog
  participant RelationDisplayStore as relation_display_metadata
  participant ViewApi as getViewIdFromDatabaseId/loadViewMeta

  RelationHeader->>useRelationData: mount(fieldId, enabled=true)
  useRelationData->>WorkspaceDatabaseCatalog: getWorkspaceDatabaseCatalog(workspaceId)
  Note over useRelationData,WorkspaceDatabaseCatalog: wait for catalog result

  alt database catalogued
    WorkspaceDatabaseCatalog-->>useRelationData: candidates with relatedDatabaseId
    useRelationData->>RelationDisplayStore: subscribeRelationDisplayMetadata(workspaceId, relatedDatabaseId)
    useRelationData-->>RelationHeader: selectedView from catalog
  else database not in catalog
    WorkspaceDatabaseCatalog-->>useRelationData: []
    useRelationData->>RelationDisplayStore: loadRelationDisplayMetadata(workspaceId, relatedDatabaseId, knownViewId)
    RelationDisplayStore->>ViewApi: getViewIdFromDatabaseId(databaseId)
    ViewApi-->>RelationDisplayStore: viewId
    RelationDisplayStore->>ViewApi: loadViewMeta(viewId)
    ViewApi-->>RelationDisplayStore: childView (and optional parent)
    RelationDisplayStore-->>useRelationData: ready snapshot via subscribeRelationDisplayMetadata
    useRelationData-->>RelationHeader: selectedView from fallback metadata
    Note over RelationDisplayStore,RelationHeader: remount headers reuse cached snapshot without new requests
  end
Loading

File-Level Changes

Change Details Files
Add a user/workspace/database-scoped external store for relation display metadata with shared pending requests, bounded retries, and event-driven invalidation.
  • Implement relation-display-metadata cache keyed by user/workspace/database with idle/loading/ready snapshots
  • Share concurrent child/parent metadata lookups via a pendingRequests map to avoid duplicate HTTP calls
  • Publish child metadata immediately and optionally upgrade it with parent metadata, preserving child when parent is inaccessible
  • Apply TTL-based retries for failed/partial metadata (missing parent, transient errors) and prune cache by size and expiry
  • Track session and cache generations to prevent stale requests from repopulating the cache after invalidation
  • Subscribe to session invalidation to clear metadata cache and related invalidation listeners
src/application/services/js-services/relation-display-metadata.ts
Integrate relation display metadata store into relation hooks so grid headers share legacy fallback metadata and avoid extra requests on scroll/remount.
  • Extend useRelationData to read relation-display-metadata snapshots via useSyncExternalStore
  • Derive a separate catalogLoading flag so catalog fetch and fallback metadata fetch are coordinated without race conditions
  • Trigger loadRelationDisplayMetadata only when catalog has no candidate, fallback snapshot is idle, and picker is enabled
  • Use fallback metadata’s resolved view (including parent-derived name/icon) as selectedView when catalog data is absent
  • Scope loading state by workspace, field, and enabled flag, and avoid clearing fallback metadata cache on every session invalidation
  • Wire permission/access/view-meta invalidation via retainRelationDisplayMetadataInvalidation to keep headers in sync with metadata events
src/components/database/components/property/relation/useRelationData.ts
Coordinate workspace database catalog invalidation with relation display metadata invalidation to keep caches consistent.
  • Import and call invalidateRelationDisplayMetadata in invalidateWorkspaceDatabaseCatalog
  • Ensure catalog invalidation clears any shared relation metadata for that workspace and prevents stale data reuse
src/application/services/js-services/workspace-database-catalog.ts
Add focused unit tests for relation display metadata behavior and extend existing catalog and relation hook tests to cover new interactions and invariants.
  • Create relation-display-metadata.test.ts to cover caching, retries, child/parent resolution, invalidation, and user scoping
  • Mock relation-display-metadata in workspace-database-catalog tests to assert single invalidation per catalog invalidation/refresh cycle
  • Update useRelationData tests to verify catalog gating of fallback, single legacy fallback invocation, and shared metadata across normal/sticky headers and remounts
  • Add helpers (workspaceCatalog fixture and deferred promise helper) to simplify test setup for catalog and async behaviors
src/application/services/js-services/__tests__/relation-display-metadata.test.ts
src/application/services/js-services/__tests__/workspace-database-catalog.test.ts
src/components/database/components/property/relation/useRelationData.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've left some high level feedback:

  • The relation-display-metadata cache/invalidation logic is quite intricate (sessionGeneration, workspaceScope, cacheGenerations, expiryTimers, invalidationSubscriptions); consider factoring some of the responsibility into smaller, focused helpers or modules to make the lifecycle (request, cache, expiry, invalidation) easier to reason about.
  • In relation-display-metadata, requestIsCurrent depends on currentUserScope(); since currentUserScope uses getTokenParsed on every call, a token change mid-request could invalidate the request unexpectedly—if that’s intentional, a brief comment explaining the rationale near requestIsCurrent would help future maintainers.
  • In useRelationData, the loading state now depends on workspaceId, fieldId, and enabled; you may want to add a small comment around the catalogLoading computation explaining why the synchronous derivation is needed to avoid the fallback metadata effect racing ahead of the catalog effect.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The relation-display-metadata cache/invalidation logic is quite intricate (sessionGeneration, workspaceScope, cacheGenerations, expiryTimers, invalidationSubscriptions); consider factoring some of the responsibility into smaller, focused helpers or modules to make the lifecycle (request, cache, expiry, invalidation) easier to reason about.
- In relation-display-metadata, requestIsCurrent depends on currentUserScope(); since currentUserScope uses getTokenParsed on every call, a token change mid-request could invalidate the request unexpectedly—if that’s intentional, a brief comment explaining the rationale near requestIsCurrent would help future maintainers.
- In useRelationData, the loading state now depends on workspaceId, fieldId, and enabled; you may want to add a small comment around the catalogLoading computation explaining why the synchronous derivation is needed to avoid the fallback metadata effect racing ahead of the catalog effect.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@appflowy
appflowy force-pushed the fix/relation-header-metadata-requests branch from a4437d2 to 8b4c1e1 Compare August 20, 2026 14:18
@appflowy
appflowy merged commit 144b534 into main Aug 20, 2026
15 of 16 checks passed
@appflowy
appflowy deleted the fix/relation-header-metadata-requests branch August 20, 2026 15:31
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.

1 participant