feat: add durable provider cache

Add a private provider-neutral SQLite WAL cache with transactional migrations, retained raw payloads and tombstones, per-window opaque checkpoints, and reserved outbox/conflict tables.

Verify migrations, permissions, identity invariants, concurrent access, atomic pull-page rollback, and future-schema rejection with deterministic tests. No OAuth, networking, secrets, or Graph synchronization is included yet.
This commit is contained in:
2026-07-18 10:53:41 +01:00
parent cfca6ab6d0
commit 787daf00dd
9 changed files with 1794 additions and 24 deletions

View File

@@ -6,7 +6,7 @@ and terminal rendering replaceable.
```text
src/main.cpp
├── tui/ ANSI rendering, input decoding, terminal lifecycle
├── storage/ iCalendar file adapter (later: SQLite cache)
├── storage/ iCalendar file adapter and provider-neutral cache
└── domain/ Event, Calendar, civil dates, month layout and queries
future sync workers
@@ -118,13 +118,27 @@ semantically. Browsing remains available for unsupported files. This
conservative boundary is more important than partial editing because a
successful-looking edit must not erase unrelated calendar data.
Provider synchronization will not write directly into this UI file. It will use
a transaction-capable local cache and preserve remote ETags/sync tokens in a
provider metadata table.
Provider synchronization does not write directly into this UI file. Its
prerequisite durable boundary is a provider-neutral SQLite database in WAL mode.
The database is a private local file, and schema changes run as versioned,
all-or-nothing migrations so a failed upgrade cannot leave a partially migrated
cache.
The planned cache schema has `calendars`, `events`, `event_instances`, and
`sync_state`. Raw provider payloads are retained alongside normalized fields so
an older Nocal cannot destroy fields it does not understand.
The cache separates provider records from normalized projections. It retains
raw provider payloads alongside normalized fields so an older Nocal cannot
destroy fields it does not understand. Inactive calendars and tombstoned events
remain recorded rather than disappearing from history. Normalized event
instances are materialized for immutable UI snapshots; provider payload details
do not cross into rendering or the domain model.
Incremental progress is scoped by provider account, calendar, and synchronization
window. A provider's opaque cursor is committed in the same transaction as each
pulled page, preventing a crash from advancing the cursor beyond durable data.
Transactional outbox and conflict records are reserved in the boundary for
future local writes and deterministic conflict handling. This cache does not
itself implement networking, authentication, or Microsoft Graph synchronization.
OAuth tokens and other secrets are explicitly excluded from SQLite and belong
in the Freedesktop Secret Service.
## Terminal backend
@@ -149,3 +163,10 @@ Service, never in the calendar database or command line.
Conflict resolution is deterministic: unchanged side wins; otherwise retain
both versions and mark a conflict for the user. Network work happens outside the
render loop and publishes immutable snapshots to it.
The first provider-facing step after the cache is verified is an injectable HTTP
boundary, browser-based authorization-code flow with PKCE, and fake transports
for deterministic authentication and failure tests. Read-only Microsoft Graph
calendar discovery and delta synchronization follows. Graph delta links remain
opaque and are persisted only through the calendar/window transaction described
above; remote writes and conflict resolution come later.