You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

12 KiB

TimeSafari — Native-First Notification System

📝 SANITY CHECK IMPROVEMENTS APPLIED: This document has been updated to clarify current background fetch infrastructure status and iOS implementation completeness.

Status: Ready for implementation
Date: 2025-09-07
Author: Matthew Raymer


Executive Summary

Ship a single, Native-First notification system: OS-scheduled background prefetch at T–lead + pre-armed local notifications. Web-push is retired.

What we deliver

  • Closed-app delivery: Pre-armed locals fire even if the app is closed.
  • Freshness: One prefetch attempt per slot at T–lead; ETag/TTL controls; skip when stale.
  • Android precision: Exact alarms with permission; windowed fallback (±10m) otherwise.
  • Resilience: Re-arm after reboot/time-change (Android receivers; iOS on next wake/silent push).
  • Cross-platform: Same TS API (iOS/Android/Electron). Electron is best-effort while running.

Success signals

  • High delivery reliability, minute-precision on Android with permission.
  • Prefetch budget hit rate at T–lead; zero stale deliveries beyond TTL.

Strategic Plan

Goal

Deliver 1..M daily notifications with OS background prefetch at T–lead (see Glossary) and rolling-window safety (see Glossary) so messages display with fresh content even when the app is closed.

Tenets

  • Reliability first: OS delivers once scheduled; no JS at delivery time.
  • Freshness with guardrails: Prefetch at T–lead (see Glossary); enforce TTL-at-fire (see Glossary); ETag-aware.
  • Single system: One TS API; native adapters swap under the hood.
  • Platform honesty: Android exactness via permission; iOS best-effort budget.
  • No delivery-time network: Local notifications display pre-rendered content only; never fetch at delivery.

Architecture (high level)

App (Vue/TS) → Orchestrator (policy) → Native Adapters:

  • SchedulerNative — AlarmManager (Android) / UNUserNotificationCenter (iOS)
  • BackgroundPrefetchNative — WorkManager (Android) / BGTaskScheduler (+ silent push) (iOS)
  • DataStore — SQLite

Storage: Current: SharedPreferences (Android) / UserDefaults (iOS) + in-memory cache (see Glossary → Tiered Storage). Planned: one shared SQLite file (see Glossary → Shared DB); the app owns schema/migrations; the plugin opens the same path with WAL (see Glossary → WAL); background writes are short & serialized. (Keep the "(Planned)" label until Phase 1 ships.)

SQLite Ownership & Concurrency (Planned)

  • One DB file: The plugin will open the same path the app uses (no second DB).
  • Migrations owned by app: The app executes schema migrations and bumps PRAGMA user_version (see Glossary → 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 (see Glossary → 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.

Note: Currently using SharedPreferences (Android) / UserDefaults (iOS) with in-memory cache. See Implementation Roadmap → Phase 1.1 for migration steps.

Scheduling & T–lead (Planned)

  • Arm rolling window (see Glossary → Rolling window). (Planned)
  • Exactly one online-first fetch at T–lead (see Glossary → T–lead) with 12s timeout; ETag/304 respected. (Planned)
  • If fresh and within TTL-at-fire (see Glossary → TTL), (re)arm; otherwise keep prior content. (Planned)
  • If the OS skips the wake, the pre-armed local still fires from cache. (Planned)

Note: Current implementation has basic scheduling and WorkManager infrastructure (Android) but lacks T–lead prefetch logic, rolling window logic, and iOS background tasks. See Implementation Roadmap → Phase 1-2.

Policies (Mixed Implementation)

  • TTL-at-fire (see Glossary → TTL): Before arming for T, if (T − fetchedAt) > ttlSecondsskip. (Planned)
  • Android exactness (see Glossary → Exact alarm): (Partial) — permission flow exists; finalize ±10m window & deep-link to settings.
  • Reboot/time change: (Partial) — Android receivers partially present; iOS via next wake/silent push.
  • No delivery-time network (see Glossary → No delivery-time network): Local notifications display pre-rendered content only; never fetch at delivery. (Implemented)

Implementation Guide

1) Interfaces (TS stable)

  • SchedulerNative: scheduleExact({slotId, whenMs, title, body, extra}), scheduleWindow(..., windowLenMs), cancelBySlot, rescheduleAll, capabilities()
  • BackgroundPrefetchNative: schedulePrefetch(slotId, atMs), cancelPrefetch(slotId)
  • DataStore: SQLite adapters (notif_contents, notif_deliveries, notif_config)
  • Public API: configure, requestPermissions, runFullPipelineNow, reschedule, getState

DB Path & Adapter Configuration (Planned)

  • Configure option: dbPath: string (absolute path or platform alias) will be 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

Note: Currently using SharedPreferences/UserDefaults (see Glossary → Tiered Storage). Database configuration is planned for Phase 1.

See Implementation Roadmap → Phase 1.1 for migration steps and schema.

Type (TS) extension (Planned)

export type ConfigureOptions = {
  // …existing fields…
  dbPath: string;                 // shared DB file the plugin will open
  storage: 'shared';              // canonical value; plugin-owned DB is not used
};

Note: Current NotificationOptions interface exists but lacks dbPath configuration. This will be added in Phase 1.

Plugin side (pseudo)

// 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" }
// 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.
  • Route all arming through SchedulerNative to centralize Android exact/window semantics.

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.
  • Never fetch at delivery time.

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

  • Request SCHEDULE_EXACT_ALARM; deep-link if denied; fallback to setWindow(start,len) (±10m).
  • Receivers: BOOT_COMPLETED, TIMEZONE_CHANGED, TIME_SET → recompute & re-arm for next 24h and schedule T–lead prefetch.

6) iOS specifics

  • BGTaskScheduler for T–lead prefetch (best-effort). Optional silent push nudge.
  • Locals: UNCalendarNotificationTrigger (one-shots); no NSE mutation for locals.

