react-native-update 10.40.0-beta.2 → 10.40.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.
Files changed (34) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/README-CN.md +0 -12
  3. package/README.md +1 -30
  4. package/android/jni/Application.mk +1 -0
  5. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  6. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  7. package/android/lib/x86/librnupdate.so +0 -0
  8. package/android/lib/x86_64/librnupdate.so +0 -0
  9. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +4 -29
  10. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +0 -30
  11. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +28 -1
  12. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +0 -6
  13. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +0 -4
  14. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +0 -4
  15. package/android/src/main/java/expo/modules/pushy/ExpoPushyPackage.java +1 -4
  16. package/cpp/patch_core/patch_core.cpp +14 -9
  17. package/cpp/patch_core/tests/patch_core_test.cpp +72 -0
  18. package/harmony/pushy/src/main/ets/DownloadTask.ts +49 -45
  19. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +27 -27
  20. package/harmony/pushy/src/test/DownloadTaskParams.test.ets +44 -0
  21. package/harmony/pushy/src/test/EventHub.test.ets +51 -0
  22. package/harmony/pushy/src/test/List.test.ets +11 -0
  23. package/harmony/pushy/src/test/ManifestParsing.test.ets +107 -0
  24. package/harmony/pushy/src/test/Validation.test.ets +94 -0
  25. package/harmony/pushy.har +0 -0
  26. package/package/harmony/pushy.har +0 -0
  27. package/package.json +1 -1
  28. package/react-native-update-10.40.0.tgz +0 -0
  29. package/src/__tests__/setup.ts +4 -0
  30. package/src/client.ts +8 -9
  31. package/src/isInRollout.ts +1 -1
  32. package/src/permissions.ts +2 -1
  33. package/src/utils.ts +9 -10
  34. package/react-native-update-10.40.0-beta.1.tgz +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(./scripts/test-patch-core.sh:*)",
5
+ "Bash(bun test:*)"
6
+ ]
7
+ }
8
+ }
package/README-CN.md CHANGED
@@ -53,18 +53,6 @@
53
53
  | **更新成功率** | ⭐⭐⭐⭐⭐ 极高(国内 CDN 优势) | ⭐⭐⭐ 中等 | ❌ **已停运** |
54
54
 
55
55
 
56
-
57
- ### 本地开发
58
-
59
- ```
60
- $ git clone git@github.com:reactnativecn/react-native-update.git
61
- $ cd react-native-pushy/Example/testHotUpdate
62
- $ yarn
63
- $ yarn start
64
- ```
65
-
66
- 本地库文件使用 yarn link 链接,因此可直接在源文件中修改,在 testHotUpdate 项目中调试。
67
-
68
56
  ### 关于我们
69
57
 
