react-native-update 10.44.0 → 10.46.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 (37) 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/DownloadTask.java +10 -1
  7. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +24 -0
  8. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +2 -1
  9. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +2 -1
  10. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +19 -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 +15 -15
  13. package/cpp/patch_core/error_codes.h +43 -0
  14. package/cpp/patch_core/patch_core.cpp +16 -1
  15. package/cpp/patch_core/patch_core.h +6 -0
  16. package/cpp/patch_core/tests/patch_core_test.cpp +77 -0
  17. package/harmony/pushy/src/main/cpp/pushy.cpp +1 -93
  18. package/harmony/pushy/src/main/ets/DownloadTask.ts +13 -10
  19. package/harmony/pushy/src/main/ets/NativePatchCore.ts +0 -4
  20. package/harmony/pushy/src/main/ets/UpdateContext.ts +20 -2
  21. package/harmony/pushy.har +0 -0
  22. package/ios/RCTPushy/RCTPushy.mm +223 -132
  23. package/ios/RCTPushy/RCTPushyDownloader.mm +22 -1
  24. package/package.json +7 -3
  25. package/src/client.ts +151 -53
  26. package/src/context.ts +21 -7
  27. package/src/core.ts +8 -2
  28. package/src/endpoint.ts +4 -2
  29. package/src/error.ts +68 -0
  30. package/src/index.ts +10 -1
  31. package/src/locales/en.ts +5 -1
  32. package/src/locales/zh.ts +3 -1
  33. package/src/provider.tsx +101 -54
  34. package/src/type.ts +6 -0
  35. package/src/utils.ts +56 -16
  36. package/android/jni/DownloadTask.c +0 -56
  37. package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
@@ -2,6 +2,7 @@
2
2
  #import "RCTPushyDownloader.h"
3
3
  #import "ZipArchive.h"
4
4
  #include "../../cpp/patch_core/archive_patch_core.h"
5
+ #include "../../cpp/patch_core/error_codes.h"
5
6
  #include "../../cpp/patch_core/patch_core.h"
6
7
  #include "../../cpp/patch_core/state_core.h"
7
8
 
@@ -14,6 +15,9 @@
14
15
 
15
16
  #import <React/RCTConvert.h>
16
17
  #import <React/RCTLog.h>
18
+ #import <os/lock.h>
19
+
20
+ #include <atomic>
17
21
 
18
22
  static NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
19
23
  static NSString *const paramPackageVersion = @"packageVersion";
@@ -34,9 +38,15 @@ static NSString * const BUNDLE_FILE_NAME = @"index.bundlejs";
34
38
  static NSString * const SOURCE_PATCH_NAME = @"__diff.json";
35
39
  static NSString * const BUNDLE_PATCH_NAME = @"index.bundlejs.patch";
36
40
 
37
- // error def
41
+ // error def — messages are human-readable; the stable cross-platform codes
42
+ // live in cpp/patch_core/error_codes.h and travel in PushyErrorCodeKey.
38
43
  static NSString * const ERROR_OPTIONS = @"options error";
39
44
  static NSString * const ERROR_FILE_OPERATION = @"file operation error";
45
+ static NSString * const PushyErrorCodeKey = @"PushyErrorCode";
46
+
47
+ static NSString *PushyCode(const char *code) {
48
+ return [NSString stringWithUTF8String:code];
49
+ }
40
50
 
41
51
  // event def
42
52
  static NSString * const EVENT_PROGRESS_DOWNLOAD = @"RCTPushyDownloadProgress";
@@ -52,7 +62,20 @@ typedef NS_ENUM(NSInteger, PushyType) {
52
62
  //TASK_TYPE_PLAIN_DOWNLOAD=4?
53
63
  };
54
64
 
