react-native-update 10.59.1 → 10.60.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 (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +11 -12
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +19 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2628
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +74 -83
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
@@ -0,0 +1,314 @@
1
+ // Persisted update state: the defaults keys, the state lock, the State <->
2
+ // defaults mapping, the string/error/JSON helpers every other file uses, and
3
+ // the embedded bundle's hash.
4
+ #import "RCTPushyInternal.h"
5
+ #include "../../cpp/patch_core/digest.h"
6
+
7
+ #import <React/RCTConvert.h>
8
+ #import <os/lock.h>
9
+
10
+ PUSHY_HIDDEN NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
11
+ // Binary identity (SyncBinaryVersion input). Prefixed like every other key:
12
+ // the unprefixed names collided with generic host-app/SDK defaults, and a
13
+ // foreign value there made every launch look like a rebuilt binary and wipe
14
+ // the installed update.
15
+ static NSString *const paramPackageVersion = @"REACTNATIVECN_PUSHY_PACKAGEVERSION";
16
+ static NSString *const paramBuildTime = @"REACTNATIVECN_PUSHY_BUILDTIME";
17
+ // The unprefixed names earlier SDKs wrote. Read only as a fallback until the
18
+ // first state write migrates them (PushyStateFromDefaults /
19
+ // PushyApplyStateToDefaults).
20
+ static NSString *const legacyParamPackageVersion = @"packageVersion";
21
+ static NSString *const legacyParamBuildTime = @"buildTime";
22
+ static NSString *const paramLastVersion = @"lastVersion";
23
+ PUSHY_HIDDEN NSString *const paramCurrentVersion = @"currentVersion";
24
+ static NSString *const paramIsFirstTime = @"isFirstTime";
25
+ static NSString *const paramIsFirstLoadOk = @"isFirstLoadOK";
26
+ PUSHY_HIDDEN NSString *const keyUuid = @"REACTNATIVECN_PUSHY_UUID";
27
+ PUSHY_HIDDEN NSString *const keyHashInfo = @"REACTNATIVECN_PUSHY_HASH_";
28
+ PUSHY_HIDDEN NSString *const keyFirstLoadMarked = @"REACTNATIVECN_PUSHY_FIRSTLOADMARKED_KEY";
29
+ PUSHY_HIDDEN NSString *const keyRolledBackMarked = @"REACTNATIVECN_PUSHY_ROLLEDBACKMARKED_KEY";
30
+ // Encoded constants (RCTPushyRevealText) are decoded on first use, not by
31
+ // static initializers at image load.
32
+ PUSHY_HIDDEN NSString *KeyPackageUpdatedMarked(void) {
33
+ static NSString *value = RCTPushyRevealText("0832d5f29aa549710b0939daf88ca058790f3dded7e888b451640d2ec3f69099bb43711523cafa8ea7bb4372132a");
34
+ return value;
35
+ }
36
+ // bundleHash cache: "<cacheKey>|<sha256hex>" where cacheKey identifies the
37
+ // installed binary (packageVersion + embedded bundle size + mtime). Recomputed
38
+ // only when the key changes, i.e. once per install.
39
+ static NSString *const keyBundleHashCache = @"REACTNATIVECN_PUSHY_BUNDLEHASH_KEY";
40
+ // Raw response cache written by the native cold-start check for the JS side
41
+ // to reuse (§10.3), scoped to the request and config that produced it.
42
+ PUSHY_HIDDEN NSString *const keyNativeCheckCache = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_RESP_KEY";
43
+
44
+ // error def — messages are human-readable; the stable cross-platform codes
45
+ // live in cpp/patch_core/error_codes.h and travel in RCTPushyErrorCodeKey.
46
+ PUSHY_HIDDEN NSString *const ERROR_OPTIONS = @"options error";
47
+
48
+ PUSHY_HIDDEN std::atomic<bool> ignoreRollback{false};
49
+ // Bumped by resetToPackagedBundle only (config replacement has its own
50
+ // pushyNativeConfigGeneration). The cold-start check runs for minutes and
51
+ // may already hold a decision when the app resets to the packaged bundle; it
52
+ // samples this counter and abandons activation (and its response cache) when
53
+ // the value moved, so an in-flight rescue can never resurrect the version the
54
+ // app just reset away from.
55
+ PUSHY_HIDDEN std::atomic<uint64_t> pushyResetGeneration{0};
56
+
57
+ PUSHY_HIDDEN NSDictionary *PushyHostResult(NSString *status, NSString *reason,
58
+ NSString *hash, BOOL activated) {
59
+ return @{@"status": status, @"reason": reason ?: @"",
60
+ @"hash": hash ?: @"", @"activated": @(activated)};
61
+ }
62
+
63
+ PUSHY_HIDDEN NSTimeInterval PushyMonotonicNow(void) {
64
+ return [NSProcessInfo processInfo].systemUptime;
65
+ }
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, the file queue), 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
+ PUSHY_HIDDEN void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) {
75
+ #if DEBUG
76
+ // Not re-entrant: a nested acquisition deadlocks in release, so catch it
77
+ // where it is written.
78
+ os_unfair_lock_assert_not_owner(&pushyStateLock);
79
+ #endif
80
+ os_unfair_lock_lock(&pushyStateLock);
81
+ @try {
82
+ block();
83
+ } @finally {
84
+ os_unfair_lock_unlock(&pushyStateLock);
85
+ }
86
+ }
87
+
88
+ PUSHY_HIDDEN std::string PushyToStdString(NSString *value) {
89
+ if (value == nil) {
90
+ return std::string();
91
+ }
92
+ return std::string([value UTF8String]);
93
+ }
94
+
95
+ PUSHY_HIDDEN NSUserDefaults *PushyDefaults(void) {
96
+ return [NSUserDefaults standardUserDefaults];
97
+ }
98
+
99
+ PUSHY_HIDDEN NSString *PushyFromStdString(const std::string &value) {
100
+ if (value.empty()) {
101
+ return nil;
102
+ }
103
+ return [NSString stringWithUTF8String:value.c_str()];
104
+ }
105
+
106
+ static void PushySetNullableString(NSUserDefaults *defaults, NSString *key, NSString *value) {
107
+ if (value != nil) {
108
+ [defaults setObject:value forKey:key];
109
+ } else {
110
+ [defaults removeObjectForKey:key];
111
+ }
112
+ }
113
+
114
+ PUSHY_HIDDEN NSString *PushyHashInfoKey(NSString *hash) {
115
+ return [keyHashInfo stringByAppendingString:hash ?: @""];
116
+ }
117
+
118
+ PUSHY_HIDDEN NSString *PushyOptionString(NSDictionary *options, NSString *key) {
119
+ return [RCTConvert NSString:options[key]];
120
+ }
121
+
122
+ PUSHY_HIDDEN BOOL PushyStringIsBlank(NSString *value) {
123
+ if (value == nil || [value isKindOfClass:[NSNull class]]) {
124
+ return YES;
125
+ }
126
+ return [[value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0;
127
+ }
128
+
129
+ // Server-provided identifiers (hash/originHash) are used as child names under
130
+ // the download root; anything that could resolve outside of it (path
131
+ // separators, "..", ".") must be rejected before touching the filesystem.
132
+ PUSHY_HIDDEN BOOL PushyIsSafePathComponent(NSString *value) {
133
+ if (PushyStringIsBlank(value)) {
134
+ return NO;
135
+ }
136
+ if ([value isEqualToString:@"."] || [value isEqualToString:@".."]) {
137
+ return NO;
138
+ }
139
+ if ([value containsString:@"/"] || [value containsString:@"\\"]) {
140
+ return NO;
141
+ }
142
+ if ([value rangeOfString:@"\0"].location != NSNotFound) {
143
+ return NO;
144
+ }
145
+ return YES;
146
+ }
147
+
148
+ PUSHY_HIDDEN NSError *PushyErrorWithCode(const char *code, NSString *message) {
149
+ return [NSError errorWithDomain:RCTPushyErrorDomain
150
+ code:-1
151
+ userInfo:@{
152
+ NSLocalizedDescriptionKey: message ?: @"unknown error",
153
+ RCTPushyErrorCodeKey: @(code),
154
+ }];
155
+ }
156
+
157
+ PUSHY_HIDDEN void PushyRejectError(RCTPromiseRejectBlock reject, NSError *error) {
158
+ // Prefer the stable cross-platform code (error_codes.h); fall back to the
159
+ // numeric NSError code for system errors that were not classified.
160
+ NSString *code = error.userInfo[RCTPushyErrorCodeKey];
161
+ if (code == nil) {
162
+ code = [NSString stringWithFormat:@"%ld", (long)error.code];
163
+ }
164
+ reject(code, error.localizedDescription, error);
165
+ }
166
+
167
+ PUSHY_HIDDEN NSDictionary *PushyJsonDictionary(id json, NSError **error) {
168
+ NSData *data = [json isKindOfClass:[NSString class]]
169
+ ? [(NSString *)json dataUsingEncoding:NSUTF8StringEncoding]
170
+ : ([json isKindOfClass:[NSData class]] ? (NSData *)json : nil);
171
+ id object = data == nil ? nil
172
+ : [NSJSONSerialization JSONObjectWithData:data options:0 error:error];
173
+ return [object isKindOfClass:[NSDictionary class]] ? (NSDictionary *)object : nil;
174
+ }
175
+
176
+ PUSHY_HIDDEN NSString *PushyJsonString(id object) {
177
+ NSData *data = object == nil ? nil
178
+ : [NSJSONSerialization dataWithJSONObject:object options:0 error:nil];
179
+ return data == nil ? nil : [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
180
+ }
181
+
182
+ // Binary identity under the prefixed key, or — until the first state write
183
+ // migrates it — the value an earlier SDK left under the unprefixed one, so an
184
+ // installed update survives the SDK upgrade instead of reading as a rebuilt
185
+ // binary.
186
+ static NSString *PushyBinaryIdentityValue(NSUserDefaults *defaults, NSString *key, NSString *legacyKey) {
187
+ return [defaults stringForKey:key] ?: [defaults stringForKey:legacyKey];
188
+ }
189
+
190
+ PUSHY_HIDDEN pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults) {
191
+ pushy::state::State state;
192
+ state.package_version = PushyToStdString(
193
+ PushyBinaryIdentityValue(defaults, paramPackageVersion, legacyParamPackageVersion));
194
+ state.build_time = PushyToStdString(
195
+ PushyBinaryIdentityValue(defaults, paramBuildTime, legacyParamBuildTime));
196
+ NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
197
+ if (pushyInfo != nil) {
198
+ state.current_version = PushyToStdString(pushyInfo[paramCurrentVersion]);
199
+ state.last_version = PushyToStdString(pushyInfo[paramLastVersion]);
200
+ state.first_time = [pushyInfo[paramIsFirstTime] boolValue];
201
+ id firstLoadOk = pushyInfo[paramIsFirstLoadOk];
202
+ state.first_time_ok = firstLoadOk == nil ? true : [firstLoadOk boolValue];
203
+ }
204
+ state.rolled_back_version = PushyToStdString([defaults stringForKey:keyRolledBackMarked]);
205
+ return state;
206
+ }
207
+
208
+ PUSHY_HIDDEN void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::state::State &state) {
209
+ PushySetNullableString(defaults, paramPackageVersion, PushyFromStdString(state.package_version));
210
+ PushySetNullableString(defaults, paramBuildTime, PushyFromStdString(state.build_time));
211
+ // One-time migration: once the prefixed keys hold the identity, the
212
+ // legacy ones are retired so a host-app write there is never read again.
213
+ [defaults removeObjectForKey:legacyParamPackageVersion];
214
+ [defaults removeObjectForKey:legacyParamBuildTime];
215
+
216
+ BOOL hasPushyInfo = !state.current_version.empty() || !state.last_version.empty() || state.first_time || !state.first_time_ok;
217
+ if (hasPushyInfo) {
218
+ NSMutableDictionary *newInfo = [[NSMutableDictionary alloc] init];
219
+ if (!state.current_version.empty()) {
220
+ newInfo[paramCurrentVersion] = PushyFromStdString(state.current_version);
221
+ }
222
+ if (!state.last_version.empty()) {
223
+ newInfo[paramLastVersion] = PushyFromStdString(state.last_version);
224
+ }
225
+ newInfo[paramIsFirstTime] = @(state.first_time);
226
+ newInfo[paramIsFirstLoadOk] = @(state.first_time_ok);
227
+ [defaults setObject:newInfo forKey:keyPushyInfo];
228
+ } else {
229
+ [defaults removeObjectForKey:keyPushyInfo];
230
+ }
231
+
232
+ PushySetNullableString(
233
+ defaults,
234
+ keyRolledBackMarked,
235
+ PushyFromStdString(state.rolled_back_version));
236
+ }
237
+
238
+ // Version switch without acquiring the state lock: the caller must already
239
+ // hold it. Lets the cold-start check commit its whole result (version info,
240
+ // switch, response cache) inside one lock acquisition, so resetToPackagedBundle
241
+ // can never interleave between the generation check and the writes.
242
+ PUSHY_HIDDEN void PushySwitchVersionLocked(NSString *hash) {
243
+ NSUserDefaults *defaults = PushyDefaults();
244
+ pushy::state::State next = pushy::state::SwitchVersion(
245
+ PushyStateFromDefaults(defaults),
246
+ PushyToStdString(hash)
247
+ );
248
+ PushyApplyStateToDefaults(defaults, next);
249
+ // Re-enable first-load consumption and rollback checks for the newly selected bundle.
250
+ ignoreRollback = false;
251
+ }
252
+
253
+ static NSString *PushyCachedBundleHash(NSUserDefaults *defaults, NSString *cachedPrefix) {
254
+ NSString *cached = [defaults stringForKey:keyBundleHashCache];
255
+ if ([cached hasPrefix:cachedPrefix]) {
256
+ return [cached substringFromIndex:cachedPrefix.length];
257
+ }
258
+ return nil;
259
+ }
260
+
261
+ // Shared by the getBundleHash RCT method and the native cold-start check.
262
+ // Returns @"" when unknown. With computeIfMissing the sha256 of the embedded
263
+ // bundle is taken on a cache miss (once per install; blocking — call off the
264
+ // main thread). Without it only the cache is consulted: a crash rescue on
265
+ // the main thread has a 3.5s budget in total, and hashing a large bundle on
266
+ // a fresh install would spend all of it before the first request.
267
+ PUSHY_HIDDEN NSString *PushyBundleHashSync(BOOL computeIfMissing) {
268
+ NSString *path = [[RCTPushy binaryBundleURL] path];
269
+ if (path == nil) {
270
+ return @"";
271
+ }
272
+ NSDictionary *attributes =
273
+ [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
274
+ if (attributes == nil) {
275
+ return @"";
276
+ }
277
+ NSString *cacheKey = [NSString stringWithFormat:@"%@|%llu|%.0f",
278
+ [RCTPushy packageVersion],
279
+ attributes.fileSize,
280
+ [attributes.fileModificationDate timeIntervalSince1970]];
281
+
282
+ NSUserDefaults *defaults = PushyDefaults();
283
+ NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
284
+ NSString *cached = PushyCachedBundleHash(defaults, cachedPrefix);
285
+ if (cached != nil) {
286
+ return cached;
287
+ }
288
+ if (!computeIfMissing) {
289
+ return @"";
290
+ }
291
+
292
+ // Single-flight for the digest: the delayed cold-start round (utility
293
+ // queue) and JS getBundleHash (file queue) can both miss the cache right
294
+ // after an install; the second caller waits for the first and then reads
295
+ // its cache entry instead of hashing the same multi-MB file twice.
296
+ static NSLock *lock = [NSLock new];
297
+ [lock lock];
298
+ @try {
299
+ // The previous holder may have filled the cache while we waited.
300
+ cached = PushyCachedBundleHash(defaults, cachedPrefix);
301
+ if (cached != nil) {
302
+ return cached;
303
+ }
304
+ NSString *hash = PushyFromStdString(
305
+ pushy::digest::Sha256File(PushyToStdString(path))) ?: @"";
306
+ if (hash.length > 0) {
307
+ [defaults setObject:[cachedPrefix stringByAppendingString:hash]
308
+ forKey:keyBundleHashCache];
309
+ }
310
+ return hash;
311
+ } @finally {
312
+ [lock unlock];
313
+ }
314
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.59.1",
4
- "description": "react-native hot update",
3
+ "version": "10.60.0",
4
+ "description": "Hot update (OTA) for React Native with Hermes-optimized diff patches, crash rollback and staged rollout. A CodePush alternative for iOS, Android and HarmonyOS.",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
7
7
  "files": [
@@ -15,13 +15,11 @@
15
15
  "android/build.gradle",
16
16
  "android/proguard.pro",
17
17
  "ios",
18
- "cpp/patch_core",
19
- "cpp/update_flow_core",
18
+ "cpp",
20
19
  "!cpp/**/tests",
21
20
  "harmony/pushy.har",
22
21
  "harmony/hvigor-plugin.ts",
23
22
  "harmony/pushy/src",
24
- "!harmony/pushy/src/test",
25
23
  "react-native-update.podspec",
26
24
  "react-native.config.js",
27
25
  "expo-module.config.json",
@@ -30,10 +28,8 @@
30
28
  "LICENSE"
31
29
  ],
32
30
  "scripts": {
33
- "prepublishOnly": "NODE_ENV=production bun scripts/prepublish.ts",
34
- "publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
35
31
  "prepack": "bun submodule && bun lint",
36
- "lint": "biome check src && tsc --noEmit && node scripts/check-harmony-types.js && node scripts/check-native-spec-parity.js",
32
+ "lint": "biome check src && tsc --noEmit && node scripts/check-harmony-types.js && node scripts/check-native-spec-parity.js && node scripts/check-constant-parity.js",
37
33
  "lint:harmony": "node scripts/check-harmony-types.js",
38
34
  "submodule": "git submodule update --init --recursive",
39
35
  "test": "bun test src/__tests__",
@@ -43,14 +39,11 @@
43
39
  "build:harmony-har": "node scripts/build-harmony-har.js",
44
40
  "build:harmony-e2e": "bash Example/e2etest/scripts/build-harmony-e2e.sh",
45
41
  "build:so": "bun submodule && bash scripts/build-android-so.sh",
46
- "build:ios-debug": "cd Example/e2etest && bun && bunx detox build --configuration ios.sim.debug",
47
42
  "build:ios-release": "cd Example/e2etest && bun && bunx detox build --configuration ios.sim.release",
48
- "test:ios-debug": "cd Example/e2etest && E2E_PLATFORM=ios bunx detox test --configuration ios.sim.debug",
49
43
  "test:ios-release": "cd Example/e2etest && E2E_PLATFORM=ios bunx detox test --configuration ios.sim.release",
50
44
  "build:android-debug": "cd Example/e2etest && bun && bunx detox build --configuration android.emu.debug",
51
45
  "build:android-release": "cd Example/e2etest && bun && bunx detox build --configuration android.emu.release",
52
46
  "test:android-release": "cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all",
53
- "test:android-debug": "cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.debug --headless --record-logs all",
54
47
  "smoke:android-debug": "cd Example/e2etest && bunx detox test --configuration android.emu.debug --config e2e/smoke/jest.config.js",
55
48
  "e2e:ios": "bun build:ios-release && bun test:ios-release",
56
49
  "e2e:android": "bun build:android-release && bun test:android-release"
@@ -63,7 +56,21 @@
63
56
  "react-native",
64
57
  "ios",
65
58
  "android",
66
- "update"
59
+ "update",
60
+ "hot-update",
61
+ "hotfix",
62
+ "ota",
63
+ "over-the-air",
64
+ "codepush",
65
+ "code-push",
66
+ "codepush-alternative",
67
+ "hermes",
68
+ "harmonyos",
69
+ "expo",
70
+ "incremental-update",
71
+ "bsdiff",
72
+ "pushy",
73
+ "cresc"
67
74
  ],
68
75
  "author": "reactnativecn",
69
76
  "license": "MIT",
@@ -75,26 +82,21 @@
75
82
  "react-native": "*"
76
83
  },
77
84
  "homepage": "https://github.com/reactnativecn/react-native-update#readme",
78
- "dependencies": {
79
- "nanoid": "^3.3.3"
80
- },
81
85
  "codegenConfig": {
82
86
  "name": "RCTPushySpec",
83
87
  "type": "modules",
84
88
  "jsSrcsDir": "src"
85
89
  },
86
90
  "devDependencies": {
87
- "@babel/core": "^7.25.8",
88
91
  "@biomejs/biome": "^2.5.5",
89
- "@react-native/babel-preset": "^0.73.21",
90
- "@react-native/typescript-config": "0.84.1",
92
+ "@react-native/typescript-config": "0.85.2",
91
93
  "@types/bun": "^1.3.11",
92
94
  "@types/node": "^25.5.0",
93
95
  "@types/react": "^19.2.14",
94
- "@types/react-test-renderer": "18.0.7",
95
- "react": "18.2.0",
96
- "react-native": "0.73",
97
- "react-test-renderer": "18.2.0",
96
+ "@types/react-test-renderer": "19.1.0",
97
+ "react": "19.2.3",
98
+ "react-native": "0.85.2",
99
+ "react-test-renderer": "19.2.3",
98
100
  "typescript": "^7.0.2"
99
101
  }
100
- }
102
+ }
@@ -1,8 +1,6 @@
1
1
  require 'json'
