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.
 
 
 
 
 
 

28 KiB

Integration Point Refactor Analysis

Author: Matthew Raymer Date: 2025-10-29 Status: 🎯 ANALYSIS - Architectural refactoring proposal

Objective

Refactor the Daily Notification Plugin architecture so that TimeSafari-specific integration logic is implemented by the Capacitor host app rather than hardcoded in the plugin. This makes the plugin generic and reusable for other applications.

Current Architecture

Plugin-Side Integration (Current State)

The plugin currently contains TimeSafari-specific code:

  1. TimeSafariIntegrationManager.java

    • Manages API server URL and DID lifecycle
    • Coordinates fetching and scheduling
    • Converts TimeSafari data to NotificationContent
  2. EnhancedDailyNotificationFetcher.java

    • Makes HTTP calls to TimeSafari API endpoints:
      • POST /api/v2/offers/person
      • POST /api/v2/offers/plans
      • POST /api/v2/report/plansLastUpdatedBetween
    • Handles JWT authentication
    • Parses TimeSafari response formats
  3. DailyNotificationJWTManager.java

    • Generates JWT tokens for TimeSafari API authentication
    • Manages JWT expiration and refresh
  4. TimeSafari Data Models

    • Plugin imports PlanSummary, OffersResponse, TimeSafariNotificationBundle
    • Converts TimeSafari data structures to NotificationContent[]

Current Data Flow

Host App (Capacitor)
  ↓ configure() with activeDidIntegration config
Plugin (DailyNotificationPlugin)
  ↓ initializes TimeSafariIntegrationManager
TimeSafariIntegrationManager
  ↓ creates EnhancedDailyNotificationFetcher
EnhancedDailyNotificationFetcher
  ↓ makes HTTP calls with JWT
TimeSafari API Server
  ↓ returns TimeSafariNotificationBundle
EnhancedDailyNotificationFetcher
  ↓ converts to NotificationContent[]
TimeSafariIntegrationManager
  ↓ saves & schedules
Plugin Storage & Scheduler

Proposed Architecture

App-Side Integration (Target State)

The plugin becomes a generic notification scheduler that accepts a content fetcher callback from the host app:

  1. Plugin exposes interface for content fetching
  2. Host app implements the TimeSafari API integration
  3. Plugin calls app's implementation when fetching is needed
  4. Plugin remains generic - no TimeSafari-specific code

Proposed Data Flow

Host App (Capacitor)
  ↓ implements ContentFetcher interface
  ↓ provides fetcher callback to plugin
Plugin (DailyNotificationPlugin)
  ↓ calls app's fetcher callback
Host App's TimeSafari Integration
  ↓ makes HTTP calls, handles JWT, parses responses
TimeSafari API Server
  ↓ returns TimeSafariNotificationBundle
Host App's Integration
  ↓ converts to NotificationContent[]
  ↓ returns to plugin
Plugin
  ↓ saves & schedules (no TimeSafari knowledge)
Plugin Storage & Scheduler

What Needs to Change

1. Plugin Interface Changes

New TypeScript Interface

/**
 * Content fetcher callback that host app must implement
 */
export interface NotificationContentFetcher {
  /**
   * Fetch notification content from external source
   * 
   * This is called by the plugin when:
   * - Background fetch work is triggered
   * - Prefetch is scheduled before notification time
   * - Manual refresh is requested
   * 
   * @param context Context about why fetch was triggered
   * @returns Promise with notification content array
   */
  fetchContent(context: FetchContext): Promise<NotificationContent[]>;
}

export interface FetchContext {
  /**
   * Why the fetch was triggered
   */
  trigger: 'background_work' | 'prefetch' | 'manual' | 'scheduled';
  
  /**
   * Scheduled notification time (millis since epoch)
   */
  scheduledTime?: number;
  
  /**
   * When fetch was triggered (millis since epoch)
   */
  fetchTime: number;
  
  /**
   * Any additional context from plugin
   */
  metadata?: Record<string, unknown>;
}

New Plugin Method

export interface DailyNotificationPlugin {
  // ... existing methods ...
  
  /**
   * Register content fetcher callback from host app
   * 
   * @param fetcher Implementation provided by host app
   */
  setContentFetcher(fetcher: NotificationContentFetcher): void;
}

2. Android Plugin Changes

New Java Interface

/**
 * Content fetcher interface for host app to implement
 */
public interface NotificationContentFetcher {
    /**
     * Fetch notification content from external source
     * 
     * Called by plugin when background fetch is needed.
     * Host app implements TimeSafari API calls here.
     * 
     * @param context Fetch context with trigger info
     * @return Future with list of notification content
     */
    CompletableFuture<List<NotificationContent>> fetchContent(
        FetchContext context
    );
}

public class FetchContext {
    public final String trigger; // "background_work", "prefetch", "manual", "scheduled"
    public final Long scheduledTime; // Optional: when notification is scheduled
    public final long fetchTime; // When fetch was triggered
    public final Map<String, Object> metadata; // Additional context
    
    // Constructor, getters...
}

Plugin Implementation

public class DailyNotificationPlugin extends Plugin {
    private @Nullable NotificationContentFetcher contentFetcher;
    
    @PluginMethod
    public void setContentFetcher(PluginCall call) {
        // Receive callback reference from JavaScript
        // Store for use in background workers
        // Implementation depends on how to bridge JS -> Java callback
    }
    
    // In DailyNotificationFetchWorker:
    private NotificationContent fetchContentWithTimeout() {
        if (contentFetcher == null) {
            // Fallback: return null, use cached content
            return null;
        }
        
        FetchContext context = new FetchContext(
            "background_work",
            scheduledTime,
            System.currentTimeMillis(),
            metadata
        );
        
        try {
            List<NotificationContent> results = 
                contentFetcher.fetchContent(context).get(30, TimeUnit.SECONDS);
            return results.isEmpty() ? null : results.get(0);
        } catch (Exception e) {
            // Handle timeout, errors
            return null;
        }
    }
}

3. What Gets Removed from Plugin

Delete These Classes:

  • TimeSafariIntegrationManager.java - Move logic to host app
  • EnhancedDailyNotificationFetcher.java - Move to host app
  • TimeSafari-specific data model imports

Keep These (Generic):

  • DailyNotificationStorage - Generic storage
  • DailyNotificationScheduler - Generic scheduling
  • DailyNotificationPlugin - Core plugin (modified)
  • DailyNotificationFetchWorker - Modified to call callback
  • NotificationContent - Generic data model

4. Host App Implementation

TypeScript Implementation

import { DailyNotification, NotificationContentFetcher, FetchContext } 
  from '@timesafari/daily-notification-plugin';
import { TimeSafariAPI } from './timesafari-api'; // Host app's API client

class TimeSafariContentFetcher implements NotificationContentFetcher {
  constructor(
    private api: TimeSafariAPI,
    private activeDid: string
  ) {}
  
  async fetchContent(context: FetchContext): Promise<NotificationContent[]> {
    // 1. Generate JWT for authentication
    const jwt = await this.api.generateJWT(this.activeDid);
    
    // 2. Fetch TimeSafari data
    const [offersToPerson, offersToProjects, projectUpdates] = 
      await Promise.all([
        this.api.fetchOffersToPerson(jwt),
        this.api.fetchOffersToProjects(jwt),
        this.api.fetchProjectsLastUpdated(this.starredPlanIds, jwt)
      ]);
    
    // 3. Convert TimeSafari data to NotificationContent[]
    const contents: NotificationContent[] = [];
    
    // Convert offers to person
    if (offersToPerson?.data) {
      for (const offer of offersToPerson.data) {
        contents.push({
          id: `offer_${offer.id}`,
          title: `New Offer: ${offer.title}`,
          body: offer.description || '',
          scheduledTime: context.scheduledTime || Date.now() + 3600000,
          fetchTime: context.fetchTime,
          mediaUrl: offer.imageUrl
        });
      }
    }
    
    // Convert project updates
    if (projectUpdates?.data) {
      for (const update of projectUpdates.data) {
        contents.push({
          id: `project_${update.planSummary.jwtId}`,
          title: `${update.planSummary.name} Updated`,
          body: `New updates for ${update.planSummary.name}`,
          scheduledTime: context.scheduledTime || Date.now() + 3600000,
          fetchTime: context.fetchTime,
          mediaUrl: update.planSummary.image
        });
      }
    }
    
    return contents;
  }
}

