react-native-update 10.59.2 → 10.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +2 -7
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +17 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2634
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +73 -82
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
@@ -1,23 +1,27 @@
1
1
  #include <napi/native_api.h>
2
2
 
3
3
  #include "flow_json.h"
4
- #include "update_flow_core.h"
4
+ #include "native_config.h"
5
+ #include "native_round.h"
5
6
  #include <js_native_api.h>
6
7
  #include <js_native_api_types.h>
7
8
 
9
+ #include <cerrno>
10
+ #include <cstdint>
8
11
  #include <string>
12
+ #include <unistd.h>
13
+ #include <utility>
9
14
  #include <vector>
10
15
 
16
+ #include "archive_limits.h"
11
17
  #include "archive_patch_core.h"
12
18
  #include "digest.h"
19
+ #include "install_record.h"
13
20
  #include "hbc_transform_wire.h"
14
21
  #include "patch_core.h"
15
22
  #include "state_core.h"
16
23
  #include "state_ops.h"
17
24
 
18
- extern "C" {
19
- }
20
-
21
25
  namespace {
22
26
 
23
27
  using pushy::state_ops::StateOperation;
@@ -314,7 +318,11 @@ napi_value NewBoolean(napi_env env, bool value) {
314
318
 
315
319
  napi_value NewString(napi_env env, const std::string& value) {
316
320
  napi_value result = nullptr;
317
- napi_create_string_utf8(env, value.c_str(), value.size(), &result);
321
+ if (napi_create_string_utf8(env, value.c_str(), value.size(), &result) !=
322
+ napi_ok) {
323
+ ThrowError(env, "Failed to create string");
324
+ return nullptr;
325
+ }
318
326
  return result;
319
327
  }
320
328
 
@@ -337,14 +345,19 @@ void SetBoolProperty(
337
345
  napi_set_named_property(env, object, name, NewBoolean(env, value));
338
346
  }
339
347
 
348
+ // Per-operation outputs reported next to the state; all default to "nothing".
349
+ struct StateResultExtras {
350
+ bool changed = false;
351
+ std::string stale_version_to_delete;
352
+ std::string load_version;
353
+ bool did_rollback = false;
354
+ bool consumed_first_time = false;
355
+ };
356
+
340
357
  napi_value NewStateResult(
341
358
  napi_env env,
342
359
  const pushy::state::State& state,
343
- bool changed,
344
- const std::string& stale_version_to_delete,
345
- const std::string& load_version,
346
- bool did_rollback,
347
- bool consumed_first_time) {
360
+ const StateResultExtras& extras = StateResultExtras()) {
348
361
  napi_value result = nullptr;
349
362
  napi_create_object(env, &result);
350
363
 
@@ -355,21 +368,12 @@ napi_value NewStateResult(
355
368
  SetBoolProperty(env, result, "firstTime", state.first_time);
356
369
  SetBoolProperty(env, result, "firstTimeOk", state.first_time_ok);
357
370
  SetStringProperty(env, result, "rolledBackVersion", state.rolled_back_version);
358
- SetBoolProperty(env, result, "changed", changed);
359
- SetStringProperty(env, result, "staleVersionToDelete", stale_version_to_delete);
360
- SetStringProperty(env, result, "loadVersion", load_version);
361
- SetBoolProperty(env, result, "didRollback", did_rollback);
362
- SetBoolProperty(env, result, "consumedFirstTime", consumed_first_time);
363
- return result;
364
- }
365
-
366
- napi_value NewArchivePatchPlanResult(
367
- napi_env env,
368
- const pushy::archive_patch::ArchivePatchPlan& plan) {
369
- napi_value result = nullptr;
370
- napi_create_object(env, &result);
371
- SetStringProperty(env, result, "mergeSourceSubdir", plan.merge_source_subdir);
372
- SetBoolProperty(env, result, "enableMerge", plan.enable_merge);
371
+ SetBoolProperty(env, result, "changed", extras.changed);
372
+ SetStringProperty(
373
+ env, result, "staleVersionToDelete", extras.stale_version_to_delete);
374
+ SetStringProperty(env, result, "loadVersion", extras.load_version);
375
+ SetBoolProperty(env, result, "didRollback", extras.did_rollback);
376
+ SetBoolProperty(env, result, "consumedFirstTime", extras.consumed_first_time);
373
377
  return result;
374
378
  }
375
379
 
@@ -394,24 +398,18 @@ napi_value NewCopyGroupArray(
394
398
  NewString(env, groups[index].to_paths[target_index]));
395
399
  }
396
400
  napi_set_named_property(env, group, "toPaths", to_paths);
401
+ if (groups[index].has_expected_crc) {
402
+ // A CRC32 fits a double exactly, like the manifest's JSON number.
403
+ napi_value crc = nullptr;
404
+ napi_create_double(
405
+ env, static_cast<double>(groups[index].expected_crc), &crc);
406
+ napi_set_named_property(env, group, "expectedCrc", crc);
407
+ }
397
408
  napi_set_element(env, result, index, group);
398
409
  }
399
410
  return result;
400
411
  }
401
412
 
