Browse Source

docs: update notification system for shared SQLite + clear T–lead

- Add SQLite Ownership & Concurrency section with WAL mode details
- Add DB Path & Adapter Configuration with shared DB setup
- Clarify T–lead governs prefetch attempts, not arming
- Add TTL-at-fire check callout for stale notification prevention
- Add DB Sharing acceptance criteria (visibility, WAL overlap, version safety)
- Update GLOSSARY.md with shared DB, WAL, and user_version definitions
- Remove plugin-owned DB references in favor of single shared database

Files modified:
- doc/notification-system.md
- doc/GLOSSARY.md

Compliance: Implements shared SQLite architecture with clear T–lead prefetch
semantics and WAL-based concurrency for app/plugin coordination.
pull/196/head
Matthew Raymer 2 weeks ago
parent
commit
464a825a7b
  1. 16
      doc/GLOSSARY.md
  2. 64
      doc/notification-system.md

16
doc/GLOSSARY.md

@ -1,16 +1,18 @@
# Glossary
**T (slot time)** — The local wall-clock time a notification is intended to fire (e.g., 08:00).
**T (slot time)** — The local wall-clock time a notification should fire (e.g., 08:00).
**T–lead** — The moment **{prefetchLeadMinutes} minutes before T** when the system _attempts_ a background prefetch to refresh content.
**T–lead** — The moment **`prefetchLeadMinutes`** before **T** when the system *attempts* a **single** background prefetch. T–lead **controls prefetch attempts, not arming**; locals are pre-armed earlier to guarantee closed-app delivery.
- Example: If T = 12:00 and `prefetchLeadMinutes = 20`, then **T–lead = 11:40**.
- If background prefetch is skipped/denied, delivery still occurs using the most recent cached payload (rolling-window safety).
- T–lead **governs prefetch attempts, not arming**. We still arm one-shot locals early (rolling window) so closed-app delivery is guaranteed.
**Rolling window** — Always keep **today's remaining** (and tomorrow if iOS pending caps allow) locals **armed** so the OS can deliver while the app is closed.
**Rolling window** — Always keep **today’s remaining** one-shot locals armed (and optionally tomorrow, within iOS caps) so the OS can deliver while the app is closed.
**TTL (time-to-live)** — Maximum allowed payload age **at fire time**. If `T − fetchedAt > ttlSeconds`, we **skip** arming for that T.
**TTL (time-to-live)** — Maximum allowed staleness of a payload at **fire time**. If the projected age at T exceeds `ttlSeconds`, we **skip** arming.
**Shared DB (default)** — The app and plugin open the **same SQLite file**; the app owns schema/migrations, the plugin performs short writes with WAL.
**WAL (Write-Ahead Logging)** — SQLite journaling mode that permits concurrent reads during writes; recommended for foreground-read + background-write.
**`PRAGMA user_version`** — An integer the app increments on each migration; the plugin **checks** (does not migrate) to ensure compatibility.
**Exact alarm (Android)** — Minute-precise alarm via `AlarmManager.setExactAndAllowWhileIdle`, subject to policy and permission.

64
doc/notification-system.md