55
- static BOOL ignoreRollback = false;
65
+ static std::atomic<bool> ignoreRollback{false};
66
+
67
+ // Serializes every read-modify-write of the persisted update state. The state
68
+ // machine itself is a pure function (state_core), but callers run on different
69
+ // threads (main thread bundleURL, module method queue, _fileQueue), so the
70
+ // read→transform→write sequence must be atomic to avoid e.g. markSuccess being
71
+ // overwritten by a concurrent bundleURL and the version being rolled back.
72
+ static os_unfair_lock pushyStateLock = OS_UNFAIR_LOCK_INIT;
73
+
74
+ static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) {
75
+ os_unfair_lock_lock(&pushyStateLock);
76
+ block();
77
+ os_unfair_lock_unlock(&pushyStateLock);
78
+ }
56
79
 
57
80
  static std::string PushyToStdString(NSString *value) {
58
81
  if (value == nil) {
@@ -64,7 +87,10 @@ static std::string PushyToStdString(NSString *value) {
64
87
  static NSError *PushyNSErrorFromStatus(const pushy::patch::Status &status) {
65
88
  return [NSError errorWithDomain:PushyErrorDomain
66
89
  code:-1
67
- userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithUTF8String:status.message.c_str()] }];
90
+ userInfo:@{
91
+ NSLocalizedDescriptionKey: [NSString stringWithUTF8String:status.message.c_str()],
92
+ PushyErrorCodeKey: PushyCode(pushy::error_codes::kPatchFailed),
93
+ }];
68
94
  }
69
95
 
