From 343dc117bef4337edf77151ab568d910ed5c96be Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 6 Apr 2026 17:00:13 +0530 Subject: [PATCH 01/17] ENG-1616: Bulk-read settings + thread snapshot (with timing logs) 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. --- .../src/components/DiscourseFloatingMenu.tsx | 11 +- apps/roam/src/components/LeftSidebarView.tsx | 53 +++++-- .../components/settings/QueryPagesPanel.tsx | 17 ++- .../components/settings/utils/accessors.ts | 63 ++++++++- .../src/components/settings/utils/init.ts | 37 ++++- .../utils/migrateLegacyToBlockProps.ts | 10 +- apps/roam/src/index.ts | 86 +++++++++--- apps/roam/src/utils/findDiscourseNode.ts | 8 +- apps/roam/src/utils/getDiscourseNodes.ts | 14 +- .../src/utils/getDiscourseRelationLabels.ts | 17 ++- apps/roam/src/utils/getDiscourseRelations.ts | 10 +- .../src/utils/initializeDiscourseNodes.ts | 7 +- .../utils/initializeObserversAndListeners.ts | 130 ++++++++++++++---- apps/roam/src/utils/isCanvasPage.ts | 30 +++- apps/roam/src/utils/isQueryPage.ts | 11 +- apps/roam/src/utils/posthog.ts | 9 +- apps/roam/src/utils/refreshConfigTree.ts | 29 +++- .../registerDiscourseDatalogTranslators.ts | 7 +- apps/roam/src/utils/setQueryPages.ts | 12 +- 19 files changed, 441 insertions(+), 120 deletions(-) diff --git a/apps/roam/src/components/DiscourseFloatingMenu.tsx b/apps/roam/src/components/DiscourseFloatingMenu.tsx index 3a7e337b9..0f6c8e52d 100644 --- a/apps/roam/src/components/DiscourseFloatingMenu.tsx +++ b/apps/roam/src/components/DiscourseFloatingMenu.tsx @@ -13,7 +13,10 @@ import { import { FeedbackWidget } from "./BirdEatsBugs"; import { render as renderSettings } from "~/components/settings/Settings"; import posthog from "posthog-js"; -import { getPersonalSetting } from "./settings/utils/accessors"; +import { + getPersonalSetting, + type SettingsSnapshot, +} from "./settings/utils/accessors"; import { PERSONAL_KEYS } from "./settings/utils/settingKeys"; type DiscourseFloatingMenuProps = { @@ -118,6 +121,7 @@ export const showDiscourseFloatingMenu = () => { export const installDiscourseFloatingMenu = ( onLoadArgs: OnloadArgs, + snapshot?: SettingsSnapshot, props: DiscourseFloatingMenuProps = { position: "bottom-right", theme: "bp3-light", @@ -130,7 +134,10 @@ export const installDiscourseFloatingMenu = ( floatingMenuAnchor.id = ANCHOR_ID; document.getElementById("app")?.appendChild(floatingMenuAnchor); } - if (getPersonalSetting([PERSONAL_KEYS.hideFeedbackButton])) { + const hideFeedbackButton = snapshot + ? snapshot.personalSettings[PERSONAL_KEYS.hideFeedbackButton] + : getPersonalSetting([PERSONAL_KEYS.hideFeedbackButton]); + if (hideFeedbackButton) { floatingMenuAnchor.classList.add("hidden"); } ReactDOM.render( diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 46ca39cc5..abb3b6680 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -39,6 +39,7 @@ import { getPersonalSettings, setGlobalSetting, setPersonalSetting, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS, @@ -79,6 +80,10 @@ const truncate = (s: string, max: number | undefined): string => { }; const openTarget = async (e: React.MouseEvent, targetUid: string) => { + const _navStart = performance.now(); + console.log( + `[DG Nav] openTarget click t=${Math.round(_navStart)} target=${targetUid}`, + ); e.preventDefault(); e.stopPropagation(); const target = parseReference(targetUid); @@ -89,11 +94,17 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { if (target.type === "block") { if (e.shiftKey) { await openBlockInSidebar(target.uid); + console.log( + `[DG Nav] openBlockInSidebar resolved +${Math.round(performance.now() - _navStart)}ms`, + ); return; } await window.roamAlphaAPI.ui.mainWindow.openBlock({ block: { uid: target.uid }, }); + console.log( + `[DG Nav] openBlock resolved +${Math.round(performance.now() - _navStart)}ms`, + ); return; } @@ -103,10 +114,16 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { // eslint-disable-next-line @typescript-eslint/naming-convention window: { type: "outline", "block-uid": targetUid }, }); + console.log( + `[DG Nav] rightSidebar.addWindow resolved +${Math.round(performance.now() - _navStart)}ms`, + ); } else { await window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid: targetUid }, }); + console.log( + `[DG Nav] openPage resolved +${Math.round(performance.now() - _navStart)}ms`, + ); } }; @@ -336,14 +353,16 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { // TODO(ENG-1471): Remove old-system merge when migration complete — just use accessor values directly. // See mergeGlobalSectionWithAccessor/mergePersonalSectionsWithAccessor for why the merge exists. -const buildConfig = (): LeftSidebarConfig => { +const buildConfig = (snapshot?: SettingsSnapshot): LeftSidebarConfig => { // Read VALUES from accessor (handles flag routing + mismatch detection) - const globalValues = getGlobalSetting([ - GLOBAL_KEYS.leftSidebar, - ]); - const personalValues = getPersonalSetting< - ReturnType[typeof PERSONAL_KEYS.leftSidebar] - >([PERSONAL_KEYS.leftSidebar]); + const globalValues = snapshot + ? snapshot.globalSettings[GLOBAL_KEYS.leftSidebar] + : getGlobalSetting([GLOBAL_KEYS.leftSidebar]); + const personalValues = snapshot + ? snapshot.personalSettings[PERSONAL_KEYS.leftSidebar] + : getPersonalSetting< + ReturnType[typeof PERSONAL_KEYS.leftSidebar] + >([PERSONAL_KEYS.leftSidebar]); // Read UIDs from old system (needed for fold CRUD during dual-write) const oldConfig = getCurrentLeftSidebarConfig(); @@ -364,8 +383,8 @@ const buildConfig = (): LeftSidebarConfig => { }; }; -export const useConfig = () => { - const [config, setConfig] = useState(() => buildConfig()); +export const useConfig = (initialSnapshot?: SettingsSnapshot) => { + const [config, setConfig] = useState(() => buildConfig(initialSnapshot)); useEffect(() => { const handleUpdate = () => { setConfig(buildConfig()); @@ -504,8 +523,14 @@ const FavoritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { ); }; -const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { - const { config } = useConfig(); +const LeftSidebarView = ({ + onloadArgs, + initialSnapshot, +}: { + onloadArgs: OnloadArgs; + initialSnapshot?: SettingsSnapshot; +}) => { + const { config } = useConfig(initialSnapshot); return ( <> @@ -613,6 +638,7 @@ const migrateFavorites = async () => { export const mountLeftSidebar = async ( wrapper: HTMLElement, onloadArgs: OnloadArgs, + initialSnapshot?: SettingsSnapshot, ): Promise => { if (!wrapper) return; @@ -630,7 +656,10 @@ export const mountLeftSidebar = async ( } else { root.className = "starred-pages"; } - ReactDOM.render(, root); + ReactDOM.render( + , + root, + ); }; export default LeftSidebarView; diff --git a/apps/roam/src/components/settings/QueryPagesPanel.tsx b/apps/roam/src/components/settings/QueryPagesPanel.tsx index 49122bf5e..4966a4ef8 100644 --- a/apps/roam/src/components/settings/QueryPagesPanel.tsx +++ b/apps/roam/src/components/settings/QueryPagesPanel.tsx @@ -4,7 +4,9 @@ import React, { useState } from "react"; import type { OnloadArgs } from "roamjs-components/types"; import { getPersonalSetting, + readPathValue, setPersonalSetting, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS, @@ -13,11 +15,16 @@ import { // Legacy extensionAPI stored query-pages as string | string[] | Record. // Coerce to string[] for backward compatibility with old stored formats. -export const getQueryPages = (): string[] => { - const value = getPersonalSetting>([ - PERSONAL_KEYS.query, - QUERY_KEYS.queryPages, - ]); +export const getQueryPages = (snapshot?: SettingsSnapshot): string[] => { + const value = snapshot + ? (readPathValue(snapshot.personalSettings, [ + PERSONAL_KEYS.query, + QUERY_KEYS.queryPages, + ]) as string[] | string | Record | undefined) + : getPersonalSetting>([ + PERSONAL_KEYS.query, + QUERY_KEYS.queryPages, + ]); return typeof value === "string" ? [value] : Array.isArray(value) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 2eb2c0b2a..10cd32585 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -143,7 +143,7 @@ const getSchemaAtPath = ( const formatSettingPath = (keys: string[]): string => keys.length === 0 ? "(root)" : keys.join(" > "); -const readPathValue = (root: unknown, keys: string[]): unknown => +export const readPathValue = (root: unknown, keys: string[]): unknown => keys.reduce((current, key) => { if (Array.isArray(current)) { const index = Number(key); @@ -863,8 +863,10 @@ export const setGlobalSetting = (keys: string[], value: json): void => { }); }; -export const getAllRelations = (): DiscourseRelation[] => { - const settings = getGlobalSettings(); +export const getAllRelations = ( + snapshot?: SettingsSnapshot, +): DiscourseRelation[] => { + const settings = snapshot ? snapshot.globalSettings : getGlobalSettings(); return Object.entries(settings.Relations).flatMap(([id, relation]) => relation.ifConditions.map((ifCondition) => ({ @@ -909,6 +911,61 @@ export const getPersonalSetting = ( return blockPropsValue as T | undefined; }; +export type SettingsSnapshot = { + featureFlags: FeatureFlags; + globalSettings: GlobalSettings; + personalSettings: PersonalSettings; +}; + +export const bulkReadSettings = (): SettingsSnapshot => { + const start = performance.now(); + + const pageResult = window.roamAlphaAPI.pull( + "[{:block/children [:block/string :block/props]}]", + [":node/title", DG_BLOCK_PROP_SETTINGS_PAGE_TITLE], + ) as Record | null; + const afterQuery = performance.now(); + + const children = (pageResult?.[":block/children"] ?? []) as Record< + string, + json + >[]; + const personalKey = getPersonalSettingsKey(); + let featureFlagsProps: json = {}; + let globalProps: json = {}; + let personalProps: json = {}; + + for (const child of children) { + const text = child[":block/string"]; + if (typeof text !== "string") continue; + const rawBlockProps = child[":block/props"]; + const blockProps = + rawBlockProps && typeof rawBlockProps === "object" + ? normalizeProps(rawBlockProps) + : {}; + if (text === TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags) { + featureFlagsProps = blockProps; + } else if (text === TOP_LEVEL_BLOCK_PROP_KEYS.global) { + globalProps = blockProps; + } else if (text === personalKey) { + personalProps = blockProps; + } + } + + const snapshot: SettingsSnapshot = { + featureFlags: FeatureFlagsSchema.parse(featureFlagsProps || {}), + globalSettings: GlobalSettingsSchema.parse(globalProps || {}), + personalSettings: PersonalSettingsSchema.parse(personalProps || {}), + }; + + const end = performance.now(); + console.log( + `[DG Plugin] bulkReadSettings: ${Math.round(end - start)}ms (query ${Math.round(afterQuery - start)}ms, parse ${Math.round(end - afterQuery)}ms)`, + ); + + return snapshot; +}; + export const setPersonalSetting = (keys: string[], value: json): void => { if (keys.length === 0) { internalError({ diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index 07a15f1d6..487314652 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -189,15 +189,29 @@ const initializeSettingsBlockProps = ( }; const initSettingsPageBlocks = async (): Promise> => { + let t = performance.now(); + const mark = (label: string) => { + const now = performance.now(); + console.log( + `[DG Plugin] initSettingsPageBlocks.${label}: ${Math.round(now - t)}ms`, + ); + t = now; + }; + const pageUid = await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE); + mark("ensurePageExists"); const blockMap = buildBlockMap(pageUid); + mark("buildBlockMap"); const topLevelBlocks = getTopLevelBlockPropsConfig().map(({ key }) => key); await ensureBlocksExist(pageUid, topLevelBlocks, blockMap); + mark("ensureBlocksExist (top-level)"); await ensureLegacyConfigBlocks(pageUid); + mark("ensureLegacyConfigBlocks"); initializeSettingsBlockProps(pageUid, blockMap); + mark("initializeSettingsBlockProps"); return blockMap; }; @@ -411,16 +425,25 @@ const logDualReadComparison = (): void => { }; export const initSchema = async (): Promise => { + console.log("[DG Plugin] Initializing schema..."); + let t = performance.now(); + const mark = (label: string) => { + const now = performance.now(); + console.log(`[DG Plugin] initSchema.${label}: ${Math.round(now - t)}ms`); + t = now; + }; + const blockUids = await initSettingsPageBlocks(); + mark("initSettingsPageBlocks"); + await migrateGraphLevel(blockUids); + mark("migrateGraphLevel"); + const nodePageUids = await initDiscourseNodePages(); + mark("initDiscourseNodePages"); + await migratePersonalSettings(blockUids); - try { - logDualReadComparison(); - } catch (e) { - console.warn("[DG Dual-Read] Comparison failed:", e); - } - (window as unknown as Record).dgDualReadLog = - logDualReadComparison; + mark("migratePersonalSettings"); + return { blockUids, nodePageUids }; }; diff --git a/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts b/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts index 8ca1fe496..0e0fa45c9 100644 --- a/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts +++ b/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts @@ -1,7 +1,6 @@ import getBlockProps from "~/utils/getBlockProps"; import type { json } from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; -import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import { createBlock } from "roamjs-components/writes"; import { getSetting, setSetting } from "~/utils/extensionSettings"; @@ -29,11 +28,8 @@ const GRAPH_MIGRATION_MARKER = "Block props migrated"; const PERSONAL_MIGRATION_MARKER = "dg-personal-settings-migrated"; const MAX_ERROR_CONTEXT_LENGTH = 5000; -const hasGraphMigrationMarker = (): boolean => - !!getBlockUidByTextOnPage({ - text: GRAPH_MIGRATION_MARKER, - title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, - }); +const hasGraphMigrationMarker = (blockMap: Record): boolean => + !!blockMap[GRAPH_MIGRATION_MARKER]; const isPropsValid = ( schema: z.ZodTypeAny, @@ -182,7 +178,7 @@ export const migrateGraphLevel = async ( return; } - if (hasGraphMigrationMarker()) { + if (hasGraphMigrationMarker(blockUids)) { console.log(`${LOG_PREFIX} graph-level: skipped (already migrated)`); return; } diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index ff8e95d9a..3f3ad01b9 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -31,12 +31,12 @@ import { initializeSupabaseSync, setSyncActivity, } from "./utils/syncDgNodesToSupabase"; -import { initPluginTimer } from "./utils/pluginTimer"; +import { getPluginElapsedTime, initPluginTimer } from "./utils/pluginTimer"; import { initPostHog } from "./utils/posthog"; import { initSchema } from "./components/settings/utils/init"; import { - getFeatureFlag, - getPersonalSetting, + bulkReadSettings, + readPathValue, } from "./components/settings/utils/accessors"; import { PERSONAL_KEYS } from "./components/settings/utils/settingKeys"; import { setupPullWatchOnSettingsPage } from "./components/settings/utils/pullWatchers"; @@ -49,17 +49,34 @@ import { mountLeftSidebar } from "./components/LeftSidebarView"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; export default runExtension(async (onloadArgs) => { + initPluginTimer(); + console.log("[DG Plugin] load: start"); + let lastMark = performance.now(); + const mark = (label: string) => { + const now = performance.now(); + console.log( + `[DG Plugin] ${label}: +${Math.round(now - lastMark)}ms (total ${getPluginElapsedTime()}ms)`, + ); + lastMark = now; + }; + + const settingsSnapshot = bulkReadSettings(); + mark("bulkReadSettings"); + const isEncrypted = window.roamAlphaAPI.graph.isEncrypted; + mark("encrypted check"); const isOffline = window.roamAlphaAPI.graph.type === "offline"; - const disallowDiagnostics = getPersonalSetting([ + mark("offline check"); + const disallowDiagnostics = readPathValue(settingsSnapshot.personalSettings, [ PERSONAL_KEYS.disableProductDiagnostics, - ]); + ]) as boolean | undefined; + mark("diagnostics check"); if (!isEncrypted && !isOffline && !disallowDiagnostics) { initPostHog(); } - + mark("posthog init"); initFeedbackWidget(); - + mark("feedback widget init"); if (window?.roamjs?.loaded?.has("query-builder")) { renderToast({ timeout: 10000, @@ -78,34 +95,51 @@ export default runExtension(async (onloadArgs) => { timeout: 500, }); } + mark("posthog + feedback widget + load check"); + await initializeDiscourseNodes(settingsSnapshot); + mark("initializeDiscourseNodes"); - initPluginTimer(); - - await initializeDiscourseNodes(); - refreshConfigTree(); + refreshConfigTree(settingsSnapshot); + mark("refreshConfigTree"); addGraphViewNodeStyling(); + mark("graph view styling"); registerCommandPaletteCommands(onloadArgs); + mark("command palette commands"); createSettingsPanel(onloadArgs); + mark("settings panel"); registerSmartBlock(onloadArgs); - setInitialQueryPages(onloadArgs); + mark("registerSmartBlock"); + + setInitialQueryPages(onloadArgs, settingsSnapshot); + mark("setInitialQueryPages"); const style = addStyle(styles); + mark("addStyle styles"); const discourseGraphStyle = addStyle(discourseGraphStyles); + mark("addStyle discourseGraphStyles"); const settingsStyle = addStyle(settingsStyles); + mark("addStyle settingsStyles"); const discourseFloatingMenuStyle = addStyle(discourseFloatingMenuStyles); + mark("addStyle discourseFloatingMenuStyles"); // Add streamline styling only if enabled - const isStreamlineStylingEnabled = getPersonalSetting([ - PERSONAL_KEYS.streamlineStyling, - ]); + const isStreamlineStylingEnabled = readPathValue( + settingsSnapshot.personalSettings, + [PERSONAL_KEYS.streamlineStyling], + ) as boolean | undefined; let streamlineStyleElement: HTMLStyleElement | null = null; if (isStreamlineStylingEnabled) { streamlineStyleElement = addStyle(streamlineStyling); streamlineStyleElement.id = "streamline-styling"; } + mark("streamline style check"); - const { observers, listeners, cleanups } = initObservers({ onloadArgs }); + const { observers, listeners, cleanups } = initObservers({ + onloadArgs, + settingsSnapshot, + }); + mark("initObservers"); const { pageActionListener, hashChangeListener, @@ -114,14 +148,20 @@ export default runExtension(async (onloadArgs) => { nodeCreationPopoverListener, } = listeners; document.addEventListener("roamjs:query-builder:action", pageActionListener); + mark("pageActionListener addEventListener"); window.addEventListener("hashchange", hashChangeListener); + mark("hashChangeListener addEventListener"); document.addEventListener("keydown", nodeMenuTriggerListener); + mark("nodeMenuTriggerListener addEventListener"); document.addEventListener("input", discourseNodeSearchTriggerListener); + mark("discourseNodeSearchTriggerListener addEventListener"); document.addEventListener("selectionchange", nodeCreationPopoverListener); + mark("document event listeners"); - if (getFeatureFlag("Suggestive mode enabled")) { + if (settingsSnapshot.featureFlags["Suggestive mode enabled"]) { initializeSupabaseSync(); } + mark("suggestive supabase init"); const unsubSuggestiveMode = onSettingChange( settingKeys.suggestiveModeEnabled, @@ -133,6 +173,7 @@ export default runExtension(async (onloadArgs) => { } }, ); + mark("unsubSuggestiveMode onSettingChange"); const { extensionAPI } = onloadArgs; window.roamjs.extension.queryBuilder = { @@ -149,12 +190,15 @@ export default runExtension(async (onloadArgs) => { // @ts-expect-error - we are still using roamjs-components global definition getDiscourseNodes: getDiscourseNodes, }; + mark("roamjs.extension.queryBuilder assign"); - installDiscourseFloatingMenu(onloadArgs); + installDiscourseFloatingMenu(onloadArgs, settingsSnapshot); + mark("installDiscourseFloatingMenu"); const leftSidebarScript = document.querySelector( 'script#roam-left-sidebar[src="https://sid597.github.io/roam-left-sidebar/js/main.js"]', ); + mark("leftSidebarScript querySelector"); if (leftSidebarScript) { renderToast({ @@ -165,6 +209,7 @@ export default runExtension(async (onloadArgs) => { "Discourse Graph detected the Roam left sidebar script. Running both sidebars may cause issues. Please remove the Roam left sidebar script from your Roam instance, and reload the graph.", }); } + mark("leftSidebarScript conflict toast"); const unsubLeftSidebarFlag = onSettingChange( settingKeys.leftSidebarFlag, @@ -188,9 +233,14 @@ export default runExtension(async (onloadArgs) => { } }, ); + mark("unsubLeftSidebarFlag onSettingChange"); const { blockUids } = await initSchema(); + mark("initSchema"); const cleanupPullWatchers = setupPullWatchOnSettingsPage(blockUids); + mark("setupPullWatchOnSettingsPage"); + + console.log(`[DG Plugin] load: done in ${getPluginElapsedTime()}ms`); return { elements: [ diff --git a/apps/roam/src/utils/findDiscourseNode.ts b/apps/roam/src/utils/findDiscourseNode.ts index 345d0b41d..e0af95981 100644 --- a/apps/roam/src/utils/findDiscourseNode.ts +++ b/apps/roam/src/utils/findDiscourseNode.ts @@ -1,23 +1,27 @@ import getDiscourseNodes, { type DiscourseNode } from "./getDiscourseNodes"; import matchDiscourseNode from "./matchDiscourseNode"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; const discourseNodeTypeCache: Record = {}; const findDiscourseNode = ({ uid, title, - nodes = getDiscourseNodes(), + nodes, + snapshot, }: { uid: string; title?: string; nodes?: DiscourseNode[]; + snapshot?: SettingsSnapshot; }): DiscourseNode | false => { if (typeof discourseNodeTypeCache[uid] !== "undefined") { return discourseNodeTypeCache[uid]; } + const resolvedNodes = nodes ?? getDiscourseNodes(undefined, snapshot); const matchingNode = - nodes.find((node) => + resolvedNodes.find((node) => title === undefined ? matchDiscourseNode({ ...node, uid }) : matchDiscourseNode({ ...node, title }), diff --git a/apps/roam/src/utils/getDiscourseNodes.ts b/apps/roam/src/utils/getDiscourseNodes.ts index cd83f940f..765d12422 100644 --- a/apps/roam/src/utils/getDiscourseNodes.ts +++ b/apps/roam/src/utils/getDiscourseNodes.ts @@ -3,6 +3,7 @@ import getSubTree from "roamjs-components/util/getSubTree"; import { isNewSettingsStoreEnabled, getAllDiscourseNodes, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import discourseConfigRef from "./discourseConfigRef"; import getDiscourseRelations from "./getDiscourseRelations"; @@ -106,9 +107,16 @@ const getUidAndBooleanSetting = ({ }; }; -const getDiscourseNodes = (relations = getDiscourseRelations()) => { +const getDiscourseNodes = ( + relations?: ReturnType, + snapshot?: SettingsSnapshot, +) => { + const resolvedRelations = relations ?? getDiscourseRelations(snapshot); + const newStoreEnabled = snapshot + ? snapshot.featureFlags["Use new settings store"] + : isNewSettingsStoreEnabled(); const configuredNodes = ( - isNewSettingsStoreEnabled() + newStoreEnabled ? getAllDiscourseNodes() : Object.entries(discourseConfigRef.nodes).map( ([type, { text, children }]): DiscourseNode => { @@ -158,7 +166,7 @@ const getDiscourseNodes = (relations = getDiscourseRelations()) => { }, ) ).concat( - relations + resolvedRelations .filter((r) => r.triples.some((t) => t.some((n) => /anchor/i.test(n)))) .map((r) => ({ format: "", diff --git a/apps/roam/src/utils/getDiscourseRelationLabels.ts b/apps/roam/src/utils/getDiscourseRelationLabels.ts index 3355b8f22..33089618c 100644 --- a/apps/roam/src/utils/getDiscourseRelationLabels.ts +++ b/apps/roam/src/utils/getDiscourseRelationLabels.ts @@ -1,8 +1,17 @@ import getDiscourseRelations from "./getDiscourseRelations"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; -const getDiscourseRelationLabels = (relations = getDiscourseRelations()) => - Array.from(new Set(relations.flatMap((r) => [r.label, r.complement]))).filter( - (s) => !!s, - ); +const getDiscourseRelationLabels = ( + relations?: ReturnType, + snapshot?: SettingsSnapshot, +) => + Array.from( + new Set( + (relations ?? getDiscourseRelations(snapshot)).flatMap((r) => [ + r.label, + r.complement, + ]), + ), + ).filter((s) => !!s); export default getDiscourseRelationLabels; diff --git a/apps/roam/src/utils/getDiscourseRelations.ts b/apps/roam/src/utils/getDiscourseRelations.ts index c9f24a911..d7e36cab7 100644 --- a/apps/roam/src/utils/getDiscourseRelations.ts +++ b/apps/roam/src/utils/getDiscourseRelations.ts @@ -9,6 +9,7 @@ import DEFAULT_RELATION_VALUES from "~/data/defaultDiscourseRelations"; import { isNewSettingsStoreEnabled, getAllRelations, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import discourseConfigRef from "./discourseConfigRef"; @@ -35,9 +36,12 @@ export const getRelationsNode = (grammarNode = getGrammarNode()) => { return grammarNode?.children.find(matchNodeText("relations")); }; -const getDiscourseRelations = () => { - if (isNewSettingsStoreEnabled()) { - return getAllRelations(); +const getDiscourseRelations = (snapshot?: SettingsSnapshot) => { + const newStoreEnabled = snapshot + ? snapshot.featureFlags["Use new settings store"] + : isNewSettingsStoreEnabled(); + if (newStoreEnabled) { + return getAllRelations(snapshot); } const grammarNode = getGrammarNode(); diff --git a/apps/roam/src/utils/initializeDiscourseNodes.ts b/apps/roam/src/utils/initializeDiscourseNodes.ts index 46a832b48..6ab88ddf1 100644 --- a/apps/roam/src/utils/initializeDiscourseNodes.ts +++ b/apps/roam/src/utils/initializeDiscourseNodes.ts @@ -2,9 +2,12 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit import { createPage } from "roamjs-components/writes"; import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; import getDiscourseNodes, { excludeDefaultNodes } from "./getDiscourseNodes"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; -const initializeDiscourseNodes = async () => { - const nodes = getDiscourseNodes().filter(excludeDefaultNodes); +const initializeDiscourseNodes = async (snapshot?: SettingsSnapshot) => { + const nodes = getDiscourseNodes(undefined, snapshot).filter( + excludeDefaultNodes, + ); if (nodes.length === 0) { await Promise.all( INITIAL_NODE_VALUES.map( diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index c946134ce..b49e08c79 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -25,7 +25,9 @@ import { onPageRefObserverChange, getSuggestiveOverlayHandler, } from "~/utils/pageRefObserverHandlers"; -import getDiscourseNodes from "~/utils/getDiscourseNodes"; +import getDiscourseNodes, { + type DiscourseNode, +} from "~/utils/getDiscourseNodes"; import { OnloadArgs } from "roamjs-components/types"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { render as renderGraphOverviewExport } from "~/components/ExportDiscourseContext"; @@ -52,9 +54,9 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import findDiscourseNode from "./findDiscourseNode"; import { - getPersonalSetting, - getFeatureFlag, - getGlobalSetting, + readPathValue, + bulkReadSettings, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { onSettingChange, @@ -88,8 +90,10 @@ const getTitleAndUidFromHeader = (h1: HTMLHeadingElement) => { export const initObservers = ({ onloadArgs, + settingsSnapshot, }: { onloadArgs: OnloadArgs; + settingsSnapshot: SettingsSnapshot; }): { observers: MutationObserver[]; listeners: { @@ -101,17 +105,33 @@ export const initObservers = ({ }; cleanups: Array<() => void>; } => { + let markT = performance.now(); + const markPhase = (label: string) => { + const now = performance.now(); + console.log( + `[DG Plugin] initObservers.${label}: ${Math.round(now - markT)}ms`, + ); + markT = now; + }; + const pageTitleObserver = createHTMLObserver({ tag: "H1", className: "rm-title-display", callback: (e) => { const h1 = e as HTMLHeadingElement; const { title, uid } = getTitleAndUidFromHeader(h1); - const props = { title, h1, onloadArgs }; - const isSuggestiveModeEnabled = getFeatureFlag("Suggestive mode enabled"); + const callbackSnapshot = bulkReadSettings(); - const node = findDiscourseNode({ uid, title }); + const props = { title, h1, onloadArgs }; + + const isSuggestiveModeEnabled = + callbackSnapshot.featureFlags["Suggestive mode enabled"]; + const node = findDiscourseNode({ + uid, + title, + snapshot: callbackSnapshot, + }); const isDiscourseNode = node && node.backedBy !== "default"; if (isDiscourseNode) { renderDiscourseContext({ h1, uid }); @@ -125,17 +145,36 @@ export const initObservers = ({ renderCanvasReferences(linkedReferencesDiv, uid, onloadArgs); } } - - if (isQueryPage({ title })) renderQueryPage(props); - else if (isCurrentPageCanvas(props)) renderTldrawCanvas(props); - else if (isSidebarCanvas(props)) renderTldrawCanvasInSidebar(props); + if (isQueryPage({ title, snapshot: callbackSnapshot })) { + renderQueryPage(props); + } else if ( + isCurrentPageCanvas({ title, h1, snapshot: callbackSnapshot }) + ) { + renderTldrawCanvas(props); + } else if (isSidebarCanvas({ title, h1, snapshot: callbackSnapshot })) { + renderTldrawCanvasInSidebar(props); + } }, }); + markPhase("pageTitleObserver"); const queryBlockObserver = createButtonObserver({ attribute: "query-block", render: (b) => renderQueryBlock(b, onloadArgs), }); + markPhase("queryBlockObserver"); + + let batchedTagNodes: DiscourseNode[] | null = null; + const getNodesForTagBatch = (): DiscourseNode[] => { + if (batchedTagNodes === null) { + const snap = bulkReadSettings(); + batchedTagNodes = getDiscourseNodes(undefined, snap); + queueMicrotask(() => { + batchedTagNodes = null; + }); + } + return batchedTagNodes; + }; const nodeTagPopupButtonObserver = createHTMLObserver({ className: "rm-page-ref--tag", @@ -145,7 +184,7 @@ export const initObservers = ({ if (tag) { const normalizedTag = getCleanTagText(tag); - for (const node of getDiscourseNodes()) { + for (const node of getNodesForTagBatch()) { const normalizedNodeTag = node.tag ? getCleanTagText(node.tag) : ""; if (normalizedTag === normalizedNodeTag) { renderNodeTagPopupButton(s, node, onloadArgs.extensionAPI); @@ -179,6 +218,7 @@ export const initObservers = ({ } }, }); + markPhase("nodeTagPopupButtonObserver"); const pageActionListener = (( e: CustomEvent<{ @@ -203,7 +243,11 @@ export const initObservers = ({ const suggestiveHandler = getSuggestiveOverlayHandler(onloadArgs); const toggleSuggestiveOverlay = onPageRefObserverChange(suggestiveHandler); - if (getPersonalSetting([PERSONAL_KEYS.suggestiveModeOverlay])) { + if ( + readPathValue(settingsSnapshot.personalSettings, [ + PERSONAL_KEYS.suggestiveModeOverlay, + ]) + ) { addPageRefObserver(suggestiveHandler); } @@ -213,6 +257,7 @@ export const initObservers = ({ toggleSuggestiveOverlay(Boolean(newValue)); }, ); + markPhase("pageAction/suggestive handlers"); const graphOverviewExportObserver = createHTMLObserver({ tag: "DIV", @@ -232,35 +277,62 @@ export const initObservers = ({ } }, }); + markPhase("graphOverviewExport + imageMenu observers"); - if (getPersonalSetting([PERSONAL_KEYS.pagePreview])) + if ( + readPathValue(settingsSnapshot.personalSettings, [ + PERSONAL_KEYS.pagePreview, + ]) + ) addPageRefObserver(previewPageRefHandler); - if (getPersonalSetting([PERSONAL_KEYS.discourseContextOverlay])) { + + if ( + readPathValue(settingsSnapshot.personalSettings, [ + PERSONAL_KEYS.discourseContextOverlay, + ]) + ) { const overlayHandler = getOverlayHandler(onloadArgs); onPageRefObserverChange(overlayHandler)(true); } + if (getPageRefObserversSize()) enablePageRefObserver(); + markPhase("pageRef observer wiring"); const configPageUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); + markPhase("configPageUid lookup"); const hashChangeListener = (e: Event) => { const evt = e as HashChangeEvent; + const hashStart = performance.now(); + console.log( + `[DG Nav] hashChange fired t=${Math.round(hashStart)} old=${evt.oldURL} new=${evt.newURL}`, + ); + const navSnapshot = bulkReadSettings(); // Attempt to refresh config navigating away from config page // doesn't work if they update via sidebar if ( (configPageUid && evt.oldURL.endsWith(configPageUid)) || - getDiscourseNodes().some(({ type }) => evt.oldURL.endsWith(type)) + getDiscourseNodes(undefined, navSnapshot).some(({ type }) => + evt.oldURL.endsWith(type), + ) ) { - refreshConfigTree(); + refreshConfigTree(navSnapshot); + console.log( + `[DG Nav] hashChange refreshConfigTree +${Math.round(performance.now() - hashStart)}ms`, + ); } }; + markPhase("hashChangeListener closure"); let globalTrigger = ( - getGlobalSetting([GLOBAL_KEYS.trigger]) ?? "\\" + (readPathValue(settingsSnapshot.globalSettings, [GLOBAL_KEYS.trigger]) as + | string + | undefined) ?? "\\" ).trim(); - const personalTriggerCombo = getPersonalSetting([ - PERSONAL_KEYS.personalNodeMenuTrigger, - ]); + const personalTriggerCombo = readPathValue( + settingsSnapshot.personalSettings, + [PERSONAL_KEYS.personalNodeMenuTrigger], + ) as IKeyCombo | undefined; let personalTrigger = personalTriggerCombo?.key; let personalModifiers = personalTriggerCombo ? getModifiersFromCombo(personalTriggerCombo) @@ -284,6 +356,7 @@ export const initObservers = ({ personalModifiers = combo ? getModifiersFromCombo(combo) : []; }, ); + markPhase("trigger snapshot reads + onSettingChange"); const leftSidebarObserver = createHTMLObserver({ tag: "DIV", @@ -291,15 +364,18 @@ export const initObservers = ({ className: "starred-pages-wrapper", callback: (el) => { void (async () => { - const isLeftSidebarEnabled = getFeatureFlag("Enable left sidebar"); + const callbackSnapshot = bulkReadSettings(); + const isLeftSidebarEnabled = + callbackSnapshot.featureFlags["Enable left sidebar"]; const container = el as HTMLDivElement; if (isLeftSidebarEnabled) { container.style.padding = "0"; - await mountLeftSidebar(container, onloadArgs); + await mountLeftSidebar(container, onloadArgs, callbackSnapshot); } })(); }, }); + markPhase("leftSidebarObserver"); const handleNodeMenuRender = (target: HTMLElement, evt: KeyboardEvent) => { if ( @@ -343,7 +419,9 @@ export const initObservers = ({ }; let customTrigger = - getPersonalSetting([PERSONAL_KEYS.nodeSearchMenuTrigger]) ?? "@"; + (readPathValue(settingsSnapshot.personalSettings, [ + PERSONAL_KEYS.nodeSearchMenuTrigger, + ]) as string | undefined) ?? "@"; const unsubSearchTrigger = onSettingChange( settingKeys.nodeSearchMenuTrigger, @@ -403,8 +481,9 @@ export const initObservers = ({ }; const nodeCreationPopoverListener = debounce(() => { + const snap = bulkReadSettings(); const isTextSelectionPopupEnabled = - getPersonalSetting([PERSONAL_KEYS.textSelectionPopup]) !== false; + snap.personalSettings[PERSONAL_KEYS.textSelectionPopup] !== false; if (!isTextSelectionPopupEnabled) return; @@ -437,6 +516,7 @@ export const initObservers = ({ removeTextSelectionPopup(); } }, 150); + markPhase("listener closures (nodeMenu/search/creationPopover)"); return { observers: [ diff --git a/apps/roam/src/utils/isCanvasPage.ts b/apps/roam/src/utils/isCanvasPage.ts index a9a3b7a49..52e898a6c 100644 --- a/apps/roam/src/utils/isCanvasPage.ts +++ b/apps/roam/src/utils/isCanvasPage.ts @@ -1,10 +1,24 @@ import { DEFAULT_CANVAS_PAGE_FORMAT } from ".."; -import { getGlobalSetting } from "~/components/settings/utils/accessors"; +import { + getGlobalSetting, + readPathValue, + type SettingsSnapshot, +} from "~/components/settings/utils/accessors"; import { GLOBAL_KEYS } from "~/components/settings/utils/settingKeys"; -export const isCanvasPage = ({ title }: { title: string }) => { +export const isCanvasPage = ({ + title, + snapshot, +}: { + title: string; + snapshot?: SettingsSnapshot; +}) => { const format = - getGlobalSetting([GLOBAL_KEYS.canvasPageFormat]) || + (snapshot + ? (readPathValue(snapshot.globalSettings, [ + GLOBAL_KEYS.canvasPageFormat, + ]) as string | undefined) + : getGlobalSetting([GLOBAL_KEYS.canvasPageFormat])) || DEFAULT_CANVAS_PAGE_FORMAT; const canvasRegex = new RegExp(`^${format}$`.replace(/\*/g, ".+")); return canvasRegex.test(title); @@ -13,19 +27,25 @@ export const isCanvasPage = ({ title }: { title: string }) => { export const isCurrentPageCanvas = ({ title, h1, + snapshot, }: { title: string; h1: HTMLHeadingElement; + snapshot?: SettingsSnapshot; }) => { - return isCanvasPage({ title }) && !!h1.closest(".roam-article"); + return isCanvasPage({ title, snapshot }) && !!h1.closest(".roam-article"); }; export const isSidebarCanvas = ({ title, h1, + snapshot, }: { title: string; h1: HTMLHeadingElement; + snapshot?: SettingsSnapshot; }) => { - return isCanvasPage({ title }) && !!h1.closest(".rm-sidebar-outline"); + return ( + isCanvasPage({ title, snapshot }) && !!h1.closest(".rm-sidebar-outline") + ); }; diff --git a/apps/roam/src/utils/isQueryPage.ts b/apps/roam/src/utils/isQueryPage.ts index aa3df2255..af3a831bb 100644 --- a/apps/roam/src/utils/isQueryPage.ts +++ b/apps/roam/src/utils/isQueryPage.ts @@ -1,7 +1,14 @@ import { getQueryPages } from "~/components/settings/QueryPagesPanel"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; -export const isQueryPage = ({ title }: { title: string }): boolean => { - const queryPages = getQueryPages(); +export const isQueryPage = ({ + title, + snapshot, +}: { + title: string; + snapshot?: SettingsSnapshot; +}): boolean => { + const queryPages = getQueryPages(snapshot); const matchesQueryPage = queryPages.some((queryPage) => { const escapedPattern = queryPage diff --git a/apps/roam/src/utils/posthog.ts b/apps/roam/src/utils/posthog.ts index 407c8676d..1149e319a 100644 --- a/apps/roam/src/utils/posthog.ts +++ b/apps/roam/src/utils/posthog.ts @@ -2,8 +2,6 @@ import getCurrentUserUid from "roamjs-components/queries/getCurrentUserUid"; import { getVersionWithDate } from "./getVersion"; import posthog from "posthog-js"; import type { CaptureResult } from "posthog-js"; -import { getPersonalSetting } from "~/components/settings/utils/accessors"; -import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; let initialized = false; @@ -67,10 +65,5 @@ export const disablePostHog = (): void => { }; export const initPostHog = (): void => { - const disabled = getPersonalSetting([ - PERSONAL_KEYS.disableProductDiagnostics, - ]); - if (!disabled) { - doInitPostHog(); - } + doInitPostHog(); }; diff --git a/apps/roam/src/utils/refreshConfigTree.ts b/apps/roam/src/utils/refreshConfigTree.ts index 606d8097c..5f6d7c7c0 100644 --- a/apps/roam/src/utils/refreshConfigTree.ts +++ b/apps/roam/src/utils/refreshConfigTree.ts @@ -6,6 +6,7 @@ import registerDiscourseDatalogTranslators from "./registerDiscourseDatalogTrans import { unregisterDatalogTranslator } from "./conditionToDatalog"; import type { PullBlock } from "roamjs-components/types/native"; import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/data/constants"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; const getPagesStartingWithPrefix = (prefix: string) => ( @@ -17,14 +18,29 @@ const getPagesStartingWithPrefix = (prefix: string) => uid: r[0][":block/uid"] || "", })); -const refreshConfigTree = () => { - getDiscourseRelationLabels().forEach((key) => - unregisterDatalogTranslator({ key }), - ); +const refreshConfigTree = (snapshot?: SettingsSnapshot) => { + let t = performance.now(); + const mark = (label: string) => { + const now = performance.now(); + console.log( + `[DG Plugin] refreshConfigTree.${label}: ${Math.round(now - t)}ms`, + ); + t = now; + }; + + const labels = getDiscourseRelationLabels(undefined, snapshot); + mark("getDiscourseRelationLabels"); + labels.forEach((key) => unregisterDatalogTranslator({ key })); + mark("unregisterDatalogTranslator loop"); + discourseConfigRef.tree = getBasicTreeByParentUid( getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE), ); + mark("getBasicTreeByParentUid(DG config page)"); + const pages = getPagesStartingWithPrefix("discourse-graph/nodes"); + mark(`getPagesStartingWithPrefix (${pages.length} pages)`); + discourseConfigRef.nodes = Object.fromEntries( pages.map(({ title, uid }) => { return [ @@ -36,7 +52,10 @@ const refreshConfigTree = () => { ]; }), ); - registerDiscourseDatalogTranslators(); + mark(`getBasicTreeByParentUid per-page loop (${pages.length} pages)`); + + registerDiscourseDatalogTranslators(snapshot); + mark("registerDiscourseDatalogTranslators"); }; export default refreshConfigTree; diff --git a/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts b/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts index 091ec978d..b9bd53719 100644 --- a/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts +++ b/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts @@ -21,6 +21,7 @@ import { fireQuerySync, getWhereClauses } from "./fireQuery"; import { toVar } from "./compileDatalog"; import { getExistingRelationPageUid } from "./createReifiedBlock"; import { getStoredRelationsEnabled } from "./storedRelations"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; const hasTag = (node: DiscourseNode): node is DiscourseNode & { tag: string } => !!node.tag; @@ -87,9 +88,9 @@ const collectVariables = (clauses: DatalogClause[]): Set => const ANY_DISCOURSE_NODE = "Any discourse node"; -const registerDiscourseDatalogTranslators = () => { - const discourseRelations = getDiscourseRelations(); - const discourseNodes = getDiscourseNodes(discourseRelations); +const registerDiscourseDatalogTranslators = (snapshot?: SettingsSnapshot) => { + const discourseRelations = getDiscourseRelations(snapshot); + const discourseNodes = getDiscourseNodes(discourseRelations, snapshot); const isACallback: Parameters< typeof registerDatalogTranslator diff --git a/apps/roam/src/utils/setQueryPages.ts b/apps/roam/src/utils/setQueryPages.ts index cf5a8be13..bc39d5176 100644 --- a/apps/roam/src/utils/setQueryPages.ts +++ b/apps/roam/src/utils/setQueryPages.ts @@ -1,20 +1,24 @@ import { OnloadArgs } from "roamjs-components/types"; import { - getPersonalSetting, + readPathValue, setPersonalSetting, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS, QUERY_KEYS, } from "~/components/settings/utils/settingKeys"; -export const setInitialQueryPages = (onloadArgs: OnloadArgs) => { +export const setInitialQueryPages = ( + onloadArgs: OnloadArgs, + settingsSnapshot: SettingsSnapshot, +) => { // Legacy extensionAPI stored query-pages as string | string[] | Record. // Coerce to string[] for backward compatibility with old stored formats. - const raw = getPersonalSetting>([ + const raw = readPathValue(settingsSnapshot.personalSettings, [ PERSONAL_KEYS.query, QUERY_KEYS.queryPages, - ]); + ]) as string[] | string | Record | undefined; const queryPageArray = Array.isArray(raw) ? raw : typeof raw === "string" && raw From 4d5c72040835e1ad09f70d06acfad1a2484dc141 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 6 Apr 2026 17:04:06 +0530 Subject: [PATCH 02/17] ENG-1616: Remove plugin-load timing logs 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 (343dc117) kept as a checkpoint for future drill-downs. --- apps/roam/src/components/LeftSidebarView.tsx | 16 ------- .../components/settings/utils/accessors.ts | 12 +---- .../src/components/settings/utils/init.ts | 30 ------------ apps/roam/src/index.ts | 47 +------------------ .../utils/initializeObserversAndListeners.ts | 27 ----------- apps/roam/src/utils/refreshConfigTree.ts | 24 ++-------- 6 files changed, 5 insertions(+), 151 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index abb3b6680..fc6946ba7 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -80,10 +80,6 @@ const truncate = (s: string, max: number | undefined): string => { }; const openTarget = async (e: React.MouseEvent, targetUid: string) => { - const _navStart = performance.now(); - console.log( - `[DG Nav] openTarget click t=${Math.round(_navStart)} target=${targetUid}`, - ); e.preventDefault(); e.stopPropagation(); const target = parseReference(targetUid); @@ -94,17 +90,11 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { if (target.type === "block") { if (e.shiftKey) { await openBlockInSidebar(target.uid); - console.log( - `[DG Nav] openBlockInSidebar resolved +${Math.round(performance.now() - _navStart)}ms`, - ); return; } await window.roamAlphaAPI.ui.mainWindow.openBlock({ block: { uid: target.uid }, }); - console.log( - `[DG Nav] openBlock resolved +${Math.round(performance.now() - _navStart)}ms`, - ); return; } @@ -114,16 +104,10 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { // eslint-disable-next-line @typescript-eslint/naming-convention window: { type: "outline", "block-uid": targetUid }, }); - console.log( - `[DG Nav] rightSidebar.addWindow resolved +${Math.round(performance.now() - _navStart)}ms`, - ); } else { await window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid: targetUid }, }); - console.log( - `[DG Nav] openPage resolved +${Math.round(performance.now() - _navStart)}ms`, - ); } }; diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 10cd32585..25ccf1f9d 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -918,13 +918,10 @@ export type SettingsSnapshot = { }; export const bulkReadSettings = (): SettingsSnapshot => { - const start = performance.now(); - const pageResult = window.roamAlphaAPI.pull( "[{:block/children [:block/string :block/props]}]", [":node/title", DG_BLOCK_PROP_SETTINGS_PAGE_TITLE], ) as Record | null; - const afterQuery = performance.now(); const children = (pageResult?.[":block/children"] ?? []) as Record< string, @@ -952,18 +949,11 @@ export const bulkReadSettings = (): SettingsSnapshot => { } } - const snapshot: SettingsSnapshot = { + return { featureFlags: FeatureFlagsSchema.parse(featureFlagsProps || {}), globalSettings: GlobalSettingsSchema.parse(globalProps || {}), personalSettings: PersonalSettingsSchema.parse(personalProps || {}), }; - - const end = performance.now(); - console.log( - `[DG Plugin] bulkReadSettings: ${Math.round(end - start)}ms (query ${Math.round(afterQuery - start)}ms, parse ${Math.round(end - afterQuery)}ms)`, - ); - - return snapshot; }; export const setPersonalSetting = (keys: string[], value: json): void => { diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index 487314652..c9be88963 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -189,29 +189,15 @@ const initializeSettingsBlockProps = ( }; const initSettingsPageBlocks = async (): Promise> => { - let t = performance.now(); - const mark = (label: string) => { - const now = performance.now(); - console.log( - `[DG Plugin] initSettingsPageBlocks.${label}: ${Math.round(now - t)}ms`, - ); - t = now; - }; - const pageUid = await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE); - mark("ensurePageExists"); const blockMap = buildBlockMap(pageUid); - mark("buildBlockMap"); const topLevelBlocks = getTopLevelBlockPropsConfig().map(({ key }) => key); await ensureBlocksExist(pageUid, topLevelBlocks, blockMap); - mark("ensureBlocksExist (top-level)"); await ensureLegacyConfigBlocks(pageUid); - mark("ensureLegacyConfigBlocks"); initializeSettingsBlockProps(pageUid, blockMap); - mark("initializeSettingsBlockProps"); return blockMap; }; @@ -425,25 +411,9 @@ const logDualReadComparison = (): void => { }; export const initSchema = async (): Promise => { - console.log("[DG Plugin] Initializing schema..."); - let t = performance.now(); - const mark = (label: string) => { - const now = performance.now(); - console.log(`[DG Plugin] initSchema.${label}: ${Math.round(now - t)}ms`); - t = now; - }; - const blockUids = await initSettingsPageBlocks(); - mark("initSettingsPageBlocks"); - await migrateGraphLevel(blockUids); - mark("migrateGraphLevel"); - const nodePageUids = await initDiscourseNodePages(); - mark("initDiscourseNodePages"); - await migratePersonalSettings(blockUids); - mark("migratePersonalSettings"); - return { blockUids, nodePageUids }; }; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 3f3ad01b9..4beaec802 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -31,7 +31,7 @@ import { initializeSupabaseSync, setSyncActivity, } from "./utils/syncDgNodesToSupabase"; -import { getPluginElapsedTime, initPluginTimer } from "./utils/pluginTimer"; +import { initPluginTimer } from "./utils/pluginTimer"; import { initPostHog } from "./utils/posthog"; import { initSchema } from "./components/settings/utils/init"; import { @@ -50,33 +50,18 @@ export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; export default runExtension(async (onloadArgs) => { initPluginTimer(); - console.log("[DG Plugin] load: start"); - let lastMark = performance.now(); - const mark = (label: string) => { - const now = performance.now(); - console.log( - `[DG Plugin] ${label}: +${Math.round(now - lastMark)}ms (total ${getPluginElapsedTime()}ms)`, - ); - lastMark = now; - }; const settingsSnapshot = bulkReadSettings(); - mark("bulkReadSettings"); const isEncrypted = window.roamAlphaAPI.graph.isEncrypted; - mark("encrypted check"); const isOffline = window.roamAlphaAPI.graph.type === "offline"; - mark("offline check"); const disallowDiagnostics = readPathValue(settingsSnapshot.personalSettings, [ PERSONAL_KEYS.disableProductDiagnostics, ]) as boolean | undefined; - mark("diagnostics check"); if (!isEncrypted && !isOffline && !disallowDiagnostics) { initPostHog(); } - mark("posthog init"); initFeedbackWidget(); - mark("feedback widget init"); if (window?.roamjs?.loaded?.has("query-builder")) { renderToast({ timeout: 10000, @@ -95,33 +80,21 @@ export default runExtension(async (onloadArgs) => { timeout: 500, }); } - mark("posthog + feedback widget + load check"); await initializeDiscourseNodes(settingsSnapshot); - mark("initializeDiscourseNodes"); refreshConfigTree(settingsSnapshot); - mark("refreshConfigTree"); addGraphViewNodeStyling(); - mark("graph view styling"); registerCommandPaletteCommands(onloadArgs); - mark("command palette commands"); createSettingsPanel(onloadArgs); - mark("settings panel"); registerSmartBlock(onloadArgs); - mark("registerSmartBlock"); setInitialQueryPages(onloadArgs, settingsSnapshot); - mark("setInitialQueryPages"); const style = addStyle(styles); - mark("addStyle styles"); const discourseGraphStyle = addStyle(discourseGraphStyles); - mark("addStyle discourseGraphStyles"); const settingsStyle = addStyle(settingsStyles); - mark("addStyle settingsStyles"); const discourseFloatingMenuStyle = addStyle(discourseFloatingMenuStyles); - mark("addStyle discourseFloatingMenuStyles"); // Add streamline styling only if enabled const isStreamlineStylingEnabled = readPathValue( @@ -133,13 +106,11 @@ export default runExtension(async (onloadArgs) => { streamlineStyleElement = addStyle(streamlineStyling); streamlineStyleElement.id = "streamline-styling"; } - mark("streamline style check"); const { observers, listeners, cleanups } = initObservers({ onloadArgs, settingsSnapshot, }); - mark("initObservers"); const { pageActionListener, hashChangeListener, @@ -148,20 +119,14 @@ export default runExtension(async (onloadArgs) => { nodeCreationPopoverListener, } = listeners; document.addEventListener("roamjs:query-builder:action", pageActionListener); - mark("pageActionListener addEventListener"); window.addEventListener("hashchange", hashChangeListener); - mark("hashChangeListener addEventListener"); document.addEventListener("keydown", nodeMenuTriggerListener); - mark("nodeMenuTriggerListener addEventListener"); document.addEventListener("input", discourseNodeSearchTriggerListener); - mark("discourseNodeSearchTriggerListener addEventListener"); document.addEventListener("selectionchange", nodeCreationPopoverListener); - mark("document event listeners"); if (settingsSnapshot.featureFlags["Suggestive mode enabled"]) { initializeSupabaseSync(); } - mark("suggestive supabase init"); const unsubSuggestiveMode = onSettingChange( settingKeys.suggestiveModeEnabled, @@ -173,7 +138,6 @@ export default runExtension(async (onloadArgs) => { } }, ); - mark("unsubSuggestiveMode onSettingChange"); const { extensionAPI } = onloadArgs; window.roamjs.extension.queryBuilder = { @@ -190,15 +154,12 @@ export default runExtension(async (onloadArgs) => { // @ts-expect-error - we are still using roamjs-components global definition getDiscourseNodes: getDiscourseNodes, }; - mark("roamjs.extension.queryBuilder assign"); installDiscourseFloatingMenu(onloadArgs, settingsSnapshot); - mark("installDiscourseFloatingMenu"); const leftSidebarScript = document.querySelector( 'script#roam-left-sidebar[src="https://sid597.github.io/roam-left-sidebar/js/main.js"]', ); - mark("leftSidebarScript querySelector"); if (leftSidebarScript) { renderToast({ @@ -209,7 +170,6 @@ export default runExtension(async (onloadArgs) => { "Discourse Graph detected the Roam left sidebar script. Running both sidebars may cause issues. Please remove the Roam left sidebar script from your Roam instance, and reload the graph.", }); } - mark("leftSidebarScript conflict toast"); const unsubLeftSidebarFlag = onSettingChange( settingKeys.leftSidebarFlag, @@ -233,14 +193,9 @@ export default runExtension(async (onloadArgs) => { } }, ); - mark("unsubLeftSidebarFlag onSettingChange"); const { blockUids } = await initSchema(); - mark("initSchema"); const cleanupPullWatchers = setupPullWatchOnSettingsPage(blockUids); - mark("setupPullWatchOnSettingsPage"); - - console.log(`[DG Plugin] load: done in ${getPluginElapsedTime()}ms`); return { elements: [ diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index b49e08c79..b601f79d9 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -105,15 +105,6 @@ export const initObservers = ({ }; cleanups: Array<() => void>; } => { - let markT = performance.now(); - const markPhase = (label: string) => { - const now = performance.now(); - console.log( - `[DG Plugin] initObservers.${label}: ${Math.round(now - markT)}ms`, - ); - markT = now; - }; - const pageTitleObserver = createHTMLObserver({ tag: "H1", className: "rm-title-display", @@ -156,13 +147,11 @@ export const initObservers = ({ } }, }); - markPhase("pageTitleObserver"); const queryBlockObserver = createButtonObserver({ attribute: "query-block", render: (b) => renderQueryBlock(b, onloadArgs), }); - markPhase("queryBlockObserver"); let batchedTagNodes: DiscourseNode[] | null = null; const getNodesForTagBatch = (): DiscourseNode[] => { @@ -218,7 +207,6 @@ export const initObservers = ({ } }, }); - markPhase("nodeTagPopupButtonObserver"); const pageActionListener = (( e: CustomEvent<{ @@ -257,7 +245,6 @@ export const initObservers = ({ toggleSuggestiveOverlay(Boolean(newValue)); }, ); - markPhase("pageAction/suggestive handlers"); const graphOverviewExportObserver = createHTMLObserver({ tag: "DIV", @@ -277,7 +264,6 @@ export const initObservers = ({ } }, }); - markPhase("graphOverviewExport + imageMenu observers"); if ( readPathValue(settingsSnapshot.personalSettings, [ @@ -296,17 +282,11 @@ export const initObservers = ({ } if (getPageRefObserversSize()) enablePageRefObserver(); - markPhase("pageRef observer wiring"); const configPageUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); - markPhase("configPageUid lookup"); const hashChangeListener = (e: Event) => { const evt = e as HashChangeEvent; - const hashStart = performance.now(); - console.log( - `[DG Nav] hashChange fired t=${Math.round(hashStart)} old=${evt.oldURL} new=${evt.newURL}`, - ); const navSnapshot = bulkReadSettings(); // Attempt to refresh config navigating away from config page // doesn't work if they update via sidebar @@ -317,12 +297,8 @@ export const initObservers = ({ ) ) { refreshConfigTree(navSnapshot); - console.log( - `[DG Nav] hashChange refreshConfigTree +${Math.round(performance.now() - hashStart)}ms`, - ); } }; - markPhase("hashChangeListener closure"); let globalTrigger = ( (readPathValue(settingsSnapshot.globalSettings, [GLOBAL_KEYS.trigger]) as @@ -356,7 +332,6 @@ export const initObservers = ({ personalModifiers = combo ? getModifiersFromCombo(combo) : []; }, ); - markPhase("trigger snapshot reads + onSettingChange"); const leftSidebarObserver = createHTMLObserver({ tag: "DIV", @@ -375,7 +350,6 @@ export const initObservers = ({ })(); }, }); - markPhase("leftSidebarObserver"); const handleNodeMenuRender = (target: HTMLElement, evt: KeyboardEvent) => { if ( @@ -516,7 +490,6 @@ export const initObservers = ({ removeTextSelectionPopup(); } }, 150); - markPhase("listener closures (nodeMenu/search/creationPopover)"); return { observers: [ diff --git a/apps/roam/src/utils/refreshConfigTree.ts b/apps/roam/src/utils/refreshConfigTree.ts index 5f6d7c7c0..8d23401c8 100644 --- a/apps/roam/src/utils/refreshConfigTree.ts +++ b/apps/roam/src/utils/refreshConfigTree.ts @@ -19,28 +19,13 @@ const getPagesStartingWithPrefix = (prefix: string) => })); const refreshConfigTree = (snapshot?: SettingsSnapshot) => { - let t = performance.now(); - const mark = (label: string) => { - const now = performance.now(); - console.log( - `[DG Plugin] refreshConfigTree.${label}: ${Math.round(now - t)}ms`, - ); - t = now; - }; - - const labels = getDiscourseRelationLabels(undefined, snapshot); - mark("getDiscourseRelationLabels"); - labels.forEach((key) => unregisterDatalogTranslator({ key })); - mark("unregisterDatalogTranslator loop"); - + getDiscourseRelationLabels(undefined, snapshot).forEach((key) => + unregisterDatalogTranslator({ key }), + ); discourseConfigRef.tree = getBasicTreeByParentUid( getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE), ); - mark("getBasicTreeByParentUid(DG config page)"); - const pages = getPagesStartingWithPrefix("discourse-graph/nodes"); - mark(`getPagesStartingWithPrefix (${pages.length} pages)`); - discourseConfigRef.nodes = Object.fromEntries( pages.map(({ title, uid }) => { return [ @@ -52,10 +37,7 @@ const refreshConfigTree = (snapshot?: SettingsSnapshot) => { ]; }), ); - mark(`getBasicTreeByParentUid per-page loop (${pages.length} pages)`); - registerDiscourseDatalogTranslators(snapshot); - mark("registerDiscourseDatalogTranslators"); }; export default refreshConfigTree; From b13d401b0954820e9dd05f1fba3e92b76cfdcae1 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 6 Apr 2026 20:26:44 +0530 Subject: [PATCH 03/17] =?UTF-8?q?ENG-1616:=20Address=20review=20=E2=80=94?= =?UTF-8?q?=20typed=20indexing,=20restore=20dgDualReadLog,=20optional=20sn?= =?UTF-8?q?apshot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../components/settings/QueryPagesPanel.tsx | 8 +--- .../src/components/settings/utils/init.ts | 5 +++ apps/roam/src/index.ts | 23 +++++------ .../utils/initializeObserversAndListeners.ts | 38 ++++++------------- apps/roam/src/utils/isCanvasPage.ts | 5 +-- apps/roam/src/utils/setQueryPages.ts | 14 ++++--- 6 files changed, 38 insertions(+), 55 deletions(-) diff --git a/apps/roam/src/components/settings/QueryPagesPanel.tsx b/apps/roam/src/components/settings/QueryPagesPanel.tsx index 4966a4ef8..c6f792dca 100644 --- a/apps/roam/src/components/settings/QueryPagesPanel.tsx +++ b/apps/roam/src/components/settings/QueryPagesPanel.tsx @@ -4,7 +4,6 @@ import React, { useState } from "react"; import type { OnloadArgs } from "roamjs-components/types"; import { getPersonalSetting, - readPathValue, setPersonalSetting, type SettingsSnapshot, } from "~/components/settings/utils/accessors"; @@ -16,11 +15,8 @@ import { // Legacy extensionAPI stored query-pages as string | string[] | Record. // Coerce to string[] for backward compatibility with old stored formats. export const getQueryPages = (snapshot?: SettingsSnapshot): string[] => { - const value = snapshot - ? (readPathValue(snapshot.personalSettings, [ - PERSONAL_KEYS.query, - QUERY_KEYS.queryPages, - ]) as string[] | string | Record | undefined) + const value: string[] | string | Record | undefined = snapshot + ? snapshot.personalSettings[PERSONAL_KEYS.query][QUERY_KEYS.queryPages] : getPersonalSetting>([ PERSONAL_KEYS.query, QUERY_KEYS.queryPages, diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index c9be88963..3720f939e 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -336,6 +336,9 @@ export type InitSchemaResult = { nodePageUids: Record; }; +// On-demand dual-read comparison. Not called automatically on init — +// invoke from the console via window.dgDualReadLog() to inspect the legacy +// settings tree vs. the block-prop store. const logDualReadComparison = (): void => { if (!isNewSettingsStoreEnabled()) return; @@ -415,5 +418,7 @@ export const initSchema = async (): Promise => { await migrateGraphLevel(blockUids); const nodePageUids = await initDiscourseNodePages(); await migratePersonalSettings(blockUids); + (window as unknown as Record).dgDualReadLog = + logDualReadComparison; return { blockUids, nodePageUids }; }; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 4beaec802..6d17d92d3 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -34,10 +34,7 @@ import { import { initPluginTimer } from "./utils/pluginTimer"; import { initPostHog } from "./utils/posthog"; import { initSchema } from "./components/settings/utils/init"; -import { - bulkReadSettings, - readPathValue, -} from "./components/settings/utils/accessors"; +import { bulkReadSettings } from "./components/settings/utils/accessors"; import { PERSONAL_KEYS } from "./components/settings/utils/settingKeys"; import { setupPullWatchOnSettingsPage } from "./components/settings/utils/pullWatchers"; import { @@ -49,19 +46,18 @@ import { mountLeftSidebar } from "./components/LeftSidebarView"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; export default runExtension(async (onloadArgs) => { - initPluginTimer(); - const settingsSnapshot = bulkReadSettings(); const isEncrypted = window.roamAlphaAPI.graph.isEncrypted; const isOffline = window.roamAlphaAPI.graph.type === "offline"; - const disallowDiagnostics = readPathValue(settingsSnapshot.personalSettings, [ - PERSONAL_KEYS.disableProductDiagnostics, - ]) as boolean | undefined; + const disallowDiagnostics = + settingsSnapshot.personalSettings[PERSONAL_KEYS.disableProductDiagnostics]; if (!isEncrypted && !isOffline && !disallowDiagnostics) { initPostHog(); } + initFeedbackWidget(); + if (window?.roamjs?.loaded?.has("query-builder")) { renderToast({ timeout: 10000, @@ -80,6 +76,9 @@ export default runExtension(async (onloadArgs) => { timeout: 500, }); } + + initPluginTimer(); + await initializeDiscourseNodes(settingsSnapshot); refreshConfigTree(settingsSnapshot); @@ -97,10 +96,8 @@ export default runExtension(async (onloadArgs) => { const discourseFloatingMenuStyle = addStyle(discourseFloatingMenuStyles); // Add streamline styling only if enabled - const isStreamlineStylingEnabled = readPathValue( - settingsSnapshot.personalSettings, - [PERSONAL_KEYS.streamlineStyling], - ) as boolean | undefined; + const isStreamlineStylingEnabled = + settingsSnapshot.personalSettings[PERSONAL_KEYS.streamlineStyling]; let streamlineStyleElement: HTMLStyleElement | null = null; if (isStreamlineStylingEnabled) { streamlineStyleElement = addStyle(streamlineStyling); diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index b601f79d9..8f4fc5620 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -54,7 +54,6 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import findDiscourseNode from "./findDiscourseNode"; import { - readPathValue, bulkReadSettings, type SettingsSnapshot, } from "~/components/settings/utils/accessors"; @@ -231,11 +230,7 @@ export const initObservers = ({ const suggestiveHandler = getSuggestiveOverlayHandler(onloadArgs); const toggleSuggestiveOverlay = onPageRefObserverChange(suggestiveHandler); - if ( - readPathValue(settingsSnapshot.personalSettings, [ - PERSONAL_KEYS.suggestiveModeOverlay, - ]) - ) { + if (settingsSnapshot.personalSettings[PERSONAL_KEYS.suggestiveModeOverlay]) { addPageRefObserver(suggestiveHandler); } @@ -265,17 +260,11 @@ export const initObservers = ({ }, }); - if ( - readPathValue(settingsSnapshot.personalSettings, [ - PERSONAL_KEYS.pagePreview, - ]) - ) + if (settingsSnapshot.personalSettings[PERSONAL_KEYS.pagePreview]) addPageRefObserver(previewPageRefHandler); if ( - readPathValue(settingsSnapshot.personalSettings, [ - PERSONAL_KEYS.discourseContextOverlay, - ]) + settingsSnapshot.personalSettings[PERSONAL_KEYS.discourseContextOverlay] ) { const overlayHandler = getOverlayHandler(onloadArgs); onPageRefObserverChange(overlayHandler)(true); @@ -300,15 +289,14 @@ export const initObservers = ({ } }; - let globalTrigger = ( - (readPathValue(settingsSnapshot.globalSettings, [GLOBAL_KEYS.trigger]) as - | string - | undefined) ?? "\\" - ).trim(); - const personalTriggerCombo = readPathValue( - settingsSnapshot.personalSettings, - [PERSONAL_KEYS.personalNodeMenuTrigger], - ) as IKeyCombo | undefined; + let globalTrigger = + settingsSnapshot.globalSettings[GLOBAL_KEYS.trigger].trim(); + const personalTriggerComboRaw = + settingsSnapshot.personalSettings[PERSONAL_KEYS.personalNodeMenuTrigger]; + const personalTriggerCombo = + typeof personalTriggerComboRaw === "object" + ? (personalTriggerComboRaw as IKeyCombo) + : undefined; let personalTrigger = personalTriggerCombo?.key; let personalModifiers = personalTriggerCombo ? getModifiersFromCombo(personalTriggerCombo) @@ -393,9 +381,7 @@ export const initObservers = ({ }; let customTrigger = - (readPathValue(settingsSnapshot.personalSettings, [ - PERSONAL_KEYS.nodeSearchMenuTrigger, - ]) as string | undefined) ?? "@"; + settingsSnapshot.personalSettings[PERSONAL_KEYS.nodeSearchMenuTrigger]; const unsubSearchTrigger = onSettingChange( settingKeys.nodeSearchMenuTrigger, diff --git a/apps/roam/src/utils/isCanvasPage.ts b/apps/roam/src/utils/isCanvasPage.ts index 52e898a6c..daea2ebc8 100644 --- a/apps/roam/src/utils/isCanvasPage.ts +++ b/apps/roam/src/utils/isCanvasPage.ts @@ -1,7 +1,6 @@ import { DEFAULT_CANVAS_PAGE_FORMAT } from ".."; import { getGlobalSetting, - readPathValue, type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { GLOBAL_KEYS } from "~/components/settings/utils/settingKeys"; @@ -15,9 +14,7 @@ export const isCanvasPage = ({ }) => { const format = (snapshot - ? (readPathValue(snapshot.globalSettings, [ - GLOBAL_KEYS.canvasPageFormat, - ]) as string | undefined) + ? snapshot.globalSettings[GLOBAL_KEYS.canvasPageFormat] : getGlobalSetting([GLOBAL_KEYS.canvasPageFormat])) || DEFAULT_CANVAS_PAGE_FORMAT; const canvasRegex = new RegExp(`^${format}$`.replace(/\*/g, ".+")); diff --git a/apps/roam/src/utils/setQueryPages.ts b/apps/roam/src/utils/setQueryPages.ts index bc39d5176..a29b9bdc8 100644 --- a/apps/roam/src/utils/setQueryPages.ts +++ b/apps/roam/src/utils/setQueryPages.ts @@ -1,6 +1,6 @@ import { OnloadArgs } from "roamjs-components/types"; import { - readPathValue, + getPersonalSetting, setPersonalSetting, type SettingsSnapshot, } from "~/components/settings/utils/accessors"; @@ -11,14 +11,16 @@ import { export const setInitialQueryPages = ( onloadArgs: OnloadArgs, - settingsSnapshot: SettingsSnapshot, + snapshot?: SettingsSnapshot, ) => { // Legacy extensionAPI stored query-pages as string | string[] | Record. // Coerce to string[] for backward compatibility with old stored formats. - const raw = readPathValue(settingsSnapshot.personalSettings, [ - PERSONAL_KEYS.query, - QUERY_KEYS.queryPages, - ]) as string[] | string | Record | undefined; + const raw: string[] | string | Record | undefined = snapshot + ? snapshot.personalSettings[PERSONAL_KEYS.query][QUERY_KEYS.queryPages] + : getPersonalSetting>([ + PERSONAL_KEYS.query, + QUERY_KEYS.queryPages, + ]); const queryPageArray = Array.isArray(raw) ? raw : typeof raw === "string" && raw From 4140f5c1fc53cd0254051357c328cd36fd502a0e Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 6 Apr 2026 20:31:54 +0530 Subject: [PATCH 04/17] ENG-1616: Log total plugin load time 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. --- apps/roam/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 6d17d92d3..26946798d 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -46,6 +46,8 @@ import { mountLeftSidebar } from "./components/LeftSidebarView"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; export default runExtension(async (onloadArgs) => { + const pluginLoadStart = performance.now(); + const settingsSnapshot = bulkReadSettings(); const isEncrypted = window.roamAlphaAPI.graph.isEncrypted; @@ -194,6 +196,10 @@ export default runExtension(async (onloadArgs) => { const { blockUids } = await initSchema(); const cleanupPullWatchers = setupPullWatchOnSettingsPage(blockUids); + console.log( + `[DG Plugin] Total load: ${Math.round(performance.now() - pluginLoadStart)}ms`, + ); + return { elements: [ style, From aea086d2620123a136e637349adb6de6c888d02d Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 8 Apr 2026 16:02:29 +0530 Subject: [PATCH 05/17] ENG-1616: Tighten init-only leaves to required snapshot, AGENTS.md compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- .../src/components/DiscourseFloatingMenu.tsx | 25 ++++++------------- apps/roam/src/components/LeftSidebarView.tsx | 20 ++++++++++----- .../components/settings/utils/accessors.ts | 2 +- apps/roam/src/index.ts | 2 +- .../src/utils/initializeDiscourseNodes.ts | 2 +- .../utils/initializeObserversAndListeners.ts | 13 +++++----- apps/roam/src/utils/isCanvasPage.ts | 4 +-- apps/roam/src/utils/isQueryPage.ts | 2 +- apps/roam/src/utils/posthog.ts | 8 ++---- apps/roam/src/utils/setQueryPages.ts | 18 +++---------- 10 files changed, 40 insertions(+), 56 deletions(-) diff --git a/apps/roam/src/components/DiscourseFloatingMenu.tsx b/apps/roam/src/components/DiscourseFloatingMenu.tsx index 0f6c8e52d..a96c33f1b 100644 --- a/apps/roam/src/components/DiscourseFloatingMenu.tsx +++ b/apps/roam/src/components/DiscourseFloatingMenu.tsx @@ -13,10 +13,7 @@ import { import { FeedbackWidget } from "./BirdEatsBugs"; import { render as renderSettings } from "~/components/settings/Settings"; import posthog from "posthog-js"; -import { - getPersonalSetting, - type SettingsSnapshot, -} from "./settings/utils/accessors"; +import { type SettingsSnapshot } from "./settings/utils/accessors"; import { PERSONAL_KEYS } from "./settings/utils/settingKeys"; type DiscourseFloatingMenuProps = { @@ -121,12 +118,7 @@ export const showDiscourseFloatingMenu = () => { export const installDiscourseFloatingMenu = ( onLoadArgs: OnloadArgs, - snapshot?: SettingsSnapshot, - props: DiscourseFloatingMenuProps = { - position: "bottom-right", - theme: "bp3-light", - buttonTheme: "bp3-light", - }, + snapshot: SettingsSnapshot, ) => { let floatingMenuAnchor = document.getElementById(ANCHOR_ID); if (!floatingMenuAnchor) { @@ -134,17 +126,15 @@ export const installDiscourseFloatingMenu = ( floatingMenuAnchor.id = ANCHOR_ID; document.getElementById("app")?.appendChild(floatingMenuAnchor); } - const hideFeedbackButton = snapshot - ? snapshot.personalSettings[PERSONAL_KEYS.hideFeedbackButton] - : getPersonalSetting([PERSONAL_KEYS.hideFeedbackButton]); - if (hideFeedbackButton) { + if (snapshot.personalSettings[PERSONAL_KEYS.hideFeedbackButton]) { floatingMenuAnchor.classList.add("hidden"); } + // eslint-disable-next-line react/no-deprecated ReactDOM.render( , floatingMenuAnchor, @@ -155,6 +145,7 @@ export const removeDiscourseFloatingMenu = () => { const anchor = document.getElementById(ANCHOR_ID); if (anchor) { try { + // eslint-disable-next-line react/no-deprecated ReactDOM.unmountComponentAtNode(anchor); } catch (e) { // no-op: unmount best-effort diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index fc6946ba7..b5793707c 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -619,11 +619,15 @@ const migrateFavorites = async () => { refreshConfigTree(); }; -export const mountLeftSidebar = async ( - wrapper: HTMLElement, - onloadArgs: OnloadArgs, - initialSnapshot?: SettingsSnapshot, -): Promise => { +export const mountLeftSidebar = async ({ + wrapper, + onloadArgs, + initialSnapshot, +}: { + wrapper: HTMLElement; + onloadArgs: OnloadArgs; + initialSnapshot?: SettingsSnapshot; +}): Promise => { if (!wrapper) return; const id = "dg-left-sidebar-root"; @@ -640,8 +644,12 @@ export const mountLeftSidebar = async ( } else { root.className = "starred-pages"; } + // eslint-disable-next-line react/no-deprecated ReactDOM.render( - , + , root, ); }; diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 25ccf1f9d..6be04569b 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -143,7 +143,7 @@ const getSchemaAtPath = ( const formatSettingPath = (keys: string[]): string => keys.length === 0 ? "(root)" : keys.join(" > "); -export const readPathValue = (root: unknown, keys: string[]): unknown => +const readPathValue = (root: unknown, keys: string[]): unknown => keys.reduce((current, key) => { if (Array.isArray(current)) { const index = Number(key); diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 26946798d..daec39cc4 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -180,7 +180,7 @@ export default runExtension(async (onloadArgs) => { if (!wrapper) return; if (enabled) { wrapper.style.padding = "0"; - void mountLeftSidebar(wrapper, onloadArgs); + void mountLeftSidebar({ wrapper, onloadArgs }); } else { const root = wrapper.querySelector("#dg-left-sidebar-root"); if (root) { diff --git a/apps/roam/src/utils/initializeDiscourseNodes.ts b/apps/roam/src/utils/initializeDiscourseNodes.ts index 6ab88ddf1..847e8fdf7 100644 --- a/apps/roam/src/utils/initializeDiscourseNodes.ts +++ b/apps/roam/src/utils/initializeDiscourseNodes.ts @@ -4,7 +4,7 @@ import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; import getDiscourseNodes, { excludeDefaultNodes } from "./getDiscourseNodes"; import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; -const initializeDiscourseNodes = async (snapshot?: SettingsSnapshot) => { +const initializeDiscourseNodes = async (snapshot: SettingsSnapshot) => { const nodes = getDiscourseNodes(undefined, snapshot).filter( excludeDefaultNodes, ); diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 8f4fc5620..ace07fb31 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -295,7 +295,7 @@ export const initObservers = ({ settingsSnapshot.personalSettings[PERSONAL_KEYS.personalNodeMenuTrigger]; const personalTriggerCombo = typeof personalTriggerComboRaw === "object" - ? (personalTriggerComboRaw as IKeyCombo) + ? personalTriggerComboRaw : undefined; let personalTrigger = personalTriggerCombo?.key; let personalModifiers = personalTriggerCombo @@ -333,7 +333,11 @@ export const initObservers = ({ const container = el as HTMLDivElement; if (isLeftSidebarEnabled) { container.style.padding = "0"; - await mountLeftSidebar(container, onloadArgs, callbackSnapshot); + await mountLeftSidebar({ + wrapper: container, + onloadArgs, + initialSnapshot: callbackSnapshot, + }); } })(); }, @@ -442,10 +446,7 @@ export const initObservers = ({ const nodeCreationPopoverListener = debounce(() => { const snap = bulkReadSettings(); - const isTextSelectionPopupEnabled = - snap.personalSettings[PERSONAL_KEYS.textSelectionPopup] !== false; - - if (!isTextSelectionPopupEnabled) return; + if (!snap.personalSettings[PERSONAL_KEYS.textSelectionPopup]) return; const selection = window.getSelection(); diff --git a/apps/roam/src/utils/isCanvasPage.ts b/apps/roam/src/utils/isCanvasPage.ts index daea2ebc8..4c7d97331 100644 --- a/apps/roam/src/utils/isCanvasPage.ts +++ b/apps/roam/src/utils/isCanvasPage.ts @@ -28,7 +28,7 @@ export const isCurrentPageCanvas = ({ }: { title: string; h1: HTMLHeadingElement; - snapshot?: SettingsSnapshot; + snapshot: SettingsSnapshot; }) => { return isCanvasPage({ title, snapshot }) && !!h1.closest(".roam-article"); }; @@ -40,7 +40,7 @@ export const isSidebarCanvas = ({ }: { title: string; h1: HTMLHeadingElement; - snapshot?: SettingsSnapshot; + snapshot: SettingsSnapshot; }) => { return ( isCanvasPage({ title, snapshot }) && !!h1.closest(".rm-sidebar-outline") diff --git a/apps/roam/src/utils/isQueryPage.ts b/apps/roam/src/utils/isQueryPage.ts index af3a831bb..4e6109944 100644 --- a/apps/roam/src/utils/isQueryPage.ts +++ b/apps/roam/src/utils/isQueryPage.ts @@ -6,7 +6,7 @@ export const isQueryPage = ({ snapshot, }: { title: string; - snapshot?: SettingsSnapshot; + snapshot: SettingsSnapshot; }): boolean => { const queryPages = getQueryPages(snapshot); diff --git a/apps/roam/src/utils/posthog.ts b/apps/roam/src/utils/posthog.ts index 1149e319a..9b19e63cd 100644 --- a/apps/roam/src/utils/posthog.ts +++ b/apps/roam/src/utils/posthog.ts @@ -5,7 +5,7 @@ import type { CaptureResult } from "posthog-js"; let initialized = false; -const doInitPostHog = (): void => { +export const initPostHog = (): void => { if (initialized) return; const propertyDenylist = new Set([ "$ip", @@ -56,14 +56,10 @@ const doInitPostHog = (): void => { }; export const enablePostHog = (): void => { - doInitPostHog(); + initPostHog(); posthog.opt_in_capturing(); }; export const disablePostHog = (): void => { if (initialized) posthog.opt_out_capturing(); }; - -export const initPostHog = (): void => { - doInitPostHog(); -}; diff --git a/apps/roam/src/utils/setQueryPages.ts b/apps/roam/src/utils/setQueryPages.ts index a29b9bdc8..f75fb521b 100644 --- a/apps/roam/src/utils/setQueryPages.ts +++ b/apps/roam/src/utils/setQueryPages.ts @@ -1,6 +1,5 @@ import { OnloadArgs } from "roamjs-components/types"; import { - getPersonalSetting, setPersonalSetting, type SettingsSnapshot, } from "~/components/settings/utils/accessors"; @@ -11,21 +10,10 @@ import { export const setInitialQueryPages = ( onloadArgs: OnloadArgs, - snapshot?: SettingsSnapshot, + snapshot: SettingsSnapshot, ) => { - // Legacy extensionAPI stored query-pages as string | string[] | Record. - // Coerce to string[] for backward compatibility with old stored formats. - const raw: string[] | string | Record | undefined = snapshot - ? snapshot.personalSettings[PERSONAL_KEYS.query][QUERY_KEYS.queryPages] - : getPersonalSetting>([ - PERSONAL_KEYS.query, - QUERY_KEYS.queryPages, - ]); - const queryPageArray = Array.isArray(raw) - ? raw - : typeof raw === "string" && raw - ? [raw] - : []; + const queryPageArray = + snapshot.personalSettings[PERSONAL_KEYS.query][QUERY_KEYS.queryPages]; if (!queryPageArray.includes("discourse-graph/queries/*")) { const updated = [...queryPageArray, "discourse-graph/queries/*"]; void onloadArgs.extensionAPI.settings.set("query-pages", updated); From 076910fa912c02a280a898fc77d6f0410e2d0a56 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 8 Apr 2026 16:06:37 +0530 Subject: [PATCH 06/17] ENG-1617: Single-pull settings reads + dialog snapshot threading `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` 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. --- .../roam/src/components/DiscourseNodeMenu.tsx | 18 +++----- .../components/DiscourseNodeSearchMenu.tsx | 9 ++-- .../settings/HomePersonalSettings.tsx | 38 +++++++++++++--- .../settings/KeyboardShortcutInput.tsx | 15 ++----- .../roam/src/components/settings/Settings.tsx | 22 +++++++++- .../components/BlockPropSettingPanels.tsx | 5 ++- .../components/settings/utils/accessors.ts | 43 +++++++++++++------ apps/roam/src/utils/getDiscourseNodes.ts | 2 +- apps/roam/src/utils/getDiscourseRelations.ts | 4 +- 9 files changed, 105 insertions(+), 51 deletions(-) diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index c7f2e2b96..e25ca7e61 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -27,11 +27,9 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils"; import { OnloadArgs } from "roamjs-components/types"; import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; -import { - getPersonalSetting, - setPersonalSetting, -} from "~/components/settings/utils/accessors"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; +import type { PersonalSettings } from "~/components/settings/utils/zodSchema"; type Props = { textarea?: HTMLTextAreaElement; @@ -420,19 +418,15 @@ export const comboToString = (combo: IKeyCombo): string => { export const NodeMenuTriggerComponent = ({ extensionAPI, + initialValue, }: { extensionAPI: OnloadArgs["extensionAPI"]; + initialValue: PersonalSettings["Personal node menu trigger"]; }) => { const inputRef = useRef(null); const [isActive, setIsActive] = useState(false); - const [comboKey, setComboKey] = useState( - () => - getPersonalSetting([ - PERSONAL_KEYS.personalNodeMenuTrigger, - ]) || { - modifiers: 0, - key: "", - }, + const [comboKey, setComboKey] = useState(() => + typeof initialValue === "object" ? initialValue : { modifiers: 0, key: "" }, ); const handleKeyDown = useCallback( diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 03fdfc82d..8058d869e 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -25,10 +25,7 @@ import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import { Result } from "~/utils/types"; import MiniSearch from "minisearch"; -import { - getPersonalSetting, - setPersonalSetting, -} from "~/components/settings/utils/accessors"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; type Props = { @@ -709,12 +706,14 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => { export const NodeSearchMenuTriggerSetting = ({ onloadArgs, + initialValue, }: { onloadArgs: OnloadArgs; + initialValue: string; }) => { const extensionAPI = onloadArgs.extensionAPI; const [nodeSearchTrigger, setNodeSearchTrigger] = useState( - getPersonalSetting([PERSONAL_KEYS.nodeSearchMenuTrigger]) ?? "@", + () => initialValue, ); const handleNodeSearchTriggerChange = ( diff --git a/apps/roam/src/components/settings/HomePersonalSettings.tsx b/apps/roam/src/components/settings/HomePersonalSettings.tsx index 075ce7d3c..cdfc4b52c 100644 --- a/apps/roam/src/components/settings/HomePersonalSettings.tsx +++ b/apps/roam/src/components/settings/HomePersonalSettings.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react"; +import React, { useState } from "react"; import { OnloadArgs } from "roamjs-components/types"; import { render as renderToast } from "roamjs-components/components/Toast"; import { Label, Dialog, Button, Intent, Classes } from "@blueprintjs/core"; @@ -28,7 +28,6 @@ import { setSetting } from "~/utils/extensionSettings"; import { enablePostHog, disablePostHog } from "~/utils/posthog"; import KeyboardShortcutInput from "./KeyboardShortcutInput"; import streamlineStyling from "~/styles/streamlineStyling"; -import { getFeatureFlag } from "~/components/settings/utils/accessors"; import { PersonalFlagPanel } from "./components/BlockPropSettingPanels"; import { PERSONAL_KEYS } from "./utils/settingKeys"; import migrateRelations from "~/utils/migrateRelations"; @@ -36,6 +35,7 @@ import { countReifiedRelations } from "~/utils/createReifiedBlock"; import posthog from "posthog-js"; import internalError from "~/utils/internalError"; import { setPersonalSetting } from "./utils/accessors"; +import type { FeatureFlags, PersonalSettings } from "./utils/zodSchema"; import { getStoredRelationsEnabled } from "~/utils/storedRelations"; const enum RelationMigrationDialog { @@ -45,7 +45,15 @@ const enum RelationMigrationDialog { "reactivate", } -const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { +const HomePersonalSettings = ({ + onloadArgs, + featureFlags, + personalSettings, +}: { + onloadArgs: OnloadArgs; + featureFlags: FeatureFlags; + personalSettings: PersonalSettings; +}) => { const extensionAPI = onloadArgs.extensionAPI; const overlayHandler = getOverlayHandler(onloadArgs); const [activeRelationMigration, setActiveRelationMigration] = @@ -116,12 +124,18 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { { label="Discourse tool keyboard shortcut" description="Set a single key to activate the discourse tool in tldraw. Only single keys (no modifiers) are supported. Leave empty for no shortcut." placeholder="Click to set single key" + initialValue={personalSettings[PERSONAL_KEYS.discourseToolShortcut]} /> { void setSetting("discourse-context-overlay", checked); onPageRefObserverChange(overlayHandler)(checked); @@ -143,11 +159,12 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { }); }} /> - {getFeatureFlag("Suggestive mode enabled") && ( + {featureFlags["Suggestive mode enabled"] && ( { void setSetting("suggestive-mode-overlay", checked); onPageRefObserverChange(getSuggestiveOverlayHandler(onloadArgs))( @@ -161,6 +178,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Enable stored relations" description="Use stored relations instead of legacy pattern-based relations" settingKeys={["Reified relation triples"]} + initialValue={personalSettings["Reified relation triples"]} value={storedRelations} onBeforeChange={async (checked) => { if (checked) { @@ -182,6 +200,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Text selection popup" description="Whether or not to show the discourse node menu when selecting text." settingKeys={[PERSONAL_KEYS.textSelectionPopup]} + initialValue={personalSettings[PERSONAL_KEYS.textSelectionPopup]} onChange={(checked) => { void setSetting("text-selection-popup", checked); }} @@ -190,6 +209,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Disable sidebar open" description="Disable opening new nodes in the sidebar when created" settingKeys={[PERSONAL_KEYS.disableSidebarOpen]} + initialValue={personalSettings[PERSONAL_KEYS.disableSidebarOpen]} onChange={(checked) => { void setSetting("disable-sidebar-open", checked); }} @@ -198,6 +218,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Page preview" description="Whether or not to display page previews when hovering over page refs" settingKeys={[PERSONAL_KEYS.pagePreview]} + initialValue={personalSettings[PERSONAL_KEYS.pagePreview]} onChange={(checked) => { void setSetting("page-preview", checked); onPageRefObserverChange(previewPageRefHandler)(checked); @@ -207,6 +228,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Hide feedback button" description="Hide the 'Send feedback' button at the bottom right of the screen." settingKeys={[PERSONAL_KEYS.hideFeedbackButton]} + initialValue={personalSettings[PERSONAL_KEYS.hideFeedbackButton]} onChange={(checked) => { void setSetting("hide-feedback-button", checked); if (checked) { @@ -220,6 +242,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Auto canvas relations" description="Automatically add discourse relations to canvas when a node is added" settingKeys={[PERSONAL_KEYS.autoCanvasRelations]} + initialValue={personalSettings[PERSONAL_KEYS.autoCanvasRelations]} onChange={(checked) => { void setSetting(AUTO_CANVAS_RELATIONS_KEY, checked); }} @@ -229,6 +252,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="(BETA) Overlay in canvas" description="Whether or not to overlay discourse context information over canvas nodes." settingKeys={[PERSONAL_KEYS.overlayInCanvas]} + initialValue={personalSettings[PERSONAL_KEYS.overlayInCanvas]} onChange={(checked) => { void setSetting(DISCOURSE_CONTEXT_OVERLAY_IN_CANVAS_KEY, checked); }} @@ -237,6 +261,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Streamline styling" description="Apply streamlined styling to your personal graph for a cleaner appearance." settingKeys={[PERSONAL_KEYS.streamlineStyling]} + initialValue={personalSettings[PERSONAL_KEYS.streamlineStyling]} onChange={(checked) => { void setSetting(STREAMLINE_STYLING_KEY, checked); const existingStyleElement = @@ -254,6 +279,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { title="Disable product diagnostics" description="Disable sending usage signals and error reports that help us improve the product." settingKeys={[PERSONAL_KEYS.disableProductDiagnostics]} + initialValue={personalSettings[PERSONAL_KEYS.disableProductDiagnostics]} onChange={(checked) => { void setSetting(DISALLOW_DIAGNOSTICS, checked); if (checked) { diff --git a/apps/roam/src/components/settings/KeyboardShortcutInput.tsx b/apps/roam/src/components/settings/KeyboardShortcutInput.tsx index 32caf2947..c7e9d5041 100644 --- a/apps/roam/src/components/settings/KeyboardShortcutInput.tsx +++ b/apps/roam/src/components/settings/KeyboardShortcutInput.tsx @@ -9,10 +9,7 @@ import { } from "@blueprintjs/core"; import Description from "roamjs-components/components/Description"; import { DISCOURSE_TOOL_SHORTCUT_KEY } from "~/data/userSettings"; -import { - getPersonalSetting, - setPersonalSetting, -} from "~/components/settings/utils/accessors"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; import { comboToString } from "~/components/DiscourseNodeMenu"; import type { PersonalSettings } from "~/components/settings/utils/zodSchema"; @@ -22,6 +19,7 @@ type KeyboardShortcutInputProps = { blockPropKey: keyof PersonalSettings; label: string; description: string; + initialValue: IKeyCombo; placeholder?: string; }; @@ -31,18 +29,13 @@ const KeyboardShortcutInput = ({ blockPropKey, label, description, + initialValue, placeholder = "Click to set shortcut", }: KeyboardShortcutInputProps) => { const extensionAPI = onloadArgs.extensionAPI; const inputRef = useRef(null); const [isActive, setIsActive] = useState(false); - const [comboKey, setComboKey] = useState( - () => - getPersonalSetting([blockPropKey]) || { - modifiers: 0, - key: "", - }, - ); + const [comboKey, setComboKey] = useState(() => initialValue); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 80c35bdcd..25869a93a 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -28,6 +28,7 @@ import { FeedbackWidget } from "~/components/BirdEatsBugs"; import { getVersionWithDate } from "~/utils/getVersion"; import { LeftSidebarPersonalSections } from "./LeftSidebarPersonalSettings"; import { LeftSidebarGlobalSections } from "./LeftSidebarGlobalSettings"; +import { bulkReadSettings } from "./utils/accessors"; import posthog from "posthog-js"; type SectionHeaderProps = { @@ -72,6 +73,7 @@ export const SettingsDialog = ({ onClose?: () => void; selectedTabId?: TabId; }) => { + const [mountStart] = useState(() => performance.now()); const extensionAPI = onloadArgs.extensionAPI; const grammarNode = discourseConfigRef.tree.find( (node) => node.text === "grammar", @@ -80,7 +82,10 @@ export const SettingsDialog = ({ (node) => node.text === "relations", ); const nodesNode = grammarNode?.children.find((node) => node.text === "nodes"); - const nodes = getDiscourseNodes().filter(excludeDefaultNodes); + const [settingsSnapshot] = useState(() => bulkReadSettings()); + const [nodes] = useState(() => + getDiscourseNodes(undefined, settingsSnapshot).filter(excludeDefaultNodes), + ); const [activeTabId, setActiveTabId] = useState( selectedTabId ?? "discourse-graph-home-personal", ); @@ -88,6 +93,13 @@ export const SettingsDialog = ({ window.roamAlphaAPI.graph.name === "discourse-graphs" || false, ); + // ENG-1617: surface settings dialog open time in devtools for perf monitoring. + useEffect(() => { + console.log( + `[settings] open ${(performance.now() - mountStart).toFixed(0)}ms`, + ); + }, [mountStart]); + useEffect(() => { posthog.capture("Settings: Dialog Opened", { initialTabId: String(selectedTabId ?? "discourse-graph-home-personal"), @@ -166,7 +178,13 @@ export const SettingsDialog = ({ id="discourse-graph-home-personal" title="Home" className="overflow-y-auto" - panel={} + panel={ + + } /> ( /> ); +// Prop wins so HomePersonalSettings can hand down a value from the dialog-open +// snapshot. QuerySettings still calls without `initialValue`, so the fallback +// handles that path. export const PersonalFlagPanel = (props: FlagWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getPersonalSetting(props.settingKeys) } {...personalAccessors.flag} /> diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 25ccf1f9d..21e6fa9dc 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -4,7 +4,6 @@ import getBlockProps, { } from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; -import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import { getSubTree } from "roamjs-components/util"; import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree"; @@ -650,6 +649,9 @@ const setBlockPropAtPath = ( setBlockProps(blockUid, updatedProps, false); }; +// Single pull on the settings page returns all top-level children +// (Feature Flags / Global / ) with their string + uid + props. +// Avoids the slow `q`-based getBlockUidByTextOnPage (~290ms per call). const getBlockPropBasedSettings = ({ keys, }: { @@ -663,16 +665,36 @@ const getBlockPropBasedSettings = ({ return { blockProps: undefined, blockUid: "" }; } - const blockUid = getBlockUidByTextOnPage({ - text: keys[0], - title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, - }); + const pageResult = window.roamAlphaAPI.pull( + "[{:block/children [:block/string :block/uid :block/props]}]", + [":node/title", DG_BLOCK_PROP_SETTINGS_PAGE_TITLE], + ) as Record | null; - if (!blockUid) { + const children = (pageResult?.[":block/children"] ?? []) as Record< + string, + json + >[]; + const child = children.find((c) => c[":block/string"] === keys[0]); + if (!child) { return { blockProps: undefined, blockUid: "" }; } - const blockProps = getBlockPropsByUid(blockUid, keys.slice(1)); + const blockUid = (child[":block/uid"] as string) || ""; + const rawProps = child[":block/props"]; + const allBlockProps: json = + rawProps && typeof rawProps === "object" ? normalizeProps(rawProps) : {}; + + if (keys.length === 1) { + return { blockProps: allBlockProps, blockUid }; + } + + const blockProps = keys.slice(1).reduce((current, key) => { + if (current && typeof current === "object" && !Array.isArray(current)) { + const value = current[key]; + return value === undefined ? undefined : value; + } + return undefined; + }, allBlockProps); return { blockProps, blockUid }; }; @@ -692,10 +714,7 @@ const setBlockPropBasedSettings = ({ return; } - const blockUid = getBlockUidByTextOnPage({ - text: keys[0], - title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, - }); + const { blockUid } = getBlockPropBasedSettings({ keys: [keys[0]] }); if (!blockUid) { internalError({ @@ -864,7 +883,7 @@ export const setGlobalSetting = (keys: string[], value: json): void => { }; export const getAllRelations = ( - snapshot?: SettingsSnapshot, + snapshot?: Pick, ): DiscourseRelation[] => { const settings = snapshot ? snapshot.globalSettings : getGlobalSettings(); diff --git a/apps/roam/src/utils/getDiscourseNodes.ts b/apps/roam/src/utils/getDiscourseNodes.ts index 765d12422..f34d3b4d9 100644 --- a/apps/roam/src/utils/getDiscourseNodes.ts +++ b/apps/roam/src/utils/getDiscourseNodes.ts @@ -109,7 +109,7 @@ const getUidAndBooleanSetting = ({ const getDiscourseNodes = ( relations?: ReturnType, - snapshot?: SettingsSnapshot, + snapshot?: Pick, ) => { const resolvedRelations = relations ?? getDiscourseRelations(snapshot); const newStoreEnabled = snapshot diff --git a/apps/roam/src/utils/getDiscourseRelations.ts b/apps/roam/src/utils/getDiscourseRelations.ts index d7e36cab7..9eabd1012 100644 --- a/apps/roam/src/utils/getDiscourseRelations.ts +++ b/apps/roam/src/utils/getDiscourseRelations.ts @@ -36,7 +36,9 @@ export const getRelationsNode = (grammarNode = getGrammarNode()) => { return grammarNode?.children.find(matchNodeText("relations")); }; -const getDiscourseRelations = (snapshot?: SettingsSnapshot) => { +const getDiscourseRelations = ( + snapshot?: Pick, +) => { const newStoreEnabled = snapshot ? snapshot.featureFlags["Use new settings store"] : isNewSettingsStoreEnabled(); From 533e59d11dbee0cb57e1cd575825019724982c56 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 8 Apr 2026 16:40:13 +0530 Subject: [PATCH 07/17] ENG-1617: Refresh snapshot on Home tab nav + reuse readPathValue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/roam/src/components/settings/Settings.tsx | 11 ++++++++++- apps/roam/src/components/settings/utils/accessors.ts | 10 +++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 25869a93a..4a1b51f0f 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -82,7 +82,9 @@ export const SettingsDialog = ({ (node) => node.text === "relations", ); const nodesNode = grammarNode?.children.find((node) => node.text === "nodes"); - const [settingsSnapshot] = useState(() => bulkReadSettings()); + const [settingsSnapshot, setSettingsSnapshot] = useState(() => + bulkReadSettings(), + ); const [nodes] = useState(() => getDiscourseNodes(undefined, settingsSnapshot).filter(excludeDefaultNodes), ); @@ -162,6 +164,13 @@ export const SettingsDialog = ({ { + // Tab panels mount lazily (`renderActiveTabPanelOnly`), so each + // re-mount re-seeds child `useState` from `initialValue`. Refresh + // the snapshot when navigating back to Home so user toggles made + // earlier in the same dialog session don't visually revert. + if (id === "discourse-graph-home-personal") { + setSettingsSnapshot(bulkReadSettings()); + } setActiveTabId(id); posthog.capture("Settings: Tab Opened", { tabId: String(id), diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 21e6fa9dc..da2865699 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -688,13 +688,9 @@ const getBlockPropBasedSettings = ({ return { blockProps: allBlockProps, blockUid }; } - const blockProps = keys.slice(1).reduce((current, key) => { - if (current && typeof current === "object" && !Array.isArray(current)) { - const value = current[key]; - return value === undefined ? undefined : value; - } - return undefined; - }, allBlockProps); + const blockProps = readPathValue(allBlockProps, keys.slice(1)) as + | json + | undefined; return { blockProps, blockUid }; }; From 09e9a259f2723297e8e713fe7adc4fe3de8751cc Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 8 Apr 2026 21:31:10 +0530 Subject: [PATCH 08/17] ENG-1617: Per-tab snapshot threading via bulkReadSettings 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 type narrowings are all gone. --- .../components/settings/ExportSettings.tsx | 12 ++- .../components/settings/GeneralSettings.tsx | 6 +- .../settings/HomePersonalSettings.tsx | 18 ++--- .../src/components/settings/QuerySettings.tsx | 8 +- .../roam/src/components/settings/Settings.tsx | 22 +----- .../components/BlockPropSettingPanels.tsx | 21 +++--- .../components/settings/utils/accessors.ts | 74 +++---------------- apps/roam/src/utils/getDiscourseNodes.ts | 2 +- apps/roam/src/utils/getDiscourseRelations.ts | 4 +- 9 files changed, 51 insertions(+), 116 deletions(-) diff --git a/apps/roam/src/components/settings/ExportSettings.tsx b/apps/roam/src/components/settings/ExportSettings.tsx index 0b4c06577..fdf5edf96 100644 --- a/apps/roam/src/components/settings/ExportSettings.tsx +++ b/apps/roam/src/components/settings/ExportSettings.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { getExportSettingsAndUids } from "~/utils/getExportSettings"; import { GlobalFlagPanel, @@ -10,8 +10,11 @@ import { GLOBAL_KEYS, EXPORT_KEYS, } from "~/components/settings/utils/settingKeys"; +import { bulkReadSettings } from "./utils/accessors"; const DiscourseGraphExport = () => { + const [snapshot] = useState(() => bulkReadSettings()); + const exportBlockProps = snapshot.globalSettings.Export; const exportSettings = getExportSettingsAndUids(); const parentUid = exportSettings.exportUid; return ( @@ -26,6 +29,7 @@ const DiscourseGraphExport = () => { GLOBAL_KEYS.export, EXPORT_KEYS.removeSpecialCharacters, ]} + initialValue={exportBlockProps[EXPORT_KEYS.removeSpecialCharacters]} order={1} uid={exportSettings.removeSpecialCharacters.uid} parentUid={parentUid} @@ -35,6 +39,7 @@ const DiscourseGraphExport = () => { title="resolve block references" description="Replaces block references in the markdown content with the block's content" settingKeys={[GLOBAL_KEYS.export, EXPORT_KEYS.resolveBlockReferences]} + initialValue={exportBlockProps[EXPORT_KEYS.resolveBlockReferences]} order={3} uid={exportSettings.optsRefs.uid} parentUid={parentUid} @@ -43,6 +48,7 @@ const DiscourseGraphExport = () => { title="resolve block embeds" description="Replaces block embeds in the markdown content with the block's content tree" settingKeys={[GLOBAL_KEYS.export, EXPORT_KEYS.resolveBlockEmbeds]} + initialValue={exportBlockProps[EXPORT_KEYS.resolveBlockEmbeds]} order={4} uid={exportSettings.optsEmbeds.uid} parentUid={parentUid} @@ -52,6 +58,7 @@ const DiscourseGraphExport = () => { title="append referenced node" description="If a referenced node is defined in a node's format, it will be appended to the discourse context" settingKeys={[GLOBAL_KEYS.export, EXPORT_KEYS.appendReferencedNode]} + initialValue={exportBlockProps[EXPORT_KEYS.appendReferencedNode]} order={6} uid={exportSettings.appendRefNodeContext.uid} parentUid={parentUid} @@ -62,6 +69,7 @@ const DiscourseGraphExport = () => { title="link type" description="How to format links that appear in your export." settingKeys={[GLOBAL_KEYS.export, EXPORT_KEYS.linkType]} + initialValue={exportBlockProps[EXPORT_KEYS.linkType]} order={5} options={["alias", "wikilinks", "roam url"]} uid={exportSettings.linkType.uid} @@ -72,6 +80,7 @@ const DiscourseGraphExport = () => { title="max filename length" description="Set the maximum name length for markdown file exports" settingKeys={[GLOBAL_KEYS.export, EXPORT_KEYS.maxFilenameLength]} + initialValue={exportBlockProps[EXPORT_KEYS.maxFilenameLength]} order={0} uid={exportSettings.maxFilenameLength.uid} parentUid={parentUid} @@ -80,6 +89,7 @@ const DiscourseGraphExport = () => { title="frontmatter" description="Specify all the lines that should go to the Frontmatter of the markdown file" settingKeys={[GLOBAL_KEYS.export, EXPORT_KEYS.frontmatter]} + initialValue={exportBlockProps[EXPORT_KEYS.frontmatter]} order={2} uid={exportSettings.frontmatter.uid} parentUid={parentUid} diff --git a/apps/roam/src/components/settings/GeneralSettings.tsx b/apps/roam/src/components/settings/GeneralSettings.tsx index a89d3847a..1f5d6d883 100644 --- a/apps/roam/src/components/settings/GeneralSettings.tsx +++ b/apps/roam/src/components/settings/GeneralSettings.tsx @@ -7,7 +7,7 @@ import { FeatureFlagPanel, } from "./components/BlockPropSettingPanels"; import { GLOBAL_KEYS } from "~/components/settings/utils/settingKeys"; -import { isNewSettingsStoreEnabled } from "./utils/accessors"; +import { bulkReadSettings, isNewSettingsStoreEnabled } from "./utils/accessors"; import posthog from "posthog-js"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import { @@ -17,6 +17,8 @@ import { import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/data/constants"; const DiscourseGraphHome = () => { + const [snapshot] = useState(() => bulkReadSettings()); + const globalSettings = snapshot.globalSettings; const settings = useMemo(() => { refreshConfigTree(); const tree = discourseConfigRef.tree; @@ -43,6 +45,7 @@ const DiscourseGraphHome = () => { title="trigger" description="The trigger to create the node menu." settingKeys={[GLOBAL_KEYS.trigger]} + initialValue={globalSettings[GLOBAL_KEYS.trigger]} order={0} uid={settings.triggerUid} parentUid={settings.settingsUid} @@ -51,6 +54,7 @@ const DiscourseGraphHome = () => { title="Canvas Page Format" description="The page format for canvas pages" settingKeys={[GLOBAL_KEYS.canvasPageFormat]} + initialValue={globalSettings[GLOBAL_KEYS.canvasPageFormat]} order={1} uid={settings.canvasPageFormatUid} parentUid={settings.settingsUid} diff --git a/apps/roam/src/components/settings/HomePersonalSettings.tsx b/apps/roam/src/components/settings/HomePersonalSettings.tsx index cdfc4b52c..9f20b7078 100644 --- a/apps/roam/src/components/settings/HomePersonalSettings.tsx +++ b/apps/roam/src/components/settings/HomePersonalSettings.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { OnloadArgs } from "roamjs-components/types"; import { render as renderToast } from "roamjs-components/components/Toast"; import { Label, Dialog, Button, Intent, Classes } from "@blueprintjs/core"; @@ -34,8 +34,7 @@ import migrateRelations from "~/utils/migrateRelations"; import { countReifiedRelations } from "~/utils/createReifiedBlock"; import posthog from "posthog-js"; import internalError from "~/utils/internalError"; -import { setPersonalSetting } from "./utils/accessors"; -import type { FeatureFlags, PersonalSettings } from "./utils/zodSchema"; +import { bulkReadSettings, setPersonalSetting } from "./utils/accessors"; import { getStoredRelationsEnabled } from "~/utils/storedRelations"; const enum RelationMigrationDialog { @@ -45,15 +44,10 @@ const enum RelationMigrationDialog { "reactivate", } -const HomePersonalSettings = ({ - onloadArgs, - featureFlags, - personalSettings, -}: { - onloadArgs: OnloadArgs; - featureFlags: FeatureFlags; - personalSettings: PersonalSettings; -}) => { +const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { + const [snapshot] = useState(() => bulkReadSettings()); + const personalSettings = snapshot.personalSettings; + const featureFlags = snapshot.featureFlags; const extensionAPI = onloadArgs.extensionAPI; const overlayHandler = getOverlayHandler(onloadArgs); const [activeRelationMigration, setActiveRelationMigration] = diff --git a/apps/roam/src/components/settings/QuerySettings.tsx b/apps/roam/src/components/settings/QuerySettings.tsx index f78abd0a2..377318d08 100644 --- a/apps/roam/src/components/settings/QuerySettings.tsx +++ b/apps/roam/src/components/settings/QuerySettings.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { OnloadArgs } from "roamjs-components/types"; import { Label } from "@blueprintjs/core"; import Description from "roamjs-components/components/Description"; @@ -13,6 +13,7 @@ import { PERSONAL_KEYS, QUERY_KEYS, } from "~/components/settings/utils/settingKeys"; +import { bulkReadSettings } from "./utils/accessors"; import posthog from "posthog-js"; const QuerySettings = ({ @@ -20,12 +21,15 @@ const QuerySettings = ({ }: { extensionAPI: OnloadArgs["extensionAPI"]; }) => { + const [snapshot] = useState(() => bulkReadSettings()); + const querySettings = snapshot.personalSettings.Query; return (
{ void extensionAPI.settings.set(HIDE_METADATA_KEY, checked); posthog.capture("Query Settings: Hide Metadata Toggled", { @@ -37,6 +41,7 @@ const QuerySettings = ({ title="Default page size" description="The default page size used for query results" settingKeys={[PERSONAL_KEYS.query, QUERY_KEYS.defaultPageSize]} + initialValue={querySettings[QUERY_KEYS.defaultPageSize]} onChange={(value) => { void extensionAPI.settings.set(DEFAULT_PAGE_SIZE_KEY, value); posthog.capture("Query Settings: Default Page Size Changed", { @@ -48,6 +53,7 @@ const QuerySettings = ({ title="Query pages" description="The title formats of pages that you would like to serve as pages that generate queries" settingKeys={[PERSONAL_KEYS.query, QUERY_KEYS.queryPages]} + initialValue={querySettings[QUERY_KEYS.queryPages]} onChange={(values) => { void extensionAPI.settings.set("query-pages", values); }} diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 4a1b51f0f..44a72dd30 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -28,7 +28,6 @@ import { FeedbackWidget } from "~/components/BirdEatsBugs"; import { getVersionWithDate } from "~/utils/getVersion"; import { LeftSidebarPersonalSections } from "./LeftSidebarPersonalSettings"; import { LeftSidebarGlobalSections } from "./LeftSidebarGlobalSettings"; -import { bulkReadSettings } from "./utils/accessors"; import posthog from "posthog-js"; type SectionHeaderProps = { @@ -82,11 +81,8 @@ export const SettingsDialog = ({ (node) => node.text === "relations", ); const nodesNode = grammarNode?.children.find((node) => node.text === "nodes"); - const [settingsSnapshot, setSettingsSnapshot] = useState(() => - bulkReadSettings(), - ); const [nodes] = useState(() => - getDiscourseNodes(undefined, settingsSnapshot).filter(excludeDefaultNodes), + getDiscourseNodes().filter(excludeDefaultNodes), ); const [activeTabId, setActiveTabId] = useState( selectedTabId ?? "discourse-graph-home-personal", @@ -95,7 +91,6 @@ export const SettingsDialog = ({ window.roamAlphaAPI.graph.name === "discourse-graphs" || false, ); - // ENG-1617: surface settings dialog open time in devtools for perf monitoring. useEffect(() => { console.log( `[settings] open ${(performance.now() - mountStart).toFixed(0)}ms`, @@ -164,13 +159,6 @@ export const SettingsDialog = ({ { - // Tab panels mount lazily (`renderActiveTabPanelOnly`), so each - // re-mount re-seeds child `useState` from `initialValue`. Refresh - // the snapshot when navigating back to Home so user toggles made - // earlier in the same dialog session don't visually revert. - if (id === "discourse-graph-home-personal") { - setSettingsSnapshot(bulkReadSettings()); - } setActiveTabId(id); posthog.capture("Settings: Tab Opened", { tabId: String(id), @@ -187,13 +175,7 @@ export const SettingsDialog = ({ id="discourse-graph-home-personal" title="Home" className="overflow-y-auto" - panel={ - - } + panel={} /> ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getGlobalSetting(props.settingKeys) } {...globalAccessors.text} /> @@ -566,7 +566,7 @@ export const GlobalFlagPanel = (props: FlagWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getGlobalSetting(props.settingKeys) } {...globalAccessors.flag} /> @@ -576,7 +576,7 @@ export const GlobalNumberPanel = (props: NumberWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getGlobalSetting(props.settingKeys) } {...globalAccessors.number} /> @@ -586,7 +586,7 @@ export const GlobalSelectPanel = (props: SelectWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getGlobalSetting(props.settingKeys) } {...globalAccessors.text} /> @@ -596,7 +596,7 @@ export const GlobalMultiTextPanel = (props: MultiTextWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getGlobalSetting(props.settingKeys) } {...globalAccessors.multiText} /> @@ -606,15 +606,12 @@ export const PersonalTextPanel = ({ setter, ...props }: TextWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getPersonalSetting(props.settingKeys) } setter={setter ?? personalAccessors.text.setter} /> ); -// Prop wins so HomePersonalSettings can hand down a value from the dialog-open -// snapshot. QuerySettings still calls without `initialValue`, so the fallback -// handles that path. export const PersonalFlagPanel = (props: FlagWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getPersonalSetting(props.settingKeys) } setter={setter ?? personalAccessors.number.setter} /> @@ -642,7 +639,7 @@ export const PersonalSelectPanel = (props: SelectWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getPersonalSetting(props.settingKeys) } {...personalAccessors.text} /> @@ -652,7 +649,7 @@ export const PersonalMultiTextPanel = (props: MultiTextWrapperProps) => ( (props.settingKeys) ?? props.initialValue + props.initialValue ?? getPersonalSetting(props.settingKeys) } {...personalAccessors.multiText} /> diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index da2865699..bbd4cd192 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -4,6 +4,7 @@ import getBlockProps, { } from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; +import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import { getSubTree } from "roamjs-components/util"; import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree"; @@ -649,52 +650,6 @@ const setBlockPropAtPath = ( setBlockProps(blockUid, updatedProps, false); }; -// Single pull on the settings page returns all top-level children -// (Feature Flags / Global / ) with their string + uid + props. -// Avoids the slow `q`-based getBlockUidByTextOnPage (~290ms per call). -const getBlockPropBasedSettings = ({ - keys, -}: { - keys: string[]; -}): { blockProps: json | undefined; blockUid: string } => { - if (keys.length === 0) { - internalError({ - error: "getBlockPropBasedSettings called with no keys", - type: "DG Accessor", - }); - return { blockProps: undefined, blockUid: "" }; - } - - const pageResult = window.roamAlphaAPI.pull( - "[{:block/children [:block/string :block/uid :block/props]}]", - [":node/title", DG_BLOCK_PROP_SETTINGS_PAGE_TITLE], - ) as Record | null; - - const children = (pageResult?.[":block/children"] ?? []) as Record< - string, - json - >[]; - const child = children.find((c) => c[":block/string"] === keys[0]); - if (!child) { - return { blockProps: undefined, blockUid: "" }; - } - - const blockUid = (child[":block/uid"] as string) || ""; - const rawProps = child[":block/props"]; - const allBlockProps: json = - rawProps && typeof rawProps === "object" ? normalizeProps(rawProps) : {}; - - if (keys.length === 1) { - return { blockProps: allBlockProps, blockUid }; - } - - const blockProps = readPathValue(allBlockProps, keys.slice(1)) as - | json - | undefined; - - return { blockProps, blockUid }; -}; - const setBlockPropBasedSettings = ({ keys, value, @@ -710,7 +665,10 @@ const setBlockPropBasedSettings = ({ return; } - const { blockUid } = getBlockPropBasedSettings({ keys: [keys[0]] }); + const blockUid = getBlockUidByTextOnPage({ + text: keys[0], + title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + }); if (!blockUid) { internalError({ @@ -724,11 +682,7 @@ const setBlockPropBasedSettings = ({ }; export const getFeatureFlags = (): FeatureFlags => { - const { blockProps } = getBlockPropBasedSettings({ - keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags], - }); - - return FeatureFlagsSchema.parse(blockProps || {}); + return bulkReadSettings().featureFlags; }; /* eslint-disable @typescript-eslint/naming-convention */ @@ -824,11 +778,7 @@ export const setFeatureFlag = ( }; export const getGlobalSettings = (): GlobalSettings => { - const { blockProps } = getBlockPropBasedSettings({ - keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global], - }); - - return GlobalSettingsSchema.parse(blockProps || {}); + return bulkReadSettings().globalSettings; }; export const getGlobalSetting = ( @@ -879,7 +829,7 @@ export const setGlobalSetting = (keys: string[], value: json): void => { }; export const getAllRelations = ( - snapshot?: Pick, + snapshot?: SettingsSnapshot, ): DiscourseRelation[] => { const settings = snapshot ? snapshot.globalSettings : getGlobalSettings(); @@ -896,13 +846,7 @@ export const getAllRelations = ( }; export const getPersonalSettings = (): PersonalSettings => { - const personalKey = getPersonalSettingsKey(); - - const { blockProps } = getBlockPropBasedSettings({ - keys: [personalKey], - }); - - return PersonalSettingsSchema.parse(blockProps || {}); + return bulkReadSettings().personalSettings; }; export const getPersonalSetting = ( diff --git a/apps/roam/src/utils/getDiscourseNodes.ts b/apps/roam/src/utils/getDiscourseNodes.ts index f34d3b4d9..765d12422 100644 --- a/apps/roam/src/utils/getDiscourseNodes.ts +++ b/apps/roam/src/utils/getDiscourseNodes.ts @@ -109,7 +109,7 @@ const getUidAndBooleanSetting = ({ const getDiscourseNodes = ( relations?: ReturnType, - snapshot?: Pick, + snapshot?: SettingsSnapshot, ) => { const resolvedRelations = relations ?? getDiscourseRelations(snapshot); const newStoreEnabled = snapshot diff --git a/apps/roam/src/utils/getDiscourseRelations.ts b/apps/roam/src/utils/getDiscourseRelations.ts index 9eabd1012..d7e36cab7 100644 --- a/apps/roam/src/utils/getDiscourseRelations.ts +++ b/apps/roam/src/utils/getDiscourseRelations.ts @@ -36,9 +36,7 @@ export const getRelationsNode = (grammarNode = getGrammarNode()) => { return grammarNode?.children.find(matchNodeText("relations")); }; -const getDiscourseRelations = ( - snapshot?: Pick, -) => { +const getDiscourseRelations = (snapshot?: SettingsSnapshot) => { const newStoreEnabled = snapshot ? snapshot.featureFlags["Use new settings store"] : isNewSettingsStoreEnabled(); From 3a7ab07e43850df1a4d68e56020419cf675d6503 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Apr 2026 12:32:34 +0530 Subject: [PATCH 09/17] ENG-1617: Lift settings snapshot to SettingsDialog, thread to all tabs 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. --- .../src/components/settings/AdminPanel.tsx | 27 +++- .../components/settings/DefaultFilters.tsx | 23 ++- .../settings/DiscourseRelationConfigPanel.tsx | 44 ++---- .../components/settings/ExportSettings.tsx | 13 +- .../components/settings/GeneralSettings.tsx | 15 +- .../settings/HomePersonalSettings.tsx | 17 ++- .../settings/LeftSidebarGlobalSettings.tsx | 24 ++- .../settings/LeftSidebarPersonalSettings.tsx | 24 ++- .../src/components/settings/NodeConfig.tsx | 10 +- .../src/components/settings/QuerySettings.tsx | 14 +- .../roam/src/components/settings/Settings.tsx | 142 ++++++++++++++---- .../settings/SuggestiveModeSettings.tsx | 25 ++- .../components/BlockPropSettingPanels.tsx | 108 +++++-------- .../components/settings/utils/accessors.ts | 49 +----- 14 files changed, 299 insertions(+), 236 deletions(-) diff --git a/apps/roam/src/components/settings/AdminPanel.tsx b/apps/roam/src/components/settings/AdminPanel.tsx index ae9e0d7d7..aeb7f5b47 100644 --- a/apps/roam/src/components/settings/AdminPanel.tsx +++ b/apps/roam/src/components/settings/AdminPanel.tsx @@ -15,8 +15,8 @@ import { import Description from "roamjs-components/components/Description"; import { Select } from "@blueprintjs/select"; import { - getFeatureFlag, setFeatureFlag, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { onSettingChange, @@ -264,7 +264,11 @@ const NodeListTab = (): React.ReactElement => { ); }; -const FeatureFlagsTab = (): React.ReactElement => { +const FeatureFlagsTab = ({ + featureFlags, +}: { + featureFlags: SettingsSnapshot["featureFlags"]; +}): React.ReactElement => { const legacySuggestiveModeMeta = useMemo(() => { refreshConfigTree(); return { @@ -276,8 +280,8 @@ const FeatureFlagsTab = (): React.ReactElement => { }; }, []); - const [suggestiveModeEnabled, setSuggestiveModeEnabled] = useState(() => - getFeatureFlag("Suggestive mode enabled"), + const [suggestiveModeEnabled, setSuggestiveModeEnabled] = useState( + featureFlags["Suggestive mode enabled"], ); const [suggestiveModeUid, setSuggestiveModeUid] = useState( legacySuggestiveModeMeta.suggestiveModeEnabledUid, @@ -365,6 +369,7 @@ const FeatureFlagsTab = (): React.ReactElement => { title="Use new settings store" description="When enabled, accessor getters read from block props instead of the old system. Surfaces dual-write gaps during development." featureKey="Use new settings store" + initialValue={featureFlags["Use new settings store"]} />
} /> - {getFeatureFlag("Suggestive mode enabled") && ( + {featureFlags["Suggestive mode enabled"] && ( { - const [snapshot] = useState(() => bulkReadSettings()); - const querySettings = snapshot.personalSettings.Query; + const querySettings = personalSettings.Query; return (
- +
); diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 44a72dd30..834e4edb3 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { OnloadArgs } from "roamjs-components/types"; import { Classes, @@ -29,6 +29,7 @@ import { getVersionWithDate } from "~/utils/getVersion"; import { LeftSidebarPersonalSections } from "./LeftSidebarPersonalSettings"; import { LeftSidebarGlobalSections } from "./LeftSidebarGlobalSettings"; import posthog from "posthog-js"; +import { bulkReadSettings } from "./utils/accessors"; type SectionHeaderProps = { children: React.ReactNode; @@ -44,6 +45,25 @@ const SectionHeader = ({ children, className }: SectionHeaderProps) => { ); }; +type TabTimingProps = { + label: string; + startRef: React.MutableRefObject; + children: React.ReactNode; +}; +const TabTiming = ({ label, startRef, children }: TabTimingProps) => { + useEffect(() => { + const commit = performance.now() - startRef.current; + requestAnimationFrame(() => { + const paint = performance.now() - startRef.current; + console.log( + `[settings-tab] ${label}: commit ${commit.toFixed(0)}ms, paint ${paint.toFixed(0)}ms`, + ); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return <>{children}; +}; + export const SettingsPanel = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { return (
@@ -73,6 +93,8 @@ export const SettingsDialog = ({ selectedTabId?: TabId; }) => { const [mountStart] = useState(() => performance.now()); + const [snapshot] = useState(() => bulkReadSettings()); + const tabSwitchStartRef = useRef(mountStart); const extensionAPI = onloadArgs.extensionAPI; const grammarNode = discourseConfigRef.tree.find( (node) => node.text === "grammar", @@ -81,9 +103,7 @@ export const SettingsDialog = ({ (node) => node.text === "relations", ); const nodesNode = grammarNode?.children.find((node) => node.text === "nodes"); - const [nodes] = useState(() => - getDiscourseNodes().filter(excludeDefaultNodes), - ); + const nodes = getDiscourseNodes().filter(excludeDefaultNodes); const [activeTabId, setActiveTabId] = useState( selectedTabId ?? "discourse-graph-home-personal", ); @@ -159,6 +179,7 @@ export const SettingsDialog = ({ { + tabSwitchStartRef.current = performance.now(); setActiveTabId(id); posthog.capture("Settings: Tab Opened", { tabId: String(id), @@ -175,19 +196,43 @@ export const SettingsDialog = ({ id="discourse-graph-home-personal" title="Home" className="overflow-y-auto" - panel={} + panel={ + + + + } /> } + panel={ + + + + } /> } + panel={ + + + + } /> Global Settings @@ -196,19 +241,41 @@ export const SettingsDialog = ({ id="discourse-graph-home" title="Home" className="overflow-y-auto" - panel={} + panel={ + + + + } /> } + panel={ + + + + } /> } + panel={ + + + + } /> Grammar + + + } /> + + + } /> Nodes @@ -245,7 +317,18 @@ export const SettingsDialog = ({ id={n.type} title={n.text} className="overflow-y-auto" - panel={} + panel={ + + + + } /> ))} @@ -255,7 +338,14 @@ export const SettingsDialog = ({ id="secret-admin-panel" title="Admin" className="overflow-y-auto" - panel={} + panel={ + + + + } />
diff --git a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx index fab4e513e..9a580a737 100644 --- a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx +++ b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx @@ -9,7 +9,7 @@ import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/data/constants"; import { createOrUpdateDiscourseEmbedding } from "~/utils/syncDgNodesToSupabase"; import { render as renderToast } from "roamjs-components/components/Toast"; import { GlobalFlagPanel } from "./components/BlockPropSettingPanels"; -import { getGlobalSetting } from "~/components/settings/utils/accessors"; +import { type SettingsSnapshot } from "~/components/settings/utils/accessors"; import { GLOBAL_KEYS, SUGGESTIVE_MODE_KEYS, @@ -17,7 +17,11 @@ import { import posthog from "posthog-js"; import { getSuggestiveModeConfigAndUids } from "~/utils/getSuggestiveModeConfigSettings"; -const SuggestiveModeSettings = () => { +const SuggestiveModeSettings = ({ + globalSettings, +}: { + globalSettings: SettingsSnapshot["globalSettings"]; +}) => { const suggestiveMode = getSuggestiveModeConfigAndUids( discourseConfigRef.tree, ); @@ -27,11 +31,10 @@ const SuggestiveModeSettings = () => { ); const pageGroupsUid = suggestiveMode.pageGroups.uid; + const suggestiveModeSettings = globalSettings[GLOBAL_KEYS.suggestiveMode]; + const [includePageRelations, setIncludePageRelations] = useState( - getGlobalSetting([ - GLOBAL_KEYS.suggestiveMode, - SUGGESTIVE_MODE_KEYS.includeCurrentPageRelations, - ]) ?? false, + suggestiveModeSettings[SUGGESTIVE_MODE_KEYS.includeCurrentPageRelations], ); useEffect(() => { @@ -91,6 +94,11 @@ const SuggestiveModeSettings = () => { GLOBAL_KEYS.suggestiveMode, SUGGESTIVE_MODE_KEYS.includeCurrentPageRelations, ]} + initialValue={ + suggestiveModeSettings[ + SUGGESTIVE_MODE_KEYS.includeCurrentPageRelations + ] + } order={0} uid={suggestiveMode.includePageRelations.uid} parentUid={effectiveSuggestiveModeUid} @@ -108,6 +116,11 @@ const SuggestiveModeSettings = () => { GLOBAL_KEYS.suggestiveMode, SUGGESTIVE_MODE_KEYS.includeParentAndChildBlocks, ]} + initialValue={ + suggestiveModeSettings[ + SUGGESTIVE_MODE_KEYS.includeParentAndChildBlocks + ] + } value={includePageRelations ? true : undefined} order={1} uid={suggestiveMode.includeParentAndChildren.uid} diff --git a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx index 0793fed09..757c0cbef 100644 --- a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx +++ b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx @@ -20,8 +20,6 @@ import useSingleChildValue from "roamjs-components/components/ConfigPanels/useSi import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { - getGlobalSetting, - getPersonalSetting, getFeatureFlag, getDiscourseNodeSetting, setGlobalSetting, @@ -470,15 +468,32 @@ const BaseMultiTextPanel = ({ ); }; -type TextWrapperProps = Omit & { +type TextWrapperProps = Omit & { setter?: TextSetter; + initialValue: string; }; -type FlagWrapperProps = Omit; -type NumberWrapperProps = Omit & { +type FlagWrapperProps = Omit & { + initialValue: boolean; +}; +type NumberWrapperProps = Omit< + BaseNumberPanelProps, + "setter" | "initialValue" +> & { setter?: NumberSetter; + initialValue: number; +}; +type SelectWrapperProps = Omit< + BaseSelectPanelProps, + "setter" | "initialValue" +> & { + initialValue: string; +}; +type MultiTextWrapperProps = Omit< + BaseMultiTextPanelProps, + "setter" | "initialValue" +> & { + initialValue: string[]; }; -type SelectWrapperProps = Omit; -type MultiTextWrapperProps = Omit; const featureFlagSetter: FlagSetter = (keys, value) => { const key = keys[0]; @@ -513,6 +528,7 @@ export const FeatureFlagPanel = ({ title, description, featureKey, + initialValue, onBeforeEnable, onAfterChange, parentUid, @@ -522,6 +538,7 @@ export const FeatureFlagPanel = ({ title: string; description: string; featureKey: keyof FeatureFlags; + initialValue?: boolean; onBeforeEnable?: () => Promise; onAfterChange?: (checked: boolean) => void; } & RoamBlockSyncProps) => { @@ -542,7 +559,7 @@ export const FeatureFlagPanel = ({ description={description} settingKeys={[featureKey as string]} setter={featureFlagSetter} - initialValue={getFeatureFlag(featureKey)} + initialValue={initialValue ?? getFeatureFlag(featureKey)} onBeforeChange={handleBeforeChange} onChange={onAfterChange} parentUid={parentUid} @@ -553,73 +570,31 @@ export const FeatureFlagPanel = ({ }; export const GlobalTextPanel = (props: TextWrapperProps) => ( - (props.settingKeys) - } - {...globalAccessors.text} - /> + ); export const GlobalFlagPanel = (props: FlagWrapperProps) => ( - (props.settingKeys) - } - {...globalAccessors.flag} - /> + ); export const GlobalNumberPanel = (props: NumberWrapperProps) => ( - (props.settingKeys) - } - {...globalAccessors.number} - /> + ); export const GlobalSelectPanel = (props: SelectWrapperProps) => ( - (props.settingKeys) - } - {...globalAccessors.text} - /> + ); export const GlobalMultiTextPanel = (props: MultiTextWrapperProps) => ( - (props.settingKeys) - } - {...globalAccessors.multiText} - /> + ); export const PersonalTextPanel = ({ setter, ...props }: TextWrapperProps) => ( - (props.settingKeys) - } - setter={setter ?? personalAccessors.text.setter} - /> + ); export const PersonalFlagPanel = (props: FlagWrapperProps) => ( - (props.settingKeys) - } - {...personalAccessors.flag} - /> + ); export const PersonalNumberPanel = ({ @@ -628,31 +603,16 @@ export const PersonalNumberPanel = ({ }: NumberWrapperProps) => ( (props.settingKeys) - } setter={setter ?? personalAccessors.number.setter} /> ); export const PersonalSelectPanel = (props: SelectWrapperProps) => ( - (props.settingKeys) - } - {...personalAccessors.text} - /> + ); export const PersonalMultiTextPanel = (props: MultiTextWrapperProps) => ( - (props.settingKeys) - } - {...personalAccessors.multiText} - /> + ); const createDiscourseNodeSetter = diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index bbd4cd192..398c13d40 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -706,25 +706,14 @@ export const getFeatureFlag = (key: keyof FeatureFlags): boolean => { const legacyReader = FEATURE_FLAG_LEGACY_MAP[key]; if (!legacyReader) { - // Block-props-only flag (no legacy equivalent) - const flags = getFeatureFlags(); - return flags[key]; + return getFeatureFlags()[key]; } if (!isNewSettingsStoreEnabled()) { return legacyReader(); } - const flags = getFeatureFlags(); - const blockPropsValue = flags[key]; - const legacyValue = legacyReader(); - if (blockPropsValue !== legacyValue) { - console.warn(`[DG Dual-Read] Mismatch at Feature Flag > ${key}`, { - blockProps: blockPropsValue, - legacy: legacyValue, - }); - } - return blockPropsValue; + return getFeatureFlags()[key]; }; export const isNewSettingsStoreEnabled = (): boolean => { @@ -790,16 +779,7 @@ export const getGlobalSetting = ( return getLegacyGlobalSetting(keys) as T | undefined; } - const settings = getGlobalSettings(); - const blockPropsValue = readPathValue(settings, keys); - const legacyValue = getLegacyGlobalSetting(keys); - if (!deepEqual(blockPropsValue, legacyValue)) { - console.warn( - `[DG Dual-Read] Mismatch at Global > ${formatSettingPath(keys)}`, - { blockProps: blockPropsValue, legacy: legacyValue }, - ); - } - return blockPropsValue as T | undefined; + return readPathValue(getGlobalSettings(), keys) as T | undefined; }; export const setGlobalSetting = (keys: string[], value: json): void => { @@ -858,16 +838,7 @@ export const getPersonalSetting = ( return getLegacyPersonalSetting(keys) as T | undefined; } - const settings = getPersonalSettings(); - const blockPropsValue = readPathValue(settings, keys); - const legacyValue = getLegacyPersonalSetting(keys); - if (!deepEqual(blockPropsValue, legacyValue)) { - console.warn( - `[DG Dual-Read] Mismatch at Personal > ${formatSettingPath(keys)}`, - { blockProps: blockPropsValue, legacy: legacyValue }, - ); - } - return blockPropsValue as T | undefined; + return readPathValue(getPersonalSettings(), keys) as T | undefined; }; export type SettingsSnapshot = { @@ -992,15 +963,9 @@ export const getDiscourseNodeSetting = ( } const settings = getDiscourseNodeSettings(nodeType); - const blockPropsValue = settings ? readPathValue(settings, keys) : undefined; - const legacyValue = getLegacyDiscourseNodeSetting(nodeType, keys); - if (!deepEqual(blockPropsValue, legacyValue)) { - console.warn( - `[DG Dual-Read] Mismatch at Discourse Node (${nodeType}) > ${formatSettingPath(keys)}`, - { blockProps: blockPropsValue, legacy: legacyValue }, - ); - } - return blockPropsValue as T | undefined; + return (settings ? readPathValue(settings, keys) : undefined) as + | T + | undefined; }; export const setDiscourseNodeSetting = ( From ba709522d42c7e999d52416129e508a03723b2c3 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Apr 2026 13:12:01 +0530 Subject: [PATCH 10/17] ENG-1617: Remove timing instrumentation, per-call dual-read, flag-aware 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 --- .../roam/src/components/settings/Settings.tsx | 149 ++++++------------ .../components/BlockPropSettingPanels.tsx | 49 +++--- .../components/settings/utils/accessors.ts | 14 +- 3 files changed, 76 insertions(+), 136 deletions(-) diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 834e4edb3..86ca3c8e6 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; import { OnloadArgs } from "roamjs-components/types"; import { Classes, @@ -45,25 +45,6 @@ const SectionHeader = ({ children, className }: SectionHeaderProps) => { ); }; -type TabTimingProps = { - label: string; - startRef: React.MutableRefObject; - children: React.ReactNode; -}; -const TabTiming = ({ label, startRef, children }: TabTimingProps) => { - useEffect(() => { - const commit = performance.now() - startRef.current; - requestAnimationFrame(() => { - const paint = performance.now() - startRef.current; - console.log( - `[settings-tab] ${label}: commit ${commit.toFixed(0)}ms, paint ${paint.toFixed(0)}ms`, - ); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return <>{children}; -}; - export const SettingsPanel = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { return (
@@ -92,9 +73,7 @@ export const SettingsDialog = ({ onClose?: () => void; selectedTabId?: TabId; }) => { - const [mountStart] = useState(() => performance.now()); const [snapshot] = useState(() => bulkReadSettings()); - const tabSwitchStartRef = useRef(mountStart); const extensionAPI = onloadArgs.extensionAPI; const grammarNode = discourseConfigRef.tree.find( (node) => node.text === "grammar", @@ -111,12 +90,6 @@ export const SettingsDialog = ({ window.roamAlphaAPI.graph.name === "discourse-graphs" || false, ); - useEffect(() => { - console.log( - `[settings] open ${(performance.now() - mountStart).toFixed(0)}ms`, - ); - }, [mountStart]); - useEffect(() => { posthog.capture("Settings: Dialog Opened", { initialTabId: String(selectedTabId ?? "discourse-graph-home-personal"), @@ -179,7 +152,6 @@ export const SettingsDialog = ({ { - tabSwitchStartRef.current = performance.now(); setActiveTabId(id); posthog.capture("Settings: Tab Opened", { tabId: String(id), @@ -197,13 +169,11 @@ export const SettingsDialog = ({ title="Home" className="overflow-y-auto" panel={ - - - + } /> - - + } /> - - + } /> @@ -242,12 +205,10 @@ export const SettingsDialog = ({ title="Home" className="overflow-y-auto" panel={ - - - + } /> - - + } /> - - + } /> Grammar @@ -283,15 +235,13 @@ export const SettingsDialog = ({ title="Relations" className="overflow-y-auto" panel={ - - - + } /> - - + } /> Nodes @@ -318,16 +266,11 @@ export const SettingsDialog = ({ title={n.text} className="overflow-y-auto" panel={ - - - + } /> ))} @@ -339,12 +282,10 @@ export const SettingsDialog = ({ title="Admin" className="overflow-y-auto" panel={ - - - + } /> diff --git a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx index 757c0cbef..b63385f8a 100644 --- a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx +++ b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx @@ -48,7 +48,7 @@ type BaseTextPanelProps = { description: string; settingKeys: string[]; setter: TextSetter; - initialValue?: string; + initialValue: string; placeholder?: string; multiline?: boolean; error?: string; @@ -60,7 +60,7 @@ type BaseFlagPanelProps = { description: string; settingKeys: string[]; setter: FlagSetter; - initialValue?: boolean; + initialValue: boolean; value?: boolean; disabled?: boolean; onBeforeChange?: (checked: boolean) => Promise; @@ -72,7 +72,7 @@ type BaseNumberPanelProps = { description: string; settingKeys: string[]; setter: NumberSetter; - initialValue?: number; + initialValue: number; min?: number; max?: number; onChange?: (value: number) => void; @@ -84,7 +84,7 @@ type BaseSelectPanelProps = { settingKeys: string[]; setter: TextSetter; options: string[]; - initialValue?: string; + initialValue: string; } & RoamBlockSyncProps; type BaseMultiTextPanelProps = { @@ -92,7 +92,7 @@ type BaseMultiTextPanelProps = { description: string; settingKeys: string[]; setter: MultiTextSetter; - initialValue?: string[]; + initialValue: string[]; onChange?: (values: string[]) => void; } & RoamBlockSyncProps; @@ -468,32 +468,15 @@ const BaseMultiTextPanel = ({ ); }; -type TextWrapperProps = Omit & { +type TextWrapperProps = Omit & { setter?: TextSetter; - initialValue: string; }; -type FlagWrapperProps = Omit & { - initialValue: boolean; -}; -type NumberWrapperProps = Omit< - BaseNumberPanelProps, - "setter" | "initialValue" -> & { +type FlagWrapperProps = Omit; +type NumberWrapperProps = Omit & { setter?: NumberSetter; - initialValue: number; -}; -type SelectWrapperProps = Omit< - BaseSelectPanelProps, - "setter" | "initialValue" -> & { - initialValue: string; -}; -type MultiTextWrapperProps = Omit< - BaseMultiTextPanelProps, - "setter" | "initialValue" -> & { - initialValue: string[]; }; +type SelectWrapperProps = Omit; +type MultiTextWrapperProps = Omit; const featureFlagSetter: FlagSetter = (keys, value) => { const key = keys[0]; @@ -642,7 +625,8 @@ export const DiscourseNodeTextPanel = ({ {...props} initialValue={ getDiscourseNodeSetting(nodeType, props.settingKeys) ?? - props.initialValue + props.initialValue ?? + "" } setter={createDiscourseNodeSetter(nodeType)} /> @@ -662,7 +646,8 @@ export const DiscourseNodeFlagPanel = ({ {...props} initialValue={ getDiscourseNodeSetting(nodeType, props.settingKeys) ?? - props.initialValue + props.initialValue ?? + false } setter={createDiscourseNodeSetter(nodeType)} /> @@ -677,7 +662,8 @@ export const DiscourseNodeSelectPanel = ({ {...props} initialValue={ getDiscourseNodeSetting(nodeType, props.settingKeys) ?? - props.initialValue + props.initialValue ?? + "" } setter={createDiscourseNodeSetter(nodeType)} /> @@ -696,7 +682,8 @@ export const DiscourseNodeNumberPanel = ({ {...props} initialValue={ getDiscourseNodeSetting(nodeType, props.settingKeys) ?? - props.initialValue + props.initialValue ?? + 0 } setter={createDiscourseNodeSetter(nodeType)} /> diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 398c13d40..7a8cffe92 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -879,8 +879,20 @@ export const bulkReadSettings = (): SettingsSnapshot => { } } + const featureFlags = FeatureFlagsSchema.parse(featureFlagsProps || {}); + + if (!featureFlags["Use new settings store"]) { + return { + featureFlags, + globalSettings: GlobalSettingsSchema.parse(readAllLegacyGlobalSettings()), + personalSettings: PersonalSettingsSchema.parse( + readAllLegacyPersonalSettings(), + ), + }; + } + return { - featureFlags: FeatureFlagsSchema.parse(featureFlagsProps || {}), + featureFlags, globalSettings: GlobalSettingsSchema.parse(globalProps || {}), personalSettings: PersonalSettingsSchema.parse(personalProps || {}), }; From 38996b37508a022464114684172c4d289e534ac2 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Apr 2026 13:53:43 +0530 Subject: [PATCH 11/17] ENG-1617: Eliminate double bulkReadSettings calls in accessor functions 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. --- .../components/settings/utils/accessors.ts | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 7a8cffe92..8e56ea728 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -703,17 +703,7 @@ const FEATURE_FLAG_LEGACY_MAP: Partial< /* eslint-enable @typescript-eslint/naming-convention */ export const getFeatureFlag = (key: keyof FeatureFlags): boolean => { - const legacyReader = FEATURE_FLAG_LEGACY_MAP[key]; - - if (!legacyReader) { - return getFeatureFlags()[key]; - } - - if (!isNewSettingsStoreEnabled()) { - return legacyReader(); - } - - return getFeatureFlags()[key]; + return bulkReadSettings().featureFlags[key]; }; export const isNewSettingsStoreEnabled = (): boolean => { @@ -774,12 +764,9 @@ export const getGlobalSetting = ( keys: string[], ): T | undefined => { if (keys.length === 0) return undefined; - - if (!isNewSettingsStoreEnabled()) { - return getLegacyGlobalSetting(keys) as T | undefined; - } - - return readPathValue(getGlobalSettings(), keys) as T | undefined; + return readPathValue(bulkReadSettings().globalSettings, keys) as + | T + | undefined; }; export const setGlobalSetting = (keys: string[], value: json): void => { @@ -833,12 +820,9 @@ export const getPersonalSetting = ( keys: string[], ): T | undefined => { if (keys.length === 0) return undefined; - - if (!isNewSettingsStoreEnabled()) { - return getLegacyPersonalSetting(keys) as T | undefined; - } - - return readPathValue(getPersonalSettings(), keys) as T | undefined; + return readPathValue(bulkReadSettings().personalSettings, keys) as + | T + | undefined; }; export type SettingsSnapshot = { @@ -970,7 +954,7 @@ export const getDiscourseNodeSetting = ( nodeType: string, keys: string[], ): T | undefined => { - if (!isNewSettingsStoreEnabled()) { + if (!bulkReadSettings().featureFlags["Use new settings store"]) { return getLegacyDiscourseNodeSetting(nodeType, keys) as T | undefined; } From bcf19ec3ec7d3ed96ec6c064d3b5c16b77d6346c Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Apr 2026 14:04:27 +0530 Subject: [PATCH 12/17] ENG-1617: Re-read snapshot on tab change to prevent stale initialValues 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. --- apps/roam/src/components/settings/Settings.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 86ca3c8e6..1273afbd6 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { OnloadArgs } from "roamjs-components/types"; import { Classes, @@ -73,7 +73,6 @@ export const SettingsDialog = ({ onClose?: () => void; selectedTabId?: TabId; }) => { - const [snapshot] = useState(() => bulkReadSettings()); const extensionAPI = onloadArgs.extensionAPI; const grammarNode = discourseConfigRef.tree.find( (node) => node.text === "grammar", @@ -86,6 +85,8 @@ export const SettingsDialog = ({ const [activeTabId, setActiveTabId] = useState( selectedTabId ?? "discourse-graph-home-personal", ); + // eslint-disable-next-line react-hooks/exhaustive-deps + const snapshot = useMemo(() => bulkReadSettings(), [activeTabId]); const [showAdminPanel, setShowAdminPanel] = useState( window.roamAlphaAPI.graph.name === "discourse-graphs" || false, ); From 24e521b47a8afe0dab24262fbc5bef1f51879519 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Apr 2026 14:29:24 +0530 Subject: [PATCH 13/17] =?UTF-8?q?ENG-1616:=20Address=20review=20=E2=80=94?= =?UTF-8?q?=20rename=20snapshot=20vars,=20flag-gate=20bulkRead,=20move=20P?= =?UTF-8?q?ostHog=20guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../components/settings/utils/accessors.ts | 14 ++++- apps/roam/src/index.ts | 22 ++++---- .../utils/initializeObserversAndListeners.ts | 53 +++++++++---------- apps/roam/src/utils/posthog.ts | 2 + 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 6be04569b..b23b466b3 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -949,8 +949,20 @@ export const bulkReadSettings = (): SettingsSnapshot => { } } + const featureFlags = FeatureFlagsSchema.parse(featureFlagsProps || {}); + + if (!featureFlags["Use new settings store"]) { + return { + featureFlags, + globalSettings: GlobalSettingsSchema.parse(readAllLegacyGlobalSettings()), + personalSettings: PersonalSettingsSchema.parse( + readAllLegacyPersonalSettings(), + ), + }; + } + return { - featureFlags: FeatureFlagsSchema.parse(featureFlagsProps || {}), + featureFlags, globalSettings: GlobalSettingsSchema.parse(globalProps || {}), personalSettings: PersonalSettingsSchema.parse(personalProps || {}), }; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index daec39cc4..68d404ede 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -48,13 +48,9 @@ export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; export default runExtension(async (onloadArgs) => { const pluginLoadStart = performance.now(); - const settingsSnapshot = bulkReadSettings(); + const settings = bulkReadSettings(); - const isEncrypted = window.roamAlphaAPI.graph.isEncrypted; - const isOffline = window.roamAlphaAPI.graph.type === "offline"; - const disallowDiagnostics = - settingsSnapshot.personalSettings[PERSONAL_KEYS.disableProductDiagnostics]; - if (!isEncrypted && !isOffline && !disallowDiagnostics) { + if (!settings.personalSettings[PERSONAL_KEYS.disableProductDiagnostics]) { initPostHog(); } @@ -81,16 +77,16 @@ export default runExtension(async (onloadArgs) => { initPluginTimer(); - await initializeDiscourseNodes(settingsSnapshot); + await initializeDiscourseNodes(settings); - refreshConfigTree(settingsSnapshot); + refreshConfigTree(settings); addGraphViewNodeStyling(); registerCommandPaletteCommands(onloadArgs); createSettingsPanel(onloadArgs); registerSmartBlock(onloadArgs); - setInitialQueryPages(onloadArgs, settingsSnapshot); + setInitialQueryPages(onloadArgs, settings); const style = addStyle(styles); const discourseGraphStyle = addStyle(discourseGraphStyles); @@ -99,7 +95,7 @@ export default runExtension(async (onloadArgs) => { // Add streamline styling only if enabled const isStreamlineStylingEnabled = - settingsSnapshot.personalSettings[PERSONAL_KEYS.streamlineStyling]; + settings.personalSettings[PERSONAL_KEYS.streamlineStyling]; let streamlineStyleElement: HTMLStyleElement | null = null; if (isStreamlineStylingEnabled) { streamlineStyleElement = addStyle(streamlineStyling); @@ -108,7 +104,7 @@ export default runExtension(async (onloadArgs) => { const { observers, listeners, cleanups } = initObservers({ onloadArgs, - settingsSnapshot, + settings, }); const { pageActionListener, @@ -123,7 +119,7 @@ export default runExtension(async (onloadArgs) => { document.addEventListener("input", discourseNodeSearchTriggerListener); document.addEventListener("selectionchange", nodeCreationPopoverListener); - if (settingsSnapshot.featureFlags["Suggestive mode enabled"]) { + if (settings.featureFlags["Suggestive mode enabled"]) { initializeSupabaseSync(); } @@ -154,7 +150,7 @@ export default runExtension(async (onloadArgs) => { getDiscourseNodes: getDiscourseNodes, }; - installDiscourseFloatingMenu(onloadArgs, settingsSnapshot); + installDiscourseFloatingMenu(onloadArgs, settings); const leftSidebarScript = document.querySelector( 'script#roam-left-sidebar[src="https://sid597.github.io/roam-left-sidebar/js/main.js"]', diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index ace07fb31..27a095aba 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -89,10 +89,10 @@ const getTitleAndUidFromHeader = (h1: HTMLHeadingElement) => { export const initObservers = ({ onloadArgs, - settingsSnapshot, + settings, }: { onloadArgs: OnloadArgs; - settingsSnapshot: SettingsSnapshot; + settings: SettingsSnapshot; }): { observers: MutationObserver[]; listeners: { @@ -111,16 +111,16 @@ export const initObservers = ({ const h1 = e as HTMLHeadingElement; const { title, uid } = getTitleAndUidFromHeader(h1); - const callbackSnapshot = bulkReadSettings(); + const settings = bulkReadSettings(); const props = { title, h1, onloadArgs }; const isSuggestiveModeEnabled = - callbackSnapshot.featureFlags["Suggestive mode enabled"]; + settings.featureFlags["Suggestive mode enabled"]; const node = findDiscourseNode({ uid, title, - snapshot: callbackSnapshot, + snapshot: settings, }); const isDiscourseNode = node && node.backedBy !== "default"; if (isDiscourseNode) { @@ -135,13 +135,11 @@ export const initObservers = ({ renderCanvasReferences(linkedReferencesDiv, uid, onloadArgs); } } - if (isQueryPage({ title, snapshot: callbackSnapshot })) { + if (isQueryPage({ title, snapshot: settings })) { renderQueryPage(props); - } else if ( - isCurrentPageCanvas({ title, h1, snapshot: callbackSnapshot }) - ) { + } else if (isCurrentPageCanvas({ title, h1, snapshot: settings })) { renderTldrawCanvas(props); - } else if (isSidebarCanvas({ title, h1, snapshot: callbackSnapshot })) { + } else if (isSidebarCanvas({ title, h1, snapshot: settings })) { renderTldrawCanvasInSidebar(props); } }, @@ -155,8 +153,8 @@ export const initObservers = ({ let batchedTagNodes: DiscourseNode[] | null = null; const getNodesForTagBatch = (): DiscourseNode[] => { if (batchedTagNodes === null) { - const snap = bulkReadSettings(); - batchedTagNodes = getDiscourseNodes(undefined, snap); + const settings = bulkReadSettings(); + batchedTagNodes = getDiscourseNodes(undefined, settings); queueMicrotask(() => { batchedTagNodes = null; }); @@ -230,7 +228,7 @@ export const initObservers = ({ const suggestiveHandler = getSuggestiveOverlayHandler(onloadArgs); const toggleSuggestiveOverlay = onPageRefObserverChange(suggestiveHandler); - if (settingsSnapshot.personalSettings[PERSONAL_KEYS.suggestiveModeOverlay]) { + if (settings.personalSettings[PERSONAL_KEYS.suggestiveModeOverlay]) { addPageRefObserver(suggestiveHandler); } @@ -260,12 +258,10 @@ export const initObservers = ({ }, }); - if (settingsSnapshot.personalSettings[PERSONAL_KEYS.pagePreview]) + if (settings.personalSettings[PERSONAL_KEYS.pagePreview]) addPageRefObserver(previewPageRefHandler); - if ( - settingsSnapshot.personalSettings[PERSONAL_KEYS.discourseContextOverlay] - ) { + if (settings.personalSettings[PERSONAL_KEYS.discourseContextOverlay]) { const overlayHandler = getOverlayHandler(onloadArgs); onPageRefObserverChange(overlayHandler)(true); } @@ -276,23 +272,22 @@ export const initObservers = ({ const hashChangeListener = (e: Event) => { const evt = e as HashChangeEvent; - const navSnapshot = bulkReadSettings(); + const settings = bulkReadSettings(); // Attempt to refresh config navigating away from config page // doesn't work if they update via sidebar if ( (configPageUid && evt.oldURL.endsWith(configPageUid)) || - getDiscourseNodes(undefined, navSnapshot).some(({ type }) => + getDiscourseNodes(undefined, settings).some(({ type }) => evt.oldURL.endsWith(type), ) ) { - refreshConfigTree(navSnapshot); + refreshConfigTree(settings); } }; - let globalTrigger = - settingsSnapshot.globalSettings[GLOBAL_KEYS.trigger].trim(); + let globalTrigger = settings.globalSettings[GLOBAL_KEYS.trigger].trim(); const personalTriggerComboRaw = - settingsSnapshot.personalSettings[PERSONAL_KEYS.personalNodeMenuTrigger]; + settings.personalSettings[PERSONAL_KEYS.personalNodeMenuTrigger]; const personalTriggerCombo = typeof personalTriggerComboRaw === "object" ? personalTriggerComboRaw @@ -327,16 +322,16 @@ export const initObservers = ({ className: "starred-pages-wrapper", callback: (el) => { void (async () => { - const callbackSnapshot = bulkReadSettings(); + const settings = bulkReadSettings(); const isLeftSidebarEnabled = - callbackSnapshot.featureFlags["Enable left sidebar"]; + settings.featureFlags["Enable left sidebar"]; const container = el as HTMLDivElement; if (isLeftSidebarEnabled) { container.style.padding = "0"; await mountLeftSidebar({ wrapper: container, onloadArgs, - initialSnapshot: callbackSnapshot, + initialSnapshot: settings, }); } })(); @@ -385,7 +380,7 @@ export const initObservers = ({ }; let customTrigger = - settingsSnapshot.personalSettings[PERSONAL_KEYS.nodeSearchMenuTrigger]; + settings.personalSettings[PERSONAL_KEYS.nodeSearchMenuTrigger]; const unsubSearchTrigger = onSettingChange( settingKeys.nodeSearchMenuTrigger, @@ -445,8 +440,8 @@ export const initObservers = ({ }; const nodeCreationPopoverListener = debounce(() => { - const snap = bulkReadSettings(); - if (!snap.personalSettings[PERSONAL_KEYS.textSelectionPopup]) return; + const settings = bulkReadSettings(); + if (!settings.personalSettings[PERSONAL_KEYS.textSelectionPopup]) return; const selection = window.getSelection(); diff --git a/apps/roam/src/utils/posthog.ts b/apps/roam/src/utils/posthog.ts index 9b19e63cd..c2b4bc8a5 100644 --- a/apps/roam/src/utils/posthog.ts +++ b/apps/roam/src/utils/posthog.ts @@ -7,6 +7,8 @@ let initialized = false; export const initPostHog = (): void => { if (initialized) return; + if (window.roamAlphaAPI.graph.isEncrypted) return; + if (window.roamAlphaAPI.graph.type === "offline") return; const propertyDenylist = new Set([ "$ip", "$device_id", From 0d852820a2ec63003b24eef0322bba0fc37fed28 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Apr 2026 20:46:08 +0530 Subject: [PATCH 14/17] ENG-1617: Fix DiscourseNodeSelectPanel fallback to use first option instead of empty string --- .../components/settings/components/BlockPropSettingPanels.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx index b63385f8a..d4044436e 100644 --- a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx +++ b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx @@ -663,6 +663,7 @@ export const DiscourseNodeSelectPanel = ({ initialValue={ getDiscourseNodeSetting(nodeType, props.settingKeys) ?? props.initialValue ?? + props.options[0] ?? "" } setter={createDiscourseNodeSetter(nodeType)} From 52ffaedf5ddf75bd38fc922c83c420801e648961 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sat, 11 Apr 2026 21:22:43 +0530 Subject: [PATCH 15/17] ENG-1617: Rename snapshot variables to settings for clarity --- .../components/settings/QueryPagesPanel.tsx | 6 ++--- .../roam/src/components/settings/Settings.tsx | 26 +++++++++---------- .../components/settings/utils/accessors.ts | 8 +++--- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/apps/roam/src/components/settings/QueryPagesPanel.tsx b/apps/roam/src/components/settings/QueryPagesPanel.tsx index 4966a4ef8..363257e64 100644 --- a/apps/roam/src/components/settings/QueryPagesPanel.tsx +++ b/apps/roam/src/components/settings/QueryPagesPanel.tsx @@ -15,9 +15,9 @@ import { // Legacy extensionAPI stored query-pages as string | string[] | Record. // Coerce to string[] for backward compatibility with old stored formats. -export const getQueryPages = (snapshot?: SettingsSnapshot): string[] => { - const value = snapshot - ? (readPathValue(snapshot.personalSettings, [ +export const getQueryPages = (settings?: SettingsSnapshot): string[] => { + const value = settings + ? (readPathValue(settings.personalSettings, [ PERSONAL_KEYS.query, QUERY_KEYS.queryPages, ]) as string[] | string | Record | undefined) diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 1273afbd6..3d96c6f60 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -86,7 +86,7 @@ export const SettingsDialog = ({ selectedTabId ?? "discourse-graph-home-personal", ); // eslint-disable-next-line react-hooks/exhaustive-deps - const snapshot = useMemo(() => bulkReadSettings(), [activeTabId]); + const settings = useMemo(() => bulkReadSettings(), [activeTabId]); const [showAdminPanel, setShowAdminPanel] = useState( window.roamAlphaAPI.graph.name === "discourse-graphs" || false, ); @@ -172,8 +172,8 @@ export const SettingsDialog = ({ panel={ } /> @@ -184,7 +184,7 @@ export const SettingsDialog = ({ panel={ } /> @@ -194,7 +194,7 @@ export const SettingsDialog = ({ className="overflow-y-auto" panel={ } /> @@ -207,8 +207,8 @@ export const SettingsDialog = ({ className="overflow-y-auto" panel={ } /> @@ -217,7 +217,7 @@ export const SettingsDialog = ({ title="Export" className="overflow-y-auto" panel={ - + } /> } /> @@ -241,7 +241,7 @@ export const SettingsDialog = ({ title="Relations" parentUid={grammarNode?.uid || ""} uid={relationsNode?.uid || ""} - globalSettings={snapshot.globalSettings} + globalSettings={settings.globalSettings} /> } /> @@ -270,7 +270,7 @@ export const SettingsDialog = ({ } /> @@ -284,8 +284,8 @@ export const SettingsDialog = ({ className="overflow-y-auto" panel={ } /> diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 8e56ea728..bb310e170 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -796,11 +796,13 @@ export const setGlobalSetting = (keys: string[], value: json): void => { }; export const getAllRelations = ( - snapshot?: SettingsSnapshot, + settings?: SettingsSnapshot, ): DiscourseRelation[] => { - const settings = snapshot ? snapshot.globalSettings : getGlobalSettings(); + const globalSettings = settings + ? settings.globalSettings + : getGlobalSettings(); - return Object.entries(settings.Relations).flatMap(([id, relation]) => + return Object.entries(globalSettings.Relations).flatMap(([id, relation]) => relation.ifConditions.map((ifCondition) => ({ id, label: relation.label, From b15b0ab47f7da987734a505218969ce3a068b26c Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 15 Apr 2026 21:22:41 +0530 Subject: [PATCH 16/17] Fix legacy bulk settings fallback --- apps/roam/src/components/settings/utils/accessors.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index b23b466b3..775945a32 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -954,10 +954,8 @@ export const bulkReadSettings = (): SettingsSnapshot => { if (!featureFlags["Use new settings store"]) { return { featureFlags, - globalSettings: GlobalSettingsSchema.parse(readAllLegacyGlobalSettings()), - personalSettings: PersonalSettingsSchema.parse( - readAllLegacyPersonalSettings(), - ), + globalSettings: readAllLegacyGlobalSettings() as GlobalSettings, + personalSettings: readAllLegacyPersonalSettings() as PersonalSettings, }; } From 5b4b2337dc5160e022a80e69c8570c3c08198b6e Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 15 Apr 2026 22:29:51 +0530 Subject: [PATCH 17/17] fix bug similar code --- .../components/settings/utils/accessors.ts | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index d926742c7..d5595696f 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -882,61 +882,6 @@ export const bulkReadSettings = (): SettingsSnapshot => { }; }; -export type SettingsSnapshot = { - featureFlags: FeatureFlags; - globalSettings: GlobalSettings; - personalSettings: PersonalSettings; -}; - -export const bulkReadSettings = (): SettingsSnapshot => { - const pageResult = window.roamAlphaAPI.pull( - "[{:block/children [:block/string :block/props]}]", - [":node/title", DG_BLOCK_PROP_SETTINGS_PAGE_TITLE], - ) as Record | null; - - const children = (pageResult?.[":block/children"] ?? []) as Record< - string, - json - >[]; - const personalKey = getPersonalSettingsKey(); - let featureFlagsProps: json = {}; - let globalProps: json = {}; - let personalProps: json = {}; - - for (const child of children) { - const text = child[":block/string"]; - if (typeof text !== "string") continue; - const rawBlockProps = child[":block/props"]; - const blockProps = - rawBlockProps && typeof rawBlockProps === "object" - ? normalizeProps(rawBlockProps) - : {}; - if (text === TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags) { - featureFlagsProps = blockProps; - } else if (text === TOP_LEVEL_BLOCK_PROP_KEYS.global) { - globalProps = blockProps; - } else if (text === personalKey) { - personalProps = blockProps; - } - } - - const featureFlags = FeatureFlagsSchema.parse(featureFlagsProps || {}); - - if (!featureFlags["Use new settings store"]) { - return { - featureFlags, - globalSettings: readAllLegacyGlobalSettings() as GlobalSettings, - personalSettings: readAllLegacyPersonalSettings() as PersonalSettings, - }; - } - - return { - featureFlags, - globalSettings: GlobalSettingsSchema.parse(globalProps || {}), - personalSettings: PersonalSettingsSchema.parse(personalProps || {}), - }; -}; - export const setPersonalSetting = (keys: string[], value: json): void => { if (keys.length === 0) { internalError({