Skip to content

ENG-1617: se existing 'getConfigTree equivalent functions' for specific setting groups#946

Open
sid597 wants to merge 20 commits intoeng-1470-test-dual-read-with-flag-on-and-fix-gapsv2from
eng-1617-use-existing-getconfigtree-equivalent-functions-for-specific
Open

ENG-1617: se existing 'getConfigTree equivalent functions' for specific setting groups#946
sid597 wants to merge 20 commits intoeng-1470-test-dual-read-with-flag-on-and-fix-gapsv2from
eng-1617-use-existing-getconfigtree-equivalent-functions-for-specific

Conversation

@sid597
Copy link
Copy Markdown
Collaborator

@sid597 sid597 commented Apr 8, 2026

https://www.loom.com/share/3bed40fc5de943dcb4e7181888b42442

getBlockPropBasedSettings now does one Roam pull that returns the settings page's children with their string + uid + props in one shot, replacing the q-based getBlockUidByTextOnPage (~290ms per call) plus a second pull for props. setBlockPropBasedSettings reuses the same helper for the uid lookup so we have a single pull-and-walk pattern.

SettingsDialog captures a full settings snapshot once at mount via useState(() => bulkReadSettings()) and threads featureFlags and personalSettings down to HomePersonalSettings. Each child component (PersonalFlagPanel, NodeMenuTriggerComponent,
NodeSearchMenuTriggerSetting, KeyboardShortcutInput) accepts an initialValue prop and seeds its local state from the snapshot instead of calling getPersonalSetting on mount. PersonalFlagPanel's initialValue precedence flips so the prop wins when provided; QuerySettings callers without a prop still hit the existing fallback.

getDiscourseNodes, getDiscourseRelations, and getAllRelations narrow their snapshot parameter to Pick<SettingsSnapshot, ...> to declare which fields each function actually reads.

Adds a one-line console.log in SettingsDialog reporting the dialog open time, kept as an ongoing perf monitor.

Summary by CodeRabbit

Release Notes

  • Refactor
    • Restructured settings system architecture to use centralized settings snapshot instead of scattered getter functions, improving consistency and maintainability across settings components.

sid597 added 6 commits April 6, 2026 17:00
Cut plugin load from ~20925ms to ~1327ms (94%) on a real graph by collapsing
per-call settings accessors into a single bulk read at init and threading
that snapshot through the init chain + observer callbacks.

Key changes:
- accessors.ts: bulkReadSettings() runs ONE pull query against the settings
  page's direct children and returns { featureFlags, globalSettings,
  personalSettings } parsed via Zod. readPathValue exported.
- getDiscourseNodes / getDiscourseRelations / getAllRelations: optional
  snapshot param threaded through, no breaking changes to existing callers.
- initializeDiscourseNodes + refreshConfigTree (+ registerDiscourseDatalog-
  Translators, getDiscourseRelationLabels): accept and forward snapshot.
- index.ts: bulkReadSettings() at the top of init; snapshot threaded into
  initializeDiscourseNodes, refreshConfigTree, initObservers,
  installDiscourseFloatingMenu, setInitialQueryPages, and the 3 sync sites
  inside index.ts itself.
- initializeObserversAndListeners.ts: snapshot threaded into the sync-init
  body; pageTitleObserver + leftSidebarObserver callbacks call
  bulkReadSettings() per fire (fresh, not stale); nodeTagPopupButtonObserver
  uses per-sync-batch memoization via queueMicrotask; hashChangeListener
  and nodeCreationPopoverListener use bulkReadSettings() per fire.
- findDiscourseNode: snapshot param added; getDiscourseNodes() default-arg
  moved inside the cache-miss branch so cache hits don't waste the call.
- isQueryPage / isCanvasPage / QueryPagesPanel.getQueryPages: optional
  snapshot param.
- LeftSidebarView.buildConfig / useConfig / mountLeftSidebar: optional
  initialSnapshot threaded for the first render; emitter-driven updates
  keep using live reads for post-mount reactivity.
- DiscourseFloatingMenu.installDiscourseFloatingMenu: optional snapshot.
- posthog.initPostHog: removed redundant internal getPersonalSetting check
  (caller already guards from the snapshot).
- migrateLegacyToBlockProps.hasGraphMigrationMarker: accepts the existing
  blockMap and does an O(1) lookup instead of a getBlockUidByTextOnPage scan.

