3 KiB
ADR 001: UI Loading & Refresh Behavior
Status
Accepted
Context
When users navigate rapidly between notes, or when Obsidian's metadata cache is delayed (e.g. creating a new file), multiple asynchronous requests to render or update the Coalesce UI can fire concurrently for the same markdown view leaf.
Previously, the plugin handled concurrent requests via two aggressive locks:
processingFiles(per-file strict drop): Immediately dropped subsequent update calls for a given file.processingViews(per-view strict drop): Immediately dropped subsequent update calls for a specific active view pane.
However, this led to "stuck" UI states where rendering requests for a newer note were simply dropped because the system was still blocking from a rapidly preceding click. Additionally, PluginEvents.ts was performing its own un-tracked DOM injections that bypassed the internal state tracking dictionary (lastFilePathByViewId), causing the view to permanently stick to the original note since it falsely assumed subsequent notes in the same leaf pane were just redundant updates for the original file.
Decision
- Centralized Update Invocation (Removal of event-bypass injections): Both
file:openedandlayout:changedinjections insidePluginEvents.tswere stripped out and replaced with standard delegations toPluginViewInitializer'supdateForFile()method. This ensureslastFilePathByViewIdcorrectly tracks exactly what node path was last requested and painted per view. externalForceRefreshFlag: Introduced an override flag into the view initializer. The 100ms fallback timeout, which is triggered by theactive-leaf-changeevent, now uses thisexternalForceRefreshflag to forcefully bypass the1000msdebounce ofshouldProcessFile(). This secures repopulation of empty backlinks that initially failed because the metadata cache hadn't fully hydrated upon note entry.- Promise Queuing & Validation replacing Strict Dropping locks: The aggressive
processingFilesearly return lock was completely removed.processingViewsnow functions as an asynchronous wait queue inside the matching views iterator (await processingViews.get(viewId)). Instead of instantly destroying incoming render requests on lock presence, it waits patiently for the active request to complete, and then dynamically verifiesif (view.file.path !== filePath)before lifting a finger.- If the user clicked to a new note while waiting, the obsolete request cleanly aborts.
- If the user is currently staring at the note they originally requested, it securely renders it as the final sequence layer.
Consequences
- Positive: UI updates no longer permanently drop and lock up during rapid note switching.
- Positive: Fast note creations guarantee that delayed metadata cache population reliably causes the Coalesce view to render upon cache hydration, instead of being blocked by hard debounces.
- Negative: Slightly increased complexity involving asynchronous promise queueing and mid-stream promise cancellation validation inside
updateForFile.