2
2
  require 'rubygems' # Required for version comparison
3
3
 
4
-
5
- new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
6
4
  package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
7
5
  podspec_dir = File.dirname(__FILE__)
8
6
 
@@ -20,37 +18,40 @@ Pod::Spec.new do |s|
20
18
  # Silently ignore errors during check
21
19
  end
22
20
 
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
-
32
- # Determine final validity by checking Podfile presence AND Expo version
33
- valid_expo_project = false # Default
21
+ # One node call answers every Expo question: expo's version, and where
22
+ # expo-modules-core lives and its version. expo-modules-core is a dependency
23
+ # of expo, not of the app: resolve it from expo's own location. A bare
24
+ # require from this podspec's directory misses it when npm nests it under
25
+ # expo/node_modules (seen with react-native-tvos), which left
26
+ # EXPO_SUPPORTS_BUNDLEURL unset; the createBridge fallback that selects no
27
+ # longer exists in ExpoModulesCore (SDK 57), so the build failed.
28
+ expo_info = {}
34
29
  if is_expo_in_podfile
35
- # Only check expo version if use_expo_modules! is present
36
- is_version_sufficient = false
30
+ expo_info_js = <<~JS
31
+ const path = require('path');
32
+ const info = {};
33
+ try {
34
+ const expoJson = require.resolve('expo/package.json');
35
+ info.expoVersion = require(expoJson).version;
36
+ const coreJson = require.resolve('expo-modules-core/package.json',
37
+ { paths: [path.dirname(expoJson)] });
38
+ info.coreDir = path.dirname(coreJson);
39
+ info.coreVersion = require(coreJson).version;
40
+ } catch (e) {}
41
+ console.log(JSON.stringify(info));
42
+ JS
37
43
  begin
