react-native-update 10.50.0 → 10.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  2. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  3. package/README-CN.md +14 -1
  4. package/README.md +14 -1
  5. package/android/build.gradle +10 -0
  6. package/android/jni/Android.mk +5 -1
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
  16. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
  17. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  18. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  19. package/cpp/update_flow_core/flow_json.cpp +522 -0
  20. package/cpp/update_flow_core/flow_json.h +103 -0
  21. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  22. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  23. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  24. package/cpp/update_flow_core/update_flow_core.h +77 -0
  25. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  26. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  27. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
  28. package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
  29. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  30. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  31. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  32. package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
  33. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  34. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  35. package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
  36. package/harmony/pushy.har +0 -0
  37. package/ios/RCTPushy/RCTPushy.mm +779 -61
  38. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  39. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  40. package/package.json +3 -1
  41. package/react-native-update.podspec +2 -0
  42. package/src/NativePushy.ts +14 -0
  43. package/src/client.ts +341 -118
  44. package/src/endpoint.ts +7 -29
  45. package/src/provider.tsx +38 -2
  46. package/src/type.ts +26 -0
  47. package/src/updateFlowCore.ts +320 -0
  48. package/src/utils.ts +1 -12
  49. package/src/isInRollout.ts +0 -87
  50. package/src/resolveCheckResult.ts +0 -30
