obsidianmd/obsidian-api

Bug: FileManager processFrontmatter failing to save data when used in a loop

qaptoR opened this issue · 3 comments

qaptoR commented

Steps to reproduce:

This has happened in multiple contexts for getting an array of TFiles
eg: using TFolder.children or like with the code example below, using the Dataview API to return an array from a specific query.

At first I thought the problem was that the TFiles in the original array that is returned were being swapped around, so that when looping over with (file of files) syntax there were misses and the solution of creating a second array to store the files after checking for validity seemed to work when using TFolder.children.

However, as you can see, though I'm using that fix in the snippet below, it turns out that it doesn't work.
The files definitely are getting processed (when you open the file the actual text is changed), but the frontmatter in the metadatacache is not updated with the new value.

Therefore, it's clearly not my initial suspicion that the array was the issue, but that the TFile is not being saved properly.
If I open the file in the editor make any sort of change to the text and then save manually, suddenly the metadatacache matches the text.

A simple reload of Obsidian does not refresh the metadatacache from this issue.

When I look in app.workspace.activeEditor from the console, there is a discrepency between the lastFrontmatter and the lasSavedData

processFMpng

What's interesting is that this problem only occurs on some of the array, something like 1/5th of the size, but possibly it becomes an issue once the array reaches 10-14 entries, it seems to happen very inconsistently depending on the list of files, sometimes affecting the same files (on repeated attempts of the same list) and sometimes only a few (but usually of the same set of affected files). yet there doesn't seem to be a clear pattern of what it is about the files that is making them affected in this way.


Callback for Command

export const updatePathsSeclectionQuery = async function (editor :Editor, view :MarkdownView) {
    
    const query :string = editor.getSelection()

    try {
        const updateArgs :GenericTwoInputArgs = await GenericTwoInputTextModal.Request(
            view.app, {
            fieldOne: "ARCH_IVE Path to Change",
            fieldTwo: "New ARCH_IVE Path",
        })

        const dv = getAPI()
        if (!dv) throw new Error("Could not get Dataview API");

        const results = await dv.tryQuery(query)
        
        const files = []

        for (const entry of results.values) {
            if (!('path' in entry)) continue

            const file = view.app.vault.getAbstractFileByPath(entry.path)
            if (!(file instanceof TFile) || file.extension != 'md') continue
            
            files.push(file)
        }

        for (const file of files) {
            await updateFile.bind(view)(file, updateArgs)
        }
    } catch (e) {
        console.log(e)
    }

}

function to process frontmatter

export const updateFile = async function (file :TFile, args :GenericTwoInputArgs) {
    
    if (!this.app.metadataCache.getFileCache(file)?.frontmatter?.['arch-ive-path']) {
        console.log("no path found")
        return
    }
    const newArchivePath = this.app.metadataCache.getFileCache(file)
    ?.frontmatter?.['arch-ive-path'].replace(args.fieldOne, args.fieldTwo)
    
    try {
        await this.app.fileManager.processFrontMatter(file, (fm :any) => {
            fm['arch-ive-path'] = newArchivePath
        })
        await tryHashNoCollision(file)
    } catch (e) {
        console.log(e)
    }
}
lishid commented

You're doing weird stuff to the internal API like file.saving = true (Don't do that! saving is there for a reason!). Plus lastFrontmatter and lastSavedData are also both internal data structures that you should not be using or relying on.

If you can find a case that is reproducible and purely using the public API, I can look into it, but otherwise there isn't much I can do.

qaptoR commented

a) sorry, I'm not actually relying on file.saving. That was a misplaced holdover from my just firing shots into the dark to try and figure out what could be the issue.

b) I'm also not relying on lastFrontmatter or lastSavedData. I only mentioned them because from what I could see those values were not the same and I expect that they ought to be, no? They were just a starting point for looking for a

c) as for a reproducible case, as I mentioned before, the issue exists whether I use a Dataview returned array of TFiles or just access TFolder.children.
The function above called updateFile is the culprit, so a minimum reproducible case is

for (const file of someTFolder.children) {
    await updateFile.bind(view)(file, {fieldOne: "some-field", fieldTwo: "some-new-field"})
}

I also forgot that updateFile calls tryHashNoCollision, so here is that as well

export async function tryHashNoCollision (file :TFile) {
    const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter
    if (!frontmatter?.['arch-ive-path'] || !frontmatter?.['arch-ive-title']) return

    const toHash = frontmatter['arch-ive-path'] + frontmatter['arch-ive-title']
    let i= 0;
    do {
        const basenameHash = getBasenameHash(toHash + i.toString(), 10)
        const newPath  = `${file.parent.path}/${basenameHash}.md`

        const lookup = this.app.vault.getAbstractFileByPath(newPath)
        if (lookup && lookup instanceof TFile) {
            i += 1; 
            continue;
        }

        await this.app.fileManager.renameFile(file, newPath)
        break;

    } while (true)
}
lishid commented

So you rename the file immediately after editing it? Is that what's causing your issue?

Can you come up with a minimally reproducible code sample that we can use to test?