From da4d214c051152880e2450555c8f867013b80e03 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Jul 2026 11:51:24 +0530 Subject: [PATCH 1/9] [ENG-1855] Add Roam shared-node import discovery --- .../components/DiscoverSharedNodesDialog.tsx | 263 ++++++++++++++ .../__tests__/discoverSharedNodes.test.ts | 163 +++++++++ apps/roam/src/utils/discoverSharedNodes.ts | 333 ++++++++++++++++++ .../utils/registerCommandPaletteCommands.ts | 7 + 4 files changed, 766 insertions(+) create mode 100644 apps/roam/src/components/DiscoverSharedNodesDialog.tsx create mode 100644 apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts create mode 100644 apps/roam/src/utils/discoverSharedNodes.ts diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx new file mode 100644 index 000000000..a60962ba3 --- /dev/null +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -0,0 +1,263 @@ +import { + Button, + Callout, + Classes, + Dialog, + HTMLTable, + InputGroup, + Intent, + NonIdealState, + Spinner, + Tag, + Tooltip, +} from "@blueprintjs/core"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import createOverlayRender from "roamjs-components/util/createOverlayRender"; +import { + discoverSharedNodes, + type DiscoveredSharedNode, +} from "~/utils/discoverSharedNodes"; +import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; + +const formatModifiedAt = (modifiedAt: string): string => + new Date(modifiedAt).toLocaleString(); + +const SharedNodeRow = ({ node }: { node: DiscoveredSharedNode }) => ( + + + {node.sourceApp} + + +
+ {node.sourceSpaceName} +
+
+ {node.sourceSpaceId} +
+ + +
+ {node.title} +
+ + + {node.sourceNodeId ? ( +
+ {node.sourceNodeId} +
+ ) : ( + Not provided + )} + + + {formatModifiedAt(node.modifiedAt)} + + + {node.alreadyImported ? ( + + Imported + + ) : ( + Available + )} + + +); + +const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { + const [nodes, setNodes] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + + const loadNodes = useCallback(async (): Promise => { + setLoading(true); + setError(""); + try { + const [client, context] = await Promise.all([ + getLoggedInClient(), + getSupabaseContext(), + ]); + if (!client || !context) + throw new Error("Could not connect to shared persistence."); + setNodes( + await discoverSharedNodes({ + client, + currentSpaceId: context.spaceId, + }), + ); + } catch (loadError) { + console.error("Failed to discover shared nodes:", loadError); + setError( + loadError instanceof Error + ? loadError.message + : "Could not load shared nodes.", + ); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + void loadNodes(); + }, [loadNodes]); + + const visibleNodes = useMemo(() => { + const normalizedSearch = search.trim().toLocaleLowerCase(); + if (!normalizedSearch) return nodes; + return nodes.filter((node) => + [ + node.sourceApp, + node.sourceSpaceName, + node.sourceSpaceId, + node.title, + node.sourceNodeId, + ].some((value) => value?.toLocaleLowerCase().includes(normalizedSearch)), + ); + }, [nodes, search]); + + return ( + +
+
+ ) => + setSearch(event.target.value) + } + placeholder="Search shared nodes" + value={search} + /> + +
+ + {loading ? ( +
+ +
+ ) : error ? ( + +
{error}
+ +
+ ) : visibleNodes.length === 0 ? ( +
+ +
+ ) : ( +
+ + + + Source app + Source space + Title + Source ID + Modified + Status + + + + {visibleNodes.map((node) => ( + + ))} + + +
+ )} +
+
+
+ + {loading || error + ? "" + : `${visibleNodes.length} of ${nodes.length} nodes`} + + +
+
+
+ ); +}; + +type Props = Record; + +export const renderDiscoverSharedNodesDialog = createOverlayRender( + "discourse-discover-shared-nodes", + DiscoverSharedNodesDialog, +); + +export default DiscoverSharedNodesDialog; diff --git a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts new file mode 100644 index 000000000..ca007246f --- /dev/null +++ b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from "vitest"; +import { buildDiscoveredSharedNodes } from "~/utils/discoverSharedNodes"; + +type BuildArgs = Parameters[0]; + +const resources: BuildArgs["resources"] = [ + { space_id: 20, source_local_id: "node-1" }, + { space_id: 20, source_local_id: "schema-1" }, +]; +const spaces: BuildArgs["spaces"] = [ + { + id: 20, + name: "Research vault", + platform: "Obsidian", + url: "obsidian:vault-a", + }, +]; +const concepts: BuildArgs["concepts"] = [ + { + is_schema: false, + last_modified: "2026-06-14T12:00:00.000Z", + schema_id: 200, + source_local_id: "node-1", + space_id: 20, + }, +]; +const contents: BuildArgs["contents"] = [ + { + content_type: "text/plain", + last_modified: "2026-06-14T13:00:00.000Z", + source_local_id: "node-1", + space_id: 20, + text: "EVD - REM sleep and recall", + variant: "direct", + }, + { + content_type: "text/markdown", + last_modified: "2026-06-14T15:00:00.000Z", + source_local_id: "node-1", + space_id: 20, + text: "# EVD - REM sleep and recall", + variant: "full", + }, +]; +const sourceNodeRid = "orn:obsidian.note:vault-a/node-1"; + +const build = ({ + conceptsOverride = concepts, + contentsOverride = contents, + currentSpaceId = 10, + importedSourceRids = new Set(), + resourcesOverride = resources, + spacesOverride = spaces, +}: { + conceptsOverride?: typeof concepts; + contentsOverride?: typeof contents; + currentSpaceId?: number; + importedSourceRids?: ReadonlySet; + resourcesOverride?: typeof resources; + spacesOverride?: typeof spaces; +} = {}) => + buildDiscoveredSharedNodes({ + concepts: conceptsOverride, + contents: contentsOverride, + currentSpaceId, + importedSourceRids, + resources: resourcesOverride, + spaces: spacesOverride, + }); + +describe("buildDiscoveredSharedNodes", () => { + it("builds a group-shared contract node with stable source identity", () => { + expect(build({ importedSourceRids: new Set([sourceNodeRid]) })).toEqual([ + { + alreadyImported: true, + modifiedAt: "2026-06-14T15:00:00.000Z", + sourceApp: "Obsidian", + sourceNodeId: "node-1", + sourceNodeRid, + sourceSpaceId: "obsidian:vault-a", + sourceSpaceName: "Research vault", + title: "EVD - REM sleep and recall", + }, + ]); + }); + + it("does not discover shared resources from the current space", () => { + expect(build({ currentSpaceId: 20 })).toEqual([]); + }); + + it("requires the exact shared resource identity", () => { + expect( + build({ + resourcesOverride: [{ space_id: 21, source_local_id: "node-1" }], + }), + ).toEqual([]); + }); + + it.each([ + { + name: "schema concept", + conceptsOverride: [{ ...concepts[0], is_schema: true }], + contentsOverride: contents, + }, + { + name: "missing node type", + conceptsOverride: [{ ...concepts[0], schema_id: null }], + contentsOverride: contents, + }, + { + name: "missing direct content", + conceptsOverride: concepts, + contentsOverride: [contents[1]], + }, + { + name: "missing full content", + conceptsOverride: concepts, + contentsOverride: [contents[0]], + }, + { + name: "untyped full content", + conceptsOverride: concepts, + contentsOverride: [contents[0], { ...contents[1], content_type: null }], + }, + ])("filters a node with $name", ({ conceptsOverride, contentsOverride }) => { + expect(build({ conceptsOverride, contentsOverride })).toEqual([]); + }); + + it("matches imports by RID rather than source-local ID alone", () => { + expect( + build({ + importedSourceRids: new Set(["orn:obsidian.note:another-vault/node-1"]), + })[0]?.alreadyImported, + ).toBe(false); + }); + + it("sorts newest nodes first", () => { + const olderConcept = { + ...concepts[0], + last_modified: "2026-06-10T12:00:00.000Z", + source_local_id: "node-2", + }; + const olderContents = contents.map((content) => ({ + ...content, + last_modified: "2026-06-10T12:00:00.000Z", + source_local_id: "node-2", + text: + content.variant === "direct" + ? "Older shared node" + : "# Older shared node", + })); + expect( + build({ + conceptsOverride: [olderConcept, concepts[0]], + contentsOverride: [...olderContents, ...contents], + resourcesOverride: [ + ...resources, + { space_id: 20, source_local_id: "node-2" }, + ], + }).map((node) => node.sourceNodeId), + ).toEqual(["node-1", "node-2"]); + }); +}); diff --git a/apps/roam/src/utils/discoverSharedNodes.ts b/apps/roam/src/utils/discoverSharedNodes.ts new file mode 100644 index 000000000..ae78fded5 --- /dev/null +++ b/apps/roam/src/utils/discoverSharedNodes.ts @@ -0,0 +1,333 @@ +import type { DGSupabaseClient } from "@repo/database/lib/client"; +import { getAvailableGroupIds } from "@repo/database/lib/groups"; +import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; +import type { Tables } from "@repo/database/dbTypes"; +import { DISCOURSE_GRAPH_PROP_NAME } from "./createReifiedBlock"; + +const IMPORTED_FROM_PROP_KEY = "importedFrom"; +const PAGE_SIZE = 1000; +const RESOURCE_ID_CHUNK_SIZE = 100; + +type ResourceAccess = Pick< + Tables<"ResourceAccess">, + "space_id" | "source_local_id" +>; +type SharedConcept = Pick< + Tables<"my_concepts">, + "is_schema" | "last_modified" | "schema_id" | "source_local_id" | "space_id" +>; +type SharedContent = Pick< + Tables<"my_contents">, + | "content_type" + | "last_modified" + | "source_local_id" + | "space_id" + | "text" + | "variant" +>; +type SharedSpace = Pick< + Tables<"my_spaces">, + "id" | "name" | "platform" | "url" +>; +type ValidSharedSpace = { + id: number; + name: string; + platform: "Roam" | "Obsidian"; + url: string; +}; + +export type DiscoveredSharedNode = { + alreadyImported: boolean; + modifiedAt: string; + sourceApp: "Roam" | "Obsidian"; + sourceNodeId?: string; + sourceNodeRid: string; + sourceSpaceId: string; + sourceSpaceName: string; + title: string; +}; + +type SharedNodeRows = { + concepts: SharedConcept[]; + contents: SharedContent[]; + resources: ResourceAccess[]; + spaces: SharedSpace[]; +}; + +const getResourceKey = ({ + sourceLocalId, + spaceId, +}: { + sourceLocalId: string; + spaceId: number; +}): string => `${spaceId}:${sourceLocalId}`; + +const getLatestTimestamp = (timestamps: (string | null)[]): string | null => { + const validTimestamps = timestamps.filter( + (timestamp): timestamp is string => + typeof timestamp === "string" && !Number.isNaN(Date.parse(timestamp)), + ); + if (validTimestamps.length === 0) return null; + return validTimestamps.reduce((latest, timestamp) => + Date.parse(timestamp) > Date.parse(latest) ? timestamp : latest, + ); +}; + +export const buildDiscoveredSharedNodes = ({ + concepts, + contents, + currentSpaceId, + importedSourceRids, + resources, + spaces, +}: SharedNodeRows & { + currentSpaceId: number; + importedSourceRids: ReadonlySet; +}): DiscoveredSharedNode[] => { + const sharedResourceKeys = new Set( + resources + .filter((resource) => resource.space_id !== currentSpaceId) + .map((resource) => + getResourceKey({ + sourceLocalId: resource.source_local_id, + spaceId: resource.space_id, + }), + ), + ); + const spacesById = new Map( + spaces.flatMap((space): [number, ValidSharedSpace][] => { + if ( + typeof space.id !== "number" || + typeof space.name !== "string" || + (space.platform !== "Roam" && space.platform !== "Obsidian") || + typeof space.url !== "string" + ) + return []; + return [ + [ + space.id, + { + id: space.id, + name: space.name, + platform: space.platform, + url: space.url, + }, + ], + ]; + }), + ); + const contentByResource = new Map< + string, + Partial> + >(); + + contents.forEach((content) => { + if ( + typeof content.space_id !== "number" || + typeof content.source_local_id !== "string" || + (content.variant !== "direct" && content.variant !== "full") + ) + return; + const key = getResourceKey({ + sourceLocalId: content.source_local_id, + spaceId: content.space_id, + }); + const variants = contentByResource.get(key) ?? {}; + variants[content.variant] = content; + contentByResource.set(key, variants); + }); + + return concepts + .flatMap((concept): DiscoveredSharedNode[] => { + if ( + concept.is_schema !== false || + concept.schema_id === null || + typeof concept.space_id !== "number" || + typeof concept.source_local_id !== "string" + ) + return []; + + const resourceKey = getResourceKey({ + sourceLocalId: concept.source_local_id, + spaceId: concept.space_id, + }); + if (!sharedResourceKeys.has(resourceKey)) return []; + + const space = spacesById.get(concept.space_id); + const variants = contentByResource.get(resourceKey); + const direct = variants?.direct; + const full = variants?.full; + if ( + !space || + typeof direct?.text !== "string" || + typeof full?.text !== "string" || + typeof full.content_type !== "string" + ) + return []; + + const modifiedAt = getLatestTimestamp([ + concept.last_modified, + direct.last_modified, + full.last_modified, + ]); + if (!modifiedAt) return []; + + let sourceNodeRid: string; + try { + sourceNodeRid = spaceUriAndLocalIdToRid( + space.url, + concept.source_local_id, + space.platform === "Obsidian" ? "note" : undefined, + ); + } catch { + return []; + } + + return [ + { + alreadyImported: importedSourceRids.has(sourceNodeRid), + modifiedAt, + sourceApp: space.platform, + sourceNodeId: concept.source_local_id || undefined, + sourceNodeRid, + sourceSpaceId: space.url, + sourceSpaceName: space.name, + title: direct.text, + }, + ]; + }) + .sort( + (left, right) => + Date.parse(right.modifiedAt) - Date.parse(left.modifiedAt) || + left.title.localeCompare(right.title), + ); +}; + +const getGroupSharedResources = async ( + client: DGSupabaseClient, +): Promise => { + const groupIds = await getAvailableGroupIds(client); + if (groupIds.length === 0) return []; + + const resources: ResourceAccess[] = []; + for (let from = 0; ; from += PAGE_SIZE) { + const response = await client + .from("ResourceAccess") + .select("space_id, source_local_id") + .in("account_uid", groupIds) + .order("space_id") + .order("source_local_id") + .range(from, from + PAGE_SIZE - 1); + if (response.error) throw response.error; + resources.push(...response.data); + if (response.data.length < PAGE_SIZE) break; + } + + return [ + ...new Map( + resources.map((resource) => [ + getResourceKey({ + sourceLocalId: resource.source_local_id, + spaceId: resource.space_id, + }), + resource, + ]), + ).values(), + ]; +}; + +const chunk = (values: T[], size: number): T[][] => + Array.from({ length: Math.ceil(values.length / size) }, (_, index) => + values.slice(index * size, (index + 1) * size), + ); + +const getSharedNodeRows = async ({ + client, + currentSpaceId, +}: { + client: DGSupabaseClient; + currentSpaceId: number; +}): Promise => { + const resources = (await getGroupSharedResources(client)).filter( + (resource) => resource.space_id !== currentSpaceId, + ); + if (resources.length === 0) + return { concepts: [], contents: [], resources, spaces: [] }; + + const spaceIds = [...new Set(resources.map((resource) => resource.space_id))]; + const spacesResponse = await client + .from("my_spaces") + .select("id, name, platform, url") + .in("id", spaceIds); + if (spacesResponse.error) throw spacesResponse.error; + + const concepts: SharedConcept[] = []; + const contents: SharedContent[] = []; + for (const spaceId of spaceIds) { + const sourceLocalIds = resources + .filter((resource) => resource.space_id === spaceId) + .map((resource) => resource.source_local_id); + for (const ids of chunk(sourceLocalIds, RESOURCE_ID_CHUNK_SIZE)) { + const [conceptsResponse, contentsResponse] = await Promise.all([ + client + .from("my_concepts") + .select( + "is_schema, last_modified, schema_id, source_local_id, space_id", + ) + .eq("space_id", spaceId) + .eq("is_schema", false) + .in("source_local_id", ids), + client + .from("my_contents") + .select( + "content_type, last_modified, source_local_id, space_id, text, variant", + ) + .eq("space_id", spaceId) + .in("source_local_id", ids) + .in("variant", ["direct", "full"]), + ]); + if (conceptsResponse.error) throw conceptsResponse.error; + if (contentsResponse.error) throw contentsResponse.error; + concepts.push(...conceptsResponse.data); + contents.push(...contentsResponse.data); + } + } + + return { + concepts, + contents, + resources, + spaces: spacesResponse.data, + }; +}; + +export const getImportedSourceRids = async (): Promise> => { + const query = `[:find [?rid ...] + :where + [?page :block/props ?props] + [(get ?props :${DISCOURSE_GRAPH_PROP_NAME}) ?dgData] + [(get ?dgData :${IMPORTED_FROM_PROP_KEY}) ?imported] + [(get ?imported :sourceNodeRid) ?rid]]`; + const result = (await window.roamAlphaAPI.data.async.q(query)) as unknown[]; + return new Set( + result.filter((rid): rid is string => typeof rid === "string"), + ); +}; + +export const discoverSharedNodes = async ({ + client, + currentSpaceId, +}: { + client: DGSupabaseClient; + currentSpaceId: number; +}): Promise => { + const [rows, importedSourceRids] = await Promise.all([ + getSharedNodeRows({ client, currentSpaceId }), + getImportedSourceRids(), + ]); + return buildDiscoveredSharedNodes({ + ...rows, + currentSpaceId, + importedSourceRids, + }); +}; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 00c7ff076..11d55ba38 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -53,6 +53,7 @@ import { getBlockSelection, insertPageRefAtRange, } from "./advancedSearchFooterUtils"; +import { renderDiscoverSharedNodesDialog } from "~/components/DiscoverSharedNodesDialog"; export const createDiscourseNodeFromCommand = ( extensionAPI: OnloadArgs["extensionAPI"], @@ -341,6 +342,11 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { renderSettings({ onloadArgs }); }; + const discoverSharedNodes = () => { + posthog.capture("Shared Nodes: Discover Command Triggered"); + renderDiscoverSharedNodesDialog({}); + }; + const toggleDiscourseContextOverlay = async () => { const currentValue = getPersonalSetting([ PERSONAL_KEYS.discourseContextOverlay, @@ -415,6 +421,7 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { void addCommand("DG: Export - Discourse graph", exportDiscourseGraph); void addCommand("DG: Open - Discourse settings", renderSettingsPopup); if (isSyncEnabled()) { + void addCommand("DG: Discover shared nodes", discoverSharedNodes); void addCommand("DG: Share current node", shareCurrentNode); } if (getFeatureFlag("Advanced node search enabled")) { From b805ddf2c534959da3d9540db13bb5bd9fddfa5a Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Jul 2026 15:53:25 +0530 Subject: [PATCH 2/9] [ENG-1855] Keep discovery dialog open --- apps/roam/src/components/DiscoverSharedNodesDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index a60962ba3..49da7b85c 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -143,7 +143,7 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { return ( Date: Fri, 10 Jul 2026 17:40:29 +0530 Subject: [PATCH 3/9] [ENG-1855] Fix discovery dialog dismissing on keyboard launch Roam's command palette runs commands on Enter keyup, which landed as a click on the dialog's autofocused close button and dismissed it instantly. Disable autoFocus/enforceFocus like AdvancedSearchDialog and restore outside-click close. Also from review: use Tailwind classes instead of inline styles, drop the unused ValidSharedSpace.id field, and unexport the file-internal getImportedSourceRids. --- .../components/DiscoverSharedNodesDialog.tsx | 86 ++++++------------- apps/roam/src/utils/discoverSharedNodes.ts | 4 +- 2 files changed, 27 insertions(+), 63 deletions(-) diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index 49da7b85c..a45a52556 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -28,43 +28,31 @@ const SharedNodeRow = ({ node }: { node: DiscoveredSharedNode }) => ( {node.sourceApp} -
+
{node.sourceSpaceName}
{node.sourceSpaceId}
-
+
{node.title}
{node.sourceNodeId ? (
{node.sourceNodeId} @@ -73,7 +61,7 @@ const SharedNodeRow = ({ node }: { node: DiscoveredSharedNode }) => ( Not provided )} - + {formatModifiedAt(node.modifiedAt)} @@ -142,25 +130,23 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { return (
-
+
) => setSearch(event.target.value) @@ -180,40 +166,26 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => {
{loading ? ( -
+
) : error ? ( -
{error}
+
{error}
) : visibleNodes.length === 0 ? ( -
+
) : ( -
- +
+ Source app @@ -234,14 +206,8 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { )}
-
- +
+ {loading || error ? "" : `${visibleNodes.length} of ${nodes.length} nodes`} diff --git a/apps/roam/src/utils/discoverSharedNodes.ts b/apps/roam/src/utils/discoverSharedNodes.ts index ae78fded5..89045db80 100644 --- a/apps/roam/src/utils/discoverSharedNodes.ts +++ b/apps/roam/src/utils/discoverSharedNodes.ts @@ -30,7 +30,6 @@ type SharedSpace = Pick< "id" | "name" | "platform" | "url" >; type ValidSharedSpace = { - id: number; name: string; platform: "Roam" | "Obsidian"; url: string; @@ -107,7 +106,6 @@ export const buildDiscoveredSharedNodes = ({ [ space.id, { - id: space.id, name: space.name, platform: space.platform, url: space.url, @@ -301,7 +299,7 @@ const getSharedNodeRows = async ({ }; }; -export const getImportedSourceRids = async (): Promise> => { +const getImportedSourceRids = async (): Promise> => { const query = `[:find [?rid ...] :where [?page :block/props ?props] From aba903dad2ed8c2814d3efdf58a4f2220428d0ec Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 10 Jul 2026 18:00:17 +0530 Subject: [PATCH 4/9] [ENG-1855] Use getAllPages for ResourceAccess pagination Replaces the hand-rolled paging loop with the existing @repo/database/lib/pagination helper, matching how syncDgNodesToSupabase already reads ResourceAccess. --- apps/roam/src/utils/discoverSharedNodes.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/roam/src/utils/discoverSharedNodes.ts b/apps/roam/src/utils/discoverSharedNodes.ts index 89045db80..0b5463601 100644 --- a/apps/roam/src/utils/discoverSharedNodes.ts +++ b/apps/roam/src/utils/discoverSharedNodes.ts @@ -1,5 +1,6 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; import { getAvailableGroupIds } from "@repo/database/lib/groups"; +import { getAllPages } from "@repo/database/lib/pagination"; import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; import type { Tables } from "@repo/database/dbTypes"; import { DISCOURSE_GRAPH_PROP_NAME } from "./createReifiedBlock"; @@ -207,19 +208,16 @@ const getGroupSharedResources = async ( const groupIds = await getAvailableGroupIds(client); if (groupIds.length === 0) return []; - const resources: ResourceAccess[] = []; - for (let from = 0; ; from += PAGE_SIZE) { - const response = await client + const resources = await getAllPages( + client .from("ResourceAccess") .select("space_id, source_local_id") .in("account_uid", groupIds) .order("space_id") - .order("source_local_id") - .range(from, from + PAGE_SIZE - 1); - if (response.error) throw response.error; - resources.push(...response.data); - if (response.data.length < PAGE_SIZE) break; - } + .order("source_local_id"), + PAGE_SIZE, + ); + if (!Array.isArray(resources)) throw resources; return [ ...new Map( From 9f7f93e8988bc3ed13763c6d3e5434ee24c68a79 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 12 Jul 2026 14:50:22 +0530 Subject: [PATCH 5/9] Avoid racing Supabase context initialization --- apps/roam/src/components/DiscoverSharedNodesDialog.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index a45a52556..ffb117c65 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -86,12 +86,10 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { setLoading(true); setError(""); try { - const [client, context] = await Promise.all([ - getLoggedInClient(), - getSupabaseContext(), - ]); - if (!client || !context) - throw new Error("Could not connect to shared persistence."); + const context = await getSupabaseContext(); + if (!context) throw new Error("Could not connect to shared persistence."); + const client = await getLoggedInClient(); + if (!client) throw new Error("Could not connect to shared persistence."); setNodes( await discoverSharedNodes({ client, From d70f4d18abcc2d9c84d158bc79140dd43a722826 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 15 Jul 2026 10:12:38 +0530 Subject: [PATCH 6/9] [ENG-1855] Address shared-node discovery review feedback --- .../components/DiscoverSharedNodesDialog.tsx | 22 ++- .../__tests__/discoverSharedNodes.test.ts | 113 ++++++++--- apps/roam/src/utils/discoverSharedNodes.ts | 183 +++++++++++------- packages/database/src/lib/rid.ts | 3 + 4 files changed, 208 insertions(+), 113 deletions(-) diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index ffb117c65..a27a5f1eb 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -17,6 +17,7 @@ import { discoverSharedNodes, type DiscoveredSharedNode, } from "~/utils/discoverSharedNodes"; +import internalError from "~/utils/internalError"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; const formatModifiedAt = (modifiedAt: string): string => @@ -80,7 +81,7 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { const [nodes, setNodes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [search, setSearch] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); const loadNodes = useCallback(async (): Promise => { setLoading(true); @@ -97,7 +98,12 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { }), ); } catch (loadError) { - console.error("Failed to discover shared nodes:", loadError); + internalError({ + error: loadError, + type: "Shared node discovery failed", + context: { operation: "load-shared-nodes" }, + sendEmail: false, + }); setError( loadError instanceof Error ? loadError.message @@ -113,7 +119,7 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { }, [loadNodes]); const visibleNodes = useMemo(() => { - const normalizedSearch = search.trim().toLocaleLowerCase(); + const normalizedSearch = searchTerm.trim().toLocaleLowerCase(); if (!normalizedSearch) return nodes; return nodes.filter((node) => [ @@ -124,7 +130,7 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { node.sourceNodeId, ].some((value) => value?.toLocaleLowerCase().includes(normalizedSearch)), ); - }, [nodes, search]); + }, [nodes, searchTerm]); return ( void }) => { className="min-w-0 flex-1" leftIcon="search" onChange={(event: React.ChangeEvent) => - setSearch(event.target.value) + setSearchTerm(event.target.value) } placeholder="Search shared nodes" - value={search} + value={searchTerm} />