Skip to content

Conversation

@thomasttvo
Copy link
Collaborator

@thomasttvo thomasttvo commented Oct 17, 2025

Fixes notification race condition where queued uploads could overwrite the active upload's notification settings. The bug occurred because each Upload object carried its own notification config—when a Wi-Fi-only upload was queued while a regular upload was running, the queued upload could touch the shared system notification and apply its wifiOnly=true preference before actually executing.

Centralization solves this by making UploadNotification the single source of truth for notification state. It tracks exactly one activeUpload at a time (synchronized), and notification content is built by reading getActiveUpload()?.wifiOnly rather than arbitrary upload configs. Queued uploads can exist in memory with their settings, but they can't affect the notification until they call setActiveUpload() when they start executing. This eliminates the race condition—only the actively running upload determines what the user sees.

Test Plan:

  1. Start a regular upload (non-Wi-Fi-only)
  2. While it's running, queue a Wi-Fi-only upload
  3. Verify notification shows correct connectivity preference for the actively running upload (not the queued one)
  4. Wait for first upload to complete, verify notification updates to reflect the now-active Wi-Fi-only upload
  5. Test notification persistence across app kills and device restarts
  6. Verify both uploads complete successfully

Note

Android notification handling consolidated and race fixed

  • Introduces UploadNotification singleton and Connectivity.fetch to build/update a single foreground notification reflecting active upload or queue needs (WiFi vs internet), with shared maxRetries
  • Removes per-upload Android notification options from Upload; adds initialize(options) to set global notification config (JS/types updated)
  • Refactors UploadWorker to use centralized notification/ connectivity, mark active upload, and update notification on progress/success/error; retries now use global maxRetries
  • Enhances UploadProgress to track wifiOnly and expose hasNonWifiOnlyUploads() for accurate notification messages
  • Updates example app to call Upload.initialize and starts multiple uploads; bumps package to 7.6.0

Written by Cursor Bugbot for commit c2c9922. Configure here.

NoWifi, NoInternet, Ok;

companion object {
fun fetch(context: Context, wifiOnly: Boolean): Connectivity {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved from UploadWorker.ts no logical change

}

// builds the notification required to enable Foreground mode
fun build(context: Context): Notification {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved from UploadWorker.ts. Not much has changed except for val wifiOnly = getActiveUpload()?.wifiOnly ?: false

if (this.activeUpload?.id == upload.id) this.activeUpload = null
}

fun setOptions(opts: ReadableMap) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these options are moved over from the Upload class, so they are now global options instead of per-Upload options

Copy link

@elliottkember elliottkember left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

setProgress(undefined);
console.log('Upload error!', err);
});
for (let i = 0; i < 100; i++) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment explaining the magic number 100 and why we have to perform Upload.startUpload 100 times?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah this is just the test app, but it's good to add a comment still

thomasvo and others added 6 commits December 11, 2025 16:42
- Name response variable in use block (UploadUtils.kt)
- Fix shadowed it in takeIf lambda (UploadUtils.kt)
- Rename set() to setIfNotNull() (UploadProgress.kt)
- Add complete() method to Progress class (UploadProgress.kt)
- Rename clearIfNeeded() to clearIfCompleted() (UploadProgress.kt)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@thomasttvo thomasttvo changed the base branch from thomas/simplify-progress to master January 21, 2026 01:41
@thomasttvo
Copy link
Collaborator Author

bugbot run

cursor[bot]

This comment was marked as outdated.

thomasvo and others added 4 commits January 21, 2026 15:02
Ensures the notification shows correct "Waiting for WiFi/Internet"
title when network degrades after initial foreground setup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@thomasttvo thomasttvo force-pushed the thomas/fix-unsynced-notification branch from cbc41c0 to c2c9922 Compare January 22, 2026 01:53
@thomasttvo
Copy link
Collaborator Author

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

channel = opts.getString("notificationChannel")
?: throw MissingOptionException("notificationChannel")
maxRetries = if (opts.hasKey("maxRetries")) opts.getInt("maxRetries") else 5
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread-safety gap in setOptions vs concurrent readers

Medium Severity

The setOptions() method writes to shared properties (id, title, channel, maxRetries, etc.) without @Synchronized, while build(), getForegroundInfo(), and checkRetry() read these properties from background worker threads. This creates a race condition where a worker resuming from a previous session could call build() while setOptions() is executing, resulting in inconsistent notification content (e.g., old channel ID paired with new title). Other methods in UploadNotification that access activeUpload are correctly synchronized, but this synchronization pattern isn't applied to the configuration properties.

Additional Locations (1)

Fix in Cursor Fix in Web

val url: String,
val path: String,
val method: String,
val maxRetries: Int,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resumed workers use uninitialized notification settings after app restart

Medium Severity

The notification settings (notificationChannel, notificationId, etc.) were removed from the Upload class, so they're no longer serialized with WorkManager input data. When workers resume after an app restart (process death), they depend on UploadNotification singleton state which hasn't been initialized yet—initialize() is called by JS code that may not have executed. The worker uses the default channel "File Uploads" which likely doesn't exist, causing the foreground notification to fail. This could result in missing notifications or setForeground() failures that prevent the worker from running properly.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants