react-native-update 10.47.0 → 10.48.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 (63) hide show
  1. package/android/jni/Android.mk +2 -0
  2. package/android/jni/hpatch.c +55 -20
  3. package/android/jni/hpatch.h +0 -6
  4. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  5. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  6. package/android/lib/x86/librnupdate.so +0 -0
  7. package/android/lib/x86_64/librnupdate.so +0 -0
  8. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +29 -6
  9. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +2 -0
  10. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +1 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java +12 -0
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +47 -2
  13. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +12 -3
  14. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +13 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +11 -0
  16. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  17. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  18. package/cpp/patch_core/error_codes.h +3 -0
  19. package/cpp/patch_core/hbc_transform.cpp +147 -0
  20. package/cpp/patch_core/hbc_transform.h +53 -0
  21. package/cpp/patch_core/hbc_transform_wire.cpp +327 -0
  22. package/cpp/patch_core/hbc_transform_wire.h +52 -0
  23. package/cpp/patch_core/patch_core.cpp +126 -4
  24. package/cpp/patch_core/patch_core.h +5 -0
  25. package/cpp/patch_core/patch_core_android.cpp +3 -1
  26. package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
  27. package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
  28. package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
  29. package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
  30. package/cpp/patch_core/tests/fixtures/v96.meta.json +1 -0
  31. package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
  32. package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
  33. package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
  34. package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
  35. package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
  36. package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
  37. package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
  38. package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
  39. package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
  40. package/cpp/patch_core/tests/hbc_transform_test.cpp +384 -0
  41. package/cpp/patch_core/tests/patch_core_test.cpp +150 -1
  42. package/cpp/patch_core/update_core_android.cpp +10 -0
  43. package/harmony/pushy/src/main/cpp/CMakeLists.txt +33 -39
  44. package/harmony/pushy/src/main/cpp/pushy.cpp +31 -6
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +34 -2
  46. package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
  47. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
  48. package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -2
  49. package/harmony/pushy.har +0 -0
  50. package/ios/RCTPushy/RCTPushy.mm +93 -1
  51. package/ios/RCTPushy/RCTPushyDownloader.mm +11 -2
  52. package/package.json +1 -1
  53. package/react-native-update.podspec +2 -0
  54. package/src/NativePushy.ts +1 -0
  55. package/src/client.ts +167 -38
  56. package/src/context.ts +4 -0
  57. package/src/core.ts +4 -0
  58. package/src/error.ts +51 -5
  59. package/src/locales/en.ts +2 -0
  60. package/src/locales/zh.ts +2 -0
  61. package/src/provider.tsx +32 -15
  62. package/src/telemetry.ts +6 -1
  63. package/src/type.ts +3 -0
package/src/client.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  cInfo,
10
10
  currentVersion,
11
11
  currentVersionInfo,
12
+ supportedDiffVersion,
12
13
  isFirstTime,
13
14
  isRolledBack,
14
15
  packageVersion,
@@ -39,7 +40,12 @@ import {
39
40
  testUrls,
40
41
  } from './utils';
41
42
  import i18n from './i18n';
42
- import { toUpdateError, UpdateError, UpdateErrorCode } from './error';
43
+ import {
44
+ asUpdateErrorCode,
45
+ toUpdateError,
46
+ UpdateError,
47
+ UpdateErrorCode,
48
+ } from './error';
43
49
  import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
