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
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
- 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)
- Fetch: WorkManager (
- Callback Registry (Week 2) — shared TS interface + native bridges
- Observability & Health (Week 2–3) — event codes, status endpoints, history compaction
- iOS Parity (Week 3–4) — BGTaskScheduler + UNUserNotificationCenter
- Web SW/Push (Week 4) — SW events + IndexedDB (mirror schema), periodic sync fallback
- 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