70
96
  static NSUserDefaults *PushyDefaults(void) {
@@ -102,21 +128,24 @@ static BOOL PushyStringIsBlank(NSString *value) {
102
128
  }
103
129
 
104
130
  static void PushyRejectError(RCTPromiseRejectBlock reject, NSError *error) {
105
- reject([NSString stringWithFormat:@"%ld", (long)error.code], error.localizedDescription, error);
131
+ // Prefer the stable cross-platform code (error_codes.h); fall back to the
132
+ // numeric NSError code for system errors that were not classified.
133
+ NSString *code = error.userInfo[PushyErrorCodeKey];
134
+ if (code == nil) {
135
+ code = [NSString stringWithFormat:@"%ld", (long)error.code];
136
+ }
137
+ reject(code, error.localizedDescription, error);
106
138
  }
107
139
 
108
- static NSError *PushyErrorWithMessage(NSString *message) {
140
+ static NSError *PushyErrorWithCode(const char *code, NSString *message) {
109
141
  return [NSError errorWithDomain:PushyErrorDomain
110
142
  code:-1
111
143
  userInfo:@{
112
144
  NSLocalizedDescriptionKey: message ?: @"unknown error",
145
+ PushyErrorCodeKey: PushyCode(code),
113
146
  }];
114
147
  }
115
148
 
116
- static void PushyRejectMessage(RCTPromiseRejectBlock reject, NSString *message) {
117
- PushyRejectError(reject, PushyErrorWithMessage(message));
118
- }
119
-
120
149
  static pushy::patch::PatchManifest PushyPatchManifestFromJson(NSDictionary *json) {
121
150
  pushy::patch::PatchManifest manifest;
122
151
 
@@ -221,64 +250,84 @@ RCT_EXPORT_MODULE(RCTPushy);
221
250
 
222
251
  + (NSURL *)bundleURL
223
252
  {
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
- }
253
+ __block NSURL *resolvedURL = nil;
254
+ PushyWithStateLock(^{
255
+ NSUserDefaults *defaults = PushyDefaults();
240
256
 
241
- if (!state.current_version.empty()) {
242
- pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
257
+ NSString *curPackageVersion = [RCTPushy packageVersion];
258
+ NSString *curBuildTime = [RCTPushy buildTime];
259
+
260
+ pushy::state::State state = PushyStateFromDefaults(defaults);
261
+ pushy::state::BinaryVersionSyncResult sync = pushy::state::SyncBinaryVersion(
243
262
  state,
244
- ignoreRollback,
245
- true
263
+ PushyToStdString(curPackageVersion),
264
+ PushyToStdString(curBuildTime)
246
265
  );
247
- state = decision.state;
248
-
249
- if (decision.did_rollback || decision.consumed_first_time) {
266
+ if (sync.changed) {
267
+ [defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked];
268
+ state = sync.state;
250
269
  PushyApplyStateToDefaults(defaults, state);
251
270
  }
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
271
 
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);
272
+ if (!state.current_version.empty()) {
273
+ std::string const versionBeforeLaunch = state.current_version;
274
+ pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
275
+ state,
276
+ ignoreRollback.load(),
277
+ true
278
+ );
279
+ state = decision.state;
280
+
281
+ if (decision.did_rollback) {
282
+ // The crash-protection rollback: the new version never called
283
+ // markSuccess. Keep this visible in release logs.
284
+ RCTLogWarn(@"RCTPushy -- version %@ was not marked as successful, rolled back to %@",
285
+ PushyFromStdString(versionBeforeLaunch),
286
+ PushyFromStdString(state.current_version));
287
+ }
288
+ if (decision.did_rollback || decision.consumed_first_time) {
268
289
  PushyApplyStateToDefaults(defaults, state);
269
- loadVersion = PushyFromStdString(state.current_version);
290
+ }
291
+ if (decision.consumed_first_time) {
292
+ // bundleURL may be called many times, ignore rollbacks before process restarted again.
293
+ ignoreRollback = true;
294
+ [defaults setObject:@(YES) forKey:keyFirstLoadMarked];
295
+ }
296
+
297
+ NSString *loadVersion = PushyFromStdString(decision.load_version);
298
+ NSString *downloadDir = [RCTPushy downloadDir];
299
+ // Guard the rollback chain against cycles: a corrupted state
300
+ // returning an already-visited version would otherwise spin this
301
+ // loop forever during startup (Android has the same guard).
302
+ NSMutableSet<NSString *> *visitedVersions = [NSMutableSet set];
303
+ while (loadVersion.length && ![visitedVersions containsObject:loadVersion]) {
304
+ [visitedVersions addObject:loadVersion];
305
+ NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
306
+ if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
307
+ resolvedURL = [NSURL fileURLWithPath:bundlePath];
308
+ return;
309
+ } else {
310
+ RCTLogError(@"RCTPushy -- bundle version %@ not found, rolling back", loadVersion);
311
+ state = pushy::state::Rollback(state);
312
+ PushyApplyStateToDefaults(defaults, state);
313
+ loadVersion = PushyFromStdString(state.current_version);
314
+ }
270
315
  }
271
316
  }
272
- }
273
-
274
- return [RCTPushy binaryBundleURL];
317
+ });
318
+
319
+ return resolvedURL ?: [RCTPushy binaryBundleURL];
275
320
  }
276
321
 
277
322
  + (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);
323
+ __block NSString *currentVersion = nil;
324
+ PushyWithStateLock(^{
325
+ NSUserDefaults *defaults = PushyDefaults();
326
+ pushy::state::State state = pushy::state::Rollback(PushyStateFromDefaults(defaults));
327
+ PushyApplyStateToDefaults(defaults, state);
328
+ currentVersion = PushyFromStdString(state.current_version);
329
+ });
330
+ return currentVersion;
282
331
  }
283
332
 
284
333
  + (BOOL)requiresMainQueueSetup
@@ -288,36 +337,37 @@ RCT_EXPORT_MODULE(RCTPushy);
288
337
 
289
338
  - (NSDictionary *)constantsToExport
