react-native-update 10.55.1 → 10.56.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 (76) hide show
  1. package/README-CN.md +35 -0
  2. package/README.md +37 -0
  3. package/android/build.gradle +53 -27
  4. package/android/jni/Application.mk +6 -1
  5. package/android/jni/hpatch.c +59 -4
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/proguard.pro +70 -14
  11. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
  12. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
  14. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
  21. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
  22. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
  23. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
  24. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
  25. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
  26. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
  27. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
  28. package/cpp/patch_core/archive_limits.h +6 -0
  29. package/cpp/patch_core/jni_util.h +6 -2
  30. package/cpp/patch_core/patch_core.cpp +237 -84
  31. package/cpp/patch_core/patch_core.h +11 -0
  32. package/cpp/patch_core/patch_core_android.cpp +49 -33
  33. package/cpp/patch_core/update_core_android.cpp +134 -103
  34. package/cpp/update_flow_core/flow_json.cpp +150 -17
  35. package/cpp/update_flow_core/flow_json.h +13 -1
  36. package/cpp/update_flow_core/update_flow_core.cpp +78 -8
  37. package/cpp/update_flow_core/update_flow_core.h +9 -0
  38. package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
  39. package/harmony/hvigor-plugin.ts +168 -21
  40. package/harmony/pushy/src/README.md +74 -0
  41. package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
  42. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
  43. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
  44. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
  46. package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
  47. package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
  48. package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
  49. package/harmony/pushy/src/main/ets/Logger.ts +42 -16
  50. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
  51. package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
  52. package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
  53. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
  54. package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
  55. package/harmony/pushy/src/main/module.json5 +5 -1
  56. package/harmony/pushy.har +0 -0
  57. package/ios/PrivacyInfo.xcprivacy +79 -0
  58. package/ios/RCTPushy/RCTPushy.mm +399 -170
  59. package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
  60. package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
  61. package/package.json +14 -24
  62. package/react-native-update.podspec +43 -3
  63. package/src/client.ts +236 -82
  64. package/src/context.ts +40 -27
  65. package/src/core.ts +41 -16
  66. package/src/endpoint.ts +2 -1
  67. package/src/error.ts +49 -11
  68. package/src/index.ts +14 -0
  69. package/src/locales/en.ts +1 -23
  70. package/src/locales/zh.ts +1 -22
  71. package/src/provider.tsx +159 -111
  72. package/src/type.ts +0 -2
  73. package/src/updateFlowCore.ts +10 -2
  74. package/src/utils.ts +61 -4
  75. package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
  76. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <cstdint>
4
4
  #include <string>
5
+ #include <vector>
5
6
 
6
7
  #include "flow_json.h"
7
8
 
@@ -24,6 +25,14 @@ uint32_t Murmur3_32(const std::string& key, uint32_t seed = 0);
24
25
  // murmur(uuid) % 100 < rollout — the gray-release bucketing predicate.
25
26
  bool IsInRollout(double rollout, const std::string& uuid);
26
27
 
28
+ // Whether a failed download attempt should move on to the next mirror of the
29
+ // same artifact. Anything but PATCH_FAILED is: every mirror serves the same
30
+ // bytes, so a patch that could not be applied fails on all of them and only
31
+ // the next strategy can help. An absent code (empty string) is retryable.
32
+ // Mirrors isMirrorRetryableCode; covered by the golden vectors so the native
33
+ // orchestrators' retry loops cannot drift from the TS reference.
34
+ bool IsMirrorRetryableCode(const std::string& code);
35
+
27
36
  // paths × fileName -> candidate URL array; Undefined when fileName is falsy
28
37
  // (mirrors joinUrls returning undefined without a file name).
