react-native-update 10.50.0 → 10.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  2. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  3. package/README-CN.md +14 -1
  4. package/README.md +14 -1
  5. package/android/build.gradle +10 -0
  6. package/android/jni/Android.mk +5 -1
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
  16. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
  17. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  18. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  19. package/cpp/update_flow_core/flow_json.cpp +522 -0
  20. package/cpp/update_flow_core/flow_json.h +103 -0
  21. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  22. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  23. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  24. package/cpp/update_flow_core/update_flow_core.h +77 -0
  25. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  26. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  27. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
  28. package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
  29. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  30. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  31. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  32. package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
  33. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  34. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  35. package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
  36. package/harmony/pushy.har +0 -0
  37. package/ios/RCTPushy/RCTPushy.mm +779 -61
  38. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  39. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  40. package/package.json +3 -1
  41. package/react-native-update.podspec +2 -0
  42. package/src/NativePushy.ts +14 -0
  43. package/src/client.ts +341 -118
  44. package/src/endpoint.ts +7 -29
  45. package/src/provider.tsx +38 -2
  46. package/src/type.ts +26 -0
  47. package/src/updateFlowCore.ts +320 -0
  48. package/src/utils.ts +1 -12
  49. package/src/isInRollout.ts +0 -87
  50. package/src/resolveCheckResult.ts +0 -30
@@ -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,32 +22,14 @@ 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;
21
31
  };
22
32
 
23
- // 服务端下发的 hash/originHash/fileName 会拼进 rootDir 作为子路径;凡是可能
24
- // 逃出 rootDir 的值(路径分隔符、".."、".")必须在触碰文件系统前拒绝。
25
- export function isSafePathComponent(name: string): boolean {
26
- return (
27
- typeof name === 'string' &&
28
- name.length > 0 &&
29
- name !== '.' &&
30
- name !== '..' &&
31
- !name.includes('/') &&
32
- !name.includes('\\') &&
33
- !name.includes('\0')
34
- );
35
- }
36
-
37
- function assertSafePathComponent(name: string): string {
38
- if (!isSafePathComponent(name)) {
39
- throw Error(`Invalid path component: ${name}`);
40
- }
41
- return name;
42
- }
43
-
44
33
  export class UpdateContext {
45
34
  private context: common.UIAbilityContext;
46
35
  private rootDir: string;
@@ -52,6 +41,10 @@ export class UpdateContext {
52
41
  // resetToPackagedBundle 不能删它的目录:热更包内的图片等资源是运行时按需
53
42
  // 读盘的,静默(不重启)reset 若删掉会导致后续所有未加载过的资源失败。
54
43
  private static launchVersion: string = '';
44
+ // 由 resetToPackagedBundle 递增。原生冷启动检测可能跑数分钟并已握有决策,
45
+ // 期间发生的 reset 必须赢:编排器采样该值,发现变化即放弃激活与响应缓存,
46
+ // 在飞的救援不会把刚被重置掉的版本装回去。
47
+ private static resetGeneration: number = 0;
55
48
  private static cachedPackageVersion: string = '';
56
49
  private static cachedBuildTime: string = '';
57
50
  // 单例:确保 bundle provider 与 TurboModule 共用同一份 preferences 内存状态,
@@ -338,6 +331,15 @@ export class UpdateContext {
338
331
 
339
332
  private async executeTask(params: DownloadTaskParams): Promise<void> {
340
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
+ }
341
343
  const downloadTask = new DownloadTask(this.context);
342
344
  return downloadTask.execute(params);
343
345
  });
@@ -451,7 +453,16 @@ export class UpdateContext {
451
453
  console.error('Failed to clear hash info on reset:', e);
452
454
  }
453
455
  }
456
+ // 先让在飞的原生检测轮次失效,再清理状态:随后在同一(单线程)执行序里
457
+ // 提交的轮次会看到新代数并整轮丢弃。
458
+ UpdateContext.resetGeneration += 1;
454
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
+ }
455
466
  UpdateContext.ignoreRollback = false;
456
467
 
457
468
  // maxAgeDays=0:删除下载目录内容,仅保留当前运行版本的目录(残留目录由
@@ -469,7 +480,44 @@ export class UpdateContext {
469
480
  this.trace('resetToPackagedBundle:after');
470
481
  }
471
482
 
