react-native-update 10.43.3 → 10.45.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 (41) hide show
  1. package/android/jni/Android.mk +0 -1
  2. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  3. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  4. package/android/lib/x86/librnupdate.so +0 -0
  5. package/android/lib/x86_64/librnupdate.so +0 -0
  6. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +24 -12
  7. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +33 -9
  8. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
  9. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
  10. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +8 -4
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +8 -8
  13. package/cpp/patch_core/archive_patch_core.cpp +16 -0
  14. package/cpp/patch_core/archive_patch_core.h +5 -0
  15. package/cpp/patch_core/jni_util.h +56 -0
  16. package/cpp/patch_core/patch_core.cpp +16 -1
  17. package/cpp/patch_core/patch_core.h +6 -0
  18. package/cpp/patch_core/patch_core_android.cpp +4 -37
  19. package/cpp/patch_core/state_ops.h +24 -0
  20. package/cpp/patch_core/tests/patch_core_test.cpp +109 -2
  21. package/cpp/patch_core/update_core_android.cpp +43 -69
  22. package/harmony/pushy/src/main/cpp/pushy.cpp +163 -128
  23. package/harmony/pushy/src/main/ets/DownloadTask.ts +72 -19
  24. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -6
  25. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +10 -10
  26. package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
  27. package/harmony/pushy.har +0 -0
  28. package/ios/RCTPushy/RCTPushy.mm +142 -111
  29. package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
  30. package/package.json +7 -3
  31. package/src/client.ts +161 -101
  32. package/src/context.ts +21 -7
  33. package/src/core.ts +5 -1
  34. package/src/index.ts +7 -1
  35. package/src/provider.tsx +91 -56
  36. package/src/utils.ts +58 -18
  37. package/android/jni/DownloadTask.c +0 -56
  38. package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
  39. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  40. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  41. package/src/__tests__/setup.ts +0 -44
@@ -8,21 +8,14 @@
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
- #include "hpatch.h"
14
14
  }
15
15
 