Includes per-phase timing console.logs across index.ts, refreshConfigTree,
init.ts, initSettingsPageBlocks, and initObservers. Committed as a
checkpoint so we can reference measurements later; will be removed in the
next commit.
Removes the per-phase console.log instrumentation added in the previous
commit. All the [DG Plugin] / [DG Nav] logs and their `mark()` / `markPhase()`
helpers are gone. Code behavior unchanged.

Dropped in this commit:
- index.ts: mark() closure, load start/done logs, and all phase marks.
- initializeObserversAndListeners.ts: markPhase() closure, per-observer marks,
  pageTitleObserver fire log, hashChangeListener [DG Nav] logs.
- LeftSidebarView.tsx: openTarget [DG Nav] click/resolve logs.
- refreshConfigTree.ts: mark() closure and all phase marks.
- init.ts: mark() closures in initSchema and initSettingsPageBlocks.
- accessors.ts: bulkReadSettings internal timing log.
- index.ts: unused getPluginElapsedTime import.

Previous commit (343dc11) kept as a checkpoint for future drill-downs.
…ional snapshot

- index.ts: move initPluginTimer() back to its original position (after
  early-return checks) so timing isn't started for graphs that bail out.
- Replace readPathValue + `as T | undefined` casts with direct typed
  indexing on the Zod-derived snapshot types across:
    - index.ts (disallowDiagnostics, isStreamlineStylingEnabled)
    - initializeObserversAndListeners.ts (suggestiveModeOverlay,
      pagePreview, discourseContextOverlay, globalTrigger,
      personalTriggerCombo, customTrigger) — also drops dead `?? "\\"`
      and `?? "@"` fallbacks since Zod defaults already populate them.
    - isCanvasPage.ts (canvasPageFormat)
    - setQueryPages.ts + QueryPagesPanel.tsx (nested [Query][Query pages])
- setQueryPages.setInitialQueryPages: snapshot is now optional with a
  getPersonalSetting fallback, matching the pattern used elsewhere
  (getQueryPages, isCanvasPage, etc.).
- init.ts: restore logDualReadComparison + window.dgDualReadLog so the
  on-demand console helper is available again. NOT auto-called on init —
  invoke window.dgDualReadLog() manually to dump the comparison.
Capture performance.now() at the top of runExtension and log the
elapsed milliseconds just before the unload handler is wired, so we
have a single broad measurement of plugin init cost on each load.
…mpliance

Make snapshot required at six init-only leaves where caller audit
showed every site already passed one: installDiscourseFloatingMenu,
initializeDiscourseNodes, setInitialQueryPages, isQueryPage,
isCurrentPageCanvas, isSidebarCanvas. No cascade — only at the leaves.

Drop dead fallback code that was reachable only via the optional path:
- setQueryPages: legacy string|Record coercion ladder (snapshot is Zod-typed string[])
- DiscourseFloatingMenu: getPersonalSetting<boolean> cast site
- DiscourseFloatingMenu: unused props parameter (no caller ever overrode default)
- initializeObserversAndListeners: !== false dead pattern (Zod boolean default)
- initializeObserversAndListeners: as IKeyCombo cast (schema is structurally compatible)

AGENTS.md compliance for >2-arg functions:
- mountLeftSidebar: object-destructured params, both call sites updated
- installDiscourseFloatingMenu: kept at 2 positional via dead-props removal

posthog: collapse doInitPostHog wrapper into initPostHog (caller-side gating).
accessors: revert speculative readPathValue export (no consumer).
LeftSidebarView/DiscourseFloatingMenu: eslint-disable react/no-deprecated on
ReactDOM.render rewritten lines, matching existing codebase convention.
`getBlockPropBasedSettings` now does one Roam `pull` that returns the
settings page's children with their string + uid + props in one shot,
replacing the `q`-based `getBlockUidByTextOnPage` (~290ms per call) plus
a second `pull` for props. `setBlockPropBasedSettings` reuses the same
helper for the uid lookup so we have a single pull-and-walk pattern.

`SettingsDialog` captures a full settings snapshot once at mount via
`useState(() => bulkReadSettings())` and threads `featureFlags` and
`personalSettings` down to `HomePersonalSettings`. Each child component
(`PersonalFlagPanel`, `NodeMenuTriggerComponent`,
`NodeSearchMenuTriggerSetting`, `KeyboardShortcutInput`) accepts an
`initialValue` prop and seeds its local state from the snapshot instead
of calling `getPersonalSetting` on mount. `PersonalFlagPanel`'s
`initialValue` precedence flips so the prop wins when provided;
`QuerySettings` callers without a prop still hit the existing fallback.

