react-native-update 10.43.3 → 10.44.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 (27) hide show
  1. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +24 -12
  2. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +23 -8
  3. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
  4. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
  5. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +2 -2
  6. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +8 -8
  7. package/cpp/patch_core/archive_patch_core.cpp +16 -0
  8. package/cpp/patch_core/archive_patch_core.h +5 -0
  9. package/cpp/patch_core/jni_util.h +56 -0
  10. package/cpp/patch_core/patch_core_android.cpp +4 -37
  11. package/cpp/patch_core/state_ops.h +24 -0
  12. package/cpp/patch_core/tests/patch_core_test.cpp +32 -2
  13. package/cpp/patch_core/update_core_android.cpp +43 -69
  14. package/harmony/pushy/src/main/cpp/pushy.cpp +162 -35
  15. package/harmony/pushy/src/main/ets/DownloadTask.ts +59 -9
  16. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -2
  17. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +10 -10
  18. package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
  19. package/harmony/pushy.har +0 -0
  20. package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
  21. package/package.json +2 -1
  22. package/src/client.ts +130 -75
  23. package/src/provider.tsx +40 -26
  24. package/src/utils.ts +2 -2
  25. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  26. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  27. package/src/__tests__/setup.ts +0 -44
@@ -4,56 +4,16 @@
4
4
  #include <vector>
5
5
 
6
6
  #include "archive_patch_core.h"
7
+ #include "jni_util.h"
7
8
  #include "state_core.h"
9
+ #include "state_ops.h"
8
10
 