16
16
  namespace {
17
17
 
18
- enum class StateOperation {
19
- kSwitchVersion = 1,
20
- kMarkSuccess = 2,
21
- kRollback = 3,
22
- kClearFirstTime = 4,
23
- kClearRollbackMark = 5,
24
- kResolveLaunch = 6,
25
- };
18
+ using pushy::state_ops::StateOperation;
26
19
 
27
20
  constexpr const char* kDefaultBundlePatchEntryName = "index.bundlejs.patch";
28
21
 
@@ -413,96 +406,6 @@ pushy::patch::PatchManifest BuildManifest(
413
406
  return manifest;
414
407
  }
415
408
 
416
- napi_value HdiffPatch(napi_env env, napi_callback_info info) {
417
- napi_value args[2] = {nullptr, nullptr};
418
- size_t argc = 2;
419
- if (!GetArgCount(env, info, &argc, args) || argc < 2) {
420
- ThrowError(env, "Wrong number of arguments");
421
- return nullptr;
422
- }
423
-
424
- bool is_typed_array = false;
425
- if (napi_is_typedarray(env, args[0], &is_typed_array) != napi_ok || !is_typed_array) {
426
- ThrowError(env, "First argument must be a TypedArray");
427
- return nullptr;
428
- }
429
-
430
- uint8_t* origin_ptr = nullptr;
431
- size_t origin_length = 0;
432
- if (napi_get_typedarray_info(
433
- env,
434
- args[0],
435
- nullptr,
436
- &origin_length,
437
- reinterpret_cast<void**>(&origin_ptr),
438
- nullptr,
439
- nullptr) != napi_ok) {
440
- ThrowError(env, "Failed to get origin buffer");
441
- return nullptr;
442
- }
443
-
444
- if (napi_is_typedarray(env, args[1], &is_typed_array) != napi_ok || !is_typed_array) {
445
- ThrowError(env, "Second argument must be a TypedArray");
446
- return nullptr;
447
- }
448
-
449
- uint8_t* patch_ptr = nullptr;
450
- size_t patch_length = 0;
451
- if (napi_get_typedarray_info(
452
- env,
453
- args[1],
454
- nullptr,
455
- &patch_length,
456
- reinterpret_cast<void**>(&patch_ptr),
457
- nullptr,
458
- nullptr) != napi_ok) {
459
- ThrowError(env, "Failed to get patch buffer");
460
- return nullptr;
461
- }
462
-
463
- hpatch_singleCompressedDiffInfo patch_info;
464
- if (!((origin_length == 0) || origin_ptr) || !patch_ptr || patch_length == 0) {
465
- ThrowError(env, "Corrupt patch");
466
- return nullptr;
467
- }
468
- if (kHPatch_ok != hpatch_getInfo_by_mem(&patch_info, patch_ptr, patch_length)) {
469
- ThrowError(env, "Error info in hpatch");
470
- return nullptr;
471
- }
472
- if (origin_length != patch_info.oldDataSize) {
473
- ThrowError(env, "Error oldDataSize in hpatch");
474
- return nullptr;
475
- }
476
-
477
- size_t new_size = static_cast<size_t>(patch_info.newDataSize);
478
- if (sizeof(size_t) != sizeof(hpatch_StreamPos_t) &&
479
- new_size != patch_info.newDataSize) {
480
- ThrowError(env, "Error newDataSize in hpatch");
481
- return nullptr;
482
- }
483
-
484
- void* output_data = nullptr;
485
- napi_value result = nullptr;
486
- if (napi_create_arraybuffer(env, new_size, &output_data, &result) != napi_ok) {
487
- ThrowError(env, "Failed to create result buffer");
488
- return nullptr;
489
- }
490
-
491
- if (kHPatch_ok != hpatch_by_mem(
492
- origin_ptr,
493
- origin_length,
494
- static_cast<uint8_t*>(output_data),
495
- new_size,
496
- patch_ptr,
497
- patch_length,
498
- &patch_info)) {
499
- ThrowError(env, "hpatch");
500
- return nullptr;
501
- }
502
-
503
- return result;
504
- }
505
-
506
409
  napi_value SyncStateWithBinaryVersion(napi_env env, napi_callback_info info) {
507
410
  napi_value args[3] = {nullptr, nullptr, nullptr};
508
411
  size_t argc = 3;
@@ -670,9 +573,14 @@ napi_value BuildArchivePatchPlan(napi_env env, napi_callback_info info) {
670
573
 
671
574
  const pushy::patch::PatchManifest manifest =
672
575
  BuildManifest(copy_froms, copy_tos, deletes);
576
+ pushy::archive_patch::ArchivePatchType archive_type;
577
+ if (!pushy::archive_patch::TryParseArchivePatchType(patch_type, &archive_type)) {
578
+ ThrowError(env, "Unknown archive patch type");
579
+ return nullptr;
580
+ }
673
581
  pushy::archive_patch::ArchivePatchPlan plan;
674
582
  const pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
675
- static_cast<pushy::archive_patch::ArchivePatchType>(patch_type),
583
+ archive_type,
676
584
  manifest,
677
585
  entry_names,
678
586
  &plan,
@@ -717,6 +625,47 @@ napi_value BuildCopyGroups(napi_env env, napi_callback_info info) {
717
625
  return NewCopyGroupArray(env, groups);
718
626
  }
719
627
 
628
+ // ---------------------------------------------------------------------------
629
+ // Async work plumbing for the heavy patch operations.
630
+ //
631
+ // applyPatchFromFileSource and cleanupOldEntries run hdiff / recursive file IO
632
+ // that can take hundreds of ms to seconds. The Pushy TurboModule executes on
633
+ // the UI thread, so running these synchronously froze the UI. These are now
634
+ // wrapped in napi_create_async_work: arguments are parsed on the JS thread, the
635
+ // heavy work runs on a libuv worker thread, and the returned Promise is settled
636
+ // back on the JS thread.
637
+ // ---------------------------------------------------------------------------
638
+
639
+ // Reject an already-created deferred with an Error(message). Used when async
640
+ // work fails to be created/queued, so the Promise never hangs pending.
641
+ void RejectDeferredWithMessage(
642
+ napi_env env,
643
+ napi_deferred deferred,
644
+ const char* message) {
645
+ napi_value error = nullptr;
646
+ napi_value message_value = nullptr;
647
+ napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &message_value);
648
+ napi_create_error(env, nullptr, message_value, &error);
649
+ napi_reject_deferred(env, deferred, error);
650
+ }
651
+
652
+ struct ApplyPatchWork {
653
+ napi_async_work work = nullptr;
654
+ napi_deferred deferred = nullptr;
655
+ pushy::patch::FileSourcePatchOptions options;
656
+ pushy::patch::Status status{false, ""};
657
+ };
658
+
659
+ struct CleanupWork {
660
+ napi_async_work work = nullptr;
661
+ napi_deferred deferred = nullptr;
662
+ std::string root_dir;
663
+ std::string keep_current;
664
+ std::string keep_previous;
665
+ int32_t max_age_days = 0;
666
+ pushy::patch::Status status{false, ""};
667
+ };
668
+
720
669
  napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
721
670
  napi_value args[1] = {nullptr};
722
671
  size_t argc = 1;
@@ -756,26 +705,69 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
756
705
  return nullptr;
757
706
  }
758
707
 
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;
708
+ auto* work_data = new ApplyPatchWork();
709
+ work_data->options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
710
+ work_data->options.source_root = source_root;
711
+ work_data->options.target_root = target_root;
712
+ work_data->options.origin_bundle_path = origin_bundle_path;
713
+ work_data->options.bundle_patch_path = bundle_patch_path;
714
+ work_data->options.bundle_output_path = bundle_output_path;
715
+ work_data->options.merge_source_subdir = merge_source_subdir;
716
+ work_data->options.enable_merge = enable_merge;
768
717
 
769
- const pushy::patch::Status status =
770
- pushy::patch::ApplyPatchFromFileSource(options);
771
- if (!status.ok) {
772
- ThrowError(env, status.message);
718
+ napi_value promise = nullptr;
719
+ if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
720
+ delete work_data;
721
+ ThrowError(env, "Unable to create promise");
773
722
  return nullptr;
774
723
  }
775
724
 
776
- napi_value undefined_value = nullptr;
777
- napi_get_undefined(env, &undefined_value);
778
- return undefined_value;
725
+ napi_value resource_name = nullptr;
726
+ napi_create_string_utf8(
727
+ env, "applyPatchFromFileSource", NAPI_AUTO_LENGTH, &resource_name);
728
+ if (napi_create_async_work(
729
+ env,
730
+ nullptr,
731
+ resource_name,
732
+ [](napi_env, void* data) {
733
+ auto* w = static_cast<ApplyPatchWork*>(data);
734
+ w->status = pushy::patch::ApplyPatchFromFileSource(w->options);
735
+ },
736
+ [](napi_env cb_env, napi_status, void* data) {
737
+ auto* w = static_cast<ApplyPatchWork*>(data);
738
+ if (w->status.ok) {
739
+ napi_value undefined_value = nullptr;
740
+ napi_get_undefined(cb_env, &undefined_value);
741
+ napi_resolve_deferred(cb_env, w->deferred, undefined_value);
742
+ } else {
743
+ napi_value error = nullptr;
744
+ napi_value message = nullptr;
745
+ napi_create_string_utf8(
746
+ cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
747
+ napi_create_error(cb_env, nullptr, message, &error);
748
+ napi_reject_deferred(cb_env, w->deferred, error);
749
+ }
750
+ napi_delete_async_work(cb_env, w->work);
751
+ delete w;
752
+ },
753
+ work_data,
754
+ &work_data->work) != napi_ok) {
755
+ // Work was never created: settle the promise and free the data so it does
756
+ // not leak / hang pending forever.
757
+ RejectDeferredWithMessage(
758
+ env, work_data->deferred, "Unable to create async work");
759
+ delete work_data;
760
+ return promise;
761
+ }
762
+ if (napi_queue_async_work(env, work_data->work) != napi_ok) {
763
+ // Queued failed: the complete callback will never run, so clean up here.
764
+ napi_delete_async_work(env, work_data->work);
765
+ RejectDeferredWithMessage(
766
+ env, work_data->deferred, "Unable to queue async work");
767
+ delete work_data;
768
+ return promise;
769
+ }
770
+ return promise;
779
771
  }
780
772
 
781
773
  napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
@@ -804,19 +796,63 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
804
796
  return nullptr;
805
797
  }
