@kesha-antonov/react-native-background-downloader 4.0.0-alpha.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ ## v4.0.0
4
+
5
+ > 📖 **Upgrading from v3.x?** See the [Migration Guide](./MIGRATION.md) for detailed instructions.
6
+
7
+ ### ⚠️ Breaking Changes
8
+
9
+ - **API Renamed:** `checkForExistingDownloads()` → `getExistingDownloadTasks()` - Now returns a Promise with better naming
10
+ - **API Renamed:** `download()` → `createDownloadTask()` - Downloads now require explicit `.start()` call
11
+ - **Download Tasks Start Explicitly:** Tasks created with `createDownloadTask()` are now in `PENDING` state and must call `.start()` to begin downloading
12
+ - **New Config Option:** Added `progressMinBytes` to `setConfig()` - controls minimum bytes change before progress callback fires (default: 1MB)
13
+ - **Source Structure Changed:** Code moved from `lib/` to `src/` directory with proper TypeScript types
14
+
15
+ ### ✨ New Features
16
+
17
+ - **React Native New Architecture Support:** Full TurboModules support for both iOS and Android
18
+ - **Expo Config Plugin:** Added automatic iOS native code integration for Expo projects via `app.plugin.js`
19
+ - **Android Kotlin Migration:** All Java code converted to Kotlin
20
+ - **`maxRedirects` Option:** Configure maximum redirects for Android downloads (resolves #15)
21
+ - **`progressMinBytes` Option:** Hybrid progress reporting - callbacks fire based on time interval OR bytes downloaded
22
+ - **Android 15+ Support:** Added support for 16KB memory page sizes
23
+ - **Architecture Fallback:** Comprehensive x86/ARMv7 support with SharedPreferences fallback
24
+
25
+ ### 🐛 Bug Fixes
26
+
27
+ - **iOS Pause/Resume:** Fixed pause and resume functionality on iOS
28
+ - **RN 0.78+ Compatibility:** Fixed bridge checks with safe emitter checks
29
+ - **New Architecture Events:** Fixed `downloadBegin` and `downloadProgress` events emission
30
+ - **Android Background Downloads:** Fixed completed files not moving to destination
31
+ - **Progress Callback Unknown Total:** Fixed progress callback not firing when total bytes unknown
32
+ - **Android 12 MMKV Crash:** Added robust error handling
33
+ - **`checkForExistingDownloads` TypeError:** Fixed TypeError on Android with architecture fallback
34
+ - **Firebase Performance Compatibility:** Fixed `completeHandler` method compatibility on Android
35
+ - **Slow Connection Handling:** Better handling of slow-responding URLs with timeouts
36
+ - **Android OldArch Export:** Fixed module method export issue (#79)
37
+ - **MMKV Compatibility:** Support for react-native-mmkv 4+ with mmkv-shared dependency
38
+
39
+ ### 📦 Dependencies & Infrastructure
40
+
41
+ - **React Native:** Updated example app to RN 0.81.4
42
+ - **TypeScript:** Full TypeScript types in `src/types.ts`
43
+ - **iOS Native:** Converted from `.m` to `.mm` (Objective-C++)
44
+ - **Package Manager:** Switched to yarn as preferred package manager
45
+
46
+ ### 📚 Documentation
47
+
48
+ - Added documentation for `progressMinBytes` option
49
+ - Updated README for React Native 0.77+ instructions
50
+ - Improved Expo config plugin examples
package/README.md CHANGED
@@ -1,16 +1,29 @@
1
- ![react-native-background-downloader banner](https://d1w2zhnqcy4l8f.cloudfront.net/content/falcon/production/projects/V5EEOX_fast/RNBD-190702083358.png)
1
+
2
+ <p align="center">
3
+ <img width="300" src="https://github.com/user-attachments/assets/25e89808-9eb7-42b2-8031-b48d8c24796c" />
4
+ </p>
2
5
 
3
6
  [![npm version](https://badge.fury.io/js/@kesha-antonov%2Freact-native-background-downloader.svg)](https://badge.fury.io/js/@kesha-antonov%2Freact-native-background-downloader)
4
7
 
5
- ## 🚧👷 Important notice
8
+ ## 🎉 Version 4.0.0 Released!
9
+
10
+ **v4.0.0** is now available with full **React Native New Architecture (TurboModules)** support!
6
11
 
7
- There's currently WIP going on to make the library support New Architecture. If you have any issues, please report them. If you want to contribute, please do so.
12
+ ### What's New
13
+ - ✅ Full TurboModules support for iOS and Android
14
+ - ✅ Expo Config Plugin for automatic iOS setup
15
+ - ✅ Android code converted to Kotlin
16
+ - ✅ Full TypeScript support
17
+ - ✅ New `progressMinBytes` option for hybrid progress reporting
18
+ - ✅ `maxRedirects` option for Android
8
19
 
9
- The most stable version is `3.2.6`. If you want to use the latest version, please be aware that it's a work in progress.
20
+ ### Upgrading from v3.x?
21
+ 📖 See the [Migration Guide](./MIGRATION.md) for detailed upgrade instructions and breaking changes.
10
22
 
11
- Readme for that version: [3.2.6 readme](https://github.com/kesha-antonov/react-native-background-downloader/blob/8f4b8a844a2d7f00d1558f6ea65bac94c8dd6fc9/README.md)
23
+ 📋 See the [Changelog](./CHANGELOG.md) for the full list of changes.
12
24
 
13
- I'm working on making the library compatible with the New Architecture while keeping backward compatibility with the old one. I plan to use Nitro Modules so apps on the old architecture can also benefit from the performance improvements.
25
+ ### Looking for v3.2.6?
26
+ If you need the previous stable version: [3.2.6 readme](https://github.com/kesha-antonov/react-native-background-downloader/blob/8f4b8a844a2d7f00d1558f6ea65bac94c8dd6fc9/README.md)
14
27
 
15
28
  # @kesha-antonov/react-native-background-downloader
16
29
 
@@ -51,15 +64,6 @@ Then:
51
64
  cd ios && pod install
52
65
  ```
53
66
 
54
- ### New Architecture Support
55
-
56
- This library supports React Native's New Architecture (Fabric + TurboModules) starting from React Native 0.70+.
57
-
58
- #### Automatic Detection
59
- The library automatically detects whether the New Architecture is enabled in your app and uses the appropriate implementation:
60
- - **New Architecture**: Uses TurboModules for optimal performance
61
- - **Legacy Architecture**: Uses the traditional bridge implementation
62
-
63
67
  #### Manual Setup (Advanced)
64
68
  If you need to manually configure the package for New Architecture:
65
69
 
@@ -43,6 +43,8 @@ static CompletionHandler storedCompletionHandler;
43
43
  NSMutableDictionary<NSString *, NSDictionary *> *progressReports;
44
44
  NSMutableDictionary<NSString *, NSNumber *> *idToLastBytesMap;
45
45
  NSMutableSet<NSString *> *idsToPauseSet;
46
+ // Tracks tasks that have already been retried once after a decode error (-1015)
47
+ NSMutableSet<NSString *> *decodeErrorRetriedIds;
46
48
  float progressInterval;
47
49
  int64_t progressMinBytes;
48
50
  NSDate *lastProgressReportedAt;
@@ -56,16 +58,16 @@ RCT_EXPORT_MODULE();
56
58
  // Enable interop layer so NativeModules.RNBackgroundDownloader is available
57
59
  // This is required for NativeEventEmitter to work with TurboModules
58
60
  + (BOOL)requiresMainQueueSetup {
59
- return YES;
61
+ return NO;
60
62
  }
61
63
 
62
64
  #pragma mark - Helper methods
63
65
 
64
66
  - (BOOL)canSendEvents {
65
- // Always return YES - let RCTEventEmitter handle listener management
66
- // The warning "Sending X with no listeners registered" is harmless
67
- // and events will be properly received once JS listeners are set up
68
- return YES;
67
+ // Only send events if JavaScript has fully loaded
68
+ // This prevents "Invariant Violation: Failed to call into JavaScript module method"
69
+ // errors that occur when native code tries to send events before the JS bridge is ready
70
+ return isJavascriptLoaded;
69
71
  }
70
72
 
71
73
  - (RNBGDTaskConfig *)configForTask:(NSURLSessionTask *)task {
@@ -124,6 +126,11 @@ RCT_EXPORT_MODULE();
124
126
  self = [super initWithDisabledObservation];
125
127
  #endif
126
128
  if (self) {
129
+ #ifdef RCT_NEW_ARCH_ENABLED
130
+ // TurboModules are lazily initialized when JS first accesses them,
131
+ // so JavaScript is ready when init is called
132
+ isJavascriptLoaded = YES;
133
+ #endif
127
134
  [MMKV initializeMMKV:nil];
128
135
  mmkv = [MMKV mmkvWithID:@"RNBackgroundDownloader"];
129
136
 
@@ -160,6 +167,7 @@ RCT_EXPORT_MODULE();
160
167
  int64_t progressMinBytesScope = [mmkv getInt64ForKey:PROGRESS_MIN_BYTES_KEY];
161
168
  progressMinBytes = progressMinBytesScope > 0 ? progressMinBytesScope : 0;
162
169
  lastProgressReportedAt = [[NSDate alloc] init];
170
+ decodeErrorRetriedIds = [[NSMutableSet alloc] init];
163
171
 
164
172
  [self registerBridgeListener];
165
173
  }
@@ -365,7 +373,7 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
365
373
  }
366
374
  }
367
375
 
368
- - (void)pauseTask:(NSString *)identifier {
376
+ - (void)pauseTaskInternal:(NSString *)identifier {
369
377
  DLog(identifier, @"[RNBackgroundDownloader] - [pauseTask]");
370
378
  @synchronized (sharedLock) {
371
379
  NSURLSessionDownloadTask *task = self->idToTaskMap[identifier];
@@ -389,13 +397,17 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
389
397
  }
390
398
  }
391
399
 
392
- #ifndef RCT_NEW_ARCH_ENABLED
400
+ #ifdef RCT_NEW_ARCH_ENABLED
401
+ - (void)pauseTask:(NSString *)identifier {
402
+ [self pauseTaskInternal:identifier];
403
+ }
404
+ #else
393
405
  RCT_EXPORT_METHOD(pauseTask: (NSString *)id) {
394
- [self pauseTask:id];
406
+ [self pauseTaskInternal:id];
395
407
  }
396
408
  #endif
397
409
 
398
- - (void)resumeTask:(NSString *)identifier {
410
+ - (void)resumeTaskInternal:(NSString *)identifier {
399
411
  DLog(identifier, @"[RNBackgroundDownloader] - [resumeTask]");
400
412
  @synchronized (sharedLock) {
401
413
  [self lazyRegisterSession];
@@ -446,13 +458,17 @@ RCT_EXPORT_METHOD(pauseTask: (NSString *)id) {
446
458
  }
447
459
  }
448
460
 
449
- #ifndef RCT_NEW_ARCH_ENABLED
461
+ #ifdef RCT_NEW_ARCH_ENABLED
462
+ - (void)resumeTask:(NSString *)identifier {
463
+ [self resumeTaskInternal:identifier];
464
+ }
465
+ #else
450
466
  RCT_EXPORT_METHOD(resumeTask: (NSString *)id) {
451
- [self resumeTask:id];
467
+ [self resumeTaskInternal:id];
452
468
  }
453
469
  #endif
454
470
 
455
- - (void)stopTask:(NSString *)identifier {
471
+ - (void)stopTaskInternal:(NSString *)identifier {
456
472
  DLog(identifier, @"[RNBackgroundDownloader] - [stopTask]");
457
473
  @synchronized (sharedLock) {
458
474
  NSURLSessionDownloadTask *task = self->idToTaskMap[identifier];
@@ -465,9 +481,13 @@ RCT_EXPORT_METHOD(resumeTask: (NSString *)id) {
465
481
  }
466
482
  }
467
483
 
468
- #ifndef RCT_NEW_ARCH_ENABLED
484
+ #ifdef RCT_NEW_ARCH_ENABLED
485
+ - (void)stopTask:(NSString *)identifier {
486
+ [self stopTaskInternal:identifier];
487
+ }
488
+ #else
469
489
  RCT_EXPORT_METHOD(stopTask: (NSString *)id) {
470
- [self stopTask:id];
490
+ [self stopTaskInternal:id];
471
491
  }
472
492
  #endif
473
493
 
@@ -818,6 +838,46 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
818
838
  return;
819
839
  }
820
840
 
841
+ // Fallback: certain servers return -1015 (NSURLErrorCannotDecodeRawData) on resumed tasks.
842
+ // Instead of failing permanently, attempt ONE fresh retry without resume data.
843
+ if (error.code == NSURLErrorCannotDecodeRawData && ![decodeErrorRetriedIds containsObject:taskConfig.id]) {
844
+ [decodeErrorRetriedIds addObject:taskConfig.id];
845
+ DLog(taskConfig.id, @"[RNBackgroundDownloader] - [didCompleteWithError] attempting fresh retry after decode error");
846
+
847
+ // Build a fresh request replicating original headers
848
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:taskConfig.url]];
849
+ // Reapply original request headers if available (including our internal identifier header)
850
+ NSDictionary *originalHeaders = task.originalRequest.allHTTPHeaderFields;
851
+ if (originalHeaders != nil) {
852
+ for (NSString *headerKey in originalHeaders) {
853
+ [request setValue:[originalHeaders valueForKey:headerKey] forHTTPHeaderField:headerKey];
854
+ }
855
+ }
856
+ [request setValue:taskConfig.id forHTTPHeaderField:@"configId"]; // ensure config id header
857
+
858
+ // Remove old mapping keyed by previous task identifier
859
+ [taskToConfigMap removeObjectForKey:@(task.taskIdentifier)];
860
+
861
+ NSURLSessionDownloadTask *newTask = [urlSession downloadTaskWithRequest:request];
862
+ if (newTask != nil) {
863
+ // Reset task state for fresh attempt
864
+ taskConfig.state = NSURLSessionTaskStateRunning;
865
+ taskConfig.errorCode = 0;
866
+ taskConfig.bytesDownloaded = 0;
867
+ taskConfig.bytesTotal = 0;
868
+ taskToConfigMap[@(newTask.taskIdentifier)] = taskConfig;
869
+ [mmkv setData:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY];
870
+ idToTaskMap[taskConfig.id] = newTask;
871
+ idToPercentMap[taskConfig.id] = @0.0;
872
+ idToLastBytesMap[taskConfig.id] = @0;
873
+ [newTask resume];
874
+ DLog(taskConfig.id, @"[RNBackgroundDownloader] - [didCompleteWithError] fresh retry started");
875
+ return; // Do not emit failure yet
876
+ } else {
877
+ DLog(taskConfig.id, @"[RNBackgroundDownloader] - [didCompleteWithError] fresh retry creation failed");
878
+ }
879
+ }
880
+
821
881
  // Handle failure
822
882
  if ([self canSendEvents]) {
823
883
  #ifdef RCT_NEW_ARCH_ENABLED
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kesha-antonov/react-native-background-downloader",
3
- "version": "4.0.0-alpha.0",
3
+ "version": "4.0.2",
4
4
  "description": "A library for React-Native to help you download large files on iOS and Android both in the foreground and most importantly in the background.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -83,9 +83,9 @@
83
83
  "@babel/preset-typescript": "^7.28.5",
84
84
  "@babel/runtime": "^7.28.4",
85
85
  "@expo/config-plugins": "^54.0.2",
86
- "@react-native/babel-preset": "^0.81.4",
87
- "@react-native/eslint-config": "^0.81.4",
88
- "@react-native/metro-config": "^0.81.4",
86
+ "@react-native/babel-preset": "^0.81.5",
87
+ "@react-native/eslint-config": "^0.81.5",
88
+ "@react-native/metro-config": "^0.81.5",
89
89
  "@typescript-eslint/eslint-plugin": "^8.46.1",
90
90
  "@typescript-eslint/parser": "^8.46.1",
91
91
  "babel-jest": "^29.7.0",
@@ -105,7 +105,7 @@
105
105
  "lint-staged": ">=16",
106
106
  "metro-react-native-babel-preset": "^0.77.0",
107
107
  "react": "19.1.0",
108
- "react-native": "0.81.4",
108
+ "react-native": "0.81.5",
109
109
  "react-native-fs": "^2.20.0",
110
110
  "react-native-vector-icons": "^10.3.0",
111
111
  "react-test-renderer": "19.2.0",
@@ -15,7 +15,7 @@ import {
15
15
  DownloadTaskState,
16
16
  Metadata,
17
17
  } from './types'
18
- import { config, log } from '.'
18
+ import { config, log } from './config'
19
19
  import type { Spec } from './NativeRNBackgroundDownloader'
20
20
 
21
21
  // Try to get the native module using TurboModuleRegistry first (new architecture),
package/src/config.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Headers } from './types'
2
+
3
+ export const DEFAULT_PROGRESS_INTERVAL = 1000
4
+ export const DEFAULT_PROGRESS_MIN_BYTES = 1024 * 1024 // 1MB
5
+
6
+ interface ConfigState {
7
+ headers: Headers
8
+ progressInterval: number
9
+ progressMinBytes: number
10
+ isLogsEnabled: boolean
11
+ }
12
+
13
+ export const config: ConfigState = {
14
+ headers: {},
15
+ progressInterval: DEFAULT_PROGRESS_INTERVAL,
16
+ progressMinBytes: DEFAULT_PROGRESS_MIN_BYTES,
17
+ isLogsEnabled: false,
18
+ }
19
+
20
+ export const log = (...args: unknown[]): void => {
21
+ if (config.isLogsEnabled)
22
+ console.log('[RNBackgroundDownloader]', ...args)
23
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { NativeModules, Platform, TurboModuleRegistry, DeviceEventEmitter } from 'react-native'
1
+ import { NativeModules, Platform, TurboModuleRegistry, NativeEventEmitter, NativeModule } from 'react-native'
2
2
  import DownloadTask from './DownloadTask'
3
3
  import { Config, DownloadParams, Headers, TaskInfo, TaskInfoNative } from './types'
4
+ import { config, log, DEFAULT_PROGRESS_INTERVAL, DEFAULT_PROGRESS_MIN_BYTES } from './config'
4
5
  import type { Spec } from './NativeRNBackgroundDownloader'
5
6
 
6
- type NativeModule = Spec & {
7
+ type RNBackgroundDownloaderModule = Spec & {
7
8
  TaskRunning: number
8
9
  TaskSuspended: number
9
10
  TaskCanceling: number
@@ -11,61 +12,61 @@ type NativeModule = Spec & {
11
12
  documents: string
12
13
  }
13
14
 
14
- // Try to get the native module using TurboModuleRegistry first (new architecture),
15
- // then fall back to NativeModules (old architecture)
16
- let RNBackgroundDownloader: NativeModule
17
-
18
- // Try TurboModules first
19
- const turboModule = TurboModuleRegistry.get<Spec>('RNBackgroundDownloader')
20
- const isNewArchitecture = turboModule != null
21
-
22
- if (turboModule) {
23
- // TurboModules use getConstants() method
24
- const constants = turboModule.getConstants()
25
- RNBackgroundDownloader = Object.assign(turboModule, constants) as NativeModule
26
- } else {
27
- // Fall back to old architecture
28
- RNBackgroundDownloader = NativeModules.RNBackgroundDownloader
29
-
30
- // For old architecture, constants may need to be fetched via getConstants() as well
31
- if (RNBackgroundDownloader && !RNBackgroundDownloader.documents && typeof RNBackgroundDownloader.getConstants === 'function') {
32
- const constants = RNBackgroundDownloader.getConstants()
33
- if (constants)
34
- Object.assign(RNBackgroundDownloader, constants)
15
+ // Lazy initialization state
16
+ let RNBackgroundDownloader: RNBackgroundDownloaderModule & NativeModule
17
+ let turboModule: Spec | null = null
18
+ let isNewArchitecture = false
19
+ let isInitialized = false
20
+
21
+ /**
22
+ * Lazily initialize the native module.
23
+ * This is called on first actual use of the module, not at import time.
24
+ * This prevents issues with module loading before React Native's bridge is ready.
25
+ */
26
+ function ensureNativeModuleInitialized (): RNBackgroundDownloaderModule & NativeModule {
27
+ if (isInitialized && RNBackgroundDownloader)
28
+ return RNBackgroundDownloader
29
+
30
+ // Try TurboModules first
31
+ turboModule = TurboModuleRegistry.get<Spec>('RNBackgroundDownloader')
32
+ // Check if the new architecture event emitters are available
33
+ // TurboModuleRegistry.get() can return a module even with old arch, but event emitters won't exist
34
+ isNewArchitecture = turboModule != null && typeof turboModule.onDownloadBegin === 'function'
35
+
36
+ if (isNewArchitecture && turboModule) {
37
+ // New architecture: TurboModules use getConstants() method
38
+ const constants = turboModule.getConstants()
39
+ RNBackgroundDownloader = Object.assign(turboModule, constants) as RNBackgroundDownloaderModule & NativeModule
40
+ } else {
41
+ // Fall back to old architecture - must use NativeModules for proper event emission
42
+ RNBackgroundDownloader = NativeModules.RNBackgroundDownloader
43
+
44
+ // For old architecture, constants may need to be fetched via getConstants() as well
45
+ if (RNBackgroundDownloader && !RNBackgroundDownloader.documents && typeof RNBackgroundDownloader.getConstants === 'function') {
46
+ const constants = RNBackgroundDownloader.getConstants()
47
+ if (constants)
48
+ Object.assign(RNBackgroundDownloader, constants)
49
+ }
35
50
  }
36
- }
37
51
 
38
- if (!RNBackgroundDownloader)
39
- throw new Error(
40
- 'The package \'@kesha-antonov/react-native-background-downloader\' doesn\'t seem to be linked. Make sure: \n\n' +
41
- Platform.select({ ios: '- You have run \'pod install\'\n', default: '' }) +
42
- '- You rebuilt the app after installing the package\n' +
43
- '- You are not using Expo Go\n'
44
- )
52
+ if (!RNBackgroundDownloader)
53
+ throw new Error(
54
+ 'The package \'@kesha-antonov/react-native-background-downloader\' doesn\'t seem to be linked. Make sure: \n\n' +
55
+ Platform.select({ ios: '- You have run \'pod install\'\n', default: '' }) +
56
+ '- You rebuilt the app after installing the package\n' +
57
+ '- You are not using Expo Go\n'
58
+ )
45
59
 
46
- const MIN_PROGRESS_INTERVAL = 250
47
- const DEFAULT_PROGRESS_INTERVAL = 1000
48
- const DEFAULT_PROGRESS_MIN_BYTES = 1024 * 1024 // 1MB
49
- const tasksMap = new Map<string, DownloadTask>()
60
+ isInitialized = true
50
61
 
51
- interface ConfigState {
52
- headers: Headers
53
- progressInterval: number
54
- progressMinBytes: number
55
- isLogsEnabled: boolean
56
- }
62
+ // Initialize event listeners after native module is ready
63
+ initializeEventListeners()
57
64
 
58
- export const config: ConfigState = {
59
- headers: {},
60
- progressInterval: DEFAULT_PROGRESS_INTERVAL,
61
- progressMinBytes: DEFAULT_PROGRESS_MIN_BYTES,
62
- isLogsEnabled: false,
65
+ return RNBackgroundDownloader
63
66
  }
64
67
 
65
- export const log = (...args: unknown[]): void => {
66
- if (config.isLogsEnabled)
67
- console.log('[RNBackgroundDownloader]', ...args)
68
- }
68
+ const MIN_PROGRESS_INTERVAL = 250
69
+ const tasksMap = new Map<string, DownloadTask>()
69
70
 
70
71
  interface DownloadBeginEvent {
71
72
  id: string
@@ -92,74 +93,89 @@ interface DownloadFailedEvent {
92
93
  }
93
94
 
94
95
  // Set up event listeners based on architecture
95
- if (isNewArchitecture && turboModule) {
96
- // New architecture: use EventEmitter from TurboModule spec
97
- turboModule.onDownloadBegin((data: DownloadBeginEvent) => {
98
- const { id, ...rest } = data
99
- log('downloadBegin', id, rest)
100
- const task = tasksMap.get(id)
101
- task?.onBegin(rest)
102
- })
103
-
104
- turboModule.onDownloadProgress((events: DownloadProgressEvent[]) => {
105
- log('downloadProgress', events)
106
- for (const event of events) {
107
- const { id, ...rest } = event
96
+ // For old architecture, we need to defer NativeEventEmitter creation
97
+ // to avoid issues during module initialization
98
+ let eventListenersInitialized = false
99
+
100
+ function initializeEventListeners () {
101
+ if (eventListenersInitialized) return
102
+ eventListenersInitialized = true
103
+
104
+ if (isNewArchitecture && turboModule) {
105
+ // New architecture: use EventEmitter from TurboModule spec
106
+ turboModule.onDownloadBegin((data: DownloadBeginEvent) => {
107
+ const { id, ...rest } = data
108
+ log('downloadBegin', id, rest)
108
109
  const task = tasksMap.get(id)
109
- task?.onProgress(rest)
110
- }
111
- })
112
-
113
- turboModule.onDownloadComplete((data: DownloadCompleteEvent) => {
114
- const { id, ...rest } = data
115
- log('downloadComplete', id, rest)
116
- const task = tasksMap.get(id)
117
- task?.onDone(rest)
118
- tasksMap.delete(id)
119
- })
110
+ task?.onBegin(rest)
111
+ })
112
+
113
+ turboModule.onDownloadProgress((events: DownloadProgressEvent[]) => {
114
+ log('downloadProgress', events)
115
+ for (const event of events) {
116
+ const { id, ...rest } = event
117
+ const task = tasksMap.get(id)
118
+ task?.onProgress(rest)
119
+ }
120
+ })
120
121
 
121
- turboModule.onDownloadFailed((data: DownloadFailedEvent) => {
122
- const { id, ...rest } = data
123
- log('downloadFailed', id, rest)
124
- const task = tasksMap.get(id)
125
- task?.onError(rest)
126
- tasksMap.delete(id)
127
- })
128
- } else {
129
- // Old architecture: use DeviceEventEmitter
130
- DeviceEventEmitter.addListener('downloadBegin', (data: DownloadBeginEvent) => {
131
- const { id, ...rest } = data
132
- log('downloadBegin', id, rest)
133
- const task = tasksMap.get(id)
134
- task?.onBegin(rest)
135
- })
122
+ turboModule.onDownloadComplete((data: DownloadCompleteEvent) => {
123
+ const { id, ...rest } = data
124
+ log('downloadComplete', id, rest)
125
+ const task = tasksMap.get(id)
126
+ task?.onDone(rest)
127
+ tasksMap.delete(id)
128
+ })
136
129
 
137
- DeviceEventEmitter.addListener('downloadProgress', (events: DownloadProgressEvent[]) => {
138
- log('downloadProgress', events)
139
- for (const event of events) {
140
- const { id, ...rest } = event
130
+ turboModule.onDownloadFailed((data: DownloadFailedEvent) => {
131
+ const { id, ...rest } = data
132
+ log('downloadFailed', id, rest)
141
133
  const task = tasksMap.get(id)
142
- task?.onProgress(rest)
143
- }
144
- })
134
+ task?.onError(rest)
135
+ tasksMap.delete(id)
136
+ })
137
+ } else {
138
+ // Old architecture: use NativeEventEmitter with the native module
139
+ // RCTEventEmitter on native side requires NativeEventEmitter on JS side
140
+ const eventEmitter = new NativeEventEmitter(RNBackgroundDownloader)
141
+
142
+ eventEmitter.addListener('downloadBegin', (data: DownloadBeginEvent) => {
143
+ const { id, ...rest } = data
144
+ log('downloadBegin', id, rest)
145
+ const task = tasksMap.get(id)
146
+ task?.onBegin(rest)
147
+ })
148
+
149
+ eventEmitter.addListener('downloadProgress', (events: DownloadProgressEvent[]) => {
150
+ log('downloadProgress', events)
151
+ for (const event of events) {
152
+ const { id, ...rest } = event
153
+ const task = tasksMap.get(id)
154
+ task?.onProgress(rest)
155
+ }
156
+ })
145
157
 
146
- DeviceEventEmitter.addListener('downloadComplete', (data: DownloadCompleteEvent) => {
147
- const { id, ...rest } = data
148
- log('downloadComplete', id, rest)
149
- const task = tasksMap.get(id)
150
- task?.onDone(rest)
151
- tasksMap.delete(id)
152
- })
158
+ eventEmitter.addListener('downloadComplete', (data: DownloadCompleteEvent) => {
159
+ const { id, ...rest } = data
160
+ log('downloadComplete', id, rest)
161
+ const task = tasksMap.get(id)
162
+ task?.onDone(rest)
163
+ tasksMap.delete(id)
164
+ })
153
165
 
154
- DeviceEventEmitter.addListener('downloadFailed', (data: DownloadFailedEvent) => {
155
- const { id, ...rest } = data
156
- log('downloadFailed', id, rest)
157
- const task = tasksMap.get(id)
158
- task?.onError(rest)
159
- tasksMap.delete(id)
160
- })
166
+ eventEmitter.addListener('downloadFailed', (data: DownloadFailedEvent) => {
167
+ const { id, ...rest } = data
168
+ log('downloadFailed', id, rest)
169
+ const task = tasksMap.get(id)
170
+ task?.onError(rest)
171
+ tasksMap.delete(id)
172
+ })
173
+ }
161
174
  }
162
175
 
176
+ // Event listeners are now initialized lazily when ensureNativeModuleInitialized() is called
177
+ // This ensures the bridge is ready before any native module access
178
+
163
179
  export function setConfig ({
164
180
  headers = {},
165
181
  progressInterval = DEFAULT_PROGRESS_INTERVAL,
@@ -182,7 +198,8 @@ export function setConfig ({
182
198
  }
183
199
 
184
200
  export const getExistingDownloadTasks = async (): Promise<DownloadTask[]> => {
185
- const downloads = await RNBackgroundDownloader.getExistingDownloadTasks()
201
+ const nativeModule = ensureNativeModuleInitialized()
202
+ const downloads = await nativeModule.getExistingDownloadTasks()
186
203
  const downloadTasks: DownloadTask[] = downloads.map(downloadInfo => {
187
204
  // Parse metadata from JSON string to object
188
205
  let metadata = {}
@@ -202,15 +219,15 @@ export const getExistingDownloadTasks = async (): Promise<DownloadTask[]> => {
202
219
  const task = new DownloadTask(taskInfo, tasksMap.get(taskInfo.id))
203
220
 
204
221
  switch (taskInfo.state) {
205
- case RNBackgroundDownloader.TaskRunning: {
222
+ case nativeModule.TaskRunning: {
206
223
  task.state = 'DOWNLOADING'
207
224
  break
208
225
  }
209
- case RNBackgroundDownloader.TaskSuspended: {
226
+ case nativeModule.TaskSuspended: {
210
227
  task.state = 'PAUSED'
211
228
  break
212
229
  }
213
- case RNBackgroundDownloader.TaskCanceling: {
230
+ case nativeModule.TaskCanceling: {
214
231
  // On iOS, paused tasks (via cancelByProducingResumeData) are in Canceling state with errorCode -999
215
232
  if (taskInfo.errorCode === -999) {
216
233
  task.state = 'PAUSED'
@@ -220,7 +237,7 @@ export const getExistingDownloadTasks = async (): Promise<DownloadTask[]> => {
220
237
  }
221
238
  break
222
239
  }
223
- case RNBackgroundDownloader.TaskCompleted: {
240
+ case nativeModule.TaskCompleted: {
224
241
  if (taskInfo.bytesDownloaded === taskInfo.bytesTotal)
225
242
  task.state = 'DONE'
226
243
  else
@@ -254,7 +271,8 @@ export const completeHandler = (jobId: string) => {
254
271
  return
255
272
  }
256
273
 
257
- return RNBackgroundDownloader.completeHandler(jobId)
274
+ const nativeModule = ensureNativeModuleInitialized()
275
+ return nativeModule.completeHandler(jobId)
258
276
  }
259
277
 
260
278
  export function createDownloadTask ({
@@ -263,6 +281,9 @@ export function createDownloadTask ({
263
281
  isNotificationVisible = false,
264
282
  ...rest
265
283
  }: TaskInfo & DownloadParams) {
284
+ // Ensure native module and event listeners are initialized before creating tasks
285
+ ensureNativeModuleInitialized()
286
+
266
287
  if (!rest.id || !rest.url || !rest.destination)
267
288
  throw new Error('[RNBackgroundDownloader] id, url and destination are required')
268
289
 
@@ -287,8 +308,11 @@ export function createDownloadTask ({
287
308
  return task
288
309
  }
289
310
 
311
+ // Use getter to lazily initialize native module when directories are accessed
290
312
  export const directories = {
291
- documents: RNBackgroundDownloader.documents,
313
+ get documents () {
314
+ return ensureNativeModuleInitialized().documents
315
+ },
292
316
  }
293
317
 
294
318
  export default {