// In host app initialization:
const plugin = new DailyNotification();
const fetcher = new TimeSafariContentFetcher(apiClient, activeDid);
plugin.setContentFetcher(fetcher);

Android Implementation (if needed)

If you need native Android implementation (for WorkManager background tasks):

// In host app's native code
class TimeSafariContentFetcher : NotificationContentFetcher {
    private val apiClient: TimeSafariAPIClient
    private val activeDid: String
    
    override fun fetchContent(context: FetchContext): CompletableFuture<List<NotificationContent>> {
        return CompletableFuture.supplyAsync {
            // Make TimeSafari API calls
            // Convert to NotificationContent[]
            // Return list
        }
    }
}

Implementation Challenges

Challenge 1: JavaScript → Java Callback Bridge

Problem: Capacitor plugins need to bridge JavaScript callbacks to Java for background workers.

Solutions:

  1. Bridge via Plugin Method (Simplest)

    • Host app calls plugin method with callback ID
    • Plugin stores callback reference
    • Plugin calls back via Capacitor bridge when needed
    • Limitations: Callback must be serializable
  2. Bridge via Event System (More Flexible)

    • Plugin emits events when fetch needed
    • Host app listens and responds
    • Plugin waits for response
    • More complex but more flexible
  3. Native Implementation (Best Performance)

    • Host app provides native Java/Kotlin implementation
    • Direct Java interface (no JS bridge needed)
    • Best for background work
    • Requires host app to include native code

Recommendation: Start with Option 1 (Plugin Method), add Option 3 (Native) later if needed.

Challenge 2: Background Worker Execution

Problem: Background WorkManager workers need to call host app's fetcher, but WorkManager runs in separate process.

Solutions:

  1. Store callback reference in SharedPreferences (Limited)

    • Serialize callback config, not actual callback
    • Host app provides API endpoint URL + auth info
    • Plugin makes HTTP call to host app's endpoint
    • Host app handles TimeSafari logic server-side
  2. Use Capacitor Background Tasks (Platform-specific)

    • iOS: BGTaskScheduler
    • Android: WorkManager with Capacitor bridge
    • Bridge JS callback from background task
    • Complex but works
  3. Native Background Implementation (Recommended)

    • Host app provides native Java/Kotlin fetcher
    • No JS bridge needed in background
    • Direct Java interface
    • Best performance

Recommendation: Option 3 (Native) for background work, Option 1 for foreground.

Challenge 3: Data Model Conversion

Problem: Plugin's generic NotificationContent vs TimeSafari's specific data structures.

Solution:

  • Host app owns conversion logic
  • Plugin never sees TimeSafari data structures
  • Plugin only knows about NotificationContent[]

This is actually simpler than current architecture.

Challenge 4: Configuration Management

Problem: Plugin currently manages TimeSafari config (apiServer, activeDid, starredPlanIds).

Solution:

  • Host app manages all TimeSafari config
  • Plugin receives only fetcher callback
  • Host app provides config to fetcher, not plugin
  • Plugin becomes stateless regarding TimeSafari

Migration Path

