feat: vault timekeep registry

This commit is contained in:
Jacobtread 2026-03-28 16:32:28 +13:00
parent 87de57bde0
commit b3dc28a7e8
6 changed files with 290 additions and 0 deletions

28
package-lock.json generated
View file

@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"moment": "^2.30.1",
"p-limit": "^7.3.0",
"pdfmake": "^0.3.7",
"uuid": "^11.0.5",
"zod": "^4.3.6"
@ -2210,6 +2211,21 @@
"@oxlint-tsgolint/win32-x64": "0.17.4"
}
},
"node_modules/p-limit": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz",
"integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==",
"license": "MIT",
"dependencies": {
"yocto-queue": "^1.2.1"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@ -2736,6 +2752,18 @@
"node": ">=12.0.0"
}
},
"node_modules/yocto-queue": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
"integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",

View file

@ -19,6 +19,7 @@
},
"dependencies": {
"moment": "^2.30.1",
"p-limit": "^7.3.0",
"pdfmake": "^0.3.7",
"uuid": "^11.0.5",
"zod": "^4.3.6"

View file

@ -24,6 +24,7 @@ import { load, replaceTimekeepCodeblock, extractTimekeepCodeblocks } from "@/tim
import { stopAllTimekeeps } from "./commands/stopAllTimekeeps";
import { stopFileTimekeeps } from "./commands/stopFileTimekeeps";
import { CustomOutputFormat } from "./output";
import { TimekeepRegistry } from "./service/registry";
import { Timekeep, TimeEntry } from "./timekeep/schema";
import { TimekeepLocatorModal } from "./views/timekeep-locator-modal";
import { TimekeepMarkdownView } from "./views/timekeep-markdown-view";
@ -48,6 +49,9 @@ export default class TimekeepPlugin extends Plugin {
stopAllTimekeeps: (vault: Vault, currentTime: Moment) => Promise<number>;
stopFileTimekeeps: (vault: Vault, file: TFile, currentTime: Moment) => Promise<number>;
/** Registry of all timekeeps within the vault */
registry: TimekeepRegistry | null = null;
constructor(app: ObsidianApp, manifest: PluginManifest) {
super(app, manifest);
@ -90,6 +94,19 @@ export default class TimekeepPlugin extends Plugin {
this.addSettingTab(new TimekeepSettingsTab(this.app, this));
this.app.workspace.onLayoutReady(() => {
const settings = this.settingsStore.getState();
if (!settings.registryEnabled) {
return;
}
// Initialize the registry (After the layout is ready and the vault is loaded)
this.registry = new TimekeepRegistry(this.app.vault);
this.registry.concurrencyLimit = settings.registryConcurrencyLimit;
this.registry.onload();
});
this.registerMarkdownCodeBlockProcessor(
"timekeep",
(source: string, el: HTMLElement, context: MarkdownPostProcessorContext) => {

194
src/service/registry.ts Normal file
View file

@ -0,0 +1,194 @@
import { EventRef, TAbstractFile, TFile, Vault } from "obsidian";
import { limitFunction } from "p-limit";
import { createStore, Store } from "@/store";
import {
extractTimekeepCodeblocksWithPosition,
type TimekeepWithPosition,
} from "@/timekeep/parser";
type TimekeepRegistryEntry = {
file: TFile;
timekeeps: TimekeepWithPosition[];
};
/**
* Obsidian vault registry of all timekeep instances
*/
export class TimekeepRegistry {
/** Vault that the registry is operating on */
vault: Vault;
/** Store for entries within the registry */
entries: Store<TimekeepRegistryEntry[]>;
/** Tracked events */
events: EventRef[] = [];
/** Concurrency limit for registry indexing */
concurrencyLimit: number = 10;
constructor(vault: Vault) {
this.vault = vault;
this.entries = createStore([]);
}
onload() {
// Attach vault events
const createEvent = this.vault.on("create", this.onFileCreated.bind(this));
const modifyEvent = this.vault.on("modify", this.onFileModified.bind(this));
const deleteEvent = this.vault.on("delete", this.onFileRemoved.bind(this));
this.events.push(createEvent, modifyEvent, deleteEvent);
// Load the registry from the vault
void this.loadFromVault()
//
.catch((error) => {
console.error("failed to load vault timekeep data", error);
});
}
/**
* Handle the creation of new files with the vault, indexing them
* into the timekeep registry
*
* @param file The file that was created
*/
onFileCreated(file: TAbstractFile) {
if (!(file instanceof TFile)) {
return;
}
void this.updateFromFile(file).catch(() =>
console.error("failed to update timekeep changes within file")
);
}
/**
* Handle a file being modified within the vault, reloads
* the change and indexes it again
*
* @param file The modified file
*/
onFileModified(file: TAbstractFile) {
if (!(file instanceof TFile)) {
return;
}
void this.updateFromFile(file).catch(() =>
console.error("failed to update timekeep changes within file")
);
}
/**
* Handles a file being removed from the vault removes
* the file if registered
*
* @param file The removed file
*/
onFileRemoved(file: TAbstractFile) {
if (!(file instanceof TFile)) {
return;
}
this.entries.setState((entries) => {
const newEntries = entries.filter((entry) => {
console.log(file, entry.file, file === entry.file);
return entry.file !== file;
});
return newEntries;
});
}
/**
* Load the registry from the current vault
*/
async loadFromVault() {
const entries = await TimekeepRegistry.getTimekeepsWithinVault(
this.vault,
true,
this.concurrencyLimit
);
this.entries.setState(entries);
}
/**
* Update a file within the vault loading its timekeeps then
* replacing them within the registry
*
* @param file
*/
async updateFromFile(file: TFile) {
const timekeeps = await TimekeepRegistry.getTimekeepsWithinFile(this.vault, file, true);
this.entries.setState((entries) => {
const filteredEntries: TimekeepRegistryEntry[] = entries.filter(
(entry) => entry.file !== file
);
const newEntry: TimekeepRegistryEntry = {
file,
timekeeps,
};
const newEntries = [...filteredEntries, newEntry];
return newEntries;
});
}
/**
* Collect all timekeep's within the provided vault
*
* @param vault The vault to search within
* @param cached Whether to perform a cached read when checking
* @param concurrencyLimit Maximum number of files to search at once
* @returns The collection of timekeeps with their positions in each file
*/
static async getTimekeepsWithinVault(
vault: Vault,
cached: boolean = true,
concurrencyLimit: number = 15
): Promise<TimekeepRegistryEntry[]> {
const markdownFiles = vault.getMarkdownFiles();
// Concurrency limited parallel file processing
const processFile = limitFunction(
async (file: TFile): Promise<TimekeepRegistryEntry> => {
const timekeeps = await TimekeepRegistry.getTimekeepsWithinFile(
vault,
file,
cached
);
return { file, timekeeps };
},
{ concurrency: concurrencyLimit }
);
const promises = markdownFiles.map(processFile);
return Promise.all(promises);
}
/**
* Collect all timekeep's within the provided file in the
* provided vault
*
* @param vault The vault to search within
* @param file The file to search within
* @param cached Whether to perform a cached read
* @returns The collection of timekeeps with their positions in each file
*/
static async getTimekeepsWithinFile(
vault: Vault,
file: TFile,
cached: boolean = true
): Promise<TimekeepWithPosition[]> {
let content: string;
if (cached) {
content = await vault.cachedRead(file);
} else {
content = await vault.read(file);
}
return extractTimekeepCodeblocksWithPosition(content);
}
}

View file

@ -375,5 +375,49 @@ export class TimekeepSettingsTab extends PluginSettingTab {
}));
});
});
// Registry settings section
new Setting(this.containerEl)
.setName("Registry")
.setDesc(
"Timekeep uses an internal registry to track timekeep instances within your vault for functionality like autocomplete"
)
.setHeading();
new Setting(this.containerEl)
.setName("Enabled")
.setDesc(
"Whether to enable the registry, this can be disabled to reduce memory usage if you don't need the features that depend on it."
)
.addToggle((t) => {
t.setValue(settings.registryEnabled);
t.onChange((v) => {
this.settingsStore.setState((currentValue) => ({
...currentValue,
registryEnabled: v,
}));
});
});
new Setting(this.containerEl)
.setName("Index Concurrency")
.setDesc(
"Maximum files to read concurrently on initialization (decrease this if you find you are lagging when opening your vault because of timekeep)"
)
.addText((t) => {
t.setValue(String(settings.registryConcurrencyLimit));
t.onChange((v) => {
const value = Number(v);
this.settingsStore.setState((currentValue) => ({
...currentValue,
registryConcurrencyLimit:
Number.isFinite(value) && Number.isSafeInteger(value)
? value
: defaultSettings.registryConcurrencyLimit,
}));
});
});
}
}

View file

@ -69,6 +69,9 @@ export interface TimekeepSettings {
sortOrder: SortOrder;
unstartedOrder: UnstartedOrder;
registryEnabled: boolean;
registryConcurrencyLimit: number;
}
export const defaultSettings: TimekeepSettings = {
@ -92,6 +95,9 @@ export const defaultSettings: TimekeepSettings = {
sortOrder: SortOrder.INSERTION,
unstartedOrder: UnstartedOrder.LAST,
registryEnabled: true,
registryConcurrencyLimit: 15,
};
export function legacySettingsCompatibility(settings: TimekeepSettings): void {