react-native-update 10.55.0 → 10.56.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 (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 +406 -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 +36 -3
  63. package/src/client.ts +244 -86
  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
@@ -25,23 +25,47 @@ import NativePatchCore, {
25
25
  StateCoreResult,
26
26
  } from './NativePatchCore';
27
27
  import { assertSafePathComponent } from './PathUtils';
28
+ import {
29
+ ERROR_SWITCH_VERSION_FAILED,
30
+ createUpdateError,
31
+ getErrorMessage,
32
+ toUpdateError,
33
+ } from './ErrorCodes';
28
34
 
29
35
  export { isSafePathComponent } from './PathUtils';
30
36
 
31
- type FlushablePreferences = preferences.Preferences & {
37
+ const TAG = 'UpdateContext';
38
+ // 常规清理保留最近 3 天内触碰过的条目(续传 partial、staging 同样按 mtime)。
39
+ const CLEANUP_MAX_AGE_DAYS = 3;
40
+
41
+ // Preferences.flushSync 是 API 14 才有、getAllSync 老 SDK 声明里没有:HAR 兼容
42
+ // API 12,按结构化视图探测(不用交叉类型——ArkTS 不支持)。
43
+ interface OptionalPreferencesApi {
32
44
  flushSync?: () => void;
33
- };
45
+ getAllSync?: () => Record<string, unknown>;
46
+ }
47
+
48
+ interface PersistOptions {
49
+ removeStaleHash?: boolean;
50
+ cleanUp?: boolean;
51
+ markFirstLoadMarker?: boolean;
52
+ clearFirstLoadMarker?: boolean;
53
+ }
34
54
 
35
55
  export class UpdateContext {
36
56
  private context: common.UIAbilityContext;
37
57
  private rootDir: string;
38
58
  private preferences!: preferences.Preferences;
59
+ // 宿主传入的调试标志(RNOH isDebugModeEnabled)。debug 下 markSuccess 空转、
60
+ // 不算内嵌 bundle 摘要,与 Android 的 debug 库对齐。HAR 自身没有构建期
61
+ // DEBUG 信号(hvigor 不为 HAR 生成 BuildProfile),所以只看宿主。
39
62
  private static DEBUG: boolean = false;
40
63
  private static isUsingBundleUrl: boolean = false;
41
64
  private static ignoreRollback: boolean = false;
42
65
  // 本进程实际加载的热更版本(getBundleUrl 解析成功时记录)。
43
66
  // resetToPackagedBundle 不能删它的目录:热更包内的图片等资源是运行时按需
44
67
  // 读盘的,静默(不重启)reset 若删掉会导致后续所有未加载过的资源失败。
68
+ // 常规清理同样必须保留它:一次进程内两次切换后 current/last 都不再是它。
45
69
  private static launchVersion: string = '';
46
70
  // 由 resetToPackagedBundle 递增。原生冷启动检测可能跑数分钟并已握有决策,
47
71
  // 期间发生的 reset 必须赢:编排器采样该值,发现变化即放弃激活与响应缓存,
@@ -55,7 +79,18 @@ export class UpdateContext {
55
79
  private static instanceCounter: number = 0;
56
80
  private readonly instanceId: string;
57
81
 
58
- public static getInstance(context: common.UIAbilityContext): UpdateContext {
82
+ /**
83
+ * @param isDebugHost 宿主的调试标志(RNOH isDebugModeEnabled)。bundle
84
+ * provider 先于 TurboModule 构造,不传该参数;TurboModule 随后显式传入时
85
+ * 会重新解析。
86
+ */
87
+ public static getInstance(
88
+ context: common.UIAbilityContext,
89
+ isDebugHost?: boolean,
90
+ ): UpdateContext {
91
+ if (isDebugHost !== undefined) {
92
+ UpdateContext.DEBUG = isDebugHost;
93
+ }
59
94
  if (!UpdateContext.instance) {
60
95
  UpdateContext.instance = new UpdateContext(context);
61
96
  }
@@ -72,7 +107,7 @@ export class UpdateContext {
72
107
  fileIo.mkdirSync(this.rootDir);
73
108
  }
74
109
  } catch (e) {
75
- console.error('Failed to create root directory:', e);
110
+ logger.error(TAG, `Failed to create root directory: ${getErrorMessage(e)}`);
76
111
  }
77
112
  this.initPreferences();
78
113
  this.trace('ctor');
@@ -89,7 +124,7 @@ export class UpdateContext {
89
124
  private trace(point: string): void {
90
125
  const snap = this.getStateSnapshot();
91
126
  logger.debug(
92
- 'UpdateContext',
127
+ TAG,
93
128
  `trace id=${this.instanceId} ${point}` +
94
129
  ` pkg=${snap.packageVersion} bt=${snap.buildTime}` +
95
130
  ` cv=${snap.currentVersion} lv=${snap.lastVersion}` +
@@ -115,11 +150,15 @@ export class UpdateContext {
115
150
  // which disables rollback protection. Rethrow so the failure surfaces at
116
151
  // construction time instead of later as an unrelated TypeError on the
117
152
  // undefined `preferences` handle.
118
- console.error('Failed to init preferences:', e);
153
+ logger.error(TAG, `Failed to init preferences: ${getErrorMessage(e)}`);
119
154
  throw e;
120
155
  }
121
156
  }
122
157
 
158
+ private optionalPreferencesApi(): OptionalPreferencesApi {
159
+ return this.preferences as unknown as OptionalPreferencesApi;
160
+ }
161
+
123
162
  private getBundleFlags(): bundleManager.BundleFlag {
124
163
  return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
125
164
  }
@@ -135,7 +174,7 @@ export class UpdateContext {
135
174
  UpdateContext.cachedPackageVersion = bundleInfo?.versionName || 'Unknown';
136
175
  return UpdateContext.cachedPackageVersion;
137
176
  } catch (error) {
138
- console.error('Failed to get bundle info:', error);
177
+ logger.error(TAG, `Failed to get bundle info: ${getErrorMessage(error)}`);
139
178
  return '';
140
179
  }
141
180
  }
@@ -155,7 +194,10 @@ export class UpdateContext {
155
194
  return UpdateContext.cachedBuildTime;
156
195
  }
157
196
  } catch (error) {
158
- console.error('Failed to read build time from raw file:', error);
197
+ logger.error(
198
+ TAG,
199
+ `Failed to read build time from raw file: ${getErrorMessage(error)}`,
200
+ );
159
201
  }
160
202
  return '';
161
203
  }
@@ -213,33 +255,56 @@ export class UpdateContext {
213
255
  this.flushBatchDepth += 1;
214
256
  }
215
257
 
216
- private endFlushBatch(reason: string): void {
258
+ /** 关闭最外层批次时执行唯一一次落盘;返回该次落盘的 promise。 */
259
+ private endFlushBatch(reason: string): Promise<void> {
217
260
  this.flushBatchDepth = Math.max(0, this.flushBatchDepth - 1);
218
261
  if (this.flushBatchDepth === 0) {
219
- this.flushPreferences(reason);
262
+ return this.flushPreferences(reason);
220
263
  }
264
+ return Promise.resolve();
221
265
  }
222
266
 
223
- private flushPreferences(reason: string): void {
267
+ /**
268
+ * 落盘。flushSync(API 14)可用时同步完成;否则返回 preferences.flush() 的
269
+ * promise。有 promise 可拒绝的操作(切换、markSuccess、setKv…)必须 await
270
+ * 它——切换只有真正写进磁盘后才能向 JS 报成功,reloadUpdate 紧接着就会
271
+ * 杀进程(SR-5,Android 是 persistEditorOrThrow)。无法 await 的同步路径
272
+ * (启动解析、getConstants)用 flushInBackground,失败只记日志。
273
+ * 批次开着时不落盘,由 endFlushBatch 统一执行。
274
+ */
275
+ private flushPreferences(reason: string): Promise<void> {
224
276
  if (this.flushBatchDepth > 0) {
225
- return;
277
+ return Promise.resolve();
226
278
  }
227
- const flushablePreferences = this.preferences as FlushablePreferences;
228
- if (typeof flushablePreferences.flushSync === 'function') {
279
+ const api = this.optionalPreferencesApi();
280
+ if (typeof api.flushSync === 'function') {
229
281
  try {
230
- flushablePreferences.flushSync();
231
- return;
282
+ api.flushSync();
283
+ return Promise.resolve();
232
284
  } catch (error) {
233
- console.error(`Failed to flushSync preferences for ${reason}:`, error);
234
- // fall through to async flush rather than failing the whole operation
285
+ logger.error(
286
+ TAG,
287
+ `flushSync failed for ${reason}, falling back to flush(): ` +
288
+ getErrorMessage(error),
289
+ );
290
+ // fall through to the async flush rather than failing outright
235
291
  }
236
292
  }
237
- // flushSync unavailable or failed: writes are already applied in memory via
238
- // putSync/deleteSync; persist asynchronously as a best-effort so the state
239
- // operation still succeeds instead of throwing (which is worse than a
240
- // slightly delayed persist).
241
- this.preferences.flush().catch((error: Object) => {
242
- console.error(`Failed to flush preferences for ${reason}:`, error);
293
+ return this.preferences.flush().then(
294
+ () => undefined,
295
+ (error: Object) => {
296
+ logger.error(
297
+ TAG,
298
+ `Failed to flush preferences for ${reason}: ${getErrorMessage(error)}`,
299
+ );
300
+ throw error;
301
+ },
302
+ );
303
+ }
304
+
305
+ private flushInBackground(reason: string): void {
306
+ this.flushPreferences(reason).catch(() => {
307
+ // 已在 flushPreferences 里记录。
243
308
  });
244
309
  }
245
310
 
@@ -277,19 +342,8 @@ export class UpdateContext {
277
342
  this.putNullableString('rolledBackVersion', state.rolledBackVersion);
278
343
  }
279
344
 
280
- private persistState(
281
- state: StateCoreResult,
282
- options: {
283
- clearExisting?: boolean;
284
- removeStaleHash?: boolean;
285
- cleanUp?: boolean;
286
- markFirstLoadMarker?: boolean;
287
- clearFirstLoadMarker?: boolean;
288
- } = {},
289
- ): void {
290
- if (options.clearExisting) {
291
- this.preferences.clear();
292
- }
345
+ /** 内存写入(不落盘):状态字段 + 可选的陈旧 hash / 首载标记 / 清理。 */
346
+ private writeState(state: StateCoreResult, options: PersistOptions): void {
293
347
  this.applyState(state);
294
348
  if (options.removeStaleHash && state.staleVersionToDelete) {
295
349
  this.preferences.deleteSync(`hash_${state.staleVersionToDelete}`);
@@ -300,27 +354,31 @@ export class UpdateContext {
300
354
  if (options.clearFirstLoadMarker) {
301
355
  this.preferences.deleteSync('firstLoadMarked');
302
356
  }
303
- this.flushPreferences('persist state');
304
357
  if (options.cleanUp) {
305
358
  this.cleanUp();
306
359
  }
307
360
  }
308
361
 
362
+ private persistState(
363
+ state: StateCoreResult,
364
+ options: PersistOptions = {},
365
+ ): Promise<void> {
366
+ this.writeState(state, options);
367
+ return this.flushPreferences('persist state');
368
+ }
369
+
370
+ /** 跑一步状态机并写入内存;落盘由调用方决定(await 或后台)。 */
309
371
  private runStateOperation(
310
372
  operation: number,
311
373
  stringArg: string = '',
312
- options: {
313
- removeStaleHash?: boolean;
314
- cleanUp?: boolean;
315
- clearFirstLoadMarker?: boolean;
316
- } = {},
374
+ options: PersistOptions = {},
317
375
  ): StateCoreResult {
318
376
  const nextState = NativePatchCore.runStateCore(
319
377
  operation,
320
378
  this.getStateSnapshot(),
321
379
  stringArg,
322
380
  );
323
- this.persistState(nextState, options);
381
+ this.writeState(nextState, options);
324
382
  return nextState;
325
383
  }
326
384
 
@@ -336,10 +394,11 @@ export class UpdateContext {
336
394
  return params;
337
395
  }
338
396
 
339
- // 串行化下载/补丁任务与破坏性清理(reset 的全量删除):Android 靠单线程
340
- // download executor 天然串行,Harmony 的 NAPI 任务跑在 libuv worker 池上,
341
- // 若不排队,reset 的 RemovePathRecursively 可能与正在写入的解压/打补丁并发,
342
- // 产出"bundle 在、资源半删"的目录且可能被后续 switchVersion 激活。
397
+ // 串行化下载/补丁任务与破坏性清理(reset 的全量删除、按 mtime 的常规清理):
398
+ // Android 靠单线程 download executor 天然串行,Harmony 的 NAPI 任务跑在
399
+ // libuv worker 池上,若不排队,reset 的 RemovePathRecursively 可能与正在
400
+ // 写入的解压/打补丁并发,产出"bundle 在、资源半删"的目录且可能被后续
401
+ // switchVersion 激活。
343
402
  private taskChain: Promise<void> = Promise.resolve();
344
403
 
345
404
  private enqueueSerialTask<T>(job: () => Promise<T>): Promise<T> {
@@ -385,7 +444,7 @@ export class UpdateContext {
385
444
  }
386
445
 
387
446
  logger.info(
388
- 'UpdateContext',
447
+ TAG,
389
448
  `binary version changed, resetting update state id=${this.instanceId}`,
390
449
  );
391
450
  UpdateContext.ignoreRollback = false;
@@ -394,21 +453,23 @@ export class UpdateContext {
394
453
  // rolledBackVersion)。不再 clearExisting,避免连带清除 uuid / firstLoadMarked /
395
454
  // hash_* 等与 binary 版本无关的 KV —— 它们在多实例场景下本就脆弱,连带清除会
396
455
  // 让 getConstants() 永远读到空,从而 isFirstTime=false、markSuccess 永不执行。
397
- this.persistState(nextState);
456
+ this.writeState(nextState, {});
457
+ this.flushInBackground('sync binary version');
398
458
  }
399
459
 
400
- public setKv(key: string, value: string): void {
460
+ /** 写入并落盘;flushSync 不可用时以 flush() 的结果拒绝。 */
461
+ public setKv(key: string, value: string): Promise<void> {
401
462
  this.preferences.putSync(key, value);
402
- this.flushPreferences(`set key ${key}`);
463
+ return this.flushPreferences(`set key ${key}`);
403
464
  }
404
465
 
405
466
  public getKv(key: string): string {
406
467
  return this.readString(key);
407
468
  }
408
469
 
409
- public removeKv(key: string): void {
470
+ public removeKv(key: string): Promise<void> {
410
471
  this.preferences.deleteSync(key);
411
- this.flushPreferences(`remove key ${key}`);
472
+ return this.flushPreferences(`remove key ${key}`);
412
473
  }
413
474
 
414
475
  public isFirstTime(): boolean {
@@ -419,7 +480,7 @@ export class UpdateContext {
419
480
  return this.getStateSnapshot().rolledBackVersion || '';
420
481
  }
421
482
 
422
- public markSuccess(): void {
483
+ public async markSuccess(): Promise<void> {
423
484
  if (UpdateContext.DEBUG) {
424
485
  return;
425
486
  }
@@ -428,27 +489,32 @@ export class UpdateContext {
428
489
  removeStaleHash: true,
429
490
  cleanUp: true,
430
491
  });
492
+ await this.flushPreferences('mark success');
431
493
  }
432
494
 
433
- public clearFirstTime(): void {
495
+ public async clearFirstTime(): Promise<void> {
434
496
  this.runStateOperation(STATE_OP_CLEAR_FIRST_TIME, '', {
435
497
  cleanUp: true,
436
498
  clearFirstLoadMarker: true,
437
499
  });
500
+ await this.flushPreferences('clear first time');
438
501
  }
439
502
 
503
+ /** getConstants(同步)里调用:落盘失败只记日志。 */
440
504
  public clearRollbackMark(): void {
441
505
  this.runStateOperation(STATE_OP_CLEAR_ROLLBACK_MARK, '', {
442
506
  cleanUp: true,
443
507
  });
508
+ this.flushInBackground('clear rollback mark');
444
509
  }
445
510
 
446
511
  /**
447
512
  * 恢复到二进制内置包:清空整个更新状态机(下次启动即回内置 bundle)并删除
448
513
  * 已下载版本——仅保留当前运行版本的目录(静默 reset 不能破坏运行中 bundle
449
514
  * 的按需资源加载)。uuid 保留 —— 它标识安装实例、用于灰度分桶,reset 不应改变。
515
+ * 状态与响应缓存一次落盘;落盘失败拒绝(JS 按 RESET_FAILED 处理)。
450
516
  */
451
- public resetToPackagedBundle(): void {
517
+ public async resetToPackagedBundle(): Promise<void> {
452
518
  this.trace('resetToPackagedBundle:before');
453
519
  // 实时读取二进制身份(与 Android/iOS 对齐),而非 preferences 快照:
454
520
  // meta.json 读取失败时快照可能为空,reset 持久化空值会让下次启动误判
@@ -462,47 +528,50 @@ export class UpdateContext {
462
528
  firstTimeOk: true,
463
529
  rolledBackVersion: '',
464
530
  };
465
- // 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
466
- // uuid —— 见 syncStateWithBinaryVersion 的注释)。getAllSync 在旧 SDK 上
467
- // 可能不存在,此时残留的 hash_* 只是无害孤儿数据,不影响 reset 语义。
468
- const prefsWithGetAll = this.preferences as preferences.Preferences & {
469
- getAllSync?: () => Record<string, unknown>;
470
- };
471
- if (typeof prefsWithGetAll.getAllSync === 'function') {
472
- try {
473
- const all = prefsWithGetAll.getAllSync();
474
- for (const key of Object.keys(all)) {
475
- if (key.startsWith('hash_')) {
476
- this.preferences.deleteSync(key);
477
- }
478
- }
479
- } catch (e: any) {
480
- console.error('Failed to clear hash info on reset:', e);
481
- }
482
- }
483
531
  // 先让在飞的原生检测轮次失效,再清理状态:随后在同一(单线程)执行序里
484
532
  // 提交的轮次会看到新代数并整轮丢弃。
485
533
  UpdateContext.resetGeneration += 1;
486
- this.persistState(resetState, { clearFirstLoadMarker: true });
487
- // 缓存里的响应仍在宣告本次 reset 刚删掉的版本,一并丢弃,避免 JS 侧复用。
534
+ this.beginFlushBatch();
535
+ let flushed: Promise<void> = Promise.resolve();
488
536
  try {
537
+ // 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
538
+ // uuid —— 见 syncStateWithBinaryVersion 的注释)。getAllSync 在旧 SDK 上
539
+ // 可能不存在,此时残留的 hash_* 只是无害孤儿数据,不影响 reset 语义。
540
+ const api = this.optionalPreferencesApi();
541
+ if (typeof api.getAllSync === 'function') {
542
+ try {
543
+ const all = api.getAllSync();
544
+ for (const key of Object.keys(all)) {
545
+ if (key.startsWith('hash_')) {
546
+ this.preferences.deleteSync(key);
547
+ }
548
+ }
549
+ } catch (e) {
550
+ logger.error(
551
+ TAG,
552
+ `Failed to clear hash info on reset: ${getErrorMessage(e)}`,
553
+ );
554
+ }
555
+ }
556
+ this.writeState(resetState, { clearFirstLoadMarker: true });
557
+ // 缓存里的响应仍在宣告本次 reset 刚删掉的版本,一并丢弃,避免 JS 侧复用。
489
558
  this.preferences.deleteSync(KEY_RESP_CACHE);
490
- } catch (e: any) {
491
- console.error('Failed to clear native check cache on reset:', e);
559
+ } finally {
560
+ flushed = this.endFlushBatch('reset to packaged bundle');
492
561
  }
493
562
  UpdateContext.ignoreRollback = false;
563
+ await flushed;
494
564
 
495
565
  // maxAgeDays=0:删除下载目录内容,仅保留当前运行版本的目录(残留目录由
496
566
  // 下次常规清理回收)。挂到串行任务链尾,避免与在飞的解压/打补丁并发。
497
567
  this.enqueueSerialTask(() =>
498
568
  NativePatchCore.cleanupOldEntries(
499
569
  this.rootDir,
500
- UpdateContext.launchVersion,
501
- '',
570
+ [UpdateContext.launchVersion].filter(name => name.length > 0),
502
571
  0,
503
572
  ),
504
573
  ).catch((error: Object) => {
505
- console.error('reset cleanup failed:', error);
574
+ logger.error(TAG, `reset cleanup failed: ${getErrorMessage(error)}`);
506
575
  });
507
576
  this.trace('resetToPackagedBundle:after');
508
577
  }
@@ -513,37 +582,48 @@ export class UpdateContext {
513
582
  }
514
583
 
515
584
  /**
516
- * 一次性提交原生检测轮次的全部持久化结果(版本元信息、激活、响应缓存):
517
- * 先复核 reset 代数,再落所有写入。ArkTS 单线程 + 本方法内无 await,因此
518
- * 校验与写入之间不存在可插入 reset 的窗口(iOS/Android 用锁达到同一效果)。
519
- * 返回是否提交成功。
585
+ * 一次性提交原生检测轮次的全部持久化结果(版本元信息、激活、响应缓存)。
586
+ * 切换校验(含工作线程上的 bundle 摘要复核)放在第一个 putSync 之前:校验
587
+ * 失败时什么都不写,不会出现"元信息已落盘、切换没发生"的半提交。校验后
588
+ * 复核 reset 代数,之后到写入完成之间没有 await(ArkTS 单线程),因此不存在
589
+ * 可插入 reset 的窗口(iOS/Android 用锁达到同一效果)。三项写入以一次
590
+ * flush 落盘,失败拒绝。返回是否提交成功。
520
591
  */
521
- public commitNativeCheckResult(
592
+ public async commitNativeCheckResult(
522
593
  expectedGeneration: number,
523
594
  hash: string,
524
595
  hashInfoJson: string,
525
596
  activate: boolean,
526
597
  responseCacheJson: string,
527
- ): boolean {
598
+ ): Promise<boolean> {
528
599
  if (UpdateContext.resetGeneration !== expectedGeneration) {
529
600
  return false;
530
601
  }
602
+ const shouldSwitch = activate && !!hash;
603
+ if (shouldSwitch) {
604
+ await this.verifyActivation(hash);
605
+ if (UpdateContext.resetGeneration !== expectedGeneration) {
606
+ return false;
607
+ }
608
+ }
531
609
  // One flush for the whole round (see beginFlushBatch): version info,
532
610
  // activation and response cache land together or not at all.
533
611
  this.beginFlushBatch();
612
+ let flushed: Promise<void> = Promise.resolve();
534
613
  try {
535
614
  if (hash && hashInfoJson) {
536
- this.setKv(`hash_${hash}`, hashInfoJson);
615
+ this.preferences.putSync(`hash_${hash}`, hashInfoJson);
537
616
  }
538
- if (activate && hash) {
539
- this.switchVersion(hash);
617
+ if (shouldSwitch) {
618
+ this.applySwitch(hash);
540
619
  }
541
620
  if (responseCacheJson) {
542
- this.setKv(KEY_RESP_CACHE, responseCacheJson);
621
+ this.preferences.putSync(KEY_RESP_CACHE, responseCacheJson);
543
622
  }
544
623
  } finally {
545
- this.endFlushBatch('commit native check result');
624
+ flushed = this.endFlushBatch('commit native check result');
546
625
  }
626
+ await flushed;
547
627
  return true;
548
628
  }
549
629
 
@@ -563,25 +643,11 @@ export class UpdateContext {
563
643
  params.deadlineUptimeMs = deadlineUptimeMs;
564
644
  await this.executeTask(params);
565
645
  } catch (e) {
566
- console.error('Failed to download full update:', e);
646
+ logger.error(TAG, `Failed to download full update: ${getErrorMessage(e)}`);
567
647
  throw e;
568
648
  }
569
649
  }
570
650
 
571
- public async downloadFile(
572
- url: string,
573
- hash: string,
574
- fileName: string,
575
- ): Promise<void> {
576
- const params = this.createTaskParams(
577
- DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD,
578
- url,
579
- hash,
580
- );
581
- params.targetFile = this.rootDir + '/' + assertSafePathComponent(fileName);
582
- await this.executeTask(params);
583
- }
584
-
585
651
  public async downloadPatchFromPpk(
586
652
  url: string,
587
653
  hash: string,
@@ -617,13 +683,14 @@ export class UpdateContext {
617
683
  params.deadlineUptimeMs = deadlineUptimeMs;
618
684
  return await this.executeTask(params);
619
685
  } catch (e) {
620
- console.error('Failed to download package patch:', e);
686
+ logger.error(
687
+ TAG,
688
+ `Failed to download package patch: ${getErrorMessage(e)}`,
689
+ );
621
690
  throw e;
622
691
  }
623
692
  }
624
693
 
625
- // 原生冷启动检测(NativeCheckOrchestrator)用来跳过已就绪版本的重复下载
626
- // ——alert 类策略下版本已下载但未激活,若不判在这里会每次冷启动重下一遍。
627
694
  // 运行中热更版本安装记录里的 bundleSha256(崩溃归因用);内置 bundle /
628
695
  // 历史安装 / 记录不可读时为空串。
629
696
  public currentBundleSha256(hash: string): string {
@@ -640,6 +707,8 @@ export class UpdateContext {
640
707
  }
641
708
  }
642
709
 
710
+ // 原生冷启动检测(NativeCheckOrchestrator)用来跳过已就绪版本的重复下载
711
+ // ——alert 类策略下版本已下载但未激活,若不判在这里会每次冷启动重下一遍。
643
712
  public hasDownloadedVersion(hash: string): boolean {
644
713
  try {
645
714
  const safeHash = assertSafePathComponent(hash);
@@ -650,40 +719,64 @@ export class UpdateContext {
650
719
  }
651
720
  }
652
721
 
653
- public switchVersion(hash: string): void {
722
+ /**
723
+ * 激活前置校验(同步,不写任何东西):bundle 在盘上,且要么是本 SDK 记录为
724
+ * 完整安装的版本(bundle + 记录),要么是标记机制之前就激活过的历史版本
725
+ * (current/last 里有它)。其他无记录的目录可能是崩溃留下的半成品。
726
+ * 返回是否还需复核安装记录里的 bundle 摘要(历史版本无记录可核)。
727
+ */
728
+ private assertActivatable(safeHash: string): boolean {
729
+ if (!fileIo.accessSync(this.getBundlePath(safeHash))) {
730
+ throw createUpdateError(
731
+ ERROR_SWITCH_VERSION_FAILED,
732
+ `Bundle version ${safeHash} not found.`,
733
+ );
734
+ }
735
+ const legacyActivated =
736
+ this.readString('currentVersion') === safeHash ||
737
+ this.readString('lastVersion') === safeHash;
738
+ if (!this.hasDownloadedVersion(safeHash) && !legacyActivated) {
739
+ throw createUpdateError(
740
+ ERROR_SWITCH_VERSION_FAILED,
741
+ `Bundle version ${safeHash} is incomplete.`,
742
+ );
743
+ }
744
+ return !legacyActivated;
745
+ }
746
+
747
+ /**
748
+ * 完整的激活校验(含 native 工作线程上的 bundle 摘要复核),不写状态。
749
+ * 记录里的 bundle 摘要必须与盘上字节一致,才能把下次启动指向它。
750
+ */
751
+ public async verifyActivation(hash: string): Promise<void> {
752
+ const safeHash = assertSafePathComponent(hash);
753
+ if (this.assertActivatable(safeHash)) {
754
+ await verifyInstallForActivation(
755
+ `${this.rootDir}/${safeHash}`,
756
+ safeHash,
757
+ this.getBundlePath(safeHash),
758
+ );
759
+ }
760
+ }
761
+
762
+ /** 内存里切换状态(不落盘);调用方负责 flush。 */
763
+ private applySwitch(hash: string): void {
764
+ this.trace(`switchVersion:before ${hash}`);
765
+ this.runStateOperation(STATE_OP_SWITCH_VERSION, hash);
766
+ UpdateContext.ignoreRollback = false;
767
+ this.trace(`switchVersion:after ${hash}`);
768
+ }
769
+
770
+ /** 校验 + 切换 + 落盘;任一步失败拒绝(SWITCH_VERSION_FAILED)。 */
771
+ public async switchVersion(hash: string): Promise<void> {
654
772
  try {
655
773
  const safeHash = assertSafePathComponent(hash);
656
- const bundlePath = this.getBundlePath(safeHash);
657
- if (!fileIo.accessSync(bundlePath)) {
658
- throw Error(`Bundle version ${hash} not found.`);
659
- }
660
- // Same rule as Android/iOS: only a version this SDK recorded as
661
- // completely installed (bundle + marker) may be activated. Versions
662
- // activated before markers existed are grandfathered through
663
- // current/last state; any other markerless directory may be a
664
- // crash-left partial install.
665
- const legacyActivated =
666
- this.readString('currentVersion') === safeHash ||
667
- this.readString('lastVersion') === safeHash;
668
- if (!this.hasDownloadedVersion(safeHash) && !legacyActivated) {
669
- throw Error(`Bundle version ${hash} is incomplete.`);
670
- }
671
- if (!legacyActivated) {
672
- // 记录里的 bundle 摘要必须与盘上字节一致,才能把下次启动指向它。
673
- verifyInstallForActivation(
674
- `${this.rootDir}/${safeHash}`,
675
- safeHash,
676
- bundlePath,
677
- );
678
- }
679
-
680
- this.trace(`switchVersion:before ${hash}`);
681
- this.runStateOperation(STATE_OP_SWITCH_VERSION, hash);
682
- UpdateContext.ignoreRollback = false;
683
- this.trace(`switchVersion:after ${hash}`);
774
+ await this.verifyActivation(safeHash);
775
+ this.applySwitch(safeHash);
776
+ await this.flushPreferences(`switch version ${safeHash}`);
684
777
  } catch (e) {
685
- console.error('Failed to switch version:', e);
686
- throw e;
778
+ logger.error(TAG, `Failed to switch version: ${getErrorMessage(e)}`);
779
+ throw toUpdateError(e, ERROR_SWITCH_VERSION_FAILED);
687
780
  }
688
781
  }
689
782
 
@@ -692,7 +785,7 @@ export class UpdateContext {
692
785
  this.trace(`consumeFirstLoadMarker:marked=${marked}`);
693
786
  if (marked) {
694
787
  this.preferences.deleteSync('firstLoadMarked');
695
- this.flushPreferences('clear first load marker');
788
+ this.flushInBackground('clear first load marker');
696
789
  }
697
790
  return marked;
698
791
  }
@@ -714,15 +807,17 @@ export class UpdateContext {
714
807
  if (launchState.didRollback) {
715
808
  // The crash-protection rollback: the new version never called
716
809
  // markSuccess. Keep this visible in release logs.
717
- console.error(
810
+ logger.error(
811
+ TAG,
718
812
  `Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
719
813
  ` rolled back to ${launchState.currentVersion}`,
720
814
  );
721
815
  }
722
816
  if (launchState.didRollback || launchState.consumedFirstTime) {
817
+ // 同步启动路径:落盘失败只记日志(状态已在内存里生效)。
723
818
  this.persistState(launchState, {
724
819
  markFirstLoadMarker: launchState.consumedFirstTime,
725
- });
820
+ }).catch(() => {});
726
821
  }
727
822
  if (launchState.consumedFirstTime) {
728
823
  UpdateContext.ignoreRollback = true;
@@ -743,7 +838,7 @@ export class UpdateContext {
743
838
  const bundleFile = this.getBundlePath(version);
744
839
  try {
745
840
  if (!fileIo.accessSync(bundleFile)) {
746
- console.error(`Bundle version ${version} not found.`);
841
+ logger.error(TAG, `Bundle version ${version} not found.`);
747
842
  version = this.rollBack();
748
843
  nativeCheckRolledBackVersion = this.rolledBackVersion();
749
844
  continue;
@@ -752,7 +847,7 @@ export class UpdateContext {
752
847
  nativeCheckRolledBackVersion = this.rolledBackVersion();
753
848
  return bundleFile;
754
849
  } catch (e) {
755
- console.error('Failed to access bundle file:', e);
850
+ logger.error(TAG, `Failed to access bundle file: ${getErrorMessage(e)}`);
756
851
  version = this.rollBack();
757
852
  nativeCheckRolledBackVersion = this.rolledBackVersion();
758
853
  }
@@ -775,25 +870,36 @@ export class UpdateContext {
775
870
  private rollBack(): string {
776
871
  const stateBefore = this.getStateSnapshot();
777
872
  const nextState = this.runStateOperation(STATE_OP_ROLLBACK);
778
- console.error(
873
+ this.flushInBackground('rollback');
874
+ logger.error(
875
+ TAG,
779
876
  `Rolling back version ${stateBefore.currentVersion} to ${nextState.currentVersion}`,
780
877
  );
781
878
  return nextState.currentVersion || '';
782
879
  }
783
880
 
881
+ /**
882
+ * 常规清理(best-effort 后台维护,无人等待其完成):删除 3 天未触碰且不在
883
+ * 保留名单里的条目。保留 current/last 与本进程启动的版本。挂到串行任务链
884
+ * 上——下载可能正在追加超过 3 天的续传 partial 或填充 staging,按 mtime
885
+ * 的清理若并发跑会把它们当陈旧条目删掉(Android 与下载共用同一单线程
886
+ * executor)。
887
+ */
784
888
  public cleanUp(): void {
785
889
  const state = this.getStateSnapshot();
786
- // cleanupOldEntries now runs on a native worker thread (returns a Promise).
787
- // Cleanup is best-effort background maintenance and no caller depends on its
788
- // completion, so fire-and-forget it off the UI thread and just log failures
789
- // instead of blocking the state operation (or cold start) on disk I/O.
790
- NativePatchCore.cleanupOldEntries(
791
- this.rootDir,
890
+ const keepNames = [
792
891
  state.currentVersion || '',
793
892
  state.lastVersion || '',
794
- 3,
893
+ UpdateContext.launchVersion,
894
+ ].filter(name => name.length > 0);
895
+ this.enqueueSerialTask(() =>
896
+ NativePatchCore.cleanupOldEntries(
897
+ this.rootDir,
898
+ keepNames,
899
+ CLEANUP_MAX_AGE_DAYS,
900
+ ),
795
901
  ).catch((error: Object) => {
796
- console.error('cleanupOldEntries failed:', error);
902
+ logger.error(TAG, `cleanupOldEntries failed: ${getErrorMessage(error)}`);
797
903
  });
798
904
  }
799
905
 
@@ -812,7 +918,10 @@ export class UpdateContext {
812
918
  );
813
919
  return bundleInfo?.updateTime ?? 0;
814
920
  } catch (error) {
815
- console.error('Failed to get bundle update time:', error);
921
+ logger.error(
922
+ TAG,
923
+ `Failed to get bundle update time: ${getErrorMessage(error)}`,
924
+ );
816
925
  return 0;
817
926
  }
818
927
  }
@@ -842,7 +951,7 @@ export class UpdateContext {
842
951
  hash = NativePatchCore.sha256Hex(content);
843
952
  } catch (error) {
844
953
  // rawfile 缺 bundle(未打包 release bundle)是合法的"未知",不是错误。
845
- console.info('Cannot hash embedded bundle:', error);
954
+ logger.info(TAG, `Cannot hash embedded bundle: ${getErrorMessage(error)}`);
846
955
  return '';
847
956
  }
848
957
  if (hash) {
@@ -850,7 +959,8 @@ export class UpdateContext {
850
959
  UpdateContext.KEY_BUNDLE_HASH_CACHE,
851
960
  cachePrefix + hash,
852
961
  );
853
- this.flushPreferences('cache bundle hash');
962
+ // 缓存落盘失败不影响返回值(下次重算即可)。
963
+ this.flushInBackground('cache bundle hash');
854
964
  }
855
965
  return hash;
856
966
  }