7) Network & Timeouts

  • Content fetch: 12s timeout; single attempt at T–lead; ETag/304 respected.
  • ACK/Error: 8s timeout, fire-and-forget.

8) Electron

  • Notifications while app is running; recommend Start-on-Login. No true background scheduling when fully closed.

9) Telemetry

  • Record scheduled|shown|error; ACK deliveries (8s timeout); include slot/times/TZ/app version.

Capability Matrix

Capability Android (Native) iOS (Native) Electron Web
Multi-daily locals (closed app) (app running)
Prefetch at T–lead (app closed) WorkManager ⚠️ BGTask (best-effort) (app running)
Re-arm after reboot/time-change Receivers ⚠️ On next wake/silent push Start-on-Login
Minute-precision alarms with exact permission not guaranteed timer best-effort
Delivery-time mutation for locals
ETag/TTL enforcement
Rolling-window safety

Acceptance Criteria

Core

  • Closed-app delivery: Armed locals fire at T with last rendered content. No delivery-time network.
  • T–lead prefetch: Single background attempt at T–lead; if skipped, delivery still occurs from cache.
  • TTL-at-fire: No armed local violates TTL at T.

Android

  • Exact permission path: With SCHEDULE_EXACT_ALARM → within ±1m; else ±10m window.
  • Reboot recovery: After reboot, receivers re-arm next 24h and schedule T–lead prefetch.
  • TZ/DST change: Recompute & re-arm; future slots align to new wall-clock.

iOS

  • BGTask budget respected: Prefetch often runs but may be skipped; delivery still occurs via rolling window.
  • Force-quit caveat: No background execution after user terminate; delivery still occurs if pre-armed.

Electron

  • Running-app rule: Delivery only while app runs; with Start-on-Login, after reboot the orchestrator re-arms and subsequent slots deliver.

Network

  • Content fetch timeout 12s; ACK/Error 8s; no retries inside lead; ETag honored.

Observability

  • Log/telemetry for scheduled|shown|error; ACK payload includes slot, times, device TZ, app version.

DB Sharing (Planned)

  • 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).

Note: Currently using SharedPreferences/UserDefaults with in-memory cache. SQLite sharing is planned for Phase 1.


Web-Push Cleanup

Web-push functionality has been retired due to unreliability. All web-push related code paths and documentation sections should be removed or marked as deprecated. See web-push-cleanup-guide.md for detailed cleanup steps.


This document consolidates the Native-First notification system strategy, implementation details, capabilities, and acceptance criteria into a single comprehensive reference.