38
- expo_version_str = `node --print \"require('expo/package.json').version\"`.strip
39
- if expo_version_str && !expo_version_str.empty?
40
- match = expo_version_str.match(/^\d+/)
41
- if match
42
- major_version = match[0].to_i
43
- is_version_sufficient = major_version >= 50
44
- end
45
- end
46
- rescue
47
- # Node command failed, version remains insufficient
44
+ output = IO.popen(['node', '-e', expo_info_js], &:read)
45
+ expo_info = JSON.parse(output) if $?.success?
46
+ rescue StandardError
47
+ # node missing or unparseable output: not a supported Expo project.
48
48
  end
49
-
50
- # Final check
51
- valid_expo_project = is_version_sufficient
52
49
  end
53
50
 
51
+ # use_expo_modules! in the Podfile and Expo SDK >= 50.
52
+ valid_expo_project =
53
+ is_expo_in_podfile && expo_info['expoVersion'].to_s[/^\d+/].to_i >= 50
54
+
54
55
  # Set platform based on whether it's a valid Expo project and if we can parse its target
55
56
  # React Native's own minimum (defined by react_native_pods.rb, which the
56
57
  # Podfile loads before evaluating podspecs). The pod platform version also
@@ -59,16 +60,23 @@ Pod::Spec.new do |s|
59
60
  # anything below 15.0 there, so a hard-coded 11.0 fails the build.
