react-native-update 10.57.1 → 10.58.1

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.
Binary file
Binary file
@@ -76,6 +76,15 @@ State Rollback(const State& state) {
76
76
  return next;
77
77
  }
78
78
 
79
+ State ForgetPurgedVersions(const State& state) {
80
+ State next = state;
81
+ next.current_version.clear();
82
+ next.last_version.clear();
83
+ next.first_time = false;
84
+ next.first_time_ok = true;
85
+ return next;
86
+ }
87
+
79
88
  bool ShouldRollbackForBrokenFirstLoad(const State& state) {
80
89
  return !state.first_time && !state.first_time_ok;
81
90
  }
@@ -47,6 +47,12 @@ State ClearRollbackMark(const State& state);
47
47
 
48
48
  State Rollback(const State& state);
49
49
 
50
+ // The OS deleted the installed versions' files (tvOS purges Caches while the
51
+ // app is not running): they were not rejected, so unlike Rollback this leaves
52
+ // no rolled-back mark that would make the next check skip reinstalling them.
53
+ // An existing mark (a rollback earlier in this launch) is kept.
54
+ State ForgetPurgedVersions(const State& state);
55
+
50
56
  bool ShouldRollbackForBrokenFirstLoad(const State& state);
51
57
 
52
58
  LaunchDecision ResolveLaunchState(
package/harmony/pushy.har CHANGED
Binary file
@@ -98,6 +98,14 @@ static NSString *PushyCode(const char *code) {
98
98
  return [NSString stringWithUTF8String:code];
99
99
  }
100
100
 
101
+ // cInfo.os label prefix; must match the JS side (core.ts) so the server
102
+ // buckets tvOS devices apart from iOS ones.
103
+ #if TARGET_OS_TV
104
+ static NSString * const PushyOsName = @"tvos";
105
+ #else
106
+ static NSString * const PushyOsName = @"ios";
107
+ #endif
108
+
101
109
  // event def
102
110
  static NSString * const EVENT_PROGRESS_DOWNLOAD = @"RCTPushyDownloadProgress";
103
111
  static NSString * const PARAM_PROGRESS_HASH = @"hash";
@@ -732,6 +740,10 @@ static void PushySwitchVersionLocked(NSString *hash) {
732
740
  + (BOOL)hasRunnableConfig;
733
741
  + (NSDictionary *)checkAndUpdate;
734
742
  + (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion;
743
+ #if TARGET_OS_TV && !DEBUG
744
+ + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion
745
+ rolledBack:(NSString *)launchRolledBackVersion;
746
+ #endif
735
747
  + (void)markJsCheckCompleted:(NSString *)config;
736
748
  + (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime;
737
749
  + (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime;
@@ -742,7 +754,8 @@ static void PushySwitchVersionLocked(NSString *hash) {
742
754
  responseText:(NSString *)responseText
743
755
  request:(NSString *)requestBody
744
756
  config:(NSString *)configJson
745
- responseAt:(long long)responseAtSeconds;
757
+ responseAt:(long long)responseAtSeconds
758
+ activated:(BOOL *)activatedOut;
746
759
  @end
747
760
 
748
761
  // One round per process, whoever starts it first — the delayed cold-start
@@ -755,6 +768,15 @@ static std::atomic<bool> pushyRoundCompleted{false};
755
768
  // it downloads (§11.3).
756
769
  static std::atomic<bool> pushyCrashRescueActive{false};
757
770
  static std::atomic<bool> pushyRescueAttempted{false};
771
+ // Set when this process's round is the tvOS purged-version restore.
772
+ static std::atomic<bool> pushyPurgeRestoreActive{false};
773
+ // True while +bundleURL waits for that restore. Only then may the round
774
+ // activate what it downloads on its own (JS has not started to decide), since
775
+ // the launch resolves the bundle again afterwards. Closed once the wait ends,
776
+ // timeout or not; guarded by the state lock that every activation commits
777
+ // under, so a commit lands either before the close (and launches) or after it
778
+ // (and is left to JS) — never selected behind the packaged bundle's back.
779
+ static bool pushyPurgeRestoreWindowOpen = false;
758
780
  static dispatch_semaphore_t pushyRoundDone;
759
781
  static NSString *pushyLaunchRolledBackForRescue = nil;
760
782
  // A version this process downloaded but left for JS to activate. If the
@@ -785,6 +807,9 @@ static const NSTimeInterval kPushyRescueTriggerUptime = 60;
785
807
  static const NSTimeInterval kPushyRescueBudgetBackgroundThread = 10;
786
808
  // A held main thread freezes UI teardown; stay well under the watchdog.
787
809
  static const NSTimeInterval kPushyRescueBudgetMainThread = 3.5;
810
+ // The purged-version restore blocks +bundleURL, usually inside
811
+ // application:didFinishLaunching: — stay clear of the launch watchdog.
812
+ static const NSTimeInterval kPushyPurgeRestoreBudget = 12;
788
813
 
789
814
  static void PushyMaybeHoldForRescue(void) {
790
815
  // Once per process; a second crashing thread passes straight through
@@ -935,8 +960,42 @@ RCT_EXPORT_MODULE(RCTPushy);
935
960
  + (NSURL *)bundleURL
936
961
  {
937
962
  pushyIsUsingBundleUrl = true;
963
+ NSString *launchRolledBackVersion = nil;
964
+ @try {
965
+ NSString *purgedVersion = nil;
966
+ NSURL *resolvedURL = [RCTPushy resolveLaunchBundleURL:&launchRolledBackVersion
967
+ purgedVersion:&purgedVersion];
968
+ #if TARGET_OS_TV && !DEBUG
969
+ // tvOS deleted the installed version while the app was not running.
970
+ // The app is expected to be online: block the launch on a native
971
+ // round that reinstalls the latest version, instead of showing the
972
+ // packaged bundle and jumping forward again on a later launch.
973
+ if (purgedVersion != nil &&
974
+ [RCTPushyOrchestrator restorePurgedLaunch:purgedVersion
975
+ rolledBack:launchRolledBackVersion]) {
976
+ resolvedURL = [RCTPushy resolveLaunchBundleURL:&launchRolledBackVersion
977
+ purgedVersion:&purgedVersion];
978
+ }
979
+ #endif
980
+ return resolvedURL ?: [RCTPushy binaryBundleURL];
981
+ } @finally {
982
+ // State corruption is exactly when the rescue path matters most. If
983
+ // resolution throws before a snapshot exists, nil safely omits only
984
+ // this launch's rollback guard instead of disabling the check.
985
+ [RCTPushyOrchestrator scheduleFromColdStart:launchRolledBackVersion];
986
+ }
987
+ }
988
+
989
+ // Resolves the bundle to launch from the persisted state; nil means the
990
+ // packaged bundle. Consumes the launch's one-shot state (first_time), so a
991
+ // second call is only valid after something re-armed it (a switchVersion).
992
+ // purgedVersion is set on tvOS when the installed version's files are gone.
993
+ + (NSURL *)resolveLaunchBundleURL:(NSString **)launchRolledBackVersionOut
994
+ purgedVersion:(NSString **)purgedVersionOut
995
+ {
938
996
  __block NSURL *resolvedURL = nil;
939
997
  __block NSString *launchRolledBackVersion = nil;
998
+ __block NSString *purgedVersion = nil;
940
999
  @try {
941
1000
  PushyWithStateLock(^{
942
1001
  NSUserDefaults *defaults = PushyDefaults();
@@ -995,10 +1054,29 @@ RCT_EXPORT_MODULE(RCTPushy);
995
1054
  resolvedURL = [NSURL fileURLWithPath:bundlePath];
996
1055
  break;
997
1056
  } else {
1057
+ #if TARGET_OS_TV
1058
+ // Caches is purgeable on tvOS: a missing bundle is the OS
1059
+ // reclaiming space, not a bad release. Everything under
1060
+ // rctpushy went with it, so walking back to last_version
1061
+ // is pointless, and a rollback mark would make this
1062
+ // launch's check refuse to reinstall the version.
1063
+ RCTLogWarn(@"RCTPushy -- bundle version %@ was purged by the system", loadVersion);
1064
+ purgedVersion = loadVersion;
1065
+ state = pushy::state::ForgetPurgedVersions(state);
1066
+ PushyApplyStateToDefaults(defaults, state);
1067
+ if (decision.consumed_first_time) {
1068
+ // The first load being consumed never happens: the
1069
+ // packaged bundle must not report isFirstTime.
1070
+ ignoreRollback = false;
1071
+ [defaults removeObjectForKey:keyFirstLoadMarked];
1072
+ }
1073
+ break;
1074
+ #else
998
1075
  RCTLogError(@"RCTPushy -- bundle version %@ not found, rolling back", loadVersion);
999
1076
  state = pushy::state::Rollback(state);
1000
1077
  PushyApplyStateToDefaults(defaults, state);
1001
1078
  loadVersion = PushyFromStdString(state.current_version);
1079
+ #endif
1002
1080
  }
1003
1081
  }
1004
1082
  }
@@ -1007,13 +1085,11 @@ RCT_EXPORT_MODULE(RCTPushy);
1007
1085
  // launch just rolled back.
1008
1086
  launchRolledBackVersion = PushyFromStdString(state.rolled_back_version);
1009
1087
  });
1010
- return resolvedURL ?: [RCTPushy binaryBundleURL];
1011
1088
  } @finally {
1012
- // State corruption is exactly when the rescue path matters most. If
1013
- // resolution throws before a snapshot exists, nil safely omits only
1014
- // this launch's rollback guard instead of disabling the check.
1015
- [RCTPushyOrchestrator scheduleFromColdStart:launchRolledBackVersion];
1089
+ *launchRolledBackVersionOut = launchRolledBackVersion;
1090
+ *purgedVersionOut = purgedVersion;
1016
1091
  }
1092
+ return resolvedURL;
1017
1093
  }
1018
1094
 
1019
1095
  + (NSString *) rollback {
@@ -1919,6 +1995,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
1919
1995
  // excluded from backups.
1920
1996
  + (void)excludeFromBackup:(NSString *)path
1921
1997
  {
1998
+ #if !TARGET_OS_TV // tvOS stores under Caches, which is never backed up.
1922
1999
  NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES];
1923
2000
  NSError *error = nil;
1924
2001
  if (![url setResourceValue:@YES
@@ -1926,6 +2003,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
1926
2003
  error:&error]) {
1927
2004
  RCTLogWarn(@"Pushy exclude from backup error: %@", error.localizedDescription);
1928
2005
  }
2006
+ #endif
1929
2007
  }
1930
2008
 
1931
2009
  - (void)unzipFileAtPath:(NSString *)path
@@ -2047,7 +2125,15 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
2047
2125
 
2048
2126
  + (NSString *)downloadDir
2049
2127
  {
2050
- NSString *directory = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject];
2128
+ #if TARGET_OS_TV
2129
+ // tvOS apps cannot write Application Support; Caches is the only
2130
+ // writable, non-temporary location, and the system may purge it while
2131
+ // the app is not running (see the purged-version path in +bundleURL).
2132
+ NSSearchPathDirectory base = NSCachesDirectory;
2133
+ #else
2134
+ NSSearchPathDirectory base = NSApplicationSupportDirectory;
2135
+ #endif
2136
+ NSString *directory = [NSSearchPathForDirectoriesInDomains(base, NSUserDomainMask, YES) firstObject];
2051
2137
  return [directory stringByAppendingPathComponent:@"rctpushy"];
2052
2138
  }
2053
2139
 
@@ -2248,12 +2334,9 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2248
2334
  && !config.Get("appKey").AsString().empty();
2249
2335
  }
2250
2336
 
2251
- + (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion {
2252
- #if !DEBUG
2253
- // Once per process; a few seconds of delay keeps the check away from the
2254
- // cold-start critical path (§7 R5) — its result targets the NEXT launch.
2255
- // Unless the previous process died mid-round (residual incomplete
2256
- // marker), in which case every launch second counts (§11.4).
2337
+ // Process-wide round state, set up by whichever launch path needs a round
2338
+ // first: the delayed cold-start check or the tvOS purged-version restore.
2339
+ + (void)prepareProcess:(NSString *)launchRolledBackVersion {
2257
2340
  static dispatch_once_t onceToken;
2258
2341
  dispatch_once(&onceToken, ^{
2259
2342
  pushyRoundDone = dispatch_semaphore_create(0);
@@ -2262,12 +2345,24 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2262
2345
  pushyProcessAnchorUptime = PushyMonotonicNow();
2263
2346
  pushyLaunchRolledBackForRescue = [launchRolledBackVersion copy];
2264
2347
  pushyNativeCheckReady.store(true);
2265
- NSUserDefaults *defaults = PushyDefaults();
2266
2348
  // The crash-hold rescue shares the orchestrator's rollout gate: no
2267
2349
  // persisted config, no handler (§11.3).
2268
- if ([defaults stringForKey:keyNativeConfig].length > 0) {
2350
+ if ([PushyDefaults() stringForKey:keyNativeConfig].length > 0) {
2269
2351
  PushyInstallCrashRescueHandler();
2270
2352
  }
2353
+ });
2354
+ }
2355
+
2356
+ + (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion {
2357
+ #if !DEBUG
2358
+ // Once per process; a few seconds of delay keeps the check away from the
2359
+ // cold-start critical path (§7 R5) — its result targets the NEXT launch.
2360
+ // Unless the previous process died mid-round (residual incomplete
2361
+ // marker), in which case every launch second counts (§11.4).
2362
+ static dispatch_once_t onceToken;
2363
+ dispatch_once(&onceToken, ^{
2364
+ [self prepareProcess:launchRolledBackVersion];
2365
+ NSUserDefaults *defaults = PushyDefaults();
2271
2366
  int64_t delaySeconds =
2272
2367
  [defaults objectForKey:keyNativeCheckIncomplete] != nil ? 0 : 5;
2273
2368
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delaySeconds * NSEC_PER_SEC),
@@ -2284,6 +2379,52 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2284
2379
  #endif
2285
2380
  }
2286
2381
 
2382
+ #if TARGET_OS_TV && !DEBUG
2383
+ // Runs this process's round before the first bundle loads, blocking the
2384
+ // caller for at most kPushyPurgeRestoreBudget. The state no longer has a
2385
+ // current version, so the server answers with the latest version for the
2386
+ // packaged bundle, and the round activates it while the restore window is
2387
+ // open. Returns NO when no round can run; otherwise the caller must resolve
2388
+ // the launch bundle again: it launches whatever was activated before the
2389
+ // window closed, or the packaged bundle. The round stops at the same
2390
+ // deadline, keeping a partial download for the next check to resume.
2391
+ + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion
2392
+ rolledBack:(NSString *)launchRolledBackVersion {
2393
+ [self prepareProcess:launchRolledBackVersion];
2394
+ if (![self hasRunnableConfig]) {
2395
+ RCTLogWarn(@"RCTPushy -- version %@ was purged and no native check is "
2396
+ @"configured; launching the packaged bundle", purgedVersion);
2397
+ return NO;
2398
+ }
2399
+ PushyWithStateLock(^{
2400
+ pushyPurgeRestoreWindowOpen = true;
2401
+ });
2402
+ pushyPurgeRestoreActive.store(true);
2403
+ NSTimeInterval deadline = PushyMonotonicNow() + kPushyPurgeRestoreBudget;
2404
+ dispatch_semaphore_t done = dispatch_semaphore_create(0);
2405
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
2406
+ [self startRoundWithDeadline:deadline];
2407
+ dispatch_semaphore_signal(done);
2408
+ });
2409
+ BOOL timedOut = dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW,
2410
+ (int64_t)(kPushyPurgeRestoreBudget * NSEC_PER_SEC))) != 0;
2411
+ // The round's deadline does not bound a slow response (the request
2412
+ // timeout is an idle timeout) nor a version already on disk, so the close
2413
+ // is what stops a late activation — not the deadline.
2414
+ PushyWithStateLock(^{
2415
+ pushyPurgeRestoreWindowOpen = false;
2416
+ });
2417
+ if (timedOut) {
2418
+ RCTLogWarn(@"RCTPushy -- purged version %@ restore timed out", purgedVersion);
2419
+ } else {
2420
+ NSDictionary *result = pushyHostRoundResult;
2421
+ RCTLogInfo(@"RCTPushy -- purged version %@ restore: %@ %@ %@", purgedVersion,
2422
+ result[@"status"], result[@"reason"], result[@"hash"]);
2423
+ }
2424
+ return YES;
2425
+ }
2426
+ #endif
2427
+
2287
2428
  + (NSDictionary *)checkAndUpdate {
2288
2429
  #if DEBUG
2289
2430
  return PushyHostResult(@"skipped", @"debug", nil, NO);
@@ -2430,7 +2571,8 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2430
2571
  responseText:nil
2431
2572
  request:nil
2432
2573
  config:nil
2433
- responseAt:0];
2574
+ responseAt:0
2575
+ activated:NULL];
2434
2576
  if (committed) {
2435
2577
  @synchronized (self) {
2436
2578
  pushyUnactivatedHash = nil;
@@ -2532,7 +2674,7 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2532
2674
  cInfo.Set("rnu", config.Get("rnu"));
2533
2675
  cInfo.Set("rn", config.Get("rn"));
2534
2676
  cInfo.Set("os", flowjson::Value::String(PushyToStdString([NSString
2535
- stringWithFormat:@"ios %@", [[UIDevice currentDevice] systemVersion]])));
2677
+ stringWithFormat:@"%@ %@", PushyOsName, [[UIDevice currentDevice] systemVersion]])));
2536
2678
  cInfo.Set("uuid", flowjson::Value::String(PushyToStdString(uuid)));