`getDiscourseNodes`, `getDiscourseRelations`, and `getAllRelations`
narrow their snapshot parameter to `Pick<SettingsSnapshot, ...>` to
declare which fields each function actually reads.

Adds a one-line `console.log` in `SettingsDialog` reporting the dialog
open time, kept as an ongoing perf monitor.
@linear
Copy link
Copy Markdown

linear bot commented Apr 8, 2026

@supabase
Copy link
Copy Markdown

supabase bot commented Apr 8, 2026

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@sid597
Copy link
Copy Markdown
Collaborator Author

sid597 commented Apr 8, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

A refactoring that shifts settings initialization from component-level accessor reads to snapshot-injection. Components now receive initial values and settings data through props rather than calling getPersonalSetting, getGlobalSetting, and getFeatureFlag internally. The SettingsDialog centralizes settings access via bulkReadSettings() and distributes relevant snapshots to child components.

Changes

Cohort / File(s) Summary
Settings Container & Snapshot Distribution
apps/roam/src/components/settings/Settings.tsx
Added bulkReadSettings() call with memoization to create a settings snapshot and passes filtered portions (personalSettings, featureFlags, globalSettings) to child panels (HomePersonalSettings, QuerySettings, LeftSidebarPersonalSections, DiscourseGraphHome, DiscourseGraphExport, LeftSidebarGlobalSections, DiscourseRelationConfigPanel, AdminPanel, and NodeConfig tabs).
Core Settings Accessors
apps/roam/src/components/settings/utils/accessors.ts
Refactored to use bulkReadSettings() instead of block-prop reading; removed getBlockPropBasedSettings, simplified feature-flag and setting reads to single-source lookups, updated getDiscourseNodeSetting to branch on new-store flag, modified getAllRelations() signature to accept Pick<SettingsSnapshot, "globalSettings"> instead of full snapshot.
Setting Panel Base Components
apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx
Made initialValue required (non-optional) across BaseTextPanelProps, BaseFlagPanelProps, BaseNumberPanelProps, BaseSelectPanelProps, BaseMultiTextPanelProps; removed getGlobalSetting / getPersonalSetting fallback logic; updated FeatureFlagPanel to accept optional initialValue with fallback to getFeatureFlag; added explicit type defaults for DiscourseNode*Panel wrappers.
Node Trigger & Keyboard Components
apps/roam/src/components/DiscourseNodeMenu.tsx, apps/roam/src/components/DiscourseNodeSearchMenu.tsx, apps/roam/src/components/settings/KeyboardShortcutInput.tsx
Updated NodeMenuTriggerComponent, NodeSearchMenuTriggerSetting, and KeyboardShortcutInput to accept and initialize from initialValue props instead of calling getPersonalSetting internally; removed dependency on persisted setting reads for initial state.
Personal Settings Hub
apps/roam/src/components/settings/HomePersonalSettings.tsx
Changed props to receive personalSettings and featureFlags from snapshot; passes initialValue from snapshot to NodeMenuTriggerComponent, NodeSearchMenuTriggerSetting, KeyboardShortcutInput, and PersonalFlagPanel instances; updated "Suggestive mode enabled" check to use injected featureFlags.
Query & Filter Settings
apps/roam/src/components/settings/QuerySettings.tsx, apps/roam/src/components/settings/DefaultFilters.tsx
Updated to accept personalSettings and defaultFilters as props; derive initial values from injected querySettings/defaultFilters objects instead of calling getPersonalSetting; added mount guard to skip initial persist in DefaultFilters.
Admin & Feature Flags
apps/roam/src/components/settings/AdminPanel.tsx
Changed AdminPanel and FeatureFlagsTab to accept featureFlags and globalSettings props; replaced getFeatureFlag calls with injected snapshot values; updated "Suggestive mode enabled" and "Use new settings store" initialization.
General & Suggestive Mode Settings
apps/roam/src/components/settings/GeneralSettings.tsx, apps/roam/src/components/settings/SuggestiveModeSettings.tsx
Updated DiscourseGraphHome and SuggestiveModeSettings to accept globalSettings and featureFlags props; replaced getGlobalSetting calls with direct snapshot lookups; updated conditional flag checks.
Export Settings
apps/roam/src/components/settings/ExportSettings.tsx
Changed DiscourseGraphExport to accept globalSettings prop; derives exportBlockProps from snapshot and passes initialValue to panel components instead of relying on internal accessor reads.
Sidebar Settings
apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx, apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx
Updated LeftSidebarGlobalSections and LeftSidebarPersonalSections to accept globalSettings and personalSettings props respectively; replaced getGlobalSetting / getPersonalSetting calls with direct snapshot lookups.
Relation & Node Configuration
apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx, apps/roam/src/components/settings/NodeConfig.tsx
Updated DiscourseRelationConfigPanel and RelationEditPanel to accept globalSettings prop; updated NodeConfig to accept featureFlags prop for conditional "Suggestive mode" tab rendering; removed internal getter calls.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is unclear and appears incomplete; it contains 'se' instead of 'use' and is grammatically incorrect, making it difficult to understand the actual change being made. Correct the typo and rephrase to clearly convey the main objective, such as: 'Refactor settings initialization to pass snapshots down to child components instead of calling accessors on mount'.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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.

