react-native-update 10.49.0 → 10.51.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 (61) hide show
  1. package/BUNDLEHASH_MIGRATION.md +22 -2
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  3. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  4. package/README-CN.md +14 -1
  5. package/README.md +14 -1
  6. package/android/jni/Android.mk +5 -1
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +42 -9
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
  13. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
  18. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
  19. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  20. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  21. package/cpp/patch_core/digest.cpp +53 -0
  22. package/cpp/patch_core/digest.h +18 -0
  23. package/cpp/patch_core/patch_core.cpp +17 -1
  24. package/cpp/patch_core/patch_core.h +8 -0
  25. package/cpp/patch_core/tests/patch_core_test.cpp +99 -0
  26. package/cpp/update_flow_core/flow_json.cpp +522 -0
  27. package/cpp/update_flow_core/flow_json.h +103 -0
  28. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  29. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  30. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  31. package/cpp/update_flow_core/update_flow_core.h +77 -0
  32. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  33. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  34. package/harmony/pushy/src/main/cpp/pushy.cpp +150 -1
  35. package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -9
  36. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  37. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  38. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  39. package/harmony/pushy/src/main/ets/NativePatchCore.ts +16 -0
  40. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  41. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  42. package/harmony/pushy/src/main/ets/UpdateContext.ts +152 -52
  43. package/harmony/pushy.har +0 -0
  44. package/ios/RCTPushy/RCTPushy.mm +840 -68
  45. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  46. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  47. package/package.json +3 -1
  48. package/react-native-update.podspec +2 -0
  49. package/src/NativePushy.ts +14 -0
  50. package/src/client.ts +439 -122
  51. package/src/endpoint.ts +105 -80
  52. package/src/error.ts +4 -0
  53. package/src/locales/en.ts +4 -0
  54. package/src/locales/zh.ts +4 -0
  55. package/src/provider.tsx +74 -9
  56. package/src/telemetry.ts +8 -0
  57. package/src/type.ts +31 -0
  58. package/src/updateFlowCore.ts +320 -0
  59. package/src/utils.ts +14 -12
  60. package/src/isInRollout.ts +0 -86
  61. package/src/resolveCheckResult.ts +0 -30
@@ -1,11 +1,18 @@
1
1
  import preferences from '@ohos.data.preferences';
2
2
  import fileIo from '@ohos.file.fs';
3
- import { DownloadTask } from './DownloadTask';
3
+ import {
4
+ DownloadTask,
5
+ VERSION_COMPLETE_FILE_NAME,
6
+ } from './DownloadTask';
4
7
  import common from '@ohos.app.ability.common';
5
8
  import { DownloadTaskParams } from './DownloadTaskParams';
6
9
  import { bundleManager } from '@kit.AbilityKit';
7
10
  import { util } from '@kit.ArkTS';
8
11
  import logger from './Logger';