472
- 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> {
473
521
  try {
474
522
  const params = this.createTaskParams(
475
523
  DownloadTaskParams.TASK_TYPE_PATCH_FULL,
@@ -478,6 +526,7 @@ export class UpdateContext {
478
526
  );
479
527
  params.targetFile = `${this.rootDir}/${hash}.ppk`;
480
528
  params.unzipDirectory = `${this.rootDir}/${hash}`;
529
+ params.deadlineUptimeMs = deadlineUptimeMs;
481
530
  await this.executeTask(params);
482
531
  } catch (e) {
483
532
  console.error('Failed to download full update:', e);
@@ -503,6 +552,7 @@ export class UpdateContext {
503
552
  url: string,
504
553
  hash: string,
505
554
  originHash: string,
555
+ deadlineUptimeMs: number = 0,
506
556
  ): Promise<void> {
507
557
  const params = this.createTaskParams(
508
558
  DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK,
@@ -513,12 +563,14 @@ export class UpdateContext {
513
563
  params.targetFile = `${this.rootDir}/${originHash}_${hash}.ppk.patch`;
514
564
  params.unzipDirectory = `${this.rootDir}/${hash}`;
515
565
  params.originDirectory = `${this.rootDir}/${params.originHash}`;
566
+ params.deadlineUptimeMs = deadlineUptimeMs;
516
567
  await this.executeTask(params);
517
568
  }
518
569
 
519
570
  public async downloadPatchFromPackage(
520
571
  url: string,
521
572
  hash: string,
573
+ deadlineUptimeMs: number = 0,
522
574
  ): Promise<void> {
523
575
  try {
524
576
  const params = this.createTaskParams(
@@ -528,6 +580,7 @@ export class UpdateContext {
528
580
  );
529
581
  params.targetFile = `${this.rootDir}/${hash}.app.patch`;
530
582
  params.unzipDirectory = `${this.rootDir}/${hash}`;
583
+ params.deadlineUptimeMs = deadlineUptimeMs;
531
584
  return await this.executeTask(params);
532
585
  } catch (e) {
533
586
  console.error('Failed to download package patch:', e);
@@ -535,6 +588,20 @@ export class UpdateContext {
535
588
  }
536
589
  }
537
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
+
538
605
  public switchVersion(hash: string): void {
539
606
  try {
540
607
  const bundlePath = this.getBundlePath(assertSafePathComponent(hash));
@@ -565,58 +632,70 @@ export class UpdateContext {
565
632
  public getBundleUrl() {
566
633
  UpdateContext.isUsingBundleUrl = true;
567
634
  this.trace('getBundleUrl:enter');
568
- const stateBeforeLaunch = this.getStateSnapshot();
569
- const launchState = NativePatchCore.runStateCore(
570
- STATE_OP_RESOLVE_LAUNCH,
571
- stateBeforeLaunch,
572
- '',
573
- UpdateContext.ignoreRollback,
574
- true,
575
- );
576
- if (launchState.didRollback) {
577
- // The crash-protection rollback: the new version never called
578
- // markSuccess. Keep this visible in release logs.
579
- console.error(
580
- `Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
581
- ` 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}`,
582
666
  );
583
- }
584
- if (launchState.didRollback || launchState.consumedFirstTime) {
585
- this.persistState(launchState, {
586
- markFirstLoadMarker: launchState.consumedFirstTime,
587
- });
588
- }
589
- if (launchState.consumedFirstTime) {
590
- UpdateContext.ignoreRollback = true;
591
- }
592
- this.trace(
593
- `getBundleUrl:load=${launchState.loadVersion}` +
594
- ` consumed=${launchState.consumedFirstTime}` +
595
- ` rollback=${launchState.didRollback}`,
596
- );
597
667
 
598
- let version = launchState.loadVersion || '';
599
- // Guard the rollback chain against cycles: a corrupted state returning an
600
- // already-visited version would otherwise spin this loop forever during
601
- // startup (Android has the same guard).
602
- const visitedVersions = new Set<string>();
603
- while (version && !visitedVersions.has(version)) {
604
- visitedVersions.add(version);
605
- const bundleFile = this.getBundlePath(version);
606
- try {
607
- if (!fileIo.accessSync(bundleFile)) {
608
- 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);
609
688
  version = this.rollBack();
610
- continue;
689
+ nativeCheckRolledBackVersion = this.rolledBackVersion();
611
690
  }
612
- UpdateContext.launchVersion = version;
613
- return bundleFile;
614
- } catch (e) {
615
- console.error('Failed to access bundle file:', e);
616
- version = this.rollBack();
617
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);
618
698
  }
619
- return '';
620
699
  }
621
700
 
622
701
  public getCurrentVersion(): string {
package/harmony/pushy.har CHANGED
Binary file