react-native-update 10.43.3 → 10.45.0

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.
Files changed (41) hide show
  1. package/android/jni/Android.mk +0 -1
  2. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  3. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  4. package/android/lib/x86/librnupdate.so +0 -0
  5. package/android/lib/x86_64/librnupdate.so +0 -0
  6. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +24 -12
  7. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +33 -9
  8. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
  9. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
  10. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +8 -4
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +8 -8
  13. package/cpp/patch_core/archive_patch_core.cpp +16 -0
  14. package/cpp/patch_core/archive_patch_core.h +5 -0
  15. package/cpp/patch_core/jni_util.h +56 -0
  16. package/cpp/patch_core/patch_core.cpp +16 -1
  17. package/cpp/patch_core/patch_core.h +6 -0
  18. package/cpp/patch_core/patch_core_android.cpp +4 -37
  19. package/cpp/patch_core/state_ops.h +24 -0
  20. package/cpp/patch_core/tests/patch_core_test.cpp +109 -2
  21. package/cpp/patch_core/update_core_android.cpp +43 -69
  22. package/harmony/pushy/src/main/cpp/pushy.cpp +163 -128
  23. package/harmony/pushy/src/main/ets/DownloadTask.ts +72 -19
  24. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -6
  25. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +10 -10
  26. package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
  27. package/harmony/pushy.har +0 -0
  28. package/ios/RCTPushy/RCTPushy.mm +142 -111
  29. package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
  30. package/package.json +7 -3
  31. package/src/client.ts +161 -101
  32. package/src/context.ts +21 -7
  33. package/src/core.ts +5 -1
  34. package/src/index.ts +7 -1
  35. package/src/provider.tsx +91 -56
  36. package/src/utils.ts +58 -18
  37. package/android/jni/DownloadTask.c +0 -56
  38. package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
  39. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  40. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  41. package/src/__tests__/setup.ts +0 -44