806
798
 
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);
799
+ auto* work_data = new CleanupWork();
800
+ work_data->root_dir = root_dir;
801
+ work_data->keep_current = keep_current;
802
+ work_data->keep_previous = keep_previous;
803
+ work_data->max_age_days = max_age_days;
804
+
805
+ napi_value promise = nullptr;
806
+ if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
807
+ delete work_data;
808
+ ThrowError(env, "Unable to create promise");
814
809
  return nullptr;
815
810
  }
816
811
 
817
- napi_value undefined_value = nullptr;
818
- napi_get_undefined(env, &undefined_value);
819
- return undefined_value;
812
+ napi_value resource_name = nullptr;
813
+ napi_create_string_utf8(
814
+ env, "cleanupOldEntries", NAPI_AUTO_LENGTH, &resource_name);
815
+ if (napi_create_async_work(
816
+ env,
817
+ nullptr,
818
+ resource_name,
819
+ [](napi_env, void* data) {
820
+ auto* w = static_cast<CleanupWork*>(data);
821
+ w->status = pushy::patch::CleanupOldEntries(
822
+ w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
823
+ },
824
+ [](napi_env cb_env, napi_status, void* data) {
825
+ auto* w = static_cast<CleanupWork*>(data);
826
+ if (w->status.ok) {
827
+ napi_value undefined_value = nullptr;
828
+ napi_get_undefined(cb_env, &undefined_value);
829
+ napi_resolve_deferred(cb_env, w->deferred, undefined_value);
830
+ } else {
831
+ napi_value error = nullptr;
832
+ napi_value message = nullptr;
833
+ napi_create_string_utf8(
834
+ cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
835
+ napi_create_error(cb_env, nullptr, message, &error);
836
+ napi_reject_deferred(cb_env, w->deferred, error);
837
+ }
838
+ napi_delete_async_work(cb_env, w->work);
839
+ delete w;
840
+ },
841
+ work_data,
842
+ &work_data->work) != napi_ok) {
843
+ RejectDeferredWithMessage(
844
+ env, work_data->deferred, "Unable to create async work");
845
+ delete work_data;
846
+ return promise;
847
+ }
848
+ if (napi_queue_async_work(env, work_data->work) != napi_ok) {
849
+ napi_delete_async_work(env, work_data->work);
850
+ RejectDeferredWithMessage(
851
+ env, work_data->deferred, "Unable to queue async work");
852
+ delete work_data;
853
+ return promise;
854
+ }
855
+ return promise;
820
856
  }
821
857
 
822
858
  bool ExportFunction(
@@ -841,8 +877,7 @@ bool ExportFunction(
841
877
  } // namespace
842
878
 
843
879
  napi_value Init(napi_env env, napi_value exports) {
844
- if (!ExportFunction(env, exports, "hdiffPatch", HdiffPatch) ||
845
- !ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
880
+ if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
846
881
  !ExportFunction(env, exports, "runStateCore", RunStateCore) ||
847
882
  !ExportFunction(env, exports, "buildArchivePatchPlan", BuildArchivePatchPlan) ||
848
883
  !ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
@@ -69,7 +69,7 @@ const FILE_COPY_BUFFER_SIZE = 64 * 1024;
69
69
 
70
70
  export class DownloadTask {
71
71
  private context: common.Context;
72
- private hash: string;
72
+ private hash = '';
73
73
  private eventHub: EventHub;
74
74
 
75
75
  constructor(context: common.Context) {
@@ -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: [],
@@ -244,7 +244,7 @@ export class DownloadTask {
244
244
  bundleOutputPath: outputFile,
245
245
  enableMerge: false,
246
246
  });
247
- } catch (error) {
247
+ } catch (error: any) {
248
248
  error.message = `Failed to process bundle patch: ${error.message}`;
249
249
  throw error;
250
250
  } finally {
@@ -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) {
@@ -346,23 +395,25 @@ export class DownloadTask {
346
395
  fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE,
347
396
  );
348
397
 
349
- httpRequest.on('headersReceive', (header: Record<string, string>) => {
398
+ httpRequest.on('headersReceive', (header: Object) => {
350
399
  if (!header) {
351
400
  return;
352
401
  }
353
- const lengthKey = Object.keys(header).find(
402
+ const headers = header as Record<string, string>;
403
+ const lengthKey = Object.keys(headers).find(
354
404
  key => key.toLowerCase() === 'content-length',
355
405
  );
356
406
  if (!lengthKey) {
357
407
  return;
358
408
  }
359
- const length = parseInt(header[lengthKey], 10);
409
+ const length = parseInt(headers[lengthKey], 10);
360
410
  if (!Number.isNaN(length)) {
361
411
  contentLength = length;
362
412
  }
363
413
  });
364
414
 
365
415
  httpRequest.on('dataReceive', (data: ArrayBuffer) => {
416
+ refreshWatchdog();
366
417
  if (writeError) {
367
418
  return;
368
419
  }
@@ -377,22 +428,21 @@ export class DownloadTask {
377
428
  writeError = error as Error;
378
429
  }
379
430
  });
380
- this.onProgressUpdate(received, contentLength);
431
+ reportProgress();
381
432
  });
382
433
 
383
434
  httpRequest.on(
384
435
  'dataReceiveProgress',
385
436
  (data: http.DataReceiveProgressInfo) => {
437
+ // Only refine the known total here; progress events are emitted from
438
+ // the dataReceive handler to avoid double-firing.
386
439
  if (data.totalSize > 0) {
387
440
  contentLength = data.totalSize;
388
441
  }
389
- if (data.receiveSize > received) {
390
- received = data.receiveSize;
391
- }
392
- this.onProgressUpdate(received, contentLength);
393
442
  },
394
443
  );
395
444
 
445
+ refreshWatchdog();
396
446
  const responseCode = await httpRequest.requestInStream(params.url, {
397
447
  method: http.RequestMethod.GET,
398
448
  readTimeout: 60000,
@@ -405,7 +455,7 @@ export class DownloadTask {
405
455
  throw Error(`Server error: ${responseCode}`);
406
456
  }
407
457
 
408
- await dataEndPromise;
458
+ await Promise.race([dataEndPromise, inactivityPromise]);
409
459
  const stats = await fileIo.stat(params.targetFile);
410
460
  const fileSize = stats.size;
411
461
  if (contentLength > 0 && fileSize !== contentLength) {
@@ -417,6 +467,7 @@ export class DownloadTask {
417
467
  console.error('Download failed:', error);
418
468
  throw error;
419
469
  } finally {
470
+ clearWatchdog();
420
471
  try {
421
472
  await closeWriter();
422
473
  } catch (closeError) {
@@ -518,7 +569,7 @@ export class DownloadTask {
518
569
  manifestArrays.deletes,
519
570
  HARMONY_BUNDLE_PATCH_ENTRY,
520
571
  );
521
- NativePatchCore.applyPatchFromFileSource({
572
+ await NativePatchCore.applyPatchFromFileSource({
522
573
  copyFroms: manifestArrays.copyFroms,
523
574
  copyTos: manifestArrays.copyTos,
524
575
  deletes: manifestArrays.deletes,
@@ -556,9 +607,11 @@ export class DownloadTask {
556
607
  }
557
608
 
558
609
  if (currentFrom.startsWith('resources/base/media/')) {
610
+ // Strip only the final extension: 'icon.round.png' -> 'icon.round',
611
+ // not 'icon' (getMediaByName expects the full resource name).
559
612
  const mediaName = currentFrom
560
613
  .replace('resources/base/media/', '')
561
- .split('.')[0];
614
+ .replace(/\.[^.]+$/, '');
562
615
  const mediaBuffer = await resourceManager.getMediaByName(mediaName);
563
616
  const parentDirs = [
564
617
  ...new Set(
@@ -599,7 +652,7 @@ export class DownloadTask {
599
652
  }
600
653
  }
601
654
  }
602
- } catch (error) {
655
+ } catch (error: any) {
603
656
  error.message =
604
657
  'Copy from resource failed:' +
605
658
  currentFrom +
@@ -614,13 +667,13 @@ export class DownloadTask {
614
667
 
615
668
  private async doCleanUp(params: DownloadTaskParams): Promise<void> {
616
669
  try {
617
- NativePatchCore.cleanupOldEntries(
670
+ await NativePatchCore.cleanupOldEntries(
618
671
  params.unzipDirectory,
619
672
  params.hash || '',
620
673
  params.originHash || '',
621
674
  3,
622
675
  );
623
- } catch (error) {
676
+ } catch (error: any) {
624
677
  error.message = 'Cleanup failed:' + error.message;
625
678
  console.error(error);
626
679
  throw error;
@@ -648,7 +701,7 @@ export class DownloadTask {
648
701
  default:
649
702
  throw Error(`Unknown task type: ${params.type}`);
650
703
  }
651
- } catch (error) {
704
+ } catch (error: any) {
652
705
  console.error('Task execution failed:', error.message);
653
706
  if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
654
707
  try {
@@ -657,7 +710,7 @@ export class DownloadTask {
657
710
  } else {
658
711
  await this.removeDirectory(params.unzipDirectory);
659
712
  }
660
- } catch (cleanupError) {
713
+ } catch (cleanupError: any) {
661
714
  console.error('Cleanup after error failed:', cleanupError.message);
662
715
  }
663
716
  }
@@ -50,10 +50,6 @@ export interface FileSourcePatchRequest {
50
50
  }
51
51
 
52
52
  interface NativePatchCoreBindings {
53
- hdiffPatch(
54
- origin: Uint8Array,
55
- patch: Uint8Array,
56
- ): ArrayBuffer | Uint8Array;
57
53
  syncStateWithBinaryVersion(
58
54
  packageVersion: string,
59
55
  buildTime: string,
@@ -75,13 +71,13 @@ interface NativePatchCoreBindings {
75
71
  bundlePatchEntryName?: string,
76
72
  ): ArchivePatchPlanResult;
77
73
  buildCopyGroups(copyFroms: string[], copyTos: string[]): CopyGroupResult[];
78
- applyPatchFromFileSource(options: FileSourcePatchRequest): void;
74
+ applyPatchFromFileSource(options: FileSourcePatchRequest): Promise<void>;
79
75
  cleanupOldEntries(
80
76
  rootDir: string,
81
77
  keepCurrent: string,
82
78
  keepPrevious: string,
83
79
  maxAgeDays: number,
84
- ): void;
80
+ ): Promise<void>;
85
81
  }
86
82
 
87
83
  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;