react-native-update 10.48.2 → 10.49.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 (46) hide show
  1. package/BUNDLEHASH_MIGRATION.md +335 -0
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +192 -0
  3. package/README-CN.md +40 -4
  4. package/README.md +40 -4
  5. package/android/jni/Android.mk +1 -0
  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/src/main/java/cn/reactnative/modules/update/UpdateContext.java +89 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -4
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +11 -0
  13. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  14. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  15. package/biome.json +59 -0
  16. package/cpp/patch_core/digest.cpp +177 -0
  17. package/cpp/patch_core/digest.h +43 -0
  18. package/cpp/patch_core/tests/patch_core_test.cpp +70 -0
  19. package/harmony/pushy/src/main/cpp/CMakeLists.txt +1 -0
  20. package/harmony/pushy/src/main/cpp/pushy.cpp +47 -0
  21. package/harmony/pushy/src/main/ets/DownloadTask.ts +2 -2
  22. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -0
  23. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -0
  24. package/harmony/pushy/src/main/ets/UpdateContext.ts +54 -0
  25. package/harmony/pushy.har +0 -0
  26. package/ios/RCTPushy/RCTPushy.mm +55 -0
  27. package/package.json +5 -6
  28. package/react-native-update.podspec +1 -0
  29. package/src/NativePushy.ts +7 -1
  30. package/src/client.ts +83 -57
  31. package/src/context.ts +7 -7
  32. package/src/core.ts +78 -51
  33. package/src/endpoint.ts +7 -7
  34. package/src/error.ts +3 -3
  35. package/src/i18n.ts +2 -2
  36. package/src/index.ts +4 -4
  37. package/src/isInRollout.ts +10 -1
  38. package/src/locales/en.ts +2 -0
  39. package/src/locales/zh.ts +2 -0
  40. package/src/provider.tsx +46 -56
  41. package/src/resolveCheckResult.ts +1 -1
  42. package/src/telemetry.ts +1 -1
  43. package/src/type.ts +31 -5
  44. package/src/utils.ts +30 -30
  45. package/.eslintrc.js +0 -4
  46. package/.prettierrc.js +0 -6
@@ -6,6 +6,7 @@
6
6
  #include <vector>
7
7
 
8
8
  #include "archive_patch_core.h"
9
+ #include "digest.h"
9
10
  #include "hbc_transform_wire.h"
10
11
  #include "patch_core.h"
11
12
  #include "state_core.h"
@@ -890,6 +891,51 @@ bool ExportFunction(
890
891
  } // namespace
891
892
 
892
893
 
894
+ // sha256(Uint8Array | ArrayBuffer) -> 小写 hex。同步:bundleHash 的输入是
895
+ // rawfile 里的 bundle(已整体读入内存,与 pdiff 的读法一致),哈希本身毫秒级。
896
+ static napi_value Sha256Hex(napi_env env, napi_callback_info info) {
897
+ size_t argc = 1;
898
+ napi_value args[1] = {nullptr};
899
+ if (!GetArgCount(env, info, &argc, args) || argc < 1) {
900
+ ThrowError(env, "sha256Hex: missing data argument");
901
+ return nullptr;
902
+ }
903
+
904
+ void* data = nullptr;
905
+ size_t length = 0;
906
+ bool is_typedarray = false;
907
+ napi_is_typedarray(env, args[0], &is_typedarray);
908
+ if (is_typedarray) {
909
+ napi_typedarray_type type;
910
+ napi_value arraybuffer = nullptr;
911
+ size_t offset = 0;
912
+ if (napi_get_typedarray_info(
913
+ env, args[0], &type, &length, &data, &arraybuffer, &offset) !=
914
+ napi_ok) {
915
+ ThrowError(env, "sha256Hex: cannot read typed array");
916
+ return nullptr;
917
+ }
918
+ } else {
919
+ bool is_arraybuffer = false;
920
+ napi_is_arraybuffer(env, args[0], &is_arraybuffer);
921
+ if (!is_arraybuffer ||
922
+ napi_get_arraybuffer_info(env, args[0], &data, &length) != napi_ok) {
923
+ ThrowError(env, "sha256Hex: expected Uint8Array or ArrayBuffer");
924
+ return nullptr;
925
+ }
926
+ }
927
+
928
+ pushy::digest::Sha256 hasher;
929
+ if (length > 0) {
930
+ hasher.Update(static_cast<const uint8_t*>(data), length);
931
+ }
932
+ const std::string hex = hasher.HexDigest();
933
+
934
+ napi_value result = nullptr;
935
+ napi_create_string_utf8(env, hex.c_str(), hex.size(), &result);
936
+ return result;
937
+ }
938
+
893
939
  // 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道)
