diff --git a/apps/obsidian/src/components/ImportNodesModal.tsx b/apps/obsidian/src/components/ImportNodesModal.tsx index 16f72d78c..f926a3e16 100644 --- a/apps/obsidian/src/components/ImportNodesModal.tsx +++ b/apps/obsidian/src/components/ImportNodesModal.tsx @@ -4,7 +4,7 @@ import { StrictMode, useState, useEffect, useCallback } from "react"; import type DiscourseGraphPlugin from "../index"; import type { ImportableNode, GroupWithNodes } from "~/types"; import { - getAvailableGroups, + getAvailableGroupIds, getPublishedNodesForGroups, getLocalNodeInstanceIds, getSpaceNameFromIds, @@ -45,15 +45,13 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { return; } - const groups = await getAvailableGroups(client); - if (groups.length === 0) { + const groupIds = await getAvailableGroupIds(client); + if (groupIds.length === 0) { new Notice("You are not a member of any groups"); onClose(); return; } - const groupIds = groups.map((g) => g.group_id); - const publishedNodes = await getPublishedNodesForGroups({ client, groupIds, diff --git a/apps/obsidian/src/components/RelationshipSection.tsx b/apps/obsidian/src/components/RelationshipSection.tsx index 47da12c2f..62c00f288 100644 --- a/apps/obsidian/src/components/RelationshipSection.tsx +++ b/apps/obsidian/src/components/RelationshipSection.tsx @@ -402,7 +402,7 @@ const CurrentRelationships = ({ const group = tempRelationships.get(relationKey)!; const otherId = isSource ? r.destination : r.source; - const linkedFile = await getFileForNodeInstanceId(plugin, otherId); + const linkedFile = getFileForNodeInstanceId(plugin, otherId); if ( linkedFile && !group.linkedFiles.some((f) => f.path === linkedFile.path) diff --git a/apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx b/apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx index 8bba05dd2..9cbcfc292 100644 --- a/apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx +++ b/apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx @@ -540,10 +540,7 @@ const computeRelations = async ( const nodeInstanceId = await getNodeInstanceIdForFile(plugin, file); if (!nodeInstanceId) return []; - const relations = await getRelationsForNodeInstanceId( - plugin, - nodeInstanceId, - ); + const relations = await getRelationsForNodeInstanceId(plugin, nodeInstanceId); const result = new Map(); for (const relationType of plugin.settings.relationTypes) { @@ -573,7 +570,7 @@ const computeRelations = async ( const group = result.get(key)!; for (const r of instanceRels) { const otherId = r.source === nodeInstanceId ? r.destination : r.source; - const linked = await getFileForNodeInstanceId(plugin, otherId); + const linked = getFileForNodeInstanceId(plugin, otherId); if (linked && !group.linkedFiles.some((f) => f.path === linked.path)) { group.linkedFiles.push(linked); } diff --git a/apps/obsidian/src/services/QueryEngine.ts b/apps/obsidian/src/services/QueryEngine.ts index 6c524a70f..25595be1e 100644 --- a/apps/obsidian/src/services/QueryEngine.ts +++ b/apps/obsidian/src/services/QueryEngine.ts @@ -37,6 +37,8 @@ export class QueryEngine { this.app = app; } + functional = () => !!this.dc; + /** * Search across all discourse nodes (files that have frontmatter nodeTypeId) */ @@ -81,6 +83,33 @@ export class QueryEngine { } }; + /** + * Search across all discourse nodes that have nodeInstanceId + */ + getDiscourseNodeById = (nodeInstanceId: string): TFile | null => { + if (!this.dc) { + console.warn( + "Datacore API not available. Search functionality is not available.", + ); + return null; + } + + if (!nodeInstanceId.match(/^[-.+\w]+$/)) { + console.error("Malformed id:", nodeInstanceId); + return null; + } + try { + const dcQuery = `@page and exists(nodeInstanceId) and nodeInstanceId = "${nodeInstanceId}"`; + const potentialNodes = this.dc.query(dcQuery); + const path = potentialNodes.at(0)?.$path; + if (!path) return null; + return this.app.vault.getFileByPath(path); + } catch (error) { + console.error("Error in searchDiscourseNodeById:", error); + return null; + } + }; + searchCompatibleNodeByTitle = async ({ query, compatibleNodeTypeIds, diff --git a/apps/obsidian/src/utils/conceptConversion.ts b/apps/obsidian/src/utils/conceptConversion.ts index 238c5f7db..53b740533 100644 --- a/apps/obsidian/src/utils/conceptConversion.ts +++ b/apps/obsidian/src/utils/conceptConversion.ts @@ -288,6 +288,8 @@ const orderConceptsRec = ({ remainder: { [key: string]: LocalConceptDataInput }; processed: Set; }): Set => { + // Add to processed at the start to prevent cycles + processed.add(concept.source_local_id!); const relatedConceptIds = relatedConcepts(concept); let missing: Set = new Set(); while (relatedConceptIds.length > 0) { @@ -310,7 +312,6 @@ const orderConceptsRec = ({ } } ordered.push(concept); - processed.add(concept.source_local_id!); delete remainder[concept.source_local_id!]; return missing; }; diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index bf7159f7a..8169bb1b5 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -9,9 +9,9 @@ import type { DiscourseNode, ImportableNode } from "~/types"; import { QueryEngine } from "~/services/QueryEngine"; import { spaceUriAndLocalIdToRid, ridToSpaceUriAndLocalId } from "./rid"; -export const getAvailableGroups = async ( +export const getAvailableGroupIds = async ( client: DGSupabaseClient, -): Promise<{ group_id: string }[]> => { +): Promise => { const { data, error } = await client .from("group_membership") .select("group_id") @@ -22,7 +22,7 @@ export const getAvailableGroups = async ( throw new Error(`Failed to fetch groups: ${error.message}`); } - return data || []; + return (data || []).map((g) => g.group_id); }; export const getPublishedNodesForGroups = async ({ diff --git a/apps/obsidian/src/utils/publishNode.ts b/apps/obsidian/src/utils/publishNode.ts index ae2f08cdc..fe6c538a0 100644 --- a/apps/obsidian/src/utils/publishNode.ts +++ b/apps/obsidian/src/utils/publishNode.ts @@ -3,7 +3,17 @@ import type { default as DiscourseGraphPlugin } from "~/index"; import { getLoggedInClient, getSupabaseContext } from "./supabaseContext"; import { addFile } from "@repo/database/lib/files"; import mime from "mime-types"; -import { DGSupabaseClient } from "@repo/database/lib/client"; +import type { DGSupabaseClient } from "@repo/database/lib/client"; +import { + getRelationsForNodeInstanceId, + getFileForNodeInstanceId, + getFileForNodeInstanceIds, + loadRelations, + saveRelations, +} from "./relationsStore"; +import type { RelationInstance } from "~/types"; +import { getAvailableGroupIds } from "./importNodes"; +import { syncAllNodesAndRelations } from "./syncDgNodesToSupabase"; const publishSchema = async ({ client, @@ -56,6 +66,170 @@ const publishSchema = async ({ } }; +const intersection = (set1: Set, set2: Set): Set => { + // @ts-expect-error - Set.intersection is ES2025 feature + if (set1.intersection) return set1.intersection(set2); // eslint-disable-line + const r: Set = new Set(); + for (const x of set1) { + if (set2.has(x)) r.add(x); + } + return r; +}; + +export const publishNewRelation = async ( + plugin: DiscourseGraphPlugin, + relation: RelationInstance, +): Promise => { + const client = await getLoggedInClient(plugin); + if (!client) throw new Error("Cannot get client"); + const context = await getSupabaseContext(plugin); + if (!context) throw new Error("Cannot get context"); + const sourceFile = getFileForNodeInstanceId(plugin, relation.source); + const destinationFile = getFileForNodeInstanceId( + plugin, + relation.destination, + ); + if (!sourceFile || !destinationFile) return false; + const sourceFm = + plugin.app.metadataCache.getFileCache(sourceFile)?.frontmatter; + const destinationFm = + plugin.app.metadataCache.getFileCache(destinationFile)?.frontmatter; + if (!sourceFm || !destinationFm) return false; + + const sourceGroups = sourceFm.publishedToGroups as string[] | undefined; + const destinationGroups = destinationFm.publishedToGroups as + | string[] + | undefined; + if (!Array.isArray(sourceGroups) || !Array.isArray(destinationGroups)) + return false; + const relationTriples = plugin.settings.discourseRelations ?? []; + const triple = relationTriples.find( + (triple) => + triple.relationshipTypeId === relation.type && + triple.sourceId === sourceFm.nodeTypeId && + triple.destinationId === destinationFm.nodeTypeId, + ); + if (!triple) return false; + const resourceIds = [relation.id, relation.type, triple.id]; + const myGroups = await getAvailableGroupIds(client); + const targetGroups = intersection( + new Set(myGroups), + intersection( + new Set(sourceGroups), + new Set(destinationGroups), + ), + ); + if (!targetGroups.size) return false; + // in that case, sync all relations (only) before publishing + await syncAllNodesAndRelations(plugin, context, true); + const entries = []; + for (const group of targetGroups) { + for (const id of resourceIds) { + entries.push({ + /* eslint-disable @typescript-eslint/naming-convention */ + account_uid: group, + source_local_id: id, + space_id: context.spaceId, + /* eslint-enable @typescript-eslint/naming-convention */ + }); + } + } + const publishResponse = await client + .from("ResourceAccess") + .upsert(entries, { ignoreDuplicates: true }); + if (publishResponse.error && publishResponse.error.code !== "23505") + throw publishResponse.error; + relation.publishedToGroupId = [ + ...new Set([ + ...(relation.publishedToGroupId || []), + ...targetGroups.values(), + ]).values(), + ]; + return true; +}; + +export const publishNodeRelations = async ({ + plugin, + client, + nodeId, + myGroup, + spaceId, +}: { + plugin: DiscourseGraphPlugin; + client: DGSupabaseClient; + nodeId: string; + myGroup: string; + spaceId: number; +}): Promise => { + const relations = await getRelationsForNodeInstanceId(plugin, nodeId); + const resourceIds: Set = new Set(); + const relationTriples = plugin.settings.discourseRelations ?? []; + const relevantNodeIds: Set = new Set(); + relations.map((relation) => { + relevantNodeIds.add(relation.source); + relevantNodeIds.add(relation.destination); + }); + const relevantNodeFiles = getFileForNodeInstanceIds(plugin, relevantNodeIds); + const relevantNodeTypeById: Record = {}; + Object.entries(relevantNodeFiles).map(([id, file]: [string, TFile]) => { + const fm = plugin.app.metadataCache.getFileCache(file)?.frontmatter; + if (fm === undefined) return; + if (fm.nodeInstanceId !== nodeId) { + // check if published to same group. + // Note: current node's pub status not in cache yet! + if (!Array.isArray(fm.publishedToGroups)) return; + const publishedToGroups: string[] = + (fm.publishedToGroups as string[]) || []; + if (!publishedToGroups.includes(myGroup)) return; + } + if (fm.importedFromRid) return; // temporary, should be removed after eng-1475 + relevantNodeTypeById[id] = fm.nodeTypeId as string; + }); + relations.map((relation) => { + if ((relation.publishedToGroupId ?? []).includes(myGroup)) return; + const triple = relationTriples.find( + (triple) => + triple.relationshipTypeId === relation.type && + triple.sourceId === relevantNodeTypeById[relation.source] && + triple.destinationId === relevantNodeTypeById[relation.destination], + ); + if (triple) { + resourceIds.add(relation.id); + resourceIds.add(relation.type); + resourceIds.add(triple.id); + } + }); + const publishResponse = await client.from("ResourceAccess").upsert( + [...resourceIds.values()].map((sourceLocalId: string) => ({ + /* eslint-disable @typescript-eslint/naming-convention */ + account_uid: myGroup, + source_local_id: sourceLocalId, + space_id: spaceId, + /* eslint-enable @typescript-eslint/naming-convention */ + })), + { ignoreDuplicates: true }, + ); + if (publishResponse.error && publishResponse.error.code !== "23505") + throw publishResponse.error; + const relData = await loadRelations(plugin); + let dataChanged = false; + relations + .filter((rel) => resourceIds.has(rel.id)) + .map((rel) => { + const savedRel = relData.relations[rel.id]; + if (!savedRel) return; + const publishedTo = savedRel.publishedToGroupId; + if (!publishedTo) { + savedRel.publishedToGroupId = [myGroup]; + dataChanged = true; + } else if (!publishedTo.includes(myGroup)) { + publishedTo.push(myGroup); + dataChanged = true; + } + }); + if (dataChanged) await saveRelations(plugin, relData); +}; + export const publishNode = async ({ plugin, file, @@ -65,25 +239,39 @@ export const publishNode = async ({ file: TFile; frontmatter: FrontMatterCache; }): Promise => { - const nodeId = frontmatter.nodeInstanceId as string | undefined; - if (!nodeId) throw new Error("Please sync the node first"); const client = await getLoggedInClient(plugin); if (!client) throw new Error("Cannot get client"); - const context = await getSupabaseContext(plugin); - if (!context) throw new Error("Cannot get context"); - const spaceId = context.spaceId; - const myGroupsResponse = await client - .from("group_membership") - .select("group_id"); - if (myGroupsResponse.error) throw myGroupsResponse.error; - const myGroups = new Set( - myGroupsResponse.data.map(({ group_id }) => group_id), - ); + const myGroups = new Set(await getAvailableGroupIds(client)); if (myGroups.size === 0) throw new Error("Cannot get group"); const existingPublish = (frontmatter.publishedToGroups as undefined | string[]) || []; const commonGroups = existingPublish.filter((g) => myGroups.has(g)); + // temporary single-group assumption const myGroup = (commonGroups.length > 0 ? commonGroups : [...myGroups])[0]!; + return await publishNodeToGroup({ plugin, file, frontmatter, myGroup }); +}; + +export const publishNodeToGroup = async ({ + plugin, + file, + frontmatter, + myGroup, +}: { + plugin: DiscourseGraphPlugin; + file: TFile; + frontmatter: FrontMatterCache; + myGroup: string; +}): Promise => { + const nodeId = frontmatter.nodeInstanceId as string | undefined; + if (!nodeId) throw new Error("Please sync the node first"); + const context = await getSupabaseContext(plugin); + if (!context) throw new Error("Cannot get context"); + const spaceId = context.spaceId; + const client = await getLoggedInClient(plugin); + if (!client) throw new Error("Cannot get client"); + const existingPublish = + (frontmatter.publishedToGroups as undefined | string[]) || []; + const idResponse = await client .from("Content") .select("last_modified") @@ -97,6 +285,12 @@ export const publishNode = async ({ const lastModifiedDb = new Date( idResponse.data.last_modified + "Z", ).getTime(); + try { + await publishNodeRelations({ plugin, client, nodeId, myGroup, spaceId }); + } catch (error) { + // do not fail to publish node for that reason + console.error("Could not publish relations", error); + } const embeds = plugin.app.metadataCache.getFileCache(file)?.embeds ?? []; const attachments = embeds .map(({ link }) => { @@ -116,7 +310,7 @@ export const publishNode = async ({ ); const skipPublishAccess = - commonGroups.length > 0 && lastModified <= lastModifiedDb; + existingPublish.includes(myGroup) && lastModified <= lastModifiedDb; if (!skipPublishAccess) { const publishSpaceResponse = await client.from("SpaceAccess").upsert( @@ -210,7 +404,7 @@ export const publishNode = async ({ // do not fail on cleanup if (cleanupResult.error) console.error(cleanupResult.error); - if (commonGroups.length === 0) + if (!existingPublish.includes(myGroup)) await plugin.app.fileManager.processFrontMatter( file, (fm: Record) => { diff --git a/apps/obsidian/src/utils/registerCommands.ts b/apps/obsidian/src/utils/registerCommands.ts index 00accd707..394ede96b 100644 --- a/apps/obsidian/src/utils/registerCommands.ts +++ b/apps/obsidian/src/utils/registerCommands.ts @@ -8,12 +8,11 @@ import { createDiscourseNode } from "./createNode"; import { refreshAllImportedFiles } from "./importNodes"; import { VIEW_TYPE_MARKDOWN, VIEW_TYPE_TLDRAW_DG_PREVIEW } from "~/constants"; import { createCanvas } from "~/components/canvas/utils/tldraw"; -import { createOrUpdateDiscourseEmbedding } from "./syncDgNodesToSupabase"; +import { syncAllNodesAndRelations } from "./syncDgNodesToSupabase"; import { publishNode } from "./publishNode"; import { addRelationIfRequested } from "~/components/canvas/utils/relationJsonUtils"; import type { DiscourseNode } from "~/types"; - type ModifyNodeSubmitParams = { nodeType: DiscourseNode; title: string; @@ -234,7 +233,7 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => { return false; } if (!checking) { - void createOrUpdateDiscourseEmbedding(plugin) + void syncAllNodesAndRelations(plugin) .then(() => { new Notice("Discourse nodes synced successfully", 3000); }) diff --git a/apps/obsidian/src/utils/relationsStore.ts b/apps/obsidian/src/utils/relationsStore.ts index 6bb079adf..e859f3c8f 100644 --- a/apps/obsidian/src/utils/relationsStore.ts +++ b/apps/obsidian/src/utils/relationsStore.ts @@ -5,6 +5,8 @@ import { ensureNodeInstanceId } from "~/utils/nodeInstanceId"; import { checkAndCreateFolder } from "~/utils/file"; import { getVaultId } from "./supabaseContext"; import type { RelationInstance } from "~/types"; +import { QueryEngine } from "~/services/QueryEngine"; +import { publishNewRelation } from "./publishNode"; const RELATIONS_FILE_NAME = "relations.json"; const RELATIONS_FILE_VERSION = 1; @@ -113,7 +115,18 @@ export const addRelationNoCheck = async ( }; const data = await loadRelations(plugin); data.relations[id] = instance; + // save so it can be synced if needed await saveRelations(plugin, data); + try { + const published = await publishNewRelation(plugin, instance); + if (published) { + // save again with publication data + await saveRelations(plugin, data); + } + } catch (error) { + console.error(error); + // do not fail adding the relation; but we need a way to look at this later. + } return id; }; @@ -227,10 +240,13 @@ export const getNodeTypeIdForFile = async ( return typeof nodeTypeId === "string" ? nodeTypeId : null; }; -export const getFileForNodeInstanceId = async ( +export const getFileForNodeInstanceId = ( plugin: DiscourseGraphPlugin, nodeInstanceId: string, -): Promise => { +): TFile | null => { + const queryEngine = new QueryEngine(plugin.app); + if (queryEngine.functional()) + return queryEngine.getDiscourseNodeById(nodeInstanceId); const files = plugin.app.vault.getMarkdownFiles(); for (const file of files) { const cache = plugin.app.metadataCache.getFileCache(file); @@ -243,6 +259,33 @@ export const getFileForNodeInstanceId = async ( return null; }; +export const getFileForNodeInstanceIds = ( + plugin: DiscourseGraphPlugin, + nodeInstanceIds: Set, +): Record => { + const result: Record = {}; + if (nodeInstanceIds.size == 0) return result; + const queryEngine = new QueryEngine(plugin.app); + if (queryEngine.functional()) { + [...nodeInstanceIds.values()].map((nodeId) => { + const f = queryEngine.getDiscourseNodeById(nodeId); + if (f) result[nodeId] = f; + }); + } else { + // query engine not available, fallback + const files = plugin.app.vault.getMarkdownFiles(); + for (const file of files) { + const cache = plugin.app.metadataCache.getFileCache(file); + const id = (cache?.frontmatter as Record | undefined) + ?.nodeInstanceId as string | undefined; + if (id && nodeInstanceIds.has(id)) { + result[id] = file; + } + } + } + return result; +}; + /** * Find a relation instance by source, destination, and type. Returns the first match. */ diff --git a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts index 7a2ec59a5..589550659 100644 --- a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts +++ b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts @@ -403,41 +403,13 @@ const buildChangedNodesFromNodes = async ({ return changedNodes; }; -/** - * Get all discourse nodes that have changed compared to what's stored in Supabase. - * Detects what specifically changed: title, content, or new file - * - * Flow: - * 1. Collect all discourse nodes from vault - * 2. Query database for existing titles - * 3. Get last sync time for the space - * 4. For each node, detect what changed - * 5. Return only nodes that have changes - */ -const getChangedDiscourseNodes = async ({ - plugin, - supabaseClient, - context, -}: { - plugin: DiscourseGraphPlugin; - supabaseClient: DGSupabaseClient; - context: SupabaseContext; -}): Promise => { - const dgNodesInVault = await collectDiscourseNodesFromVault(plugin); - - return buildChangedNodesFromNodes({ - nodes: dgNodesInVault, - supabaseClient, - context, - }); -}; - -export const createOrUpdateDiscourseEmbedding = async ( +export const syncAllNodesAndRelations = async ( plugin: DiscourseGraphPlugin, supabaseContext?: SupabaseContext, + relationsOnly?: boolean, ): Promise => { try { - console.debug("Starting createOrUpdateDiscourseEmbedding"); + console.debug("Starting syncAllNodesAndRelations"); const context = supabaseContext ?? (await getSupabaseContext(plugin)); if (!context) { @@ -451,14 +423,18 @@ export const createOrUpdateDiscourseEmbedding = async ( } console.debug("Supabase client:", supabaseClient); - // Get all discourse nodes that have changed compared to what's stored in Supabase - const allNodeInstances = await getChangedDiscourseNodes({ - plugin, - supabaseClient, - context, - }); - console.log("allNodeInstances", allNodeInstances); - console.debug(`Found ${allNodeInstances.length} nodes to sync`); + const allNodes = await collectDiscourseNodesFromVault(plugin); + + const changedNodeInstances = relationsOnly + ? [] + : await buildChangedNodesFromNodes({ + nodes: allNodes, + supabaseClient, + context, + }); + + console.log("changedNodeInstances", changedNodeInstances); + console.debug(`Found ${changedNodeInstances.length} nodes to sync`); const accountLocalId = plugin.settings.accountLocalId; if (!accountLocalId) { @@ -466,7 +442,7 @@ export const createOrUpdateDiscourseEmbedding = async ( } await upsertNodesToSupabaseAsContentWithEmbeddings({ - obsidianNodes: allNodeInstances, + obsidianNodes: changedNodeInstances, supabaseClient, context, accountLocalId, @@ -474,19 +450,20 @@ export const createOrUpdateDiscourseEmbedding = async ( }); await convertDgToSupabaseConcepts({ - nodesSince: allNodeInstances, + nodesSince: changedNodeInstances, supabaseClient, context, accountLocalId, plugin, + allNodes, }); // When synced nodes are already published, ensure non-text assets are in storage. - await syncPublishedNodesAssets(plugin, allNodeInstances); + await syncPublishedNodesAssets(plugin, changedNodeInstances); console.debug("Sync completed successfully"); } catch (error) { - console.error("createOrUpdateDiscourseEmbedding: Process failed:", error); + console.error("syncAllNodesAndRelations: Process failed:", error); throw error; } }; @@ -497,12 +474,14 @@ const convertDgToSupabaseConcepts = async ({ context, accountLocalId, plugin, + allNodes, }: { nodesSince: ObsidianDiscourseNodeData[]; supabaseClient: DGSupabaseClient; context: SupabaseContext; accountLocalId: string; plugin: DiscourseGraphPlugin; + allNodes?: DiscourseNodeInVault[]; }): Promise => { const lastNodeSchemaSync = ( await getLastNodeSchemaSyncTime(supabaseClient, context.spaceId) @@ -516,7 +495,7 @@ const convertDgToSupabaseConcepts = async ({ const nodeTypes = plugin.settings.nodeTypes ?? []; const relationTypes = plugin.settings.relationTypes ?? []; const discourseRelations = plugin.settings.discourseRelations ?? []; - const allNodes = await collectDiscourseNodesFromVault(plugin); + allNodes = allNodes ?? (await collectDiscourseNodesFromVault(plugin)); const allNodesById = Object.fromEntries( allNodes.map((n) => [n.nodeInstanceId, n]), ); @@ -904,7 +883,7 @@ export const initializeSupabaseSync = async ( ); } - await createOrUpdateDiscourseEmbedding(plugin, context).catch((error) => { + await syncAllNodesAndRelations(plugin, context).catch((error) => { new Notice(`Initial sync failed: ${error}`); console.error("Initial sync failed:", error); }); diff --git a/apps/roam/src/utils/conceptConversion.ts b/apps/roam/src/utils/conceptConversion.ts index 15037fef6..8ee98dae4 100644 --- a/apps/roam/src/utils/conceptConversion.ts +++ b/apps/roam/src/utils/conceptConversion.ts @@ -237,6 +237,9 @@ const orderConceptsRec = ({ remainder: { [key: string]: LocalConceptDataInput }; processed: Set; }): Set => { + // Add to processed at the start to prevent cycles + processed.add(concept.source_local_id!); + const relatedConceptIds = relatedConcepts(concept); let missing: Set = new Set(); while (relatedConceptIds.length > 0) { @@ -259,7 +262,6 @@ const orderConceptsRec = ({ } } ordered.push(concept); - processed.add(concept.source_local_id!); delete remainder[concept.source_local_id!]; return missing; };