diff --git a/src/events/handler.ts b/src/events/handler.ts new file mode 100644 index 0000000..3c16499 --- /dev/null +++ b/src/events/handler.ts @@ -0,0 +1,97 @@ +import { Vault, TAbstractFile } from "obsidian"; +import { Event } from "./types"; +import MetadataStore from "../metadata-store"; +import EventsQueue from "./queue"; + +/** + * Tracks changes to local sync directory and updates files metadata. + */ +export default class EventsHandler { + private eventsQueue: EventsQueue = new EventsQueue(); + + constructor( + private vault: Vault, + private metadataStore: MetadataStore, + private localContentDir: string, + private repoContentDir: string, + ) { + this.vault.on("create", this.onCreate.bind(this)); + this.vault.on("delete", this.onDelete.bind(this)); + this.vault.on("modify", this.onModify.bind(this)); + this.vault.on("rename", this.onRename.bind(this)); + } + + private async onCreate(file: TAbstractFile) { + if (!file.path.startsWith(this.localContentDir)) { + // The file has not been created in directory that we're syncing with GitHub + return; + } + this.metadataStore.data[file.path] = { + localPath: file.path, + remotePath: file.path.replace(this.localContentDir, this.repoContentDir), + sha: null, + dirty: true, + }; + await this.metadataStore.save(); + this.eventsQueue.enqueue({ + type: "create", + filePath: file.path, + }); + } + + private async onDelete(file: TAbstractFile) { + if (!file.path.startsWith(this.localContentDir)) { + // The file was not in directory that we're syncing with GitHub + return; + } + delete this.metadataStore.data[file.path]; + await this.metadataStore.save(); + this.eventsQueue.enqueue({ + type: "delete", + filePath: file.path, + }); + } + + private async onModify(file: TAbstractFile) { + if (!file.path.startsWith(this.localContentDir)) { + // The file has not been create in directory that we're syncing with GitHub + return; + } + this.metadataStore.data[file.path].dirty = true; + await this.metadataStore.save(); + this.eventsQueue.enqueue({ + type: "modify", + filePath: file.path, + }); + } + + private async onRename(file: TAbstractFile, oldPath: string) { + if ( + !file.path.startsWith(this.localContentDir) && + !oldPath.startsWith(this.localContentDir) + ) { + // Both are not in directory that we're syncing with GitHub + return; + } + + if ( + file.path.startsWith(this.localContentDir) && + oldPath.startsWith(this.localContentDir) + ) { + // Both files are in the synced directory + // First create the new one + await this.onCreate(file); + // Then delete the old one + await this.onDelete(file); + return; + } else if (file.path.startsWith(this.localContentDir)) { + // Only the new file is in the local directory + await this.onCreate(file); + return; + } else if (oldPath.startsWith(this.localContentDir)) { + // Only the old file was in the local directory + await this.onDelete(file); + return; + } + } +} diff --git a/src/events/queue.ts b/src/events/queue.ts new file mode 100644 index 0000000..6dbbdff --- /dev/null +++ b/src/events/queue.ts @@ -0,0 +1,47 @@ +import { Event } from "./types"; + +/** + * A custom queue to better handle events. + */ +export default class EventsQueue { + private eventsQueue: Map = new Map(); + + /** + * Enqueues an event in the queue. + * Handles special cases when the previous event and the new one + * would cancel themselves out. + */ + enqueue(event: Event) { + if (!this.eventsQueue.has(event.filePath)) { + // No other event exist for this file, just enqueue it + this.eventsQueue.set(event.filePath, event); + return; + } + + if ( + this.eventsQueue.get(event.filePath)?.type === "create" && + event.type === "delete" + ) { + // The previous event was a create and the new one is a delete. + // Just delete the previous one as they would amount to the same outcome. + this.eventsQueue.delete(event.filePath); + } else if ( + this.eventsQueue.get(event.filePath)?.type === "delete" && + event.type === "create" + ) { + // The old event was a delete and the new one is a create. + // Delete the old one and enqueue a modify event as it likely + // that the content changed. + this.eventsQueue.delete(event.filePath); + this.eventsQueue.set(event.filePath, { + type: "modify", + filePath: event.filePath, + }); + } else { + // Delete and enqueue the event in all other cases. + // We first delete the event to change the order of the queue + this.eventsQueue.delete(event.filePath); + this.eventsQueue.set(event.filePath, event); + } + } +} diff --git a/src/events/types.ts b/src/events/types.ts new file mode 100644 index 0000000..9d21510 --- /dev/null +++ b/src/events/types.ts @@ -0,0 +1,4 @@ +export type Event = { + type: "create" | "delete" | "modify"; + filePath: string; +};