@@ -68,7 +68,7 @@ export class UpdateContext {
68
68
  */
69
69
  private trace(point: string): void {
70
70
  const snap = this.getStateSnapshot();
71
- logger.info(
71
+ logger.debug(
72
72
  'UpdateContext',
73
73
  `trace id=${this.instanceId} ${point}` +
74
74
  ` pkg=${snap.packageVersion} bt=${snap.buildTime}` +
@@ -91,7 +91,12 @@ export class UpdateContext {
91
91
  name: 'update',
92
92
  });
93
93
  } catch (e) {
94
+ // Fail fast: a missing preferences store means no state can be persisted,
95
+ // which disables rollback protection. Rethrow so the failure surfaces at
96
+ // construction time instead of later as an unrelated TypeError on the
97
+ // undefined `preferences` handle.
94
98
  console.error('Failed to init preferences:', e);
99
+ throw e;
95
100
  }
96
101
  }
97
102
 
@@ -179,17 +184,23 @@ export class UpdateContext {
179
184
  }
180
185
 
181
186
  private flushPreferences(reason: string): void {
182
- try {
183
- const flushablePreferences = this.preferences as FlushablePreferences;
184
- if (typeof flushablePreferences.flushSync === 'function') {
187
+ const flushablePreferences = this.preferences as FlushablePreferences;
188
+ if (typeof flushablePreferences.flushSync === 'function') {
189
+ try {
185
190
  flushablePreferences.flushSync();
186
191
  return;
192
+ } catch (error) {
193
+ console.error(`Failed to flushSync preferences for ${reason}:`, error);
194
+ // fall through to async flush rather than failing the whole operation
187
195
  }
188
- throw Error('preferences.flushSync is not available');
189
- } catch (error) {
190
- console.error(`Failed to flush preferences for ${reason}:`, error);
191
- throw error;
192
196
  }
197
+ // flushSync unavailable or failed: writes are already applied in memory via
198
+ // putSync/deleteSync; persist asynchronously as a best-effort so the state
199
+ // operation still succeeds instead of throwing (which is worse than a
200
+ // slightly delayed persist).
201
+ this.preferences.flush().catch((error: Object) => {
202
+ console.error(`Failed to flush preferences for ${reason}:`, error);
203
+ });
193
204
  }
194
205
 
195
206
  private putNullableString(key: string, value?: string): void {
@@ -509,12 +520,18 @@ export class UpdateContext {
509
520
 
510
521
  public cleanUp(): void {
511
522
  const state = this.getStateSnapshot();
523
+ // cleanupOldEntries now runs on a native worker thread (returns a Promise).
524
+ // Cleanup is best-effort background maintenance and no caller depends on its
525
+ // completion, so fire-and-forget it off the UI thread and just log failures
526
+ // instead of blocking the state operation (or cold start) on disk I/O.
512
527
  NativePatchCore.cleanupOldEntries(
513
528
  this.rootDir,
514
529
  state.currentVersion || '',
515
530
  state.lastVersion || '',
516
531
  3,
517
- );
532
+ ).catch((error: Object) => {
533
+ console.error('cleanupOldEntries failed:', error);
534
+ });
518
535
  }
519
536
 
520
537
  public getIsUsingBundleUrl(): boolean {
package/harmony/pushy.har CHANGED
Binary file
@@ -14,6 +14,9 @@
14
14
 
15
15
  #import <React/RCTConvert.h>
16
16
  #import <React/RCTLog.h>
17
+ #import <os/lock.h>
18
+
19
+ #include <atomic>
17
20
 
18
21
  static NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
19
22
  static NSString *const paramPackageVersion = @"packageVersion";
@@ -52,7 +55,20 @@ typedef NS_ENUM(NSInteger, PushyType) {
52
55
  //TASK_TYPE_PLAIN_DOWNLOAD=4?
53
56
  };
54
57
 
55
- static BOOL ignoreRollback = false;
58
+ static std::atomic<bool> ignoreRollback{false};
59
+
60
+ // Serializes every read-modify-write of the persisted update state. The state
61
+ // machine itself is a pure function (state_core), but callers run on different
62
+ // threads (main thread bundleURL, module method queue, _fileQueue), so the
63
+ // read→transform→write sequence must be atomic to avoid e.g. markSuccess being
64
+ // overwritten by a concurrent bundleURL and the version being rolled back.
65
+ static os_unfair_lock pushyStateLock = OS_UNFAIR_LOCK_INIT;
66
+
67
+ static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) {
68
+ os_unfair_lock_lock(&pushyStateLock);
69
+ block();
70
+ os_unfair_lock_unlock(&pushyStateLock);
71
+ }
56
72
 
57
73
  static std::string PushyToStdString(NSString *value) {
58
74
  if (value == nil) {
@@ -221,64 +237,71 @@ RCT_EXPORT_MODULE(RCTPushy);
221
237
 
222
238
  + (NSURL *)bundleURL
223
239
  {
224
- NSUserDefaults *defaults = PushyDefaults();
225
-
226
- NSString *curPackageVersion = [RCTPushy packageVersion];
227
- NSString *curBuildTime = [RCTPushy buildTime];
228
-
229
- pushy::state::State state = PushyStateFromDefaults(defaults);
230
- pushy::state::BinaryVersionSyncResult sync = pushy::state::SyncBinaryVersion(
231
- state,
232
- PushyToStdString(curPackageVersion),
233
- PushyToStdString(curBuildTime)
234
- );
235
- if (sync.changed) {
236
- [defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked];
237
- state = sync.state;
238
- PushyApplyStateToDefaults(defaults, state);
239
- }
240
+ __block NSURL *resolvedURL = nil;
241
+ PushyWithStateLock(^{
242
+ NSUserDefaults *defaults = PushyDefaults();
243
+
244
+ NSString *curPackageVersion = [RCTPushy packageVersion];
245
+ NSString *curBuildTime = [RCTPushy buildTime];
240
246
 
241
- if (!state.current_version.empty()) {
242
- pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
247
+ pushy::state::State state = PushyStateFromDefaults(defaults);
248
+ pushy::state::BinaryVersionSyncResult sync = pushy::state::SyncBinaryVersion(
243
249
  state,
244
- ignoreRollback,
245
- true
250
+ PushyToStdString(curPackageVersion),
251
+ PushyToStdString(curBuildTime)
246
252
  );
247
- state = decision.state;
248
-
249
- if (decision.did_rollback || decision.consumed_first_time) {
253
+ if (sync.changed) {
254
+ [defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked];
255
+ state = sync.state;
250
256
  PushyApplyStateToDefaults(defaults, state);
251
257
  }
252
- if (decision.consumed_first_time) {
253
- // bundleURL may be called many times, ignore rollbacks before process restarted again.
254
- ignoreRollback = true;
255
- [defaults setObject:@(YES) forKey:keyFirstLoadMarked];
256
- }
257
258
 
258
- NSString *loadVersion = PushyFromStdString(decision.load_version);
259
- NSString *downloadDir = [RCTPushy downloadDir];
260
- while (loadVersion.length) {
261
- NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
262
- if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
263
- NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath];
264
- return bundleURL;
265
- } else {
266
- RCTLogError(@"RCTPushy -- bundle version %@ not found", loadVersion);
267
- state = pushy::state::Rollback(state);
259
+ if (!state.current_version.empty()) {
260
+ pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
261
+ state,
262
+ ignoreRollback.load(),
263
+ true
264
+ );
265
+ state = decision.state;
266
+
267
+ if (decision.did_rollback || decision.consumed_first_time) {
268
268
  PushyApplyStateToDefaults(defaults, state);
269
- loadVersion = PushyFromStdString(state.current_version);
269
+ }
270
+ if (decision.consumed_first_time) {
271
+ // bundleURL may be called many times, ignore rollbacks before process restarted again.
272
+ ignoreRollback = true;
273
+ [defaults setObject:@(YES) forKey:keyFirstLoadMarked];
274
+ }
275
+
276
+ NSString *loadVersion = PushyFromStdString(decision.load_version);
277
+ NSString *downloadDir = [RCTPushy downloadDir];
278
+ while (loadVersion.length) {
279
+ NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
280
+ if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
281
+ resolvedURL = [NSURL fileURLWithPath:bundlePath];
282
+ return;
283
+ } else {
284
+ RCTLogError(@"RCTPushy -- bundle version %@ not found", loadVersion);
285
+ state = pushy::state::Rollback(state);
286
+ PushyApplyStateToDefaults(defaults, state);
287
+ loadVersion = PushyFromStdString(state.current_version);
288
+ }
270
289
  }
271
290
  }
272
- }
273
-
274
- return [RCTPushy binaryBundleURL];
291
+ });
292
+
293
+ return resolvedURL ?: [RCTPushy binaryBundleURL];
275
294
  }
276
295
 
277
296
  + (NSString *) rollback {
278
- NSUserDefaults *defaults = PushyDefaults();
279
- pushy::state::State state = pushy::state::Rollback(PushyStateFromDefaults(defaults));
280
- PushyApplyStateToDefaults(defaults, state);
281
- return PushyFromStdString(state.current_version);
297
+ __block NSString *currentVersion = nil;
298
+ PushyWithStateLock(^{
299
+ NSUserDefaults *defaults = PushyDefaults();
300
+ pushy::state::State state = pushy::state::Rollback(PushyStateFromDefaults(defaults));
301
+ PushyApplyStateToDefaults(defaults, state);
302
+ currentVersion = PushyFromStdString(state.current_version);
303
+ });
304
+ return currentVersion;
282
305
  }
283
306
 
284
307
  + (BOOL)requiresMainQueueSetup
@@ -288,36 +311,37 @@ RCT_EXPORT_MODULE(RCTPushy);
288
311
 
289
312
  - (NSDictionary *)constantsToExport
290
313
  {
291
- NSUserDefaults *defaults = PushyDefaults();
292
-
293
314
  NSMutableDictionary *ret = [NSMutableDictionary new];
294
- ret[@"downloadRootDir"] = [RCTPushy downloadDir];
295
- ret[@"packageVersion"] = [RCTPushy packageVersion];
296
- ret[@"buildTime"] = [RCTPushy buildTime];
297
- ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
298
- ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
299
- ret[@"uuid"] = [defaults objectForKey:keyUuid];
300
- NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
301
- NSString *currentVersion = [pushyInfo objectForKey:paramCurrentVersion];
302
- ret[@"currentVersion"] = currentVersion;
303
- if (currentVersion != nil) {
304
- ret[@"currentVersionInfo"] = [defaults objectForKey:PushyHashInfoKey(currentVersion)];
305
- }
306
-
307
- if (ret[@"isFirstTime"]) {
308
- [defaults removeObjectForKey:keyFirstLoadMarked];
309
- }
310
-
311
- if (ret[@"rolledBackVersion"] != nil) {
312
- [defaults removeObjectForKey:keyRolledBackMarked];
313
- [self clearInvalidFiles];
314
- }
315
-
316
- if ([[defaults objectForKey:KeyPackageUpdatedMarked] boolValue]) {
317
- [defaults removeObjectForKey:KeyPackageUpdatedMarked];
318
- [self clearInvalidFiles];
319
- }
320
-
315
+ PushyWithStateLock(^{
316
+ NSUserDefaults *defaults = PushyDefaults();
317
+
318
+ ret[@"downloadRootDir"] = [RCTPushy downloadDir];
319
+ ret[@"packageVersion"] = [RCTPushy packageVersion];
320
+ ret[@"buildTime"] = [RCTPushy buildTime];
321
+ ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
322
+ ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
323
+ ret[@"uuid"] = [defaults objectForKey:keyUuid];
324
+ NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
325
+ NSString *currentVersion = [pushyInfo objectForKey:paramCurrentVersion];
326
+ ret[@"currentVersion"] = currentVersion;
327
+ if (currentVersion != nil) {
328
+ ret[@"currentVersionInfo"] = [defaults objectForKey:PushyHashInfoKey(currentVersion)];
329
+ }
330
+
331
+ if (ret[@"isFirstTime"]) {
332
+ [defaults removeObjectForKey:keyFirstLoadMarked];
333
+ }
334
+
335
+ if (ret[@"rolledBackVersion"] != nil) {
336
+ [defaults removeObjectForKey:keyRolledBackMarked];
337
+ [self clearInvalidFiles];
338
+ }
339
+
340
+ if ([[defaults objectForKey:KeyPackageUpdatedMarked] boolValue]) {
341
+ [defaults removeObjectForKey:KeyPackageUpdatedMarked];
342
+ [self clearInvalidFiles];
343
+ }
344
+ });
321
345
 
322
346
  return ret;
323
347
  }
@@ -438,13 +462,15 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
438
462
  resolve(@true);
439
463
  #else
440
464
 
441
- NSUserDefaults *defaults = PushyDefaults();
442
- pushy::state::MarkSuccessResult result =
443
- pushy::state::MarkSuccess(PushyStateFromDefaults(defaults));
444
- if (!result.stale_version_to_delete.empty()) {
445
- [defaults removeObjectForKey:PushyHashInfoKey(PushyFromStdString(result.stale_version_to_delete))];
446
- }
447
- PushyApplyStateToDefaults(defaults, result.state);
465
+ PushyWithStateLock(^{
466
+ NSUserDefaults *defaults = PushyDefaults();
467
+ pushy::state::MarkSuccessResult result =
468
+ pushy::state::MarkSuccess(PushyStateFromDefaults(defaults));
469
+ if (!result.stale_version_to_delete.empty()) {
470
+ [defaults removeObjectForKey:PushyHashInfoKey(PushyFromStdString(result.stale_version_to_delete))];
471
+ }
472
+ PushyApplyStateToDefaults(defaults, result.state);
473
+ });
448
474
 
449
475
  [self clearInvalidFiles];
450
476
  resolve(@true);
@@ -674,38 +700,39 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
674
700
  return NO;
675
701
  }
676
702
 
677
- NSUserDefaults *defaults = PushyDefaults();
678
- pushy::state::State next = pushy::state::SwitchVersion(
679
- PushyStateFromDefaults(defaults),
680
- PushyToStdString(hash)
681
- );
682
- PushyApplyStateToDefaults(defaults, next);
683
- // Re-enable first-load consumption and rollback checks for the newly selected bundle.
684
- ignoreRollback = false;
703
+ PushyWithStateLock(^{
704
+ NSUserDefaults *defaults = PushyDefaults();
705
+ pushy::state::State next = pushy::state::SwitchVersion(
706
+ PushyStateFromDefaults(defaults),
707
+ PushyToStdString(hash)
708
+ );
709
+ PushyApplyStateToDefaults(defaults, next);
710
+ // Re-enable first-load consumption and rollback checks for the newly selected bundle.
711
+ ignoreRollback = false;
712
+ });
685
713
  return YES;
686
714
  }
687
715
 
688
716
  - (BOOL)ensureDirectoryExistsAtPath:(NSString *)path
689
717
  {
690
- __block BOOL success = NO;
691
-
692
- dispatch_sync(_fileQueue, ^{
693
- NSFileManager *fileManager = [NSFileManager defaultManager];
694
- BOOL isDirectory = NO;
695
- if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
696
- success = isDirectory;
697
- return;
698
- }
718
+ // No _fileQueue hop here: that queue also runs multi-second unzip/patch
719
+ // work, and a dispatch_sync onto it would block the whole module method
720
+ // queue for the duration. createDirectoryAtPath is idempotent and
721
+ // thread-safe, so checking inline is fine.
722
+ NSFileManager *fileManager = [NSFileManager defaultManager];
723
+ BOOL isDirectory = NO;
724
+ if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
725
+ return isDirectory;
726
+ }
699
727
 
700
- NSError *error = nil;
701
- success = [fileManager createDirectoryAtPath:path
702
- withIntermediateDirectories:YES
703
- attributes:nil
704
- error:&error];
705
- if (!success && error != nil) {
706
- RCTLogWarn(@"Pushy create directory error: %@", error.localizedDescription);
707
- }
708
- });
728
+ NSError *error = nil;
729
+ BOOL success = [fileManager createDirectoryAtPath:path
730
+ withIntermediateDirectories:YES
731
+ attributes:nil
732
+ error:&error];
733
+ if (!success && error != nil) {
734
+ RCTLogWarn(@"Pushy create directory error: %@", error.localizedDescription);
735
+ }
709
736
 
710
737
  return success;
711
738
  }
@@ -741,8 +768,12 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
741
768
  - (void)clearInvalidFiles
742
769
  {
743
770
  dispatch_async(_fileQueue, ^{
744
- NSUserDefaults *defaults = PushyDefaults();
745
- pushy::state::State state = PushyStateFromDefaults(defaults);
771
+ // Snapshot the state under the lock, but run the (slow) filesystem
772
+ // cleanup outside of it so state operations are not blocked.
773
+ __block pushy::state::State state;
774
+ PushyWithStateLock(^{
775
+ state = PushyStateFromDefaults(PushyDefaults());
776
+ });
746
777
  NSString *downloadDir = [RCTPushy downloadDir];
747
778
  pushy::patch::Status status = pushy::patch::CleanupOldEntries(
748
779
  PushyToStdString(downloadDir),
@@ -10,6 +10,8 @@ static NSString *const RCTPushyDownloaderErrorDomain = @"cn.reactnative.pushy";
10
10
  @property (copy) NSString *savePath;
11
11
  @property (nonatomic, strong) NSError *fileError;
12
12
  @property (nonatomic, assign) BOOL finished;
13
+ @property (nonatomic, assign) int lastReportedPercentage;
14
+ @property (nonatomic, assign) long long lastReportedBytes;
13
15
  @end
14
16
 
15
17
  @implementation RCTPushyDownloader
@@ -42,6 +44,10 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
42
44
  }
43
45
 
44
46
  NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
47
+ // Avoid hanging forever on a stalled connection (default resource timeout
48
+ // is 7 days). These are generous enough for large bundles on slow networks.
49
+ sessionConfig.timeoutIntervalForRequest = 30;
50
+ sessionConfig.timeoutIntervalForResource = 300;
45
51
  self.session = [NSURLSession sessionWithConfiguration:sessionConfig
46
52
  delegate:self
47
53
  delegateQueue:nil];
@@ -77,14 +83,47 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
77
83
  totalBytesWritten:(int64_t)totalBytesWritten
78
84
  totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
79
85
  {
80
- if (self.progressHandler) {
81
- self.progressHandler(totalBytesWritten ,totalBytesExpectedToWrite);
86
+ if (!self.progressHandler) {
87
+ return;
88
+ }
89
+ // Normalize an unknown total (NSURLSessionTransferSizeUnknown == -1) to 0 so
90
+ // the JS side does not compute a negative/NaN percentage.
91
+ long long total = totalBytesExpectedToWrite > 0 ? totalBytesExpectedToWrite : 0;
92
+ if (total > 0) {
93
+ int percentage = (int)((totalBytesWritten * 100.0 / total) + 0.5);
94
+ if (percentage <= self.lastReportedPercentage) {
95
+ return;
96
+ }
97
+ self.lastReportedPercentage = percentage;
98
+ } else {
99
+ // Total unknown: throttle by bytes to avoid flooding the bridge.
100
+ if (totalBytesWritten - self.lastReportedBytes < 256 * 1024) {
101
+ return;
102
+ }
103
+ self.lastReportedBytes = totalBytesWritten;
82
104
  }
105
+ self.progressHandler(totalBytesWritten, total);
83
106
  }
84
107
 
85
108
  - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
86
109
  didFinishDownloadingToURL:(NSURL *)location
87
110
  {
111
+ // A completed transfer does not imply success: 404/500 pages, CDN error
112
+ // bodies and captive-portal HTML all arrive here. Reject non-2xx responses
113
+ // before touching savePath so an existing valid package is not destroyed.
114
+ NSURLResponse *response = downloadTask.response;
115
+ if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
116
+ NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
117
+ if (statusCode < 200 || statusCode >= 300) {
118
+ self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
119
+ code:statusCode
120
+ userInfo:@{
121
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"unexpected http status code %ld", (long)statusCode],
122
+ }];
123
+ return;
124
+ }
125
+ }
126
+
88
127
  NSFileManager *fileManager = [NSFileManager defaultManager];
89
128
  [fileManager removeItemAtPath:self.savePath error:nil];
90
129
 
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.43.3",
3
+ "version": "10.45.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
+ "types": "src/index.ts",
6
7
  "scripts": {
7
8
  "prepublishOnly": "NODE_ENV=production bun scripts/prepublish.ts",
8
9
  "publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
9
10
  "postinstall": "node scripts/check-expo-version.js",
10
11
  "prepack": "bun submodule && bun lint",
11
- "lint": "eslint \"src/*.@(ts|tsx|js|jsx)\" && tsc --noEmit",
12
+ "lint": "eslint \"src/*.@(ts|tsx|js|jsx)\" && tsc --noEmit && node scripts/check-harmony-types.js",
13
+ "lint:harmony": "node scripts/check-harmony-types.js",
12
14
  "submodule": "git submodule update --init --recursive",
13
15
  "test": "bun test src/__tests__",
14
16
  "test:patch-core": "./scripts/test-patch-core.sh",
15
17
  "build:harmony-har": "node scripts/build-harmony-har.js",
16
- "build:so": "bun submodule && $ANDROID_HOME/ndk/28.2.13676358/ndk-build NDK_PROJECT_PATH=android APP_BUILD_SCRIPT=android/jni/Android.mk NDK_APPLICATION_MK=android/jni/Application.mk NDK_LIBS_OUT=android/lib",
18
+ "build:so": "bun submodule && bash scripts/build-android-so.sh",
17
19
  "build:ios-debug": "cd Example/e2etest && bun && detox build --configuration ios.sim.debug",
18
20
  "build:ios-release": "cd Example/e2etest && bun && detox build --configuration ios.sim.release",
19
21
  "test:ios-debug": "cd Example/e2etest && E2E_PLATFORM=ios detox test --configuration ios.sim.debug",
@@ -67,6 +69,7 @@
67
69
  "@types/jest": "^30.0.0",
68
70
  "@types/node": "^25.5.0",
69
71
  "@types/react": "^19.2.14",
72
+ "@types/react-test-renderer": "18.0.7",
70
73
  "detox": "^20.50.1",
71
74
  "eslint": "^8.57.0",
72
75
  "firebase-tools": "^15.11.0",
@@ -75,6 +78,7 @@
75
78
  "prettier": "^2",
76
79
  "react": "18.2.0",
77
80
  "react-native": "0.73",
81
+ "react-test-renderer": "18.2.0",
78
82
  "ts-jest": "^29.4.6",
79
83
  "typescript": "^5.6.3"
80
84
  }