12
+ import {
13
+ KEY_RESP_CACHE,
14
+ scheduleNativeCheck,
15
+ } from './NativeCheckOrchestrator';
9
16
  import NativePatchCore, {
10
17
  STATE_OP_CLEAR_FIRST_TIME,
11
18
  STATE_OP_CLEAR_ROLLBACK_MARK,
@@ -15,6 +22,9 @@ import NativePatchCore, {
15
22
  STATE_OP_SWITCH_VERSION,
16
23
  StateCoreResult,
17
24
  } from './NativePatchCore';
25
+ import { assertSafePathComponent } from './PathUtils';
26
+
27
+ export { isSafePathComponent } from './PathUtils';
18
28
 
19
29
  type FlushablePreferences = preferences.Preferences & {
20
30
  flushSync?: () => void;
@@ -31,6 +41,10 @@ export class UpdateContext {
31
41
  // resetToPackagedBundle 不能删它的目录:热更包内的图片等资源是运行时按需
32
42
  // 读盘的,静默(不重启)reset 若删掉会导致后续所有未加载过的资源失败。
33
43
  private static launchVersion: string = '';
44
+ // 由 resetToPackagedBundle 递增。原生冷启动检测可能跑数分钟并已握有决策,
45
+ // 期间发生的 reset 必须赢:编排器采样该值,发现变化即放弃激活与响应缓存,
46
+ // 在飞的救援不会把刚被重置掉的版本装回去。
47
+ private static resetGeneration: number = 0;
34
48
  private static cachedPackageVersion: string = '';
35
49
  private static cachedBuildTime: string = '';
36
50
  // 单例:确保 bundle provider 与 TurboModule 共用同一份 preferences 内存状态,
@@ -296,7 +310,7 @@ export class UpdateContext {
296
310
  const params = new DownloadTaskParams();
297
311
  params.type = type;
298
312
  params.url = url;
299
- params.hash = hash;
313
+ params.hash = assertSafePathComponent(hash);
300
314
  return params;
301
315
  }
302
316
 
@@ -317,6 +331,15 @@ export class UpdateContext {
317
331
 
318
332
  private async executeTask(params: DownloadTaskParams): Promise<void> {
319
333
  await this.enqueueSerialTask(() => {
334
+ const isPatchTask =
335
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FULL ||
336
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP ||
337
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
338
+ // Re-check only when this queued job actually starts. A JS download may
339
+ // have completed while the cold-start duplicate waited behind it.
340
+ if (isPatchTask && this.hasDownloadedVersion(params.hash)) {
341
+ return Promise.resolve();
342
+ }
320
343
  const downloadTask = new DownloadTask(this.context);
321
344
  return downloadTask.execute(params);
322
345
  });
@@ -430,7 +453,16 @@ export class UpdateContext {
430
453
  console.error('Failed to clear hash info on reset:', e);
431
454
  }
432
455
  }
456
+ // 先让在飞的原生检测轮次失效,再清理状态:随后在同一(单线程)执行序里
457
+ // 提交的轮次会看到新代数并整轮丢弃。
458
+ UpdateContext.resetGeneration += 1;
433
459
  this.persistState(resetState, { clearFirstLoadMarker: true });
460
+ // 缓存里的响应仍在宣告本次 reset 刚删掉的版本,一并丢弃,避免 JS 侧复用。
461
+ try {
462
+ this.preferences.deleteSync(KEY_RESP_CACHE);
463
+ } catch (e: any) {
464
+ console.error('Failed to clear native check cache on reset:', e);
465
+ }
434
466
  UpdateContext.ignoreRollback = false;
435
467
 
436
468
  // maxAgeDays=0:删除下载目录内容,仅保留当前运行版本的目录(残留目录由
@@ -448,7 +480,44 @@ export class UpdateContext {
448
480
  this.trace('resetToPackagedBundle:after');
449
481
  }
450
482
 
451
- public async downloadFullUpdate(url: string, hash: string): Promise<void> {
483
+ /** 供原生检测编排器采样/比对的 reset 代数(见 resetGeneration 注释)。 */
484
+ public getResetGeneration(): number {
485
+ return UpdateContext.resetGeneration;
486
+ }
487
+
488
+ /**
489
+ * 一次性提交原生检测轮次的全部持久化结果(版本元信息、激活、响应缓存):
490
+ * 先复核 reset 代数,再落所有写入。ArkTS 单线程 + 本方法内无 await,因此
491
+ * 校验与写入之间不存在可插入 reset 的窗口(iOS/Android 用锁达到同一效果)。
492
+ * 返回是否提交成功。
493
+ */
494
+ public commitNativeCheckResult(
495
+ expectedGeneration: number,
496
+ hash: string,
497
+ hashInfoJson: string,
498
+ activate: boolean,
499
+ responseCacheJson: string,
500
+ ): boolean {
501
+ if (UpdateContext.resetGeneration !== expectedGeneration) {
502
+ return false;
503
+ }
504
+ if (hash && hashInfoJson) {
505
+ this.setKv(`hash_${hash}`, hashInfoJson);
506
+ }
507
+ if (activate && hash) {
508
+ this.switchVersion(hash);
509
+ }
510
+ if (responseCacheJson) {
511
+ this.setKv(KEY_RESP_CACHE, responseCacheJson);
512
+ }
513
+ return true;
514
+ }
515
+
516
+ public async downloadFullUpdate(
517
+ url: string,
518
+ hash: string,
519
+ deadlineUptimeMs: number = 0,
520
+ ): Promise<void> {
452
521
  try {
453
522
  const params = this.createTaskParams(
454
523
  DownloadTaskParams.TASK_TYPE_PATCH_FULL,
@@ -457,6 +526,7 @@ export class UpdateContext {
457
526
  );
458
527
  params.targetFile = `${this.rootDir}/${hash}.ppk`;
459
528
  params.unzipDirectory = `${this.rootDir}/${hash}`;
529
+ params.deadlineUptimeMs = deadlineUptimeMs;
460
530
  await this.executeTask(params);
461
531
  } catch (e) {
462
532
  console.error('Failed to download full update:', e);
@@ -474,7 +544,7 @@ export class UpdateContext {
474
544
  url,
475
545
  hash,
476
546
  );
477
- params.targetFile = this.rootDir + '/' + fileName;
547
+ params.targetFile = this.rootDir + '/' + assertSafePathComponent(fileName);
478
548
  await this.executeTask(params);
479
549
  }
480
550
 
@@ -482,22 +552,25 @@ export class UpdateContext {
482
552
  url: string,
483
553
  hash: string,
484
554
  originHash: string,
555
+ deadlineUptimeMs: number = 0,
485
556
  ): Promise<void> {
486
557
  const params = this.createTaskParams(
487
558
  DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK,
488
559
  url,
489
560
  hash,
490
561
  );
491
- params.originHash = originHash;
562
+ params.originHash = assertSafePathComponent(originHash);
492
563
  params.targetFile = `${this.rootDir}/${originHash}_${hash}.ppk.patch`;
493
564
  params.unzipDirectory = `${this.rootDir}/${hash}`;
494
565
  params.originDirectory = `${this.rootDir}/${params.originHash}`;
566
+ params.deadlineUptimeMs = deadlineUptimeMs;
495
567
  await this.executeTask(params);
496
568
  }
497
569
 
498
570
  public async downloadPatchFromPackage(
499
571
  url: string,
500
572
  hash: string,
573
+ deadlineUptimeMs: number = 0,
501
574
  ): Promise<void> {
502
575
  try {
503
576
  const params = this.createTaskParams(
@@ -507,6 +580,7 @@ export class UpdateContext {
507
580
  );
508
581
  params.targetFile = `${this.rootDir}/${hash}.app.patch`;
509
582
  params.unzipDirectory = `${this.rootDir}/${hash}`;
583
+ params.deadlineUptimeMs = deadlineUptimeMs;
510
584
  return await this.executeTask(params);
511
585
  } catch (e) {
512
586
  console.error('Failed to download package patch:', e);
@@ -514,9 +588,23 @@ export class UpdateContext {
514
588
  }
515
589
  }
516
590
 
591
+ // 原生冷启动检测(NativeCheckOrchestrator)用来跳过已就绪版本的重复下载
592
+ // ——alert 类策略下版本已下载但未激活,若不判在这里会每次冷启动重下一遍。
593
+ public hasDownloadedVersion(hash: string): boolean {
594
+ try {
595
+ const safeHash = assertSafePathComponent(hash);
596
+ return fileIo.accessSync(this.getBundlePath(safeHash))
597
+ && fileIo.accessSync(
598
+ `${this.rootDir}/${safeHash}/${VERSION_COMPLETE_FILE_NAME}`,
599
+ );
600
+ } catch (e) {
601
+ return false;
602
+ }
603
+ }
604
+
517
605
  public switchVersion(hash: string): void {
518
606
  try {
519
- const bundlePath = this.getBundlePath(hash);
607
+ const bundlePath = this.getBundlePath(assertSafePathComponent(hash));
520
608
  if (!fileIo.accessSync(bundlePath)) {
521
609
  throw Error(`Bundle version ${hash} not found.`);
522
610
  }
@@ -544,58 +632,70 @@ export class UpdateContext {
544
632
  public getBundleUrl() {
545
633
  UpdateContext.isUsingBundleUrl = true;
546
634
  this.trace('getBundleUrl:enter');
547
- const stateBeforeLaunch = this.getStateSnapshot();
548
- const launchState = NativePatchCore.runStateCore(
549
- STATE_OP_RESOLVE_LAUNCH,
550
- stateBeforeLaunch,
551
- '',
552
- UpdateContext.ignoreRollback,
553
- true,
554
- );
555
- if (launchState.didRollback) {
556
- // The crash-protection rollback: the new version never called
557
- // markSuccess. Keep this visible in release logs.
558
- console.error(
559
- `Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
560
- ` rolled back to ${launchState.currentVersion}`,
635
+ let nativeCheckRolledBackVersion = '';
636
+ try {
637
+ const stateBeforeLaunch = this.getStateSnapshot();
638
+ const launchState = NativePatchCore.runStateCore(
639
+ STATE_OP_RESOLVE_LAUNCH,
640
+ stateBeforeLaunch,
641
+ '',
642
+ UpdateContext.ignoreRollback,
643
+ true,
644
+ );
645
+ nativeCheckRolledBackVersion = launchState.rolledBackVersion || '';
646
+ if (launchState.didRollback) {
647
+ // The crash-protection rollback: the new version never called
648
+ // markSuccess. Keep this visible in release logs.
649
+ console.error(
650
+ `Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
651
+ ` rolled back to ${launchState.currentVersion}`,
652
+ );
653
+ }
654
+ if (launchState.didRollback || launchState.consumedFirstTime) {
655
+ this.persistState(launchState, {
656
+ markFirstLoadMarker: launchState.consumedFirstTime,
657
+ });
658
+ }
659
+ if (launchState.consumedFirstTime) {
660
+ UpdateContext.ignoreRollback = true;
661
+ }
662
+ this.trace(
663
+ `getBundleUrl:load=${launchState.loadVersion}` +
664
+ ` consumed=${launchState.consumedFirstTime}` +
665
+ ` rollback=${launchState.didRollback}`,
561
666
  );
562
- }
563
- if (launchState.didRollback || launchState.consumedFirstTime) {
564
- this.persistState(launchState, {
565
- markFirstLoadMarker: launchState.consumedFirstTime,
566
- });
567
- }
568
- if (launchState.consumedFirstTime) {
569
- UpdateContext.ignoreRollback = true;
570
- }
571
- this.trace(
572
- `getBundleUrl:load=${launchState.loadVersion}` +
573
- ` consumed=${launchState.consumedFirstTime}` +
574
- ` rollback=${launchState.didRollback}`,
575
- );
576
667
 
577
- let version = launchState.loadVersion || '';
578
- // Guard the rollback chain against cycles: a corrupted state returning an
579
- // already-visited version would otherwise spin this loop forever during
580
- // startup (Android has the same guard).
581
- const visitedVersions = new Set<string>();
582
- while (version && !visitedVersions.has(version)) {
583
- visitedVersions.add(version);
584
- const bundleFile = this.getBundlePath(version);
585
- try {
586
- if (!fileIo.accessSync(bundleFile)) {
587
- console.error(`Bundle version ${version} not found.`);
668
+ let version = launchState.loadVersion || '';
669
+ // Guard the rollback chain against cycles: a corrupted state returning an
670
+ // already-visited version would otherwise spin this loop forever during
671
+ // startup (Android has the same guard).
672
+ const visitedVersions = new Set<string>();
673
+ while (version && !visitedVersions.has(version)) {
674
+ visitedVersions.add(version);
675
+ const bundleFile = this.getBundlePath(version);
676
+ try {
677
+ if (!fileIo.accessSync(bundleFile)) {
678
+ console.error(`Bundle version ${version} not found.`);
679
+ version = this.rollBack();
680
+ nativeCheckRolledBackVersion = this.rolledBackVersion();
681
+ continue;
682
+ }
683
+ UpdateContext.launchVersion = version;
684
+ nativeCheckRolledBackVersion = this.rolledBackVersion();
685
+ return bundleFile;
686
+ } catch (e) {
687
+ console.error('Failed to access bundle file:', e);
588
688
  version = this.rollBack();
589
- continue;
689
+ nativeCheckRolledBackVersion = this.rolledBackVersion();
590
690
  }
591
- UpdateContext.launchVersion = version;
592
- return bundleFile;
593
- } catch (e) {
594
- console.error('Failed to access bundle file:', e);
595
- version = this.rollBack();
596
691
  }
692
+ nativeCheckRolledBackVersion = this.rolledBackVersion();
693
+ return '';
694
+ } finally {
695
+ // State corruption is exactly when the native rescue check is needed;
696
+ // schedule even if state parsing/rollback throws before a normal exit.
697
+ scheduleNativeCheck(this, nativeCheckRolledBackVersion);
597
698
  }
598
- return '';
599
699
  }
600
700
 
601
701
  public getCurrentVersion(): string {
package/harmony/pushy.har CHANGED
Binary file