Skip to content

Latest commit

 

History

History
 
 

src

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Plugin Architecture

Obsidian Full Calendar's goal is to give users a robust and feature-ful calendar view into their Obsidian Vault. In addition to displaying and modifying events stored in note frontmatter and daily note bulleted lists, it can also read events from the Internet in CalDAV and ICS format.

Obsidian Full Calendar takes its name from FullCalendar, a "Full-sized drag & drop event calendar in JavaScript." This plugin uses FullCalendar as its calendar view. While the naming can be ambiguous, this document will always refer to the FullCalendar view library without any spaces, or as fullcalendar.io. The plugin will be referred to either as "the plugin", "Full Calendar" with a space, or "Obsidian Full Calendar".

As of now, the plugin supports events from the following sources and formats:

  • Frontmatter of notes in the open Obsidian Vault.
  • Bullet list items in Daily Notes generated by the core Daily Notes or Periodic Notes plugins.
  • ICS files publicly accessible at a URL.
  • CalDAV servers authenticated with HTTP basic authentication.

Much of the code of Full Calendar exists to deal with the normalization of these formats so they can be handled by the view layer without worrying about what sources different events are actually from.

Below is a birds-eye view of the different components of the plugin, and the interactions between them.

                 ┌──────────────┐
                 │              │
┌────────────┐   │              │       ┌─────────────┐     ┌──────────────┐
│            ├───►              ├───────►             ├─────►              │
│ EventStore │   │  EventCache  │       │ view.ts +   │     │ FullCalendar │
│            ◄───┤              ◄───────┤ calendar.ts ◄─────┤ View*        │
└────────────┘   │              │       │             │     │              │
                 │              │       └─────────────┘     └──────────────┘
             ┌──►└──────▲──┬────┘
             │          │  │
             │          │  │
             │    ┌─────┴──▼─────┐
             │    │              │
             │    │  Calendars   │
             │    │              │
             │    └─▲──┬─────▲───┘
             │      │  │     │
 ┌───────────┴──────┴──▼──┐  │
 │                        │ ┌┴────────────┐
 │Obsidian Vault APIs*    │ │   The       │
 │                        │ │   Internet* │
 └────────────────────────┘ └─────────────┘
 * Components with an asterisk are not part of the plugin's code.

Codemap

Following the advice in this blog post on architecture docs, the following section will list out the main modules of the code without linking out to specific file locations that may quickly become stale. Make use of code search and the TypeScript Language Server to jump around and explore the code, with this section as your guide.

types

This module defines some common types used throughout the code. The most prevalent is OFCEvent, short for Obsidian Full Calendar Event, that specifies the intermediate representation for all events in the plugin. Note that FullCalendar.io uses a different event format called EventInput, which you can read about in their documentation.

OFCEvent is derived from a Zod parser that handles parsing/validating JavaScript objects into the expected shape of an event. You can check out the parser in types/schema.ts.

Translation between OFCEvent and EventInput is handled in interop.ts. Each Calendar subclass (see below) handles its own translation from its source format into OFCEvent.

core

The core directory consists of two classes, EventStore and EventCache. These two classes comprise the plugin's main event-managing logic.

The EventStore is the source of truth for events in the plugin. Its interface is similar to a simplified database that stores events, calendars and file locations. Files and calendars are one-to-many relationships: every event is related to exactly one calendar and at most one file, but calendars and files can have many events within them. The EventStore allows for effecient querying of events grouped by calendars and files. Every event in the EventStore has an ID associated with it. Local events have random IDs that are generated at insert time, but remote events using the iCal spec have UIDs that are plumbed through.

The EventCache manages the state stored in the EventStore. Its main job is co-ordinating with both the view layer and the Calendars which perform I/O to actually read events from disk or the network. The EventCache has two main hooks to update the EventStore:

  • Hook (via MetadataCache.on('update')) for when a file has changed so that it can tell Calendars to re-parse that file.
  • Hook for when an event with a given ID has been modified from the view. Other components can subscribe to state updates on the EventCache. Right now, the view is the only subscriber, but in the future it may be possible for other plugins to subscribe to updates.

Notably, while the core components have some knowledge of Obsidian APIs (mostly the TFile type and the ability to show Notice toasts to the user), they do not hold references to the App, Vault, MetadataCache or any other API that deals with file I/O. File I/O is handled entirely by the Calendar subclasses. This simplifies testing dramatically, since the Obsidian API does not need to be mocked out when testing the EventCache logic.

The plugin has exactly one EventCache instance at any given time. It is initialized and hooked up to Vault and MetadataCache listeners when the plugin is initialized, in main.ts.

While calendars that store their events on disk are kept up-to-date with listeners from the Obsidian API, remote calendars are managed with the Stale-While-Revalidating (SWR) cache strategy.

calendars

Each source of events has its own Calendar subclass that handles the relevant I/O operations and parses events into the common format. There are two abstract subclasses: RemoteCalendar and EditableCalendar. RemoteCalendars should cache their responses from the Internet, and have a method to re-fetch their input as part of the SWR cache strategy.

EditableCalendars are constructed with references to an ObsidianAdapter instance that handles all interactions with the Obsidian API. This adapter is useful for testing, since it reduces the surface area of APIs to be mocked from the entire API to a handful of functions that the plugin actually uses. It also allows for useful and safe abstractions on top of the Obsidian API, so that its harder for Calendars to do incorrect things, like write a stale copy of a file back to disk.

ui

While core and calendars make up the Model in the MVC pattern, the Views and Controllers are currently both living in the ui directory. The view connector to the FullCalendar library lives in calendar.ts. Most of the controller logic that interfaces with the EventCache lives, somewhat confusingly, in view.ts, which also instantiates the Obsidian plugin View. Auxilliary views, like the edit/create modal and settings selectors, are React components that live in their own .tsx files and are mounted into the DOM when needed.

Architecture Invariant: All interactions with event data should be mediated by the EventCache. Code in the ui directory should not reference or call out to the EventStore, Obsidian Vault APIs, or Calendar subclasses.