coderabbitai[bot]

This comment was marked as resolved.

sid597 added 2 commits April 8, 2026 16:40
CodeRabbit catch: with `renderActiveTabPanelOnly={true}`, the Home tab's
panel unmounts/remounts when the user navigates away and back. Each
re-mount re-runs `useState(() => initialValue ?? false)` in
`BaseFlagPanel`, re-seeding from whatever `initialValue` is currently
passed. Because the dialog held the snapshot in a non-updating
`useState`, that path served stale values, so toggles made earlier in
the same dialog session would visually revert after a tab round-trip.

Fix: hold the snapshot in a stateful slot and refresh it via
`bulkReadSettings()` from the Tabs `onChange` handler when the user
navigates back to Home. The setState batches with `setActiveTabId`, so
the new mount sees the fresh snapshot in the same render.

Also replace the inline reducer in `getBlockPropBasedSettings` with the
existing `readPathValue` util — same traversal but consistent with the
rest of the file and adds array-index handling for free.
Replaces the dialog-level snapshot from earlier commits with a per-tab
snapshot model that scales to every tab without per-tab plumbing in the
dialog itself.

In accessors.ts, the three plural getters (getFeatureFlags,
getGlobalSettings, getPersonalSettings) now delegate to the existing
bulkReadSettings, which does one Roam pull on the settings page and
parses all three schemas in a single pass. The slow q-based
getBlockPropBasedSettings is deleted (it was only used by the plural
getters and the set path); setBlockPropBasedSettings goes back to
calling getBlockUidByTextOnPage directly. Writes are infrequent enough
that the q cost is acceptable on the set path.

Each tab container that renders panels at mount captures one snapshot
via useState(() => bulkReadSettings()) and threads the relevant slice as
initialValue down to its panels: HomePersonalSettings, QuerySettings,
GeneralSettings, ExportSettings. The Personal and Global panels in
BlockPropSettingPanels.tsx flip their initialValue precedence to prefer
the prop and fall back to the live read only when the prop is missing,
so callers that don't pass initialValue (e.g. LeftSidebarGlobalSettings,
which already passes its own value) continue to behave the same way.

NodeMenuTriggerComponent, NodeSearchMenuTriggerSetting, and
KeyboardShortcutInput accept an initialValue prop and seed local state
from it instead of calling getPersonalSetting in their useState
initializer.

Settings.tsx wraps getDiscourseNodes() in useState so it doesn't re-run
on every dialog re-render.

The dialog-level snapshot, the snapshot-refresh-on-Home-tab-nav
workaround, and the Pick<SettingsSnapshot, ...> type narrowings are all
gone.
devin-ai-integration[bot]

This comment was marked as resolved.

Move bulkReadSettings() from per-tab useState into a single call at
SettingsDialog mount. Each tab receives its snapshot slice (globalSettings,
personalSettings, featureFlags) as props. Remove dual-read mismatch
console.warn logic from accessors. Make initialValue caller-provided in
BlockPropSettingPanel wrappers. Add TabTiming wrapper for per-tab
commit/paint perf measurement.
devin-ai-integration[bot]

This comment was marked as resolved.

…re bulkReadSettings

- Remove TabTiming component and all console.log timing from Settings dialog
- Remove per-call dual-read comparison from getGlobalSetting, getPersonalSetting,
  getDiscourseNodeSetting (keep logDualReadComparison for manual use)
- Make bulkReadSettings flag-aware: reads from legacy when flag is OFF,
  block props when ON
- Remove accessor fallbacks from Global/Personal wrapper panels (initialValue
  now always passed from snapshot)
- Remove getGlobalSetting/getPersonalSetting imports from BlockPropSettingPanels
devin-ai-integration[bot]