290
339
  {
291
- NSUserDefaults *defaults = PushyDefaults();
292
-
293
340
  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
-
341
+ PushyWithStateLock(^{
342
+ NSUserDefaults *defaults = PushyDefaults();
343
+
344
+ ret[@"downloadRootDir"] = [RCTPushy downloadDir];
345
+ ret[@"packageVersion"] = [RCTPushy packageVersion];
346
+ ret[@"buildTime"] = [RCTPushy buildTime];
347
+ ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
348
+ ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
349
+ ret[@"uuid"] = [defaults objectForKey:keyUuid];
350
+ NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
351
+ NSString *currentVersion = [pushyInfo objectForKey:paramCurrentVersion];
352
+ ret[@"currentVersion"] = currentVersion;
353
+ if (currentVersion != nil) {
354
+ ret[@"currentVersionInfo"] = [defaults objectForKey:PushyHashInfoKey(currentVersion)];
355
+ }
356
+
357
+ if (ret[@"isFirstTime"]) {
358
+ [defaults removeObjectForKey:keyFirstLoadMarked];
359
+ }
360
+
361
+ if (ret[@"rolledBackVersion"] != nil) {
362
+ [defaults removeObjectForKey:keyRolledBackMarked];
363
+ [self clearInvalidFiles];
364
+ }
365
+
366
+ if ([[defaults objectForKey:KeyPackageUpdatedMarked] boolValue]) {
367
+ [defaults removeObjectForKey:KeyPackageUpdatedMarked];
368
+ [self clearInvalidFiles];
369
+ }
370
+ });
321
371
 
322
372
  return ret;
323
373
  }
@@ -335,7 +385,7 @@ RCT_EXPORT_METHOD(setUuid:(NSString *)uuid resolver:(RCTPromiseResolveBlock)res
335
385
  rejecter:(RCTPromiseRejectBlock)reject)
336
386
  {
337
387
  if (PushyStringIsBlank(uuid)) {
338
- PushyRejectError(reject, PushyErrorWithMessage(ERROR_OPTIONS));
388
+ PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
339
389
  return;
340
390
  }
341
391
 
@@ -349,7 +399,7 @@ RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
349
399
  rejecter:(RCTPromiseRejectBlock)reject)
350
400
  {
351
401
  if (PushyStringIsBlank(hash) || PushyStringIsBlank(value)) {
352
- PushyRejectMessage(reject, ERROR_OPTIONS);
402
+ PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
353
403
  return;
354
404
  }
355
405
 
@@ -362,7 +412,9 @@ RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
362
412
 
363
413
  resolve(@true);
364
414
  } else {
365
- PushyRejectError(reject, error ?: PushyErrorWithMessage(@"json格式校验报错"));
415
+ PushyRejectError(reject, PushyErrorWithCode(
416
+ pushy::error_codes::kInvalidHashInfo,
417
+ error != nil ? error.localizedDescription : @"invalid json string"));
366
418
  }
367
419
  }
368
420
 
@@ -397,6 +449,15 @@ RCT_EXPORT_METHOD(downloadPatchFromPpk:(NSDictionary *)options
397
449
  [self downloadUpdate:PushyTypePatchFromPpk options:options resolver:resolve rejecter:reject];
398
450
  }
399
451
 
452
+ RCT_EXPORT_METHOD(downloadAndInstallApk:(NSDictionary *)options
453
+ resolver:(RCTPromiseResolveBlock)resolve
454
+ rejecter:(RCTPromiseRejectBlock)reject)
455
+ {
456
+ PushyRejectError(reject, PushyErrorWithCode(
457
+ pushy::error_codes::kUnsupportedPlatform,
458
+ @"downloadAndInstallApk is only supported on Android"));
459
+ }
460
+
400
461
  RCT_EXPORT_METHOD(setNeedUpdate:(NSDictionary *)options
401
462
  resolver:(RCTPromiseResolveBlock)resolve
402
463
  rejecter:(RCTPromiseRejectBlock)reject)
@@ -438,13 +499,15 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
438
499
  resolve(@true);
439
500
  #else
