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.
 
 
 
 
 
 

8.7 KiB

Daily Notification Plugin — Phase 2 Recommendations (v3)

This directive assumes Phase 1 (API surface + tests) is complete and aligns with the current codebase. It focuses on platform implementations, storage/TTL, callbacks, observability, and security.


1) Milestones & Order of Work

  1. Android Core (Week 1–2)
    • Fetch: WorkManager (Constraints: NETWORK_CONNECTED, backoff: exponential)
    • Notify: AlarmManager (or Exact alarms if permitted), NotificationManager
    • Boot resilience: RECEIVE_BOOT_COMPLETED receiver reschedules jobs
    • Shared SQLite schema + DAO layer (Room recommended)
  2. Callback Registry (Week 2) — shared TS interface + native bridges
  3. Observability & Health (Week 2–3) — event codes, status endpoints, history compaction
  4. iOS Parity (Week 3–4) — BGTaskScheduler + UNUserNotificationCenter
  5. Web SW/Push (Week 4) — SW events + IndexedDB (mirror schema), periodic sync fallback
  6. Docs & Examples (Week 4) — migration, enterprise callbacks, health dashboards

2) Storage & TTL — Concrete Schema

Keep TTL-at-fire invariant and rolling window armed. Use normalized tables and a minimal DAO.

SQLite (DDL)

CREATE TABLE IF NOT EXISTS content_cache (
  id TEXT PRIMARY KEY,
  fetched_at INTEGER NOT NULL,         -- epoch ms
  ttl_seconds INTEGER NOT NULL,
  payload BLOB NOT NULL,
  meta TEXT
);

CREATE TABLE IF NOT EXISTS schedules (
  id TEXT PRIMARY KEY,
  kind TEXT NOT NULL CHECK (kind IN ('fetch','notify')),
  cron TEXT,                           -- optional: cron expression
  clock_time TEXT,                     -- optional: HH:mm
  enabled INTEGER NOT NULL DEFAULT 1,
  last_run_at INTEGER,
  next_run_at INTEGER,
  jitter_ms INTEGER DEFAULT 0,
  backoff_policy TEXT DEFAULT 'exp',
  state_json TEXT
);

CREATE TABLE IF NOT EXISTS callbacks (
  id TEXT PRIMARY KEY,
  kind TEXT NOT NULL CHECK (kind IN ('http','local','queue')),
  target TEXT NOT NULL,                -- url_or_local
  headers_json TEXT,
  enabled INTEGER NOT NULL DEFAULT 1,
  created_at INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS history (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  ref_id TEXT,                         -- content or schedule id
  kind TEXT NOT NULL,                  -- fetch/notify/callback
  occurred_at INTEGER NOT NULL,
  duration_ms INTEGER,
  outcome TEXT NOT NULL,               -- success|failure|skipped_ttl|circuit_open
  diag_json TEXT
);

CREATE INDEX IF NOT EXISTS idx_history_time ON history(occurred_at);
CREATE INDEX IF NOT EXISTS idx_cache_time ON content_cache(fetched_at);

TTL-at-fire Rule

  • On notification fire: if (now > fetched_at + ttl_seconds) -> skip (record outcome=skipped_ttl).
  • Maintain a prep guarantee: ensure a fresh cache entry for the next window even after failures (schedule a fetch on next window).

3) Android Implementation Sketch

WorkManager for Fetch

class FetchWorker(
  appContext: Context,
  workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

  override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
    val start = SystemClock.elapsedRealtime()
    try {
      val payload = fetchContent()           // http call / local generator
      dao.upsertCache(ContentCache(...))
      logEvent("DNP-FETCH-SUCCESS", start)
      Result.success()
    } catch (e: IOException) {
      logEvent("DNP-FETCH-FAILURE", start, e)
      Result.retry()
    } catch (e: Throwable) {
      logEvent("DNP-FETCH-FAILURE", start, e)
      Result.failure()
    }
  }
}

Constraints: Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
Backoff: setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)

AlarmManager for Notify

fun scheduleExactNotification(context: Context, triggerAtMillis: Long) {
  val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
  val pi = PendingIntent.getBroadcast(context, REQ_ID, Intent(context, NotifyReceiver::class.java), FLAG_IMMUTABLE)
  alarmMgr.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pi)
}