894
940
  static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
895
941
  napi_value result = nullptr;
@@ -907,6 +953,7 @@ napi_value Init(napi_env env, napi_value exports) {
907
953
  !ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
908
954
  !ExportFunction(env, exports, "applyPatchFromFileSource", ApplyPatchFromFileSource) ||
909
955
  !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
956
+ !ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
910
957
  !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion)) {
911
958
  return nullptr;
912
959
  }
@@ -72,7 +72,7 @@ function toArrayBufferSlice(
72
72
  offset: number,
73
73
  length: number,
74
74
  ): ArrayBuffer {
75
- return payload.buffer.slice(
75
+ return (payload.buffer as ArrayBuffer).slice(
76
76
  payload.byteOffset + offset,
77
77
  payload.byteOffset + offset + length,
78
78
  );
@@ -560,7 +560,7 @@ export class DownloadTask {
560
560
  'bundle.harmony.js',
561
561
  );
562
562
  await this.applyBundlePatchFromFileSource(
563
- originContent,
563
+ originContent.buffer as ArrayBuffer,
564
564
  params.unzipDirectory,
565
565
  bundlePatchPath,
566
566
  `${params.unzipDirectory}/bundle.harmony.js`,
@@ -80,6 +80,8 @@ interface NativePatchCoreBindings {
80
80
  keepPrevious: string,
81
81
  maxAgeDays: number,
82
82
  ): Promise<void>;
83
+ /** sha256(小写 hex)。同步:输入是已在内存的 rawfile bundle,哈希毫秒级 */
84
+ sha256Hex(data: Uint8Array): string;
83
85
  /** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */
84
86
  getSupportedDiffVersion(): number;
85
87
  }
@@ -233,6 +233,17 @@ export class PushyTurboModule extends UITurboModule {
233
233
  }
234
234
  }
235
235
 
236
+ async getBundleHash(): Promise<string> {
237
+ logger.debug(TAG, ',call getBundleHash');
238
+ try {
239
+ return this.context.getBundleHash();
240
+ } catch (error) {
241
+ // 空串 = "未知",服务端回退 buildTime 启发式;该方法永不 reject。
242
+ logger.error(TAG, `getBundleHash failed: ${getErrorMessage(error)}`);
243
+ return '';
244
+ }
245
+ }
246
+
236
247
  async resetToPackagedBundle(): Promise<void> {
237
248
  logger.debug(TAG, ',call resetToPackagedBundle');
238
249
  try {
@@ -632,4 +632,58 @@ export class UpdateContext {
632
632
  public getIsUsingBundleUrl(): boolean {
633
633
  return UpdateContext.isUsingBundleUrl;
634
634
  }
635
+
636
+ // bundleHash 缓存:"<packageVersion>|<updateTime>|<sha256hex>"。键标识当前
637
+ // 安装的二进制,每次(覆盖)安装 updateTime 都会变,每个安装只算一次。
638
+ private static readonly KEY_BUNDLE_HASH_CACHE = 'bundleHashCache';
639
+
640
+ private getBundleUpdateTime(): number {
641
+ try {
642
+ const bundleInfo = bundleManager.getBundleInfoForSelfSync(
643
+ this.getBundleFlags(),
644
+ );
645
+ return bundleInfo?.updateTime ?? 0;
646
+ } catch (error) {
647
+ console.error('Failed to get bundle update time:', error);
648
+ return 0;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * bundleHash = 二进制内嵌 JS bundle 的 sha256 —— 标识二进制本身,与当前运行
654
+ * 的热更版本无关。哈希对象与 pdiff 的源读取完全一致:rawfile 的
655
+ * bundle.harmony.js(DownloadTask.doPatchFromApp 同一读法)。懒计算 + 缓存;
656
+ * 永不失败——空串表示"未知",服务端回退 buildTime 启发式。
657
+ */
658
+ public async getBundleHash(): Promise<string> {
659
+ if (UpdateContext.DEBUG) {
660
+ // debug 下 bundle 由 metro 提供,与 dev 删 buildTime 的行为对齐。
661
+ return '';
662
+ }
663
+ const cachePrefix = `${this.getPackageVersion()}|${this.getBundleUpdateTime()}|`;
664
+ const cached = this.readString(UpdateContext.KEY_BUNDLE_HASH_CACHE);
665
+ if (cached.startsWith(cachePrefix)) {
666
+ return cached.slice(cachePrefix.length);
667
+ }
668
+ let hash = '';
669
+ try {
670
+ // 异步读:TurboModule 跑在 UI 线程,同步读几 MB 会卡主线程一次。C++
671
+ // 哈希本身毫秒级,同步无妨。
672
+ const content =
673
+ await this.context.resourceManager.getRawFileContent('bundle.harmony.js');
674
+ hash = NativePatchCore.sha256Hex(content);
675
+ } catch (error) {
676
+ // rawfile 缺 bundle(未打包 release bundle)是合法的"未知",不是错误。
677
+ console.info('Cannot hash embedded bundle:', error);
678
+ return '';
679
+ }
680
+ if (hash) {
681
+ this.preferences.putSync(
682
+ UpdateContext.KEY_BUNDLE_HASH_CACHE,
683
+ cachePrefix + hash,
684
+ );
685
+ this.flushPreferences('cache bundle hash');
686
+ }
687
+ return hash;
688
+ }
635
689
  }
package/harmony/pushy.har CHANGED
Binary file
@@ -2,6 +2,7 @@
2
2
  #import "RCTPushyDownloader.h"
3
3
  #import "ZipArchive.h"
4
4
  #include "../../cpp/patch_core/archive_patch_core.h"
5
+ #include "../../cpp/patch_core/digest.h"
5
6
  #include "../../cpp/patch_core/hbc_transform_wire.h"
6
7
  #include "../../cpp/patch_core/error_codes.h"
7
8
  #include "../../cpp/patch_core/patch_core.h"
@@ -32,6 +33,10 @@ static NSString *const keyHashInfo = @"REACTNATIVECN_PUSHY_HASH_";
32
33
  static NSString *const keyFirstLoadMarked = @"REACTNATIVECN_PUSHY_FIRSTLOADMARKED_KEY";
33
34
  static NSString *const keyRolledBackMarked = @"REACTNATIVECN_PUSHY_ROLLEDBACKMARKED_KEY";
34
35
  static NSString *const KeyPackageUpdatedMarked = @"REACTNATIVECN_PUSHY_ISPACKAGEUPDATEDMARKED_KEY";
36
+ // bundleHash cache: "<cacheKey>|<sha256hex>" where cacheKey identifies the
37
+ // installed binary (packageVersion + embedded bundle size + mtime). Recomputed
38
+ // only when the key changes, i.e. once per install.
39
+ static NSString *const keyBundleHashCache = @"REACTNATIVECN_PUSHY_BUNDLEHASH_KEY";
35
40
  static NSString *const PushyErrorDomain = @"cn.reactnative.pushy";
36
41
 
37
42
  // file def
@@ -512,6 +517,56 @@ RCT_EXPORT_METHOD(restartApp:(RCTPromiseResolveBlock)resolve
512
517
  resolve(@true);
513
518
  }
514
519
 
520
+ RCT_EXPORT_METHOD(getBundleHash:(RCTPromiseResolveBlock)resolve
521
+ rejecter:(RCTPromiseRejectBlock)reject)
522
+ {
523
+ // bundleHash = sha256 of the bundle embedded in the binary — the identity
524
+ // of the binary itself, not of whatever hot update is currently running.
525
+ // It must hash exactly the bytes pdiff patches from (binaryBundleURL, the
526
+ // pdiff fromBundle). Lazily computed once per install, cached in defaults.
527
+ // Never rejects: an empty string means "unknown" and the server falls back
528
+ // to the buildTime heuristic.
529
+ #if DEBUG
530
+ // Metro serves the bundle in debug; the embedded file (if any) is not what
531
+ // is running. Mirrors the dev behaviour of buildTime.
532
+ resolve(@"");
533
+ #else
534
+ dispatch_async(_fileQueue, ^{
535
+ NSString *path = [[RCTPushy binaryBundleURL] path];
536
+ if (path == nil) {
537
+ resolve(@"");
538
+ return;
539
+ }
540
+ NSDictionary *attributes =
541
+ [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
542
+ if (attributes == nil) {
543
+ resolve(@"");
544
+ return;
545
+ }
546
+ NSString *cacheKey = [NSString stringWithFormat:@"%@|%llu|%.0f",
547
+ [RCTPushy packageVersion],
548
+ attributes.fileSize,
549
+ [attributes.fileModificationDate timeIntervalSince1970]];
550
+
551
+ NSUserDefaults *defaults = PushyDefaults();
552
+ NSString *cached = [defaults stringForKey:keyBundleHashCache];
553
+ NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
554
+ if ([cached hasPrefix:cachedPrefix]) {
555
+ resolve([cached substringFromIndex:cachedPrefix.length]);
556
+ return;
557
+ }
558
+
559
+ NSString *hash = PushyFromStdString(
560
+ pushy::digest::Sha256File(PushyToStdString(path))) ?: @"";
561
+ if (hash.length > 0) {
562
+ [defaults setObject:[cachedPrefix stringByAppendingString:hash]
563
+ forKey:keyBundleHashCache];
564
+ }
565
+ resolve(hash);
566
+ });
567
+ #endif
568
+ }
569
+
515
570
  RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
516
571
  rejecter:(RCTPromiseRejectBlock)reject)
517
572
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.48.2",
3
+ "version": "10.49.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
@@ -9,7 +9,7 @@
9
9
  "publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
10
10
  "postinstall": "node scripts/check-expo-version.js",
11
11
  "prepack": "bun submodule && bun lint",
12
- "lint": "eslint \"src/*.@(ts|tsx|js|jsx)\" && tsc --noEmit && node scripts/check-harmony-types.js",
12
+ "lint": "biome check src && tsc --noEmit && node scripts/check-harmony-types.js",
13
13
  "lint:harmony": "node scripts/check-harmony-types.js",
14
14
  "submodule": "git submodule update --init --recursive",
15
15
  "test": "bun test src/__tests__",
@@ -25,6 +25,7 @@
25
25
  "build:android-release": "cd Example/e2etest && bun && detox build --configuration android.emu.release",
26
26
  "test:android-release": "cd Example/e2etest && E2E_PLATFORM=android bun detox test --configuration android.emu.release --headless --record-logs all",
27
27
  "test:android-debug": "cd Example/e2etest && E2E_PLATFORM=android detox test --configuration android.emu.debug --headless --record-logs all",
28
+ "smoke:android-debug": "cd Example/e2etest && detox test --configuration android.emu.debug --config e2e/smoke/jest.config.js",
28
29
  "e2e:ios": "bun build:ios-release && bun test:ios-release",
29
30
  "e2e:android": "bun build:android-release && bun test:android-release",
30
31
  "tests:emulator:prepare": "cd .github/workflows/scripts/functions && bun && bun build",
@@ -63,8 +64,8 @@
63
64
  },
64
65
  "devDependencies": {
65
66
  "@babel/core": "^7.25.8",
67
+ "@biomejs/biome": "^2.5.5",
66
68
  "@react-native/babel-preset": "^0.73.21",
67
- "@react-native/eslint-config": "0.84.1",
68
69
  "@react-native/typescript-config": "0.84.1",
69
70
  "@types/bun": "^1.3.11",
70
71
  "@types/jest": "^30.0.0",
@@ -72,15 +73,13 @@
72
73
  "@types/react": "^19.2.14",
73
74
  "@types/react-test-renderer": "18.0.7",
74
75
  "detox": "^20.50.1",
75
- "eslint": "^8.57.0",
76
76
  "firebase-tools": "^15.11.0",
77
77
  "jest": "^30.3.0",
78
78
  "pod-install": "^1.0.14",
79
- "prettier": "^2",
80
79
  "react": "18.2.0",
81
80
  "react-native": "0.73",
82
81
  "react-test-renderer": "18.2.0",
83
82
  "ts-jest": "^29.4.6",
84
- "typescript": "^5.6.3"
83
+ "typescript": "^7.0.2"
85
84
  }
86
85
  }
@@ -111,6 +111,7 @@ Pod::Spec.new do |s|
111
111
  s.subspec 'RCTPushy' do |ss|
112
112
  ss.source_files = ['ios/RCTPushy/*.{h,m,mm}',
113
113
  'cpp/patch_core/archive_patch_core.{h,cpp}',
114
+ 'cpp/patch_core/digest.{h,cpp}',
114
115
  'cpp/patch_core/hbc_transform.{h,cpp}',
115
116
  'cpp/patch_core/hbc_transform_wire.{h,cpp}',
116
117
  'cpp/patch_core/patch_core.{h,cpp}',
@@ -1,4 +1,4 @@
1
- import { TurboModule, TurboModuleRegistry } from 'react-native';
1
+ import { type TurboModule, TurboModuleRegistry } from 'react-native';
2
2
 
3
3
  export interface Spec extends TurboModule {
4
4
  getConstants: () => {
@@ -20,6 +20,12 @@ export interface Spec extends TurboModule {
20
20
  restartApp(): Promise<void>;
21
21
  setNeedUpdate(options: { hash: string }): Promise<void>;
22
22
  markSuccess(): Promise<void>;
23
+ /**
24
+ * sha256 of the JS bundle embedded in the binary (the pdiff source), lazily
25
+ * computed and cached natively. Resolves to an empty string when unknown
26
+ * (debug build, no embedded bundle, hash failure) — never rejects.
27
+ */
28
+ getBundleHash(): Promise<string>;
23
29
  resetToPackagedBundle(): Promise<void>;
24
30
  downloadPatchFromPpk(options: {
25
31
  updateUrl: string;