440
501
 
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);
502
+ PushyWithStateLock(^{
503
+ NSUserDefaults *defaults = PushyDefaults();
504
+ pushy::state::MarkSuccessResult result =
505
+ pushy::state::MarkSuccess(PushyStateFromDefaults(defaults));
506
+ if (!result.stale_version_to_delete.empty()) {
507
+ [defaults removeObjectForKey:PushyHashInfoKey(PushyFromStdString(result.stale_version_to_delete))];
508
+ }
509
+ PushyApplyStateToDefaults(defaults, result.state);
510
+ });
448
511
 
449
512
  [self clearInvalidFiles];
450
513
  resolve(@true);
@@ -476,6 +539,12 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
476
539
  {
477
540
  [self performUpdate:type options:options callback:^(NSError *error) {
478
541
  if (error != nil) {
542
+ if (error.userInfo[PushyErrorCodeKey] == nil) {
543
+ // Unclassified (system/network) errors from the download
544
+ // pipeline; keep the original message.
545
+ error = PushyErrorWithCode(pushy::error_codes::kDownloadFailed,
546
+ error.localizedDescription);
547
+ }
479
548
  PushyRejectError(reject, error);
480
549
  return;
481
550
  }
@@ -501,24 +570,38 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
501
570
  NSString *hash = PushyOptionString(options, @"hash");
502
571
 
503
572
  if (PushyStringIsBlank(updateUrl) || PushyStringIsBlank(hash)) {
504
- callback(PushyErrorWithMessage(ERROR_OPTIONS));
573
+ callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
505
574
  return;
506
575
  }
507
576
  NSString *originHash = PushyOptionString(options, @"originHash");
508
577
  if (type == PushyTypePatchFromPpk && PushyStringIsBlank(originHash)) {
509
- callback(PushyErrorWithMessage(ERROR_OPTIONS));
578
+ callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
510
579
  return;
511
580
  }
512
581
 
513
582
  NSString *dir = [RCTPushy downloadDir];
514
583
  BOOL success = [self ensureDirectoryExistsAtPath:dir];
515
584
  if (!success) {
516
- callback(PushyErrorWithMessage(ERROR_FILE_OPERATION));
585
+ callback(PushyErrorWithCode(pushy::error_codes::kFileOperationFailed, ERROR_FILE_OPERATION));
517
586
  return;
518
587
  }
519
588
 
520
589
  NSString *zipFilePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@",hash, [self zipExtension:type]]];
521
590
 
591
+ // On failure, remove the partial version directory like Android/Harmony
592
+ // do: a half-unzipped/half-patched dir leaks disk and could later be
593
+ // mistaken for a complete version. hash is validated non-blank above, so
594
+ // this can never resolve to the download root itself.
595
+ NSString *unzipDir = [dir stringByAppendingPathComponent:hash];
596
+ void (^completion)(NSError *) = ^(NSError *error) {
597
+ if (error != nil) {
598
+ dispatch_async(self->_fileQueue, ^{
599
+ [[NSFileManager defaultManager] removeItemAtPath:unzipDir error:nil];
600
+ });
601
+ }
602
+ callback(error);
603
+ };
604
+
522
605
  RCTLogInfo(@"RCTPushy -- download file %@", updateUrl);
523
606
  [RCTPushyDownloader download:updateUrl savePath:zipFilePath progressHandler:^(long long receivedBytes, long long totalBytes) {
524
607
  if (self->hasListeners) {
@@ -530,14 +613,14 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
530
613
  }
531
614
  } completionHandler:^(NSString *path, NSError *error) {
532
615
  if (error != nil) {
533
- callback(error);
616
+ completion(error);
534
617
  return;
535
618
  }
536
619
  [self unzipDownloadedPackage:zipFilePath
537
620
  hash:hash
538
621
  type:type
539
622
  originHash:originHash
540
- callback:callback];
623
+ callback:completion];
541
624
  }];
542
625
  }
543
626
 
@@ -603,18 +686,21 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
603
686
  NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
604
687
  NSData *data = [NSData dataWithContentsOfFile:sourcePatch];
605
688
  if (data == nil) {
606
- callback(PushyErrorWithMessage(@"missing patch manifest"));
689
+ callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"missing patch manifest"));
607
690
  return;
