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
|
||||
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 skippedCount = 0;
|
||||
for (NotificationContent content : contents) {
|
||||
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
|
||||
storage.saveNotificationContent(content);
|
||||
|
||||
@@ -275,7 +301,8 @@ public class DailyNotificationFetchWorker extends Worker {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -145,6 +145,97 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
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
|
||||
*/
|
||||
@@ -962,6 +1053,12 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
try {
|
||||
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
|
||||
long fetchTime = scheduledTime - TimeUnit.MINUTES.toMillis(5);
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
Reference in New Issue
Block a user