Phase 1: Add Callback Interface (Non-Breaking)

  1. Add setContentFetcher() method to plugin
  2. Add callback interface definitions
  3. Keep existing TimeSafari integration as fallback
  4. Allow host app to opt-in to callback approach

Phase 2: Move TimeSafari Code to Host App

  1. Create TimeSafariContentFetcher in host app
  2. Migrate EnhancedDailyNotificationFetcher logic
  3. Migrate JWT generation logic
  4. Test with callback approach

Phase 3: Remove Plugin Integration (Breaking)

  1. Remove TimeSafariIntegrationManager from plugin
  2. Remove EnhancedDailyNotificationFetcher from plugin
  3. Remove TimeSafari data model imports
  4. Make callback approach required
  5. Update documentation

Effort Estimation

Plugin Changes

  • Add callback interface: 4-8 hours
  • Modify fetch worker: 4-8 hours
  • Update documentation: 2-4 hours
  • Testing: 8-16 hours
  • Total: 18-36 hours

Host App Changes

  • Create TimeSafariContentFetcher: 8-16 hours
  • Migrate API client logic: 8-16 hours
  • Migrate JWT logic: 4-8 hours
  • Testing: 8-16 hours
  • Total: 28-56 hours

Total Effort

Estimated: 46-92 hours (6-12 days) for complete migration

Benefits

  1. Generic Plugin: Plugin can be used by any app, not just TimeSafari
  2. Clear Separation: Plugin handles scheduling, app handles fetching
  3. Easier Testing: Mock fetcher for unit tests
  4. Better Maintainability: TimeSafari changes don't require plugin updates
  5. Flexibility: Host app controls all API integration details

Risks

  1. Breaking Changes: Requires migration of host app
  2. Complexity: Callback bridge adds complexity
  3. Performance: JavaScript bridge may be slower for background work
  4. Testing Burden: More complex integration testing

Recommendations

Short Term (Keep Current Architecture)

Recommendation: Keep current architecture for now, add callback interface as optional enhancement.

Rationale:

  • Current architecture works
  • Migration is significant effort
  • Benefits may not justify costs yet
  • Can add callback interface without removing existing code

Long Term (Full Refactor)

Recommendation: Plan for full refactor when:

  • Plugin needs to support multiple apps
  • TimeSafari API changes frequently
  • Plugin team wants to decouple from TimeSafari

Approach:

  1. Phase 1: Add callback interface alongside existing code
  2. Migrate TimeSafari app to use callbacks
  3. Phase 3: Remove old integration code

Alternative: Hybrid Approach

Keep TimeSafari integration in plugin but make it optional and pluggable:

interface DailyNotificationPlugin {
  // Current approach (TimeSafari-specific)
  configure(options: ConfigureOptions): void;
  
  // New approach (generic callback)
  setContentFetcher(fetcher: NotificationContentFetcher): void;
  
  // Plugin uses callback if provided, falls back to TimeSafari integration
}

This allows:

  • Existing apps continue working (no breaking changes)
  • New apps can use callback approach
  • Plugin gradually becomes generic

Questions to Resolve

  1. Background Work: How to call JS callback from background WorkManager?

    • Native implementation required?
    • Or HTTP callback to host app endpoint?
  2. Error Handling: How does plugin handle fetcher errors?

    • Retry logic?
    • Fallback to cached content?
    • Event notifications?
  3. Configuration: Where does TimeSafari config live?

    • Host app only?
    • Or plugin still needs some config?
  4. Testing: How to test plugin without TimeSafari integration?

    • Mock fetcher interface?
    • Test fixtures?

Improvement Directive Integration

This analysis has been superseded by the Improvement Directive — Daily Notification Plugin "Integration Point" Refactor (see below). The directive provides concrete, implementable specifications with clear interfaces, test contracts, and migration paths.