608
691
  }
609
692
 
610
693
  NSError *error = nil;
611
694
  id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
612
695
  if (error != nil) {
613
- callback(error);
696
+ // Classify as a patch failure like the sibling manifest branches;
697
+ // unclassified errors would otherwise be tagged DOWNLOAD_FAILED by the
698
+ // downloadUpdate fallback even though the download itself succeeded.
699
+ callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, error.localizedDescription));
614
700
  return;
615
701
  }
616
702
  if (![jsonObject isKindOfClass:[NSDictionary class]]) {
617
- callback(PushyErrorWithMessage(@"invalid patch manifest"));
703
+ callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"invalid patch manifest"));
618
704
  return;
619
705
  }
620
706
  NSDictionary *json = (NSDictionary *)jsonObject;
@@ -669,43 +755,44 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
669
755
  {
670
756
  if (PushyStringIsBlank(hash)) {
671
757
  if (error != NULL) {
672
- *error = PushyErrorWithMessage(ERROR_OPTIONS);
758
+ *error = PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS);
673
759
  }
674
760
  return NO;
675
761
  }
676
762
 
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;
763
+ PushyWithStateLock(^{
764
+ NSUserDefaults *defaults = PushyDefaults();
765
+ pushy::state::State next = pushy::state::SwitchVersion(
766
+ PushyStateFromDefaults(defaults),
767
+ PushyToStdString(hash)
768
+ );
769
+ PushyApplyStateToDefaults(defaults, next);
770
+ // Re-enable first-load consumption and rollback checks for the newly selected bundle.
771
+ ignoreRollback = false;
772
+ });
685
773
  return YES;
686
774
  }
687
775
 
688
776
  - (BOOL)ensureDirectoryExistsAtPath:(NSString *)path
689
777
  {
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
- }
778
+ // No _fileQueue hop here: that queue also runs multi-second unzip/patch
779
+ // work, and a dispatch_sync onto it would block the whole module method
780
+ // queue for the duration. createDirectoryAtPath is idempotent and
781
+ // thread-safe, so checking inline is fine.
782
+ NSFileManager *fileManager = [NSFileManager defaultManager];
783
+ BOOL isDirectory = NO;
784
+ if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
785
+ return isDirectory;
786
+ }
699
787
 
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
- });
788
+ NSError *error = nil;
789
+ BOOL success = [fileManager createDirectoryAtPath:path
790
+ withIntermediateDirectories:YES
791
+ attributes:nil
792
+ error:&error];
793
+ if (!success && error != nil) {
794
+ RCTLogWarn(@"Pushy create directory error: %@", error.localizedDescription);
795
+ }
709
796
 
710
797
  return success;
711
798
  }
@@ -731,7 +818,7 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
731
818
 
732
819
  NSError *unzipError = error;
733
820
  if (!succeeded && unzipError == nil) {
734
- unzipError = PushyErrorWithMessage(@"unzip failed");
821
+ unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"unzip failed");
735
822
  }
736
823
  completionHandler(unzipError);
737
824
  }];
@@ -741,8 +828,12 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
741
828
  - (void)clearInvalidFiles
