react-native-update 10.59.1 → 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 +11 -12
  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 +19 -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 -2628
  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 +74 -83
  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
@@ -6,10 +6,12 @@ import type { UpdateContext } from './UpdateContext';
6
6
  import { BundlePreparationRound, bundlePreparationResult } from './BundlePreparationResult';
7
7
  import type { BundlePreparationResult } from './BundlePreparationResult';
8
8
  import { isSafePathComponent } from './PathUtils';
9
- import { QUERY_PATH, STATUS_NONE } from './Texts';
9
+ import { STATUS_NONE } from './Texts';
10
10
  import { monotonicNowMs } from './MonotonicClock';
11
11
  import {
12
+ ERROR_DELTA_FAILED,
12
13
  ERROR_DOWNLOAD_FAILED,
14
+ PushyError,
13
15
  createPushyError,
14
16
  getErrorMessage,
15
17
  } from './ErrorCodes';
@@ -38,38 +40,26 @@ const DOWNLOAD_PHASE_TIMEOUT_MS = 10 * 60 * 1000;
38
40
  const DOWNLOAD_TYPE_DIFF = 'diff';
39
41
  const DOWNLOAD_TYPE_PDIFF = 'pdiff';
40
42
 
41
- interface NativeConfig {
42
- appKey?: string;
43
- packageVersion?: string;
44
- endpoints?: string[];
45
- queryUrls?: string[];
46
- afterDownload?: string;
47
- rnu?: string;
48
- rn?: string;
49
- disabled?: boolean;
50
- }
51
-
52
- interface FlowIdentity {
43
+ // 本次启动的事实,交给共享内核(cpp/update_flow_core/native_round.h)。
44
+ interface RoundEnv {
53
45
  packageVersion: string;
54
- currentVersion?: string;
46
+ currentVersion: string;
55
47
  uuid: string;
56
- rolledBackVersion?: string;
48
+ rolledBackVersion: string;
49
+ buildTime: string;
50
+ os: string;
51
+ bundleHash: string;
57
52
  }
58
53
 
59
- interface FlowCInfo {
60
- rnu: string;
61
- rn: string;
62
- os: string;
63
- uuid: string;
54
+ interface PreparedRound {
55
+ body: string;
56
+ query: Object;
64
57
  }
65
58
 
66
- interface FlowInput {
67
- packageVersion: string;
68
- currentVersion?: string;
69
- buildTime: string;
70
- cInfo: FlowCInfo;
71
- supportedDiffVersion: number;
72
- bundleHash: string;
59
+ interface QueryStep {
60
+ url?: string;
61
+ post?: boolean;
62
+ found?: boolean;
73
63
  }
74
64
 
75
65
  interface DecisionAttempt {
@@ -77,35 +67,16 @@ interface DecisionAttempt {
77
67
  urls?: string[];
78
68
  }
79
69
 
80
- interface DecisionInfoConfig {
81
- forceBoot?: boolean;
82
- }
83
-
84
- interface DecisionInfo {
85
- name?: string;
86
- description?: string;
87
- metaInfo?: string;
88
- // 服务端按版本下发的配置(forceBoot = 救砖指令)。
89
- config?: DecisionInfoConfig;
90
- // 写进 hash 信息:这个版本是被救援通道送进来的,JS 在 markSuccess 时据此
91
- // 上报 forceBootRescue 回执(与 Android/iOS 一致)。
92
- forceBootRescue?: boolean;
93
- }
94
-
95
70
  interface Decision {
96
71
  action?: string;
97
72
  reason?: string;
98
73
  hash?: string;
99
74
  attempts?: DecisionAttempt[];
100
75
  activate?: boolean;
101
- info?: DecisionInfo;
102
- }
103
-
104
- interface RespCacheEntry {
105
- ts: number;
106
- body: string;
107
- request: string;
108
- config: string;
76
+ // 要与版本一起持久化的元信息(JS 侧 setLocalHashInfo;forceBootRescue 由内核标记)。
77
+ hashInfo?: Object;
78
+ // 供 JS 复用的响应缓存文本(§10.3)。
79
+ cache?: string;
109
80
  }
110
81
 
111
82
  let scheduled = false;
@@ -137,28 +108,37 @@ function startNativeRound(
137
108
  });
138
109
  }
139
110
 
111
+ interface ConfigRead {
112
+ json: string;
113
+ /** Set when the persisted configuration cannot start a round. */
114
+ rejection?: BundlePreparationResult;
115
+ }
116
+
117
+ // The single configuration gate shared by the preflight, the host API and
118
+ // the round itself, so the three can never disagree about what is runnable.
119
+ // The verdict is the shared core's (cpp/update_flow_core ParseRunnableConfig,
120
+ // also used by Android and iOS).
121
+ interface RunnableVerdict {
122
+ status?: string;
123
+ reason?: string;
124
+ }
125
+
126
+ function readNativeConfig(context: UpdateContext): ConfigRead {
127
+ const json = context.getKv(KEY_CONFIG);
128
+ const verdict = JSON.parse(NativePatchCore.runnableConfig(json)) as RunnableVerdict;
129
+ if (verdict.status !== undefined) {
130
+ return {
131
+ json,
132
+ rejection: bundlePreparationResult(verdict.status, verdict.reason ?? ''),
133
+ };
134
+ }
135
+ return { json };
136
+ }
137
+
140
138
  // A preflight skip must not consume the process's only round: configuration
141
139
  // may arrive after the delayed startup timer on a first-ever launch.
142
140
  function configurationError(context: UpdateContext): BundlePreparationResult | undefined {
143
- const json = context.getKv(KEY_CONFIG);
144
- if (!json) {
145
- return bundlePreparationResult('skipped', 'not_configured');
146
- }
147
- try {
148
- const config = JSON.parse(json) as NativeConfig;
149
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
150
- return bundlePreparationResult('failed', 'invalid_config');
151
- }
152
- if (config.disabled) {
153
- return bundlePreparationResult('skipped', 'disabled');
154
- }
155
- if (typeof config.appKey !== 'string' || config.appKey.trim().length === 0) {
156
- return bundlePreparationResult('failed', 'invalid_config');
157
- }
158
- } catch (e) {
159
- return bundlePreparationResult('failed', 'invalid_config');
160
- }
161
- return undefined;
141
+ return readNativeConfig(context).rejection;
162
142
  }
163
143
 
164
144
  export async function prepareBundleNative(
@@ -167,24 +147,11 @@ export async function prepareBundleNative(
167
147
  if (scheduledContext !== context) {
168
148
  return bundlePreparationResult('skipped', 'not_initialized');
169
149
  }
170
- const configJson = context.getKv(KEY_CONFIG);
171
- if (!configJson) {
172
- return bundlePreparationResult('skipped', 'not_configured');
173
- }
174
- try {
175
- const config = JSON.parse(configJson) as NativeConfig;
176
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
177
- return bundlePreparationResult('failed', 'invalid_config');
178
- }
179
- if (config.disabled) {
180
- return bundlePreparationResult('skipped', 'disabled');
181
- }
182
- if (typeof config.appKey !== 'string' || !config.appKey) {
183
- return bundlePreparationResult('failed', 'invalid_config');
184
- }
185
- } catch (e) {
186
- return bundlePreparationResult('failed', 'invalid_config');
150
+ const read = readNativeConfig(context);
151
+ if (read.rejection !== undefined) {
152
+ return read.rejection;
187
153
  }
154
+ const configJson = read.json;
188
155
  const result = await startNativeRound(context, scheduledRollback);
189
156
  if (roundConfigGeneration !== context.getNativeConfigGeneration()
190
157
  || configJson !== roundConfigJson || configJson !== context.getKv(KEY_CONFIG)) {
@@ -205,13 +172,11 @@ export function recordJsRound(config: string): void {
205
172
  jsCompletedConfig = config;
206
173
  }
207
174
 
208
- // JS 已用与原生完全相同的配置拿到有效响应(§10.3):延迟轮次就是一次重复请求。
175
+ // JS 已用与原生等价的配置(C1:规范形式相同,而非逐字相同——宿主持有的配置
176
+ // 与 JS 序列化格式不同)拿到有效响应(§10.3):延迟轮次就是一次重复请求。
209
177
  // 只有计划内的轮次会问这个问题——JS 没起来、启动即崩、检查失败时都不会有信号。
210
178
  function hasJsRound(context: UpdateContext): boolean {
211
- return (
212
- jsCompletedConfig !== undefined &&
213
- jsCompletedConfig === context.getKv(KEY_CONFIG)
214
- );
179
+ return !!jsCompletedConfig && context.isStoredNativeConfig(jsCompletedConfig);
215
180
  }
216
181
 
217
182
  export function scheduleNativeSync(
@@ -252,33 +217,13 @@ async function runOnce(
252
217
  roundGeneration = resetGeneration;
253
218
  roundConfigGeneration = context.getNativeConfigGeneration();
254
219
  roundResult = bundlePreparationResult('failed', 'check_failed');
255
- const configJson = context.getKv(KEY_CONFIG);
256
- roundConfigJson = configJson;
257
- if (!configJson) {
258
- // No persisted configuration: report the rollout gate to native callers.
259
- roundResult = bundlePreparationResult('skipped', 'not_configured');
260
- return;
261
- }
262
- let config: NativeConfig;
263
- try {
264
- config = JSON.parse(configJson) as NativeConfig;
265
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
266
- roundResult = bundlePreparationResult('failed', 'invalid_config');
267
- return;
268
- }
269
- } catch (e) {
270
- roundResult = bundlePreparationResult('failed', 'invalid_config');
271
- return;
272
- }
273
- if (config.disabled) {
274
- roundResult = bundlePreparationResult('skipped', 'disabled');
275
- return;
276
- }
277
- const appKey = config.appKey ?? '';
278
- if (!appKey) {
279
- roundResult = bundlePreparationResult('failed', 'invalid_config');
220
+ const read = readNativeConfig(context);
221
+ roundConfigJson = read.json;
222
+ if (read.rejection !== undefined) {
223
+ roundResult = read.rejection;
280
224
  return;
281
225
  }
226
+ const configJson = read.json;
282
227
  // 从这里起本轮开始做真实工作:留下面包屑,死于轮中时下次启动零延迟续传。
283
228
  logger.info(
284
229
  TAG,
@@ -296,8 +241,6 @@ async function runOnce(
296
241
  launchRolledBackVersion,
297
242
  resetGeneration,
298
243
  configJson,
299
- config,
300
- appKey,
301
244
  );
302
245
  } finally {
303
246
  try {
@@ -309,103 +252,84 @@ async function runOnce(
309
252
  }
310
253
  }
311
254
 
312
- // 配置端点全为 https 时,网络下发的端点列表/制品 URL 一律拒绝明文 http:
313
- // @ohos.net.http 内部跟随重定向且不暴露最终 URL,这是鸿蒙侧唯一能做的
314
- // https→http 降级防线(SR-1);其余依赖宿主 network_config.json 的
315
- // cleartextTrafficPermitted:false(见 harmony/pushy/src/README.md)。
316
- function isHttpsOnly(config: NativeConfig): boolean {
317
- const endpoints = config.endpoints ?? [];
318
- return (
319
- endpoints.length > 0 &&
320
- endpoints.every(endpoint => isHttpsUrl(endpoint))
321
- );
322
- }
323
-
324
- function isHttpsUrl(url: string): boolean {
325
- return typeof url === 'string' && url.toLowerCase().startsWith('https://');
326
- }
327
-
328
- function isCleartextUrl(url: string): boolean {
329
- return typeof url === 'string' && url.toLowerCase().startsWith('http://');
330
- }
331
-
332
255
  async function runConfiguredRound(
333
256
  context: UpdateContext,
334
257
  launchRolledBackVersion: string,
335
258
  resetGeneration: number,
336
259
  configJson: string,
337
- config: NativeConfig,
338
- appKey: string,
339
260
  ): Promise<void> {
340
261
  const currentVersion = context.getCurrentVersion();
341
- // getConstants consumes the persisted rollback marker during startup; use
342
- // the launch-path snapshot captured before that happens.
343
- const rolledBackVersion = launchRolledBackVersion;
344
- const uuid = context.getKv('uuid') ?? '';
345
- const packageVersion = config.packageVersion || context.getPackageVersion();
346
-
347
- const identity: FlowIdentity = {
348
- packageVersion,
262
+ const env: RoundEnv = {
263
+ packageVersion: context.getPackageVersion(),
264
+ // 与 JS 常量同形(无版本时为 ''),JS 才能复用这次请求的响应缓存。
349
265
  currentVersion,
350
- uuid,
351
- };
352
- if (rolledBackVersion) {
353
- identity.rolledBackVersion = rolledBackVersion;
354
- }
355
-
356
- const cInfo: FlowCInfo = {
357
- rnu: config.rnu ?? '',
358
- rn: config.rn ?? '',
266
+ uuid: context.getKv('uuid') ?? '',
267
+ // getConstants consumes the persisted rollback marker during startup; use
268
+ // the launch-path snapshot captured before that happens.
269
+ rolledBackVersion: launchRolledBackVersion,
270
+ buildTime: context.getBuildTime(),
359
271
  // RNOH's Platform.Version is osFullName; use the same value so JS can
360
272
  // reuse the response cache produced by this native request.
361
273
  os: `harmony ${deviceInfo.osFullName}`,
362
- uuid,
363
- };
364
-
365
- const input: FlowInput = {
366
- packageVersion: identity.packageVersion,
367
- currentVersion,
368
- buildTime: context.getBuildTime(),
369
- cInfo,
370
- supportedDiffVersion: NativePatchCore.getSupportedDiffVersion(),
371
274
  bundleHash: await context.getBundleHash(),
372
275
  };
373
- const body = NativePatchCore.buildRequestBody(JSON.stringify(input));
374
- if (!body) {
276
+ const envJson = JSON.stringify(env);
277
+ const preparedJson = NativePatchCore.prepareRound(configJson, envJson, Math.random());
278
+ if (!preparedJson) {
375
279
  roundResult = bundlePreparationResult('failed', 'invalid_request');
376
280
  return;
377
281
  }
378
-
379
- const httpsOnly = isHttpsOnly(config);
380
- const responseText = await runQueryRequest(config, appKey, body, httpsOnly);
282
+ const prepared = JSON.parse(preparedJson) as PreparedRound;
283
+ const responseText = await runQueryRequest(
284
+ JSON.stringify(prepared.query),
285
+ prepared.body,
286
+ );
381
287
  if (!responseText) {
382
288
  logger.warn(TAG, 'no endpoint reachable, giving up until next launch');
383
289
  return;
384
290
  }
385
- // Anchor cache freshness to response arrival, before download/patch work.
386
- const responseAtSeconds = Math.floor(Date.now() / 1000);
387
-
291
+ // 规划好的 attempts、hashInfo 与响应缓存(时间戳取响应到达时)都来自共享内核。
388
292
  const decisionJson = NativePatchCore.handleResponse(
389
293
  responseText,
390
- JSON.stringify(identity),
391
- config.afterDownload ?? '',
294
+ configJson,
295
+ envJson,
296
+ prepared.body,
297
+ Math.floor(Date.now() / 1000),
392
298
  );
393
299
  if (!decisionJson) {
394
300
  roundResult = bundlePreparationResult('failed', 'invalid_response');
395
301
  return;
396
302
  }
397
303
  const decision = JSON.parse(decisionJson) as Decision;
398
- if (decision.action !== 'download') {
304
+ const responseCacheJson = decision.cache ?? '';
305
+ // Commits the round's persistent outcome (see commitSyncResult) and maps
306
+ // it to the round result; a reset during the round wins over `result`.
307
+ const commit = async (
308
+ hash: string,
309
+ hashInfoJson: string,
310
+ activate: boolean,
311
+ result: BundlePreparationResult,
312
+ ): Promise<boolean> => {
399
313
  const committed = await context.commitSyncResult(
400
314
  resetGeneration,
315
+ hash,
316
+ hashInfoJson,
317
+ activate,
318
+ responseCacheJson,
319
+ );
320
+ roundResult = committed
321
+ ? result
322
+ : bundlePreparationResult('cancelled', 'reset');
323
+ return committed;
324
+ };
325
+
326
+ if (decision.action !== 'download') {
327
+ await commit(
401
328
  '',
402
329
  '',
403
330
  false,
404
- buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
331
+ bundlePreparationResult(STATUS_NONE, decision.reason ?? ''),
405
332
  );
406
- roundResult = committed
407
- ? bundlePreparationResult(STATUS_NONE, decision.reason ?? '')
408
- : bundlePreparationResult('cancelled', 'reset');
409
333
  logger.info(TAG, `nothing to do (${decision.reason ?? ''})`);
410
334
  return;
411
335
  }
@@ -426,46 +350,15 @@ async function runConfiguredRound(
426
350
  decision.attempts ?? [],
427
351
  hash,
428
352
  currentVersion,
429
- httpsOnly,
430
353
  );
431
354
  }
432
355
  if (!downloaded) {
433
356
  logger.warn(TAG, `all download attempts for ${hash} failed`);
434
- const committed = await context.commitSyncResult(
435
- resetGeneration,
436
- '',
437
- '',
438
- false,
439
- buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
440
- );
441
- roundResult = committed
442
- ? bundlePreparationResult('failed', 'download_failed')
443
- : bundlePreparationResult('cancelled', 'reset');
357
+ await commit('', '', false, bundlePreparationResult('failed', 'download_failed'));
444
358
  return;
445
359
  }
446
360
 
447
- // 与 JS 侧下载成功后的 setLocalHashInfo 对齐,持久化版本元信息。
448
- const info = decision.info;
449
- let hashInfoJson = '';
450
- if (info) {
451
- const hashInfo: DecisionInfo = {};
452
- if (typeof info.name === 'string') {
453
- hashInfo.name = info.name;
454
- }
455
- if (typeof info.description === 'string') {
456
- hashInfo.description = info.description;
457
- }
458
- if (typeof info.metaInfo === 'string') {
459
- hashInfo.metaInfo = info.metaInfo;
460
- }
461
- // forceBoot 激活就是救砖路径:记进持久化元信息,这个版本活到 markSuccess
462
- // 时 JS 上报 forceBootRescue。只认服务端下发的指令——静默策略的激活是
463
- // 普通投递(Android/iOS 同一规则)。
464
- if (info.config && info.config.forceBoot) {
465
- hashInfo.forceBootRescue = true;
466
- }
467
- hashInfoJson = JSON.stringify(hashInfo);
468
- }
361
+ const hashInfoJson = JSON.stringify(decision.hashInfo);
469
362
 
470
363
  // 版本元信息、激活与响应缓存一次性原子提交(见 commitSyncResult);
471
364
  // 缓存只在原生文件/状态工作结束后公开,避免 JS 观察到响应后并发下载。
@@ -473,12 +366,11 @@ async function runConfiguredRound(
473
366
  const activate = decision.activate === true;
474
367
  let committed = false;
475
368
  try {
476
- committed = await context.commitSyncResult(
477
- resetGeneration,
369
+ committed = await commit(
478
370
  hash,
479
371
  hashInfoJson,
480
372
  activate,
481
- buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
373
+ bundlePreparationResult('downloaded', '', hash, activate),
482
374
  );
483
375
  } catch (e) {
484
376
  logger.error(TAG, `commit failed: ${getErrorMessage(e)}`);
@@ -492,35 +384,6 @@ async function runConfiguredRound(
492
384
  } else {
493
385
  logger.info(TAG, `downloaded ${hash}, activation left to JS`);
494
386
  }
495
- roundResult = committed
496
- ? bundlePreparationResult('downloaded', '', hash, activate)
497
- : bundlePreparationResult('cancelled', 'reset');
498
- }
499
-
500
- function buildResponseCacheJson(
501
- configJson: string,
502
- requestBody: string,
503
- responseText: string,
504
- responseAtSeconds: number,
505
- ): string {
506
- const cacheEntry: RespCacheEntry = {
507
- ts: responseAtSeconds,
508
- body: responseText,
509
- request: requestBody,
510
- config: configJson,
511
- };
512
- return JSON.stringify(cacheEntry);
513
- }
514
-
515
- function isValidResponse(responseText: string | undefined): boolean {
516
- if (responseText === undefined) {
517
- return false;
518
- }
519
- try {
520
- return NativePatchCore.isValidResponse(responseText);
521
- } catch (e) {
522
- return false;
523
- }
524
387
  }
525
388
 
526
389
  async function httpRequest(
@@ -568,92 +431,32 @@ async function httpRequest(
568
431
  }
569
432
  }
570
433
 
571
- // 顺序回退(§5.1):按纯层给出的候选序逐个请求,单请求超时;整轮失败后经
572
- // queryUrls 发现远程候选(排除已试过的)再来一轮。刻意不做 hedged race——
573
- // 该路径对延迟不敏感。
434
+ // 顺序回退(§5.1):按共享内核给出的下一步逐个请求,单请求超时
435
+ // (NativePatchCore.nextQueryStep:候选序、queryUrls 发现、远程端点的 https
436
+ // 门槛、请求预算、响应 schema)。刻意不做 hedged race——该路径对延迟不敏感。
574
437
  async function runQueryRequest(
575
- config: NativeConfig,
576
- appKey: string,
438
+ query: string,
577
439
  body: string,
578
- httpsOnly: boolean,
579
440
  ): Promise<string | undefined> {
580
- const orderedJson = NativePatchCore.orderEndpointCandidates(
581
- JSON.stringify(config.endpoints ?? []),
582
- Math.random(),
583
- );
584
- if (!orderedJson) {
585
- return undefined;
586
- }
587
- const ordered = JSON.parse(orderedJson) as string[];
588
- const tried = new Set<string>();
589
- let httpAttempts = 0;
590
- for (const rawBase of ordered) {
591
- const base = normalizeEndpointBase(rawBase);
592
- if (!base || tried.has(base)) {
593
- continue;
441
+ let state: string | undefined = query;
442
+ let text: string | undefined = undefined;
443
+ while (state !== undefined) {
444
+ state = NativePatchCore.nextQueryStep(state, text);
445
+ if (state === undefined) {
446
+ break;
594
447
  }
595
- if (httpAttempts++ >= MAX_QUERY_HTTP_ATTEMPTS) {
596
- return undefined;
448
+ const step = JSON.parse(state) as QueryStep;
449
+ if (step.found) {
450
+ return text;
597
451
  }
598
- tried.add(base);
599
- const response = await httpRequest(`${base}${QUERY_PATH}${appKey}`, body);
600
- if (isValidResponse(response)) {
601
- return response;
452
+ if (!step.url) {
453
+ break;
602
454
  }
603
- }
604
- for (const listUrl of config.queryUrls ?? []) {
605
- if (!listUrl) {
606
- continue;
607
- }
608
- if (httpAttempts++ >= MAX_QUERY_HTTP_ATTEMPTS) {
609
- return undefined;
610
- }
611
- const listText = await httpRequest(listUrl);
612
- if (listText === undefined) {
613
- continue;
614
- }
615
- let remote: string[];
616
- try {
617
- const parsed = JSON.parse(listText) as Object;
618
- if (!Array.isArray(parsed)) {
619
- continue;
620
- }
621
- remote = parsed as string[];
622
- } catch (e) {
623
- continue;
624
- }
625
- for (const rawBase of remote) {
626
- if (typeof rawBase !== 'string') {
627
- continue;
628
- }
629
- const base = normalizeEndpointBase(rawBase);
630
- if (!base || tried.has(base)) {
631
- continue;
632
- }
633
- if (httpsOnly && !isHttpsUrl(base)) {
634
- // 远程注入的端点不得把 https 配置降级成明文。
635
- logger.warn(TAG, `ignoring non-https remote endpoint ${base}`);
636
- continue;
637
- }
638
- if (httpAttempts++ >= MAX_QUERY_HTTP_ATTEMPTS) {
639
- return undefined;
640
- }
641
- tried.add(base);
642
- const response = await httpRequest(`${base}${QUERY_PATH}${appKey}`, body);
643
- if (isValidResponse(response)) {
644
- return response;
645
- }
646
- }
647
- // 拉到一份可解析的远程列表就够了。
648
- break;
455
+ text = await httpRequest(step.url, step.post ? body : undefined);
649
456
  }
650
457
  return undefined;
651
458
  }
652
459
 
653
- function normalizeEndpointBase(base: string): string {
654
- return base.replace(/\/+$/, '');
655
- }
656
-
657
460
  async function runWithinDeadline(
658
461
  start: () => Promise<void>,
659
462
  deadlineUptimeMs: number,
@@ -691,16 +494,11 @@ async function performAttempts(
691
494
  attempts: DecisionAttempt[],
692
495
  hash: string,
693
496
  originHash: string,
694
- httpsOnly: boolean,
695
497
  ): Promise<boolean> {
696
498
  const incrementalDeadline = monotonicNowMs() + DOWNLOAD_PHASE_TIMEOUT_MS;
697
499
  let fullDeadline = 0;
698
500
  for (const attempt of attempts) {
699
501
  const type = attempt.type ?? '';
700
- if (type === DOWNLOAD_TYPE_DIFF && !originHash) {
701
- // diff 以当前运行版本为源;没有运行中的热更版本就跳过。
702
- continue;
703
- }
704
502
  const isFullAttempt =
705
503
  type !== DOWNLOAD_TYPE_DIFF && type !== DOWNLOAD_TYPE_PDIFF;
706
504
  if (isFullAttempt && fullDeadline === 0) {
@@ -710,14 +508,6 @@ async function performAttempts(
710
508
  }
711
509
  const deadline = isFullAttempt ? fullDeadline : incrementalDeadline;
712
510
  for (const url of attempt.urls ?? []) {
713
- if (!url) {
714
- continue;
715
- }
716
- if (httpsOnly && isCleartextUrl(url)) {
717
- // 决策里的明文制品 URL 不得把 https 配置降级(SR-1)。
718
- logger.warn(TAG, `ignoring cleartext ${type} url ${url}`);
719
- continue;
720
- }
721
511
  if (monotonicNowMs() >= deadline) {
722
512
  if (isFullAttempt) {
723
513
  return false;
@@ -744,8 +534,19 @@ async function performAttempts(
744
534
  return true;
745
535
  } catch (e) {
746
536
  logger.warn(TAG, `${type} attempt failed: ${getErrorMessage(e)}`);
537
+ if (!isMirrorRetryable(e)) {
538
+ // The bytes arrived but could not be applied: every mirror serves
539
+ // the same artifact, so move on to the next strategy instead of
540
+ // re-downloading it (update_flow_core IsMirrorRetryableCode, JS
541
+ // client.ts isMirrorRetryableCode).
542
+ break;
543
+ }
747
544
  }
748
545
  }
749
546
  }
750
547
  return false;
751
548
  }
549
+
550
+ function isMirrorRetryable(error: unknown): boolean {
551
+ return !(error instanceof PushyError && error.code === ERROR_DELTA_FAILED);
552
+ }
@@ -13,12 +13,8 @@ export function revealText(hex: string): string {
13
13
  return text;
14
14
  }
15
15
 
16
- // Request path appended to an endpoint base.
17
- export const QUERY_PATH: string = revealText('7514fcd4ad805d55263e08fc99');
18
-
19
16
  // Protocol values, version-info flags and storage names, stored encoded.
20
17
  export const STATUS_NONE: string = revealText('3418c1c1aa8a7c40');
21
- export const POLICY_NEXT_LAUNCH: string = revealText('2912e0ffab8e6c70323b1dedd3');
22
18
  export const STORAGE_DIR_NAME: string = revealText('0502e4d5af9f6d');
23
19
  export const PREFERENCES_NAME: string = revealText('2f07f0d0ba8e');
24
20
  export const PPK_DELTA_SUFFIX: string = revealText('7407e4dae09b69512137');