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.

View File

@@ -112,6 +112,29 @@ values incompatible with the event's `DTSTART` time basis or `TZID`, detached
read-only. Remaining local-data work includes detached overrides and rule
editing.
## Remote calendar foundation
Remote calendars must cross a durable local boundary before they are exposed to
the UI. That boundary is a private, provider-neutral SQLite cache in WAL mode,
with versioned migrations that either commit completely or leave the prior
schema intact. It keeps raw provider payloads, inactive calendars, and
tombstoned events so refreshes and older clients do not erase information. Each
opaque incremental cursor belongs to a particular calendar and time window and
is committed atomically with every pulled page. Normalized event instances feed
immutable snapshots rather than exposing provider response types to the month
view.
The cache also reserves transactional outbox and conflict records for future
offline writes, but remote mutation behavior is not part of this phase. OAuth
tokens and other credentials never belong in the calendar database; account
secrets will be held by the Freedesktop Secret Service.
This foundation does not make Office 365 usable yet: Nocal currently performs no
OAuth flow, HTTP request, or Microsoft Graph synchronization. The next product
slice introduces injectable HTTP and PKCE authentication with fake transports,
then adds read-only Graph delta synchronization. Remote writes remain gated on
the durability and conflict paths being exercised end to end.
## Local file interchange
Import and export are explicit command-line workflows. They never contact the

View File

@@ -24,12 +24,22 @@
- Embedded VTIMEZONE support
- Screen-reader-friendly linear view and full Unicode display-width handling
## 0.2 — durable cache and provider boundary
## 0.2 — durable cache and provider boundary — in progress
- SQLite WAL cache and migration framework
- Background job queue, immutable UI snapshots, observable sync status
- Secret Service credentials and OAuth callback helper
- Generic `SyncProvider` contract plus a fake provider test suite
Completed cache foundation:
- Provider-neutral SQLite WAL cache in a private local file
- Versioned, all-or-nothing schema migrations
- Raw provider payload retention, inactive calendars, and event tombstones
- Per-calendar/window opaque cursors committed atomically with pulled pages
- Normalized event instances for immutable snapshots
- Reserved transactional outbox and conflict records for later remote writes
Next provider-boundary slices:
- Injectable HTTP, PKCE authentication, and deterministic fake transports
- Secret Service credentials; OAuth secrets are never stored in SQLite
- Generic `SyncProvider` contract and observable sync status
## 0.3 — CalDAV
@@ -40,9 +50,15 @@
## 0.4 — hosted providers
- Google Calendar adapter with incremental synchronization
- Microsoft 365 adapter over Microsoft Graph delta queries
- Microsoft 365 read-only discovery and Graph delta synchronization, followed by
ETag-aware remote writes after the conflict boundary is proven
- Account/calendar management UI and per-calendar ANSI identity
The current cache work is only the prerequisite durable boundary. No network,
OAuth, or Graph synchronization is implemented in that phase. After the cache
passes its verification gates, the immediate sequence is injectable HTTP plus
PKCE and fake transports, then read-only Microsoft Graph delta synchronization.
## 1.0 — distribution quality
- Stable configuration/data format and documented recovery procedures