742
829
  {
743
830
  dispatch_async(_fileQueue, ^{
744
- NSUserDefaults *defaults = PushyDefaults();
745
- pushy::state::State state = PushyStateFromDefaults(defaults);
831
+ // Snapshot the state under the lock, but run the (slow) filesystem
832
+ // cleanup outside of it so state operations are not blocked.
833
+ __block pushy::state::State state;
834
+ PushyWithStateLock(^{
835
+ state = PushyStateFromDefaults(PushyDefaults());
836
+ });
746
837
  NSString *downloadDir = [RCTPushy downloadDir];
747
838
  pushy::patch::Status status = pushy::patch::CleanupOldEntries(
748
839
  PushyToStdString(downloadDir),
@@ -113,7 +113,8 @@ didFinishDownloadingToURL:(NSURL *)location
113
113
  // before touching savePath so an existing valid package is not destroyed.
114
114
  NSURLResponse *response = downloadTask.response;
115
115
  if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
116
- NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
116
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
117
+ NSInteger statusCode = httpResponse.statusCode;
117
118
  if (statusCode < 200 || statusCode >= 300) {
118
119
  self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
119
120
  code:statusCode
@@ -122,6 +123,26 @@ didFinishDownloadingToURL:(NSURL *)location
122
123
  }];
123
124
  return;
124
125
  }
126
+
127
+ // Reject truncated transfers like Android/Harmony do. Skip the check
128
+ // for encoded responses (e.g. gzip): NSURLSession decompresses them
129
+ // transparently, so the on-disk size legitimately differs from the
130
+ // Content-Length of the encoded body.
131
+ NSString *contentEncoding = [httpResponse.allHeaderFields[@"Content-Encoding"] lowercaseString];
132
+ BOOL isEncodedBody = contentEncoding.length > 0 && ![contentEncoding isEqualToString:@"identity"];
133
+ long long expectedLength = httpResponse.expectedContentLength;
134
+ if (!isEncodedBody && expectedLength > 0) {
135
+ NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:location.path
136
+ error:nil][NSFileSize];
137
+ if (fileSize != nil && fileSize.longLongValue != expectedLength) {
138
+ self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
139
+ code:-1
140
+ userInfo:@{
141
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"download incomplete: expected %lld bytes, got %lld", expectedLength, fileSize.longLongValue],
142
+ }];
143
+ return;
144
+ }
145
+ }
125
146
  }
126
147
 
127
148
  NSFileManager *fileManager = [NSFileManager defaultManager];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.44.0",
3
+ "version": "10.46.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
@@ -9,12 +9,14 @@
9
9
  "publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
10
10
  "postinstall": "node scripts/check-expo-version.js",
11
11
  "prepack": "bun submodule && bun lint",
12
- "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",
13
14
  "submodule": "git submodule update --init --recursive",
14
15
  "test": "bun test src/__tests__",
15
16
  "test:patch-core": "./scripts/test-patch-core.sh",
16
17
  "build:harmony-har": "node scripts/build-harmony-har.js",
17
- "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:harmony-e2e": "bash Example/e2etest/scripts/build-harmony-e2e.sh",
19
+ "build:so": "bun submodule && bash scripts/build-android-so.sh",
18
20
  "build:ios-debug": "cd Example/e2etest && bun && detox build --configuration ios.sim.debug",
19
21
  "build:ios-release": "cd Example/e2etest && bun && detox build --configuration ios.sim.release",
20
22
  "test:ios-debug": "cd Example/e2etest && E2E_PLATFORM=ios detox test --configuration ios.sim.debug",
@@ -68,6 +70,7 @@
68
70
  "@types/jest": "^30.0.0",
69
71
  "@types/node": "^25.5.0",
70
72
  "@types/react": "^19.2.14",
73
+ "@types/react-test-renderer": "18.0.7",
71
74
  "detox": "^20.50.1",
72
75
  "eslint": "^8.57.0",
73
76
  "firebase-tools": "^15.11.0",
@@ -76,6 +79,7 @@
76
79
  "prettier": "^2",
77
80
  "react": "18.2.0",
78
81
  "react-native": "0.73",
82
+ "react-test-renderer": "18.2.0",
79
83
  "ts-jest": "^29.4.6",
80
84
  "typescript": "^5.6.3"
81
85
  }