react-native-update 10.59.2 → 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 +2 -7
  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 +17 -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 -2634
  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 +73 -82
  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
@@ -1,8 +1,9 @@
1
1
  import preferences from '@ohos.data.preferences';
2
2
  import fileIo from '@ohos.file.fs';
3
3
  import {
4
+ HARMONY_BUNDLE_FILE_NAME,
4
5
  isInstallComplete,
5
- readInstallRecord,
6
+ recordedBundleSha256,
6
7
  verifyInstallForActivation,
7
8
  } from './InstallRecord';
8
9
  import { DownloadTask } from './DownloadTask';
@@ -18,7 +19,6 @@ import {
18
19
  scheduleNativeSync,
19
20
  } from './SyncCoordinator';
20
21
  import NativePatchCore, {
21
- STATE_OP_CLEAR_FIRST_TIME,
22
22
  STATE_OP_CLEAR_ROLLBACK_MARK,
23
23
  STATE_OP_MARK_SUCCESS,
24
24
  STATE_OP_RESOLVE_LAUNCH,
@@ -27,9 +27,9 @@ import NativePatchCore, {
27
27
  StateCoreResult,
28
28
  } from './NativePatchCore';
29
29
  import { assertSafePathComponent } from './PathUtils';
30
+ import { withRawFd } from './SaveFile';
30
31
  import {
31
32
  ERROR_SWITCH_VERSION_FAILED,
32
- createPushyError,
33
33
  getErrorMessage,
34
34
  toPushyError,
35
35
  } from './ErrorCodes';
@@ -41,11 +41,18 @@ const TAG = 'PushyContext';
41
41
  // 常规清理保留最近 3 天内触碰过的条目(续传 partial、staging 同样按 mtime)。
42
42
  const CLEANUP_MAX_AGE_DAYS = 3;
43
43
 
44
- // Preferences.flushSync 是 API 14 才有、getAllSync 老 SDK 声明里没有:HAR 兼容
45
- // API 12,按结构化视图探测(不用交叉类型——ArkTS 不支持)。
44
+ // Preferences.flushSync 是 API 14 才有:HAR 兼容 API 12,按结构化视图探测
45
+ // (不用交叉类型——ArkTS 不支持)。
46
46
  interface OptionalPreferencesApi {
47
47
  flushSync?: () => void;
48
- getAllSync?: () => Record<string, unknown>;
48
+ }
49
+
50
+ // C1 配置等价(cpp/update_flow_core ConfigsEquivalent,与 Android/iOS 共用):
51
+ // 缺省键补默认值、端点/发现地址去尾斜杠并保序去重、忽略键序。等价的配置不算
52
+ // "变更"——宿主重复 configure、或 JS 同步一份只差格式的同一配置,都不应取消
53
+ // 在飞轮次或丢弃响应缓存。任一缺省即不等价。
54
+ function nativeConfigsEquivalent(a: string | undefined, b: string | undefined): boolean {
55
+ return NativePatchCore.configsEquivalent(a, b);
49
56
  }
50
57
 
51
58
  interface PersistOptions {
@@ -320,7 +327,7 @@ export class UpdateContext {
320
327
  }
321
328
 
322
329
  private getBundlePath(hash: string): string {
323
- return `${this.rootDir}/${hash}/bundle.harmony.js`;
330
+ return `${this.rootDir}/${hash}/${HARMONY_BUNDLE_FILE_NAME}`;
324
331
  }
325
332
 
326
333
  private getStateSnapshot(): StateCoreResult {
@@ -415,13 +422,9 @@ export class UpdateContext {
415
422
 
416
423
  private async executeTask(params: DownloadTaskParams): Promise<void> {
417
424
  await this.enqueueSerialTask(() => {
418
- const isPatchTask =
419
- params.type === DownloadTaskParams.TASK_TYPE_PATCH_FULL ||
420
- params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP ||
421
- params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
422
425
  // Re-check only when this queued job actually starts. A JS download may
423
426
  // have completed while the cold-start duplicate waited behind it.
424
- if (isPatchTask && this.hasDownloadedVersion(params.hash)) {
427
+ if (this.hasDownloadedVersion(params.hash)) {
425
428
  return Promise.resolve();
426
429
  }
427
430
  const downloadTask = new DownloadTask(this.context);
@@ -460,19 +463,29 @@ export class UpdateContext {
460
463
  this.flushInBackground('sync binary version');
461
464
  }
462
465
 
463
- /** 写入并落盘;flushSync 不可用时以 flush() 的结果拒绝。 */
464
- /** Shared JS/native configuration writer; all mutations precede the first await. */
465
466
  private static nativeConfigGeneration: number = 0;
466
467
 
467
468
  public getNativeConfigGeneration(): number {
468
469
  return UpdateContext.nativeConfigGeneration;
469
470
  }
470
471
 
472
+ /** 与持久化的配置是否等价(C1)。 */
473
+ public isStoredNativeConfig(config: string | undefined): boolean {
474
+ return nativeConfigsEquivalent(config, this.getKv(KEY_CONFIG));
475
+ }
476
+
477
+ /**
478
+ * Shared JS/native configuration writer; all mutations precede the first
479
+ * await. An equivalent configuration (C1) is not a change: the stored string,
480
+ * response cache and generations stay as they are.
481
+ */
471
482
  public setNativeConfig(config: string): Promise<void> {
472
483
  if (!this.getKv('uuid')) {
473
484
  this.preferences.putSync('uuid', util.generateRandomUUID());
474
485
  }
475
- if (this.getKv(KEY_CONFIG) !== config) {
486
+ const current = this.getKv(KEY_CONFIG);
487
+ // Byte-identical is trivially no change, even for a value C1 cannot parse.
488
+ if (!current || (current !== config && !nativeConfigsEquivalent(current, config))) {
476
489
  // Also guards A -> B -> A replacements and late download commits.
477
490
  UpdateContext.resetGeneration += 1;
478
491
  UpdateContext.nativeConfigGeneration += 1;
@@ -484,6 +497,7 @@ export class UpdateContext {
484
497
  return this.flushPreferences('persist configuration');
485
498
  }
486
499
 
500
+ /** 写入并落盘;flushSync 不可用时以 flush() 的结果拒绝。 */
487
501
  public setKv(key: string, value: string): Promise<void> {
488
502
  this.preferences.putSync(key, value);
489
503
  return this.flushPreferences(`set key ${key}`);
@@ -498,10 +512,6 @@ export class UpdateContext {
498
512
  return this.flushPreferences(`remove key ${key}`);
499
513
  }
500
514
 
501
- public isFirstTime(): boolean {
502
- return this.getStateSnapshot().firstTime;
503
- }
504
-
505
515
  public rolledBackVersion(): string {
506
516
  return this.getStateSnapshot().rolledBackVersion || '';
507
517
  }
@@ -518,14 +528,6 @@ export class UpdateContext {
518
528
  await this.flushPreferences('mark success');
519
529
  }
520
530
 
521
- public async clearFirstTime(): Promise<void> {
522
- this.runStateOperation(STATE_OP_CLEAR_FIRST_TIME, '', {
523
- cleanUp: true,
524
- clearFirstLoadMarker: true,
525
- });
526
- await this.flushPreferences('clear first time');
527
- }
528
-
529
531
  /** getConstants(同步)里调用:落盘失败只记日志。 */
530
532
  public clearRollbackMark(): void {
531
533
  this.runStateOperation(STATE_OP_CLEAR_ROLLBACK_MARK, '', {
@@ -561,23 +563,20 @@ export class UpdateContext {
561
563
  let flushed: Promise<void> = Promise.resolve();
562
564
  try {
563
565
  // 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
564
- // uuid —— 见 syncStateWithBinaryVersion 的注释)。getAllSync 在旧 SDK 上
565
- // 可能不存在,此时残留的 hash_* 只是无害孤儿数据,不影响 reset 语义。
566
- const api = this.optionalPreferencesApi();
567
- if (typeof api.getAllSync === 'function') {
568
- try {
569
- const all = api.getAllSync();
570
- for (const key of Object.keys(all)) {
571
- if (key.startsWith('hash_')) {
572
- this.preferences.deleteSync(key);
573
- }
566
+ // uuid —— 见 syncStateWithBinaryVersion 的注释)。读取失败时残留的
567
+ // hash_* 只是无害孤儿数据,不影响 reset 语义。
568
+ try {
569
+ const all = this.preferences.getAllSync();
570
+ for (const key of Object.keys(all)) {
571
+ if (key.startsWith('hash_')) {
572
+ this.preferences.deleteSync(key);
574
573
  }
575
- } catch (e) {
576
- logger.error(
577
- TAG,
578
- `Failed to clear hash info on reset: ${getErrorMessage(e)}`,
579
- );
580
574
  }
575
+ } catch (e) {
576
+ logger.error(
577
+ TAG,
578
+ `Failed to clear hash info on reset: ${getErrorMessage(e)}`,
579
+ );
581
580
  }
582
581
  this.writeState(resetState, { clearFirstLoadMarker: true });
583
582
  // 缓存里的响应仍在宣告本次 reset 刚删掉的版本,一并丢弃,避免 JS 侧复用。
@@ -658,20 +657,15 @@ export class UpdateContext {
658
657
  hash: string,
659
658
  deadlineUptimeMs: number = 0,
660
659
  ): Promise<void> {
661
- try {
662
- const params = this.createTaskParams(
663
- DownloadTaskParams.TASK_TYPE_PATCH_FULL,
664
- url,
665
- hash,
666
- );
667
- params.targetFile = `${this.rootDir}/${hash}.ppk`;
668
- params.unzipDirectory = `${this.rootDir}/${hash}`;
669
- params.deadlineUptimeMs = deadlineUptimeMs;
670
- await this.executeTask(params);
671
- } catch (e) {
672
- logger.error(TAG, `Failed to download full package: ${getErrorMessage(e)}`);
673
- throw e;
674
- }
660
+ const params = this.createTaskParams(
661
+ DownloadTaskParams.TASK_TYPE_PATCH_FULL,
662
+ url,
663
+ hash,
664
+ );
665
+ params.targetFile = `${this.rootDir}/${hash}.ppk`;
666
+ params.unzipDirectory = `${this.rootDir}/${hash}`;
667
+ params.deadlineUptimeMs = deadlineUptimeMs;
668
+ await this.executeTask(params);
675
669
  }
676
670
 
677
671
  public async downloadPatchFromPpk(
@@ -698,23 +692,15 @@ export class UpdateContext {
698
692
  hash: string,
699
693
  deadlineUptimeMs: number = 0,
700
694
  ): Promise<void> {
701
- try {
702
- const params = this.createTaskParams(
703
- DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP,
704
- url,
705
- hash,
706
- );
707
- params.targetFile = `${this.rootDir}/${hash}${APP_DELTA_SUFFIX}`;
708
- params.unzipDirectory = `${this.rootDir}/${hash}`;
709
- params.deadlineUptimeMs = deadlineUptimeMs;
710
- return await this.executeTask(params);
711
- } catch (e) {
712
- logger.error(
713
- TAG,
714
- `Failed to download package delta: ${getErrorMessage(e)}`,
715
- );
716
- throw e;
717
- }
695
+ const params = this.createTaskParams(
696
+ DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP,
697
+ url,
698
+ hash,
699
+ );
700
+ params.targetFile = `${this.rootDir}/${hash}${APP_DELTA_SUFFIX}`;
701
+ params.unzipDirectory = `${this.rootDir}/${hash}`;
702
+ params.deadlineUptimeMs = deadlineUptimeMs;
703
+ await this.executeTask(params);
718
704
  }
719
705
 
720
706
  // 运行中热更版本安装记录里的 bundleSha256(崩溃归因用);内置 bundle /
@@ -724,10 +710,9 @@ export class UpdateContext {
724
710
  return '';
725
711
  }
726
712
  try {
727
- const record = readInstallRecord(
713
+ return recordedBundleSha256(
728
714
  `${this.rootDir}/${assertSafePathComponent(hash)}`,
729
715
  );
730
- return record?.bundleSha256 ?? '';
731
716
  } catch (e) {
732
717
  return '';
733
718
  }
@@ -738,51 +723,26 @@ export class UpdateContext {
738
723
  public hasDownloadedVersion(hash: string): boolean {
739
724
  try {
740
725
  const safeHash = assertSafePathComponent(hash);
741
- return fileIo.accessSync(this.getBundlePath(safeHash))
742
- && isInstallComplete(`${this.rootDir}/${safeHash}`, safeHash);
726
+ return isInstallComplete(`${this.rootDir}/${safeHash}`, safeHash);
743
727
  } catch (e) {
744
728
  return false;
745
729
  }
746
730
  }
747
731
 
748
732
  /**
749
- * 激活前置校验(同步,不写任何东西):bundle 在盘上,且要么是本 SDK 记录为
750
- * 完整安装的版本(bundle + 记录),要么是标记机制之前就激活过的历史版本
751
- * (current/last 里有它)。其他无记录的目录可能是崩溃留下的半成品。
752
- * 返回是否还需复核安装记录里的 bundle 摘要(历史版本无记录可核)。
753
- */
754
- private assertActivatable(safeHash: string): boolean {
755
- if (!fileIo.accessSync(this.getBundlePath(safeHash))) {
756
- throw createPushyError(
757
- ERROR_SWITCH_VERSION_FAILED,
758
- `Bundle version ${safeHash} not found.`,
759
- );
760
- }
761
- const legacyActivated =
762
- this.readString('currentVersion') === safeHash ||
763
- this.readString('lastVersion') === safeHash;
764
- if (!this.hasDownloadedVersion(safeHash) && !legacyActivated) {
765
- throw createPushyError(
766
- ERROR_SWITCH_VERSION_FAILED,
767
- `Bundle version ${safeHash} is incomplete.`,
768
- );
769
- }
770
- return !legacyActivated;
771
- }
772
-
773
- /**
774
- * 完整的激活校验(含 native 工作线程上的 bundle 摘要复核),不写状态。
775
- * 记录里的 bundle 摘要必须与盘上字节一致,才能把下次启动指向它。
733
+ * 完整的激活校验(含 native 工作线程上的 bundle 摘要复核),不写状态:
734
+ * bundle 在盘上,且记录里的 bundle 摘要与盘上字节一致,才能把下次启动指向
735
+ * 它。标记机制之前就激活过、目录里没有记录文件的历史版本(current/last
736
+ * 里有它)免检;有记录文件的目录即便是 current/last 也照常复核。
776
737
  */
777
738
  public async verifyActivation(hash: string): Promise<void> {
778
739
  const safeHash = assertSafePathComponent(hash);
779
- if (this.assertActivatable(safeHash)) {
780
- await verifyInstallForActivation(
781
- `${this.rootDir}/${safeHash}`,
782
- safeHash,
783
- this.getBundlePath(safeHash),
784
- );
785
- }
740
+ await verifyInstallForActivation(
741
+ `${this.rootDir}/${safeHash}`,
742
+ safeHash,
743
+ this.readString('currentVersion') === safeHash ||
744
+ this.readString('lastVersion') === safeHash,
745
+ );
786
746
  }
787
747
 
788
748
  /** 内存里切换状态(不落盘);调用方负责 flush。 */
@@ -952,29 +912,56 @@ export class UpdateContext {
952
912
  }
953
913
  }
954
914
 
915
+ // 进行中的摘要计算:JS getBundleHash 与原生轮次可能同时要它,共用一次。
916
+ private bundleHashTask: Promise<string> | null = null;
917
+
955
918
  /**
956
919
  * bundleHash = 二进制内嵌 JS bundle 的 sha256 —— 标识二进制本身,与当前运行
957
920
  * 的热更版本无关。哈希对象与 pdiff 的源读取完全一致:rawfile 的
958
921
  * bundle.harmony.js(DownloadTask.doPatchFromApp 同一读法)。懒计算 + 缓存;
959
922
  * 永不失败——空串表示"未知",服务端回退 buildTime 启发式。
960
923
  */
961
- public async getBundleHash(): Promise<string> {
924
+ public getBundleHash(): Promise<string> {
962
925
  if (UpdateContext.DEBUG) {
963
926
  // debug 下 bundle 由 metro 提供,与 dev 删 buildTime 的行为对齐。
964
- return '';
927
+ return Promise.resolve('');
965
928
  }
966
929
  const cachePrefix = `${this.getPackageVersion()}|${this.getBundleModifiedTime()}|`;
967
930
  const cached = this.readString(UpdateContext.KEY_BUNDLE_HASH_CACHE);
968
931
  if (cached.startsWith(cachePrefix)) {
969
- return cached.slice(cachePrefix.length);
932
+ return Promise.resolve(cached.slice(cachePrefix.length));
933
+ }
934
+ if (!this.bundleHashTask) {
935
+ const task = this.computeBundleHash(cachePrefix);
936
+ this.bundleHashTask = task;
937
+ task.then(
938
+ () => {
939
+ this.bundleHashTask = null;
940
+ },
941
+ () => {
942
+ this.bundleHashTask = null;
943
+ },
944
+ );
970
945
  }
946
+ return this.bundleHashTask;
947
+ }
948
+
949
+ private async computeBundleHash(cachePrefix: string): Promise<string> {
971
950
  let hash = '';
972
951
  try {
973
- // 异步读:TurboModule 跑在 UI 线程,同步读几 MB 会卡主线程一次。C++
974
- // 哈希本身毫秒级,同步无妨。
975
- const content =
976
- await this.context.resourceManager.getRawFileContent('bundle.harmony.js');
977
- hash = NativePatchCore.sha256Hex(content);
952
+ // 内嵌 bundle 有几 MB:不读进内存,也不在 UI 线程(TurboModule 所在)
953
+ // 哈希——native 工作线程按 rawfile descriptor 的区间 pread 流式计算。
954
+ // fd 在 promise 落定后才关闭(withRawFd)。
955
+ hash = await withRawFd(
956
+ this.context.resourceManager,
957
+ HARMONY_BUNDLE_FILE_NAME,
958
+ descriptor =>
959
+ NativePatchCore.sha256HexRawFdAsync(
960
+ descriptor.fd,
961
+ descriptor.offset,
962
+ descriptor.length,
963
+ ),
964
+ );
978
965
  } catch (error) {
979
966
  // rawfile 缺 bundle(未打包 release bundle)是合法的"未知",不是错误。
980
967
  logger.info(TAG, `Cannot hash embedded bundle: ${getErrorMessage(error)}`);
package/harmony/pushy.har CHANGED
Binary file
@@ -0,0 +1,185 @@
1
+ // Launch bundle resolution and the identity of the running binary: which
2
+ // bundle this launch loads, where downloaded versions live, and the
3
+ // packageVersion/buildTime pair that detects a rebuilt binary.
4
+ #import "RCTPushyInternal.h"
5
+
6
+ // The version whose bundle this process actually loaded (resolved in
7
+ // +bundleURL). resetToPackagedBundle must not delete its directory: update
8
+ // assets (images/fonts) are read from it on demand at runtime, so wiping it
9
+ // under a silent (no-restart) reset would break every image the running app
10
+ // has not loaded yet. Guarded by the state lock.
11
+ PUSHY_HIDDEN NSString *pushyLaunchVersion = nil;
12
+
13
+ #if !DEBUG
14
+ static NSString *PushyReadBuildTime(void) {
15
+ // The podspec's s.resource lands in the app bundle for a static
16
+ // library but inside this pod's own framework bundle under
17
+ // use_frameworks!; look in both.
18
+ NSString *stampPath =
19
+ [[NSBundle mainBundle] pathForResource:@"pushy_build_time" ofType:@"txt"]
20
+ ?: [[NSBundle bundleForClass:[RCTPushy class]] pathForResource:@"pushy_build_time"
21
+ ofType:@"txt"];
22
+ NSString *stamp = stampPath == nil ? nil
23
+ : [[NSString stringWithContentsOfFile:stampPath encoding:NSUTF8StringEncoding error:nil]
24
+ stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
25
+ if (stamp.length == 0) {
26
+ // Never empty: with "" SyncBinaryVersion would miss a rebuilt
27
+ // binary that kept its CFBundleShortVersionString, and a stale
28
+ // hot update would outlive an App Store update. Every build
29
+ // rewrites Info.plist, so its mtime (integer seconds) stands in.
30
+ RCTLogWarn(@"RCTPushy -- pushy_build_time.txt %@; using the Info.plist mtime as buildTime",
31
+ stampPath == nil ? @"not found in the main or framework bundle" : @"is empty");
32
+ NSString *infoPlist =
33
+ [[NSBundle mainBundle].bundlePath stringByAppendingPathComponent:@"Info.plist"];
34
+ NSDate *modified = [[NSFileManager defaultManager] attributesOfItemAtPath:infoPlist
35
+ error:nil].fileModificationDate;
36
+ stamp = [NSString stringWithFormat:@"%lld", (long long)[modified timeIntervalSince1970]];
37
+ }
38
+ return stamp;
39
+ }
40
+ #endif
41
+
42
+ @implementation RCTPushy (Launch)
43
+
44
+ // Resolves the bundle to launch from the persisted state; nil means the
45
+ // packaged bundle. Consumes the launch's one-shot state (first_time), so a
46
+ // second call is only valid after something re-armed it (a switchVersion).
47
+ // purgedVersion is set on tvOS when the installed version's files are gone.
48
+ + (NSURL *)resolveLaunchBundleURL:(NSString **)launchRolledBackVersionOut
49
+ purgedVersion:(NSString **)purgedVersionOut
50
+ {
51
+ __block NSURL *resolvedURL = nil;
52
+ __block NSString *launchRolledBackVersion = nil;
53
+ __block NSString *purgedVersion = nil;
54
+ @try {
55
+ PushyWithStateLock(^{
56
+ NSUserDefaults *defaults = PushyDefaults();
57
+
58
+ NSString *curPackageVersion = [RCTPushy packageVersion];
59
+ NSString *curBuildTime = [RCTPushy buildTime];
60
+
61
+ pushy::state::State state = PushyStateFromDefaults(defaults);
62
+ pushy::state::BinaryVersionSyncResult sync = pushy::state::SyncBinaryVersion(
63
+ state,
64
+ PushyToStdString(curPackageVersion),
65
+ PushyToStdString(curBuildTime)
66
+ );
67
+ if (sync.changed) {
68
+ [defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked()];
69
+ state = sync.state;
70
+ PushyApplyStateToDefaults(defaults, state);
71
+ }
72
+
73
+ if (!state.current_version.empty()) {
74
+ std::string const versionBeforeLaunch = state.current_version;
75
+ pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
76
+ state,
77
+ ignoreRollback.load(),
78
+ true
79
+ );
80
+ state = decision.state;
81
+
82
+ if (decision.did_rollback) {
83
+ // The crash-protection rollback: the new version never called
84
+ // markSuccess. Keep this visible in release logs.
85
+ RCTLogWarn(@"RCTPushy -- version %@ was not marked as successful, rolled back to %@",
86
+ PushyFromStdString(versionBeforeLaunch),
87
+ PushyFromStdString(state.current_version));
88
+ }
89
+ if (decision.did_rollback || decision.consumed_first_time) {
90
+ PushyApplyStateToDefaults(defaults, state);
91
+ }
92
+ if (decision.consumed_first_time) {
93
+ // bundleURL may be called many times, ignore rollbacks before process restarted again.
94
+ ignoreRollback = true;
95
+ [defaults setObject:@(YES) forKey:keyFirstLoadMarked];
96
+ }
97
+
98
+ NSString *loadVersion = PushyFromStdString(decision.load_version);
99
+ // Guard the rollback chain against cycles: a corrupted state
100
+ // returning an already-visited version would otherwise spin this
101
+ // loop forever during startup (Android has the same guard).
102
+ NSMutableSet<NSString *> *visitedVersions = [NSMutableSet set];
103
+ while (loadVersion.length && ![visitedVersions containsObject:loadVersion]) {
104
+ [visitedVersions addObject:loadVersion];
105
+ NSString *bundlePath = [PushyVersionDir(loadVersion) stringByAppendingPathComponent:BUNDLE_FILE_NAME];
106
+ if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
107
+ pushyLaunchVersion = loadVersion;
108
+ resolvedURL = [NSURL fileURLWithPath:bundlePath];
109
+ break;
110
+ } else {
111
+ #if TARGET_OS_TV
112
+ // Caches is purgeable on tvOS: a missing bundle is the OS
113
+ // reclaiming space, not a bad release. Everything under
114
+ // rctpushy went with it, so walking back to last_version
115
+ // is pointless, and a rollback mark would make this
116
+ // launch's check refuse to reinstall the version.
117
+ RCTLogWarn(@"RCTPushy -- bundle version %@ was purged by the system", loadVersion);
118
+ purgedVersion = loadVersion;
119
+ state = pushy::state::ForgetPurgedVersions(state);
120
+ PushyApplyStateToDefaults(defaults, state);
121
+ if (decision.consumed_first_time) {
122
+ // The first load being consumed never happens: the
123
+ // packaged bundle must not report isFirstTime.
124
+ ignoreRollback = false;
125
+ [defaults removeObjectForKey:keyFirstLoadMarked];
126
+ }
127
+ break;
128
+ #else
129
+ RCTLogError(@"RCTPushy -- bundle version %@ not found, rolling back", loadVersion);
130
+ state = pushy::state::Rollback(state);
131
+ PushyApplyStateToDefaults(defaults, state);
132
+ loadVersion = PushyFromStdString(state.current_version);
133
+ #endif
134
+ }
135
+ }
136
+ }
137
+ // Capture before constantsToExport consumes this one-shot marker.
138
+ // The delayed native check must never forceBoot the version that this
139
+ // launch just rolled back.
140
+ launchRolledBackVersion = PushyFromStdString(state.rolled_back_version);
141
+ });
142
+ } @finally {
143
+ *launchRolledBackVersionOut = launchRolledBackVersion;
144
+ *purgedVersionOut = purgedVersion;
145
+ }
146
+ return resolvedURL;
147
+ }
148
+
149
+ + (NSString *)downloadDir
150
+ {
151
+ #if TARGET_OS_TV
152
+ // tvOS apps cannot write Application Support; Caches is the only
153
+ // writable, non-temporary location, and the system may purge it while
154
+ // the app is not running (see the purged-version path in +bundleURL).
155
+ NSSearchPathDirectory base = NSCachesDirectory;
156
+ #else
157
+ NSSearchPathDirectory base = NSApplicationSupportDirectory;
158
+ #endif
159
+ NSString *directory = [NSSearchPathForDirectoriesInDomains(base, NSUserDomainMask, YES) firstObject];
160
+ return [directory stringByAppendingPathComponent:@"rctpushy"];
161
+ }
162
+
163
+ + (NSURL *)binaryBundleURL
164
+ {
165
+ return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
166
+ }
167
+
168
+ + (NSString *)packageVersion
169
+ {
170
+ static NSString *version =
171
+ [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
172
+ return version;
173
+ }
174
+
175
+ + (NSString *)buildTime
176
+ {
177
+ #if DEBUG
178
+ return @"0";
179
+ #else
180
+ static NSString *buildTime = PushyReadBuildTime();
181
+ return buildTime;
182
+ #endif
183
+ }
184
+
185
+ @end
@@ -26,8 +26,20 @@ typedef void (^RCTPushyBundlePreparationCompletion)(NSDictionary<NSString *, id>
26
26
  * Result keys: status (skipped/noUpdate/downloaded/failed/cancelled), reason,
27
27
  * hash (empty when nothing was installed), activated (selected for NEXT
28
28
  * launch, not a reload of the running instance).
29
+ *
30
+ * Waits at most 30 minutes for the round (longer than any round takes).
29
31
  */
30
32
  + (void)prepareBundleWithCompletion:(RCTPushyBundlePreparationCompletion _Nullable)completion
31
33
  NS_SWIFT_NAME(prepareBundle(completion:));
32
34
 
35
+ /**
36
+ * Same as prepareBundleWithCompletion:, but the completion runs after at most
37
+ * `timeout` seconds. On expiry the result is status failed, reason timeout;
38
+ * the round keeps running in the background and its result still applies to
39
+ * the next launch.
40
+ */
41
+ + (void)prepareBundleWithTimeout:(NSTimeInterval)timeout
42
+ completion:(RCTPushyBundlePreparationCompletion _Nullable)completion
43
+ NS_SWIFT_NAME(prepareBundle(timeout:completion:));
44
+
33
45
  @end