@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 +50 -0
- package/README.md +19 -15
- package/ios/RNBackgroundDownloader.mm +74 -14
- package/package.json +5 -5
- package/src/DownloadTask.ts +1 -1
- package/src/config.ts +23 -0
- package/src/index.ts +141 -117
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
|
-
|
|
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
|
[](https://badge.fury.io/js/@kesha-antonov%2Freact-native-background-downloader)
|
|
4
7
|
|
|
5
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
20
|
+
### Upgrading from v3.x?
|
|
21
|
+
📖 See the [Migration Guide](./MIGRATION.md) for detailed upgrade instructions and breaking changes.
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
📋 See the [Changelog](./CHANGELOG.md) for the full list of changes.
|
|
12
24
|
|
|
13
|
-
|
|
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
|
|
61
|
+
return NO;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
#pragma mark - Helper methods
|
|
63
65
|
|
|
64
66
|
- (BOOL)canSendEvents {
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
return
|
|
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)
|
|
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
|
-
#
|
|
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
|
|
406
|
+
[self pauseTaskInternal:id];
|
|
395
407
|
}
|
|
396
408
|
#endif
|
|
397
409
|
|
|
398
|
-
- (void)
|
|
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
|
-
#
|
|
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
|
|
467
|
+
[self resumeTaskInternal:id];
|
|
452
468
|
}
|
|
453
469
|
#endif
|
|
454
470
|
|
|
455
|
-
- (void)
|
|
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
|
-
#
|
|
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
|
|
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.
|
|
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.
|
|
87
|
-
"@react-native/eslint-config": "^0.81.
|
|
88
|
-
"@react-native/metro-config": "^0.81.
|
|
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.
|
|
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",
|
package/src/DownloadTask.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
//
|
|
15
|
-
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
progressInterval: number
|
|
54
|
-
progressMinBytes: number
|
|
55
|
-
isLogsEnabled: boolean
|
|
56
|
-
}
|
|
62
|
+
// Initialize event listeners after native module is ready
|
|
63
|
+
initializeEventListeners()
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
headers: {},
|
|
60
|
-
progressInterval: DEFAULT_PROGRESS_INTERVAL,
|
|
61
|
-
progressMinBytes: DEFAULT_PROGRESS_MIN_BYTES,
|
|
62
|
-
isLogsEnabled: false,
|
|
65
|
+
return RNBackgroundDownloader
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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?.
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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?.
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
222
|
+
case nativeModule.TaskRunning: {
|
|
206
223
|
task.state = 'DOWNLOADING'
|
|
207
224
|
break
|
|
208
225
|
}
|
|
209
|
-
case
|
|
226
|
+
case nativeModule.TaskSuspended: {
|
|
210
227
|
task.state = 'PAUSED'
|
|
211
228
|
break
|
|
212
229
|
}
|
|
213
|
-
case
|
|
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
|
|
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
|
-
|
|
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
|
|
313
|
+
get documents () {
|
|
314
|
+
return ensureNativeModuleInitialized().documents
|
|
315
|
+
},
|
|
292
316
|
}
|
|
293
317
|
|
|
294
318
|
export default {
|