Key Directive Decisions

  1. Dual-Path SPI: Native Fetcher (required for background) + Optional JS Fetcher (foreground only)
  2. Background Reliability: Native SPI only; JS fetcher explicitly disabled in background workers
  3. Reversibility: Legacy TimeSafari code behind feature flag for one minor release
  4. Test Contract: Single JSON fixture contract used by both native and JS paths

See full directive below for complete specifications.


Improvement Directive — Daily Notification Plugin "Integration Point" Refactor

Audience: Plugin & TimeSafari app teams
Goal: Convert the analysis into an implementable, low-risk refactor with crisp interfaces, background-compatibility, testability, and a reversible migration path.
Source reviewed: docs/integration-point-refactor-analysis.md


A) Decision Record (ADR-001): Architecture Shape

Decision: Ship a dual-path SPI where the plugin exposes:

  1. a Native Fetcher SPI (Android/Kotlin, iOS/Swift) for background reliability, and
  2. an Optional JS Fetcher for foreground/manual refresh & prototyping.

Why: Your analysis correctly targets decoupling, but the WorkManager/BGTaskScheduler reality means native fetchers are the only dependable background path. The JS callback remains useful for manual refresh, prefetch while the app is foregrounded, or rapid iteration.

Reversibility: Keep the legacy TimeSafari path behind a feature flag for one minor release; remove in the next major.

Exit Criteria: Both fetcher paths produce identical NotificationContent[] and pass the same test contract.


B) Public Interfaces (Final Form)

B1. TypeScript (Host App)

// core types
export interface NotificationContent {
  id: string;
  title: string;
  body?: string;
  scheduledTime?: number;  // epoch ms
  fetchTime: number;       // epoch ms
  mediaUrl?: string;
  ttlSeconds?: number;     // cache validity
  dedupeKey?: string;      // for idempotency
  priority?: 'min'|'low'|'default'|'high'|'max';
  metadata?: Record<string, unknown>;
}

export type FetchTrigger = 'background_work' | 'prefetch' | 'manual' | 'scheduled';

export interface FetchContext {
  trigger: FetchTrigger;
  scheduledTime?: number;
  fetchTime: number;
  metadata?: Record<string, unknown>;
}

export interface JsNotificationContentFetcher {
  fetchContent(context: FetchContext): Promise<NotificationContent[]>;
}

Plugin API Additions

export interface DailyNotificationPlugin {
  setJsContentFetcher(fetcher: JsNotificationContentFetcher): void;   // foreground-only
  enableNativeFetcher(enable: boolean): Promise<void>;                // default true
  setPolicy(policy: SchedulingPolicy): Promise<void>;                 // see §C2
}

B2. Android SPI (Host App native)

interface NativeNotificationContentFetcher {
    suspend fun fetchContent(context: FetchContext): List<NotificationContent>
}

data class FetchContext(
    val trigger: String,                 // enum mirror
    val scheduledTime: Long?,            // epoch ms
    val fetchTime: Long,                 // epoch ms
    val metadata: Map<String, Any?> = emptyMap()
)

Registration: Host app injects an implementation via a generated ServiceLoader or explicit setter in Application.onCreate():

DailyNotification.setNativeFetcher(TimeSafariNativeFetcher())

Note: JS fetcher remains available but is not used by background workers.


C) Scheduling & Policy Guardrails

C1. Background Reliability

  • Android: WorkManager + setRequiredNetworkType(CONNECTED), exponential backoff, content URI triggers optional, no JS bridging in background.
  • iOS: BGAppRefreshTask + BGProcessingTask with earliestBeginDate windows; keep tasks short (< 30s), aggregate fetches.

C2. SchedulingPolicy

export interface SchedulingPolicy {
  prefetchWindowMs?: number;        // how early to prefetch before schedule
  retryBackoff: { minMs: number; maxMs: number; factor: number; jitterPct: number };
  maxBatchSize?: number;            // upper bound per run
  dedupeHorizonMs?: number;         // window for dedupeKey suppression
  cacheTtlSeconds?: number;         // default if item.ttlSeconds absent
  exactAlarmsAllowed?: boolean;     // guarded by OS version & battery policy
}

