@kesha-antonov/react-native-background-downloader 4.0.0-alpha.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## v4.0.0-alpha.0
3
+ ## v4.0.0
4
4
 
5
5
  > 📖 **Upgrading from v3.x?** See the [Migration Guide](./MIGRATION.md) for detailed instructions.
6
6
 
package/README.md CHANGED
@@ -5,11 +5,9 @@
5
5
 
6
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)
7
7
 
8
- ## 🎉 Version 4.0.0-alpha.0 Released!
8
+ ## 🎉 Version 4.0.0 Released!
9
9
 
10
- **v4.0.0-alpha.0** is now available with full **React Native New Architecture (TurboModules)** support!
11
-
12
- > ⚠️ **Alpha Release:** This version is in alpha state. Please report any issues you encounter.
10
+ **v4.0.0** is now available with full **React Native New Architecture (TurboModules)** support!
13
11
 
14
12
  ### What's New
15
13
  - ✅ Full TurboModules support for iOS and Android
@@ -58,16 +58,16 @@ RCT_EXPORT_MODULE();
58
58
  // Enable interop layer so NativeModules.RNBackgroundDownloader is available
59
59
  // This is required for NativeEventEmitter to work with TurboModules
60
60
  + (BOOL)requiresMainQueueSetup {
61
- return YES;
61
+ return NO;
62
62
  }
63
63
 
64
64
  #pragma mark - Helper methods
65
65
 
66
66
  - (BOOL)canSendEvents {
67
- // Always return YES - let RCTEventEmitter handle listener management
68
- // The warning "Sending X with no listeners registered" is harmless
69
- // and events will be properly received once JS listeners are set up
70
- 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;
71
71
  }
72
72
 
73
73
  - (RNBGDTaskConfig *)configForTask:(NSURLSessionTask *)task {
@@ -126,6 +126,11 @@ RCT_EXPORT_MODULE();
126
126
  self = [super initWithDisabledObservation];
127
127
  #endif
128
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
129
134
  [MMKV initializeMMKV:nil];
130
135
  mmkv = [MMKV mmkvWithID:@"RNBackgroundDownloader"];
131
136
 
@@ -368,7 +373,7 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
368
373
  }
369
374
  }
370
375
 
371
- - (void)pauseTask:(NSString *)identifier {
376
+ - (void)pauseTaskInternal:(NSString *)identifier {
372
377
  DLog(identifier, @"[RNBackgroundDownloader] - [pauseTask]");
373
378
  @synchronized (sharedLock) {
374
379
  NSURLSessionDownloadTask *task = self->idToTaskMap[identifier];
@@ -392,13 +397,17 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
392
397
  }
393
398
  }
394
399
 
395
- #ifndef RCT_NEW_ARCH_ENABLED
400
+ #ifdef RCT_NEW_ARCH_ENABLED
401
+ - (void)pauseTask:(NSString *)identifier {
402
+ [self pauseTaskInternal:identifier];
403
+ }
404
+ #else
396
405
  RCT_EXPORT_METHOD(pauseTask: (NSString *)id) {
397
- [self pauseTask:id];
406
+ [self pauseTaskInternal:id];
398
407
  }
399
408
  #endif
400
409
 
401
- - (void)resumeTask:(NSString *)identifier {
410
+ - (void)resumeTaskInternal:(NSString *)identifier {
402
411
  DLog(identifier, @"[RNBackgroundDownloader] - [resumeTask]");
403
412
  @synchronized (sharedLock) {
404
413
  [self lazyRegisterSession];
@@ -449,13 +458,17 @@ RCT_EXPORT_METHOD(pauseTask: (NSString *)id) {
449
458
  }
450
459
  }
451
460
 
452
- #ifndef RCT_NEW_ARCH_ENABLED
461
+ #ifdef RCT_NEW_ARCH_ENABLED
462
+ - (void)resumeTask:(NSString *)identifier {
463
+ [self resumeTaskInternal:identifier];
464
+ }
465
+ #else
453
466
  RCT_EXPORT_METHOD(resumeTask: (NSString *)id) {
454
- [self resumeTask:id];
467
+ [self resumeTaskInternal:id];
455
468
  }
456
469
  #endif
457
470
 
458
- - (void)stopTask:(NSString *)identifier {
471
+ - (void)stopTaskInternal:(NSString *)identifier {
459
472
  DLog(identifier, @"[RNBackgroundDownloader] - [stopTask]");
460
473
  @synchronized (sharedLock) {
461
474
  NSURLSessionDownloadTask *task = self->idToTaskMap[identifier];
@@ -468,9 +481,13 @@ RCT_EXPORT_METHOD(resumeTask: (NSString *)id) {
468
481
  }
469
482
  }
470
483
 
471
- #ifndef RCT_NEW_ARCH_ENABLED
484
+ #ifdef RCT_NEW_ARCH_ENABLED
485
+ - (void)stopTask:(NSString *)identifier {
486
+ [self stopTaskInternal:identifier];
487
+ }
488
+ #else
472
489
  RCT_EXPORT_METHOD(stopTask: (NSString *)id) {
473
- [self stopTask:id];
490
+ [self stopTaskInternal:id];
474
491
  }
475
492
  #endif
476
493
 
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.1",
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 {