From d8578b2db0fa42a716fdecc7107119c0cccb1d10 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 10 Apr 2026 09:45:28 -0400 Subject: [PATCH 1/3] Do a sync on publish --- apps/obsidian/src/utils/publishNode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/obsidian/src/utils/publishNode.ts b/apps/obsidian/src/utils/publishNode.ts index d50ca3089..91cb6a339 100644 --- a/apps/obsidian/src/utils/publishNode.ts +++ b/apps/obsidian/src/utils/publishNode.ts @@ -260,6 +260,7 @@ export const publishNode = async ({ if (!client) throw new Error("Cannot get client"); const myGroups = new Set(await getAvailableGroupIds(client)); if (myGroups.size === 0) throw new Error("Cannot get group"); + await syncAllNodesAndRelations(plugin); const existingPublish = (frontmatter.publishedToGroups as undefined | string[]) || []; const commonGroups = existingPublish.filter((g) => myGroups.has(g)); From 3fec3afe0815a68dbca97ac7f66728971ab78431 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 10 Apr 2026 13:33:50 -0400 Subject: [PATCH 2/3] break recursion --- apps/obsidian/src/utils/publishNode.ts | 17 ++++++++++++++--- .../obsidian/src/utils/syncDgNodesToSupabase.ts | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/obsidian/src/utils/publishNode.ts b/apps/obsidian/src/utils/publishNode.ts index 91cb6a339..380790aa9 100644 --- a/apps/obsidian/src/utils/publishNode.ts +++ b/apps/obsidian/src/utils/publishNode.ts @@ -251,22 +251,30 @@ export const publishNode = async ({ plugin, file, frontmatter, + republish, }: { plugin: DiscourseGraphPlugin; file: TFile; frontmatter: FrontMatterCache; + republish?: boolean; }): Promise => { const client = await getLoggedInClient(plugin); if (!client) throw new Error("Cannot get client"); const myGroups = new Set(await getAvailableGroupIds(client)); if (myGroups.size === 0) throw new Error("Cannot get group"); - await syncAllNodesAndRelations(plugin); const existingPublish = (frontmatter.publishedToGroups as undefined | string[]) || []; + if (!republish) await syncAllNodesAndRelations(plugin); 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 }); + return await publishNodeToGroup({ + plugin, + file, + frontmatter, + myGroup, + republish, + }); }; export const ensurePublishedRelationsAccuracy = async ({ @@ -414,11 +422,13 @@ export const publishNodeToGroup = async ({ file, frontmatter, myGroup, + republish, }: { plugin: DiscourseGraphPlugin; file: TFile; frontmatter: FrontMatterCache; myGroup: string; + republish?: boolean; }): Promise => { const nodeId = frontmatter.nodeInstanceId as string | undefined; if (!nodeId) throw new Error("Please sync the node first"); @@ -465,7 +475,8 @@ export const publishNodeToGroup = async ({ ); const skipPublishAccess = - existingPublish.includes(myGroup) && lastModified <= lastModifiedDb; + republish || + (existingPublish.includes(myGroup) && lastModified <= lastModifiedDb); if (!skipPublishAccess) { const publishSpaceResponse = await client.from("SpaceAccess").upsert( diff --git a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts index 252c32b0c..adbd4bc1b 100644 --- a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts +++ b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts @@ -598,6 +598,7 @@ const syncPublishedNodesAssets = async ( plugin, file: node.file, frontmatter: node.frontmatter as FrontMatterCache, + republish: true, }); } catch (error) { console.error( From 801c1f43f4f395863fd3450b2dcce842744e3cbe Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 12 Apr 2026 12:04:32 -0400 Subject: [PATCH 3/3] isolate asset syncing, to avoid syncing twice --- apps/obsidian/src/utils/publishNode.ts | 76 ++--------- .../src/utils/syncDgNodesToSupabase.ts | 128 +++++++++++++++--- 2 files changed, 125 insertions(+), 79 deletions(-) diff --git a/apps/obsidian/src/utils/publishNode.ts b/apps/obsidian/src/utils/publishNode.ts index 380790aa9..a002a8ffb 100644 --- a/apps/obsidian/src/utils/publishNode.ts +++ b/apps/obsidian/src/utils/publishNode.ts @@ -1,9 +1,6 @@ import type { FrontMatterCache, TFile } from "obsidian"; -import { Notice } from "obsidian"; 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 type { DGSupabaseClient } from "@repo/database/lib/client"; import { getRelationsForNodeInstanceId, @@ -15,7 +12,10 @@ import { } from "./relationsStore"; import type { RelationInstance } from "~/types"; import { getAvailableGroupIds } from "./importNodes"; -import { syncAllNodesAndRelations } from "./syncDgNodesToSupabase"; +import { + syncAllNodesAndRelations, + syncPublishedNodeAssets, +} from "./syncDgNodesToSupabase"; import type { DiscourseNodeInVault } from "./getDiscourseNodes"; import type { SupabaseContext } from "./supabaseContext"; import type { TablesInsert } from "@repo/database/dbTypes"; @@ -518,65 +518,15 @@ export const publishNodeToGroup = async ({ }); } } - - // Always sync non-text assets when node is published to this group - const existingFiles: string[] = []; - const existingReferencesReq = await client - .from("my_file_references") - .select("*") - .eq("space_id", spaceId) - .eq("source_local_id", nodeId); - if (existingReferencesReq.error) { - console.error(existingReferencesReq.error); - return; - } - const existingReferencesByPath = Object.fromEntries( - existingReferencesReq.data.map((ref) => [ref.filepath, ref]), - ); - - for (const attachment of attachments) { - const mimetype = mime.lookup(attachment.path) || "application/octet-stream"; - if (mimetype.startsWith("text/")) continue; - // Do not use standard upload for large files - if (attachment.stat.size >= 6 * 1024 * 1024) { - new Notice( - `Asset file ${attachment.path} is larger than 6Mb and will not be uploaded`, - ); - continue; - } - existingFiles.push(attachment.path); - const existingRef = existingReferencesByPath[attachment.path]; - if ( - !existingRef || - new Date(existingRef.last_modified + "Z").valueOf() < - attachment.stat.mtime - ) { - const content = await plugin.app.vault.readBinary(attachment); - await addFile({ - client, - spaceId, - sourceLocalId: nodeId, - fname: attachment.path, - mimetype, - created: new Date(attachment.stat.ctime), - lastModified: new Date(attachment.stat.mtime), - content, - }); - } - } - let cleanupCommand = client - .from("FileReference") - .delete() - .eq("space_id", spaceId) - .eq("source_local_id", nodeId); - if (existingFiles.length) - cleanupCommand = cleanupCommand.notIn("filepath", [ - ...new Set(existingFiles), - ]); - const cleanupResult = await cleanupCommand; - // do not fail on cleanup - if (cleanupResult.error) console.error(cleanupResult.error); - + if (!republish) + await syncPublishedNodeAssets({ + plugin, + client, + nodeId, + spaceId, + file, + attachments, + }); if (!existingPublish.includes(myGroup)) await plugin.app.fileManager.processFrontMatter( file, diff --git a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts index adbd4bc1b..0f4cd03d3 100644 --- a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts +++ b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { FrontMatterCache, Notice, TFile } from "obsidian"; +import { Notice, TFile } from "obsidian"; +import { addFile } from "@repo/database/lib/files"; +import mime from "mime-types"; import { ensureNodeInstanceId } from "~/utils/nodeInstanceId"; import type { DGSupabaseClient } from "@repo/database/lib/client"; import type { Json } from "@repo/database/dbTypes"; @@ -9,7 +11,7 @@ import { type SupabaseContext, } from "./supabaseContext"; import { default as DiscourseGraphPlugin } from "~/index"; -import { publishNode, ensurePublishedRelationsAccuracy } from "./publishNode"; +import { ensurePublishedRelationsAccuracy } from "./publishNode"; import { upsertNodesToSupabaseAsContentWithEmbeddings } from "./upsertNodesAsContentWithEmbeddings"; import { orderConceptsByDependency, @@ -579,6 +581,92 @@ const convertDgToSupabaseConcepts = async ({ } }; +export const syncPublishedNodeAssets = async ({ + plugin, + client, + nodeId, + spaceId, + file, + attachments, +}: { + plugin: DiscourseGraphPlugin; + client: DGSupabaseClient; + nodeId: string; + spaceId: number; + file: TFile; + attachments?: TFile[]; +}) => { + if (attachments === undefined) { + const embeds = plugin.app.metadataCache.getFileCache(file)?.embeds ?? []; + attachments = embeds + .map(({ link }) => { + const attachment = plugin.app.metadataCache.getFirstLinkpathDest( + link, + file.path, + ); + return attachment; + }) + .filter((a) => !!a); + } + // Always sync non-text assets when node is published to this group + const existingFiles: string[] = []; + const existingReferencesReq = await client + .from("my_file_references") + .select("*") + .eq("space_id", spaceId) + .eq("source_local_id", nodeId); + if (existingReferencesReq.error) { + console.error(existingReferencesReq.error); + return; + } + const existingReferencesByPath = Object.fromEntries( + existingReferencesReq.data.map((ref) => [ref.filepath, ref]), + ); + + for (const attachment of attachments) { + const mimetype = mime.lookup(attachment.path) || "application/octet-stream"; + if (mimetype.startsWith("text/")) continue; + // Do not use standard upload for large files + if (attachment.stat.size >= 6 * 1024 * 1024) { + new Notice( + `Asset file ${attachment.path} is larger than 6Mb and will not be uploaded`, + ); + continue; + } + existingFiles.push(attachment.path); + const existingRef = existingReferencesByPath[attachment.path]; + if ( + !existingRef || + new Date(existingRef.last_modified + "Z").valueOf() < + attachment.stat.mtime + ) { + const content = await plugin.app.vault.readBinary(attachment); + await addFile({ + client, + spaceId, + sourceLocalId: nodeId, + fname: attachment.path, + mimetype, + created: new Date(attachment.stat.ctime), + lastModified: new Date(attachment.stat.mtime), + content, + }); + } + } + let cleanupCommand = client + .from("FileReference") + .delete() + .eq("space_id", spaceId) + .eq("source_local_id", nodeId); + if (existingFiles.length) + cleanupCommand = cleanupCommand.notIn("filepath", [ + ...new Set(existingFiles), + ]); + const cleanupResult = await cleanupCommand; + // do not fail on cleanup + if (cleanupResult.error) console.error(cleanupResult.error); +}; + /** * For nodes that are already published, ensure non-text assets are pushed to * storage. Called after content sync so new embeds (e.g. images) get uploaded. @@ -587,25 +675,26 @@ const syncPublishedNodesAssets = async ( plugin: DiscourseGraphPlugin, nodes: ObsidianDiscourseNodeData[], ): Promise => { + 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 published = nodes.filter( (n) => ((n.frontmatter.publishedToGroups as string[] | undefined)?.length ?? 0) > 0, ); for (const node of published) { - try { - await publishNode({ - plugin, - file: node.file, - frontmatter: node.frontmatter as FrontMatterCache, - republish: true, - }); - } catch (error) { - console.error( - `Failed to sync published node assets for ${node.file.path}:`, - error, - ); - } + const nodeId = node.frontmatter.nodeInstanceId as string | undefined; + if (!nodeId) throw new Error("Please sync the node first"); + await syncPublishedNodeAssets({ + plugin, + client, + nodeId, + spaceId, + file: node.file, + }); } }; @@ -652,7 +741,14 @@ const syncChangedNodesToSupabase = async ({ // When file changes affect an already-published node, ensure new non-text // assets (e.g. images) are pushed to storage. - await syncPublishedNodesAssets(plugin, changedNodes); + try { + await syncPublishedNodesAssets(plugin, changedNodes); + } catch (error) { + // console.error( + // `Failed to sync published node assets for ${node.file.path}:`, + // error, + // ); + } }; /**