C3. Idempotency & De-duplication

  • The plugin maintains a recent keys bloom/filter using dedupeKey (fallback id) for dedupeHorizonMs.
  • No notification is enqueued if (dedupeKey, title, body) unchanged within horizon.

D) Security & Config Partition

D1. Secrets & Tokens

  • JWT generation lives in the host app, not the plugin. For extra hardening consider DPoP or JWE-wrapped access tokens if your API supports it.
  • Rotate keys; keep key refs in host app secure storage; never serialize tokens through JS bridges in background.

D2. Least-Knowledge Plugin

  • Plugin stores only generic NotificationContent.
  • No TimeSafari DTOs in plugin artifacts. (As your analysis recommends.)

E) Failure Taxonomy & Handling

Class Examples Plugin Behavior
Retryable 5xx, network, timeouts Backoff per policy; stop on maxAttempts
Unretryable 4xx auth, schema error Log & surface event; skip batch; keep schedule
Partial Some items bad Enqueue valid subset; metric + structured error
Over-quota 429 Honor Retry-After if present; otherwise backoff

Cache fallback: If fetch fails, reuse unexpired cached contents (by item ttlSeconds or default policy).


F) Metrics, Logs, Events

  • Metrics: fetch_duration_ms, fetch_success, fetch_fail_class, items_fetched, items_enqueued, deduped_count, cache_hits.
  • Structured logs: Correlate by run_id, include trigger, backoff stage, and per-item dedupeKey.
  • Event bus: Emit onFetchStart, onFetchEnd, onFetchError, onEnqueue (debug UI can subscribe).

G) Test Contract & Fixtures

G1. Golden Contract

A single JSON fixture contract used by both native & JS fetchers:

{
  "context": {"trigger":"scheduled","scheduledTime":1730179200000,"fetchTime":1730175600000},
  "output": [
    {"id":"offer_123","title":"New Offer","body":"...","fetchTime":1730175600000,"dedupeKey":"offer_123","ttlSeconds":86400}
  ]
}
  • CI runs the same contract through (1) JS fetcher (foreground test harness) and (2) Native fetcher (instrumented test) and compares normalized outputs.

G2. Failure Sims

  • Network chaos (drop, latency), 401→refresh→200, 429 with Retry-After, malformed item (should be skipped, not crash).

H) Migration Plan (Minor → Major)

Minor (X.Y):

  1. Add setJsContentFetcher, enableNativeFetcher, setPolicy.
  2. Implement Native Fetcher SPI & plugin side registry.
  3. Keep legacy TimeSafari code behind useLegacyIntegration flag (default false).

Major (X+1.0):

  1. Remove legacy TimeSafari classes (TimeSafariIntegrationManager, EnhancedDailyNotificationFetcher, etc.).
  2. Publish migration guide & codemod mapping old config → SPI registration.

I) Pull-Request Breakdown (7 PRs)

  1. PR1 — SPI Shell: Kotlin/Swift interfaces, TS types, SchedulingPolicy, no behavior change.
  2. PR2 — Background Workers: Wire Native SPI in WorkManager/BGTasks; metrics skeleton; cache store.
  3. PR3 — JS Fetcher Path: Foreground bridge, event bus, dev harness screen.
  4. PR4 — Dedupe/Idempotency: Bloom/filter store + tests; dedupeKey enforcement.
  5. PR5 — Failure Policy: Retry taxonomy, backoff, cache fallback; structured errors.
  6. PR6 — Docs & Samples: Minimal host app examples (JS + Kotlin), migration notes.
  7. PR7 — Feature Flag Legacy: Guard & regression tests; changelog for major removal.

