fix(worker): prevent duplicate notifications from prefetch
Add duplicate checking in handleSuccessfulFetch() to ensure one prefetch creates at most one notification per scheduled time. This prevents prefetch from creating duplicate notifications when a manual notification already exists for the same time. - Check existing notifications before saving prefetch-created content - Skip notification creation if duplicate found (within 1 minute tolerance) - Add null check for fetcher in scheduleBackgroundFetch() with error logging - Log skipped duplicates for debugging Ensures one prefetch → one notification pairing and prevents duplicate notifications from firing at the same time.
This commit is contained in:
@@ -258,10 +258,36 @@ public class DailyNotificationFetchWorker extends Worker {
|
|||||||
// Update last fetch time
|
// Update last fetch time
|
||||||
storage.setLastFetchTime(System.currentTimeMillis());
|
storage.setLastFetchTime(System.currentTimeMillis());
|
||||||
|
|
||||||
// Save all contents and schedule notifications
|
// Get existing notifications for duplicate checking (prevent prefetch from creating duplicate)
|
||||||
|
java.util.List<NotificationContent> existingNotifications = storage.getAllNotifications();
|
||||||
|
long toleranceMs = 60 * 1000; // 1 minute tolerance for DST shifts
|
||||||
|
|
||||||
|
// Save all contents and schedule notifications (with duplicate checking)
|
||||||
int scheduledCount = 0;
|
int scheduledCount = 0;
|
||||||
|
int skippedCount = 0;
|
||||||
for (NotificationContent content : contents) {
|
for (NotificationContent content : contents) {
|
||||||
try {
|
try {
|
||||||
|
// Check for duplicate notification at the same scheduled time
|
||||||
|
// This ensures prefetch doesn't create a duplicate if a manual notification already exists
|
||||||
|
boolean duplicateFound = false;
|
||||||
|
for (NotificationContent existing : existingNotifications) {
|
||||||
|
if (Math.abs(existing.getScheduledTime() - content.getScheduledTime()) <= toleranceMs) {
|
||||||
|
Log.w(TAG, "PR2: DUPLICATE_SKIP id=" + content.getId() +
|
||||||
|
" existing_id=" + existing.getId() +
|
||||||
|
" scheduled_time=" + content.getScheduledTime() +
|
||||||
|
" time_diff_ms=" + Math.abs(existing.getScheduledTime() - content.getScheduledTime()));
|
||||||
|
duplicateFound = true;
|
||||||
|
skippedCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateFound) {
|
||||||
|
// Skip this notification - one already exists for this time
|
||||||
|
// Ensures one prefetch → one notification pairing
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Save content to storage
|
// Save content to storage
|
||||||
storage.saveNotificationContent(content);
|
storage.saveNotificationContent(content);
|
||||||
|
|
||||||
@@ -275,7 +301,8 @@ public class DailyNotificationFetchWorker extends Worker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "PR2: Successful fetch handling completed - " + scheduledCount + "/" +
|
Log.i(TAG, "PR2: Successful fetch handling completed - " + scheduledCount + "/" +
|
||||||
contents.size() + " notifications scheduled");
|
contents.size() + " notifications scheduled" +
|
||||||
|
(skippedCount > 0 ? ", " + skippedCount + " duplicates skipped" : ""));
|
||||||
// TODO PR2: Record metrics (items_enqueued=scheduledCount)
|
// TODO PR2: Record metrics (items_enqueued=scheduledCount)
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -145,6 +145,97 @@ public class DailyNotificationPlugin extends Plugin {
|
|||||||
return nativeFetcher;
|
return nativeFetcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure native fetcher with API credentials (cross-platform method)
|
||||||
|
*
|
||||||
|
* <p>This plugin method receives configuration from TypeScript and passes it directly
|
||||||
|
* to the registered native fetcher implementation. This approach keeps the TypeScript
|
||||||
|
* interface cross-platform (works on Android, iOS, and web) without requiring
|
||||||
|
* platform-specific storage mechanisms.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Usage Flow:</b></p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Host app registers native fetcher in {@code Application.onCreate()}</li>
|
||||||
|
* <li>TypeScript calls this method with API credentials</li>
|
||||||
|
* <li>Plugin validates parameters and calls {@code nativeFetcher.configure()}</li>
|
||||||
|
* <li>Native fetcher stores configuration for use in {@code fetchContent()}</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p><b>When to call:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>After app startup, once API credentials are available</li>
|
||||||
|
* <li>After user login/authentication, when activeDid changes</li>
|
||||||
|
* <li>When API server URL changes (e.g., switching between dev/staging/prod)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>Error Handling:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Rejects if required parameters are missing</li>
|
||||||
|
* <li>Rejects if no native fetcher is registered</li>
|
||||||
|
* <li>Returns error if native fetcher's {@code configure()} throws exception</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>Example TypeScript Usage:</b></p>
|
||||||
|
* <pre>{@code
|
||||||
|
* import { DailyNotification } from '@capacitor-community/daily-notification';
|
||||||
|
*
|
||||||
|
* await DailyNotification.configureNativeFetcher({
|
||||||
|
* apiBaseUrl: 'http://10.0.2.2:3000',
|
||||||
|
* activeDid: 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F',
|
||||||
|
* jwtSecret: 'test-jwt-secret-for-development'
|
||||||
|
* });
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param call Plugin call containing configuration parameters:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code apiBaseUrl} (required): Base URL for API server.
|
||||||
|
* Android emulator: "http://10.0.2.2:3000" (maps to host localhost:3000).
|
||||||
|
* iOS simulator: "http://localhost:3000".
|
||||||
|
* Production: "https://api.timesafari.com"</li>
|
||||||
|
* <li>{@code activeDid} (required): Active DID for authentication.
|
||||||
|
* Format: "did:ethr:0x..."</li>
|
||||||
|
* <li>{@code jwtSecret} (required): JWT secret for signing tokens.
|
||||||
|
* Keep secure in production.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @throws PluginException if configuration fails (rejected via call.reject())
|
||||||
|
*
|
||||||
|
* @see NativeNotificationContentFetcher#configure(String, String, String)
|
||||||
|
*/
|
||||||
|
@PluginMethod
|
||||||
|
public void configureNativeFetcher(PluginCall call) {
|
||||||
|
try {
|
||||||
|
String apiBaseUrl = call.getString("apiBaseUrl");
|
||||||
|
String activeDid = call.getString("activeDid");
|
||||||
|
String jwtSecret = call.getString("jwtSecret");
|
||||||
|
|
||||||
|
if (apiBaseUrl == null || activeDid == null || jwtSecret == null) {
|
||||||
|
call.reject("Missing required parameters: apiBaseUrl, activeDid, and jwtSecret are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeNotificationContentFetcher fetcher = getNativeFetcher();
|
||||||
|
if (fetcher == null) {
|
||||||
|
call.reject("No native fetcher registered. Register one in Application.onCreate() before configuring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "SPI: Configuring native fetcher - apiBaseUrl: " +
|
||||||
|
apiBaseUrl.substring(0, Math.min(50, apiBaseUrl.length())) +
|
||||||
|
"... activeDid: " + activeDid.substring(0, Math.min(30, activeDid.length())) + "...");
|
||||||
|
|
||||||
|
// Call configure on the native fetcher (defaults to no-op if not implemented)
|
||||||
|
fetcher.configure(apiBaseUrl, activeDid, jwtSecret);
|
||||||
|
|
||||||
|
Log.i(TAG, "SPI: Native fetcher configured successfully");
|
||||||
|
call.resolve();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "SPI: Error configuring native fetcher", e);
|
||||||
|
call.reject("Failed to configure native fetcher: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the plugin and create notification channel
|
* Initialize the plugin and create notification channel
|
||||||
*/
|
*/
|
||||||
@@ -962,6 +1053,12 @@ public class DailyNotificationPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
Log.i(TAG, "DN|SCHEDULE_FETCH_START time=" + scheduledTime + " current=" + System.currentTimeMillis());
|
Log.i(TAG, "DN|SCHEDULE_FETCH_START time=" + scheduledTime + " current=" + System.currentTimeMillis());
|
||||||
|
|
||||||
|
// Check if fetcher is initialized
|
||||||
|
if (fetcher == null) {
|
||||||
|
Log.e(TAG, "DN|SCHEDULE_FETCH_ERR fetcher is null - cannot schedule prefetch. Plugin may not be fully loaded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule fetch 5 minutes before notification
|
// Schedule fetch 5 minutes before notification
|
||||||
long fetchTime = scheduledTime - TimeUnit.MINUTES.toMillis(5);
|
long fetchTime = scheduledTime - TimeUnit.MINUTES.toMillis(5);
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|||||||
Reference in New Issue
Block a user