@ -46,6 +46,16 @@ App (Vue/TS) → Orchestrator (policy) → Native Adapters:
- **BackgroundPrefetchNative** — WorkManager (Android) / BGTaskScheduler (+ silent push) (iOS)
- **DataStore** — SQLite
**Storage (single shared DB):** The app and the native plugin will use **the same SQLite database file**. The app owns schema/migrations; the plugin opens the same file with WAL enabled and performs short, serialized writes. This keeps one source of truth for payloads, delivery logs, and config.
### SQLite Ownership & Concurrency
* **One DB file:** The plugin opens the **same path** the app uses (no second DB).
* **Migrations owned by app:** The app executes schema migrations and bumps `PRAGMA user_version`. The plugin **never** migrates; it **asserts** the expected version.
* **WAL mode:** Open DB with `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000`, `foreign_keys=ON`. WAL allows foreground reads while a background job commits quickly.
* **Single-writer discipline:** Background jobs write in **short transactions** (UPSERT per slot), then return.
* **Encryption (optional):** If using SQLCipher, the **same key** is used by both app and plugin. Do not mix encrypted and unencrypted openings.
### Scheduling & T–lead
- **Arm** a rolling window (today + tomorrow within iOS cap).
@ -70,6 +80,50 @@ App (Vue/TS) → Orchestrator (policy) → Native Adapters:
- **DataStore**: SQLite adapters (notif_contents, notif_deliveries, notif_config)
- **Public API**: `configure`, `requestPermissions`, `runFullPipelineNow`, `reschedule`, `getState`
### DB Path & Adapter Configuration
* **Configure option:** `dbPath: string` (absolute path or platform alias) is passed from JS to the plugin during `configure()`.
* **Shared tables:**
* `notif_contents(slot_id, payload_json, fetched_at, etag, …)`
* `notif_deliveries(slot_id, fire_at, delivered_at, status, error_code, …)`
* `notif_config(k, v)`
* **Open settings:**
* `journal_mode=WAL`
* `synchronous=NORMAL`
* `busy_timeout=5000`
* `foreign_keys=ON`
**Type (TS) extension**
```ts
export type ConfigureOptions = {
// …existing fields…
dbPath: string; // shared DB file the plugin will open
storage: 'shared'; // canonical value; plugin-owned DB is not used
};
```
**Plugin side (pseudo)**
```kotlin
// Android open
val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READWRITE)
db.execSQL("PRAGMA journal_mode=WAL")
db.execSQL("PRAGMA synchronous=NORMAL")
db.execSQL("PRAGMA foreign_keys=ON")
db.execSQL("PRAGMA busy_timeout=5000")
// Verify schema version
val uv = rawQuery("PRAGMA user_version").use { it.moveToFirst(); it.getInt(0) }
require(uv >= MIN_EXPECTED_VERSION) { "Schema version too old" }
```
```swift
// iOS open (FMDB / SQLite3)
// Set WAL via PRAGMA after open; check user_version the same way.
```
### 2) Templating & Arming
- Render `title/body` **before** scheduling; pass via **SchedulerNative**.
@ -77,6 +131,8 @@ App (Vue/TS) → Orchestrator (policy) → Native Adapters:
### 3) T–lead (single attempt)
**T–lead governs prefetch, not arming.** We **arm** one-shot locals as part of the rolling window so closed-app delivery is guaranteed. At **T–lead = T − prefetchLeadMinutes**, the **native background job** attempts **one** 12s ETag-aware fetch. If fresh content arrives and will not violate **TTL-at-fire**, we (re)arm the upcoming slot; if the OS skips the wake, the pre-armed local still fires with cached content.
- Compute T–lead = `whenMs - prefetchLeadMinutes*60_000`.
- `BackgroundPrefetchNative.schedulePrefetch(slotId, atMs=T–lead)`.
- On wake: **ETag** fetch (timeout **12s**), persist, optionally cancel & re-arm if within TTL.
@ -84,6 +140,8 @@ App (Vue/TS) → Orchestrator (policy) → Native Adapters:
### 4) TTL-at-fire
**TTL-at-fire:** Before arming for time **T**, compute `T − fetchedAt`. If that exceeds `ttlSeconds`, **do not arm** (skip). This prevents posting stale notifications when the app has been closed for a long time.
`if (whenMs - fetchedAt) > ttlSeconds*1000 → skip`
### 5) Android specifics
@ -156,6 +214,12 @@ App (Vue/TS) → Orchestrator (policy) → Native Adapters:
- Log/telemetry for `scheduled|shown|error`; ACK payload includes slot, times, device TZ, app version.
### DB Sharing
* **Shared DB visibility:** A background prefetch writes `notif_contents`; the foreground UI **immediately** reads the same row.
* **WAL overlap:** With the app reading while the plugin commits, no user-visible blocking occurs.
* **Version safety:** If `user_version` is behind, the plugin emits an error and does not write (protects against partial installs).
---
## Web-Push Cleanup

Loading…
Cancel
Save