J) Acceptance Criteria (Definition of Done)

  • Background Reliability: Native fetcher runs with app killed; JS fetcher never required for background.
  • Parity: Same inputs → same NotificationContent[] via JS & Native paths (contract tests green).
  • No TimeSafari in Plugin: No imports, types, or endpoints leak into plugin modules.
  • Safety: No notifications emitted twice within dedupeHorizonMs for identical dedupeKey.
  • Observability: Metrics visible; error classes distinguish retryable vs. not; logs correlated by run.
  • Docs: Host app integration guide includes copy-paste Kotlin & TS skeletons; migration guide from legacy.
  • Battery & Quotas: Backoff respected; tasks kept <30s; policy toggles documented; exact alarms gated.

K) Open Questions (Resolve Before PR3)

  1. iOS capabilities: Which BG task identifiers and permitted intervals will Product accept (refresh vs processing)?
  2. Token Hardening: Do we want DPoP or short-lived JWE now or later?
  3. User Controls: Should we surface a per-plan "snooze/mute" map inside metadata with plugin-side persistence?

L) Example Skeletons

L1. Android Native Fetcher (Host App)

class TimeSafariNativeFetcher(
    private val api: TimeSafariApi,
    private val tokenProvider: TokenProvider
) : NativeNotificationContentFetcher {

    override suspend fun fetchContent(context: FetchContext): List<NotificationContent> {
        val jwt = tokenProvider.freshToken()
        val updates = api.updates(jwt)     // domain-specific
        val offers  = api.offers(jwt)

        return buildList {
            for (o in offers) add(
                NotificationContent(
                    id = "offer_${o.id}",
                    title = "New Offer: ${o.title}",
                    body = o.description ?: "",
                    scheduledTime = context.scheduledTime ?: (System.currentTimeMillis() + 3_600_000),
                    fetchTime = context.fetchTime,
                    mediaUrl = o.imageUrl,
                    ttlSeconds = 86_400,
                    dedupeKey = "offer_${o.id}"
                )
            )
            // … map updates similarly …
        }
    }
}

L2. JS Fetcher Registration (Foreground Only)

const fetcher: JsNotificationContentFetcher = {
  async fetchContent(ctx) {
    const jwt = await api.generateJWT(activeDid);
    const { offers, updates } = await api.fetchEverything(jwt);
    return mapToNotificationContent(offers, updates, ctx);
  }
};

DailyNotification.setJsContentFetcher(fetcher);
await DailyNotification.setPolicy({
  retryBackoff: { minMs: 2000, maxMs: 600000, factor: 2, jitterPct: 20 },
  dedupeHorizonMs: 24*60*60*1000,
  cacheTtlSeconds: 6*60*60
});

M) Docs to Produce

  • INTEGRATION_GUIDE.md (host app: Native vs JS, when to use which, copy-paste snippets).
  • SCHEDULING_POLICY.md (all knobs with OS caveats).
  • MIGRATION_GUIDE_X→X+1.md (flag removal plan, old→new mapping).
  • TEST_CONTRACTS.md (fixtures, CI commands).

N) Risk Register & Mitigations

  • Risk: JS callback used in background → fetch starves. Mitigation: Disable in background by design; lint rule + runtime guard.
  • Risk: Duplicate notifications. Mitigation: dedupeKey + horizon filter; contract tests to assert no dupes.
  • Risk: Token leakage through JS. Mitigation: Tokens stay in native path for background; JS path only in foreground; redact logs.
  • Risk: Major removal shocks dependents. Mitigation: Two-release migration window + codemod and samples.

Final Note

Your analysis nails the separation of concerns and enumerates the callback bridge challenges. This directive turns it into a concrete, staged plan with native-first background fetch, JS optionality, clear policies, and verifiable acceptance criteria while preserving a rollback path.


Status: Improvement Directive - Active Implementation Plan Next Steps: Begin PR1 (SPI Shell) implementation Dependencies: None Stakeholders: Plugin developers, TimeSafari app developers