402
- pushy::delta::PatchManifest BuildManifest(
403
- const std::vector<std::string>& copy_froms,
404
- const std::vector<std::string>& copy_tos,
405
- const std::vector<std::string>& deletes) {
406
- pushy::delta::PatchManifest manifest;
407
- for (size_t index = 0; index < copy_froms.size(); ++index) {
408
- manifest.copies.push_back(
409
- pushy::delta::CopyOperation{copy_froms[index], copy_tos[index]});
410
- }
411
- manifest.deletes = deletes;
412
- return manifest;
413
- }
414
-
415
413
  napi_value SyncStateWithBinaryVersion(napi_env env, napi_callback_info info) {
416
414
  napi_value args[3] = {nullptr, nullptr, nullptr};
417
415
  size_t argc = 3;
@@ -436,7 +434,9 @@ napi_value SyncStateWithBinaryVersion(napi_env env, napi_callback_info info) {
436
434
 
437
435
  const pushy::state::BinaryVersionSyncResult result =
438
436
  pushy::state::SyncBinaryVersion(state, package_version, build_time);
439
- return NewStateResult(env, result.state, result.changed, std::string(), std::string(), false, false);
437
+ StateResultExtras extras;
438
+ extras.changed = result.changed;
439
+ return NewStateResult(env, result.state, extras);
440
440
  }
441
441
 
442
442
  napi_value RunStateCore(napi_env env, napi_callback_info info) {
@@ -477,59 +477,32 @@ napi_value RunStateCore(napi_env env, napi_callback_info info) {
477
477
 
478
478
  switch (static_cast<StateOperation>(operation)) {
479
479
  case StateOperation::kSwitchVersion:
480
- return NewStateResult(
481
- env,
482
- pushy::state::SwitchVersion(state, string_arg),
483
- false,
484
- std::string(),
485
- std::string(),
486
- false,
487
- false);
480
+ return NewStateResult(env, pushy::state::SwitchVersion(state, string_arg));
488
481
  case StateOperation::kMarkSuccess: {
489
482
  const pushy::state::MarkSuccessResult result = pushy::state::MarkSuccess(state);
490
- return NewStateResult(
491
- env,
492
- result.state,
493
- false,
494
- result.stale_version_to_delete,
495
- std::string(),
496
- false,
497
- false);
483
+ StateResultExtras extras;
484
+ extras.stale_version_to_delete = result.stale_version_to_delete;
485
+ return NewStateResult(env, result.state, extras);
498
486
  }
499
487
  case StateOperation::kRollback: {
500
488
  const pushy::state::State next = pushy::state::Rollback(state);
501
- return NewStateResult(
502
- env, next, false, std::string(), next.current_version, true, false);
489
+ StateResultExtras extras;
490
+ extras.load_version = next.current_version;
491
+ extras.did_rollback = true;
492
+ return NewStateResult(env, next, extras);
503
493
  }
504
494
  case StateOperation::kClearFirstTime:
505
- return NewStateResult(
506
- env,
507
- pushy::state::ClearFirstTime(state),
508
- false,
509
- std::string(),
510
- std::string(),
511
- false,
512
- false);
495
+ return NewStateResult(env, pushy::state::ClearFirstTime(state));
513
496
  case StateOperation::kClearRollbackMark:
514
- return NewStateResult(
515
- env,
516
- pushy::state::ClearRollbackMark(state),
517
- false,
518
- std::string(),
519
- std::string(),
520
- false,
521
- false);
497
+ return NewStateResult(env, pushy::state::ClearRollbackMark(state));
522
498
  case StateOperation::kResolveLaunch: {
523
499
  const pushy::state::LaunchDecision decision =
524
500
  pushy::state::ResolveLaunchState(state, flag_a, flag_b);
525
- return NewStateResult(
526
- env,
527
- decision.state,
528
- false,
529
- std::string(),
530
- decision.load_version,
531
- decision.did_rollback,
532
- decision.consumed_first_time);
501
+ StateResultExtras extras;
502
+ extras.load_version = decision.load_version;
503
+ extras.did_rollback = decision.did_rollback;
504
+ extras.consumed_first_time = decision.consumed_first_time;
505
+ return NewStateResult(env, decision.state, extras);
533
506
  }
534
507
  }
535
508
 
@@ -537,105 +510,10 @@ napi_value RunStateCore(napi_env env, napi_callback_info info) {
537
510
  return nullptr;
538
511
  }
539
512
 
540
- napi_value BuildArchivePatchPlan(napi_env env, napi_callback_info info) {
541
- napi_value args[6] = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
542
- size_t argc = 6;
543
- if (!GetArgCount(env, info, &argc, args) || argc < 5) {
544
- ThrowError(env, "Wrong number of arguments");
545
- return nullptr;
546
- }
547
-
548
- int32_t patch_type = 0;
549
- if (!GetInt32(env, args[0], &patch_type)) {
550
- return nullptr;
551
- }
552
-
553
- std::vector<std::string> entry_names;
554
- std::vector<std::string> copy_froms;
555
- std::vector<std::string> copy_tos;
556
- std::vector<std::string> deletes;
557
- if (!GetStringArray(env, args[1], &entry_names) ||
558
- !GetStringArray(env, args[2], &copy_froms) ||
559
- !GetStringArray(env, args[3], &copy_tos) ||
560
- !GetStringArray(env, args[4], &deletes)) {
561
- return nullptr;
562
- }
563
- if (copy_froms.size() != copy_tos.size()) {
564
- ThrowError(env, "copyFroms and copyTos length mismatch");
565
- return nullptr;
566
- }
567
-
568
- std::string bundle_patch_entry_name =
569
- pushy::archive_patch::DefaultBundleDeltaEntryName();
570
- if (argc >= 6) {
571
- bool ok = false;
572
- const std::string candidate = GetString(env, args[5], &ok);
573
- if (!ok) {
574
- return nullptr;
575
- }
576
- if (!candidate.empty()) {
577
- bundle_patch_entry_name = candidate;
578
- }
579
- }
580
-
581
- const pushy::delta::PatchManifest manifest =
582
- BuildManifest(copy_froms, copy_tos, deletes);
583
- pushy::archive_patch::ArchivePatchType archive_type;
584
- if (!pushy::archive_patch::TryParseArchivePatchType(patch_type, &archive_type)) {
585
- ThrowError(env, "Unknown archive delta type");
586
- return nullptr;
587
- }
588
- pushy::archive_patch::ArchivePatchPlan plan;
589
- const pushy::delta::Status status = pushy::archive_patch::BuildArchivePatchPlan(
590
- archive_type,
591
- manifest,
592
- entry_names,
593
- &plan,
594
- bundle_patch_entry_name);
595
- if (!status.ok) {
596
- ThrowError(env, status.message);
597
- return nullptr;
598
- }
599
-
600
- return NewArchivePatchPlanResult(env, plan);
601
- }
602
-
603
- napi_value BuildCopyGroups(napi_env env, napi_callback_info info) {
604
- napi_value args[2] = {nullptr, nullptr};
605
- size_t argc = 2;
606
- if (!GetArgCount(env, info, &argc, args) || argc < 2) {
607
- ThrowError(env, "Wrong number of arguments");
608
- return nullptr;
609
- }
610
-
611
- std::vector<std::string> copy_froms;
612
- std::vector<std::string> copy_tos;
613
- if (!GetStringArray(env, args[0], &copy_froms) ||
614
- !GetStringArray(env, args[1], &copy_tos)) {
615
- return nullptr;
616
- }
617
- if (copy_froms.size() != copy_tos.size()) {
618
- ThrowError(env, "copyFroms and copyTos length mismatch");
619
- return nullptr;
620
- }
621
-
622
- const pushy::delta::PatchManifest manifest =
623
- BuildManifest(copy_froms, copy_tos, std::vector<std::string>());
624
- std::vector<pushy::archive_patch::CopyGroup> groups;
625
- const pushy::delta::Status status =
626
- pushy::archive_patch::BuildCopyGroups(manifest, &groups);
627
- if (!status.ok) {
628
- ThrowError(env, status.message);
629
- return nullptr;
630
- }
631
-
632
- return NewCopyGroupArray(env, groups);
633
- }
634
-
635
513
  // ---------------------------------------------------------------------------
636
514
  // Async work plumbing for the heavy patch operations.
637
515
  //
638
- // applyDeltaFromSource and cleanupOldEntries run hdiff / recursive file IO
516
+ // applyArchiveDelta and cleanupOldEntries run hdiff / recursive file IO
639
517
  // that can take hundreds of ms to seconds. The Pushy TurboModule executes on
640
518
  // the UI thread, so running these synchronously froze the UI. These are now
641
519
  // wrapped in napi_create_async_work: arguments are parsed on the JS thread, the
@@ -643,8 +521,7 @@ napi_value BuildCopyGroups(napi_env env, napi_callback_info info) {
643
521
  // back on the JS thread.
644
522
  // ---------------------------------------------------------------------------
645
523
 
646
- // Reject an already-created deferred with an Error(message). Used when async
647
- // work fails to be created/queued, so the Promise never hangs pending.
524
+ // Reject an already-created deferred with an Error(message).
648
525
  void RejectDeferredWithMessage(
649
526
  napi_env env,
650
527
  napi_deferred deferred,
@@ -656,16 +533,102 @@ void RejectDeferredWithMessage(
656
533
  napi_reject_deferred(env, deferred, error);
657
534
  }
658
535
 
659
- struct ApplyPatchWork {
536
+ void ResolveDeferredWithUndefined(napi_env env, napi_deferred deferred) {
537
+ napi_value undefined_value = nullptr;
538
+ napi_get_undefined(env, &undefined_value);
539
+ napi_resolve_deferred(env, deferred, undefined_value);
540
+ }
541
+
542
+ // Resolves with undefined, or rejects with the status message.
543
+ void SettleWithStatus(
544
+ napi_env env,
545
+ napi_deferred deferred,
546
+ const pushy::delta::Status& status) {
547
+ if (status.ok) {
548
+ ResolveDeferredWithUndefined(env, deferred);
549
+ } else {
550
+ RejectDeferredWithMessage(env, deferred, status.message.c_str());
551
+ }
552
+ }
553
+
554
+ template <typename W>
555
+ struct AsyncCall {
660
556
  napi_async_work work = nullptr;
661
557
  napi_deferred deferred = nullptr;
662
- pushy::delta::FileSourcePatchOptions options;
558
+ W data;
559
+ void (*execute)(W&) = nullptr;
560
+ void (*settle)(napi_env, napi_deferred, const W&) = nullptr;
561
+ };
562
+
563
+ // Runs execute(data) on a libuv worker thread and returns a Promise that
564
+ // settle(env, deferred, data) settles back on the JS thread. Every path
565
+ // settles the promise exactly once and frees the call: a creation or queue
566
+ // failure rejects immediately, and work that was cancelled or aborted before
567
+ // it ran is rejected instead of settled from meaningless results.
568
+ template <typename W>
569
+ napi_value QueueWork(
570
+ napi_env env,
571
+ const char* name,
572
+ W data,
573
+ void (*execute)(W&),
574
+ void (*settle)(napi_env, napi_deferred, const W&)) {
575
+ auto* call = new AsyncCall<W>();
576
+ call->data = std::move(data);
577
+ call->execute = execute;
578
+ call->settle = settle;
579
+
580
+ napi_value promise = nullptr;
581
+ if (napi_create_promise(env, &call->deferred, &promise) != napi_ok) {
582
+ delete call;
583
+ ThrowError(env, "Unable to create promise");
584
+ return nullptr;
585
+ }
586
+
587
+ napi_value resource_name = nullptr;
588
+ napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource_name);
589
+ if (napi_create_async_work(
590
+ env,
591
+ nullptr,
592
+ resource_name,
593
+ [](napi_env, void* raw) {
594
+ auto* c = static_cast<AsyncCall<W>*>(raw);
595
+ c->execute(c->data);
596
+ },
597
+ [](napi_env cb_env, napi_status status, void* raw) {
598
+ auto* c = static_cast<AsyncCall<W>*>(raw);
599
+ if (status != napi_ok) {
600
+ RejectDeferredWithMessage(cb_env, c->deferred, "async work aborted");
601
+ } else {
602
+ c->settle(cb_env, c->deferred, c->data);
603
+ }
604
+ napi_delete_async_work(cb_env, c->work);
605
+ delete c;
606
+ },
607
+ call,
608
+ &call->work) != napi_ok) {
609
+ // Work was never created: settle the promise and free the call so it
610
+ // neither leaks nor hangs pending forever.
611
+ RejectDeferredWithMessage(env, call->deferred, "Unable to create async work");
612
+ delete call;
613
+ return promise;
614
+ }
615
+ if (napi_queue_async_work(env, call->work) != napi_ok) {
616
+ // Queueing failed: the complete callback will never run, so clean up here.
617
+ napi_delete_async_work(env, call->work);
618
+ RejectDeferredWithMessage(env, call->deferred, "Unable to queue async work");
619
+ delete call;
620
+ return promise;
621
+ }
622
+ return promise;
623
+ }
624
+
625
+ struct ApplyArchiveDeltaWork {
626
+ pushy::archive_patch::ArchivePatchRequest request;
627
+ std::vector<pushy::archive_patch::CopyGroup> host_copies;
663
628
  pushy::delta::Status status{false, ""};
664
629
  };
665
630
 
666
631
  struct CleanupWork {
667
- napi_async_work work = nullptr;
668
- napi_deferred deferred = nullptr;
669
632
  std::string root_dir;
670
633
  // Every version that may still be in use: persisted current/last AND the
671
634
  // version this process booted from (two switches without a restart must not
@@ -679,13 +642,76 @@ struct CleanupWork {
679
642
  // of MB; hashing it synchronously on the UI thread (where the TurboModule
680
643
  // runs) froze the app for hundreds of ms per archive/bundle/activation.
681
644
  struct Sha256FileWork {
682
- napi_async_work work = nullptr;
683
- napi_deferred deferred = nullptr;
684
645
  std::string path;
685
646
  std::string hex;
686
647
  };
687
648
 
688
- napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
649
+ // Digest of a byte range of an fd the caller keeps open (rawfile
650
+ // descriptors: the embedded bundle lives at [offset, offset + length) inside
651
+ // the HAP). The fd is never closed here; JS closes it after the promise
652
+ // settles.
653
+ struct Sha256RangeWork {
654
+ int fd = -1;
655
+ int64_t offset = 0;
656
+ int64_t length = 0;
657
+ std::string hex;
658
+ std::string error;
659
+ };
660
+
661
+ // pread loop (positioned: never moves a file offset the caller may share).
662
+ // A short read before `length` bytes is an error, not EOF.
663
+ void HashFdRange(Sha256RangeWork& w) {
664
+ pushy::digest::Sha256 hasher;
665
+ std::vector<uint8_t> buffer(256 * 1024);
666
+ int64_t position = w.offset;
667
+ int64_t remaining = w.length;
668
+ while (remaining > 0) {
669
+ const size_t want = static_cast<size_t>(
670
+ remaining < static_cast<int64_t>(buffer.size())
671
+ ? remaining
672
+ : static_cast<int64_t>(buffer.size()));
673
+ const ssize_t read = pread(w.fd, buffer.data(), want, static_cast<off_t>(position));
674
+ if (read < 0) {
675
+ if (errno == EINTR) {
676
+ continue;
677
+ }
678
+ w.error = "sha256HexRawFdAsync: read failed (errno " +
679
+ std::to_string(errno) + ")";
680
+ return;
681
+ }
682
+ if (read == 0) {
683
+ w.error = "sha256HexRawFdAsync: unexpected end of file";
684
+ return;
685
+ }
686
+ hasher.Update(buffer.data(), static_cast<size_t>(read));
687
+ position += read;
688
+ remaining -= read;
689
+ }
690
+ w.hex = hasher.HexDigest();
691
+ }
692
+
693
+ bool GetNonNegativeInt64(
694
+ napi_env env,
695
+ napi_value value,
696
+ const char* name,
697
+ int64_t* out_value) {
698
+ napi_valuetype type = napi_undefined;
699
+ if (value == nullptr || !GetValueType(env, value, &type) ||
700
+ type != napi_number ||
701
+ napi_get_value_int64(env, value, out_value) != napi_ok ||
702
+ *out_value < 0) {
703
+ ThrowError(env, std::string("sha256HexRawFdAsync: ") + name +
704
+ " must be a non-negative number");
705
+ return false;
706
+ }
707
+ return true;
708
+ }
709
+
710
+ // applyArchiveDelta(request) -> Promise<CopyGroupResult[]>. The manifest
711
+ // text is parsed (with the rest of the patch) on the worker thread; the
712
+ // promise resolves with the copies the caller still owes (empty unless
713
+ // hostCopiesResources) and rejects with the core's message.
714
+ napi_value ApplyArchiveDelta(napi_env env, napi_callback_info info) {
689
715
  napi_value args[1] = {nullptr};
690
716
  size_t argc = 1;
691
717
  if (!GetArgCount(env, info, &argc, args) || argc < 1) {
@@ -696,105 +722,60 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
696
722
  return nullptr;
697
723
  }
698
724
 
699
- std::vector<std::string> copy_froms;
700
- std::vector<std::string> copy_tos;
701
- std::vector<std::string> deletes;
702
- std::string source_root;
703
- std::string target_root;
704
- std::string origin_bundle_path;
705
- std::string bundle_patch_path;
706
- std::string bundle_output_path;
707
- std::string merge_source_subdir;
708
- std::string bundle_hbc_transform_meta;
709
- bool enable_merge = true;
710
-
711
- if (!GetOptionalStringArrayProperty(env, args[0], "copyFroms", &copy_froms) ||
712
- !GetOptionalStringArrayProperty(env, args[0], "copyTos", &copy_tos) ||
713
- !GetOptionalStringArrayProperty(env, args[0], "deletes", &deletes) ||
714
- !GetOptionalStringProperty(env, args[0], "sourceRoot", &source_root) ||
715
- !GetOptionalStringProperty(env, args[0], "targetRoot", &target_root) ||
716
- !GetOptionalStringProperty(env, args[0], "originBundlePath", &origin_bundle_path) ||
717
- !GetOptionalStringProperty(env, args[0], "bundleDeltaPath", &bundle_patch_path) ||
718
- !GetOptionalStringProperty(env, args[0], "bundleOutputPath", &bundle_output_path) ||
719
- !GetOptionalStringProperty(env, args[0], "mergeSourceSubdir", &merge_source_subdir) ||
725
+ ApplyArchiveDeltaWork work;
726
+ pushy::archive_patch::ArchivePatchRequest& request = work.request;
727
+ napi_value patch_type_value = nullptr;
728
+ int32_t patch_type = 0;
729
+ std::string bundle_patch_entry_name;
730
+ if (!GetNamedProperty(env, args[0], "archiveType", &patch_type_value) ||
731
+ !GetInt32(env, patch_type_value, &patch_type) ||
732
+ !GetOptionalStringArrayProperty(
733
+ env, args[0], "entryNames", &request.archive.entry_names) ||
734
+ !GetOptionalStringProperty(
735
+ env, args[0], "manifestJson", &request.archive.manifest_json) ||
736
+ !GetOptionalStringProperty(
737
+ env, args[0], "bundleDeltaEntry", &bundle_patch_entry_name) ||
720
738
  !GetOptionalStringProperty(
721
- env, args[0], "bundleHbcTransformMeta", &bundle_hbc_transform_meta) ||
722
- !GetOptionalBoolProperty(env, args[0], "enableMerge", true, &enable_merge)) {
739
+ env, args[0], "copySourcePrefix", &request.archive.copy_source_prefix) ||
740
+ !GetOptionalStringProperty(env, args[0], "sourceRoot", &request.source_root) ||
741
+ !GetOptionalStringProperty(env, args[0], "targetRoot", &request.target_root) ||
742
+ !GetOptionalStringProperty(
743
+ env, args[0], "originBundlePath", &request.origin_bundle_path) ||
744
+ !GetOptionalStringProperty(
745
+ env, args[0], "bundleDeltaPath", &request.bundle_patch_path) ||
746
+ !GetOptionalStringProperty(
747
+ env, args[0], "bundleOutputPath", &request.bundle_output_path) ||
748
+ !GetOptionalBoolProperty(
749
+ env, args[0], "hostCopiesResources", false,
750
+ &request.host_copies_resources)) {
723
751
  return nullptr;
724
752
  }
725
- if (copy_froms.size() != copy_tos.size()) {
726
- ThrowError(env, "copyFroms and copyTos length mismatch");
753
+ // The shared parser keeps Android and HarmonyOS in agreement on which
754
+ // integer values are valid; neither silently falls back to kFull.
755
+ if (!pushy::archive_patch::TryParseArchivePatchType(
756
+ patch_type, &request.archive.type)) {
757
+ ThrowError(env, "Unknown archive delta type");
727
758
  return nullptr;
728
759
  }
729
-
730
- auto* work_data = new ApplyPatchWork();
731
- work_data->options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
732
- work_data->options.source_root = source_root;
733
- work_data->options.target_root = target_root;
734
- work_data->options.origin_bundle_path = origin_bundle_path;
735
- work_data->options.bundle_patch_path = bundle_patch_path;
736
- work_data->options.bundle_output_path = bundle_output_path;
737
- work_data->options.merge_source_subdir = merge_source_subdir;
738
- work_data->options.bundle_hbc_transform_meta = bundle_hbc_transform_meta;
739
- work_data->options.enable_merge = enable_merge;
740
-
741
- napi_value promise = nullptr;
742
- if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
743
- delete work_data;
744
- ThrowError(env, "Unable to create promise");
745
- return nullptr;
760
+ if (!bundle_patch_entry_name.empty()) {
761
+ request.archive.bundle_patch_entry_name = bundle_patch_entry_name;
746
762
  }
747
763
 
748
- napi_value resource_name = nullptr;
749
- napi_create_string_utf8(
750
- env, "applyDeltaFromSource", NAPI_AUTO_LENGTH, &resource_name);
751
- if (napi_create_async_work(
752
- env,
753
- nullptr,
754
- resource_name,
755
- [](napi_env, void* data) {
756
- auto* w = static_cast<ApplyPatchWork*>(data);
757
- w->status = pushy::delta::ApplyPatchFromFileSource(w->options);
758
- },
759
- [](napi_env cb_env, napi_status status, void* data) {
760
- auto* w = static_cast<ApplyPatchWork*>(data);
761
- if (status != napi_ok) {
762
- // Cancelled/aborted before execute ran: w->status is
763
- // meaningless; still settle the promise so it never hangs.
764
- RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
765
- } else if (w->status.ok) {
766
- napi_value undefined_value = nullptr;
767
- napi_get_undefined(cb_env, &undefined_value);
768
- napi_resolve_deferred(cb_env, w->deferred, undefined_value);
769
- } else {
770
- napi_value error = nullptr;
771
- napi_value message = nullptr;
772
- napi_create_string_utf8(
773
- cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
774
- napi_create_error(cb_env, nullptr, message, &error);
775
- napi_reject_deferred(cb_env, w->deferred, error);
776
- }
777
- napi_delete_async_work(cb_env, w->work);
778
- delete w;
779
- },
780
- work_data,
781
- &work_data->work) != napi_ok) {
782
- // Work was never created: settle the promise and free the data so it does
783
- // not leak / hang pending forever.
784
- RejectDeferredWithMessage(
785
- env, work_data->deferred, "Unable to create async work");
786
- delete work_data;
787
- return promise;
788
- }
789
- if (napi_queue_async_work(env, work_data->work) != napi_ok) {
790
- // Queued failed: the complete callback will never run, so clean up here.
791
- napi_delete_async_work(env, work_data->work);
792
- RejectDeferredWithMessage(
793
- env, work_data->deferred, "Unable to queue async work");
794
- delete work_data;
795
- return promise;
796
- }
797
- return promise;
764
+ return QueueWork<ApplyArchiveDeltaWork>(
765
+ env,
766
+ "applyArchiveDelta",
767
+ std::move(work),
768
+ [](ApplyArchiveDeltaWork& w) {
769
+ w.status =
770
+ pushy::archive_patch::ApplyArchivePatch(w.request, &w.host_copies);
771
+ },
772
+ [](napi_env cb_env, napi_deferred deferred, const ApplyArchiveDeltaWork& w) {
773
+ if (!w.status.ok) {
774
+ RejectDeferredWithMessage(cb_env, deferred, w.status.message.c_str());
775
+ return;
776
+ }
777
+ napi_resolve_deferred(cb_env, deferred, NewCopyGroupArray(cb_env, w.host_copies));
778
+ });
798
779
  }
799
780
 
800
781
  // cleanupOldEntries(rootDir, keepNames: string[], maxAgeDays)
@@ -820,64 +801,22 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
820
801
  return nullptr;
821
802
  }
822
803
 
823
- auto* work_data = new CleanupWork();
824
- work_data->root_dir = root_dir;
825
- work_data->keep_names = std::move(keep_names);
826
- work_data->max_age_days = max_age_days;
827
-
828
- napi_value promise = nullptr;
829
- if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
830
- delete work_data;
831
- ThrowError(env, "Unable to create promise");
832
- return nullptr;
833
- }
804
+ CleanupWork work;
805
+ work.root_dir = root_dir;
806
+ work.keep_names = std::move(keep_names);
807
+ work.max_age_days = max_age_days;
834
808
 
835
- napi_value resource_name = nullptr;
836
- napi_create_string_utf8(
837
- env, "cleanupOldEntries", NAPI_AUTO_LENGTH, &resource_name);
838
- if (napi_create_async_work(
839
- env,
840
- nullptr,
841
- resource_name,
842
- [](napi_env, void* data) {
843
- auto* w = static_cast<CleanupWork*>(data);
844
- w->status = pushy::delta::CleanupOldEntries(
845
- w->root_dir, w->keep_names, w->max_age_days);
846
- },
847
- [](napi_env cb_env, napi_status status, void* data) {
848
- auto* w = static_cast<CleanupWork*>(data);
849
- if (status != napi_ok) {
850
- RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
851
- } else if (w->status.ok) {
852
- napi_value undefined_value = nullptr;
853
- napi_get_undefined(cb_env, &undefined_value);
854
- napi_resolve_deferred(cb_env, w->deferred, undefined_value);
855
- } else {
856
- napi_value error = nullptr;
857
- napi_value message = nullptr;
858
- napi_create_string_utf8(
859
- cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
860
- napi_create_error(cb_env, nullptr, message, &error);
861
- napi_reject_deferred(cb_env, w->deferred, error);
862
- }
863
- napi_delete_async_work(cb_env, w->work);
864
- delete w;
865
- },
866
- work_data,
867
- &work_data->work) != napi_ok) {
868
- RejectDeferredWithMessage(
869
- env, work_data->deferred, "Unable to create async work");
870
- delete work_data;
871
- return promise;
872
- }
873
- if (napi_queue_async_work(env, work_data->work) != napi_ok) {
874
- napi_delete_async_work(env, work_data->work);
875
- RejectDeferredWithMessage(
876
- env, work_data->deferred, "Unable to queue async work");
877
- delete work_data;
878
- return promise;
879
- }
880
- return promise;
809
+ return QueueWork<CleanupWork>(
810
+ env,
811
+ "cleanupOldEntries",
812
+ std::move(work),
813
+ [](CleanupWork& w) {
814
+ w.status = pushy::delta::CleanupOldEntries(
815
+ w.root_dir, w.keep_names, w.max_age_days);
816
+ },
817
+ [](napi_env cb_env, napi_deferred deferred, const CleanupWork& w) {
818
+ SettleWithStatus(cb_env, deferred, w.status);
819
+ });
881
820
  }
882
821
 
883
822
  // sha256HexFileAsync(path) -> Promise<hex>. Rejects when the file cannot be
@@ -899,144 +838,334 @@ napi_value Sha256HexFileAsync(napi_env env, napi_callback_info info) {
899
838
  return nullptr;
900
839
  }
901
840
 
902
- auto* work_data = new Sha256FileWork();
903
- work_data->path = std::move(path);
841
+ Sha256FileWork work;
842
+ work.path = std::move(path);
904
843
 
905
- napi_value promise = nullptr;
906
- if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
907
- delete work_data;
908
- ThrowError(env, "Unable to create promise");
844
+ return QueueWork<Sha256FileWork>(
845
+ env,
846
+ "sha256HexFileAsync",
847
+ std::move(work),
848
+ [](Sha256FileWork& w) { w.hex = pushy::digest::Sha256File(w.path); },
849
+ [](napi_env cb_env, napi_deferred deferred, const Sha256FileWork& w) {
850
+ if (w.hex.empty()) {
851
+ RejectDeferredWithMessage(
852
+ cb_env, deferred, "sha256HexFileAsync: cannot read file");
853
+ } else {
854
+ napi_resolve_deferred(cb_env, deferred, NewString(cb_env, w.hex));
855
+ }
856
+ });
857
+ }
858
+
859
+ // sha256HexRawFdAsync(fd, offset, length) -> Promise<hex>, hashed on a libuv
860
+ // worker thread. Used for the embedded bundle (resourceManager.getRawFd): the
861
+ // UI thread neither reads the multi-MB bundle into memory nor hashes it.
862
+ // Rejects on any read failure or short read.
863
+ napi_value Sha256HexRawFdAsync(napi_env env, napi_callback_info info) {
864
+ napi_value args[3] = {nullptr, nullptr, nullptr};
865
+ size_t argc = 3;
866
+ if (!GetArgCount(env, info, &argc, args) || argc < 3) {
867
+ ThrowError(env, "sha256HexRawFdAsync: expected (fd, offset, length)");
909
868
  return nullptr;
910
869
  }
911
-
912
- napi_value resource_name = nullptr;
913
- napi_create_string_utf8(
914
- env, "sha256HexFileAsync", NAPI_AUTO_LENGTH, &resource_name);
915
- if (napi_create_async_work(
916
- env,
917
- nullptr,
918
- resource_name,
919
- [](napi_env, void* data) {
920
- auto* w = static_cast<Sha256FileWork*>(data);
921
- w->hex = pushy::digest::Sha256File(w->path);
922
- },
923
- [](napi_env cb_env, napi_status status, void* data) {
924
- auto* w = static_cast<Sha256FileWork*>(data);
925
- if (status != napi_ok) {
926
- RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
927
- } else if (w->hex.empty()) {
928
- RejectDeferredWithMessage(
929
- cb_env, w->deferred, "sha256HexFileAsync: cannot read file");
930
- } else {
931
- napi_resolve_deferred(cb_env, w->deferred, NewString(cb_env, w->hex));
932
- }
933
- napi_delete_async_work(cb_env, w->work);
934
- delete w;
935
- },
936
- work_data,
937
- &work_data->work) != napi_ok) {
938
- RejectDeferredWithMessage(
939
- env, work_data->deferred, "Unable to create async work");
940
- delete work_data;
941
- return promise;
870
+ int32_t fd = -1;
871
+ if (!GetInt32(env, args[0], &fd)) {
872
+ return nullptr;
942
873
  }
943
- if (napi_queue_async_work(env, work_data->work) != napi_ok) {
944
- napi_delete_async_work(env, work_data->work);
945
- RejectDeferredWithMessage(
946
- env, work_data->deferred, "Unable to queue async work");
947
- delete work_data;
948
- return promise;
874
+ if (fd < 0) {
875
+ ThrowError(env, "sha256HexRawFdAsync: fd must be non-negative");
876
+ return nullptr;
949
877
  }
950
- return promise;
878
+ int64_t offset = 0;
879
+ int64_t length = 0;
880
+ if (!GetNonNegativeInt64(env, args[1], "offset", &offset) ||
881
+ !GetNonNegativeInt64(env, args[2], "length", &length)) {
882
+ return nullptr;
883
+ }
884
+
885
+ Sha256RangeWork work;
886
+ work.fd = fd;
887
+ work.offset = offset;
888
+ work.length = length;
889
+
890
+ return QueueWork<Sha256RangeWork>(
891
+ env,
892
+ "sha256HexRawFdAsync",
893
+ std::move(work),
894
+ HashFdRange,
895
+ [](napi_env cb_env, napi_deferred deferred, const Sha256RangeWork& w) {
896
+ if (!w.error.empty() || w.hex.empty()) {
897
+ RejectDeferredWithMessage(
898
+ cb_env,
899
+ deferred,
900
+ w.error.empty() ? "sha256HexRawFdAsync: no digest"
901
+ : w.error.c_str());
902
+ } else {
903
+ napi_resolve_deferred(cb_env, deferred, NewString(cb_env, w.hex));
904
+ }
905
+ });
951
906
  }
952
907
 
953
- bool ExportFunction(
908
+ // ---- Completion records (cpp/patch_core/install_record.h) ----
909
+
910
+ // Reads the first `count` arguments as strings, of which the first
911
+ // `non_empty` must not be empty; throws and returns false otherwise.
912
+ bool GetStringArgs(
954
913
  napi_env env,
955
- napi_value exports,
914
+ napi_callback_info info,
956
915
  const char* name,
957
- napi_callback callback) {
958
- napi_value fn = nullptr;
959
- if (napi_create_function(env, name, NAPI_AUTO_LENGTH, callback, nullptr, &fn) !=
960
- napi_ok) {
961
- ThrowError(env, std::string("Unable to create function ") + name);
916
+ size_t count,
917
+ size_t non_empty,
918
+ std::string* out) {
919
+ napi_value args[5] = {nullptr, nullptr, nullptr, nullptr, nullptr};
920
+ size_t argc = count;
921
+ if (count > 5 || !GetArgCount(env, info, &argc, args) || argc < count) {
922
+ ThrowError(env, std::string(name) + ": wrong number of arguments");
962
923
  return false;
963
924
  }
964
-
965
- if (napi_set_named_property(env, exports, name, fn) != napi_ok) {
966
- ThrowError(env, std::string("Unable to export function ") + name);
967
- return false;
925
+ for (size_t i = 0; i < count; i++) {
926
+ bool ok = false;
927
+ out[i] = GetString(env, args[i], &ok);
928
+ if (!ok) {
929
+ return false;
930
+ }
931
+ if (i < non_empty && out[i].empty()) {
932
+ ThrowError(env, std::string(name) + ": argument " + std::to_string(i) +
933
+ " must be a non-empty string");
934
+ return false;
935
+ }
968
936
  }
969
937
  return true;
970
938
  }
971
939
 
972
- } // namespace
940
+ // isInstallComplete(versionDir, versionHash, bundleFileName) -> boolean.
941
+ // Synchronous: a stat and a read of the (at most 64 KB) record, no digest.
942
+ napi_value InstallRecordIsComplete(napi_env env, napi_callback_info info) {
943
+ std::string args[3];
944
+ if (!GetStringArgs(env, info, "isInstallComplete", 3, 3, args)) {
945
+ return nullptr;
946
+ }
947
+ return NewBoolean(env, pushy::install_record::IsComplete(args[0], args[1], args[2]));
948
+ }
949
+
950
+ // recordedBundleSha256(versionDir) -> string; "" when the record has none.
951
+ // Synchronous like isInstallComplete.
952
+ napi_value InstallRecordBundleSha256(napi_env env, napi_callback_info info) {
953
+ std::string args[1];
954
+ if (!GetStringArgs(env, info, "recordedBundleSha256", 1, 1, args)) {
955
+ return nullptr;
956
+ }
957
+ return NewString(env, pushy::install_record::RecordedBundleSha256(args[0]));
958
+ }
973
959
 
960
+ // One install_record call on a libuv worker thread: hashing a bundle (tens
961
+ // of MB) or fsyncing must not block the JS thread.
962
+ struct InstallRecordWork {
963
+ std::string args[4];
964
+ bool grandfathered = false;
965
+ pushy::delta::Status status{false, ""};
966
+ };
974
967
 
975
- // sha256(Uint8Array | ArrayBuffer) -> 小写 hex。同步:bundleHash 的输入是
976
- // rawfile 里的 bundle(已整体读入内存,与 pdiff 的读法一致),哈希本身毫秒级。
977
- static napi_value MakeUtf8String(napi_env env, const std::string& value);
968
+ void SettleInstallRecordWork(
969
+ napi_env env,
970
+ napi_deferred deferred,
971
+ const InstallRecordWork& w) {
972
+ SettleWithStatus(env, deferred, w.status);
973
+ }
978
974
 
979
- // Streaming file digest (bounded memory). Synchronous: small files only —
980
- // archives and bundles go through sha256HexFileAsync.
981
- static napi_value Sha256HexFile(napi_env env, napi_callback_info info) {
982
- size_t argc = 1;
983
- napi_value args[1] = {nullptr};
984
- if (!GetArgCount(env, info, &argc, args) || argc < 1) {
985
- ThrowError(env, "sha256HexFile: missing path argument");
975
+ // sealInstallRecord(stagingDir, versionHash, bundleFileName, artifactSha256)
976
+ // -> Promise<void>: hashes the staged bundle and writes the record.
977
+ napi_value SealInstallRecord(napi_env env, napi_callback_info info) {
978
+ InstallRecordWork work;
979
+ if (!GetStringArgs(env, info, "sealInstallRecord", 4, 3, work.args)) {
986
980
  return nullptr;
987
981
  }
988
- bool ok = false;
989
- std::string path = GetString(env, args[0], &ok);
990
- if (!ok) {
991
- // GetString already raised the pending exception.
982
+ return QueueWork<InstallRecordWork>(
983
+ env,
984
+ "sealInstallRecord",
985
+ std::move(work),
986
+ [](InstallRecordWork& w) {
987
+ w.status = pushy::install_record::Seal(
988
+ w.args[0], w.args[1], w.args[2], w.args[3], std::string())
989
+ .status;
990
+ },
991
+ SettleInstallRecordWork);
992
+ }
993
+
994
+ // promoteStaging(stagingDir, versionDir) -> Promise<void>: renames the sealed
995
+ // staging directory to the (removed) version directory.
996
+ napi_value PromoteStaging(napi_env env, napi_callback_info info) {
997
+ InstallRecordWork work;
998
+ if (!GetStringArgs(env, info, "promoteStaging", 2, 2, work.args)) {
992
999
  return nullptr;
993
1000
  }
994
- return MakeUtf8String(env, pushy::digest::Sha256File(path));
1001
+ return QueueWork<InstallRecordWork>(
1002
+ env,
1003
+ "promoteStaging",
1004
+ std::move(work),
1005
+ [](InstallRecordWork& w) {
1006
+ w.status = pushy::install_record::Promote(w.args[0], w.args[1]);
1007
+ },
1008
+ SettleInstallRecordWork);
995
1009
  }
996
1010
 
997
- static napi_value Sha256Hex(napi_env env, napi_callback_info info) {
998
- size_t argc = 1;
999
- napi_value args[1] = {nullptr};
1000
- if (!GetArgCount(env, info, &argc, args) || argc < 1) {
1001
- ThrowError(env, "sha256Hex: missing data argument");
1011
+ // checkActivation(versionDir, versionHash, bundleFileName, grandfathered)
1012
+ // -> Promise<void>, rejected with the reason the version must not be
1013
+ // activated.
1014
+ napi_value CheckActivation(napi_env env, napi_callback_info info) {
1015
+ InstallRecordWork work;
1016
+ if (!GetStringArgs(env, info, "checkActivation", 3, 3, work.args)) {
1002
1017
  return nullptr;
1003
1018
  }
1019
+ napi_value args[4] = {nullptr, nullptr, nullptr, nullptr};
1020
+ size_t argc = 4;
1021
+ if (!GetArgCount(env, info, &argc, args) || argc < 4) {
1022
+ ThrowError(env, "checkActivation: wrong number of arguments");
1023
+ return nullptr;
1024
+ }
1025
+ if (!GetBoolean(env, args[3], &work.grandfathered)) {
1026
+ return nullptr;
1027
+ }
1028
+ return QueueWork<InstallRecordWork>(
1029
+ env,
1030
+ "checkActivation",
1031
+ std::move(work),
1032
+ [](InstallRecordWork& w) {
1033
+ pushy::install_record::Activation activation;
1034
+ w.status = pushy::install_record::CheckActivation(
1035
+ w.args[0], w.args[1], w.args[2], w.grandfathered, &activation);
1036
+ },
1037
+ SettleInstallRecordWork);
1038
+ }
1004
1039
 
1005
- void* data = nullptr;
1006
- size_t length = 0;
1007
- bool is_typedarray = false;
1008
- napi_is_typedarray(env, args[0], &is_typedarray);
1009
- if (is_typedarray) {
1010
- napi_typedarray_type type;
1011
- napi_value arraybuffer = nullptr;
1012
- size_t offset = 0;
1013
- if (napi_get_typedarray_info(
1014
- env, args[0], &type, &length, &data, &arraybuffer, &offset) !=
1015
- napi_ok) {
1016
- ThrowError(env, "sha256Hex: cannot read typed array");
1017
- return nullptr;
1018
- }
1019
- } else {
1020
- bool is_arraybuffer = false;
1021
- napi_is_arraybuffer(env, args[0], &is_arraybuffer);
1022
- if (!is_arraybuffer ||
1023
- napi_get_arraybuffer_info(env, args[0], &data, &length) != napi_ok) {
1024
- ThrowError(env, "sha256Hex: expected Uint8Array or ArrayBuffer");
1025
- return nullptr;
1040
+ // ---- Archive extraction limits (cpp/patch_core/archive_limits.h) ----
1041
+
1042
+ // Reads `count` number arguments (byte counts: integers below 2^53).
1043
+ bool GetInt64Args(
1044
+ napi_env env,
1045
+ napi_callback_info info,
1046
+ const char* name,
1047
+ size_t count,
1048
+ int64_t* out) {
1049
+ napi_value args[4] = {nullptr, nullptr, nullptr, nullptr};
1050
+ size_t argc = count;
1051
+ if (count > 4 || !GetArgCount(env, info, &argc, args) || argc < count) {
1052
+ ThrowError(env, std::string(name) + ": wrong number of arguments");
1053
+ return false;
1054
+ }
1055
+ for (size_t i = 0; i < count; i++) {
1056
+ napi_valuetype type = napi_undefined;
1057
+ if (!GetValueType(env, args[i], &type) || type != napi_number ||
1058
+ napi_get_value_int64(env, args[i], &out[i]) != napi_ok) {
1059
+ ThrowError(env, std::string(name) + ": argument " + std::to_string(i) +
1060
+ " must be a number");
1061
+ return false;
1026
1062
  }
1027
1063
  }
1064
+ return true;
1065
+ }
1028
1066
 
1029
- pushy::digest::Sha256 hasher;
1030
- if (length > 0) {
1031
- hasher.Update(static_cast<const uint8_t*>(data), length);
1067
+ // checkUncompressedSize(archiveBytes, uncompressedBytes) -> string | undefined:
1068
+ // the reason the whole archive's expansion is refused.
1069
+ napi_value CheckUncompressedSize(napi_env env, napi_callback_info info) {
1070
+ int64_t args[2] = {0, 0};
1071
+ if (!GetInt64Args(env, info, "checkUncompressedSize", 2, args)) {
1072
+ return nullptr;
1032
1073
  }
1033
- const std::string hex = hasher.HexDigest();
1074
+ const pushy::delta::Status status =
1075
+ pushy::archive_limits::CheckUncompressedSize(args[0], args[1]);
1076
+ return status.ok ? nullptr : NewString(env, status.message);
1077
+ }
1034
1078
 
1079
+ // planFreeSpaceProbe(knownTotal, writtenBefore, chunkBytes, reservedUntil)
1080
+ // -> {probe, reserve, reservedUntil}, before one chunk of a download is
1081
+ // written.
1082
+ napi_value PlanFreeSpaceProbe(napi_env env, napi_callback_info info) {
1083
+ int64_t args[4] = {0, 0, 0, 0};
1084
+ if (!GetInt64Args(env, info, "planFreeSpaceProbe", 4, args)) {
1085
+ return nullptr;
1086
+ }
1087
+ const pushy::archive_limits::FreeSpaceProbe plan =
1088
+ pushy::archive_limits::PlanUnknownLengthFreeSpaceProbe(
1089
+ args[0], args[1], args[2], args[3]);
1035
1090
  napi_value result = nullptr;
1036
- napi_create_string_utf8(env, hex.c_str(), hex.size(), &result);
1091
+ napi_value reserve = nullptr;
1092
+ napi_value reserved_until = nullptr;
1093
+ if (napi_create_object(env, &result) != napi_ok ||
1094
+ napi_create_int64(env, plan.reserve, &reserve) != napi_ok ||
1095
+ napi_create_int64(env, plan.reserved_until, &reserved_until) != napi_ok) {
1096
+ ThrowError(env, "planFreeSpaceProbe: cannot create the result");
1097
+ return nullptr;
1098
+ }
1099
+ SetBoolProperty(env, result, "probe", plan.probe);
1100
+ napi_set_named_property(env, result, "reserve", reserve);
1101
+ napi_set_named_property(env, result, "reservedUntil", reserved_until);
1037
1102
  return result;
1038
1103
  }
1039
1104
 
1105
+ struct ExtractedDirectoryWork {
1106
+ std::string directory;
1107
+ int64_t archive_bytes = 0;
1108
+ pushy::delta::Status status{false, ""};
1109
+ };
1110
+
1111
+ // checkExtractedDirectory(directory, archiveBytes) -> Promise<void>: walks
1112
+ // the freshly unpacked tree on a libuv worker thread and rejects with the
1113
+ // first violation.
1114
+ napi_value CheckExtractedDirectory(napi_env env, napi_callback_info info) {
1115
+ napi_value args[2] = {nullptr, nullptr};
1116
+ size_t argc = 2;
1117
+ if (!GetArgCount(env, info, &argc, args) || argc < 2) {
1118
+ ThrowError(env, "checkExtractedDirectory: wrong number of arguments");
1119
+ return nullptr;
1120
+ }
1121
+ ExtractedDirectoryWork work;
1122
+ bool ok = false;
1123
+ work.directory = GetString(env, args[0], &ok);
1124
+ if (!ok) {
1125
+ return nullptr;
1126
+ }
1127
+ napi_valuetype type = napi_undefined;
1128
+ if (work.directory.empty() || !GetValueType(env, args[1], &type) ||
1129
+ type != napi_number ||
1130
+ napi_get_value_int64(env, args[1], &work.archive_bytes) != napi_ok) {
1131
+ ThrowError(env, "checkExtractedDirectory: expected a directory and a size");
1132
+ return nullptr;
1133
+ }
1134
+ return QueueWork<ExtractedDirectoryWork>(
1135
+ env,
1136
+ "checkExtractedDirectory",
1137
+ std::move(work),
1138
+ [](ExtractedDirectoryWork& w) {
1139
+ w.status = pushy::archive_limits::CheckExtractedDirectory(
1140
+ w.directory, w.archive_bytes);
1141
+ },
1142
+ [](napi_env cb_env, napi_deferred deferred, const ExtractedDirectoryWork& w) {
1143
+ SettleWithStatus(cb_env, deferred, w.status);
1144
+ });
1145
+ }
1146
+
1147
+ bool ExportFunction(
1148
+ napi_env env,
1149
+ napi_value exports,
1150
+ const char* name,
1151
+ napi_callback callback) {
1152
+ napi_value fn = nullptr;
1153
+ if (napi_create_function(env, name, NAPI_AUTO_LENGTH, callback, nullptr, &fn) !=
1154
+ napi_ok) {
1155
+ ThrowError(env, std::string("Unable to create function ") + name);
1156
+ return false;
1157
+ }
1158
+
1159
+ if (napi_set_named_property(env, exports, name, fn) != napi_ok) {
1160
+ ThrowError(env, std::string("Unable to export function ") + name);
1161
+ return false;
1162
+ }
1163
+ return true;
1164
+ }
1165
+
1166
+ } // namespace
1167
+
1168
+
1040
1169
  // CRC32(zip/zlib 多项式)。同步:输入是已在内存的 rawfile 资源字节,用于
1041
1170
  // pdiff 拷贝前与 __diff.json copiesCrc 比对(不符则整次 patch 失败落 full)。
1042
1171
  static napi_value Crc32(napi_env env, napi_callback_info info) {
@@ -1093,139 +1222,187 @@ static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
1093
1222
  return result;
1094
1223
  }
1095
1224
 
1096
- // ---- update-flow decision layer (NATIVE_CHECKUPDATE_DESIGN §10) ----
1097
- // String-in/string-out JSON, matching the decision layer's own boundary.
1098
- // Returning undefined (nullptr without a pending exception) means "input did
1099
- // not parse"; the ArkTS orchestrator skips the check round.
1225
+ // ---- native round (update_flow_core/native_round.h) ----
1226
+ // JSON text in and out. Returning undefined (nullptr without a pending
1227
+ // exception) means the core rejected the input; the ArkTS orchestrator fails
1228
+ // the round.
1229
+
1230
+ // Reads the first `count` arguments (at most 5) into `args`, and those of
1231
+ // them flagged in `strings` (bit i = argument i) as strings into `out[i]`
1232
+ // (null/undefined read as ""). False with an exception pending otherwise.
1233
+ bool GetFlowArgs(napi_env env, napi_callback_info info, const char* name,
1234
+ size_t count, unsigned strings, napi_value* args,
1235
+ std::string* out) {
1236
+ size_t argc = count;
1237
+ if (!GetArgCount(env, info, &argc, args) || argc < count) {
1238
+ ThrowError(env, std::string(name) + ": missing arguments");
1239
+ return false;
1240
+ }
1241
+ for (size_t i = 0; i < count; i++) {
1242
+ bool ok = true;
1243
+ if ((strings >> i) & 1u) {
1244
+ out[i] = GetString(env, args[i], &ok);
1245
+ }
1246
+ if (!ok) {
1247
+ return false;
1248
+ }
1249
+ }
1250
+ return true;
1251
+ }
1100
1252
 
1101
- static napi_value MakeUtf8String(napi_env env, const std::string& value) {
1102
- napi_value result = nullptr;
1103
- if (napi_create_string_utf8(env, value.c_str(), value.size(), &result) !=
1104
- napi_ok) {
1105
- ThrowError(env, "Failed to create string");
1106
- return nullptr;
1253
+ bool GetFlowNumber(napi_env env, napi_value value, const char* name,
1254
+ double* out) {
1255
+ if (napi_get_value_double(env, value, out) != napi_ok) {
1256
+ ThrowError(env, std::string(name) + ": expected number");
1257
+ return false;
1107
1258
  }
1108
- return result;
1259
+ return true;
1109
1260
  }
1110
1261
 
1111
- static napi_value FlowBuildRequestBody(napi_env env,
1112
- napi_callback_info info) {
1113
- size_t argc = 1;
1114
- napi_value args[1] = {nullptr};
1115
- if (!GetArgCount(env, info, &argc, args) || argc < 1) {
1116
- ThrowError(env, "buildRequestBody: missing input argument");
1262
+ // The parsed object, or Undefined (which the core rejects).
1263
+ flowjson::Value ParseFlowObject(const std::string& json) {
1264
+ bool ok = false;
1265
+ flowjson::Value value = flowjson::Parse(json, &ok);
1266
+ return ok && value.IsObject() ? value : flowjson::Value::Undefined();
1267
+ }
1268
+
1269
+ napi_value FlowJsonOrUndefined(napi_env env, const flowjson::Value& value) {
1270
+ return value.IsUndefined() ? nullptr
1271
+ : NewString(env, flowjson::Stringify(value));
1272
+ }
1273
+
1274
+ // prepareRound(configJson, envJson, randomSample)
1275
+ static napi_value FlowPrepareRound(napi_env env, napi_callback_info info) {
1276
+ napi_value args[3] = {nullptr, nullptr, nullptr};
1277
+ std::string text[3];
1278
+ double sample = 0;
1279
+ if (!GetFlowArgs(env, info, "prepareRound", 3, 0b011, args, text) ||
1280
+ !GetFlowNumber(env, args[2], "prepareRound", &sample)) {
1117
1281
  return nullptr;
1118
1282
  }
1119
- bool ok = false;
1120
- std::string input_json = GetString(env, args[0], &ok);
1121
- if (!ok) {
1283
+ return FlowJsonOrUndefined(
1284
+ env, flowcore::PrepareRound(text[0], ParseFlowObject(text[1]), sample));
1285
+ }
1286
+
1287
+ // nextQueryStep(stateJson, responseText?) — responseText undefined/null for
1288
+ // a failed (or no) request.
1289
+ static napi_value FlowNextQueryStep(napi_env env, napi_callback_info info) {
1290
+ napi_value args[2] = {nullptr, nullptr};
1291
+ std::string text[2];
1292
+ if (!GetFlowArgs(env, info, "nextQueryStep", 2, 0b11, args, text)) {
1122
1293
  return nullptr;
1123
1294
  }
1124
- bool parsed = false;
1125
- flowjson::Value input = flowjson::Parse(input_json, &parsed);
1126
- if (!parsed || !input.IsObject()) {
1295
+ return FlowJsonOrUndefined(
1296
+ env, flowcore::NextQueryStep(
1297
+ ParseFlowObject(text[0]),
1298
+ IsNullOrUndefined(env, args[1])
1299
+ ? flowjson::Value::Undefined()
1300
+ : flowjson::Value::String(std::move(text[1]))));
1301
+ }
1302
+
1303
+ // handleResponse(responseText, configJson, envJson, requestBody,
1304
+ // responseAtSeconds)
1305
+ static napi_value FlowHandleResponse(napi_env env, napi_callback_info info) {
1306
+ napi_value args[5] = {nullptr, nullptr, nullptr, nullptr, nullptr};
1307
+ std::string text[5];
1308
+ double responseAt = 0;
1309
+ if (!GetFlowArgs(env, info, "handleResponse", 5, 0b01111, args, text) ||
1310
+ !GetFlowNumber(env, args[4], "handleResponse", &responseAt)) {
1127
1311
  return nullptr;
1128
1312
  }
1129
- return MakeUtf8String(
1130
- env, flowjson::Stringify(flowcore::BuildRequestBody(input)));
1313
+ return FlowJsonOrUndefined(
1314
+ env, flowcore::HandleRoundResponse(text[0], text[1],
1315
+ ParseFlowObject(text[2]), text[3],
1316
+ responseAt));
1131
1317
  }
1132
1318
 
1133
- static napi_value FlowOrderEndpointCandidates(napi_env env,
1134
- napi_callback_info info) {
1319
+ // ---- persisted native config (update_flow_core/native_config.h) ----
1320
+ // null/undefined arguments read as "" (an absent config): never equivalent,
1321
+ // not_configured at the gate, not an object for the normalizer.
1322
+
1323
+ static napi_value FlowConfigsEquivalent(napi_env env, napi_callback_info info) {
1135
1324
  size_t argc = 2;
1136
1325
  napi_value args[2] = {nullptr, nullptr};
1137
1326
  if (!GetArgCount(env, info, &argc, args) || argc < 2) {
1138
- ThrowError(env, "orderEndpointCandidates: missing arguments");
1327
+ ThrowError(env, "configsEquivalent: missing arguments");
1139
1328
  return nullptr;
1140
1329
  }
1141
1330
  bool ok = false;
1142
- std::string endpoints_json = GetString(env, args[0], &ok);
1331
+ std::string a = GetString(env, args[0], &ok);
1143
1332
  if (!ok) {
1144
1333
  return nullptr;
1145
1334
  }
1146
- double sample = 0;
1147
- if (napi_get_value_double(env, args[1], &sample) != napi_ok) {
1148
- ThrowError(env, "orderEndpointCandidates: expected number sample");
1149
- return nullptr;
1150
- }
1151
- bool parsed = false;
1152
- flowjson::Value endpoints = flowjson::Parse(endpoints_json, &parsed);
1153
- if (!parsed || !endpoints.IsArray()) {
1335
+ std::string b = GetString(env, args[1], &ok);
1336
+ if (!ok) {
1154
1337
  return nullptr;
1155
1338
  }
1156
- return MakeUtf8String(env,
1157
- flowjson::Stringify(flowcore::OrderEndpointCandidates(
1158
- endpoints, sample)));
1339
+ napi_value result = nullptr;
1340
+ napi_get_boolean(env, flowcore::ConfigsEquivalent(a, b), &result);
1341
+ return result;
1159
1342
  }
1160
1343
 
1161
- static napi_value FlowIsValidResponse(napi_env env,
1162
- napi_callback_info info) {
1344
+ static napi_value FlowRunnableConfig(napi_env env, napi_callback_info info) {
1163
1345
  size_t argc = 1;
1164
1346
  napi_value args[1] = {nullptr};
1165
1347
  if (!GetArgCount(env, info, &argc, args) || argc < 1) {
1166
- ThrowError(env, "isValidResponse: missing arguments");
1348
+ ThrowError(env, "runnableConfig: missing argument");
1167
1349
  return nullptr;
1168
1350
  }
1169
1351
  bool ok = false;
1170
- std::string response_text = GetString(env, args[0], &ok);
1352
+ std::string config_json = GetString(env, args[0], &ok);
1171
1353
  if (!ok) {
1172
- // Exception pending (non-string argument): propagate it as a throw
1173
- // instead of a boolean that the caller would treat as a verdict.
1174
1354
  return nullptr;
1175
1355
  }
1176
- napi_value result = nullptr;
1177
- napi_get_boolean(env, flowcore::IsValidResponse(response_text),
1178
- &result);
1179
- return result;
1356
+ return NewString(env, flowcore::RunnableConfigJson(config_json));
1180
1357
  }
1181
1358
 
1182
- static napi_value FlowHandleResponse(napi_env env,
1183
- napi_callback_info info) {
1184
- size_t argc = 3;
1185
- napi_value args[3] = {nullptr, nullptr, nullptr};
1186
- if (!GetArgCount(env, info, &argc, args) || argc < 3) {
1187
- ThrowError(env, "handleResponse: missing arguments");
1359
+ // Throws Error("Invalid native configuration: ...") for options the host
1360
+ // must fix.
1361
+ static napi_value FlowNormalizeConfiguration(napi_env env,
1362
+ napi_callback_info info) {
1363
+ size_t argc = 1;
1364
+ napi_value args[1] = {nullptr};
1365
+ if (!GetArgCount(env, info, &argc, args) || argc < 1) {
1366
+ ThrowError(env, "normalizeConfiguration: missing argument");
1188
1367
  return nullptr;
1189
1368
  }
1190
1369
  bool ok = false;
1191
- std::string response_text = GetString(env, args[0], &ok);
1192
- if (!ok) {
1193
- return nullptr;
1194
- }
1195
- std::string identity_json = GetString(env, args[1], &ok);
1196
- if (!ok) {
1197
- return nullptr;
1198
- }
1199
- std::string after_download = GetString(env, args[2], &ok);
1370
+ std::string options_json = GetString(env, args[0], &ok);
1200
1371
  if (!ok) {
1201
1372
  return nullptr;
1202
1373
  }
1203
- bool parsed = false;
1204
- flowjson::Value identity = flowjson::Parse(identity_json, &parsed);
1205
- if (!parsed || !identity.IsObject()) {
1374
+ std::string config;
1375
+ std::string error;
1376
+ if (!flowcore::NormalizeConfiguration(options_json, &config, &error)) {
1377
+ ThrowError(env, error);
1206
1378
  return nullptr;
1207
1379
  }
1208
- return MakeUtf8String(env,
1209
- flowjson::Stringify(flowcore::HandleResponse(
1210
- response_text, identity, false, after_download)));
1380
+ return NewString(env, config);
1211
1381
  }
1212
1382
 
1213
1383
  napi_value Init(napi_env env, napi_value exports) {
1214
1384
  if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
1215
1385
  !ExportFunction(env, exports, "runStateCore", RunStateCore) ||
1216
- !ExportFunction(env, exports, "buildArchivePlan", BuildArchivePatchPlan) ||
1217
- !ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
1218
- !ExportFunction(env, exports, "applyDeltaFromSource", ApplyPatchFromFileSource) ||
1386
+ !ExportFunction(env, exports, "applyArchiveDelta", ApplyArchiveDelta) ||
1219
1387
  !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
1220
- !ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
1221
- !ExportFunction(env, exports, "sha256HexFile", Sha256HexFile) ||
1222
1388
  !ExportFunction(env, exports, "sha256HexFileAsync", Sha256HexFileAsync) ||
1389
+ !ExportFunction(env, exports, "sha256HexRawFdAsync", Sha256HexRawFdAsync) ||
1390
+ !ExportFunction(env, exports, "isInstallComplete", InstallRecordIsComplete) ||
1391
+ !ExportFunction(env, exports, "recordedBundleSha256", InstallRecordBundleSha256) ||
1392
+ !ExportFunction(env, exports, "sealInstallRecord", SealInstallRecord) ||
1393
+ !ExportFunction(env, exports, "promoteStaging", PromoteStaging) ||
1394
+ !ExportFunction(env, exports, "checkActivation", CheckActivation) ||
1395
+ !ExportFunction(env, exports, "checkUncompressedSize", CheckUncompressedSize) ||
1396
+ !ExportFunction(env, exports, "planFreeSpaceProbe", PlanFreeSpaceProbe) ||
1397
+ !ExportFunction(env, exports, "checkExtractedDirectory", CheckExtractedDirectory) ||
1223
1398
  !ExportFunction(env, exports, "crc32", Crc32) ||
1224
1399
  !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) ||
1225
- !ExportFunction(env, exports, "buildRequestBody", FlowBuildRequestBody) ||
1226
- !ExportFunction(env, exports, "orderEndpointCandidates", FlowOrderEndpointCandidates) ||
1227
- !ExportFunction(env, exports, "isValidResponse", FlowIsValidResponse) ||
1228
- !ExportFunction(env, exports, "handleResponse", FlowHandleResponse)) {
1400
+ !ExportFunction(env, exports, "prepareRound", FlowPrepareRound) ||
1401
+ !ExportFunction(env, exports, "nextQueryStep", FlowNextQueryStep) ||
1402
+ !ExportFunction(env, exports, "handleResponse", FlowHandleResponse) ||
1403
+ !ExportFunction(env, exports, "configsEquivalent", FlowConfigsEquivalent) ||
1404
+ !ExportFunction(env, exports, "runnableConfig", FlowRunnableConfig) ||
1405
+ !ExportFunction(env, exports, "normalizeConfiguration", FlowNormalizeConfiguration)) {
1229
1406
  return nullptr;
1230
1407
  }
1231
1408
  return exports;