This comment was marked as resolved.

sid597 added 2 commits April 10, 2026 13:53
getGlobalSetting, getPersonalSetting, getFeatureFlag, getDiscourseNodeSetting
now each do a single bulkReadSettings() call instead of calling
isNewSettingsStoreEnabled() (which triggered a separate bulkReadSettings)
followed by another bulkReadSettings via the getter. bulkReadSettings already
handles the flag check and legacy/block-props routing internally.
Replace useState with useMemo keyed on activeTabId so bulkReadSettings()
runs fresh each time the user switches tabs. Fixes stale snapshot when
renderActiveTabPanelOnly unmounts/remounts panels.
devin-ai-integration[bot]

This comment was marked as resolved.

…move PostHog guards

- Rename settingsSnapshot/callbackSnapshot/snap/navSnapshot → settings
- bulkReadSettings now checks "Use new settings store" flag and falls
  back to legacy reads when off, matching individual getter behavior
- Move encryption/offline guards into initPostHog (diagnostics check
  stays at call site to avoid race with async setSetting in enablePostHog)
@sid597
Copy link
Copy Markdown
Collaborator Author

sid597 commented Apr 10, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

✅ Actions performed

Full review triggered.

coderabbitai[bot]

This comment was marked as resolved.

@sid597 sid597 requested a review from mdroidian April 10, 2026 16:30
graphite-app[bot]

This comment was marked as resolved.

sid597 added 2 commits April 15, 2026 21:00
…-init' into eng-1617-use-existing-getconfigtree-equivalent-functions-for-specific

# Conflicts:
#	apps/roam/src/components/settings/QueryPagesPanel.tsx
@sid597 sid597 changed the base branch from eng-1616-add-getconfigtree-equivalent-for-block-pros-on-init to eng-1470-test-dual-read-with-flag-on-and-fix-gapsv2 April 15, 2026 15:56
…-init' into eng-1617-use-existing-getconfigtree-equivalent-functions-for-specific
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 20 additional findings in Devin Review.

Open in Devin Review

Comment on lines +870 to +875
if (!featureFlags["Use new settings store"]) {
return {
featureFlags,
globalSettings: readAllLegacyGlobalSettings() as GlobalSettings,
personalSettings: readAllLegacyPersonalSettings() as PersonalSettings,
};
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.

🔴 getGlobalSettings() now returns unvalidated legacy data when feature flag is off, bypassing Zod defaults

Before this PR, getGlobalSettings() always read from block props via GlobalSettingsSchema.parse(blockProps || {}), which guaranteed Zod validation and schema defaults (e.g., Relations defaulting to DEFAULT_RELATIONS_BLOCK_PROPS). After this PR, it delegates to bulkReadSettings() which, when "Use new settings store" is off, returns readAllLegacyGlobalSettings() as GlobalSettings — an unsafe cast with no Zod validation. Fields like Relations may be undefined from legacy reads because the legacy config tree stores relations under grammar > relations, not as a top-level "Relations" key. This breaks existing callers in DiscourseRelationConfigPanel.tsx:1069 (handleDelete) and DiscourseRelationConfigPanel.tsx:1088 (handleDuplicate) which destructure getGlobalSettings().Relations and will throw a TypeError if Relations is undefined. The same issue affects getPersonalSettings() at accessors.ts:817-818.

Prompt for agents
In bulkReadSettings() (accessors.ts), when the new settings store flag is off, the legacy settings data is returned with an unsafe cast (as GlobalSettings / as PersonalSettings) without Zod validation. This bypasses the schema defaults that were previously guaranteed by GlobalSettingsSchema.parse() and PersonalSettingsSchema.parse().

The fix should pass legacy data through Zod parsing to ensure schema defaults are applied, changing:
  globalSettings: readAllLegacyGlobalSettings() as GlobalSettings,
  personalSettings: readAllLegacyPersonalSettings() as PersonalSettings,
to:
  globalSettings: GlobalSettingsSchema.parse(readAllLegacyGlobalSettings()),
  personalSettings: PersonalSettingsSchema.parse(readAllLegacyPersonalSettings()),

This ensures fields like Relations always have their default values, preventing TypeError when callers access them (e.g., DiscourseRelationConfigPanel.tsx handleDelete/handleDuplicate which destructure getGlobalSettings().Relations).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

sid597 added 2 commits April 15, 2026 22:07
…to eng-1617-use-existing-getconfigtree-equivalent-functions-for-specific
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