Updated: March 17, 2026
The Notebook Navigator plugin has a multi-phase startup process that handles data synchronization and content generation. The startup behavior differs between cold boots (empty/cleared cache) and warm boots (existing cache).
A cold boot occurs when:
- The plugin is installed for the first time
- The IndexedDB cache doesn't exist or is cleared
- Database version markers are missing in vault-scoped localStorage
DB_CONTENT_VERSIONhas changed- A schema downgrade or incompatible migration requires a rebuild
Characteristics:
- Full database initialization required
- All files need content generation
- Database is either created new or cleared completely
A warm boot occurs when:
- Obsidian is restarted with the plugin already enabled
- The plugin is enabled after being disabled
- Database exists with valid schema and content versions
Characteristics:
- Database already exists with cached data
- Only changed files and files missing derived content need processing
- Metadata cache is populated asynchronously; metadata-dependent providers wait for
metadataCacheentries
Obsidian can restore open tabs/leaves from the saved workspace layout.
For Notebook Navigator this matters because:
- On warm boots, the navigator view is often created by layout restore (not by
activateView()). - When
calendarPlacementisright-sidebar, the calendar sidebar leaf is restored and managed independently of navigator leaves. - On first launch, the plugin explicitly activates the view in
HomepageController.handleWorkspaceReady()afterworkspace.onLayoutReady().
The plugin uses two version numbers to manage database state:
DB_SCHEMA_VERSION: Controls the IndexedDB structure
- Changes when database schema is modified (new indexes, stores, etc.)
- Used as the version passed to
indexedDB.open(...) - Upgrades run via
onupgradeneeded(stores/indexes/migrations); some upgrades clear legacy payloads to force a rebuild - Downgrades delete the database and rebuild
DB_CONTENT_VERSION: Controls the data format
- Changes when content structure or generation logic is modified
- Triggers data clearing but preserves database structure
- Examples: changing how previews are generated, tag extraction logic updates
Both versions are stored in localStorage to detect changes between sessions.
Version mismatches can result in a cold boot (empty MemoryFileCache + cleared stores) so providers regenerate content.
Schema downgrades and content version changes also set a pending rebuild notice that is consumed by StorageContext to
show progress.
Trigger: Obsidian calls Plugin.onload() when enabling the plugin
- Obsidian calls
Plugin.onload(). - Initialize vault-scoped localStorage (
localStorage.init) before any database work. - Register the plugin icon with Obsidian (
addIcon(...)). - Initialize IndexedDB early via
initializeDatabase(appId, ...).- Starts
db.init()(schema check +MemoryFileCachehydration) asynchronously before views mount. - Operation is idempotent to support rapid enable/disable cycles.
- Starts preview text warmup (
startPreviewTextWarmup) after init.
- Starts
- Load settings from
data.jsonand run migrations.- Sanitize keyboard shortcuts and migrate legacy fields.
- Apply default date/time formats and migrate folder note template settings.
- Sync local mirrors, load per-device UX preferences, and normalize settings.
- Resolve sync-mode local mirrors from vault-scoped localStorage.
- Load UX preferences from vault-scoped localStorage.
- Normalize tag and property settings.
- Handle first-launch setup when no saved data exists.
- Clear plugin localStorage keys (preserving IndexedDB version markers).
- Re-seed per-device localStorage mirrors for sync-mode settings and UX preferences.
- Expand the root folder when
showRootFolderis enabled. - Persist the current localStorage version (
LOCALSTORAGE_VERSION).
- Initialize recent data and UX tracking.
RecentDataManagerloads persisted recent notes and icons.RecentNotesServicestarts recording file-open history.
- Construct core services and controllers:
WorkspaceCoordinatorandHomepageControllermanage view activation and homepage flow.MetadataService,TagOperations,TagTreeService,PropertyTreeService, andCommandQueueService.FileSystemOperationswired with tag tree, property tree, and visibility preferences.OmnisearchService,NotebookNavigatorAPI, andReleaseCheckService.ExternalIconProviderControllerinitializes icon providers and syncs settings.
- Register view, commands, settings tab, and workspace integrations.
- Register both
NOTEBOOK_NAVIGATOR_VIEW(NotebookNavigatorView) andNOTEBOOK_NAVIGATOR_CALENDAR_VIEW(NotebookNavigatorCalendarView). registerNavigatorCommandswires command palette entries.registerWorkspaceEventsadds editor context menu actions, the ribbon icon, recent-note tracking, and rename/delete handlers.
- Wait for
workspace.onLayoutReady().
HomepageController.handleWorkspaceReady()activates the view on first launch and opens the configured homepage (if set).- On first launch, the Welcome modal is opened after the workspace is ready.
- Triggers Style Settings parsing, version notice checks, and optional release polling.
applyCalendarPlacementView({ force: true, reveal: false })syncs the calendar right-sidebar leaf with the effective calendar placement and detaches restored right-sidebar calendar leaves when the calendar feature is disabled.
Trigger: Obsidian restores leaves from workspace layout, the plugin calls activateView() (commands/ribbon/menu),
or calendar placement changes run after layout/settings updates.
activateView() delegates to WorkspaceCoordinator.activateNavigatorView():
- Reveals the first existing navigator leaf (when one exists).
- Otherwise creates a new left-sidebar leaf via
workspace.getLeftLeaf(false)andleaf.setViewState({ type: NOTEBOOK_NAVIGATOR_VIEW, active: true }).
- Obsidian calls
NotebookNavigatorView.onOpen()when the view is created/restored. - React app mounts with the following context providers:
SettingsProvider(settings state and update actions)UXPreferencesProvider(dual-pane and search preferences synced with the plugin)RecentDataProvider(recent notes and icon lists)ServicesProvider(Obsidian app, services, and platform flags)ShortcutsProvider(pinned shortcut hydration and operations)StorageProvider(IndexedDB access and content pipeline)ExpansionProvider(expanded folders, tags, and properties)SelectionProvider(selected items plus rename listeners from the plugin)UIStateProvider(pane focus and layout mode)
NotebookNavigatorContainerrenders a skeleton untilStorageContext.isStorageReadyis true.NotebookNavigatorView.onOpen()adds platform classes and (on Android) applies font scaling compensation before React renders:- Always adds
notebook-navigator. - Adds
notebook-navigator-mobileand platform classes on mobile (notebook-navigator-android,notebook-navigator-ios). - Adds
notebook-navigator-obsidian-1-11-plus-*whenrequireApiVersion('1.11.0')passes.
- Always adds
- Pane chrome uses headers on all platforms and toolbars on mobile:
NavigationPaneHeaderandListPaneHeaderrender in pane chrome above the scrollers.- Android mobile renders
NavigationToolbar/ListToolbarat the top. - On iOS with Obsidian 1.11+ and floating toolbars enabled, the toolbars render inside the pane; otherwise they render in the bottom toolbar container.
applyCalendarPlacementView()evaluatessettings.calendarPlacementafter layout readiness and on settings updates.- When the effective placement is
right-sidebarandcalendarEnabledis true, it callsWorkspaceCoordinator.ensureCalendarViewInRightSidebar(...). - Obsidian calls
NotebookNavigatorCalendarView.onOpen()when the calendar leaf is created/restored. - React app mounts with:
SettingsProviderServicesProviderCalendarRightSidebar
CalendarRightSidebarrendersCalendarwithweeksToShowOverride={6}and forwards date-filter actions to the navigator view.- When placement changes away from
right-sidebar, or the feature is disabled,WorkspaceCoordinator.detachCalendarViewLeaves()removes calendar leaves. Restored calendar leaves also detach themselves on open when the feature is disabled.
Trigger: Database initialization starts in Phase 1 and StorageContext awaits readiness
- StorageContext retrieves the shared database instance.
- Calls
getDBInstance()and awaitsdb.init()(useIndexedDBReady). - Logs and keeps the flag false if initialization fails.
- Calls
IndexedDBStorage.init()handles schema and content version checks.- Reads stored versions from vault-scoped localStorage.
- Runs schema upgrades via
onupgradeneededand deletes the database on schema downgrades or open errors. - Opens the database with
skipCacheLoad=trueand clears stores when version markers are missing,DB_CONTENT_VERSIONchanges, or a schema downgrade is detected. - Sets a pending rebuild notice when a schema downgrade or
DB_CONTENT_VERSIONchange is detected (not when keys are missing). - Persists the current versions back to localStorage.
- Database opening and cache hydration:
- Rebuilds start with an empty
MemoryFileCache. - Warm boots load all records into the cache for synchronous access.
- IndexedDB stores:
keyvaluepairs: file records (FileData) excluding preview text and blob payloadsfilePreviews: preview text strings keyed by path (loaded on demand / warmup)featureImageBlobs: thumbnail blobs keyed by path (cached in an in-memory LRU)
- Rebuilds start with an empty
- StorageContext creates a
ContentProviderRegistryonce and registers:MarkdownPipelineContentProvider(markdownPipeline)FeatureImageContentProvider(fileThumbnails)MetadataContentProvider(metadata)TagContentProvider(tags)
- With
isIndexedDBReadytrue, Phase 4 processing can begin.
- Database is deleted or cleared during initialization.
MemoryFileCachestarts empty because no cached data exists.- Providers remain idle until Phase 4 queues work.
- Continue to Phase 4 with an empty database snapshot.
- Database opens without recreation.
- All records load into
MemoryFileCache. - Providers have immediate access to cached content.
- Continue to Phase 4 with populated data.
Trigger: Database initialization completes (from Phase 3)
This phase handles the initial synchronization between the vault and the database, then ensures metadata is ready for tag extraction and markdown pipeline processing:
- Gather indexable files with
getIndexableFiles()fromuseStorageFileQueries(getFilteredMarkdownAndPdfFiles(..., { showHiddenItems: true })). - Calculate diffs through
calculateFileDiff().- Cold boot: all files appear as new (database cache is empty)
- Warm boot: compare against cached data to find new/modified files
- Apply the diff:
- Remove deleted paths via
removeFilesFromCache(toRemove). - Upsert new/modified files via
recordFileChanges([...toAdd, ...toUpdate], cachedFiles, pendingRenameData).- New files use
createDefaultFileData(provider processed mtimes set to0; markdown records initializetags,wordCount, andmetadatatonull;previewStatusdefaults tounprocessedfor markdown andnonefor non-markdown). - Modified files patch the stored
mtimewithout clearing existing provider outputs. Providers compare their processed mtime fields (markdownPipelineMtime,tagsMtime,metadataMtime,fileThumbnailsMtime) againstfile.stat.mtimeto detect stale content.
- New files use
- Remove deleted paths via
- Rebuild tag and property trees via
rebuildTagTree()andrebuildPropertyTree(). - Mark storage as ready (
setIsStorageReady(true)and the internal Notebook Navigator API readiness bridge). - Queue content generation:
- Determine metadata-dependent provider types with
getMetadataDependentTypes(settings):- Always includes
markdownPipeline(word count, task counters, preview/property/feature image pipelines). - Includes
tagswhenshowTagsis enabled. - Includes
metadatawhen frontmatter metadata is enabled or hidden-file frontmatter rules are active.
- Always includes
queueMetadataContentWhenReady(markdownFiles, metadataDependentTypes, settings)filters to files needing work, waits for Obsidian's metadata cache (resolvedandchanged), then queues providers inContentProviderRegistry.- When
showFeatureImageis enabled, queue thefileThumbnailsprovider for PDFs (filtered byfilterPdfFilesRequiringThumbnails).
- Determine metadata-dependent provider types with
- Vault events debounce a cache rebuild.
- Diff processing is deferred with a zero-delay
setTimeoutand uses the samecalculateFileDiff+recordFileChanges/removeFilesFromCacheflow. - Renames seed
MemoryFileCachewith the old record, move preview/feature-image artifacts, then schedule a diff to reconcile mtimes. - Metadata cache
changedevents can force regeneration (markFilesForRegeneration) so providers re-run even when file mtimes do not change (processed mtimes reset to0without clearing existing outputs). - When files are removed and tags are enabled, tag tree rebuild is scheduled.
- When files are removed and properties are enabled, property tree rebuild is scheduled (flushed after removals).
The metadata cache gating is managed by queueMetadataContentWhenReady() (using metadataCache.getFileCache(file) plus
metadataCache resolved/changed events) and queues metadata-dependent providers once cache entries exist:
graph TD
Start["From Phase 3:<br/>Database & Providers ready"] --> A["processExistingCache (initial)"]
A --> B["getIndexableFiles<br/>(markdown + PDFs)"]
B --> C[calculateFileDiff]
C --> D["removeFilesFromCache (toRemove)"]
C --> E["recordFileChanges (toAdd/toUpdate)"]
D --> F[rebuildTagTree + rebuildPropertyTree]
E --> F
F --> G[Mark storage ready<br/>notify API]
G --> H["queueMetadataContentWhenReady<br/>(filters to files needing work)"]
H --> I["metadataCache gating<br/>(getFileCache, resolved/changed)"]
I --> J["Queue markdownPipeline/tags/metadata providers"]
G --> K{Show feature images?}
K -->|Yes| L[Queue fileThumbnails for PDFs]
K -->|No| M[Skip PDF thumbnails]
J --> N[Enter Phase 5]
L --> N
M --> N
Purpose: Remove orphaned metadata for folders, tags, and files deleted or renamed outside of Obsidian. Metadata cleanup is performed manually from settings.
When It's Needed:
- Files/folders deleted directly from file system
- Files/folders renamed outside of Obsidian
- Vault synchronized with missing or renamed files
- Files renamed or deleted by external tools or scripts
- After major vault reorganization outside Obsidian
- Sync conflicts that resulted in orphaned metadata
How to Run: Open Settings → Notebook Navigator → Advanced → Clean up metadata
What Gets Cleaned:
- Folder colors, icons, sort settings, and background colors for deleted/renamed folders
- Tag colors, icons, sort settings, and background colors for removed tags
- Property colors, icons, and sort overrides for property nodes no longer present
- Pinned notes that no longer exist
- Custom appearances for non-existent items
- Navigation separators for missing targets
Technical Details: The cleanup process uses validators to compare stored metadata against the current vault state.
See MetadataService.cleanupAllMetadata() and MetadataService.getCleanupSummary() for implementation.
Trigger: Files queued by ContentProviderRegistry (from Phase 4)
Content is generated asynchronously in the background by the ContentProviderRegistry and individual providers:
-
File Detection: Each provider checks if files need processing
- TagContentProvider (markdown):
tags === nullortagsMtime !== file.stat.mtime(only whenshowTagsis enabled)- When tags are forced to regenerate (
tagsMtimereset to0), the provider retries before replacing an existing non-empty tag list with an empty result.
- When tags are forced to regenerate (
- MarkdownPipelineContentProvider (markdown): runs when any of the following are true:
markdownPipelineMtime !== file.stat.mtimewordCount === nulltaskTotal === nullortaskUnfinished === nullshowFilePreviewis enabled andpreviewStatus === 'unprocessed'showFeatureImageis enabled and (featureImageKey === nullorfeatureImageStatus === 'unprocessed')- Property pills are configured and
properties === null
- FeatureImageContentProvider (PDFs):
fileThumbnailsMtime !== file.stat.mtime,featureImageStatus === 'unprocessed',featureImageKey === null, orfeatureImageKeymismatches the expected PDF key - MetadataContentProvider (markdown):
metadata === null,metadataMtime !== file.stat.mtime, or hidden-state tracking requires an update
- TagContentProvider (markdown):
-
Queue Management: Files are queued based on enabled settings
- ContentProviderRegistry manages the queue
- Processes files in batches and yields between batches
- Uses deferred scheduling for background processing
queueMetadataContentWhenReady()delays metadata-dependent providers (markdown pipeline, tags, metadata) until Obsidian's metadata cache has entries
-
Processing: Each provider processes files independently
- TagContentProvider: Extracts tags from Obsidian's metadata cache (
getAllTags(metadata)) - MarkdownPipelineContentProvider: Uses metadata cache for frontmatter/offsets, reads markdown content when needed, runs preview/word count/task/property/feature image processors
- FeatureImageContentProvider: Generates thumbnails for non-markdown files (PDF cover thumbnails)
- MetadataContentProvider: Extracts configured frontmatter fields and hidden state from Obsidian's metadata cache
- TagContentProvider: Extracts tags from Obsidian's metadata cache (
-
Database Updates: Results stored in IndexedDB
- Providers apply updates via
IndexedDBStorage(main records + preview store + feature image blob store) - Database fires content change events
- Providers apply updates via
-
Memory Sync: MemoryFileCache automatically synced with IndexedDB changes
-
UI Updates: StorageContext listens for database changes
- Tag changes trigger tag tree rebuild (buildTagTreeFromDatabase)
- Property-related changes trigger property tree rebuild (
buildPropertyTreeFromDatabase) - Components re-render with new content via React context
The cache rebuild progress notice tracks background provider work during a full cache rebuild and other bulk regeneration triggers.
- Manual rebuilds (and settings-driven bulk regenerations) persist a vault-scoped localStorage marker (
STORAGE_KEYS.cacheRebuildNoticeKey) with the initial work total used for the progress bar. - Schema downgrades,
DB_CONTENT_VERSIONchanges, and IndexedDB open errors set an in-memorypendingRebuildNoticeflag inIndexedDBStoragethat is consumed on initial load to start the notice. - On the next startup, StorageContext restores the notice from the localStorage marker after storage is marked ready and uses current settings to determine which content types to track.
- The marker is cleared when the notice detects that all tracked content types have no pending work.
StorageContext and content providers defer heavy work so it does not run directly inside Obsidian event handlers:
- Vault diff processing is deferred with
setTimeout(..., 0)so bursts of vault events can coalesce. - Metadata cache gating batches queue flushes with
setTimeout(..., 0). - Content providers yield between batches via
requestAnimationFrame(fallback:setTimeout(..., 0)).
The plugin uses debouncers in a few specific places where Obsidian emits bursty events:
- Vault syncing uses Obsidian
debounce(..., TIMEOUTS.FILE_OPERATION_DELAY)to collapse create/delete bursts into one diff. - Tag tree rebuilds use Obsidian
debounce(..., TIMEOUTS.DEBOUNCE_TAG_TREE)because tag updates can arrive in batches. - Property tree rebuilds use the same
TIMEOUTS.DEBOUNCE_TAG_TREEdebouncer to batch rapid updates. - Content providers use
TIMEOUTS.DEBOUNCE_CONTENT(asetTimeout) to coalesce queueing before starting a processing batch.
Trigger: Obsidian calls Plugin.onunload() when disabling the plugin
Plugin.onunload()callsinitiateShutdown().initiateShutdown()sets theisUnloadingflag and flushes shutdown-critical work:- Flush pending recent-data persists.
- Clear queued command operations.
- Stop content processing in mounted navigator and calendar leaves.
- Call
shutdownDatabase()to close IndexedDB and clear in-memory caches.
preferencesController.dispose()then disposesRecentDataManagerand clears recent-data / UX listeners.- Clear listener maps to avoid callbacks during teardown:
- Settings update listeners
- File rename listeners
- Update notice listeners
- Dispose long-lived services/controllers and clear remaining references:
ExternalIconProviderController.dispose()releases icon provider hooks.MetadataService.dispose()tears down metadata watchers.- Remaining service refs are nulled after cleanup.
- Remove the ribbon icon element.
Trigger: ItemView.onClose() when a navigator or calendar leaf is destroyed
NotebookNavigatorView.onClose()removes CSS classes from the container:- notebook-navigator
- notebook-navigator-mobile (if applicable)
- notebook-navigator-android / notebook-navigator-ios (if applicable)
- notebook-navigator-obsidian-1-11-plus-android / notebook-navigator-obsidian-1-11-plus-ios (if applicable)
NotebookNavigatorView.onClose()unmounts the React root:- Call root.unmount()
- Set root to null
- Clear the container element
NotebookNavigatorCalendarView.onClose()unregisters the settings listener, unmounts the React root, and tears down the view container classes.- Storage subtree cleanup happens in two layers:
NotebookNavigatorView.stopContentProcessing()reachesStorageContext.stopAllProcessing(), which marks storage stopped, cancels debouncers/timeouts, detaches vault and metadata listeners, and clears pending metadata waits.useInitializeContentProviderRegistry()stops provider queues and clears deferred sync timers when the storage subtree unmounts.
- Clear Ownership: Plugin owns database lifecycle, not React components
- Processing Before Shutdown: Always stop content providers before closing database
- Idempotent Operations: Both initializeDatabase and shutdownDatabase are safe to call multiple times
- Prevent Late Operations: isUnloading flag prevents new operations during shutdown
- Clean Separation: Database lifecycle is separate from view lifecycle