@@ -0,0 +1,121 @@
1
+ // JNI surface of the update-flow decision layer for the Android orchestrator
2
+ // (NativeCheckOrchestrator.java). Pure string-in/string-out: every payload is
3
+ // JSON, matching the decision layer's own boundary. A null return means
4
+ // "input did not parse" and the caller skips the check round.
5
+ #include <jni.h>
6
+
7
+ #include <cstdint>
8
+ #include <limits>
9
+ #include <string>
10
+ #include <vector>
11
+
12
+ #include "../patch_core/jni_util.h"
13
+ #include "flow_json.h"
14
+ #include "update_flow_core.h"
15
+
16
+ namespace {
17
+
18
+ jstring ToJString(JNIEnv* env, const std::string& value) {
19
+ // flow_json emits standard UTF-8, while NewStringUTF expects JNI's modified
20
+ // UTF-8 and corrupts supplementary code points. Decode explicitly and pass
21
+ // UTF-16 code units to NewString. Accept encoded surrogate code points too:
22
+ // strings originating in Java may have entered through modified UTF-8.
23
+ std::vector<jchar> utf16;
24
+ utf16.reserve(value.size());
25
+ size_t i = 0;
26
+ while (i < value.size()) {
27
+ const unsigned char first = static_cast<unsigned char>(value[i]);
28
+ uint32_t codePoint = 0xfffd;
29
+ size_t width = 1;
30
+ if (first < 0x80) {
31
+ codePoint = first;
32
+ } else if (first >= 0xc2 && first <= 0xdf && i + 1 < value.size()) {
33
+ const unsigned char b1 = static_cast<unsigned char>(value[i + 1]);
34
+ if ((b1 & 0xc0) == 0x80) {
35
+ codePoint = ((first & 0x1f) << 6) | (b1 & 0x3f);
36
+ width = 2;
37
+ }
38
+ } else if (first == 0xc0 && i + 1 < value.size() &&
39
+ static_cast<unsigned char>(value[i + 1]) == 0x80) {
40
+ codePoint = 0; // modified UTF-8 encoding of U+0000
41
+ width = 2;
42
+ } else if (first >= 0xe0 && first <= 0xef && i + 2 < value.size()) {
43
+ const unsigned char b1 = static_cast<unsigned char>(value[i + 1]);
44
+ const unsigned char b2 = static_cast<unsigned char>(value[i + 2]);
45
+ if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 &&
46
+ !(first == 0xe0 && b1 < 0xa0)) {
47
+ codePoint = ((first & 0x0f) << 12) | ((b1 & 0x3f) << 6) |
48
+ (b2 & 0x3f);
49
+ width = 3;
50
+ }
51
+ } else if (first >= 0xf0 && first <= 0xf4 && i + 3 < value.size()) {
52
+ const unsigned char b1 = static_cast<unsigned char>(value[i + 1]);
53
+ const unsigned char b2 = static_cast<unsigned char>(value[i + 2]);
54
+ const unsigned char b3 = static_cast<unsigned char>(value[i + 3]);
55
+ if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 &&
56
+ (b3 & 0xc0) == 0x80 && !(first == 0xf0 && b1 < 0x90) &&
57
+ !(first == 0xf4 && b1 > 0x8f)) {
58
+ codePoint = ((first & 0x07) << 18) | ((b1 & 0x3f) << 12) |
59
+ ((b2 & 0x3f) << 6) | (b3 & 0x3f);
60
+ width = 4;
61
+ }
62
+ }
63
+ i += width;
64
+ if (codePoint <= 0xffff) {
65
+ utf16.push_back(static_cast<jchar>(codePoint));
66
+ } else {
67
+ codePoint -= 0x10000;
68
+ utf16.push_back(static_cast<jchar>(0xd800 + (codePoint >> 10)));
69
+ utf16.push_back(static_cast<jchar>(0xdc00 + (codePoint & 0x3ff)));
70
+ }
71
+ }
72
+ if (utf16.size() > static_cast<size_t>(std::numeric_limits<jsize>::max())) {
73
+ return nullptr;
74
+ }
75
+ return env->NewString(utf16.empty() ? nullptr : utf16.data(),
76
+ static_cast<jsize>(utf16.size()));
77
+ }
78
+
79
+ } // namespace
80
+
81
+ extern "C" JNIEXPORT jstring JNICALL
82
+ Java_cn_reactnative_modules_update_NativeUpdateFlow_buildCheckRequestBody(
83
+ JNIEnv* env, jclass, jstring inputJson) {
84
+ bool ok = false;
85
+ flowjson::Value input = flowjson::Parse(
86
+ pushy::jni_util::JStringToString(env, inputJson), &ok);
87
+ if (!ok || !input.IsObject()) {
88
+ return nullptr;
89
+ }
90
+ return ToJString(
91
+ env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
92
+ }
93
+
94
+ extern "C" JNIEXPORT jstring JNICALL
95
+ Java_cn_reactnative_modules_update_NativeUpdateFlow_orderEndpointCandidates(
96
+ JNIEnv* env, jclass, jstring endpointsJson, jdouble randomSample) {
97
+ bool ok = false;
98
+ flowjson::Value endpoints = flowjson::Parse(
99
+ pushy::jni_util::JStringToString(env, endpointsJson), &ok);
100
+ if (!ok || !endpoints.IsArray()) {
101
+ return nullptr;
102
+ }
103
+ return ToJString(env, flowjson::Stringify(updateflow::OrderEndpointCandidates(
104
+ endpoints, randomSample)));
105
+ }
106
+
107
+ extern "C" JNIEXPORT jstring JNICALL
108
+ Java_cn_reactnative_modules_update_NativeUpdateFlow_handleCheckResponse(
109
+ JNIEnv* env, jclass, jstring responseText, jstring identityJson,
110
+ jstring afterDownload) {
111
+ bool ok = false;
112
+ flowjson::Value identity = flowjson::Parse(
113
+ pushy::jni_util::JStringToString(env, identityJson), &ok);
114
+ if (!ok || !identity.IsObject()) {
115
+ return nullptr;
116
+ }
117
+ return ToJString(env, flowjson::Stringify(updateflow::HandleCheckResponse(
118
+ pushy::jni_util::JStringToString(env, responseText),
119
+ identity, false,
120
+ pushy::jni_util::JStringToString(env, afterDownload))));
121
+ }
@@ -17,6 +17,10 @@ set(PATCH_CORE_DIR ${REPO_ROOT}/cpp/patch_core)
17
17
  if(NOT EXISTS ${PATCH_CORE_DIR}/patch_core.cpp)
18
18
  set(PATCH_CORE_DIR ${STAGED_NATIVE_DIR}/patch_core)
19
19
  endif()
20
+ set(UPDATE_FLOW_CORE_DIR ${REPO_ROOT}/cpp/update_flow_core)
21
+ if(NOT EXISTS ${UPDATE_FLOW_CORE_DIR}/update_flow_core.cpp)
22
+ set(UPDATE_FLOW_CORE_DIR ${STAGED_NATIVE_DIR}/update_flow_core)
23
+ endif()
20
24
 
21
25
  # Always compile from source. A prebuilt-import branch used to live here, but
22
26
  # it had no symbol verification: a stale libs/<abi>/librnupdate.so missing a
@@ -32,6 +36,8 @@ set(HDP_SOURCES
32
36
  ${PATCH_CORE_DIR}/hbc_transform_wire.cpp
33
37
  ${PATCH_CORE_DIR}/patch_core.cpp
34
38
  ${PATCH_CORE_DIR}/state_core.cpp
39
+ ${UPDATE_FLOW_CORE_DIR}/flow_json.cpp
40
+ ${UPDATE_FLOW_CORE_DIR}/update_flow_core.cpp
35
41
  ${ANDROID_JNI_DIR}/hpatch.c
36
42
  ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch/patch.c
37
43
  ${HDIFFPATCH_DIR}/file_for_patch.c
@@ -46,6 +52,7 @@ add_library(rnupdate SHARED
46
52
  target_include_directories(rnupdate PRIVATE
47
53
  ${CMAKE_CURRENT_SOURCE_DIR}
48
54
  ${PATCH_CORE_DIR}
55
+ ${UPDATE_FLOW_CORE_DIR}
49
56
  ${ANDROID_JNI_DIR}
50
57
  ${HDIFFPATCH_DIR}
51
58
  ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch
@@ -1,4 +1,7 @@
1
1
  #include <napi/native_api.h>
2
+
3
+ #include "flow_json.h"
4
+ #include "update_flow_core.h"
2
5
  #include <js_native_api.h>
3
6
  #include <js_native_api_types.h>
4
7
 
@@ -992,6 +995,102 @@ static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
992
995
  return result;
993
996
  }
994
997
 
998
+ // ---- update-flow decision layer (NATIVE_CHECKUPDATE_DESIGN §10) ----
999
+ // String-in/string-out JSON, matching the decision layer's own boundary.
1000
+ // Returning undefined (nullptr without a pending exception) means "input did
1001
+ // not parse"; the ArkTS orchestrator skips the check round.
1002
+
1003
+ static napi_value MakeUtf8String(napi_env env, const std::string& value) {
1004
+ napi_value result = nullptr;
1005
+ if (napi_create_string_utf8(env, value.c_str(), value.size(), &result) !=
1006
+ napi_ok) {
1007
+ ThrowError(env, "Failed to create string");
1008
+ return nullptr;
1009
+ }
1010
+ return result;
1011
+ }
1012
+
1013
+ static napi_value FlowBuildCheckRequestBody(napi_env env,
1014
+ napi_callback_info info) {
1015
+ size_t argc = 1;
1016
+ napi_value args[1] = {nullptr};
1017
+ if (!GetArgCount(env, info, &argc, args) || argc < 1) {
1018
+ ThrowError(env, "buildCheckRequestBody: missing input argument");
1019
+ return nullptr;
1020
+ }
1021
+ bool ok = false;
1022
+ std::string input_json = GetString(env, args[0], &ok);
1023
+ if (!ok) {
1024
+ return nullptr;
1025
+ }
1026
+ bool parsed = false;
1027
+ flowjson::Value input = flowjson::Parse(input_json, &parsed);
1028
+ if (!parsed || !input.IsObject()) {
1029
+ return nullptr;
1030
+ }
1031
+ return MakeUtf8String(
1032
+ env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
1033
+ }
1034
+
1035
+ static napi_value FlowOrderEndpointCandidates(napi_env env,
1036
+ napi_callback_info info) {
1037
+ size_t argc = 2;
1038
+ napi_value args[2] = {nullptr, nullptr};
1039
+ if (!GetArgCount(env, info, &argc, args) || argc < 2) {
1040
+ ThrowError(env, "orderEndpointCandidates: missing arguments");
1041
+ return nullptr;
1042
+ }
1043
+ bool ok = false;
1044
+ std::string endpoints_json = GetString(env, args[0], &ok);
1045
+ if (!ok) {
1046
+ return nullptr;
1047
+ }
1048
+ double sample = 0;
1049
+ if (napi_get_value_double(env, args[1], &sample) != napi_ok) {
1050
+ ThrowError(env, "orderEndpointCandidates: expected number sample");
1051
+ return nullptr;
1052
+ }
1053
+ bool parsed = false;
1054
+ flowjson::Value endpoints = flowjson::Parse(endpoints_json, &parsed);
1055
+ if (!parsed || !endpoints.IsArray()) {
1056
+ return nullptr;
1057
+ }
1058
+ return MakeUtf8String(env,
1059
+ flowjson::Stringify(updateflow::OrderEndpointCandidates(
1060
+ endpoints, sample)));
1061
+ }
1062
+
1063
+ static napi_value FlowHandleCheckResponse(napi_env env,
1064
+ napi_callback_info info) {
1065
+ size_t argc = 3;
1066
+ napi_value args[3] = {nullptr, nullptr, nullptr};
1067
+ if (!GetArgCount(env, info, &argc, args) || argc < 3) {
1068
+ ThrowError(env, "handleCheckResponse: missing arguments");
1069
+ return nullptr;
1070
+ }
1071
+ bool ok = false;
1072
+ std::string response_text = GetString(env, args[0], &ok);
1073
+ if (!ok) {
1074
+ return nullptr;
1075
+ }
1076
+ std::string identity_json = GetString(env, args[1], &ok);
1077
+ if (!ok) {
1078
+ return nullptr;
1079
+ }
1080
+ std::string after_download = GetString(env, args[2], &ok);
1081
+ if (!ok) {
1082
+ return nullptr;
1083
+ }
1084
+ bool parsed = false;
1085
+ flowjson::Value identity = flowjson::Parse(identity_json, &parsed);
1086
+ if (!parsed || !identity.IsObject()) {
1087
+ return nullptr;
1088
+ }
1089
+ return MakeUtf8String(env,
1090
+ flowjson::Stringify(updateflow::HandleCheckResponse(
1091
+ response_text, identity, false, after_download)));
1092
+ }
1093
+
995
1094
  napi_value Init(napi_env env, napi_value exports) {
996
1095
  if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
997
1096
  !ExportFunction(env, exports, "runStateCore", RunStateCore) ||
@@ -1001,7 +1100,10 @@ napi_value Init(napi_env env, napi_value exports) {
1001
1100
  !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
1002
1101
  !ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
1003
1102
  !ExportFunction(env, exports, "crc32", Crc32) ||
1004
- !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion)) {
1103
+ !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) ||
1104
+ !ExportFunction(env, exports, "buildCheckRequestBody", FlowBuildCheckRequestBody) ||
1105
+ !ExportFunction(env, exports, "orderEndpointCandidates", FlowOrderEndpointCandidates) ||
1106
+ !ExportFunction(env, exports, "handleCheckResponse", FlowHandleCheckResponse)) {
1005
1107
  return nullptr;
1006
1108
  }
1007
1109
  return exports;
@@ -11,6 +11,9 @@ import NativePatchCore, {
11
11
  ARCHIVE_PATCH_TYPE_FROM_PPK,
12
12
  CopyGroupResult,
13
13
  } from './NativePatchCore';
14
+ import { monotonicNowMs } from './MonotonicClock';
15
+
16
+ export const VERSION_COMPLETE_FILE_NAME = '.pushy-complete';
14
17
 
15
18
  export interface PatchManifestArrays {
16
19
  copyFroms: string[];
@@ -95,6 +98,7 @@ const DIFF_MANIFEST_ENTRY = '__diff.json';
95
98
  const HARMONY_BUNDLE_PATCH_ENTRY = 'bundle.harmony.js.patch';
96
99
  const TEMP_ORIGIN_BUNDLE_ENTRY = '.origin.bundle.harmony.js';
97
100
  const FILE_COPY_BUFFER_SIZE = 64 * 1024;
101
+ const DOWNLOAD_CALL_TIMEOUT_MS = 10 * 60 * 1000;
98
102
 
99
103
  export class DownloadTask {
100
104
  private context: common.Context;
@@ -343,6 +347,12 @@ export class DownloadTask {
343
347
  let writeQueue = Promise.resolve();
344
348
  let lastReportedPercentage = -1;
345
349
  let lastReportedBytes = 0;
350
+ const deadlineUptimeMs = params.deadlineUptimeMs > 0
351
+ ? params.deadlineUptimeMs
352
+ : monotonicNowMs() + DOWNLOAD_CALL_TIMEOUT_MS;
353
+ if (deadlineUptimeMs <= monotonicNowMs()) {
354
+ throw Error('Download deadline expired before start');
355
+ }
346
356
 
347
357
  // Emit at most one progress event per whole-percent change (or per 256KB
348
358
  // when the total is unknown), and only from the dataReceive handler, so the
@@ -374,6 +384,7 @@ export class DownloadTask {
374
384
  // Promise (and the JS caller) forever.
375
385
  const INACTIVITY_TIMEOUT_MS = 60000;
376
386
  let watchdogTimer: number | null = null;
387
+ let deadlineTimer: number | null = null;
377
388
  let inactivityReject: ((error: Error) => void) | null = null;
378
389
  const clearWatchdog = () => {
379
390
  if (watchdogTimer !== null) {
@@ -482,14 +493,22 @@ export class DownloadTask {
482
493
  },
483
494
  );
484
495
 
485
- const responseCode = await httpRequest.requestInStream(params.url, {
486
- method: http.RequestMethod.GET,
487
- readTimeout: 60000,
488
- connectTimeout: 60000,
489
- header: {
490
- 'Content-Type': 'application/octet-stream',
491
- },
496
+ const deadlinePromise = new Promise<never>((_, reject) => {
497
+ deadlineTimer = setTimeout(() => {
498
+ reject(Error('Download exceeded its whole-call deadline'));
499
+ }, Math.max(1, deadlineUptimeMs - monotonicNowMs()));
492
500
  });
501
+ const responseCode = await Promise.race([
502
+ httpRequest.requestInStream(params.url, {
503
+ method: http.RequestMethod.GET,
504
+ readTimeout: 60000,
505
+ connectTimeout: 60000,
506
+ header: {
507
+ 'Content-Type': 'application/octet-stream',
508
+ },
509
+ }),
510
+ deadlinePromise,
511
+ ]);
493
512
  if (responseCode > 299) {
494
513
  throw Error(`Server error: ${responseCode}`);
495
514
  }
@@ -499,7 +518,7 @@ export class DownloadTask {
499
518
  // 时 reject(unhandled rejection),且 promise 一经 reject 无法复活——
500
519
  // 即使随后数据正常流入,race 也必然以 "Download stalled" 失败。
501
520
  refreshWatchdog();
502
- await Promise.race([dataEndPromise, inactivityPromise]);
521
+ await Promise.race([dataEndPromise, inactivityPromise, deadlinePromise]);
503
522
  const stats = await fileIo.stat(params.targetFile);
504
523
  const fileSize = stats.size;
505
524
  if (contentLength > 0 && fileSize !== contentLength) {
@@ -512,6 +531,9 @@ export class DownloadTask {
512
531
  throw error;
513
532
  } finally {
514
533
  clearWatchdog();
534
+ if (deadlineTimer !== null) {
535
+ clearTimeout(deadlineTimer);
536
+ }
515
537
  try {
516
538
  await closeWriter();
517
539
  } catch (closeError) {
@@ -798,6 +820,10 @@ export class DownloadTask {
798
820
  }
799
821
 
800
822
  public async execute(params: DownloadTaskParams): Promise<void> {
823
+ const isPatchTask =
824
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FULL ||
825
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP ||
826
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
801
827
  try {
802
828
  switch (params.type) {
803
829
  case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
@@ -818,13 +844,26 @@ export class DownloadTask {
818
844
  default:
819
845
  throw Error(`Unknown task type: ${params.type}`);
820
846
  }
847
+ if (isPatchTask) {
848
+ await this.writeFileContent(
849
+ `${params.unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`,
850
+ new Uint8Array(0),
851
+ );
852
+ }
821
853
  } catch (error: any) {
822
854
  console.error('Task execution failed:', error.message);
823
855
  if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
824
856
  try {
825
857
  if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
826
858
  await fileIo.unlink(params.targetFile);
827
- } else {
859
+ } else if (
860
+ !fileIo.accessSync(
861
+ `${params.unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`,
862
+ ) ||
863
+ !fileIo.accessSync(`${params.unzipDirectory}/bundle.harmony.js`)
864
+ ) {
865
+ // Never let a failed duplicate task remove an install already
866
+ // handed off by an earlier task via marker + bundle.
828
867
  await this.removeDirectory(params.unzipDirectory);
829
868
  }
830
869
  } catch (cleanupError: any) {
@@ -16,4 +16,7 @@ export class DownloadTaskParams {
16
16
  targetFile: string = ''; // 目标文件路径
17
17
  unzipDirectory: string = ''; // 解压目录路径
18
18
  originDirectory: string = ''; // 原始文件目录路径
19
+ // Native cold-start orchestrator's absolute monotonic-uptime deadline. Zero
20
+ // uses the normal public download API's 10-minute whole-call cap.
21
+ deadlineUptimeMs: number = 0;
19
22
  }
@@ -0,0 +1,13 @@
1
+ import { systemDateTime } from '@kit.BasicServicesKit';
2
+
3
+ // Absolute deadlines must not use Date.now(): automatic time synchronization
4
+ // can move the wall clock while the cold-start rescue round is running.
5
+ //
6
+ // TimeType.ACTIVE (not STARTUP) is the semantic match for the other two
7
+ // platforms: iOS NSProcessInfo.systemUptime and Android System.nanoTime both
8
+ // stop during deep sleep, so a device that sleeps mid-download resumes with
9
+ // its budget intact. STARTUP keeps counting through sleep and would abort a
10
+ // rescue that the same network completes on iOS/Android.
11
+ export function monotonicNowMs(): number {
12
+ return systemDateTime.getUptime(systemDateTime.TimeType.ACTIVE);
13
+ }