2537
2679
 
2538
2680
  flowjson::Value input = flowjson::Value::Object();
@@ -2580,7 +2722,8 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2580
2722
  responseText:responseText
2581
2723
  request:body
2582
2724
  config:configJson
2583
- responseAt:responseAtSeconds];
2725
+ responseAt:responseAtSeconds
2726
+ activated:NULL];
2584
2727
  pushyHostRoundResult = committed
2585
2728
  ? PushyHostResult(@"noUpdate", PushyFromStdString(decision.Get("reason").AsString()), nil, NO)
2586
2729
  : PushyHostResult(@"cancelled", @"reset", nil, NO);
@@ -2609,7 +2752,8 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2609
2752
  responseText:responseText
2610
2753
  request:body
2611
2754
  config:configJson
2612
- responseAt:responseAtSeconds];
2755
+ responseAt:responseAtSeconds
2756
+ activated:NULL];
2613
2757
  pushyHostRoundResult = committed
2614
2758
  ? PushyHostResult(@"failed", @"download_failed", nil, NO)
2615
2759
  : PushyHostResult(@"cancelled", @"reset", nil, NO);
@@ -2639,15 +2783,21 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2639
2783
  // remote override — the brick rescue) activate for the next launch;
2640
2784
  // otherwise activation stays with the JS side (§6/§10.1). Unless a crash
