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.
- package/android/jni/Android.mk +2 -0
- package/android/jni/hpatch.c +55 -20
- package/android/jni/hpatch.h +0 -6
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +29 -6
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +2 -0
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java +12 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +47 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +12 -3
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +13 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +11 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/cpp/patch_core/error_codes.h +3 -0
- package/cpp/patch_core/hbc_transform.cpp +147 -0
- package/cpp/patch_core/hbc_transform.h +53 -0
- package/cpp/patch_core/hbc_transform_wire.cpp +327 -0
- package/cpp/patch_core/hbc_transform_wire.h +52 -0
- package/cpp/patch_core/patch_core.cpp +126 -4
- package/cpp/patch_core/patch_core.h +5 -0
- package/cpp/patch_core/patch_core_android.cpp +3 -1
- package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.meta.json +1 -0
- package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
- package/cpp/patch_core/tests/hbc_transform_test.cpp +384 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +150 -1
- package/cpp/patch_core/update_core_android.cpp +10 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +33 -39
- package/harmony/pushy/src/main/cpp/pushy.cpp +31 -6
- package/harmony/pushy/src/main/ets/DownloadTask.ts +34 -2
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -2
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +93 -1
- package/ios/RCTPushy/RCTPushyDownloader.mm +11 -2
- package/package.json +1 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +1 -0
- package/src/client.ts +167 -38
- package/src/context.ts +4 -0
- package/src/core.ts +4 -0
- package/src/error.ts +51 -5
- package/src/locales/en.ts +2 -0
- package/src/locales/zh.ts +2 -0
- package/src/provider.tsx +32 -15
- package/src/telemetry.ts +6 -1
- 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 {
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
* comparing the caught error to the original still match; non-Error
|
|
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
|
-
|
|
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
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
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
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
if (!e
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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':
|