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.
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/cpp/patch_core/state_core.cpp +9 -0
- package/cpp/patch_core/state_core.h +6 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +199 -26
- package/package.json +2 -2
- package/react-native-update.podspec +15 -4
- package/src/client.ts +6 -5
- package/src/core.ts +10 -2
- package/src/metadata.ts +8 -3
- package/src/utils.ts +7 -2
|
Binary file
|
|
Binary file
|
|
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
|
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -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
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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 ([
|
|
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:@"
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
2712
|
-
PushySwitchVersionLocked(
|
|
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.
|
|
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": "
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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 =
|
|
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();
|