9
11
  namespace {
10
12
 
11
- enum class StateOperation {
12
- kSwitchVersion = 1,
13
- kMarkSuccess = 2,
14
- kRollback = 3,
15
- kClearFirstTime = 4,
16
- kClearRollbackMark = 5,
17
- kResolveLaunch = 6,
18
- };
19
-
20
- std::string JStringToString(JNIEnv* env, jstring value) {
21
- if (value == nullptr) {
22
- return std::string();
23
- }
24
-
25
- const char* chars = env->GetStringUTFChars(value, nullptr);
26
- if (chars == nullptr) {
27
- return std::string();
28
- }
29
-
30
- std::string result(chars);
31
- env->ReleaseStringUTFChars(value, chars);
32
- return result;
33
- }
34
-
35
- std::vector<std::string> JArrayToVector(JNIEnv* env, jobjectArray values) {
36
- std::vector<std::string> result;
37
- if (values == nullptr) {
38
- return result;
39
- }
40
-
41
- const jsize size = env->GetArrayLength(values);
42
- result.reserve(static_cast<size_t>(size));
43
- for (jsize index = 0; index < size; ++index) {
44
- auto* item = static_cast<jstring>(env->GetObjectArrayElement(values, index));
45
- result.push_back(JStringToString(env, item));
46
- env->DeleteLocalRef(item);
47
- }
48
- return result;
49
- }
50
-
51
- void ThrowRuntimeException(JNIEnv* env, const std::string& message) {
52
- jclass exception = env->FindClass("java/lang/RuntimeException");
53
- if (exception != nullptr) {
54
- env->ThrowNew(exception, message.c_str());
55
- }
56
- }
13
+ using pushy::jni_util::JArrayToVector;
14
+ using pushy::jni_util::JStringToString;
15
+ using pushy::jni_util::ThrowRuntimeException;
16
+ using pushy::state_ops::StateOperation;
57
17
 
58
18
  void SetStringField(
59
19
  JNIEnv* env,
@@ -64,6 +24,9 @@ void SetStringField(
64
24
  jfieldID field =
65
25
  env->GetFieldID(target_class, field_name, "Ljava/lang/String;");
66
26
  if (field == nullptr) {
27
+ // Clear the pending NoSuchFieldError so subsequent JNI calls are not
28
+ // executed with an exception pending (undefined behavior / CheckJNI abort).
29
+ env->ExceptionClear();
67
30
  return;
68
31
  }
69
32
  if (value.empty()) {
@@ -83,9 +46,11 @@ void SetBooleanField(
83
46
  const char* field_name,
84
47
  bool value) {
85
48
  jfieldID field = env->GetFieldID(target_class, field_name, "Z");
86
- if (field != nullptr) {
87
- env->SetBooleanField(target, field, value ? JNI_TRUE : JNI_FALSE);
49
+ if (field == nullptr) {
50
+ env->ExceptionClear();
51
+ return;
88
52
  }
53
+ env->SetBooleanField(target, field, value ? JNI_TRUE : JNI_FALSE);
89
54
  }
90
55
 
91
56
  std::string GetStringField(
@@ -96,6 +61,7 @@ std::string GetStringField(
96
61
  jfieldID field =
97
62
  env->GetFieldID(target_class, field_name, "Ljava/lang/String;");
98
63
  if (field == nullptr) {
64
+ env->ExceptionClear();
99
65
  return std::string();
100
66
  }
101
67
  auto* value = static_cast<jstring>(env->GetObjectField(target, field));
@@ -112,7 +78,11 @@ bool GetBooleanField(
112
78
  jclass target_class,
113
79
  const char* field_name) {
114
80
  jfieldID field = env->GetFieldID(target_class, field_name, "Z");
115
- return field != nullptr && env->GetBooleanField(target, field) == JNI_TRUE;
81
+ if (field == nullptr) {
82
+ env->ExceptionClear();
83
+ return false;
84
+ }
85
+ return env->GetBooleanField(target, field) == JNI_TRUE;
116
86
  }
117
87
 
118
88
  pushy::state::State ReadState(
@@ -241,13 +211,9 @@ jobject NewArchivePatchPlanResult(
241
211
 
242
212
  jobject NewCopyGroupResult(
243
213
  JNIEnv* env,
214
+ jclass result_class,
215
+ jclass string_class,
244
216
  const pushy::archive_patch::CopyGroup& group) {
245
- jclass result_class =
246
- env->FindClass("cn/reactnative/modules/update/CopyGroupResult");
247
- if (result_class == nullptr) {
248
- return nullptr;
249
- }
250
-
251
217
  jmethodID constructor = env->GetMethodID(result_class, "<init>", "()V");
252
218
  if (constructor == nullptr) {
253
219
  return nullptr;
@@ -262,7 +228,6 @@ jobject NewCopyGroupResult(
262
228
  jfieldID to_paths_field =
263
229
  env->GetFieldID(result_class, "toPaths", "[Ljava/lang/String;");
264
230
  if (to_paths_field != nullptr) {
265
- jclass string_class = env->FindClass("java/lang/String");
266
231
  jobjectArray to_paths = env->NewObjectArray(
267
232
  static_cast<jsize>(group.to_paths.size()), string_class, nullptr);
268
233
  if (to_paths != nullptr) {
@@ -275,22 +240,20 @@ jobject NewCopyGroupResult(
275
240
  env->SetObjectField(result, to_paths_field, to_paths);
276
241
  env->DeleteLocalRef(to_paths);
277
242
  }
243
+ } else {
244
+ env->ExceptionClear();
278
245
  }
279
246
 
280
247
  return result;
281
248
  }
282
249
 
283
- pushy::archive_patch::ArchivePatchType ToArchivePatchType(jint patch_type) {
284
- switch (patch_type) {
285
- case 1:
286
- return pushy::archive_patch::ArchivePatchType::kFull;
287
- case 2:
288
- return pushy::archive_patch::ArchivePatchType::kPatchFromPackage;
289
- case 3:
290
- return pushy::archive_patch::ArchivePatchType::kPatchFromPpk;
291
- default:
292
- return pushy::archive_patch::ArchivePatchType::kFull;
293
- }
250
+ bool ToArchivePatchType(
251
+ jint patch_type,
252
+ pushy::archive_patch::ArchivePatchType* out) {
253
+ // Delegate to the shared parser so Android and HarmonyOS agree on which
254
+ // integer values are valid and neither silently falls back to kFull.
255
+ return pushy::archive_patch::TryParseArchivePatchType(
256
+ static_cast<int>(patch_type), out);
294
257
  }
295
258
 
296
259
  pushy::patch::PatchManifest BuildManifest(
@@ -411,9 +374,14 @@ Java_cn_reactnative_modules_update_DownloadTask_buildArchivePatchPlan(
411
374
 
412
375
  pushy::patch::PatchManifest manifest =
413
376
  BuildManifest(from_values, to_values, JArrayToVector(env, deletes));
377
+ pushy::archive_patch::ArchivePatchType archive_type;
378
+ if (!ToArchivePatchType(patch_type, &archive_type)) {
379
+ ThrowRuntimeException(env, "Unknown archive patch type");
380
+ return nullptr;
381
+ }
414
382
  pushy::archive_patch::ArchivePatchPlan plan;
415
383
  pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
416
- ToArchivePatchType(patch_type),
384
+ archive_type,
417
385
  manifest,
418
386
  JArrayToVector(env, entry_names),
419
387
  &plan);
@@ -454,6 +422,11 @@ Java_cn_reactnative_modules_update_DownloadTask_buildCopyGroups(
454
422
  return nullptr;
455
423
  }
456
424
 
425
+ jclass string_class = env->FindClass("java/lang/String");
426
+ if (string_class == nullptr) {
427
+ return nullptr;
428
+ }
429
+
457
430
  jobjectArray result = env->NewObjectArray(
458
431
  static_cast<jsize>(groups.size()), result_class, nullptr);
459
432
  if (result == nullptr) {
@@ -461,9 +434,10 @@ Java_cn_reactnative_modules_update_DownloadTask_buildCopyGroups(
461
434
  }
462
435
 
463
436
  for (jsize index = 0; index < static_cast<jsize>(groups.size()); ++index) {
464
- jobject group = NewCopyGroupResult(env, groups[index]);
437
+ jobject group = NewCopyGroupResult(env, result_class, string_class, groups[index]);
465
438
  env->SetObjectArrayElement(result, index, group);
466
439
  env->DeleteLocalRef(group);
467
440
  }
441
+ env->DeleteLocalRef(string_class);
468
442
  return result;
469
443
  }
@@ -8,6 +8,7 @@
8
8
  #include "archive_patch_core.h"
9
9
  #include "patch_core.h"
10
10
  #include "state_core.h"
11
+ #include "state_ops.h"
11
12
 
12
13
  extern "C" {
13
14
  #include "hpatch.h"
@@ -15,14 +16,7 @@ extern "C" {
15
16
 
16
17
  namespace {
17
18
 
18
- enum class StateOperation {
19
- kSwitchVersion = 1,
20
- kMarkSuccess = 2,
21
- kRollback = 3,
22
- kClearFirstTime = 4,
23
- kClearRollbackMark = 5,
24
- kResolveLaunch = 6,
25
- };
19
+ using pushy::state_ops::StateOperation;
26
20
 
27
21
  constexpr const char* kDefaultBundlePatchEntryName = "index.bundlejs.patch";
28
22
 
@@ -670,9 +664,14 @@ napi_value BuildArchivePatchPlan(napi_env env, napi_callback_info info) {
670
664
 
671
665
  const pushy::patch::PatchManifest manifest =
672
666
  BuildManifest(copy_froms, copy_tos, deletes);
667
+ pushy::archive_patch::ArchivePatchType archive_type;
668
+ if (!pushy::archive_patch::TryParseArchivePatchType(patch_type, &archive_type)) {
669
+ ThrowError(env, "Unknown archive patch type");
670
+ return nullptr;
671
+ }
673
672
  pushy::archive_patch::ArchivePatchPlan plan;
674
673
  const pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
675
- static_cast<pushy::archive_patch::ArchivePatchType>(patch_type),
674
+ archive_type,
676
675
  manifest,
677
676
  entry_names,
678
677
  &plan,
@@ -717,6 +716,47 @@ napi_value BuildCopyGroups(napi_env env, napi_callback_info info) {
717
716
  return NewCopyGroupArray(env, groups);
718
717
  }
719
718
 
719
+ // ---------------------------------------------------------------------------
720
+ // Async work plumbing for the heavy patch operations.
721
+ //
722
+ // applyPatchFromFileSource and cleanupOldEntries run hdiff / recursive file IO
723
+ // that can take hundreds of ms to seconds. The Pushy TurboModule executes on
724
+ // the UI thread, so running these synchronously froze the UI. These are now
725
+ // wrapped in napi_create_async_work: arguments are parsed on the JS thread, the
726
+ // heavy work runs on a libuv worker thread, and the returned Promise is settled
727
+ // back on the JS thread.
728
+ // ---------------------------------------------------------------------------
729
+
730
+ // Reject an already-created deferred with an Error(message). Used when async
731
+ // work fails to be created/queued, so the Promise never hangs pending.
732
+ void RejectDeferredWithMessage(
733
+ napi_env env,
734
+ napi_deferred deferred,
735
+ const char* message) {
736
+ napi_value error = nullptr;
737
+ napi_value message_value = nullptr;
738
+ napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &message_value);
739
+ napi_create_error(env, nullptr, message_value, &error);
740
+ napi_reject_deferred(env, deferred, error);
741
+ }
742
+
743
+ struct ApplyPatchWork {
744
+ napi_async_work work = nullptr;
745
+ napi_deferred deferred = nullptr;
746
+ pushy::patch::FileSourcePatchOptions options;
747
+ pushy::patch::Status status{false, ""};
748
+ };
749
+
750
+ struct CleanupWork {
751
+ napi_async_work work = nullptr;
752
+ napi_deferred deferred = nullptr;
753
+ std::string root_dir;
754
+ std::string keep_current;
755
+ std::string keep_previous;
756
+ int32_t max_age_days = 0;
757
+ pushy::patch::Status status{false, ""};
758
+ };
759
+
720
760
  napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
721
761
  napi_value args[1] = {nullptr};
722
762
  size_t argc = 1;
@@ -756,26 +796,69 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
756
796
  return nullptr;
757
797
  }
758
798
 
759
- pushy::patch::FileSourcePatchOptions options;
760
- options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
761
- options.source_root = source_root;
762
- options.target_root = target_root;
763
- options.origin_bundle_path = origin_bundle_path;
764
- options.bundle_patch_path = bundle_patch_path;
765
- options.bundle_output_path = bundle_output_path;
766
- options.merge_source_subdir = merge_source_subdir;
767
- options.enable_merge = enable_merge;
799
+ auto* work_data = new ApplyPatchWork();
800
+ work_data->options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
801
+ work_data->options.source_root = source_root;
802
+ work_data->options.target_root = target_root;
803
+ work_data->options.origin_bundle_path = origin_bundle_path;
804
+ work_data->options.bundle_patch_path = bundle_patch_path;
805
+ work_data->options.bundle_output_path = bundle_output_path;
806
+ work_data->options.merge_source_subdir = merge_source_subdir;
807
+ work_data->options.enable_merge = enable_merge;
768
808
 
769
- const pushy::patch::Status status =
770
- pushy::patch::ApplyPatchFromFileSource(options);
771
- if (!status.ok) {
772
- ThrowError(env, status.message);
809
+ napi_value promise = nullptr;
810
+ if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
811
+ delete work_data;
812
+ ThrowError(env, "Unable to create promise");
773
813
  return nullptr;
774
814
  }
775
815
 
776
- napi_value undefined_value = nullptr;
777
- napi_get_undefined(env, &undefined_value);
778
- return undefined_value;
816
+ napi_value resource_name = nullptr;
817
+ napi_create_string_utf8(
818
+ env, "applyPatchFromFileSource", NAPI_AUTO_LENGTH, &resource_name);
819
+ if (napi_create_async_work(
820
+ env,
821
+ nullptr,
822
+ resource_name,
823
+ [](napi_env, void* data) {
824
+ auto* w = static_cast<ApplyPatchWork*>(data);
825
+ w->status = pushy::patch::ApplyPatchFromFileSource(w->options);
826
+ },
827
+ [](napi_env cb_env, napi_status, void* data) {
828
+ auto* w = static_cast<ApplyPatchWork*>(data);
829
+ if (w->status.ok) {
830
+ napi_value undefined_value = nullptr;
831
+ napi_get_undefined(cb_env, &undefined_value);
832
+ napi_resolve_deferred(cb_env, w->deferred, undefined_value);
833
+ } else {
834
+ napi_value error = nullptr;
835
+ napi_value message = nullptr;
836
+ napi_create_string_utf8(
837
+ cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
838
+ napi_create_error(cb_env, nullptr, message, &error);
839
+ napi_reject_deferred(cb_env, w->deferred, error);
840
+ }
841
+ napi_delete_async_work(cb_env, w->work);
842
+ delete w;
843
+ },
844
+ work_data,
845
+ &work_data->work) != napi_ok) {
846
+ // Work was never created: settle the promise and free the data so it does
847
+ // not leak / hang pending forever.
848
+ RejectDeferredWithMessage(
849
+ env, work_data->deferred, "Unable to create async work");
850
+ delete work_data;
851
+ return promise;
852
+ }
853
+ if (napi_queue_async_work(env, work_data->work) != napi_ok) {
854
+ // Queued failed: the complete callback will never run, so clean up here.
855
+ napi_delete_async_work(env, work_data->work);
856
+ RejectDeferredWithMessage(
857
+ env, work_data->deferred, "Unable to queue async work");
858
+ delete work_data;
859
+ return promise;
860
+ }
861
+ return promise;
779
862
  }
780
863
 
781
864
  napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
@@ -804,19 +887,63 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
804
887
  return nullptr;
805
888
  }
806
889
 
807
- const pushy::patch::Status status = pushy::patch::CleanupOldEntries(
808
- root_dir,
809
- keep_current,
810
- keep_previous,
811
- max_age_days);
812
- if (!status.ok) {
813
- ThrowError(env, status.message);
890
+ auto* work_data = new CleanupWork();
891
+ work_data->root_dir = root_dir;
892
+ work_data->keep_current = keep_current;
893
+ work_data->keep_previous = keep_previous;
894
+ work_data->max_age_days = max_age_days;
895
+
896
+ napi_value promise = nullptr;
897
+ if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
898
+ delete work_data;
899
+ ThrowError(env, "Unable to create promise");
814
900
  return nullptr;
815
901
  }
816
902
 
817
- napi_value undefined_value = nullptr;
818
- napi_get_undefined(env, &undefined_value);
819
- return undefined_value;
903
+ napi_value resource_name = nullptr;
904
+ napi_create_string_utf8(
905
+ env, "cleanupOldEntries", NAPI_AUTO_LENGTH, &resource_name);
906
+ if (napi_create_async_work(
907
+ env,
908
+ nullptr,
909
+ resource_name,
910
+ [](napi_env, void* data) {
911
+ auto* w = static_cast<CleanupWork*>(data);
912
+ w->status = pushy::patch::CleanupOldEntries(
913
+ w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
914
+ },
915
+ [](napi_env cb_env, napi_status, void* data) {
916
+ auto* w = static_cast<CleanupWork*>(data);
917
+ if (w->status.ok) {
918
+ napi_value undefined_value = nullptr;
919
+ napi_get_undefined(cb_env, &undefined_value);
920
+ napi_resolve_deferred(cb_env, w->deferred, undefined_value);
921
+ } else {
922
+ napi_value error = nullptr;
923
+ napi_value message = nullptr;
924
+ napi_create_string_utf8(
925
+ cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
926
+ napi_create_error(cb_env, nullptr, message, &error);
927
+ napi_reject_deferred(cb_env, w->deferred, error);
928
+ }
929
+ napi_delete_async_work(cb_env, w->work);
930
+ delete w;
931
+ },
932
+ work_data,
933
+ &work_data->work) != napi_ok) {
934
+ RejectDeferredWithMessage(
935
+ env, work_data->deferred, "Unable to create async work");
936
+ delete work_data;
937
+ return promise;
938
+ }
939
+ if (napi_queue_async_work(env, work_data->work) != napi_ok) {
940
+ napi_delete_async_work(env, work_data->work);
941
+ RejectDeferredWithMessage(
942
+ env, work_data->deferred, "Unable to queue async work");
943
+ delete work_data;
944
+ return promise;
945
+ }
946
+ return promise;
820
947
  }
821
948
 
822
949
  bool ExportFunction(
@@ -233,7 +233,7 @@ export class DownloadTask {
233
233
  const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
234
234
  try {
235
235
  await this.writeFileContent(originBundlePath, originContent);
236
- NativePatchCore.applyPatchFromFileSource({
236
+ await NativePatchCore.applyPatchFromFileSource({
237
237
  copyFroms: [],
238
238
  copyTos: [],
239
239
  deletes: [],
@@ -308,6 +308,26 @@ export class DownloadTask {
308
308
  let received = 0;
309
309
  let writeError: Error | null = null;
310
310
  let writeQueue = Promise.resolve();
311
+ let lastReportedPercentage = -1;
312
+ let lastReportedBytes = 0;
313
+
314
+ // Emit at most one progress event per whole-percent change (or per 256KB
315
+ // when the total is unknown), and only from the dataReceive handler, so the
316
+ // two HarmonyOS callbacks don't each fire an event per chunk.
317
+ const reportProgress = () => {
318
+ if (contentLength > 0) {
319
+ const percentage = Math.round((received * 100) / contentLength);
320
+ if (percentage <= lastReportedPercentage) {
321
+ return;
322
+ }
323
+ lastReportedPercentage = percentage;
324
+ } else if (received - lastReportedBytes < 256 * 1024) {
325
+ return;
326
+ } else {
327
+ lastReportedBytes = received;
328
+ }
329
+ this.onProgressUpdate(received, contentLength);
330
+ };
311
331
 
312
332
  const closeWriter = async () => {
313
333
  if (writer) {
@@ -316,8 +336,37 @@ export class DownloadTask {
316
336
  }
317
337
  };
318
338
 
339
+ // Watchdog: reject the download if no data is received for a while, so a
340
+ // stalled transfer after requestInStream resolves cannot hang the download
341
+ // Promise (and the JS caller) forever.
342
+ const INACTIVITY_TIMEOUT_MS = 60000;
343
+ let watchdogTimer: number | null = null;
344
+ let inactivityReject: ((error: Error) => void) | null = null;
345
+ const clearWatchdog = () => {
346
+ if (watchdogTimer !== null) {
347
+ clearTimeout(watchdogTimer);
348
+ watchdogTimer = null;
349
+ }
350
+ };
351
+ const refreshWatchdog = () => {
352
+ clearWatchdog();
353
+ watchdogTimer = setTimeout(() => {
354
+ if (inactivityReject) {
355
+ inactivityReject(
356
+ Error(
357
+ `Download stalled: no data received for ${INACTIVITY_TIMEOUT_MS}ms`,
358
+ ),
359
+ );
360
+ }
361
+ }, INACTIVITY_TIMEOUT_MS);
362
+ };
363
+ const inactivityPromise = new Promise<void>((_, reject) => {
364
+ inactivityReject = reject;
365
+ });
366
+
319
367
  const dataEndPromise = new Promise<void>((resolve, reject) => {
320
368
  httpRequest.on('dataEnd', () => {
369
+ clearWatchdog();
321
370
  writeQueue
322
371
  .then(async () => {
323
372
  if (writeError) {
@@ -363,6 +412,7 @@ export class DownloadTask {
363
412
  });
364
413
 
365
414
  httpRequest.on('dataReceive', (data: ArrayBuffer) => {
415
+ refreshWatchdog();
366
416
  if (writeError) {
367
417
  return;
368
418
  }
@@ -377,22 +427,21 @@ export class DownloadTask {
377
427
  writeError = error as Error;
378
428
  }
379
429
  });
380
- this.onProgressUpdate(received, contentLength);
430
+ reportProgress();
381
431
  });
382
432
 
383
433
  httpRequest.on(
384
434
  'dataReceiveProgress',
385
435
  (data: http.DataReceiveProgressInfo) => {
436
+ // Only refine the known total here; progress events are emitted from
437
+ // the dataReceive handler to avoid double-firing.
386
438
  if (data.totalSize > 0) {
387
439
  contentLength = data.totalSize;
388
440
  }
389
- if (data.receiveSize > received) {
390
- received = data.receiveSize;
391
- }
392
- this.onProgressUpdate(received, contentLength);
393
441
  },
394
442
  );
395
443
 
444
+ refreshWatchdog();
396
445
  const responseCode = await httpRequest.requestInStream(params.url, {
397
446
  method: http.RequestMethod.GET,
398
447
  readTimeout: 60000,
@@ -405,7 +454,7 @@ export class DownloadTask {
405
454
  throw Error(`Server error: ${responseCode}`);
406
455
  }
407
456
 
408
- await dataEndPromise;
457
+ await Promise.race([dataEndPromise, inactivityPromise]);
409
458
  const stats = await fileIo.stat(params.targetFile);
410
459
  const fileSize = stats.size;
411
460
  if (contentLength > 0 && fileSize !== contentLength) {
@@ -417,6 +466,7 @@ export class DownloadTask {
417
466
  console.error('Download failed:', error);
418
467
  throw error;
419
468
  } finally {
469
+ clearWatchdog();
420
470
  try {
421
471
  await closeWriter();
422
472
  } catch (closeError) {
@@ -518,7 +568,7 @@ export class DownloadTask {
518
568
  manifestArrays.deletes,
519
569
  HARMONY_BUNDLE_PATCH_ENTRY,
520
570
  );
521
- NativePatchCore.applyPatchFromFileSource({
571
+ await NativePatchCore.applyPatchFromFileSource({
522
572
  copyFroms: manifestArrays.copyFroms,
523
573
  copyTos: manifestArrays.copyTos,
524
574
  deletes: manifestArrays.deletes,
@@ -614,7 +664,7 @@ export class DownloadTask {
614
664
 
615
665
  private async doCleanUp(params: DownloadTaskParams): Promise<void> {
616
666
  try {
617
- NativePatchCore.cleanupOldEntries(
667
+ await NativePatchCore.cleanupOldEntries(
618
668
  params.unzipDirectory,
619
669
  params.hash || '',
620
670
  params.originHash || '',
@@ -75,13 +75,13 @@ interface NativePatchCoreBindings {
75
75
  bundlePatchEntryName?: string,
76
76
  ): ArchivePatchPlanResult;
77
77
  buildCopyGroups(copyFroms: string[], copyTos: string[]): CopyGroupResult[];
78
- applyPatchFromFileSource(options: FileSourcePatchRequest): void;
78
+ applyPatchFromFileSource(options: FileSourcePatchRequest): Promise<void>;
79
79
  cleanupOldEntries(
80
80
  rootDir: string,
81
81
  keepCurrent: string,
82
82
  keepPrevious: string,
83
83
  maxAgeDays: number,
84
- ): void;
84
+ ): Promise<void>;
85
85
  }
86
86
 
87
87
  export default NativeUpdateCore as unknown as NativePatchCoreBindings;
@@ -93,23 +93,23 @@ export class PushyTurboModule extends UITurboModule {
93
93
  }
94
94
  } else {
95
95
  logger.debug(TAG, 'reloadBridge via restartAbility (release mode)');
96
- let restarted = false;
96
+ // If the process truly restarts, this timer dies with it. It only fires
97
+ // when the app is still alive after 1.5s — i.e. restartApp resolved but
98
+ // was silently suppressed (HarmonyOS rate-limits restarts within a few
99
+ // seconds of cold start / of a previous call) — which is exactly when the
100
+ // soft reload must take over. So the timer is NOT cleared on the success
101
+ // path, only in the catch branch where the soft reload runs immediately.
97
102
  const fallbackTimer = setTimeout(() => {
98
- if (!restarted) {
99
- logger.warn(TAG, 'restartAbility did not restart the app within 1.5s, triggering soft reload fallback');
100
- const devToolsController = (this.ctx as Record<string, any>).devToolsController;
101
- if (devToolsController) {
102
- devToolsController.eventEmitter.emit("RELOAD", { reason: 'HotReload2' });
103
- }
103
+ logger.warn(TAG, 'restartAbility did not restart the app within 1.5s, triggering soft reload fallback');
104
+ const devToolsController = (this.ctx as Record<string, any>).devToolsController;
105
+ if (devToolsController) {
106
+ devToolsController.eventEmitter.emit("RELOAD", { reason: 'HotReload2' });
104
107
  }
105
108
  }, 1500);
106
109
 
107
110
  try {
108
111
  await this.restartAbility();
109
- restarted = true;
110
- clearTimeout(fallbackTimer);
111
112
  } catch (error) {
112
- restarted = false;
113
113
  clearTimeout(fallbackTimer);
114
114
  logger.error(TAG, `restartAbility failed: ${getErrorMessage(error)}, triggering soft reload fallback`);
115
115
  const devToolsController = (this.ctx as Record<string, any>).devToolsController;