feat(spi): add native fetcher SPI interface for background content fetching
- Add NativeNotificationContentFetcher interface for host app implementations - Add FetchContext class to pass fetch parameters (trigger, scheduledTime, fetchTime) - Add SchedulingPolicy class for retry backoff configuration - Add TypeScript definitions for content fetcher SPI in src/definitions.ts - Export SPI types from src/index.ts This enables host apps to provide their own content fetching implementation for background workers, following the Integration Point Refactor (PR2).
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* FetchContext.java
|
||||
*
|
||||
* Context information provided to content fetchers about why a fetch was triggered.
|
||||
*
|
||||
* This class is part of the Integration Point Refactor (PR1) SPI implementation.
|
||||
* It provides fetchers with metadata about the fetch request, including trigger
|
||||
* type, scheduling information, and optional metadata.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Context provided to content fetchers about why fetch was triggered
|
||||
*
|
||||
* This follows the TypeScript interface from src/types/content-fetcher.ts and
|
||||
* ensures type safety between JS and native fetcher implementations.
|
||||
*/
|
||||
public class FetchContext {
|
||||
|
||||
/**
|
||||
* Reason why the fetch was triggered
|
||||
*
|
||||
* Valid values: "background_work", "prefetch", "manual", "scheduled"
|
||||
*/
|
||||
@NonNull
|
||||
public final String trigger;
|
||||
|
||||
/**
|
||||
* When notification is scheduled for (optional, epoch milliseconds)
|
||||
*
|
||||
* Only present when trigger is "prefetch" or "scheduled"
|
||||
*/
|
||||
@Nullable
|
||||
public final Long scheduledTime;
|
||||
|
||||
/**
|
||||
* When the fetch was triggered (required, epoch milliseconds)
|
||||
*/
|
||||
public final long fetchTime;
|
||||
|
||||
/**
|
||||
* Additional context metadata (optional)
|
||||
*
|
||||
* Plugin may populate with app state, network info, etc.
|
||||
* Fetcher can use for logging, debugging, or conditional logic.
|
||||
*/
|
||||
@NonNull
|
||||
public final Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* Constructor with all fields
|
||||
*
|
||||
* @param trigger Trigger type (required)
|
||||
* @param scheduledTime Scheduled time (optional)
|
||||
* @param fetchTime When fetch triggered (required)
|
||||
* @param metadata Additional metadata (optional, can be null)
|
||||
*/
|
||||
public FetchContext(
|
||||
@NonNull String trigger,
|
||||
@Nullable Long scheduledTime,
|
||||
long fetchTime,
|
||||
@Nullable Map<String, Object> metadata) {
|
||||
if (trigger == null || trigger.isEmpty()) {
|
||||
throw new IllegalArgumentException("trigger is required");
|
||||
}
|
||||
|
||||
this.trigger = trigger;
|
||||
this.scheduledTime = scheduledTime;
|
||||
this.fetchTime = fetchTime;
|
||||
this.metadata = metadata != null ?
|
||||
Collections.unmodifiableMap(new HashMap<>(metadata)) :
|
||||
Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with minimal fields (no metadata)
|
||||
*
|
||||
* @param trigger Trigger type
|
||||
* @param scheduledTime Scheduled time (can be null)
|
||||
* @param fetchTime When fetch triggered
|
||||
*/
|
||||
public FetchContext(
|
||||
@NonNull String trigger,
|
||||
@Nullable Long scheduledTime,
|
||||
long fetchTime) {
|
||||
this(trigger, scheduledTime, fetchTime, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata value by key
|
||||
*
|
||||
* @param key Metadata key
|
||||
* @return Value or null if not present
|
||||
*/
|
||||
@Nullable
|
||||
public Object getMetadata(@NonNull String key) {
|
||||
return metadata.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if metadata contains key
|
||||
*
|
||||
* @param key Metadata key
|
||||
* @return True if key exists
|
||||
*/
|
||||
public boolean hasMetadata(@NonNull String key) {
|
||||
return metadata.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FetchContext{" +
|
||||
"trigger='" + trigger + '\'' +
|
||||
", scheduledTime=" + scheduledTime +
|
||||
", fetchTime=" + fetchTime +
|
||||
", metadataSize=" + metadata.size() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* NativeNotificationContentFetcher.java
|
||||
*
|
||||
* Service Provider Interface (SPI) for native content fetchers.
|
||||
*
|
||||
* This interface is part of the Integration Point Refactor (PR1) that allows
|
||||
* host apps to provide their own content fetching logic without hardcoding
|
||||
* TimeSafari-specific code in the plugin.
|
||||
*
|
||||
* Host apps implement this interface in native code (Kotlin/Java) and register
|
||||
* it with the plugin. The plugin calls this interface from background workers
|
||||
* (WorkManager) to fetch notification content.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Native content fetcher interface for host app implementations
|
||||
*
|
||||
* This interface enables the plugin to call host app's native code for
|
||||
* fetching notification content. This is the ONLY path used by background
|
||||
* workers, as JavaScript bridges are unreliable in background contexts.
|
||||
*
|
||||
* Implementation Requirements:
|
||||
* - Must be thread-safe (may be called from WorkManager background threads)
|
||||
* - Must complete within reasonable time (plugin enforces timeout)
|
||||
* - Should return empty list on failure rather than throwing exceptions
|
||||
* - Should handle errors gracefully and log for debugging
|
||||
*
|
||||
* Example Implementation:
|
||||
* <pre>
|
||||
* class TimeSafariNativeFetcher implements NativeNotificationContentFetcher {
|
||||
* private final TimeSafariApi api;
|
||||
* private final TokenProvider tokenProvider;
|
||||
*
|
||||
* @Override
|
||||
* public CompletableFuture<List<NotificationContent>> fetchContent(
|
||||
* FetchContext context) {
|
||||
* return CompletableFuture.supplyAsync(() -> {
|
||||
* try {
|
||||
* String jwt = tokenProvider.freshToken();
|
||||
* // Fetch from TimeSafari API
|
||||
* // Convert to NotificationContent[]
|
||||
* return notificationContents;
|
||||
* } catch (Exception e) {
|
||||
* Log.e("Fetcher", "Fetch failed", e);
|
||||
* return Collections.emptyList();
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public interface NativeNotificationContentFetcher {
|
||||
|
||||
/**
|
||||
* Fetch notification content from external source
|
||||
*
|
||||
* This method is called by the plugin when:
|
||||
* - Background fetch work is triggered (WorkManager)
|
||||
* - Prefetch is scheduled before notification time
|
||||
* - Manual refresh is requested (if native fetcher enabled)
|
||||
*
|
||||
* The plugin will:
|
||||
* - Enforce a timeout (default 30 seconds, configurable via SchedulingPolicy)
|
||||
* - Handle empty lists gracefully (no notifications scheduled)
|
||||
* - Log errors for debugging
|
||||
* - Retry on failure based on SchedulingPolicy
|
||||
*
|
||||
* @param context Context about why fetch was triggered, including
|
||||
* trigger type, scheduled time, and optional metadata
|
||||
* @return CompletableFuture that resolves to list of NotificationContent.
|
||||
* Empty list indicates no content available (not an error).
|
||||
* The future should complete exceptionally only on unrecoverable errors.
|
||||
*/
|
||||
@NonNull
|
||||
CompletableFuture<List<NotificationContent>> fetchContent(@NonNull FetchContext context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* SchedulingPolicy.java
|
||||
*
|
||||
* Policy configuration for notification scheduling, fetching, and retry behavior.
|
||||
*
|
||||
* This class is part of the Integration Point Refactor (PR1) SPI implementation.
|
||||
* It allows host apps to configure scheduling behavior including retry backoff,
|
||||
* prefetch timing, deduplication windows, and cache TTL.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Scheduling policy configuration
|
||||
*
|
||||
* Controls how the plugin schedules fetches, handles retries, manages
|
||||
* deduplication, and enforces TTL policies.
|
||||
*
|
||||
* This follows the TypeScript interface from src/types/content-fetcher.ts
|
||||
* and ensures consistency between JS and native configuration.
|
||||
*/
|
||||
public class SchedulingPolicy {
|
||||
|
||||
/**
|
||||
* How early to prefetch before scheduled notification time (milliseconds)
|
||||
*
|
||||
* Example: If set to 300000 (5 minutes), and notification is scheduled for
|
||||
* 8:00 AM, the fetch will be triggered at 7:55 AM.
|
||||
*
|
||||
* Default: 5 minutes (300000ms)
|
||||
*/
|
||||
@Nullable
|
||||
public Long prefetchWindowMs;
|
||||
|
||||
/**
|
||||
* Retry backoff configuration (required)
|
||||
*
|
||||
* Controls exponential backoff behavior for failed fetches
|
||||
*/
|
||||
@NonNull
|
||||
public RetryBackoff retryBackoff;
|
||||
|
||||
/**
|
||||
* Maximum items to fetch per batch
|
||||
*
|
||||
* Limits the number of NotificationContent items that can be fetched
|
||||
* in a single operation. Helps prevent oversized responses.
|
||||
*
|
||||
* Default: 50
|
||||
*/
|
||||
@Nullable
|
||||
public Integer maxBatchSize;
|
||||
|
||||
/**
|
||||
* Deduplication window (milliseconds)
|
||||
*
|
||||
* Prevents duplicate notifications within this time window. Plugin
|
||||
* uses dedupeKey (or id) to detect duplicates.
|
||||
*
|
||||
* Default: 24 hours (86400000ms)
|
||||
*/
|
||||
@Nullable
|
||||
public Long dedupeHorizonMs;
|
||||
|
||||
/**
|
||||
* Default cache TTL if item doesn't specify ttlSeconds (seconds)
|
||||
*
|
||||
* Used when NotificationContent doesn't have ttlSeconds set.
|
||||
* Determines how long cached content remains valid.
|
||||
*
|
||||
* Default: 6 hours (21600 seconds)
|
||||
*/
|
||||
@Nullable
|
||||
public Integer cacheTtlSeconds;
|
||||
|
||||
/**
|
||||
* Whether exact alarms are allowed (Android 12+)
|
||||
*
|
||||
* Controls whether plugin should attempt to use exact alarms.
|
||||
* Requires SCHEDULE_EXACT_ALARM permission on Android 12+.
|
||||
*
|
||||
* Default: false (use inexact alarms)
|
||||
*/
|
||||
@Nullable
|
||||
public Boolean exactAlarmsAllowed;
|
||||
|
||||
/**
|
||||
* Fetch timeout in milliseconds
|
||||
*
|
||||
* Maximum time to wait for native fetcher to complete.
|
||||
* Plugin enforces this timeout when calling fetchContent().
|
||||
*
|
||||
* Default: 30 seconds (30000ms)
|
||||
*/
|
||||
@Nullable
|
||||
public Long fetchTimeoutMs;
|
||||
|
||||
/**
|
||||
* Default constructor with required field
|
||||
*
|
||||
* @param retryBackoff Retry backoff configuration (required)
|
||||
*/
|
||||
public SchedulingPolicy(@NonNull RetryBackoff retryBackoff) {
|
||||
this.retryBackoff = retryBackoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry backoff configuration
|
||||
*
|
||||
* Controls exponential backoff behavior for retryable failures.
|
||||
* Delay = min(max(minMs, lastDelay * factor), maxMs) * (1 + jitterPct/100 * random)
|
||||
*/
|
||||
public static class RetryBackoff {
|
||||
|
||||
/**
|
||||
* Minimum delay between retries (milliseconds)
|
||||
*
|
||||
* First retry will wait at least this long.
|
||||
* Default: 2000ms (2 seconds)
|
||||
*/
|
||||
public long minMs;
|
||||
|
||||
/**
|
||||
* Maximum delay between retries (milliseconds)
|
||||
*
|
||||
* Retry delay will never exceed this value.
|
||||
* Default: 600000ms (10 minutes)
|
||||
*/
|
||||
public long maxMs;
|
||||
|
||||
/**
|
||||
* Exponential backoff multiplier
|
||||
*
|
||||
* Each retry multiplies previous delay by this factor.
|
||||
* Example: factor=2 means delays: 2s, 4s, 8s, 16s, ...
|
||||
* Default: 2.0
|
||||
*/
|
||||
public double factor;
|
||||
|
||||
/**
|
||||
* Jitter percentage (0-100)
|
||||
*
|
||||
* Adds randomness to prevent thundering herd.
|
||||
* Final delay = calculatedDelay * (1 + jitterPct/100 * random(0-1))
|
||||
* Default: 20 (20% jitter)
|
||||
*/
|
||||
public int jitterPct;
|
||||
|
||||
/**
|
||||
* Default constructor with sensible defaults
|
||||
*/
|
||||
public RetryBackoff() {
|
||||
this.minMs = 2000;
|
||||
this.maxMs = 600000;
|
||||
this.factor = 2.0;
|
||||
this.jitterPct = 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with all parameters
|
||||
*
|
||||
* @param minMs Minimum delay (ms)
|
||||
* @param maxMs Maximum delay (ms)
|
||||
* @param factor Exponential multiplier
|
||||
* @param jitterPct Jitter percentage (0-100)
|
||||
*/
|
||||
public RetryBackoff(long minMs, long maxMs, double factor, int jitterPct) {
|
||||
this.minMs = minMs;
|
||||
this.maxMs = maxMs;
|
||||
this.factor = factor;
|
||||
this.jitterPct = jitterPct;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default policy with sensible defaults
|
||||
*
|
||||
* @return Default SchedulingPolicy instance
|
||||
*/
|
||||
@NonNull
|
||||
public static SchedulingPolicy createDefault() {
|
||||
SchedulingPolicy policy = new SchedulingPolicy(new RetryBackoff());
|
||||
policy.prefetchWindowMs = 300000L; // 5 minutes
|
||||
policy.maxBatchSize = 50;
|
||||
policy.dedupeHorizonMs = 86400000L; // 24 hours
|
||||
policy.cacheTtlSeconds = 21600; // 6 hours
|
||||
policy.exactAlarmsAllowed = false;
|
||||
policy.fetchTimeoutMs = 30000L; // 30 seconds
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
// Import SPI types from content-fetcher.ts
|
||||
import type {
|
||||
SchedulingPolicy,
|
||||
JsNotificationContentFetcher
|
||||
} from './types/content-fetcher';
|
||||
|
||||
export interface NotificationResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -442,6 +448,43 @@ export interface DailyNotificationPlugin {
|
||||
* Update an existing daily reminder
|
||||
*/
|
||||
updateDailyReminder(reminderId: string, options: DailyReminderOptions): Promise<void>;
|
||||
|
||||
// Integration Point Refactor (PR1): SPI Registration Methods
|
||||
|
||||
/**
|
||||
* Set JavaScript content fetcher for foreground operations
|
||||
*
|
||||
* NOTE: This is a stub in PR1. Full implementation coming in PR3.
|
||||
* JS fetchers are ONLY used for foreground/manual refresh.
|
||||
* Background workers must use native fetcher.
|
||||
*
|
||||
* @param fetcher JavaScript fetcher implementation
|
||||
*/
|
||||
setJsContentFetcher(fetcher: JsNotificationContentFetcher): void;
|
||||
|
||||
/**
|
||||
* Enable or disable native fetcher
|
||||
*
|
||||
* Native fetcher is required for background workers. If disabled,
|
||||
* background fetches will fail gracefully.
|
||||
*
|
||||
* @param enable Whether to enable native fetcher
|
||||
* @returns Promise with enabled and registered status
|
||||
*/
|
||||
enableNativeFetcher(enable: boolean): Promise<{
|
||||
enabled: boolean;
|
||||
registered: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Set scheduling policy configuration
|
||||
*
|
||||
* Updates the scheduling policy used by the plugin for retry backoff,
|
||||
* prefetch timing, deduplication, and cache TTL.
|
||||
*
|
||||
* @param policy Scheduling policy configuration
|
||||
*/
|
||||
setPolicy(policy: SchedulingPolicy): Promise<void>;
|
||||
}
|
||||
|
||||
// Phase 1: TimeSafari Endorser.ch API Interfaces
|
||||
|
||||
@@ -13,6 +13,7 @@ const DailyNotification = registerPlugin<DailyNotificationPlugin>('DailyNotifica
|
||||
observability.logEvent('INFO', EVENT_CODES.FETCH_START, 'Daily Notification Plugin initialized');
|
||||
|
||||
export * from './definitions';
|
||||
export * from './types/content-fetcher'; // Export SPI types
|
||||
export * from './observability';
|
||||
export * from './timesafari-integration';
|
||||
export * from './timesafari-storage-adapter';
|
||||
|
||||
Reference in New Issue
Block a user