29
38
  flowjson::Value JoinUrls(const flowjson::Value& paths,
@@ -1,10 +1,13 @@
1
1
  // JNI surface of the update-flow decision layer for the Android orchestrator
2
2
  // (NativeCheckOrchestrator.java). Pure string-in/string-out: every payload is
3
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.
4
+ // "input did not parse" and the caller skips the check round; a C++ exception
5
+ // (bad_alloc/length_error from an oversized body) maps onto that same path —
6
+ // it must never unwind through the JNI boundary.
5
7
  #include <jni.h>
6
8
 
7
9
  #include <cstdint>
10
+ #include <exception>
8
11
  #include <limits>
9
12
  #include <string>
10
13
  #include <vector>
@@ -81,27 +84,35 @@ jstring ToJString(JNIEnv* env, const std::string& value) {
81
84
  extern "C" JNIEXPORT jstring JNICALL
82
85
  Java_cn_reactnative_modules_update_NativeUpdateFlow_buildCheckRequestBody(
83
86
  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()) {
87
+ try {
88
+ bool ok = false;
89
+ flowjson::Value input = flowjson::Parse(
90
+ pushy::jni_util::JStringToString(env, inputJson), &ok);
91
+ if (!ok || !input.IsObject()) {
92
+ return nullptr;
93
+ }
94
+ return ToJString(
95
+ env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
96
+ } catch (...) {
88
97
  return nullptr;
89
98
  }
90
- return ToJString(
91
- env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
92
99
  }
93
100
 
94
101
  extern "C" JNIEXPORT jstring JNICALL
95
102
  Java_cn_reactnative_modules_update_NativeUpdateFlow_orderEndpointCandidates(
96
103
  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()) {
104
+ try {
105
+ bool ok = false;
106
+ flowjson::Value endpoints = flowjson::Parse(
107
+ pushy::jni_util::JStringToString(env, endpointsJson), &ok);
108
+ if (!ok || !endpoints.IsArray()) {
109
+ return nullptr;
110
+ }
111
+ return ToJString(env, flowjson::Stringify(updateflow::OrderEndpointCandidates(
112
+ endpoints, randomSample)));
113
+ } catch (...) {
101
114
  return nullptr;
102
115
  }
103
- return ToJString(env, flowjson::Stringify(updateflow::OrderEndpointCandidates(
104
- endpoints, randomSample)));
105
116
  }
106
117
 
107
118
  extern "C" JNIEXPORT jboolean JNICALL
@@ -110,24 +121,32 @@ Java_cn_reactnative_modules_update_NativeUpdateFlow_isValidCheckResponse(
110
121
  if (responseText == nullptr) {
111
122
  return JNI_FALSE;
112
123
  }
113
- return updateflow::IsValidCheckResponse(
114
- pushy::jni_util::JStringToString(env, responseText))
115
- ? JNI_TRUE
116
- : JNI_FALSE;
124
+ try {
125
+ return updateflow::IsValidCheckResponse(
126
+ pushy::jni_util::JStringToString(env, responseText))
127
+ ? JNI_TRUE
128
+ : JNI_FALSE;
129
+ } catch (...) {
130
+ return JNI_FALSE;
131
+ }
117
132
  }
118
133
 
119
134
  extern "C" JNIEXPORT jstring JNICALL
120
135
  Java_cn_reactnative_modules_update_NativeUpdateFlow_handleCheckResponse(
121
136
  JNIEnv* env, jclass, jstring responseText, jstring identityJson,
122
137
  jstring afterDownload) {
123
- bool ok = false;
124
- flowjson::Value identity = flowjson::Parse(
125
- pushy::jni_util::JStringToString(env, identityJson), &ok);
126
- if (!ok || !identity.IsObject()) {
138
+ try {
139
+ bool ok = false;
140
+ flowjson::Value identity = flowjson::Parse(
141
+ pushy::jni_util::JStringToString(env, identityJson), &ok);
142
+ if (!ok || !identity.IsObject()) {
143
+ return nullptr;
144
+ }
145
+ return ToJString(env, flowjson::Stringify(updateflow::HandleCheckResponse(
146
+ pushy::jni_util::JStringToString(env, responseText),
147
+ identity, false,
148
+ pushy::jni_util::JStringToString(env, afterDownload))));
149
+ } catch (...) {
127
150
  return nullptr;
128
151
  }
129
- return ToJString(env, flowjson::Stringify(updateflow::HandleCheckResponse(
130
- pushy::jni_util::JStringToString(env, responseText),
131
- identity, false,
132
- pushy::jni_util::JStringToString(env, afterDownload))));
133
152
  }
@@ -1,34 +1,181 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
 
4
+ // Structural view of the hvigor objects this plugin touches, so the file has
5
+ // no compile-time dependency on @ohos/hvigor typings (it is compiled by the
6
+ // consumer's hvigor, whatever version that is).
7
+ interface HvigorTaskContext {
8
+ getBuildMode?: () => string;
9
+ }
10
+
11
+ interface HvigorModuleContext {
12
+ getBuildMode?: () => string;
13
+ }
14
+
15
+ interface HvigorTask {
16
+ name: string;
17
+ run: (taskContext?: HvigorTaskContext) => void;
18
+ dependencies?: string[];
19
+ postDependencies?: string[];
20
+ }
21
+
22
+ interface HvigorNodeLike {
23
+ getNodePath?: () => string;
24
+ getParentNode?: () => HvigorNodeLike | undefined;
25
+ getContext?: (pluginId: string) => HvigorModuleContext | undefined;
26
+ registerTask?: (task: HvigorTask) => void;
27
+ }
28
+
29
+ export const META_RELATIVE_PATH = 'src/main/resources/rawfile/meta.json';
30
+ const TASK_NAME = 'pushyWriteBuildMeta';
31
+ // OhosPluginId.OHOS_HAP_PLUGIN / OHOS_HSP_PLUGIN / OHOS_HAR_PLUGIN.
32
+ const MODULE_PLUGIN_IDS = ['ohosHapPlugin', 'ohosHspPlugin', 'ohosHarPlugin'];
33
+
34
+ function readVersionName(projectDir: string): string {
35
+ const appJsonPath = path.resolve(projectDir, 'AppScope/app.json5');
36
+ if (!fs.existsSync(appJsonPath)) {
37
+ return '';
38
+ }
39
+ const content = fs.readFileSync(appJsonPath, 'utf-8');
40
+ const match = content.match(
41
+ /(?:"versionName"|versionName):\s*["']([^"']+)["']/,
42
+ );
43
+ return match?.[1] || '';
44
+ }
45
+
46
+ // Only `hvigorw assemble{Hap,App,Hsp,Har}` (DevEco's Build/Run actions pass
47
+ // the same task) should touch the consumer's source tree; IDE sync
48
+ // (`--sync`), `clean` and friends must not rewrite meta.json.
49
+ export function isAssembleInvocation(argv: string[]): boolean {
50
+ return argv.some(arg => /^assemble/i.test(arg));
51
+ }
52
+
53
+ export function resolveBuildMode(
54
+ node: HvigorNodeLike,
55
+ taskContext: HvigorTaskContext | undefined,
56
+ argv: string[],
57
+ ): string {
58
+ if (taskContext && typeof taskContext.getBuildMode === 'function') {
59
+ return String(taskContext.getBuildMode());
60
+ }
61
+ if (typeof node.getContext === 'function') {
62
+ for (const pluginId of MODULE_PLUGIN_IDS) {
63
+ try {
64
+ const context = node.getContext(pluginId);
65
+ if (context && typeof context.getBuildMode === 'function') {
66
+ return String(context.getBuildMode());
67
+ }
68
+ } catch (e) {
69
+ // not this module type
70
+ }
71
+ }
72
+ }
73
+ // `hvigorw assembleHap -p buildMode=release` (DevEco passes the same
74
+ // property for its build actions); hvigor's default mode is debug.
75
+ const fromArgv = argv.find(arg => arg.startsWith('buildMode='));
76
+ return fromArgv ? fromArgv.substring('buildMode='.length) : 'debug';
77
+ }
78
+
79
+ // The plugin is meant for a module hvigorfile (entry/hvigorfile.ts). When it
80
+ // is applied from the project hvigorfile instead, the node path is the
81
+ // project root (AppScope lives there): fall back to entry/ like the original
82
+ // plugin did, so an existing setup keeps working.
83
+ export function resolveModuleDir(node: HvigorNodeLike, cwd: string): string {
84
+ const nodePath =
85
+ typeof node.getNodePath === 'function' ? node.getNodePath() : '';
86
+ if (!nodePath) {
87
+ return path.resolve(cwd, 'entry');
88
+ }
89
+ if (fs.existsSync(path.join(nodePath, 'AppScope', 'app.json5'))) {
90
+ console.warn(
91
+ 'reactNativeUpdatePlugin: applied at project level; writing meta.json ' +
92
+ 'into entry/. Apply it from the module hvigorfile instead.',
93
+ );
94
+ return path.join(nodePath, 'entry');
95
+ }
96
+ return nodePath;
97
+ }
98
+
99
+ export function resolveProjectDir(
100
+ node: HvigorNodeLike,
101
+ moduleDir: string,
102
+ ): string {
103
+ const parent =
104
+ typeof node.getParentNode === 'function' ? node.getParentNode() : undefined;
105
+ if (parent && typeof parent.getNodePath === 'function') {
106
+ const parentPath = parent.getNodePath();
107
+ if (parentPath) {
108
+ return parentPath;
109
+ }
110
+ }
111
+ return path.dirname(moduleDir);
112
+ }
113
+
114
+ // Debug builds get "0" like Android's `pushy_build_time` resValue: a debug
115
+ // rebuild must not look like a new binary and reset the update state.
116
+ export function buildMetaContent(
117
+ buildMode: string,
118
+ versionName: string,
119
+ now: number,
120
+ ): { pushy_build_time: string; versionName: string } {
121
+ return {
122
+ pushy_build_time: buildMode.toLowerCase() === 'release' ? String(now) : '0',
123
+ versionName,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Writes rawfile/meta.json (pushy_build_time + versionName) into the module
129
+ * this plugin is applied to. The write happens in a build task that runs
130
+ * before resources are compiled, not in apply(): apply() runs on every hvigor
131
+ * invocation including IDE sync, and only assemble invocations register the
132
+ * task at all.
133
+ */
4
134
  export function reactNativeUpdatePlugin() {
5
135
  return {
6
136
  pluginId: 'reactNativeUpdatePlugin',
7
- apply() {
8
- const cwd = process.cwd();
9
- const metaFilePath = path.resolve(
10
- cwd,
11
- 'entry/src/main/resources/rawfile/meta.json',
12
- );
13
- fs.mkdirSync(path.dirname(metaFilePath), { recursive: true });
14
-
15
- const moduleJsonPath = path.resolve(cwd, 'AppScope/app.json5');
16
- let versionName = '';
17
- if (fs.existsSync(moduleJsonPath)) {
18
- const content = fs.readFileSync(moduleJsonPath, 'utf-8');
19
- const match = content.match(
20
- /(?:"versionName"|versionName):\s*["']([^"']+)["']/,
21
- );
22
- versionName = match?.[1] || '';
137
+ apply(node?: HvigorNodeLike) {
138
+ const argv = process.argv;
139
+ if (!isAssembleInvocation(argv)) {
140
+ return;
23
141
  }
142
+ const safeNode: HvigorNodeLike = node ?? {};
143
+ const moduleDir = resolveModuleDir(safeNode, process.cwd());
144
+ const projectDir = resolveProjectDir(safeNode, moduleDir);
24
145
 
25
- const metaContent = {
26
- pushy_build_time: String(Date.now()),
27
- versionName,
146
+ const writeMeta = (taskContext?: HvigorTaskContext) => {
147
+ const buildMode = resolveBuildMode(safeNode, taskContext, argv);
148
+ const metaFilePath = path.resolve(moduleDir, META_RELATIVE_PATH);
149
+ fs.mkdirSync(path.dirname(metaFilePath), { recursive: true });
150
+ const metaContent = buildMetaContent(
151
+ buildMode,
152
+ readVersionName(projectDir),
153
+ Date.now(),
154
+ );
155
+ fs.writeFileSync(metaFilePath, JSON.stringify(metaContent, null, 2));
156
+ console.log(`Build time written to ${metaFilePath} (${buildMode})`);
28
157
  };
29
158
 
30
- fs.writeFileSync(metaFilePath, JSON.stringify(metaContent, null, 2));
31
- console.log(`Build time written to ${metaFilePath}`);
159
+ if (typeof safeNode.registerTask === 'function') {
160
+ try {
161
+ safeNode.registerTask({
162
+ name: TASK_NAME,
163
+ run: writeMeta,
164
+ dependencies: ['default@PreBuild'],
165
+ postDependencies: ['default@ProcessResource'],
166
+ });
167
+ return;
168
+ } catch (e) {
169
+ console.warn(
170
+ `reactNativeUpdatePlugin: task registration failed (${
171
+ e instanceof Error ? e.message : String(e)
172
+ }), writing meta.json at apply time`,
173
+ );
174
+ }
175
+ }
176
+ // Very old hvigor without task registration: keep the previous
177
+ // behaviour (write at apply time), still only for assemble builds.
178
+ writeMeta();
32
179
  },
33
180
  };
34
181
  }
@@ -0,0 +1,74 @@
1
+ # react-native-update — HarmonyOS module (`pushy`)
2
+
3
+ Integration notes for the HAR that ships in the `react-native-update` npm
4
+ package (`harmony/pushy.har`). The general setup lives in the project README;
5
+ this file covers what the host app must declare itself.
6
+
7
+ ## Permissions
8
+
9
+ The HAR does not declare `requestPermissions`: downloads and update checks run
10
+ inside the host app's process, so the **host** must request the network
11
+ permission in its entry `module.json5`:
12
+
13
+ ```json5
14
+ {
15
+ "module": {
16
+ "requestPermissions": [
17
+ { "name": "ohos.permission.INTERNET" }
18
+ ]
19
+ }
20
+ }
21
+ ```
22
+
23
+ Without it every check and download fails with a network error.
24
+
25
+ ## No https → http downgrade (`cleartextTrafficPermitted`)
26
+
27
+ `@ohos.net.http` follows redirects internally and exposes neither a switch nor
28
+ the final URL, so unlike Android/iOS the module cannot itself refuse an
29
+ `https → http` redirect. What it does: when the configured endpoints are all
30
+ `https://`, any `http://` endpoint injected through the remote endpoint list
31
+ and any `http://` artifact URL in a check decision are ignored.
32
+
33
+ To close the redirect gap, keep cleartext traffic disabled for the app in
34
+ `entry/src/main/resources/base/profile/network_config.json` (and reference it
35
+ from `module.json5` via `"metadata": [{ "name": "network_config", "resource": "$profile:network_config" }]`):
36
+
37
+ ```json
38
+ {
39
+ "network-security-config": {
40
+ "base-config": {
41
+ "cleartextTrafficPermitted": false
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Build-time metadata (`hvigor-plugin`)
48
+
49
+ Apply the plugin from the **module** hvigorfile (`entry/hvigorfile.ts`):
50
+
51
+ ```ts
52
+ import { hapTasks } from '@ohos/hvigor-ohos-plugin';
53
+ import { reactNativeUpdatePlugin } from 'pushy/hvigor-plugin';
54
+
55
+ export default {
56
+ system: hapTasks,
57
+ plugins: [reactNativeUpdatePlugin()],
58
+ };
59
+ ```
60
+
61
+ It writes `src/main/resources/rawfile/meta.json` (`pushy_build_time`,
62
+ `versionName`) into that module during `assemble*` builds only, before
63
+ resources are compiled. Debug builds get `pushy_build_time: "0"` like Android,
64
+ so a debug rebuild does not look like a new binary. Add the generated file to
65
+ `.gitignore`.
66
+
67
+ ## Versioning
68
+
69
+ `oh-package.json5` `version` mirrors the npm package version;
70
+ `scripts/build-harmony-har.js` rewrites it before `assembleHar` and
71
+ `scripts/check-release-version.js` asserts the committed value matches.
72
+
73
+ This file is packaged with the npm module (`harmony/pushy/src/`) so it is
74
+ available next to the sources in `node_modules/react-native-update`.
@@ -71,8 +71,18 @@ if(TARGET rnoh)
71
71
  target_link_libraries(rnoh_pushy PUBLIC rnoh)
72
72
  endif()
73
73
 
74
- # Optimize binary size and performance for Release builds
74
+ # Optimize binary size for Release builds of the NAPI library. --gc-sections
75
+ # only drops anything when every function/data item sits in its own section,
76
+ # and the module registers itself through NAPI_MODULE's static constructor,
77
+ # so no symbol needs default visibility. Scoped to rnupdate: rnoh_pushy must
78
+ # keep exporting PushyTurboModule (JSI_EXPORT) for the host's PackageProvider.
75
79
  if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
76
- # Strip debug symbols and perform garbage collection of unused code sections
77
- set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections -s")
80
+ target_compile_options(rnupdate PRIVATE
81
+ -ffunction-sections
82
+ -fdata-sections
83
+ -fvisibility=hidden
84
+ -fvisibility-inlines-hidden
85
+ )
86
+ # Strip debug symbols and garbage-collect the unused sections.
87
+ target_link_options(rnupdate PRIVATE -Wl,--gc-sections -s)
78
88
  endif()
@@ -45,15 +45,21 @@ jsi::Value CallAsync(
45
45
  return CallAsync(rt, turboModule, #method_name, args, count); \
46
46
  }
47
47
 
48
+ // Only the constants and the RN event plumbing are synchronous. Every
49
+ // Promise-returning spec method (src/NativePushy.ts) MUST be registered
50
+ // with PUSHY_ASYNC_METHOD: a sync registration converts the ArkTS Promise
51
+ // into an empty jsi object and its rejection becomes an unhandled ArkTS
52
+ // rejection, so JS would report e.g. setNeedUpdate as successful while
53
+ // nothing was switched. scripts/check-native-spec-parity.js enforces this.
48
54
  PUSHY_SYNC_METHOD(getConstants)
49
- PUSHY_SYNC_METHOD(setLocalHashInfo)
50
- PUSHY_SYNC_METHOD(getLocalHashInfo)
51
- PUSHY_SYNC_METHOD(setUuid)
52
- PUSHY_SYNC_METHOD(setNeedUpdate)
53
- PUSHY_SYNC_METHOD(markSuccess)
54
55
  PUSHY_SYNC_METHOD(addListener)
55
56
  PUSHY_SYNC_METHOD(removeListeners)
56
57
 
58
+ PUSHY_ASYNC_METHOD(setLocalHashInfo)
59
+ PUSHY_ASYNC_METHOD(getLocalHashInfo)
60
+ PUSHY_ASYNC_METHOD(setUuid)
61
+ PUSHY_ASYNC_METHOD(setNeedUpdate)
62
+ PUSHY_ASYNC_METHOD(markSuccess)
57
63
  PUSHY_ASYNC_METHOD(reloadUpdate)
58
64
  PUSHY_ASYNC_METHOD(restartApp)
59
65
  PUSHY_ASYNC_METHOD(downloadPatchFromPpk)
@@ -667,12 +667,24 @@ struct CleanupWork {
667
667
  napi_async_work work = nullptr;
668
668
  napi_deferred deferred = nullptr;
669
669
  std::string root_dir;
670
- std::string keep_current;
671
- std::string keep_previous;
670
+ // Every version that may still be in use: persisted current/last AND the
671
+ // version this process booted from (two switches without a restart must not
672
+ // evict the running bundle while its on-demand assets are still served).
673
+ std::vector<std::string> keep_names;
672
674
  int32_t max_age_days = 0;
673
675
  pushy::patch::Status status{false, ""};
674
676
  };
675
677
 
678
+ // Streaming file digest on a worker thread. A full package or bundle is tens
679
+ // of MB; hashing it synchronously on the UI thread (where the TurboModule
680
+ // runs) froze the app for hundreds of ms per archive/bundle/activation.
681
+ struct Sha256FileWork {
682
+ napi_async_work work = nullptr;
683
+ napi_deferred deferred = nullptr;
684
+ std::string path;
685
+ std::string hex;
686
+ };
687
+
676
688
  napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
677
689
  napi_value args[1] = {nullptr};
678
690
  size_t argc = 1;
@@ -785,10 +797,11 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
785
797
  return promise;
786
798
  }
787
799
 
800
+ // cleanupOldEntries(rootDir, keepNames: string[], maxAgeDays)
788
801
  napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
789
- napi_value args[4] = {nullptr, nullptr, nullptr, nullptr};
790
- size_t argc = 4;
791
- if (!GetArgCount(env, info, &argc, args) || argc < 4) {
802
+ napi_value args[3] = {nullptr, nullptr, nullptr};
803
+ size_t argc = 3;
804
+ if (!GetArgCount(env, info, &argc, args) || argc < 3) {
792
805
  ThrowError(env, "Wrong number of arguments");
793
806
  return nullptr;
794
807
  }
@@ -798,23 +811,18 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
798
811
  if (!ok) {
799
812
  return nullptr;
800
813
  }
801
- const std::string keep_current = GetString(env, args[1], &ok);
802
- if (!ok) {
803
- return nullptr;
804
- }
805
- const std::string keep_previous = GetString(env, args[2], &ok);
806
- if (!ok) {
814
+ std::vector<std::string> keep_names;
815
+ if (!GetStringArray(env, args[1], &keep_names)) {
807
816
  return nullptr;
808
817
  }
809
818
  int32_t max_age_days = 0;
810
- if (!GetInt32(env, args[3], &max_age_days)) {
819
+ if (!GetInt32(env, args[2], &max_age_days)) {
811
820
  return nullptr;
812
821
  }
813
822
 
814
823
  auto* work_data = new CleanupWork();
815
824
  work_data->root_dir = root_dir;
816
- work_data->keep_current = keep_current;
817
- work_data->keep_previous = keep_previous;
825
+ work_data->keep_names = std::move(keep_names);
818
826
  work_data->max_age_days = max_age_days;
819
827
 
820
828
  napi_value promise = nullptr;
@@ -834,7 +842,7 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
834
842
  [](napi_env, void* data) {
835
843
  auto* w = static_cast<CleanupWork*>(data);
836
844
  w->status = pushy::patch::CleanupOldEntries(
837
- w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
845
+ w->root_dir, w->keep_names, w->max_age_days);
838
846
  },
839
847
  [](napi_env cb_env, napi_status status, void* data) {
840
848
  auto* w = static_cast<CleanupWork*>(data);
@@ -872,6 +880,76 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
872
880
  return promise;
873
881
  }
874
882
 
883
+ // sha256HexFileAsync(path) -> Promise<hex>. Rejects when the file cannot be
884
+ // read (Sha256File returns an empty digest in that case).
885
+ napi_value Sha256HexFileAsync(napi_env env, napi_callback_info info) {
886
+ napi_value args[1] = {nullptr};
887
+ size_t argc = 1;
888
+ if (!GetArgCount(env, info, &argc, args) || argc < 1) {
889
+ ThrowError(env, "sha256HexFileAsync: missing path argument");
890
+ return nullptr;
891
+ }
892
+ bool ok = false;
893
+ std::string path = GetString(env, args[0], &ok);
894
+ if (!ok) {
895
+ return nullptr;
896
+ }
897
+ if (path.empty()) {
898
+ ThrowError(env, "sha256HexFileAsync: path must be a non-empty string");
899
+ return nullptr;
900
+ }
901
+
902
+ auto* work_data = new Sha256FileWork();
903
+ work_data->path = std::move(path);
904
+
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");
909
+ return nullptr;
910
+ }
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;
942
+ }
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;
949
+ }
950
+ return promise;
951
+ }
952
+
875
953
  bool ExportFunction(
876
954
  napi_env env,
877
955
  napi_value exports,
@@ -898,7 +976,8 @@ bool ExportFunction(
898
976
  // rawfile 里的 bundle(已整体读入内存,与 pdiff 的读法一致),哈希本身毫秒级。
899
977
  static napi_value MakeUtf8String(napi_env env, const std::string& value);
900
978
 
901
- // Streaming file digest (bounded memory) for the install record.
979
+ // Streaming file digest (bounded memory). Synchronous: small files only —
980
+ // archives and bundles go through sha256HexFileAsync.
902
981
  static napi_value Sha256HexFile(napi_env env, napi_callback_info info) {
903
982
  size_t argc = 1;
904
983
  napi_value args[1] = {nullptr};
@@ -909,7 +988,7 @@ static napi_value Sha256HexFile(napi_env env, napi_callback_info info) {
909
988
  bool ok = false;
910
989
  std::string path = GetString(env, args[0], &ok);
911
990
  if (!ok) {
912
- ThrowError(env, "sha256HexFile: path must be a string");
991
+ // GetString already raised the pending exception.
913
992
  return nullptr;
914
993
  }
915
994
  return MakeUtf8String(env, pushy::digest::Sha256File(path));
@@ -1089,8 +1168,13 @@ static napi_value FlowIsValidCheckResponse(napi_env env,
1089
1168
  }
1090
1169
  bool ok = false;
1091
1170
  std::string response_text = GetString(env, args[0], &ok);
1171
+ 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
+ return nullptr;
1175
+ }
1092
1176
  napi_value result = nullptr;
1093
- napi_get_boolean(env, ok && updateflow::IsValidCheckResponse(response_text),
1177
+ napi_get_boolean(env, updateflow::IsValidCheckResponse(response_text),
1094
1178
  &result);
1095
1179
  return result;
1096
1180
  }
@@ -1135,6 +1219,7 @@ napi_value Init(napi_env env, napi_value exports) {
1135
1219
  !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
1136
1220
  !ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
1137
1221
  !ExportFunction(env, exports, "sha256HexFile", Sha256HexFile) ||
1222
+ !ExportFunction(env, exports, "sha256HexFileAsync", Sha256HexFileAsync) ||
1138
1223
  !ExportFunction(env, exports, "crc32", Crc32) ||
1139
1224
  !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) ||
1140
1225
  !ExportFunction(env, exports, "buildCheckRequestBody", FlowBuildCheckRequestBody) ||