70
58
  本组件由[React Native 中文网](https://reactnative.cn/)独家发布,如有定制需求可以[联系我们](https://reactnative.cn/about.html#content)。
package/README.md CHANGED
@@ -29,38 +29,9 @@ See the docs:
29
29
 
30
30
  | Category | react-native-update | expo-update | react-native-code-push |
31
31
  |---------|---------------------|-------------|------------------------|
32
- | **Price / Cost** | Free tier with multiple paid plans (starting at about CNY 66/month), bandwidth included | Free tier with multiple paid plans (starting at about CNY 136/month), extra bandwidth charges apply | ❌ **Discontinued** (Microsoft App Center shut down on March 31, 2025) |
32
+ | **Price / Cost** | Free tier with multiple paid plans, bandwidth included | Free tier with multiple paid plans, extra bandwidth charges apply | ❌ **Discontinued** (Microsoft App Center shut down on March 31, 2025) |
33
33
  | **Package Size** | ⭐⭐⭐⭐⭐ Tens to hundreds of KB (incremental) | ⭐⭐⭐ Full bundle updates (usually tens of MB) | ❌ **Discontinued** |
34
- | **iOS Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
35
- | **Android Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
36
- | **HarmonyOS Support** | ✅ Supported | ❌ Not supported | ❌ **Discontinued** |
37
- | **Expo Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
38
- | **RN Version Support** | ⭐⭐⭐⭐⭐ Fast support for latest stable RN versions | ⭐⭐⭐⭐ Follows Expo SDK cadence | ❌ **Discontinued** |
39
- | **New Architecture** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
40
- | **Hermes Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
41
- | **Crash Rollback** | ✅ Automatic rollback | ✅ Supported | ❌ **Discontinued** |
42
- | **Management UI** | ✅ CLI + Web dashboard | ✅ Expo Dashboard | ❌ **Discontinued** |
43
- | **CI/CD Integration** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
44
- | **API Extensibility** | ✅ Meta info + Open API | ⚠️ Limited | ❌ **Discontinued** |
45
- | **Chinese Docs / Support** | ⭐⭐⭐⭐⭐ Complete Chinese docs and community support | ⭐⭐ Mostly English | ❌ **Discontinued** |
46
34
  | **Technical Support** | ✅ Paid dedicated support | ⚠️ Community support | ❌ **Discontinued** |
47
35
  | **Server Deployment** | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ **Discontinued** |
48
- | **Update Strategy** | Flexible configuration (silent / prompted / immediate / delayed) | More fixed workflow | ❌ **Discontinued** |
49
36
  | **Bandwidth Usage** | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ **Discontinued** |
50
37
 
51
- ## Local Development
52
-
53
- ```bash
54
- git clone git@github.com:reactnativecn/react-native-update.git
55
- cd react-native-pushy/Example/testHotUpdate
56
- yarn
57
- yarn start
58
- ```
59
-
60
- The local library is linked with `yarn link`, so you can modify the source files directly and debug with the `testHotUpdate` example project.
61
-
62
- ## About
63
-
64
- This package is published by [React Native Chinese](https://reactnative.cn/). For custom integration or service inquiries, see [Contact Us](https://reactnative.cn/about.html#content).
65
-
66
- If you find any issues, please open a thread in [Issues](https://github.com/reactnativecn/react-native-update/issues).
@@ -5,6 +5,7 @@ APP_CFLAGS += -ffunction-sections -fdata-sections
5
5
  APP_CFLAGS += -Oz -fno-unwind-tables -fno-asynchronous-unwind-tables
6
6
  APP_CPPFLAGS += -std=c++17 -Oz -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
7
7
  APP_LDFLAGS += -Wl,--gc-sections -Wl,--exclude-libs,ALL
8
+ APP_LDFLAGS += -Wl,--icf=all
8
9
  APP_BUILD_SCRIPT := Android.mk
9
10
  APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
10
11
  APP_STL := c++_static
Binary file
Binary file
@@ -38,10 +38,6 @@ final class BundledResourceCopier {
38
38
  }
39
39
 
40
40
  void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOException {
41
- if (UpdateContext.DEBUG) {
42
- Log.d(UpdateContext.TAG, "copyFromResource called, resToCopy size: " + resToCopy.size());
43
- }
44
-
45
41
  ArrayList<String> apkPaths = collectApkPaths();
46
42
  HashMap<String, ZipEntry> availableEntries = new HashMap<String, ZipEntry>();
47
43
  HashMap<String, SafeZipFile> zipFileMap = new HashMap<String, SafeZipFile>();
@@ -73,10 +69,6 @@ final class BundledResourceCopier {
73
69
  new HashMap<String, ArrayList<File>>(resToCopy);
74
70
 
75
71
  for (String fromPath : new ArrayList<String>(remainingFiles.keySet())) {
76
- if (UpdateContext.DEBUG) {
77
- Log.d(UpdateContext.TAG, "Processing fromPath: " + fromPath);
78
- }
79
-
80
72
  ArrayList<File> targets = remainingFiles.get(fromPath);
81
73
  if (targets == null || targets.isEmpty()) {
82
74
  continue;
@@ -92,9 +84,6 @@ final class BundledResourceCopier {
92
84
  if (actualEntry != null) {
93
85
  entry = availableEntries.get(actualEntry);
94
86
  actualSourcePath = actualEntry;
95
- if (UpdateContext.DEBUG) {
96
- Log.d(UpdateContext.TAG, "Normalized match: " + fromPath + " -> " + actualEntry);
97
- }
98
87
  }
99
88
  }
100
89
 
@@ -111,9 +100,6 @@ final class BundledResourceCopier {
111
100
 
112
101
  File lastTarget = null;
113
102
  for (File target : targets) {
114
- if (UpdateContext.DEBUG) {
115
- Log.d(UpdateContext.TAG, "Copying from resource " + actualSourcePath + " to " + target);
116
- }
117
103
  try {
118
104
  if (lastTarget != null) {
119
105
  UpdateFileUtils.copyFile(lastTarget, target);
@@ -146,9 +132,10 @@ final class BundledResourceCopier {
146
132
  }
147
133
 
148
134
  if (!remainingFiles.isEmpty() && UpdateContext.DEBUG) {
149
- for (String fromPath : remainingFiles.keySet()) {
150
- Log.w(UpdateContext.TAG, "Resource not found and no fallback available: " + fromPath);
151
- }
135
+ Log.w(
136
+ UpdateContext.TAG,
137
+ "Skipped " + remainingFiles.size() + " missing bundled resources"
138
+ );
152
139
  }
153
140
  } finally {
154
141
  closeZipFiles(zipFileMap);
@@ -244,12 +231,6 @@ final class BundledResourceCopier {
244
231
  resources.getValue(resourceId, typedValue, true);
245
232
  }
246
233
  } catch (Resources.NotFoundException e) {
247
- if (UpdateContext.DEBUG) {
248
- Log.d(
249
- UpdateContext.TAG,
250
- "Failed to resolve resource value for " + resourcePath + ": " + e.getMessage()
251
- );
252
- }
253
234
  return null;
254
235
  }
255
236
 
@@ -262,9 +243,6 @@ final class BundledResourceCopier {
262
243
  assetPath = assetPath.substring(1);
263
244
  }
264
245
 
265
- if (UpdateContext.DEBUG) {
266
- Log.d(UpdateContext.TAG, "Resolved resource path " + resourcePath + " -> " + assetPath);
267
- }
268
246
  return new ResolvedResourceSource(resourceId, assetPath);
269
247
  }
270
248
 
@@ -286,9 +264,6 @@ final class BundledResourceCopier {
286
264
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && appInfo.splitSourceDirs != null) {
287
265
  for (String splitPath : appInfo.splitSourceDirs) {
288
266
  apkPaths.add(splitPath);
289
- if (UpdateContext.DEBUG) {
290
- Log.d(UpdateContext.TAG, "Found split APK: " + splitPath);
291
- }
292
267
  }
293
268
  }
294
269
  } catch (PackageManager.NameNotFoundException e) {
@@ -79,10 +79,6 @@ class DownloadTask implements Runnable {
79
79
  throw new IOException("Failed to replace existing file: " + writePath);
80
80
  }
81
81
 
82
- if (UpdateContext.DEBUG) {
83
- Log.d(UpdateContext.TAG, "Downloading " + url);
84
- }
85
-
86
82
  try (Response response = HTTP_CLIENT.newCall(request).execute()) {
87
83
  if (!response.isSuccessful()) {
88
84
  throw new IOException("Server error: " + response.code() + " " + response.message());
@@ -125,9 +121,6 @@ class DownloadTask implements Runnable {
125
121
  postProgress(received, contentLength);
126
122
  }
127
123
 
128
- if (UpdateContext.DEBUG) {
129
- Log.d(UpdateContext.TAG, "Download finished");
130
- }
131
124
  }
132
125
 
133
126
  private byte[] readBytes(InputStream input) throws IOException {
@@ -251,9 +244,6 @@ class DownloadTask implements Runnable {
251
244
  }
252
245
  }
253
246
 
254
- if (UpdateContext.DEBUG) {
255
- Log.d(UpdateContext.TAG, "Unzip finished");
256
- }
257
247
  }
258
248
 
259
249
  private void doPatchFromApk() throws IOException, JSONException {
@@ -295,21 +285,7 @@ class DownloadTask implements Runnable {
295
285
  originBundleFile.delete();
296
286
  }
297
287
 
298
- if (UpdateContext.DEBUG) {
299
- Log.d(UpdateContext.TAG, "copyList size: " + copyList.size());
300
- for (String from : copyList.keySet()) {
301
- Log.d(
302
- UpdateContext.TAG,
303
- "copyList entry: " + from + " -> " + copyList.get(from).size() + " targets"
304
- );
305
- }
306
- }
307
-
308
288
  bundledResourceCopier.copyFromResource(copyList);
309
-
310
- if (UpdateContext.DEBUG) {
311
- Log.d(UpdateContext.TAG, "Unzip finished");
312
- }
313
289
  }
314
290
 
315
291
  private void doPatchFromPpk() throws IOException, JSONException {
@@ -337,15 +313,9 @@ class DownloadTask implements Runnable {
337
313
  contents.deletes.toArray(new String[0])
338
314
  );
339
315
 
340
- if (UpdateContext.DEBUG) {
341
- Log.d(UpdateContext.TAG, "Unzip finished");
342
- }
343
316
  }
344
317
 
345
318
  private void doCleanUp() {
346
- if (UpdateContext.DEBUG) {
347
- Log.d(UpdateContext.TAG, "Start cleaning up");
348
- }
349
319
  cleanupOldEntries(
350
320
  params.unzipDirectory.getAbsolutePath(),
351
321
  params.hash,
@@ -31,7 +31,8 @@ final class ReactReloadManager {
31
31
  Activity currentActivity = reactContext.getCurrentActivity();
32
32
  String updateBundlePath = updateContext.getBundleUrl();
33
33
 
34
- Object reactHost = getReactHost(currentActivity, application);
34
+ boolean newArchitectureEnabled = isNewArchitectureEnabled(application);
35
+ Object reactHost = newArchitectureEnabled ? getReactHost(currentActivity, application) : null;
35
36
  if (reactHost != null) {
36
37
  try {
37
38
  reloadReactHost(reactHost, createBundleLoader(application, updateBundlePath, true));
@@ -63,6 +64,9 @@ final class ReactReloadManager {
63
64
  }
64
65
 
65
66
  try {
67
+ if (!newArchitectureEnabled) {
68
+ throw err;
69
+ }
66
70
  Object currentReactHost = getReactHost(currentActivity, application);
67
71
  if (currentReactHost == null) {
68
72
  throw err;
@@ -180,6 +184,29 @@ final class ReactReloadManager {
180
184
  return null;
181
185
  }
182
186
 
187
+ private static boolean isNewArchitectureEnabled(Context application) {
188
+ try {
189
+ Class<?> buildConfigClass = Class.forName(application.getPackageName() + ".BuildConfig");
190
+ Field newArchitectureField = buildConfigClass.getField("IS_NEW_ARCHITECTURE_ENABLED");
191
+ return newArchitectureField.getBoolean(null);
192
+ } catch (Throwable ignored) {
193
+ }
194
+
195
+ if (application instanceof ReactApplication) {
196
+ try {
197
+ ReactNativeHost reactNativeHost = ((ReactApplication) application).getReactNativeHost();
198
+ Method isNewArchEnabledMethod =
199
+ reactNativeHost.getClass().getDeclaredMethod("isNewArchEnabled");
200
+ isNewArchEnabledMethod.setAccessible(true);
201
+ Object result = isNewArchEnabledMethod.invoke(reactNativeHost);
202
+ return result instanceof Boolean && (Boolean) result;
203
+ } catch (Throwable ignored) {
204
+ }
205
+ }
206
+
207
+ return false;
208
+ }
209
+
183
210
  private static void reloadReactHost(Object reactHost, JSBundleLoader loader) throws Throwable {
184
211
  try {
185
212
  Field devSupportField = getCompatibleField(reactHost.getClass(), "useDevSupport");
@@ -1,7 +1,5 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
- import android.util.Log;
4
-
5
3
  import java.io.BufferedInputStream;
6
4
  import java.io.BufferedOutputStream;
7
5
  import java.io.File;
@@ -66,10 +64,6 @@ public class SafeZipFile extends ZipFile {
66
64
  throw new SecurityException("Illegal name: " + name);
67
65
  }
68
66
 
69
- if (UpdateContext.DEBUG) {
70
- Log.d(UpdateContext.TAG, "Unzipping " + name);
71
- }
72
-
73
67
  if (ze.isDirectory()) {
74
68
  target.mkdirs();
75
69
  return;
@@ -1,6 +1,5 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
- import android.util.Log;
4
3
  import androidx.annotation.Nullable;
5
4
  import com.facebook.react.bridge.ReactApplicationContext;
6
5
  import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -26,9 +25,6 @@ final class UpdateEventEmitter {
26
25
  static void sendEvent(String eventName, WritableMap params) {
27
26
  ReactApplicationContext reactContext = getReactContext();
28
27
  if (reactContext == null || !reactContext.hasActiveCatalystInstance()) {
29
- if (UpdateContext.DEBUG) {
30
- Log.d(UpdateContext.TAG, "Skipping event " + eventName + " because React context is unavailable");
31
- }
32
28
  return;
33
29
  }
34
30
 
@@ -1,6 +1,5 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
- import android.util.Log;
4
3
  import java.io.File;
5
4
  import java.io.FileInputStream;
6
5
  import java.io.FileOutputStream;
@@ -27,9 +26,6 @@ final class UpdateFileUtils {
27
26
  }
28
27
 
29
28
  static void removeDirectory(File file) throws IOException {
30
- if (UpdateContext.DEBUG) {
31
- Log.d(UpdateContext.TAG, "Removing " + file);
32
- }
33
29
  if (file.isDirectory()) {
34
30
  File[] files = file.listFiles();
35
31
  if (files != null) {
@@ -1,12 +1,9 @@
1
1
  package expo.modules.pushy;
2
2
 
3
3
  import android.content.Context;
4
- import android.util.Log;
5
4
  import androidx.annotation.Nullable;
6
5
  import java.util.ArrayList;
7
- import java.util.HashMap;
8
6
  import java.util.List;
9
- import java.util.Map;
10
7
  import cn.reactnative.modules.update.UpdateContext;
11
8
  import expo.modules.core.interfaces.Package;
12
9
  import expo.modules.core.interfaces.ReactNativeHostHandler;
@@ -24,4 +21,4 @@ public class ExpoPushyPackage implements Package {
24
21
  });
25
22
  return handlers;
26
23
  }
27
- }
24
+ }
@@ -9,7 +9,6 @@
9
9
  #include <unistd.h>
10
10
 
11
11
  #include <set>
12
- #include <sstream>
13
12
  #include <vector>
14
13
 
15
14
  extern "C" {
@@ -31,12 +30,19 @@ class HdiffBundlePatcher final : public BundlePatcher {
31
30
  };
32
31
 
33
32
  Status MakeErrnoStatus(const std::string& message, int err = errno) {
34
- std::ostringstream stream;
35
- stream << message;
36
- if (err != 0) {
37
- stream << ": " << std::strerror(err);
33
+ if (err == 0) {
34
+ return Status::Error(message);
38
35
  }
39
- return Status::Error(stream.str());
36
+ return Status::Error(message + ": " + std::strerror(err));
37
+ }
38
+
39
+ std::string IntToString(int value) {
40
+ char buffer[32] = {0};
41
+ const int written = std::snprintf(buffer, sizeof(buffer), "%d", value);
42
+ if (written <= 0) {
43
+ return "0";
44
+ }
45
+ return std::string(buffer, static_cast<size_t>(written));
40
46
  }
41
47
 
42
48
  bool EndsWithSlash(const std::string& path) {
@@ -522,9 +528,8 @@ Status HdiffBundlePatcher::Apply(
522
528
  destination_bundle_path.c_str(),
523
529
  bundle_patch_path.c_str());
524
530
  if (result != 0) {
525
- std::ostringstream stream;
526
- stream << "Failed to apply bundle patch, hpatch error " << result;
527
- return Status::Error(stream.str());
531
+ return Status::Error(
532
+ "Failed to apply bundle patch, hpatch error " + IntToString(result));
528
533
  }
529
534
  return Status::Ok();
530
535
  }
@@ -442,6 +442,74 @@ void TestArchivePatchCoreSupportsCustomBundlePatchEntry() {
442
442
  ExpectEq(plan.merge_source_subdir, "", "custom bundle patch merge subdir mismatch");
443
443
  }
444
444
 
445
+ void TestArchivePatchCoreHarmonyBundlePatchFromPackage() {
446
+ PatchManifest manifest;
447
+ manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/b.png"});
448
+
449
+ pushy::archive_patch::ArchivePatchPlan plan;
450
+ Status status = pushy::archive_patch::BuildArchivePatchPlan(
451
+ pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
452
+ manifest,
453
+ {"__diff.json", "bundle.harmony.js.patch", "assets/new.png"},
454
+ &plan,
455
+ "bundle.harmony.js.patch");
456
+ Expect(status.ok, status.message);
457
+ Expect(plan.enable_merge, "harmony package plan should enable merge");
458
+ ExpectEq(plan.merge_source_subdir, "assets", "harmony package merge subdir should be assets");
459
+
460
+ // ppk variant uses empty merge subdir
461
+ pushy::archive_patch::ArchivePatchPlan ppk_plan;
462
+ status = pushy::archive_patch::BuildArchivePatchPlan(
463
+ pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
464
+ manifest,
465
+ {"__diff.json", "bundle.harmony.js.patch", "assets/new.png"},
466
+ &ppk_plan,
467
+ "bundle.harmony.js.patch");
468
+ Expect(status.ok, status.message);
469
+ Expect(ppk_plan.enable_merge, "harmony ppk plan should enable merge");
470
+ ExpectEq(ppk_plan.merge_source_subdir, "", "harmony ppk merge subdir should be empty");
471
+ }
472
+
473
+ void TestStateCoreRollbackToEmptyVersion() {
474
+ State state;
475
+ state.current_version = "current";
476
+ state.last_version = "";
477
+ state.first_time = false;
478
+ state.first_time_ok = true;
479
+
480
+ State rolled = pushy::state::Rollback(state);
481
+ Expect(rolled.current_version.empty(), "rollback with empty last should clear current");
482
+ Expect(rolled.last_version.empty(), "last_version should remain empty");
483
+ ExpectEq(rolled.rolled_back_version, "current", "rolled_back_version should record original");
484
+ Expect(!rolled.first_time, "first_time should be false after rollback");
485
+ Expect(rolled.first_time_ok, "first_time_ok should be true after rollback");
486
+ }
487
+
488
+ void TestStateCoreResolveLaunchNoCurrentVersion() {
489
+ State state;
490
+ state.current_version = "";
491
+ state.first_time = false;
492
+ state.first_time_ok = true;
493
+
494
+ LaunchDecision decision = pushy::state::ResolveLaunchState(state, false, true);
495
+ Expect(decision.load_version.empty(), "empty current should yield empty load_version");
496
+ Expect(!decision.did_rollback, "should not rollback when no current version");
497
+ Expect(!decision.consumed_first_time, "should not consume first_time when no current version");
498
+ }
499
+
500
+ void TestStateCoreSwitchToSameVersion() {
501
+ State state;
502
+ state.package_version = "1.0.0";
503
+ state.current_version = "same_hash";
504
+ state.last_version = "old_hash";
505
+
506
+ State switched = pushy::state::SwitchVersion(state, "same_hash");
507
+ ExpectEq(switched.current_version, "same_hash", "current should remain same_hash");
508
+ ExpectEq(switched.last_version, "old_hash", "last_version should not change when switching to same");
509
+ Expect(switched.first_time, "first_time should be set even when switching to same");
510
+ Expect(!switched.first_time_ok, "first_time_ok should be false");
511
+ }
512
+
445
513
  } // namespace
446
514
 
447
515
  int main() {
@@ -457,6 +525,10 @@ int main() {
457
525
  {"ArchivePatchCoreBuildPlanAndCopyGroups", TestArchivePatchCoreBuildPlanAndCopyGroups},
458
526
  {"ArchivePatchCoreRejectsMissingEntries", TestArchivePatchCoreRejectsMissingEntries},
459
527
  {"ArchivePatchCoreSupportsCustomBundlePatchEntry", TestArchivePatchCoreSupportsCustomBundlePatchEntry},
528
+ {"ArchivePatchCoreHarmonyBundlePatchFromPackage", TestArchivePatchCoreHarmonyBundlePatchFromPackage},
529
+ {"StateCoreRollbackToEmptyVersion", TestStateCoreRollbackToEmptyVersion},
530
+ {"StateCoreResolveLaunchNoCurrentVersion", TestStateCoreResolveLaunchNoCurrentVersion},
531
+ {"StateCoreSwitchToSameVersion", TestStateCoreSwitchToSameVersion},
460
532
  };
461
533
 
462
534
  for (const auto& test : tests) {
@@ -12,12 +12,45 @@ import NativePatchCore, {
12
12
  CopyGroupResult,
13
13
  } from './NativePatchCore';
14
14
 
15
- interface PatchManifestArrays {
15
+ export interface PatchManifestArrays {
16
16
  copyFroms: string[];
17
17
  copyTos: string[];
18
18
  deletes: string[];
19
19
  }
20
20
 
21
+ export function parseManifestToArrays(
22
+ manifest: Record<string, any>,
23
+ normalizeResourceCopies: boolean,
24
+ ): PatchManifestArrays {
25
+ const copyFroms: string[] = [];
26
+ const copyTos: string[] = [];
27
+ const deletesValue = manifest.deletes;
28
+ const deletes = Array.isArray(deletesValue)
29
+ ? deletesValue.map(item => String(item))
30
+ : deletesValue && typeof deletesValue === 'object'
31
+ ? Object.keys(deletesValue)
32
+ : [];
33
+
34
+ const copies = (manifest.copies || {}) as Record<string, string>;
35
+ for (const [to, rawFrom] of Object.entries(copies)) {
36
+ let from = String(rawFrom || '');
37
+ if (normalizeResourceCopies) {
38
+ from = from.replace('resources/rawfile/', '');
39
+ if (!from) {
40
+ from = to;
41
+ }
42
+ }
43
+ copyFroms.push(from);
44
+ copyTos.push(to);
45
+ }
46
+
47
+ return {
48
+ copyFroms,
49
+ copyTos,
50
+ deletes,
51
+ };
52
+ }
53
+
21
54
  const DIFF_MANIFEST_ENTRY = '__diff.json';
22
55
  const HARMONY_BUNDLE_PATCH_ENTRY = 'bundle.harmony.js.patch';
23
56
  const TEMP_ORIGIN_BUNDLE_ENTRY = '.origin.bundle.harmony.js';
@@ -40,11 +73,14 @@ export class DownloadTask {
40
73
  const stat = await fileIo.stat(path);
41
74
  if (stat.isDirectory()) {
42
75
  const files = await fileIo.listFile(path);
43
- for (const file of files) {
44
- if (file === '.' || file === '..') {
45
- continue;
46
- }
47
- await this.removeDirectory(`${path}/${file}`);
76
+
77
+ const entries = files.filter(file => file !== '.' && file !== '..');
78
+ const DELETE_CONCURRENCY = 32;
79
+ for (let i = 0; i < entries.length; i += DELETE_CONCURRENCY) {
80
+ const batch = entries.slice(i, i + DELETE_CONCURRENCY);
81
+ await Promise.all(
82
+ batch.map(file => this.removeDirectory(`${path}/${file}`)),
83
+ );
48
84
  }
49
85
  await fileIo.rmdir(path);
50
86
  } else {
@@ -166,44 +202,12 @@ export class DownloadTask {
166
202
  };
167
203
  }
168
204
 
169
- return this.manifestToArrays(
205
+ return parseManifestToArrays(
170
206
  this.parseJsonEntry(await this.readFileContent(manifestPath)),
171
207
  normalizeResourceCopies,
172
208
  );
173
209
  }
174
210
 
175
- private manifestToArrays(
176
- manifest: Record<string, any>,
177
- normalizeResourceCopies: boolean,
178
- ): PatchManifestArrays {
179
- const copyFroms: string[] = [];
180
- const copyTos: string[] = [];
181
- const deletesValue = manifest.deletes;
182
- const deletes = Array.isArray(deletesValue)
183
- ? deletesValue.map(item => String(item))
184
- : deletesValue && typeof deletesValue === 'object'
185
- ? Object.keys(deletesValue)
186
- : [];
187
-
188
- const copies = (manifest.copies || {}) as Record<string, string>;
189
- for (const [to, rawFrom] of Object.entries(copies)) {
190
- let from = String(rawFrom || '');
191
- if (normalizeResourceCopies) {
192
- from = from.replace('resources/rawfile/', '');
193
- if (!from) {
194
- from = to;
195
- }
196
- }
197
- copyFroms.push(from);
198
- copyTos.push(to);
199
- }
200
-
201
- return {
202
- copyFroms,
203
- copyTos,
204
- deletes,
205
- };
206
- }
207
211
 
208
212
  private async applyBundlePatchFromFileSource(
209
213
  originContent: ArrayBuffer,
@@ -525,9 +529,9 @@ export class DownloadTask {
525
529
  targets.map(t => t.substring(0, t.lastIndexOf('/'))).filter(Boolean),
526
530
  ),
527
531
  ];
528
- for (const dir of parentDirs) {
529
- await this.ensureDirectory(dir);
530
- }
532
+ await Promise.all(
533
+ parentDirs.map(dir => this.ensureDirectory(dir)),
534
+ );
531
535
  await Promise.all(
532
536
  targets.map(target => this.writeFileContent(target, mediaBuffer.buffer)),
533
537
  );
@@ -540,9 +544,9 @@ export class DownloadTask {
540
544
  targets.map(t => t.substring(0, t.lastIndexOf('/'))).filter(Boolean),
541
545
  ),
542
546
  ];
543
- for (const dir of parentDirs) {
544
- await this.ensureDirectory(dir);
545
- }
547
+ await Promise.all(
548
+ parentDirs.map(dir => this.ensureDirectory(dir))
549
+ );
546
550
  if (fileIo.accessSync(firstTarget)) {
547
551
  await fileIo.unlink(firstTarget);
548
552
  }
@@ -11,6 +11,24 @@ import { util } from '@kit.ArkTS';
11
11
 
12
12
  const TAG = 'PushyTurboModule';
13
13
 
14
+ export function getErrorMessage(error: any): string {
15
+ if (error && typeof error === 'object' && 'message' in error) {
16
+ return String(error.message);
17
+ }
18
+ return String(error);
19
+ }
20
+
21
+ export function validateHashInfo(info: string): void {
22
+ try {
23
+ const parsed = JSON.parse(info);
24
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
25
+ throw Error('invalid json string');
26
+ }
27
+ } catch {
28
+ throw Error('invalid json string');
29
+ }
30
+ }
31
+
14
32
  export class PushyTurboModule extends UITurboModule {
15
33
  public static readonly NAME = 'Pushy';
16
34
 
@@ -29,13 +47,6 @@ export class PushyTurboModule extends UITurboModule {
29
47
  return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
30
48
  }
31
49
 
32
- private getErrorMessage(error: any): string {
33
- if (error && typeof error === 'object' && 'message' in error) {
34
- return String(error.message);
35
- }
36
- return String(error);
37
- }
38
-
39
50
  private getPackageVersion(): string {
40
51
  try {
41
52
  const bundleInfo = bundleManager.getBundleInfoForSelfSync(
@@ -70,17 +81,6 @@ export class PushyTurboModule extends UITurboModule {
70
81
  return hash;
71
82
  }
72
83
 
73
- private validateHashInfo(info: string): void {
74
- try {
75
- const parsed = JSON.parse(info);
76
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
77
- throw Error('invalid json string');
78
- }
79
- } catch {
80
- throw Error('invalid json string');
81
- }
82
- }
83
-
84
84
  private async restartAbility(): Promise<void> {
85
85
  const bundleInfo = await bundleManager.getBundleInfoForSelf(
86
86
  this.getBundleFlags(),
@@ -127,14 +127,14 @@ export class PushyTurboModule extends UITurboModule {
127
127
 
128
128
  setLocalHashInfo(hash: string, info: string): boolean {
129
129
  logger.debug(TAG, ',call setLocalHashInfo');
130
- this.validateHashInfo(info);
130
+ validateHashInfo(info);
131
131
  this.context.setKv(`hash_${hash}`, info);
132
132
  return true;
133
133
  }
134
134
 
135
135
  getLocalHashInfo(hash: string): string {
136
136
  const value = this.context.getKv(`hash_${hash}`);
137
- this.validateHashInfo(value);
137
+ validateHashInfo(value);
138
138
  return value;
139
139
  }
140
140
 
@@ -151,8 +151,8 @@ export class PushyTurboModule extends UITurboModule {
151
151
  this.context.switchVersion(hash);
152
152
  await this.restartAbility();
153
153
  } catch (error) {
154
- logger.error(TAG, `reloadUpdate failed: ${this.getErrorMessage(error)}`);
155
- throw Error(`switchVersion failed ${this.getErrorMessage(error)}`);
154
+ logger.error(TAG, `reloadUpdate failed: ${getErrorMessage(error)}`);
155
+ throw Error(`switchVersion failed ${getErrorMessage(error)}`);
156
156
  }
157
157
  }
158
158
 
@@ -161,8 +161,8 @@ export class PushyTurboModule extends UITurboModule {
161
161
  try {
162
162
  await this.restartAbility();
163
163
  } catch (error) {
164
- logger.error(TAG, `restartApp failed: ${this.getErrorMessage(error)}`);
165
- throw Error(`restartApp failed ${this.getErrorMessage(error)}`);
164
+ logger.error(TAG, `restartApp failed: ${getErrorMessage(error)}`);
165
+ throw Error(`restartApp failed ${getErrorMessage(error)}`);
166
166
  }
167
167
  }
168
168
 
@@ -174,8 +174,8 @@ export class PushyTurboModule extends UITurboModule {
174
174
  this.context.switchVersion(hash);
175
175
  return true;
176
176
  } catch (error) {
177
- logger.error(TAG, `setNeedUpdate failed: ${this.getErrorMessage(error)}`);
178
- throw Error(`switchVersionLater failed: ${this.getErrorMessage(error)}`);
177
+ logger.error(TAG, `setNeedUpdate failed: ${getErrorMessage(error)}`);
178
+ throw Error(`switchVersionLater failed: ${getErrorMessage(error)}`);
179
179
  }
180
180
  }
181
181
 
@@ -185,7 +185,7 @@ export class PushyTurboModule extends UITurboModule {
185
185
  this.context.markSuccess();
186
186
  return true;
187
187
  } catch (error) {
188
- logger.error(TAG, `markSuccess failed: ${this.getErrorMessage(error)}`);
188
+ logger.error(TAG, `markSuccess failed: ${getErrorMessage(error)}`);
189
189
  throw error;
190
190
  }
191
191
  }
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from '@ohos/hypium';
2
+ import { DownloadTaskParams } from '../main/ets/DownloadTaskParams';
3
+
4
+ export default function downloadTaskParamsTest() {
5
+ describe('DownloadTaskParams', () => {
6
+ it('has correct task type constants', 0, () => {
7
+ expect(DownloadTaskParams.TASK_TYPE_CLEANUP).assertEqual(0);
8
+ expect(DownloadTaskParams.TASK_TYPE_PATCH_FULL).assertEqual(1);
9
+ expect(DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP).assertEqual(2);
10
+ expect(DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK).assertEqual(3);
11
+ expect(DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD).assertEqual(4);
12
+ });
13
+
14
+ it('has correct default values', 0, () => {
15
+ const params = new DownloadTaskParams();
16
+ expect(params.type).assertEqual(DownloadTaskParams.TASK_TYPE_CLEANUP);
17
+ expect(params.url).assertEqual('');
18
+ expect(params.hash).assertEqual('');
19
+ expect(params.originHash).assertEqual('');
20
+ expect(params.targetFile).assertEqual('');
21
+ expect(params.unzipDirectory).assertEqual('');
22
+ expect(params.originDirectory).assertEqual('');
23
+ });
24
+
25
+ it('allows setting all fields', 0, () => {
26
+ const params = new DownloadTaskParams();
27
+ params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
28
+ params.url = 'https://example.com/patch.ppk';
29
+ params.hash = 'abc123';
30
+ params.originHash = 'def456';
31
+ params.targetFile = '/data/patch.ppk';
32
+ params.unzipDirectory = '/data/abc123';
33
+ params.originDirectory = '/data/def456';
34
+
35
+ expect(params.type).assertEqual(3);
36
+ expect(params.url).assertEqual('https://example.com/patch.ppk');
37
+ expect(params.hash).assertEqual('abc123');
38
+ expect(params.originHash).assertEqual('def456');
39
+ expect(params.targetFile).assertEqual('/data/patch.ppk');
40
+ expect(params.unzipDirectory).assertEqual('/data/abc123');
41
+ expect(params.originDirectory).assertEqual('/data/def456');
42
+ });
43
+ });
44
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, beforeEach } from '@ohos/hypium';
2
+ import { EventHub } from '../main/ets/EventHub';
3
+
4
+ export default function eventHubTest() {
5
+ describe('EventHub', () => {
6
+ it('is a singleton', 0, () => {
7
+ const a = EventHub.getInstance();
8
+ const b = EventHub.getInstance();
9
+ expect(a === b).assertEqual(true);
10
+ });
11
+
12
+ it('does not throw when emitting without rnInstance', 0, () => {
13
+ const hub = EventHub.getInstance();
14
+ hub.setRNInstance(undefined);
15
+ let threw = false;
16
+ try {
17
+ hub.emit('test', { foo: 'bar' });
18
+ } catch {
19
+ threw = true;
20
+ }
21
+ expect(threw).assertEqual(false);
22
+ });
23
+
24
+ it('calls emitDeviceEvent on rnInstance when set', 0, () => {
25
+ const hub = EventHub.getInstance();
26
+ let capturedEvent = '';
27
+ let capturedData: any = null;
28
+ const mockRnInstance = {
29
+ emitDeviceEvent(event: string, data: any) {
30
+ capturedEvent = event;
31
+ capturedData = data;
32
+ },
33
+ };
34
+ hub.setRNInstance(mockRnInstance);
35
+ hub.emit('RCTPushyDownloadProgress', { received: 100, total: 200 });
36
+ expect(capturedEvent).assertEqual('RCTPushyDownloadProgress');
37
+ expect(capturedData.received).assertEqual(100);
38
+ expect(capturedData.total).assertEqual(200);
39
+ hub.setRNInstance(undefined);
40
+ });
41
+
42
+ it('on/off manages listeners', 0, () => {
43
+ const hub = EventHub.getInstance();
44
+ let callCount = 0;
45
+ const callback = () => { callCount++; };
46
+ hub.on('test-event', callback);
47
+ hub.off('test-event', callback);
48
+ expect(callCount).assertEqual(0);
49
+ });
50
+ });
51
+ }
@@ -0,0 +1,11 @@
1
+ import manifestParsingTest from './ManifestParsing.test';
2
+ import validationTest from './Validation.test';
3
+ import eventHubTest from './EventHub.test';
4
+ import downloadTaskParamsTest from './DownloadTaskParams.test';
5
+
6
+ export default function testsuite() {
7
+ manifestParsingTest();
8
+ validationTest();
9
+ eventHubTest();
10
+ downloadTaskParamsTest();
11
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from '@ohos/hypium';
2
+ import { parseManifestToArrays } from '../main/ets/DownloadTask';
3
+
4
+ export default function manifestParsingTest() {
5
+ describe('parseManifestToArrays', () => {
6
+ it('returns empty arrays for empty manifest', 0, () => {
7
+ const result = parseManifestToArrays({}, false);
8
+ expect(result.copyFroms.length).assertEqual(0);
9
+ expect(result.copyTos.length).assertEqual(0);
10
+ expect(result.deletes.length).assertEqual(0);
11
+ });
12
+
13
+ it('parses copies and deletes array', 0, () => {
14
+ const manifest = {
15
+ copies: {
16
+ 'assets/icon.png': 'assets/original.png',
17
+ 'assets/logo.png': 'assets/brand.png',
18
+ },
19
+ deletes: ['assets/old.png', 'assets/removed.txt'],
20
+ };
21
+ const result = parseManifestToArrays(manifest, false);
22
+ expect(result.copyFroms.length).assertEqual(2);
23
+ expect(result.copyTos.length).assertEqual(2);
24
+ expect(result.copyFroms[0]).assertEqual('assets/original.png');
25
+ expect(result.copyTos[0]).assertEqual('assets/icon.png');
26
+ expect(result.copyFroms[1]).assertEqual('assets/brand.png');
27
+ expect(result.copyTos[1]).assertEqual('assets/logo.png');
28
+ expect(result.deletes.length).assertEqual(2);
29
+ expect(result.deletes[0]).assertEqual('assets/old.png');
30
+ expect(result.deletes[1]).assertEqual('assets/removed.txt');
31
+ });
32
+
33
+ it('parses deletes as object keys', 0, () => {
34
+ const manifest = {
35
+ deletes: {
36
+ 'assets/old.png': true,
37
+ 'assets/removed.txt': true,
38
+ },
39
+ };
40
+ const result = parseManifestToArrays(manifest, false);
41
+ expect(result.deletes.length).assertEqual(2);
42
+ expect(result.deletes[0]).assertEqual('assets/old.png');
43
+ expect(result.deletes[1]).assertEqual('assets/removed.txt');
44
+ });
45
+
46
+ it('handles missing copies gracefully', 0, () => {
47
+ const manifest = {
48
+ deletes: ['a.txt'],
49
+ };
50
+ const result = parseManifestToArrays(manifest, false);
51
+ expect(result.copyFroms.length).assertEqual(0);
52
+ expect(result.copyTos.length).assertEqual(0);
53
+ expect(result.deletes.length).assertEqual(1);
54
+ });
55
+
56
+ it('handles missing deletes gracefully', 0, () => {
57
+ const manifest = {
58
+ copies: { 'a.png': 'b.png' },
59
+ };
60
+ const result = parseManifestToArrays(manifest, false);
61
+ expect(result.deletes.length).assertEqual(0);
62
+ expect(result.copyFroms.length).assertEqual(1);
63
+ });
64
+
65
+ it('normalizes resource copies when enabled', 0, () => {
66
+ const manifest = {
67
+ copies: {
68
+ 'assets/icon.png': 'resources/rawfile/assets/icon.png',
69
+ },
70
+ };
71
+ const result = parseManifestToArrays(manifest, true);
72
+ expect(result.copyFroms[0]).assertEqual('assets/icon.png');
73
+ expect(result.copyTos[0]).assertEqual('assets/icon.png');
74
+ });
75
+
76
+ it('falls back to key when normalized from is empty', 0, () => {
77
+ const manifest = {
78
+ copies: {
79
+ 'assets/icon.png': 'resources/rawfile/',
80
+ },
81
+ };
82
+ const result = parseManifestToArrays(manifest, true);
83
+ expect(result.copyFroms[0]).assertEqual('assets/icon.png');
84
+ });
85
+
86
+ it('does not normalize when disabled', 0, () => {
87
+ const manifest = {
88
+ copies: {
89
+ 'assets/icon.png': 'resources/rawfile/assets/icon.png',
90
+ },
91
+ };
92
+ const result = parseManifestToArrays(manifest, false);
93
+ expect(result.copyFroms[0]).assertEqual('resources/rawfile/assets/icon.png');
94
+ });
95
+
96
+ it('handles null-ish from values', 0, () => {
97
+ const manifest = {
98
+ copies: {
99
+ 'assets/icon.png': null,
100
+ },
101
+ };
102
+ const result = parseManifestToArrays(manifest as any, false);
103
+ expect(result.copyFroms[0]).assertEqual('');
104
+ expect(result.copyTos[0]).assertEqual('assets/icon.png');
105
+ });
106
+ });
107
+ }
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from '@ohos/hypium';
2
+ import { validateHashInfo, getErrorMessage } from '../main/ets/PushyTurboModule';
3
+
4
+ export default function validationTest() {
5
+ describe('validateHashInfo', () => {
6
+ it('accepts valid JSON object', 0, () => {
7
+ let threw = false;
8
+ try {
9
+ validateHashInfo('{"name":"v1"}');
10
+ } catch {
11
+ threw = true;
12
+ }
13
+ expect(threw).assertEqual(false);
14
+ });
15
+
16
+ it('accepts empty JSON object', 0, () => {
17
+ let threw = false;
18
+ try {
19
+ validateHashInfo('{}');
20
+ } catch {
21
+ threw = true;
22
+ }
23
+ expect(threw).assertEqual(false);
24
+ });
25
+
26
+ it('rejects JSON array', 0, () => {
27
+ let threw = false;
28
+ try {
29
+ validateHashInfo('[1,2,3]');
30
+ } catch {
31
+ threw = true;
32
+ }
33
+ expect(threw).assertEqual(true);
34
+ });
35
+
36
+ it('rejects plain string', 0, () => {
37
+ let threw = false;
38
+ try {
39
+ validateHashInfo('"hello"');
40
+ } catch {
41
+ threw = true;
42
+ }
43
+ expect(threw).assertEqual(true);
44
+ });
45
+
46
+ it('rejects invalid JSON', 0, () => {
47
+ let threw = false;
48
+ try {
49
+ validateHashInfo('{invalid}');
50
+ } catch {
51
+ threw = true;
52
+ }
53
+ expect(threw).assertEqual(true);
54
+ });
55
+
56
+ it('rejects null JSON', 0, () => {
57
+ let threw = false;
58
+ try {
59
+ validateHashInfo('null');
60
+ } catch {
61
+ threw = true;
62
+ }
63
+ expect(threw).assertEqual(true);
64
+ });
65
+ });
66
+
67
+ describe('getErrorMessage', () => {
68
+ it('extracts message from Error object', 0, () => {
69
+ const err = new Error('something broke');
70
+ expect(getErrorMessage(err)).assertEqual('something broke');
71
+ });
72
+
73
+ it('extracts message from plain object with message property', 0, () => {
74
+ const err = { message: 'custom error', code: 42 };
75
+ expect(getErrorMessage(err)).assertEqual('custom error');
76
+ });
77
+
78
+ it('converts string to string', 0, () => {
79
+ expect(getErrorMessage('raw string')).assertEqual('raw string');
80
+ });
81
+
82
+ it('converts number to string', 0, () => {
83
+ expect(getErrorMessage(404)).assertEqual('404');
84
+ });
85
+
86
+ it('converts null to string', 0, () => {
87
+ expect(getErrorMessage(null)).assertEqual('null');
88
+ });
89
+
90
+ it('converts undefined to string', 0, () => {
91
+ expect(getErrorMessage(undefined)).assertEqual('undefined');
92
+ });
93
+ });
94
+ }
package/harmony/pushy.har CHANGED
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.40.0-beta.2",
3
+ "version": "10.40.1",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "scripts": {
Binary file
@@ -38,3 +38,7 @@ mock.module('../i18n', () => {
38
38
  },
39
39
  };
40
40
  });
41
+
42
+ mock.module('react-native/Libraries/Core/ReactNativeVersion', () => ({
43
+ version: { major: 0, minor: 73, patch: 0 },
44
+ }));
package/src/client.ts CHANGED
@@ -184,7 +184,7 @@ export class Pushy {
184
184
  message?: string;
185
185
  data?: Record<string, string | number>;
186
186
  }) => {
187
- log(type + ' ' + message);
187
+ log(`${type} ${message}`);
188
188
  await this.loggerPromise.promise;
189
189
  const { logger = noop, appKey } = this.options;
190
190
  const overridePackageVersion = this.options.overridePackageVersion;
@@ -310,12 +310,12 @@ export class Pushy {
310
310
  }
311
311
  return true;
312
312
  };
313
- markSuccess = () => {
313
+ markSuccess = async () => {
314
314
  if (sharedState.marked || __DEV__ || !isFirstTime) {
315
315
  return;
316
316
  }
317
+ await Promise.resolve(PushyModule.markSuccess());
317
318
  sharedState.marked = true;
318
- PushyModule.markSuccess();
319
319
  this.report({ type: 'markSuccess' });
320
320
  };
321
321
  switchVersion = async (hash: string) => {
@@ -323,7 +323,7 @@ export class Pushy {
323
323
  return;
324
324
  }
325
325
  if (assertHash(hash) && !sharedState.applyingUpdate) {
326
- log('switchVersion: ' + hash);
326
+ log(`switchVersion: ${hash}`);
327
327
  sharedState.applyingUpdate = true;
328
328
  return PushyModule.reloadUpdate({ hash });
329
329
  }
@@ -334,7 +334,7 @@ export class Pushy {
334
334
  return;
335
335
  }
336
336
  if (assertHash(hash)) {
337
- log('switchVersionLater: ' + hash);
337
+ log(`switchVersionLater: ${hash}`);
338
338
  return PushyModule.setNeedUpdate({ hash });
339
339
  }
340
340
  };
@@ -366,7 +366,7 @@ export class Pushy {
366
366
  return result;
367
367
  }
368
368
  this.lastChecking = now;
369
- const fetchBody = {
369
+ const fetchBody: Record<string, any> = {
370
370
  packageVersion: this.options.overridePackageVersion || packageVersion,
371
371
  hash: currentVersion,
372
372
  buildTime,
@@ -374,7 +374,6 @@ export class Pushy {
374
374
  ...extra,
375
375
  };
376
376
  if (__DEV__) {
377
- // @ts-ignore
378
377
  delete fetchBody.buildTime;
379
378
  }
380
379
  const stringifyBody = JSON.stringify(fetchBody);
@@ -395,7 +394,7 @@ export class Pushy {
395
394
  try {
396
395
  this.report({
397
396
  type: 'checking',
398
- message: this.options.appKey + ': ' + stringifyBody,
397
+ message: `${this.options.appKey}: ${stringifyBody}`,
399
398
  });
400
399
  const respJsonPromise = this.fetchCheckResult(fetchPayload);
401
400
  this.lastRespJson = respJsonPromise;
@@ -496,7 +495,7 @@ export class Pushy {
496
495
  },
497
496
  });
498
497
  let lastError: any;
499
- let errorMessages: string[] = [];
498
+ const errorMessages: string[] = [];
500
499
  const diffUrl = await testUrls(joinUrls(paths, diff));
501
500
  if (diffUrl && !__DEV__) {
502
501
  log('downloading diff');
@@ -3,7 +3,7 @@
3
3
  import { cInfo } from './core';
4
4
 
5
5
  /* eslint-disable no-bitwise */
6
- function murmurhash3_32_gc(key: string, seed = 0) {
6
+ export function murmurhash3_32_gc(key: string, seed = 0) {
7
7
  let remainder, bytes, h1, h1b, c1, c2, k1, i;
8
8
 
9
9
  remainder = key.length & 3; // key.length % 4
@@ -1,4 +1,5 @@
1
1
  import type { PermissionsAndroidStatic } from 'react-native';
2
2
  import { emptyModule } from './utils';
3
3
 
4
- export const PermissionsAndroid = emptyModule as PermissionsAndroidStatic;
4
+ export const PermissionsAndroid =
5
+ emptyModule as unknown as PermissionsAndroidStatic;
package/src/utils.ts CHANGED
@@ -39,16 +39,15 @@ export function promiseAny<T>(promises: Promise<T>[]) {
39
39
 
40
40
  export const emptyObj = {};
41
41
  export const noop = () => {};
42
- class EmptyModule {
43
- constructor() {
44
- return new Proxy(this, {
45
- get() {
46
- return noop;
47
- },
48
- });
49
- }
50
- }
51
- export const emptyModule = new EmptyModule();
42
+ const emptyModuleTarget: Record<string, typeof noop> = {};
43
+ export const emptyModule = new Proxy(
44
+ emptyModuleTarget,
45
+ {
46
+ get(_target, _prop) {
47
+ return noop;
48
+ },
49
+ },
50
+ );
52
51
 
53
52
  const ping = isWeb
54
53
  ? Promise.resolve