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
package/src/client.ts CHANGED
@@ -1,24 +1,38 @@
1
1
  import {
2
2
  DeviceEventEmitter,
3
- EmitterSubscription,
3
+ type EmitterSubscription,
4
4
  Platform,
5
5
  } from 'react-native';
6
6
  import {
7
- PushyModule,
8
7
  buildTime,
9
8
  cInfo,
10
9
  currentVersion,
11
10
  currentVersionInfo,
12
- supportedDiffVersion,
11
+ getBundleHash,
13
12
  isFirstTime,
14
13
  isRolledBack,
14
+ PushyModule,
15
15
  packageVersion,
16
16
  pushyNativeEventEmitter,
17
17
  rolledBackVersion,
18
18
  setLocalHashInfo,
19
+ supportedDiffVersion,
19
20
  } from './core';
21
+ import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
22
+ import {
23
+ asUpdateErrorCode,
24
+ toUpdateError,
25
+ UpdateError,
26
+ type UpdateErrorCode,
27
+ } from './error';
28
+ import i18n from './i18n';
20
29
  import { PermissionsAndroid } from './permissions';
21
30
  import {
31
+ resolveServerEventHash,
32
+ resolveServerEventType,
33
+ truncateDetail,
34
+ } from './telemetry';
35
+ import type {
22
36
  BeforeReloadContext,
23
37
  CheckResult,
24
38
  ClientOptions,
@@ -38,20 +52,8 @@ import {
38
52
  noop,
39
53
  promiseAny,
40
54
  testUrls,
55
+ warn,
41
56
  } from './utils';
42
- import i18n from './i18n';
43
- import {
44
- asUpdateErrorCode,
45
- toUpdateError,
46
- UpdateError,
47
- UpdateErrorCode,
48
- } from './error';
49
- import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
50
- import {
51
- resolveServerEventHash,
52
- resolveServerEventType,
53
- truncateDetail,
54
- } from './telemetry';
55
57
 
56
58
  /**
57
59
  * Receives every error the client reports, alongside the report event type.
@@ -60,13 +62,16 @@ import {
60
62
  */
61
63
  export type UpdateErrorListener = (
62
64
  error: UpdateError,
63
- eventType: EventType,
65
+ eventType: EventType
64
66
  ) => void;
65
67
 
66
68
  const SERVER_PRESETS = {
67
69
  // cn
68
70
  Pushy: {
69
- main: ['https://update.react-native.cn/api', 'https://update.reactnative.cn/api'],
71
+ main: [
72
+ 'https://update.react-native.cn/api',
73
+ 'https://update.reactnative.cn/api',
74
+ ],
70
75
  queryUrls: [
71
76
  'https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json',
72
77
  'https://cdn.jsdelivr.net/gh/reactnativecn/react-native-update@master/endpoints.json',
@@ -88,15 +93,15 @@ const cloneServerConfig = (server: UpdateServerConfig): UpdateServerConfig => ({
88
93
 
89
94
  const excludeConfiguredEndpoints = (
90
95
  endpoints: string[],
91
- configuredEndpoints: string[],
96
+ configuredEndpoints: string[]
92
97
  ) => {
93
98
  const configured = new Set(configuredEndpoints);
94
- return endpoints.filter(endpoint => !configured.has(endpoint));
99
+ return endpoints.filter((endpoint) => !configured.has(endpoint));
95
100
  };
96
101
 
97
102
  assertWeb();
98
103
 
99
- const defaultClientOptions: ClientOptions = {
104
+ const createDefaultClientOptions = (): ClientOptions => ({
100
105
  appKey: '',
101
106
  autoMarkSuccess: true,
102
107
  updateStrategy: __DEV__ ? 'alwaysAlert' : 'alertUpdateAndIgnoreError',
@@ -104,7 +109,7 @@ const defaultClientOptions: ClientOptions = {
104
109
  logger: noop,
105
110
  debug: false,
106
111
  throwError: false,
107
- };
112
+ });
108
113
 
109
114
  export const sharedState: {
110
115
  progressHandlers: Record<string, EmitterSubscription>;
@@ -142,7 +147,7 @@ const assertHash = (hash: string) => {
142
147
 
143
148
  // for China users
144
149
  export class Pushy {
145
- options = { ...defaultClientOptions };
150
+ options: ClientOptions = createDefaultClientOptions();
146
151
  clientType: 'Pushy' | 'Cresc' = 'Pushy';
147
152
  lastChecking?: number;
148
153
  lastRespJson?: Promise<CheckResult>;
@@ -153,7 +158,7 @@ export class Pushy {
153
158
  version = cInfo.rnu;
154
159
  loggerPromise = (() => {
155
160
  let resolve: (value?: unknown) => void = () => {};
156
- const promise = new Promise(res => {
161
+ const promise = new Promise((res) => {
157
162
  resolve = res;
158
163
  });
159
164
  return {
@@ -167,14 +172,14 @@ export class Pushy {
167
172
  this.options.server = cloneServerConfig(SERVER_PRESETS[this.clientType]);
168
173
 
169
174
  i18n.setLocale(
170
- options.locale ?? (this.clientType === 'Pushy' ? 'zh' : 'en'),
175
+ options.locale ?? (this.clientType === 'Pushy' ? 'zh' : 'en')
171
176
  );
172
177
 
173
178
  if (Platform.OS === 'ios' || Platform.OS === 'android') {
174
179
  if (!options.appKey) {
175
180
  throw new UpdateError(
176
181
  i18n.t('error_appkey_required'),
177
- 'APPKEY_REQUIRED',
182
+ 'APPKEY_REQUIRED'
178
183
  );
179
184
  }
180
185
  }
@@ -235,7 +240,7 @@ export class Pushy {
235
240
  // no logger is ever provided.
236
241
  await Promise.race([
237
242
  this.loggerPromise.promise,
238
- new Promise(resolve => setTimeout(resolve, 10 * 1000)),
243
+ new Promise((resolve) => setTimeout(resolve, 10 * 1000)),
239
244
  ]);
240
245
  }
241
246
  const { logger = noop, appKey } = this.options;
@@ -314,7 +319,7 @@ export class Pushy {
314
319
  detail: truncateDetail(detail),
315
320
  }),
316
321
  },
317
- DEFAULT_FETCH_TIMEOUT_MS,
322
+ DEFAULT_FETCH_TIMEOUT_MS
318
323
  ).catch((e: any) => {
319
324
  log('telemetry report failed:', e?.message || e);
320
325
  });
@@ -366,7 +371,7 @@ export class Pushy {
366
371
  {
367
372
  message = error.message,
368
373
  data,
369
- }: { message?: string; data?: Record<string, string | number> } = {},
374
+ }: { message?: string; data?: Record<string, string | number> } = {}
370
375
  ) => {
371
376
  this.emittedErrors.add(error);
372
377
  this.report({
@@ -424,20 +429,20 @@ export class Pushy {
424
429
  }
425
430
  try {
426
431
  const resp = await promiseAny(
427
- server.queryUrls.map(queryUrl =>
428
- fetchWithTimeout(queryUrl, {}, DEFAULT_FETCH_TIMEOUT_MS),
429
- ),
432
+ server.queryUrls.map((queryUrl) =>
433
+ fetchWithTimeout(queryUrl, {}, DEFAULT_FETCH_TIMEOUT_MS)
434
+ )
430
435
  );
431
436
  const remoteEndpoints = await resp.json();
432
437
  log('fetch endpoints:', remoteEndpoints);
433
438
  if (Array.isArray(remoteEndpoints)) {
434
439
  return excludeConfiguredEndpoints(
435
440
  dedupeEndpoints(
436
- remoteEndpoints.filter(
437
- (endpoint): endpoint is string => typeof endpoint === 'string',
438
- ),
441
+ remoteEndpoints.filter(
442
+ (endpoint): endpoint is string => typeof endpoint === 'string'
443
+ )
439
444
  ),
440
- this.getConfiguredCheckEndpoints(),
445
+ this.getConfiguredCheckEndpoints()
441
446
  );
442
447
  }
443
448
  } catch (e) {
@@ -447,12 +452,12 @@ export class Pushy {
447
452
  };
448
453
  requestCheckResult = async (
449
454
  endpoint: string,
450
- fetchPayload: Parameters<typeof fetch>[1],
455
+ fetchPayload: Parameters<typeof fetch>[1]
451
456
  ) => {
452
457
  const resp = await fetchWithTimeout(
453
458
  this.getCheckUrl(endpoint),
454
459
  fetchPayload,
455
- DEFAULT_FETCH_TIMEOUT_MS,
460
+ DEFAULT_FETCH_TIMEOUT_MS
456
461
  );
457
462
 
458
463
  if (!resp.ok) {
@@ -463,7 +468,7 @@ export class Pushy {
463
468
  statusText: respText,
464
469
  }),
465
470
  'HTTP_STATUS',
466
- { extra: { status: resp.status } },
471
+ { extra: { status: resp.status } }
467
472
  );
468
473
  }
469
474
 
@@ -473,7 +478,7 @@ export class Pushy {
473
478
  const { endpoint, value } = await executeEndpointFallback<CheckResult>({
474
479
  configuredEndpoints: this.getConfiguredCheckEndpoints(),
475
480
  getRemoteEndpoints: this.getRemoteEndpoints,
476
- tryEndpoint: async currentEndpoint => {
481
+ tryEndpoint: async (currentEndpoint) => {
477
482
  try {
478
483
  return await this.requestCheckResult(currentEndpoint, fetchPayload);
479
484
  } catch (e) {
@@ -588,6 +593,11 @@ export class Pushy {
588
593
  this.notifyAfterCheckUpdate({ status: 'skipped' });
589
594
  return;
590
595
  }
596
+ // 内容寻址的二进制身份,服务端据此精确判定 pdiff 适用性(取代 buildTime
597
+ // 启发式)。同步读 core 里已预取的值:还没算完就省略字段(服务端回退
598
+ // buildTime 启发式),下一次检查自然带上——绝不为它 await、拖慢或复杂化
599
+ // 检查流程。
600
+ const bundleHash = __DEV__ ? '' : getBundleHash();
591
601
  const now = Date.now();
592
602
  if (
593
603
  this.lastRespJson &&
@@ -617,6 +627,7 @@ export class Pushy {
617
627
  cInfo,
618
628
  // 可消费的 diff 轨道版本(2 = hdiffv2 轨道),服务端据此门控下发
619
629
  ...(supportedDiffVersion ? { diffV: supportedDiffVersion } : {}),
630
+ ...(bundleHash ? { bundleHash } : {}),
620
631
  ...extra,
621
632
  };
622
633
  if (__DEV__) {
@@ -648,6 +659,16 @@ export class Pushy {
648
659
 
649
660
  log('checking result:', result);
650
661
 
662
+ if (result?.bundleStatus === 'unknownBundle') {
663
+ // 服务端判定当前二进制内嵌 bundle 未注册,增量已被降级为全量。只面向
664
+ // 开发者(日志 + 遥测,控制台聚合是主渠道);终端用户无感知。
665
+ warn(this.t('warn_unknown_bundle'));
666
+ this.report({
667
+ type: 'bundleMismatch',
668
+ data: bundleHash ? { bundleHash } : {},
669
+ });
670
+ }
671
+
651
672
  this.notifyAfterCheckUpdate({ status: 'completed', result });
652
673
  return result;
653
674
  } catch (e: any) {
@@ -666,7 +687,7 @@ export class Pushy {
666
687
  };
667
688
  downloadUpdate = async (
668
689
  updateInfo: CheckResult,
669
- onDownloadProgress?: (data: ProgressData) => void,
690
+ onDownloadProgress?: (data: ProgressData) => void
670
691
  ) => {
671
692
  const { hash } = updateInfo;
672
693
  if (
@@ -713,7 +734,7 @@ export class Pushy {
713
734
  };
714
735
  private performDownload = async (
715
736
  updateInfo: CheckResult,
716
- onDownloadProgress?: (data: ProgressData) => void,
737
+ onDownloadProgress?: (data: ProgressData) => void
717
738
  ) => {
718
739
  const {
719
740
  hash,
@@ -746,7 +767,9 @@ export class Pushy {
746
767
  ...data,
747
768
  progress: computeProgress(data.received, data.total),
748
769
  };
749
- callbacks.forEach(callback => callback(payload));
770
+ callbacks.forEach((callback) => {
771
+ callback(payload);
772
+ });
750
773
  };
751
774
  const onNativeProgress = (progressData: ProgressData) => {
752
775
  if (progressData.hash === hash) {
@@ -757,12 +780,12 @@ export class Pushy {
757
780
  if (Platform.OS === 'harmony') {
758
781
  sharedState.progressHandlers[hash] = DeviceEventEmitter.addListener(
759
782
  'RCTPushyDownloadProgress',
760
- onNativeProgress,
783
+ onNativeProgress
761
784
  );
762
785
  } else {
763
786
  sharedState.progressHandlers[hash] = pushyNativeEventEmitter.addListener(
764
787
  'RCTPushyDownloadProgress',
765
- onNativeProgress,
788
+ onNativeProgress
766
789
  );
767
790
  }
768
791
  const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
@@ -778,7 +801,10 @@ export class Pushy {
778
801
  type DownloadStrategy = {
779
802
  name: string;
780
803
  candidate: string | undefined;
781
- errorKey: 'error_diff_failed' | 'error_pdiff_failed' | 'error_full_patch_failed';
804
+ errorKey:
805
+ | 'error_diff_failed'
806
+ | 'error_pdiff_failed'
807
+ | 'error_full_patch_failed';
782
808
  skipInDev: boolean;
783
809
  devNoopWhenNoUrl: boolean;
784
810
  run: (url: string) => Promise<void>;
@@ -790,7 +816,7 @@ export class Pushy {
790
816
  errorKey: 'error_diff_failed',
791
817
  skipInDev: true,
792
818
  devNoopWhenNoUrl: false,
793
- run: url =>
819
+ run: (url) =>
794
820
  PushyModule.downloadPatchFromPpk({
795
821
  updateUrl: url,
796
822
  hash,
@@ -803,7 +829,7 @@ export class Pushy {
803
829
  errorKey: 'error_pdiff_failed',
804
830
  skipInDev: true,
805
831
  devNoopWhenNoUrl: false,
806
- run: url =>
832
+ run: (url) =>
807
833
  PushyModule.downloadPatchFromPackage({
808
834
  updateUrl: url,
809
835
  hash,
@@ -815,7 +841,7 @@ export class Pushy {
815
841
  errorKey: 'error_full_patch_failed',
816
842
  skipInDev: false,
817
843
  devNoopWhenNoUrl: true,
818
- run: url =>
844
+ run: (url) =>
819
845
  PushyModule.downloadFullUpdate({
820
846
  updateUrl: url,
821
847
  hash,
@@ -827,7 +853,7 @@ export class Pushy {
827
853
  if (attempt > 0) {
828
854
  const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000);
829
855
  log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
830
- await new Promise(r => setTimeout(r, backoffMs));
856
+ await new Promise((r) => setTimeout(r, backoffMs));
831
857
  errorMessages.length = 0;
832
858
  lastError = undefined;
833
859
  succeeded = '';
@@ -860,7 +886,7 @@ export class Pushy {
860
886
  lastError = new UpdateError(
861
887
  errorMessage,
862
888
  asUpdateErrorCode(e?.code) ?? 'DOWNLOAD_FAILED',
863
- { cause: e },
889
+ { cause: e }
864
890
  );
865
891
  log(errorMessage);
866
892
  }
@@ -926,7 +952,7 @@ export class Pushy {
926
952
  };
927
953
  downloadAndInstallApk = async (
928
954
  url: string,
929
- onDownloadProgress?: (data: ProgressData) => void,
955
+ onDownloadProgress?: (data: ProgressData) => void
930
956
  ) => {
931
957
  if (Platform.OS !== 'android') {
932
958
  return;
@@ -937,7 +963,7 @@ export class Pushy {
937
963
  if (sharedState.apkStatus === 'downloaded') {
938
964
  const err = new UpdateError(
939
965
  this.t('error_apk_pending_install'),
940
- 'APK_INSTALL_PENDING',
966
+ 'APK_INSTALL_PENDING'
941
967
  );
942
968
  this.emitError(err, 'errorInstallApk');
943
969
  this.throwIfEnabled(err);
@@ -946,12 +972,12 @@ export class Pushy {
946
972
  if (Platform.Version <= 23) {
947
973
  try {
948
974
  const granted = await PermissionsAndroid.request(
949
- PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
975
+ PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
950
976
  );
951
977
  if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
952
978
  const err = new UpdateError(
953
979
  this.t('error_storage_permission_rejected'),
954
- 'STORAGE_PERMISSION_REJECTED',
980
+ 'STORAGE_PERMISSION_REJECTED'
955
981
  );
956
982
  this.emitError(err, 'rejectStoragePermission');
957
983
  this.throwIfEnabled(err);
@@ -978,7 +1004,7 @@ export class Pushy {
978
1004
  if (progressData.hash === progressKey) {
979
1005
  onDownloadProgress(progressData);
980
1006
  }
981
- },
1007
+ }
982
1008
  );
983
1009
  }
984
1010
  try {
@@ -1045,7 +1071,7 @@ export class Pushy {
1045
1071
  // native module predates this method.
1046
1072
  const err = new UpdateError(
1047
1073
  this.t('error_reset_not_supported'),
1048
- 'RESET_FAILED',
1074
+ 'RESET_FAILED'
1049
1075
  );
1050
1076
  this.emitError(err, 'errorReset');
1051
1077
  this.throwIfEnabled(err);
package/src/context.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createContext, useContext, useMemo } from 'react';
2
- import { CheckResult, ProgressData } from './type';
3
- import { Pushy, Cresc } from './client';
2
+ import type { Cresc, Pushy } from './client';
4
3
  import i18n from './i18n';
4
+ import type { CheckResult, ProgressData } from './type';
5
5
 
6
6
  const noop = () => {};
7
- const asyncNoop = () => Promise.resolve();
7
+ const asyncNoop = () => Promise.resolve(undefined);
8
8
 
9
9
  export const defaultContext = {
10
10
  checkUpdate: asyncNoop,
@@ -24,12 +24,12 @@ export const defaultContext = {
24
24
  };
25
25
 
26
26
  export const UpdateContext = createContext<{
27
- checkUpdate: () => Promise<void | CheckResult>;
27
+ checkUpdate: () => Promise<undefined | CheckResult>;
28
28
  switchVersion: () => Promise<void>;
29
29
  switchVersionLater: () => Promise<void>;
30
30
  markSuccess: () => void;
31
31
  dismissError: () => void;
32
- downloadUpdate: () => Promise<boolean | void>;
32
+ downloadUpdate: () => Promise<boolean | undefined>;
33
33
  downloadAndInstallApk: (url: string) => Promise<void>;
34
34
  // @deprecated use currentVersionInfo instead
35
35
  getCurrentVersionInfo: () => Promise<{
@@ -46,7 +46,7 @@ export const UpdateContext = createContext<{
46
46
  restartApp: () => Promise<void>;
47
47
  resetToPackagedBundle: (options?: {
48
48
  restart?: boolean;
49
- }) => Promise<boolean | void>;
49
+ }) => Promise<boolean | undefined>;
50
50
  currentHash: string;
51
51
  packageVersion: string;
52
52
  client?: Pushy | Cresc;
@@ -58,7 +58,7 @@ export const UpdateContext = createContext<{
58
58
  // otherwise every tick would re-render all useUpdate() consumers even when
59
59
  // they never read progress.
60
60
  export const ProgressContext = createContext<ProgressData | undefined>(
61
- undefined,
61
+ undefined
62
62
  );
63
63
 
64
64
  /**
package/src/core.ts CHANGED
@@ -1,61 +1,61 @@
1
1
  import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
2
- import i18n from './i18n';
3
2
  import { UpdateError } from './error';
3
+ import i18n from './i18n';
4
4
  import { emptyModule, error, log } from './utils';
5
5
 
6
6
  /* eslint-disable @react-native/no-deep-imports */
7
7
  const {
8
- version: v,
8
+ version: v,
9
9
  } = require('react-native/Libraries/Core/ReactNativeVersion');
10
10
  const RNVersion = `${v.major}.${v.minor}.${v.patch}`;
11
11
  const isTurboModuleEnabled =
12
- // https://github.com/facebook/react-native/pull/48362
13
- (global as any).__turboModuleProxy || (global as any).RN$Bridgeless;
12
+ // https://github.com/facebook/react-native/pull/48362
13
+ (global as any).__turboModuleProxy || (global as any).RN$Bridgeless;
14
14
 
15
15
  export const PushyModule =
16
- Platform.OS === 'web'
17
- ? emptyModule
18
- : isTurboModuleEnabled
19
- ? require('./NativePushy').default
20
- : NativeModules.Pushy;
16
+ Platform.OS === 'web'
17
+ ? emptyModule
18
+ : isTurboModuleEnabled
19
+ ? require('./NativePushy').default
20
+ : NativeModules.Pushy;
21
21
 
22
22
  export const UpdateModule = PushyModule;
23
23
 
24
24
  if (!PushyModule) {
25
- throw new UpdateError(
26
- 'Failed to load react-native-update native module, please try to recompile',
27
- 'MODULE_NOT_LOADED',
28
- );
25
+ throw new UpdateError(
26
+ 'Failed to load react-native-update native module, please try to recompile',
27
+ 'MODULE_NOT_LOADED'
28
+ );
29
29
  }
30
30
 
31
31
  const PushyConstants = isTurboModuleEnabled
32
- ? PushyModule.getConstants()
33
- : PushyModule;
32
+ ? PushyModule.getConstants()
33
+ : PushyModule;
34
34
 
35
35
  export const downloadRootDir: string = PushyConstants.downloadRootDir;
36
36
  export const packageVersion: string = PushyConstants.packageVersion;
37
37
  export const currentVersion: string = PushyConstants.currentVersion;
38
38
 
39
39
  export function setLocalHashInfo(hash: string, info: Record<string, any>) {
40
- PushyModule.setLocalHashInfo(hash, JSON.stringify(info));
40
+ PushyModule.setLocalHashInfo(hash, JSON.stringify(info));
41
41
  }
42
42
 
43
43
  const currentVersionInfoString: string = PushyConstants.currentVersionInfo;
44
44
  let _currentVersionInfo: Record<string, any> = {};
45
45
  let isDebugChannel = false;
46
46
  if (currentVersionInfoString) {
47
- try {
48
- _currentVersionInfo = JSON.parse(currentVersionInfoString);
49
- if (_currentVersionInfo.debugChannel) {
50
- isDebugChannel = true;
51
- delete _currentVersionInfo.debugChannel;
52
- setLocalHashInfo(currentVersion, _currentVersionInfo);
53
- }
54
- } catch {
55
- error(
56
- i18n.t('error_parse_version_info', { info: currentVersionInfoString }),
57
- );
58
- }
47
+ try {
48
+ _currentVersionInfo = JSON.parse(currentVersionInfoString);
49
+ if (_currentVersionInfo.debugChannel) {
50
+ isDebugChannel = true;
51
+ delete _currentVersionInfo.debugChannel;
52
+ setLocalHashInfo(currentVersion, _currentVersionInfo);
53
+ }
54
+ } catch {
55
+ error(
56
+ i18n.t('error_parse_version_info', { info: currentVersionInfoString })
57
+ );
58
+ }
59
59
  }
60
60
  export const currentVersionInfo = _currentVersionInfo;
61
61
 
@@ -68,46 +68,73 @@ export const buildTime: string = PushyConstants.buildTime;
68
68
  // 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道:HBC 变换 +
69
69
  // 流式容器);旧原生无此常量时为 0(不上报,服务端只发 baseline)
70
70
  export const supportedDiffVersion: number =
71
- PushyConstants.supportedDiffVersion || 0;
71
+ PushyConstants.supportedDiffVersion || 0;
72
72
  let uuid = PushyConstants.uuid;
73
73
 
74
+ // bundleHash = sha256 of the JS bundle embedded in the binary (the pdiff
75
+ // source). Content identity for the server's pdiff-applicability check,
76
+ // replacing the buildTime heuristic. Natively lazy-computed and cached;
77
+ // prefetched fire-and-forget at module load so the value is usually settled
78
+ // before the first checkUpdate builds its body. Until then (first install,
79
+ // slow device) getBundleHash returns '' and the field is simply omitted — the
80
+ // server falls back to the buildTime heuristic and the next check carries it.
81
+ // Deliberately synchronous at read time: checkUpdate must not await anything
82
+ // between its dedup window and the fetch, and a hash is never worth delaying
83
+ // a check for.
84
+ let bundleHash = '';
85
+ // The JS layer can arrive via hot update onto an older binary whose native
86
+ // module predates the method, hence the feature detect. On web PushyModule is
87
+ // a noop Proxy and the call settles to ''.
88
+ if (typeof PushyModule.getBundleHash === 'function') {
89
+ Promise.resolve(PushyModule.getBundleHash())
90
+ .then((hash: unknown) => {
91
+ bundleHash = String(hash || '');
92
+ })
93
+ .catch((e: any) => {
94
+ log('getBundleHash failed:', e?.message || e);
95
+ });
96
+ }
97
+
98
+ /** '' while unknown (not yet computed, debug build, no embedded bundle, older native). */
99
+ export const getBundleHash = (): string => bundleHash;
100
+
74
101
  async function getLocalHashInfo(hash: string) {
75
- return JSON.parse(await PushyModule.getLocalHashInfo(hash));
102
+ return JSON.parse(await PushyModule.getLocalHashInfo(hash));
76
103
  }
77
104
 
78
105
  // @deprecated use currentVersionInfo instead
79
106
  export async function getCurrentVersionInfo(): Promise<{
80
- name?: string;
81
- description?: string;
82
- metaInfo?: string;
107
+ name?: string;
108
+ description?: string;
109
+ metaInfo?: string;
83
110
  }> {
84
- return currentVersion ? (await getLocalHashInfo(currentVersion)) || {} : {};
111
+ return currentVersion ? (await getLocalHashInfo(currentVersion)) || {} : {};
85
112
  }
86
113
 
87
114
  export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule);
88
115
 
89
116
  if (!uuid) {
90
- uuid = require('nanoid/non-secure').nanoid();
91
- // If persisting fails the uuid drifts on every launch, which skews gray
92
- // release bucketing and inflates stats — log it instead of failing silently.
93
- Promise.resolve(PushyModule.setUuid(uuid)).catch((e: any) => {
94
- log('setUuid error:', e?.message || e);
95
- });
117
+ uuid = require('nanoid/non-secure').nanoid();
118
+ // If persisting fails the uuid drifts on every launch, which skews gray
119
+ // release bucketing and inflates stats — log it instead of failing silently.
120
+ Promise.resolve(PushyModule.setUuid(uuid)).catch((e: any) => {
121
+ log('setUuid error:', e?.message || e);
122
+ });
96
123
  }
97
124
 
98
125
  export const cInfo = {
99
- rnu: require('../package.json').version,
100
- rn: RNVersion,
101
- os: `${Platform.OS} ${Platform.Version}`,
102
- uuid,
126
+ rnu: require('../package.json').version,
127
+ rn: RNVersion,
128
+ os: `${Platform.OS} ${Platform.Version}`,
129
+ uuid,
103
130
  };
104
131
 
105
132
  log('bootup status', {
106
- packageVersion,
107
- currentVersion,
108
- currentVersionInfo,
109
- isFirstTime,
110
- isFirstTimeDebug,
111
- isDebugChannel,
112
- cInfo,
133
+ packageVersion,
134
+ currentVersion,
135
+ currentVersionInfo,
136
+ isFirstTime,
137
+ isFirstTimeDebug,
138
+ isDebugChannel,
139
+ cInfo,
113
140
  });