2641
2785
  // is being held: JS is dead, deferring to it would leave the fix on disk
2642
- // forever (§11.3).
2643
- BOOL activate = decision.Get("activate").Truthy() || pushyCrashRescueActive.load();
2786
+ // forever (§11.3). Or the launch is blocked on reinstalling a version
2787
+ // tvOS purged: JS has not started, and the whole point is to boot it —
2788
+ // commitRoundWithGeneration drops that one once the restore window closed.
2789
+ BOOL activate = decision.Get("activate").Truthy() || pushyCrashRescueActive.load()
2790
+ || pushyPurgeRestoreActive.load();
2791
+ BOOL activated = NO;
2644
2792
  BOOL committed = [self commitRoundWithGeneration:resetGeneration
2645
2793
  hashInfo:@{@"hash": hash, @"info": versionInfo}
2646
2794
  activate:activate ? hash : nil
2647
2795
  responseText:responseText
2648
2796
  request:body
2649
2797
  config:configJson
2650
- responseAt:responseAtSeconds];
2798
+ responseAt:responseAtSeconds
2799
+ activated:&activated];
2800
+ activate = activated;
2651
2801
  if (!committed) {
2652
2802
  RCTLogInfo(@"RCTPushy -- native check: reset during round, dropping result");
2653
2803
  } else if (activate) {
@@ -2680,7 +2830,8 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2680
2830
  responseText:(NSString *)responseText
2681
2831
  request:(NSString *)requestBody
2682
2832
  config:(NSString *)configJson
2683
- responseAt:(long long)responseAtSeconds {
2833
+ responseAt:(long long)responseAtSeconds
2834
+ activated:(BOOL *)activatedOut {
2684
2835
  // responseText is nil for the crash handler's activation-only commit
2685
2836
  // (activatePendingVersion): no round ran, so there is no cache to write.
2686
2837
  NSData *cacheData = nil;
@@ -2694,13 +2845,31 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2694
2845
  cacheData = [NSJSONSerialization dataWithJSONObject:cacheEntry options:0 error:nil];
2695
2846
  }
2696
2847
  __block BOOL committed = NO;
2848
+ __block BOOL activated = NO;
2697
2849
  PushyWithStateLock(^{
2698
2850
  if (pushyResetGeneration.load() != generation) {
2699
2851
  return;
2700
2852
  }
2853
+ NSString *activation = hashToActivate;
2854
+ NSDictionary *info = hashInfoEntry[@"info"];
2855
+ // The tvOS purged-version restore round (a round commit, not the
2856
+ // crash handler's activation-only one): it may take over this launch
2857
+ // only while +bundleURL still waits for it; after that the version is
2858
+ // left to JS unless a crash is being held (JS is dead then).
2859
+ if (responseText != nil && pushyPurgeRestoreActive.load()) {
2860
+ if (pushyPurgeRestoreWindowOpen) {
2861
+ if (activation != nil && info != nil) {
2862
+ NSMutableDictionary *marked = [info mutableCopy];
2863
+ marked[@"purgeRestore"] = @YES;
2864
+ info = marked;
2865
+ }
2866
+ } else if (!pushyCrashRescueActive.load()) {
2867
+ activation = nil;
2868
+ }
2869
+ }
2701
2870
  NSUserDefaults *defaults = PushyDefaults();
2702
2871
  if (hashInfoEntry != nil) {
2703
- NSData *infoData = [NSJSONSerialization dataWithJSONObject:hashInfoEntry[@"info"]
2872
+ NSData *infoData = [NSJSONSerialization dataWithJSONObject:info
2704
2873
  options:0
2705
2874
  error:nil];
2706
2875
  if (infoData != nil) {
@@ -2708,8 +2877,9 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2708
2877
  forKey:PushyHashInfoKey(hashInfoEntry[@"hash"])];
2709
2878
  }
2710
2879
  }
2711
- if (hashToActivate != nil) {
2712
- PushySwitchVersionLocked(hashToActivate);
2880
+ if (activation != nil) {
2881
+ PushySwitchVersionLocked(activation);
2882
+ activated = YES;
2713
2883
  }
2714
2884
  if (cacheData != nil) {
2715
2885
  [defaults setObject:[[NSString alloc] initWithData:cacheData encoding:NSUTF8StringEncoding]
@@ -2717,6 +2887,9 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
2717
2887
  }
2718
2888
  committed = YES;
2719
2889
  });
2890
+ if (activatedOut != NULL) {
2891
+ *activatedOut = activated;
2892
+ }
2720
2893
  return committed;
2721
2894
  }
2722
2895
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.57.1",
3
+ "version": "10.58.1",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "peerDependencies": {
74
74
  "react": ">=16.8.0",
75
- "react-native": ">=0.59.0"
75
+ "react-native": "*"
76
76
  },
77
77
  "homepage": "https://github.com/reactnativecn/react-native-update#readme",
78
78
  "dependencies": {
@@ -20,6 +20,15 @@ Pod::Spec.new do |s|
20
20
  # Silently ignore errors during check
21
21
  end
22
22
 
23
+ # expo-modules-core is a dependency of expo, not of the app: resolve it from
24
+ # expo's own location. A bare require from this podspec's directory misses
25
+ # it when npm nests it under expo/node_modules (seen with react-native-tvos),
26
+ # which left EXPO_SUPPORTS_BUNDLEURL unset; the createBridge fallback that
27
+ # selects no longer exists in ExpoModulesCore (SDK 57), so the build failed.
28
+ expo_modules_core_package_json_js =
29
+ "require.resolve('expo-modules-core/package.json', " \
30
+ "{ paths: [require('path').dirname(require.resolve('expo/package.json'))] })"
31
+
23
32
  # Determine final validity by checking Podfile presence AND Expo version
24
33
  valid_expo_project = false # Default
25
34
  if is_expo_in_podfile
@@ -55,7 +64,7 @@ Pod::Spec.new do |s|
55
64
  # --- Try to find and parse ExpoModulesCore.podspec only if it's an Expo project ---
56
65
  parsed_expo_ios_target = nil
57
66
  expo_modules_core_podspec_path = begin
58
- package_json_path = `node -p "require.resolve('expo-modules-core/package.json')"`.strip
67
+ package_json_path = `node -p "#{expo_modules_core_package_json_js}"`.strip
59
68
  File.join(File.dirname(package_json_path), 'ExpoModulesCore.podspec') if $?.success? && package_json_path && !package_json_path.empty?
60
69
  rescue
61
70
  nil
@@ -82,7 +91,10 @@ Pod::Spec.new do |s|
82
91
  end
83
92
  end
84
93
 
85
- s.platforms = { :ios => final_ios_deployment_target }
94
+ # tvOS (react-native-tvos) shares the iOS sources; the only platform
95
+ # differences live behind TARGET_OS_TV in RCTPushy.mm (download directory,
96
+ # purged-cache recovery).
97
+ s.platforms = { :ios => final_ios_deployment_target, :tvos => final_ios_deployment_target }
86
98
 
87
99
  s.name = package['name']
88
100
  s.version = package['version']
@@ -178,8 +190,7 @@ Pod::Spec.new do |s|
178
190
 
179
191
  # 1. Try executing node to get the version string
180
192
  expo_modules_core_version_str = begin
181
- # Use node to directly require expo-modules-core/package.json and get its version
182
- `node --print \"require('expo-modules-core/package.json').version\"` # Execute, keep raw output
193
+ `node --print "require(#{expo_modules_core_package_json_js}).version"` # Execute, keep raw output
183
194
  rescue
184
195
  # Node command failed (e.g., node not found, package not found). Return empty string.
185
196
  ''
package/src/client.ts CHANGED
@@ -742,8 +742,8 @@ export class Pushy {
742
742
  let timer: ReturnType<typeof setTimeout> | undefined;
743
743
  await Promise.race([
744
744
  this.loggerPromise.promise,
745
- new Promise((resolve) => {
746
- timer = setTimeout(resolve, 10 * 1000);
745
+ new Promise<void>((resolve) => {
746
+ timer = setTimeout(() => resolve(), 10 * 1000);
747
747
  }),
748
748
  ]);
749
749
  clearTimeout(timer);
@@ -977,8 +977,9 @@ export class Pushy {
977
977
  ) => {
978
978
  const resp = await fetchWithTimeout(
979
979
  this.getCheckUrl(endpoint),
980
- signal ? { ...fetchPayload, signal } : fetchPayload,
981
- DEFAULT_FETCH_TIMEOUT_MS
980
+ fetchPayload,
981
+ DEFAULT_FETCH_TIMEOUT_MS,
982
+ signal
982
983
  );
983
984
 
984
985
  if (!resp.ok) {
@@ -1532,7 +1533,7 @@ export class Pushy {
1532
1533
  (0.75 + Math.random() * 0.5)
1533
1534
  );
1534
1535
  log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
1535
- await new Promise((r) => setTimeout(r, backoffMs));
1536
+ await new Promise<void>((r) => setTimeout(() => r(), backoffMs));
1536
1537
  errorMessages.length = 0;
1537
1538
  errorMessages.push(...exhaustedStrategies.values());
1538
1539
  lastError = undefined;
package/src/core.ts CHANGED
@@ -3,6 +3,12 @@ import { UpdateError } from './error';
3
3
  import i18n from './i18n';
4
4
  import { emptyModule, error, log } from './utils';
5
5
 
6
+ // RN's JS global object. Declared locally because RN 0.87's own typings no
7
+ // longer define `global` (only @types/node did), and this source is
8
+ // type-checked in consumer projects; the module-scoped declaration also
9
+ // shadows @types/node's without conflicting.
10
+ declare const global: any;
11
+
6
12
  /* eslint-disable @react-native/no-deep-imports */
7
13
  const {
8
14
  version: v,
@@ -10,7 +16,7 @@ const {
10
16
  const RNVersion = `${v.major}.${v.minor}.${v.patch}`;
11
17
  const isTurboModuleEnabled =
12
18
  // https://github.com/facebook/react-native/pull/48362
13
- (global as any).__turboModuleProxy || (global as any).RN$Bridgeless;
19
+ global.__turboModuleProxy || global.RN$Bridgeless;
14
20
 
15
21
  const isWebPlatform = Platform.OS === 'web';
16
22
 
@@ -164,7 +170,9 @@ if (!uuid) {
164
170
  export const cInfo = {
165
171
  rnu: require('../package.json').version,
166
172
  rn: RNVersion,
167
- os: `${Platform.OS} ${Platform.Version}`,
173
+ // tvOS reports Platform.OS 'ios'; label it apart so the server can bucket
174
+ // tvOS devices separately. Must match PushyOsName in RCTPushy.mm.
175
+ os: `${Platform.OS === 'ios' && Platform.isTV ? 'tvos' : Platform.OS} ${Platform.Version}`,
168
176
  uuid,
169
177
  };
170
178
 
package/src/metadata.ts CHANGED
@@ -46,9 +46,12 @@ export interface UpdateMetadata {
46
46
  /**
47
47
  * How the running version got activated when not by the JS flow:
48
48
  * 'forceBoot' — the server's per-version override applied by the native
49
- * cold-start check; 'crashRescue' — activated by the crash-time rescue.
49
+ * cold-start check; 'crashRescue' — activated by the crash-time rescue;
50
+ * 'purgeRestore' — reinstalled at launch after tvOS purged the cache.
51
+ * Several can apply to one activation (a purge restore of a forceBoot
52
+ * version); this reports the highest-priority one, in the order listed.
50
53
  */
51
- rescueSource: 'forceBoot' | 'crashRescue' | null;
54
+ rescueSource: 'forceBoot' | 'crashRescue' | 'purgeRestore' | null;
52
55
  /** Stable per-install client id (gray-release bucketing key). */
53
56
  uuid: string;
54
57
  os: string;
@@ -74,7 +77,9 @@ export function getUpdateMetadata(): UpdateMetadata {
74
77
  ? 'forceBoot'
75
78
  : info.crashRescue
76
79
  ? 'crashRescue'
77
- : null,
80
+ : info.purgeRestore
81
+ ? 'purgeRestore'
82
+ : null,
78
83
  uuid: cInfo.uuid,
79
84
  os: cInfo.os,
80
85
  };
package/src/utils.ts CHANGED
@@ -132,7 +132,11 @@ export const computeProgress = (received: number, total: number): number =>
132
132
  export const fetchWithTimeout = (
133
133
  url: string,
134
134
  params: Parameters<typeof fetch>[1],
135
- timeoutMs = DEFAULT_FETCH_TIMEOUT_MS
135
+ timeoutMs = DEFAULT_FETCH_TIMEOUT_MS,
136
+ // Taken apart from params: merging it into the RequestInit only type-checks
137
+ // when the global AbortSignal and fetch's RequestInit come from the same
138
+ // declarations, which RN 0.87 globals mixed with DOM/Node/bun types break.
139
+ signal?: AbortSignal
136
140
  ): Promise<Response> => {
137
141
  // AbortController landed in the RN fetch polyfill around 0.60; we support
138
142
  // older peers, so fall back to a plain timer race when it is unavailable
@@ -161,7 +165,8 @@ export const fetchWithTimeout = (
161
165
  // The timeout controller replaces params.signal on the fetch call, so a
162
166
  // caller-provided signal (e.g. the hedged endpoint race cancelling losers)
163
167
  // must be chained onto it manually.
164
- const externalSignal = (params as any)?.signal as AbortSignal | undefined;
168
+ const externalSignal =
169
+ signal ?? ((params as any)?.signal as AbortSignal | undefined);
165
170
  const onExternalAbort = () => controller.abort();
166
171
  if (externalSignal?.aborted) {
167
172
  controller.abort();