class NotifyReceiver : BroadcastReceiver() {
  override fun onReceive(ctx: Context, intent: Intent?) {
    val cache = dao.latestCache()
    if (cache == null) return
    if (System.currentTimeMillis() > cache.fetched_at + cache.ttl_seconds * 1000) {
      recordHistory("notify", "skipped_ttl"); return
    }
    showNotification(ctx, cache)
    recordHistory("notify", "success")
    fireCallbacks("onNotifyDelivered")
  }
}

Boot Reschedule

  • Manifest: RECEIVE_BOOT_COMPLETED
  • On boot: read schedules.enabled=1 and re-schedule WorkManager/AlarmManager

4) Callback Registry — Minimal Viable Implementation

TS Core

export type CallbackKind = 'http' | 'local' | 'queue';

export interface CallbackEvent {
  id: string;
  at: number;
  type: 'onFetchStart' | 'onFetchSuccess' | 'onFetchFailure' |
        'onNotifyStart' | 'onNotifyDelivered' | 'onNotifySkippedTTL' | 'onNotifyFailure';
  payload?: unknown;
}

export type CallbackFunction = (e: CallbackEvent) => Promise<void> | void;

Delivery Semantics

  • Exactly-once attempt per event, persisted history row
  • Retry: exponential backoff with cap; open circuit per callback.id on repeated failures
  • Redaction: apply header/body redaction before persisting diag_json

HTTP Example

async function deliverHttpCallback(cb: CallbackRecord, event: CallbackEvent) {
  const start = performance.now();
  try {
    const res = await fetch(cb.target, {
      method: 'POST',
      headers: { 'content-type': 'application/json', ...(cb.headers ?? {}) },
      body: JSON.stringify(event),
    });
    recordHistory(cb.id, 'callback', 'success', start, { status: res.status });
  } catch (err) {
    scheduleRetry(cb.id, event); // capped exponential
    recordHistory(cb.id, 'callback', 'failure', start, { error: String(err) });
  }
}

5) Observability & Health

  • Event Codes: DNP-FETCH-*, DNP-NOTIFY-*, DNP-CB-*
  • Health API (TS): getDualScheduleStatus() returns { nextRuns, lastOutcomes, cacheAgeMs, staleArmed, queueDepth }
  • Compaction: nightly job to prune history > 30 days
  • Device Debug: Android broadcast to dump status to logcat for field diagnostics

6) Security & Permissions

  • Default HTTPS-only callbacks, opt-out via explicit dev flag
  • Android: runtime gate for POST_NOTIFICATIONS; show rationale UI for exact alarms (if requested)
  • PII/Secrets: redact before persistence; never log tokens
  • Input Validation: sanitize HTTP callback targets; enforce allowlist pattern (e.g., https://*.yourdomain.tld in prod)

7) Performance & Battery

  • ±Jitter (5m) for fetch; coalesce same-minute schedules
  • Retry Caps: ≤ 5 attempts, upper bound 60 min backoff
  • Network Guards: avoid waking when offline; use WorkManager constraints to defer
  • Back-Pressure: cap concurrent callbacks; open circuit on sustained failures

8) Tests You Can Add Now

  • TTL Edge Cases: past/future timezones, DST cutovers
  • Retry & Circuit: force network failures, assert capped retries + circuit open
  • Boot Reschedule: instrumentation test to simulate reboot and check re-arming
  • SW/IndexedDB: headless test verifying cache write/read + TTL skip

9) Documentation Tasks

  • API reference for new health and callback semantics
  • Platform guides: Android exact alarm notes, iOS background limits, Web SW lifecycle
  • Migration note: why scheduleDualNotification is preferred; compat wrappers policy
  • “Runbook” for QA: how to toggle jitter/backoff; how to inspect history

10) Acceptance Criteria (Phase 2)

  • Android end-to-end demo: fetch → cache → TTL check → notify → callback(s) → history
  • Health endpoint returns non-null next run, recent outcomes, and cache age
  • iOS parity path demonstrated on simulator (background fetch + local notif)
  • Web SW functional on Chromium + Firefox with IndexedDB persistence
  • Logs show structured DNP-* events; compaction reduces history size as configured
  • Docs updated; examples build and run

11) Risks & Mitigations

  • Doze/Idle drops alarms → prefer WorkManager + exact when allowed; add tolerance window
  • iOS background unpredictability → encourage scheduled “fetch windows”; document silent-push optionality
  • Web Push unavailable → periodic sync + foreground fallback; degrade gracefully
  • Callback storms → batch events where possible; per-callback rate limit

12) Versioning

  • Release as 1.1.0 when Android path merges; mark wrappers as soft-deprecated in docs
  • Keep zero-padded doc versions in /doc/ and release notes linking to them