60
61
  final_ios_deployment_target =
61
62
  defined?(min_ios_version_supported) ? min_ios_version_supported : '11.0'
63
+ # tvOS has its own version line: react-native-tvos reports it through
64
+ # min_supported_versions[:tvos] (stock React Native has no :tvos entry).
65
+ # 15.1 is react-native-tvos's own floor since 0.76, and clears the same
66
+ # Xcode 27 resource-bundle minimum as iOS.
67
+ final_tvos_deployment_target = begin
68
+ defined?(min_supported_versions) ? min_supported_versions[:tvos] : nil
69
+ rescue StandardError
70
+ nil
71
+ end
72
+ final_tvos_deployment_target ||= '15.1'
62
73
 
63
74
  if valid_expo_project
64
75
  # --- Try to find and parse ExpoModulesCore.podspec only if it's an Expo project ---
65
76
  parsed_expo_ios_target = nil
66
- expo_modules_core_podspec_path = begin
67
- package_json_path = `node -p "#{expo_modules_core_package_json_js}"`.strip
68
- File.join(File.dirname(package_json_path), 'ExpoModulesCore.podspec') if $?.success? && package_json_path && !package_json_path.empty?
69
- rescue
70
- nil
71
- end
77
+ parsed_expo_tvos_target = nil
78
+ expo_modules_core_podspec_path =
79
+ expo_info['coreDir'] && File.join(expo_info['coreDir'], 'ExpoModulesCore.podspec')
72
80
 