44
50
  import {
45
51
  resolveServerEventHash,
@@ -103,6 +109,10 @@ const defaultClientOptions: ClientOptions = {
103
109
  export const sharedState: {
104
110
  progressHandlers: Record<string, EmitterSubscription>;
105
111
  downloadingTasks: Record<string, Promise<string | undefined>>;
112
+ // Progress callbacks per hash: concurrent downloadUpdate callers of the
113
+ // same hash each register theirs here instead of the second one being
114
+ // silently dropped by the in-flight dedup.
115
+ progressCallbacks: Record<string, Set<(data: ProgressData) => void>>;
106
116
  downloadedHash?: string;
107
117
  toHash?: string;
108
118
  apkStatus: 'downloading' | 'downloaded' | null;
@@ -111,6 +121,7 @@ export const sharedState: {
111
121
  } = {
112
122
  progressHandlers: {},
113
123
  downloadingTasks: {},
124
+ progressCallbacks: {},
114
125
  downloadedHash: undefined,
115
126
  apkStatus: null,
116
127
  marked: false,
@@ -324,6 +335,7 @@ export class Pushy {
324
335
  }
325
336
  };
326
337
  private errorListeners = new Set<UpdateErrorListener>();
338
+ private emittedErrors = new WeakSet<Error>();
327
339
  /**
328
340
  * Subscribe to every error the client reports (regardless of throwError).
329
341
  * Returns an unsubscribe function.
@@ -334,6 +346,15 @@ export class Pushy {
334
346
  this.errorListeners.delete(listener);
335
347
  };
336
348
  };
349
+ /**
350
+ * Whether this exact error object already went through emitError (and was
351
+ * therefore delivered to onError subscribers). Lets UI layers decide if a
352
+ * caught error still needs surfacing — checking `e.code` is not enough,
353
+ * since axios/system errors carry their own code without ever entering the
354
+ * pipeline.
355
+ */
356
+ wasEmitted = (e: unknown): boolean =>
357
+ e instanceof Error && this.emittedErrors.has(e);
337
358
  /**
338
359
  * Single exit point for errors: reports to the logger (with the stable
339
360
  * code) and notifies onError listeners. Whether to also throw stays with
@@ -347,7 +368,15 @@ export class Pushy {
347
368
  data,
348
369
  }: { message?: string; data?: Record<string, string | number> } = {},
349
370
  ) => {
350
- this.report({ type, message, code: error.code, data });
371
+ this.emittedErrors.add(error);
372
+ this.report({
373
+ type,
374
+ message,
375
+ code: error.code,
376
+ // Structured context from the error (e.g. HTTP status) reaches the
377
+ // logger; explicit data wins on key conflicts.
378
+ data: error.extra ? { ...error.extra, ...data } : data,
379
+ });
351
380
  for (const listener of this.errorListeners) {
352
381
  try {
353
382
  listener(error, type);
@@ -501,7 +530,10 @@ export class Pushy {
501
530
  }
502
531
  } catch (e) {
503
532
  sharedState.applyingUpdate = false;
504
- const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
533
+ // A throw from the user's beforeReload hook is business-code failure,
534
+ // not an update-pipeline one: give it a distinct code so telemetry
535
+ // excludes it from the server-side patch-health stats.
536
+ const err = toUpdateError(e, 'USER_HOOK_ERROR');
505
537
  this.emitError(err, 'errorSwitchVersion', {
506
538
  data: { newVersion: hash },
507
539
  });
@@ -562,9 +594,20 @@ export class Pushy {
562
594
  this.lastChecking &&
563
595
  now - this.lastChecking < 1000 * 5
564
596
  ) {
565
- const result = await this.lastRespJson;
566
- this.notifyAfterCheckUpdate({ status: 'completed', result });
567
- return result;
597
+ try {
598
+ const result = await this.lastRespJson;
599
+ this.notifyAfterCheckUpdate({ status: 'completed', result });
600
+ return result;
601
+ } catch (e: any) {
602
+ // The shared in-flight check failed. Its initiating call reports it
603
+ // through emitError/throw; this call must still honor its own
604
+ // contract — afterCheckUpdate always fires and throwError applies —
605
+ // without double-reporting the same error.
606
+ const err = toUpdateError(e, 'CHECK_FAILED');
607
+ this.notifyAfterCheckUpdate({ status: 'error', error: err });
608
+ this.throwIfEnabled(err);
609
+ return undefined;
610
+ }
568
611
  }
569
612
  this.lastChecking = now;
570
613
  const fetchBody: Record<string, any> = {
@@ -572,6 +615,8 @@ export class Pushy {
572
615
  hash: currentVersion,
573
616
  buildTime,
574
617
  cInfo,
618
+ // 可消费的 diff 轨道版本(2 = hdiffv2 轨道),服务端据此门控下发
619
+ ...(supportedDiffVersion ? { diffV: supportedDiffVersion } : {}),
575
620
  ...extra,
576
621
  };
577
622
  if (__DEV__) {
@@ -652,6 +697,10 @@ export class Pushy {
652
697
  const existingTask = sharedState.downloadingTasks[hash];
653
698
  if (existingTask) {
654
699
  log(`download for hash ${hash} already in progress, reusing it`);
700
+ // The second caller's progress callback must still fire.
701
+ if (onDownloadProgress) {
702
+ sharedState.progressCallbacks[hash]?.add(onDownloadProgress);
703
+ }
655
704
  return existingTask;
656
705
  }
657
706
  const task = this.performDownload(updateInfo, onDownloadProgress);
@@ -679,38 +728,42 @@ export class Pushy {
679
728
  if (!hash) {
680
729
  return;
681
730
  }
682
- if (sharedState.progressHandlers[hash]) {
683
- return;
684
- }
685
731
  const patchStartTime = Date.now();
732
+ // One native listener per hash dispatching to a callback set, so
733
+ // concurrent callers deduped onto this task can each observe progress
734
+ // (they register via downloadUpdate).
735
+ const progressCallbacks = new Set<(data: ProgressData) => void>();
686
736
  if (onDownloadProgress) {
687
- const wrapProgress = (data: ProgressData) => {
688
- onDownloadProgress({
689
- ...data,
690
- progress: computeProgress(data.received, data.total),
691
- });
737
+ progressCallbacks.add(onDownloadProgress);
738
+ }
739
+ sharedState.progressCallbacks[hash] = progressCallbacks;
740
+ const dispatchProgress = (data: ProgressData) => {
741
+ const callbacks = sharedState.progressCallbacks[hash];
742
+ if (!callbacks || callbacks.size === 0) {
743
+ return;
744
+ }
745
+ const payload = {
746
+ ...data,
747
+ progress: computeProgress(data.received, data.total),
692
748
  };
693
- // @ts-expect-error harmony not in existing platforms
694
- if (Platform.OS === 'harmony') {
695
- sharedState.progressHandlers[hash] = DeviceEventEmitter.addListener(
696
- 'RCTPushyDownloadProgress',
697
- (progressData: ProgressData) => {
698
- if (progressData.hash === hash) {
699
- wrapProgress(progressData);
700
- }
701
- },
702
- );
703
- } else {
704
- sharedState.progressHandlers[hash] =
705
- pushyNativeEventEmitter.addListener(
706
- 'RCTPushyDownloadProgress',
707
- (progressData: ProgressData) => {
708
- if (progressData.hash === hash) {
709
- wrapProgress(progressData);
710
- }
711
- },
712
- );
749
+ callbacks.forEach(callback => callback(payload));
750
+ };
751
+ const onNativeProgress = (progressData: ProgressData) => {
752
+ if (progressData.hash === hash) {
753
+ dispatchProgress(progressData);
713
754
  }
755
+ };
756
+ // @ts-expect-error harmony not in existing platforms
757
+ if (Platform.OS === 'harmony') {
758
+ sharedState.progressHandlers[hash] = DeviceEventEmitter.addListener(
759
+ 'RCTPushyDownloadProgress',
760
+ onNativeProgress,
761
+ );
762
+ } else {
763
+ sharedState.progressHandlers[hash] = pushyNativeEventEmitter.addListener(
764
+ 'RCTPushyDownloadProgress',
765
+ onNativeProgress,
766
+ );
714
767
  }
715
768
  const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
716
769
  let succeeded = '';
@@ -801,7 +854,14 @@ export class Pushy {
801
854
  message: e.message,
802
855
  });
803
856
  errorMessages.push(errorMessage);
804
- lastError = Error(errorMessage);
857
+ // Keep the i18n message for display, but preserve the native
858
+ // rejection's stable code (e.g. PATCH_FAILED vs DOWNLOAD_FAILED —
859
+ // telemetry classifies on it) and the original error as cause.
860
+ lastError = new UpdateError(
861
+ errorMessage,
862
+ asUpdateErrorCode(e?.code) ?? 'DOWNLOAD_FAILED',
863
+ { cause: e },
864
+ );
805
865
  log(errorMessage);
806
866
  }
807
867
  } else if (!url && strategy.devNoopWhenNoUrl && __DEV__) {
@@ -817,6 +877,7 @@ export class Pushy {
817
877
  sharedState.progressHandlers[hash].remove();
818
878
  delete sharedState.progressHandlers[hash];
819
879
  }
880
+ delete sharedState.progressCallbacks[hash];
820
881
  if (!succeeded) {
821
882
  const message = errorMessages.join(';');
822
883
  if (lastError) {
@@ -943,10 +1004,78 @@ export class Pushy {
943
1004
  }
944
1005
  };
945
1006
  restartApp = async () => {
946
- if (!(await this.runBeforeReload({ type: 'restartApp' }))) {
947
- return;
1007
+ try {
1008
+ if (!(await this.runBeforeReload({ type: 'restartApp' }))) {
1009
+ return;
1010
+ }
1011
+ } catch (e) {
1012
+ const err = toUpdateError(e, 'USER_HOOK_ERROR');
1013
+ this.emitError(err, 'errorRestart');
1014
+ throw err;
1015
+ }
1016
+ try {
1017
+ return await PushyModule.restartApp();
1018
+ } catch (e) {
1019
+ const err = toUpdateError(e, 'RESTART_FAILED');
1020
+ this.emitError(err, 'errorRestart');
1021
+ throw err;
1022
+ }
1023
+ };
1024
+ /**
1025
+ * Reset to the bundle packaged in the binary: wipes every downloaded update
1026
+ * and the whole update state on the native side, so the app loads the
1027
+ * built-in bundle on the next launch (or immediately with
1028
+ * `{ restart: true }`). The client uuid is preserved.
1029
+ *
1030
+ * Returns whether the reset actually happened. Like the other update-flow
1031
+ * APIs it never throws by default — failures land in lastError/onError with
1032
+ * code RESET_FAILED — but the boolean must not be ignored: a false means the
1033
+ * app is still running the hot-updated bundle. Set `throwError` to throw.
1034
+ */
1035
+ resetToPackagedBundle = async (options?: {
1036
+ restart?: boolean;
1037
+ }): Promise<boolean> => {
1038
+ if (!assertWeb()) {
1039
+ // On web PushyModule is a Proxy of noops, so the feature-detect below
1040
+ // would report a false success.
1041
+ return false;
948
1042
  }
949
- return PushyModule.restartApp();
1043
+ if (typeof PushyModule.resetToPackagedBundle !== 'function') {
1044
+ // The JS layer can arrive via hot update onto an older binary whose
1045
+ // native module predates this method.
1046
+ const err = new UpdateError(
1047
+ this.t('error_reset_not_supported'),
1048
+ 'RESET_FAILED',
1049
+ );
1050
+ this.emitError(err, 'errorReset');
1051
+ this.throwIfEnabled(err);
1052
+ return false;
1053
+ }
1054
+ try {
1055
+ await PushyModule.resetToPackagedBundle();
1056
+ } catch (e) {
1057
+ const err = toUpdateError(e, 'RESET_FAILED');
1058
+ this.emitError(err, 'errorReset');
1059
+ this.throwIfEnabled(err);
1060
+ return false;
1061
+ }
1062
+ // The downloaded versions are gone; drop JS bookkeeping referring to them
1063
+ // so a stale downloadedHash cannot be switched to.
1064
+ sharedState.downloadedHash = undefined;
1065
+ sharedState.toHash = undefined;
1066
+ sharedState.marked = false;
1067
+ this.report({ type: 'reset' });
1068
+ if (options?.restart) {
1069
+ try {
1070
+ await this.restartApp();
1071
+ } catch (e: any) {
1072
+ // The reset itself succeeded and the restart failure was already
1073
+ // reported through the pipeline; the boolean must still say "state
1074
+ // is reset".
1075
+ log('restart after reset failed:', e?.message || e);
1076
+ }
1077
+ }
1078
+ return true;
950
1079
  };
951
1080
  }
952
1081
 
package/src/context.ts CHANGED
@@ -15,6 +15,7 @@ export const defaultContext = {
15
15
  downloadUpdate: asyncNoop,
16
16
  downloadAndInstallApk: asyncNoop,
17
17
  restartApp: asyncNoop,
18
+ resetToPackagedBundle: asyncNoop,
18
19
  getCurrentVersionInfo: () => Promise.resolve({}),
19
20
  parseTestQrCode: () => false,
20
21
  currentHash: '',
@@ -43,6 +44,9 @@ export const UpdateContext = createContext<{
43
44
  } | null;
44
45
  parseTestQrCode: (code: string) => boolean;
45
46
  restartApp: () => Promise<void>;
47
+ resetToPackagedBundle: (options?: {
48
+ restart?: boolean;
49
+ }) => Promise<boolean | void>;
46
50
  currentHash: string;
47
51
  packageVersion: string;
48
52
  client?: Pushy | Cresc;
package/src/core.ts CHANGED
@@ -65,6 +65,10 @@ export const rolledBackVersion: string = PushyConstants.rolledBackVersion;
65
65
  export const isRolledBack: boolean = !!rolledBackVersion;
66
66
 
67
67
  export const buildTime: string = PushyConstants.buildTime;
68
+ // 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道:HBC 变换 +
69
+ // 流式容器);旧原生无此常量时为 0(不上报,服务端只发 baseline)
70
+ export const supportedDiffVersion: number =
71
+ PushyConstants.supportedDiffVersion || 0;
68
72
  let uuid = PushyConstants.uuid;
69
73
 
70
74
  async function getLocalHashInfo(hash: string) {
package/src/error.ts CHANGED
@@ -21,14 +21,53 @@ export type UpdateErrorCode =
21
21
  | 'STORAGE_PERMISSION_REJECTED'
22
22
  | 'STORAGE_PERMISSION_ERROR'
23
23
  | 'APK_DOWNLOAD_FAILED'
24
+ // A throw from a user-provided hook (e.g. beforeReload) — not an update
25
+ // pipeline failure, and excluded from server-side patch-health telemetry.
26
+ | 'USER_HOOK_ERROR'
24
27
  // Native codes (see cpp/patch_core/error_codes.h)
25
28
  | 'INVALID_OPTIONS'
26
29
  | 'PATCH_FAILED'
27
30
  | 'FILE_OPERATION_FAILED'
28
31
  | 'RESTART_FAILED'
32
+ | 'RESET_FAILED'
29
33
  | 'INVALID_HASH_INFO'
30
34
  | 'UNSUPPORTED_PLATFORM';
31
35
 
36
+ const KNOWN_CODES = new Set<string>([
37
+ 'MODULE_NOT_LOADED',
38
+ 'APPKEY_REQUIRED',
39
+ 'NO_ENDPOINTS',
40
+ 'HTTP_STATUS',
41
+ 'CHECK_FAILED',
42
+ 'DOWNLOAD_FAILED',
43
+ 'SWITCH_VERSION_FAILED',
44
+ 'MARK_SUCCESS_FAILED',
45
+ 'APK_INSTALL_PENDING',
46
+ 'STORAGE_PERMISSION_REJECTED',
47
+ 'STORAGE_PERMISSION_ERROR',
48
+ 'APK_DOWNLOAD_FAILED',
49
+ 'USER_HOOK_ERROR',
50
+ 'INVALID_OPTIONS',
51
+ 'PATCH_FAILED',
52
+ 'FILE_OPERATION_FAILED',
53
+ 'RESTART_FAILED',
54
+ 'RESET_FAILED',
55
+ 'INVALID_HASH_INFO',
56
+ 'UNSUPPORTED_PLATFORM',
57
+ ]);
58
+
59
+ /**
60
+ * Narrow an arbitrary `code` property (axios' ERR_NETWORK, Node's
61
+ * ECONNREFUSED, RN's default EUNSPECIFIED, ...) to our stable set; anything
62
+ * else is treated as absent so it never leaks into telemetry aggregation.
63
+ */
64
+ export const asUpdateErrorCode = (
65
+ code: unknown,
66
+ ): UpdateErrorCode | undefined =>
67
+ typeof code === 'string' && KNOWN_CODES.has(code)
68
+ ? (code as UpdateErrorCode)
69
+ : undefined;
70
+
32
71
  export class UpdateError extends Error {
33
72
  code: UpdateErrorCode;
34
73
  cause?: unknown;
@@ -49,9 +88,11 @@ export class UpdateError extends Error {
49
88
 
50
89
  /**
51
90
  * Attach a code to an unknown thrown value. An existing Error keeps its
52
- * identity (message, stack, and any code already assigned upstream) so callers
53
- * comparing the caught error to the original still match; non-Error values are
54
- * wrapped.
91
+ * identity (message, stack, and any known code already assigned upstream) so
92
+ * callers comparing the caught error to the original still match; non-Error
93
+ * values are wrapped. A foreign `code` (axios/system errors) is overwritten
94
+ * with ours; a frozen/sealed Error that rejects the assignment is wrapped
95
+ * instead (identity is lost only in that edge case).
55
96
  */
56
97
  export const toUpdateError = (
57
98
  e: unknown,
@@ -59,8 +100,13 @@ export const toUpdateError = (
59
100
  ): UpdateError => {
60
101
  if (e instanceof Error) {
61
102
  const err = e as UpdateError;
62
- if (!err.code) {
63
- err.code = code;
103
+ if (!asUpdateErrorCode(err.code)) {
104
+ try {
105
+ err.code = code;
106
+ } catch {}
107
+ if (err.code !== code) {
108
+ return new UpdateError(err.message, code, { cause: err });
109
+ }
64
110
  }
65
111
  return err;
66
112
  }
package/src/locales/en.ts CHANGED
@@ -61,6 +61,8 @@ export default {
61
61
  error_storage_permission_rejected:
62
62
  'Storage permission denied, unable to download the APK',
63
63
  error_apk_download_failed: 'Failed to download or install the APK',
64
+ error_reset_not_supported:
65
+ 'resetToPackagedBundle is not available in the installed native module. Please rebuild the app with the latest react-native-update.',
64
66
 
65
67
  // Development messages
66
68
  dev_debug_disabled:
package/src/locales/zh.ts CHANGED
@@ -56,6 +56,8 @@ export default {
56
56
  error_apk_pending_install: '安装包已下载完成,请在系统安装界面完成安装',
57
57
  error_storage_permission_rejected: '存储权限被拒绝,无法下载安装包',
58
58
  error_apk_download_failed: '安装包下载或安装失败',
59
+ error_reset_not_supported:
60
+ '当前安装的原生模块不支持 resetToPackagedBundle,请使用最新版 react-native-update 重新打包后再试。',
59
61
 
60
62
  // Development messages
61
63
  dev_debug_disabled:
package/src/provider.tsx CHANGED
@@ -152,10 +152,12 @@ export const UpdateProvider = ({
152
152
  ]);
153
153
  return true;
154
154
  } catch (e: any) {
155
- // Client pipeline errors carry a code and were already surfaced via
156
- // the onError subscription; errors thrown by user hooks
157
- // (afterDownloadUpdate) bypass the pipeline and are surfaced here.
158
- if (!e?.code) {
155
+ // Client pipeline errors were already surfaced via the onError
156
+ // subscription; errors thrown by user hooks (afterDownloadUpdate)
157
+ // bypass the pipeline and are surfaced here. Asking the client
158
+ // instead of checking `e.code` matters: axios/system errors carry
159
+ // their own code without ever entering the pipeline.
160
+ if (!client.wasEmitted(e)) {
159
161
  setLastError(e);
160
162
  alertError(client.t('update_failed'), e.message);
161
163
  }
@@ -186,10 +188,10 @@ export const UpdateProvider = ({
186
188
  try {
187
189
  rootInfo = await client.checkUpdate(extra);
188
190
  } catch (e: any) {
189
- // Client pipeline errors carry a code and were already surfaced via
190
- // the onError subscription; errors thrown by user hooks
191
- // (beforeCheckUpdate) bypass the pipeline and are surfaced here.
192
- if (!e?.code) {
191
+ // Client pipeline errors were already surfaced via the onError
192
+ // subscription; errors thrown by user hooks (beforeCheckUpdate)
193
+ // bypass the pipeline and are surfaced here (see wasEmitted).
194
+ if (!client.wasEmitted(e)) {
193
195
  setLastError(e);
194
196
  alertError(client.t('error_update_check_failed'), e.message);
195
197
  }
@@ -388,6 +390,13 @@ export const UpdateProvider = ({
388
390
  return client.restartApp();
389
391
  }, [client]);
390
392
 
393
+ const resetToPackagedBundle = useCallback(
394
+ async (resetOptions?: { restart?: boolean }) => {
395
+ return client.resetToPackagedBundle(resetOptions);
396
+ },
397
+ [client],
398
+ );
399
+
391
400
  useEffect(() => {
392
401
  if (!assertWeb()) {
393
402
  return;
@@ -396,15 +405,21 @@ export const UpdateProvider = ({
396
405
  if (!url) {
397
406
  return;
398
407
  }
399
- const params = new URL(url).searchParams;
400
- const payload = {
401
- type: params.get('type'),
402
- data: params.get('data'),
403
- };
404
- parseTestPayload(payload);
408
+ try {
409
+ const params = new URL(url).searchParams;
410
+ const payload = {
411
+ type: params.get('type'),
412
+ data: params.get('data'),
413
+ };
414
+ parseTestPayload(payload);
415
+ } catch (e: any) {
416
+ // A malformed deep link (new URL throws) must not become an
417
+ // unhandled rejection / a throw inside the 'url' event handler.
418
+ log('parseLinking: invalid url', e?.message || e);
419
+ }
405
420
  };
406
421
 
407
- Linking.getInitialURL().then(parseLinking);
422
+ Linking.getInitialURL().then(parseLinking).catch(noop);
408
423
  const linkingHandler = ({ url }: { url: string }) => {
409
424
  parseLinking(url);
410
425
  };
@@ -438,6 +453,7 @@ export const UpdateProvider = ({
438
453
  currentVersionInfo,
439
454
  parseTestQrCode,
440
455
  restartApp,
456
+ resetToPackagedBundle,
441
457
  }),
442
458
  [
443
459
  checkUpdate,
@@ -452,6 +468,7 @@ export const UpdateProvider = ({
452
468
  downloadAndInstallApk,
453
469
  parseTestQrCode,
454
470
  restartApp,
471
+ resetToPackagedBundle,
455
472
  ],
456
473
  );
457
474
 
package/src/telemetry.ts CHANGED
@@ -43,7 +43,12 @@ export const resolveServerEventType = (
43
43
  case 'errorUpdate':
44
44
  return code === 'PATCH_FAILED' ? 'patch_fail' : 'download_fail';
45
45
  case 'errorSwitchVersion':
46
- return 'patch_fail';
46
+ // Activation failures are patch-health signals, but a throw from a
47
+ // user hook (beforeReload) or a restart-mechanics failure is not —
48
+ // those must not poison the stats driving the rollback safety net.
49
+ return code === 'USER_HOOK_ERROR' || code === 'RESTART_FAILED'
50
+ ? undefined
51
+ : 'patch_fail';
47
52
  case 'markSuccess':
48
53
  return 'mark_success';
49
54
  case 'rollback':
package/src/type.ts CHANGED
@@ -55,6 +55,9 @@ export type EventType =
55
55
  | 'errorUpdate'
56
56
  | 'markSuccess'
57
57
  | 'errorMarkSuccess'
58
+ | 'reset'
59
+ | 'errorReset'
60
+ | 'errorRestart'
58
61
  | 'errorSwitchVersion'
59
62
  | 'downloadingApk'
60
63
  | 'rejectStoragePermission'