73
81
  if expo_modules_core_podspec_path && File.exist?(expo_modules_core_podspec_path)
74
82
  begin
@@ -76,11 +84,8 @@ Pod::Spec.new do |s|
76
84
  match = content.match(/s\.platforms\s*=\s*\{[\s\S]*?:ios\s*=>\s*'([^\']+)'/) # Match within s.platforms hash
77
85
  if match && match[1]
78
86
  parsed_expo_ios_target = match[1]
79
- else
80
- match = content.match(/s\.platform\s*=\s*:ios,\s*'([^\']+)'/) # Fallback to s.platform = :ios, 'version'
81
- if match && match[1]
82
- parsed_expo_ios_target = match[1]
83
- end
87
+ tvos_match = content.match(/s\.platforms\s*=\s*\{[\s\S]*?:tvos\s*=>\s*'([^\']+)'/)
88
+ parsed_expo_tvos_target = tvos_match[1] if tvos_match
84
89
  end
85
90
  rescue => e
86
91
  # Pod::UI.warn "Failed to read or parse ExpoModulesCore.podspec content: #{e.message}"
@@ -89,12 +94,15 @@ Pod::Spec.new do |s|
89
94
  if parsed_expo_ios_target
90
95
  final_ios_deployment_target = parsed_expo_ios_target
91
96
  end
97
+ if parsed_expo_tvos_target
98
+ final_tvos_deployment_target = parsed_expo_tvos_target
99
+ end
92
100
  end
93
101
 
94
102
  # tvOS (react-native-tvos) shares the iOS sources; the only platform
95
- # differences live behind TARGET_OS_TV in RCTPushy.mm (download directory,
103
+ # differences live behind TARGET_OS_TV in ios/RCTPushy (download directory,
96
104
  # purged-cache recovery).
97
- s.platforms = { :ios => final_ios_deployment_target, :tvos => final_ios_deployment_target }
105
+ s.platforms = { :ios => final_ios_deployment_target, :tvos => final_tvos_deployment_target }
98
106
 
99
107
  s.name = package['name']
100
108
  s.version = package['version']
@@ -109,6 +117,9 @@ Pod::Spec.new do |s|
109
117
  s.source = { :git => 'https://github.com/reactnativecn/react-native-update.git', :tag => "#{s.version}" }
110
118
 
111
119
  s.libraries = 'bz2', 'z'
120
+ # tvOS checks for a network path before blocking a launch on the
121
+ # purged-version restore (nw_path_monitor).
122
+ s.tvos.frameworks = 'Network'
112
123
  # No search path into the pod source: every include under ios/ resolves
113
124
  # relative to the including file (the cpp/ core via ../../cpp/...), and the
114
125
  # former absolute "#{podspec_dir}/ios" entry made the generated xcconfig
@@ -116,9 +127,9 @@ Pod::Spec.new do |s|
116
127
  # DEFINES_MODULE writes the umbrella under Headers/Public/react_native_update/
117
128
  # (underscores). CocoaPods copies public headers to react-native-update/
118
129
  # (hyphens). Include the latter so the umbrella can #import "ImportReact.h".
119
- s.pod_target_xcconfig = {
120
- 'USER_HEADER_SEARCH_PATHS' => "\"$(PODS_ROOT)/Headers/Public/react-native-update\" \"$(PODS_ROOT)/Headers/Public/SSZipArchive\" \"$(PODS_ROOT)/Headers/Public/React-Codegen/RCTPushySpec\"",
121
- "DEFINES_MODULE" => "YES"
130
+ s.pod_target_xcconfig = {
131
+ 'USER_HEADER_SEARCH_PATHS' => "\"$(PODS_ROOT)/Headers/Public/react-native-update\"",
132
+ "DEFINES_MODULE" => "YES"
122
133
  }
123
134
  # buildTime for binary-rebuild detection (SyncBinaryVersion) and the check
124
135
  # request. The stamp is refreshed on every build and ships as a resource.
@@ -147,9 +158,8 @@ Pod::Spec.new do |s|
147
158
  # Privacy manifest (required-reason APIs: disk space, file timestamps,
148
159
  # system boot time, user defaults) delivered as its own resource bundle so
149
160
  # static and dynamic integrations both carry it.
150
- s.resource_bundles = { 'react-native-update_privacy' => ['ios/PrivacyInfo.xcprivacy'] }
161
+ s.resource_bundles = { 'PushyPrivacy' => ['ios/PrivacyInfo.xcprivacy'] }
151
162
 
152
- s.dependency 'React'
153
163
  s.dependency "React-Core"
154
164
  # The unzip guard relies on the 2.x delegate contract
155
165
  # (zipArchiveShouldUnzipFileAtIndex:... cancels, _sanitizedPath); a future
@@ -171,15 +181,12 @@ Pod::Spec.new do |s|
171
181
  # Objective-C classes in the RCTPushy subspec keep default visibility, which
172
182
  # the host app and the Expo module need to link against RCTPushy.
173
183
  s.subspec 'Core' do |ss|
174
- ss.source_files = ['cpp/update_flow_core/flow_json.{h,cpp}',
175
- 'cpp/update_flow_core/update_flow_core.{h,cpp}',
176
- 'cpp/patch_core/archive_patch_core.{h,cpp}',
177
- 'cpp/patch_core/digest.{h,cpp}',
178
- 'cpp/patch_core/hbc_transform.{h,cpp}',
179
- 'cpp/patch_core/hbc_transform_wire.{h,cpp}',
180
- 'cpp/patch_core/patch_core.{h,cpp}',
181
- 'cpp/patch_core/state_core.{h,cpp}',
182
- 'android/jni/hpatch.{h,c}',
184
+ # The cores are picked up by directory (non-recursive: tests/ stays out,
185
+ # and the Android JNI glue lives in cpp/android_jni), so a new
186
+ # platform-neutral source needs no edit here.
187
+ ss.source_files = ['cpp/patch_core/*.{h,cpp}',
188
+ 'cpp/update_flow_core/*.{h,cpp}',
189
+ 'cpp/hpatch/*.{h,c}',
183
190
  'android/jni/HDiffPatch/libHDiffPatch/HPatch/*.{h,c}',
184
191
  'android/jni/HDiffPatch/file_for_patch.{h,c}',
185
192
  'android/jni/lzma/C/LzmaDec.{h,c}',
@@ -193,23 +200,21 @@ Pod::Spec.new do |s|
193
200
  s.subspec 'RCTPushy' do |ss|
194
201
  ss.dependency 'react-native-update/Core'
195
202
  ss.source_files = ['ios/RCTPushy/*.{h,m,mm}']
196
- ss.public_header_files = ['ios/RCTPushy/*.h']
203
+ ss.public_header_files = ['ios/RCTPushy/RCTPushy.h',
204
+ 'ios/RCTPushy/RCTPushyConfiguration.h',
205
+ 'ios/RCTPushy/RCTPushyDownloader.h']
206
+ # Shared by the pod's own sources only (RCTPushyInternal.h pulls in C++
207
+ # headers), so they must stay out of the module umbrella.
208
+ ss.private_header_files = ['ios/RCTPushy/RCTPushyCommon.h',
209
+ 'ios/RCTPushy/RCTPushyInternal.h']
197
210
  end
198
211
 
199
212
  # Conditionally add Expo subspec and check ExpoModulesCore version
200
213
  if valid_expo_project
201
214
  supports_bundle_url_final = false # Default
202
215
 
203
- # 1. Try executing node to get the version string
204
- expo_modules_core_version_str = begin
205
- `node --print "require(#{expo_modules_core_package_json_js}).version"` # Execute, keep raw output
206
- rescue
207
- # Node command failed (e.g., node not found, package not found). Return empty string.
208
- ''
209
- end
210
-
211
- # 2. Process the obtained version string (if not empty)
212
- if expo_modules_core_version_str && !expo_modules_core_version_str.empty?
216
+ expo_modules_core_version_str = expo_info['coreVersion'].to_s
217
+ if !expo_modules_core_version_str.empty?
213
218
  begin
214
219
  # Compare versions using Gem::Version (handles trailing newline)
215
220
  installed_version = Gem::Version.new(expo_modules_core_version_str)
@@ -228,23 +233,9 @@ Pod::Spec.new do |s|
228
233
  end
229
234
  end
230
235
 
236
+ # React Native >= 0.71 (react_native_pods.rb) wires codegen, the new
237
+ # architecture and its dependencies.
231
238
  if defined?(install_modules_dependencies()) != nil
232
- install_modules_dependencies(s);
233
- else
234
- if new_arch_enabled
235
- folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
236
-
237
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
238
-
239
- s.pod_target_xcconfig = {
240
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
241
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
242
- }.merge(s.pod_target_xcconfig)
243
- s.dependency "React-Codegen"
244
- s.dependency "RCT-Folly"
245
- s.dependency "RCTRequired"
246
- s.dependency "RCTTypeSafety"
247
- s.dependency "ReactCommon/turbomodule/core"
248
- end
239
+ install_modules_dependencies(s)
249
240
  end
250
241
  end