react-native-update 10.53.1 → 10.54.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.
- package/README-CN.md +31 -31
- package/README.md +31 -31
- package/android/jni/HDiffPatch/.travis.yml +33 -0
- package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/lzma/DOC/7zC.txt +187 -0
- package/android/jni/lzma/DOC/7zFormat.txt +469 -0
- package/android/jni/lzma/DOC/Methods.txt +173 -0
- package/android/jni/lzma/DOC/installer.txt +166 -0
- package/android/jni/lzma/DOC/lzma-history.txt +446 -0
- package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
- package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
- package/android/jni/lzma/DOC/lzma.txt +328 -0
- package/android/jni/lzma/bin/7zS2.sfx +0 -0
- package/android/jni/lzma/bin/7zS2con.sfx +0 -0
- package/android/jni/lzma/bin/7zSD.sfx +0 -0
- package/android/jni/lzma/bin/7zdec.exe +0 -0
- package/android/jni/lzma/bin/7zr.exe +0 -0
- package/android/jni/lzma/bin/installer/config.txt +5 -0
- package/android/jni/lzma/bin/installer/cr.bat +5 -0
- package/android/jni/lzma/bin/lzma.exe +0 -0
- package/android/jni/lzma/bin/x64/7zr.exe +0 -0
- 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/ArchiveLimits.java +44 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +33 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -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/archive_limits.h +38 -0
- package/cpp/patch_core/install_record.h +36 -0
- package/cpp/update_flow_core/flow_json.h +1 -0
- package/cpp/update_flow_core/update_flow_core.cpp +41 -7
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
- package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +384 -25
- package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
- package/package.json +25 -2
- package/src/NativePushy.ts +13 -0
- package/src/client.ts +160 -43
- package/src/core.ts +17 -3
- package/src/error.ts +3 -0
- package/src/index.ts +8 -0
- package/src/locales/en.ts +3 -0
- package/src/locales/zh.ts +2 -0
- package/src/metadata.ts +166 -0
- package/src/provider.tsx +52 -17
- package/src/type.ts +22 -0
- package/src/updateFlowCore.ts +95 -4
- package/src/utils.ts +31 -19
- package/.gitmodules +0 -6
- package/BUNDLEHASH_MIGRATION.md +0 -355
- package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
- package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
- package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
- package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
- package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
- package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
- package/biome.json +0 -59
- 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 +0 -1
- 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 +0 -384
- package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
- package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
- package/scripts/check-expo-version.js +0 -92
package/src/NativePushy.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface Spec extends TurboModule {
|
|
|
12
12
|
isUsingBundleUrl: boolean;
|
|
13
13
|
currentVersionInfo: string;
|
|
14
14
|
supportedDiffVersion: number;
|
|
15
|
+
/**
|
|
16
|
+
* SHA-256 of the running hot-update bundle as recorded by the install
|
|
17
|
+
* (cpp/patch_core/install_record.h); '' for the embedded bundle, legacy
|
|
18
|
+
* installs, or older native modules.
|
|
19
|
+
*/
|
|
20
|
+
currentBundleSha256?: string;
|
|
15
21
|
};
|
|
16
22
|
setLocalHashInfo(hash: string, info: string): Promise<void>;
|
|
17
23
|
getLocalHashInfo(hash: string): Promise<string>;
|
|
@@ -30,6 +36,13 @@ export interface Spec extends TurboModule {
|
|
|
30
36
|
* an empty string when absent; never rejects.
|
|
31
37
|
*/
|
|
32
38
|
getNativeCheckCache(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* JS obtained a valid check response in this process for `config` (the
|
|
41
|
+
* same JSON syncNativeConfig persists). The delayed cold-start round skips
|
|
42
|
+
* its own request when its persisted config matches; a crash-rescue round
|
|
43
|
+
* is unaffected (§10.3). Older native modules lack this method.
|
|
44
|
+
*/
|
|
45
|
+
markJsCheckCompleted(config: string): Promise<void>;
|
|
33
46
|
reloadUpdate(options: { hash: string }): Promise<void>;
|
|
34
47
|
restartApp(): Promise<void>;
|
|
35
48
|
setNeedUpdate(options: { hash: string }): Promise<void>;
|
package/src/client.ts
CHANGED
|
@@ -41,10 +41,13 @@ import type {
|
|
|
41
41
|
UpdateServerConfig,
|
|
42
42
|
} from './type';
|
|
43
43
|
import {
|
|
44
|
+
buildCheckFingerprint,
|
|
44
45
|
buildCheckRequestBody,
|
|
45
46
|
type DownloadPlan,
|
|
46
47
|
type DownloadStrategyType,
|
|
47
48
|
decideDownload,
|
|
49
|
+
isMirrorRetryableCode,
|
|
50
|
+
isValidCheckResult,
|
|
48
51
|
} from './updateFlowCore';
|
|
49
52
|
import {
|
|
50
53
|
assertWeb,
|
|
@@ -171,6 +174,7 @@ export class Pushy {
|
|
|
171
174
|
clientType: 'Pushy' | 'Cresc' = 'Pushy';
|
|
172
175
|
lastChecking?: number;
|
|
173
176
|
lastRespJson?: Promise<CheckResult>;
|
|
177
|
+
lastCheckFingerprint?: string;
|
|
174
178
|
// Endpoint that most recently served a successful checkUpdate; telemetry
|
|
175
179
|
// reuses it instead of re-running the fallback race.
|
|
176
180
|
private lastWorkingEndpoint?: string;
|
|
@@ -369,6 +373,35 @@ export class Pushy {
|
|
|
369
373
|
});
|
|
370
374
|
};
|
|
371
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Tell the native cold-start orchestrator that this process already holds a
|
|
378
|
+
* valid check response for the current config, so its delayed round (5s
|
|
379
|
+
* after launch) does not repeat the request. Fire-and-forget: a failure
|
|
380
|
+
* costs one duplicate check. Never called on a failed check — the native
|
|
381
|
+
* rescue must keep running when JS could not reach the server.
|
|
382
|
+
*/
|
|
383
|
+
private markJsCheckCompleted = () => {
|
|
384
|
+
if (
|
|
385
|
+
Platform.OS === 'web' ||
|
|
386
|
+
typeof PushyModule.markJsCheckCompleted !== 'function'
|
|
387
|
+
) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const configJson = this.getNativeConfigJson();
|
|
391
|
+
if (!configJson) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
Promise.resolve(PushyModule.markJsCheckCompleted(configJson)).catch(
|
|
396
|
+
(e: any) => {
|
|
397
|
+
log('markJsCheckCompleted failed:', e?.message || e);
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
} catch (e: any) {
|
|
401
|
+
log('markJsCheckCompleted failed:', e?.message || e);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
372
405
|
private syncNativeConfig = () => {
|
|
373
406
|
if (
|
|
374
407
|
Platform.OS === 'web' ||
|
|
@@ -738,7 +771,17 @@ export class Pushy {
|
|
|
738
771
|
);
|
|
739
772
|
}
|
|
740
773
|
|
|
741
|
-
|
|
774
|
+
const result: unknown = await resp.json();
|
|
775
|
+
if (!isValidCheckResult(result)) {
|
|
776
|
+
// A 2xx that is not a verdict (`{"error": ...}`, a captive-portal page
|
|
777
|
+
// that parsed, ...) is a failed endpoint: throwing keeps the fallback
|
|
778
|
+
// moving to the next one instead of treating it as "no update".
|
|
779
|
+
throw new UpdateError(
|
|
780
|
+
this.t('error_invalid_check_response'),
|
|
781
|
+
'INVALID_RESPONSE'
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return result;
|
|
742
785
|
};
|
|
743
786
|
fetchCheckResult = async (fetchPayload: Parameters<typeof fetch>[1]) => {
|
|
744
787
|
const { endpoint, value } = await executeEndpointFallback<CheckResult>({
|
|
@@ -927,10 +970,33 @@ export class Pushy {
|
|
|
927
970
|
// buildTime 启发式),下一次检查自然带上——绝不为它 await、拖慢或复杂化
|
|
928
971
|
// 检查流程。
|
|
929
972
|
const bundleHash = __DEV__ ? '' : getBundleHash();
|
|
973
|
+
const fetchBody = buildCheckRequestBody({
|
|
974
|
+
packageVersion: this.getEffectivePackageVersion(),
|
|
975
|
+
currentVersion,
|
|
976
|
+
buildTime,
|
|
977
|
+
cInfo,
|
|
978
|
+
supportedDiffVersion,
|
|
979
|
+
bundleHash,
|
|
980
|
+
isDev: __DEV__,
|
|
981
|
+
extra,
|
|
982
|
+
});
|
|
983
|
+
const stringifyBody = JSON.stringify(fetchBody);
|
|
984
|
+
// Identity of this check: only a check with the exact same request (same
|
|
985
|
+
// extra / test hash / bundleHash / binary identity) against the same
|
|
986
|
+
// endpoint set may share an in-flight or just-settled response. Two
|
|
987
|
+
// checks 2s apart with different `extra` are different questions.
|
|
988
|
+
const fingerprint = buildCheckFingerprint({
|
|
989
|
+
appKey: this.options.appKey,
|
|
990
|
+
endpoints: this.options.server?.main,
|
|
991
|
+
queryUrls: this.options.server?.queryUrls,
|
|
992
|
+
// The client uuid travels inside cInfo, i.e. inside `body`.
|
|
993
|
+
body: stringifyBody,
|
|
994
|
+
});
|
|
930
995
|
const now = Date.now();
|
|
931
996
|
if (
|
|
932
997
|
this.lastRespJson &&
|
|
933
998
|
this.lastChecking &&
|
|
999
|
+
this.lastCheckFingerprint === fingerprint &&
|
|
934
1000
|
now - this.lastChecking < 1000 * 5
|
|
935
1001
|
) {
|
|
936
1002
|
try {
|
|
@@ -949,17 +1015,7 @@ export class Pushy {
|
|
|
949
1015
|
}
|
|
950
1016
|
}
|
|
951
1017
|
this.lastChecking = now;
|
|
952
|
-
|
|
953
|
-
packageVersion: this.getEffectivePackageVersion(),
|
|
954
|
-
currentVersion,
|
|
955
|
-
buildTime,
|
|
956
|
-
cInfo,
|
|
957
|
-
supportedDiffVersion,
|
|
958
|
-
bundleHash,
|
|
959
|
-
isDev: __DEV__,
|
|
960
|
-
extra,
|
|
961
|
-
});
|
|
962
|
-
const stringifyBody = JSON.stringify(fetchBody);
|
|
1018
|
+
this.lastCheckFingerprint = fingerprint;
|
|
963
1019
|
// harmony fetch body is not string
|
|
964
1020
|
let body: any = fetchBody;
|
|
965
1021
|
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
@@ -973,7 +1029,6 @@ export class Pushy {
|
|
|
973
1029
|
},
|
|
974
1030
|
body,
|
|
975
1031
|
};
|
|
976
|
-
const previousRespJson = this.lastRespJson;
|
|
977
1032
|
try {
|
|
978
1033
|
this.report({
|
|
979
1034
|
type: 'checking',
|
|
@@ -1007,19 +1062,25 @@ export class Pushy {
|
|
|
1007
1062
|
}
|
|
1008
1063
|
|
|
1009
1064
|
this.notifyAfterCheckUpdate({ status: 'completed', result });
|
|
1065
|
+
this.markJsCheckCompleted();
|
|
1010
1066
|
return result;
|
|
1011
1067
|
} catch (e: any) {
|
|
1012
|
-
|
|
1068
|
+
// A failed check must not keep vouching for an older response: clear
|
|
1069
|
+
// the dedup slot so the next call re-asks the server.
|
|
1070
|
+
this.lastRespJson = undefined;
|
|
1071
|
+
this.lastCheckFingerprint = undefined;
|
|
1013
1072
|
const err = toUpdateError(e, 'CHECK_FAILED');
|
|
1014
1073
|
this.emitError(err, 'errorChecking', {
|
|
1015
1074
|
message: err.message || this.t('error_cannot_connect_server'),
|
|
1016
1075
|
});
|
|
1017
1076
|
this.notifyAfterCheckUpdate({ status: 'error', error: err });
|
|
1018
1077
|
this.throwIfEnabled(err);
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1078
|
+
// No silent fallback to a previous successful response: a stale result
|
|
1079
|
+
// would let a server outage look like "still updatable" and drive the
|
|
1080
|
+
// provider's auto-download off it. Return undefined so callers can
|
|
1081
|
+
// distinguish "check failed" from a real empty result and keep their
|
|
1082
|
+
// last good updateInfo.
|
|
1083
|
+
return undefined;
|
|
1023
1084
|
}
|
|
1024
1085
|
};
|
|
1025
1086
|
downloadUpdate = async (
|
|
@@ -1077,6 +1138,28 @@ export class Pushy {
|
|
|
1077
1138
|
delete sharedState.downloadingTasks[hash];
|
|
1078
1139
|
}
|
|
1079
1140
|
};
|
|
1141
|
+
/**
|
|
1142
|
+
* Orders candidate mirrors for one artifact: the HEAD-race winner first
|
|
1143
|
+
* (its post-redirect URL), then the remaining mirrors in configured order.
|
|
1144
|
+
* Empty when there is nothing to try.
|
|
1145
|
+
*/
|
|
1146
|
+
private rankDownloadUrls = async (urls?: string[]): Promise<string[]> => {
|
|
1147
|
+
if (!urls?.length) {
|
|
1148
|
+
return [];
|
|
1149
|
+
}
|
|
1150
|
+
const winner = await testUrls(urls);
|
|
1151
|
+
const ordered: string[] = [];
|
|
1152
|
+
const push = (url?: string | null) => {
|
|
1153
|
+
if (url && !ordered.includes(url)) {
|
|
1154
|
+
ordered.push(url);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
push(winner);
|
|
1158
|
+
for (const url of urls) {
|
|
1159
|
+
push(url);
|
|
1160
|
+
}
|
|
1161
|
+
return ordered;
|
|
1162
|
+
};
|
|
1080
1163
|
private performDownload = async (
|
|
1081
1164
|
updateInfo: CheckResult,
|
|
1082
1165
|
plan: DownloadPlan,
|
|
@@ -1103,7 +1186,13 @@ export class Pushy {
|
|
|
1103
1186
|
progress: computeProgress(data.received, data.total),
|
|
1104
1187
|
};
|
|
1105
1188
|
callbacks.forEach((callback) => {
|
|
1106
|
-
|
|
1189
|
+
// One subscriber throwing must not starve the others or abort the
|
|
1190
|
+
// download: business callbacks are isolated, the error is logged.
|
|
1191
|
+
try {
|
|
1192
|
+
callback(payload);
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
warn('onDownloadProgress callback threw', e);
|
|
1195
|
+
}
|
|
1107
1196
|
});
|
|
1108
1197
|
};
|
|
1109
1198
|
// RN >= 0.87 types native event listeners as `(...args: readonly Object[])`,
|
|
@@ -1164,7 +1253,12 @@ export class Pushy {
|
|
|
1164
1253
|
|
|
1165
1254
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1166
1255
|
if (attempt > 0) {
|
|
1167
|
-
|
|
1256
|
+
// Exponential backoff with jitter: a bad CDN edge otherwise gets
|
|
1257
|
+
// every client back at the same 1/2/4s marks.
|
|
1258
|
+
const backoffMs = Math.round(
|
|
1259
|
+
Math.min(1000 * 2 ** (attempt - 1), 10000) *
|
|
1260
|
+
(0.75 + Math.random() * 0.5)
|
|
1261
|
+
);
|
|
1168
1262
|
log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
|
|
1169
1263
|
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1170
1264
|
errorMessages.length = 0;
|
|
@@ -1186,28 +1280,39 @@ export class Pushy {
|
|
|
1186
1280
|
if (succeeded) {
|
|
1187
1281
|
break;
|
|
1188
1282
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1283
|
+
// The HEAD race only ranks mirrors; it is not proof that a mirror can
|
|
1284
|
+
// serve the whole artifact (CDNs answer HEAD and GET differently, and
|
|
1285
|
+
// a transfer can die mid-stream). Every mirror of the same artifact
|
|
1286
|
+
// gets a real download attempt before the strategy is abandoned —
|
|
1287
|
+
// the native cold-start path already behaves this way.
|
|
1288
|
+
const orderedUrls = await this.rankDownloadUrls(urls);
|
|
1289
|
+
for (const url of orderedUrls) {
|
|
1290
|
+
log(`downloading ${type} from ${url}`);
|
|
1291
|
+
try {
|
|
1292
|
+
await runners[type](url);
|
|
1293
|
+
succeeded = type;
|
|
1294
|
+
break;
|
|
1295
|
+
} catch (e: any) {
|
|
1296
|
+
const errorMessage = this.t(errorKeys[type], {
|
|
1297
|
+
message: e.message,
|
|
1298
|
+
});
|
|
1299
|
+
errorMessages.push(errorMessage);
|
|
1300
|
+
// Keep the i18n message for display, but preserve the native
|
|
1301
|
+
// rejection's stable code (e.g. PATCH_FAILED vs DOWNLOAD_FAILED —
|
|
1302
|
+
// telemetry classifies on it) and the original error as cause.
|
|
1303
|
+
lastError = new UpdateError(
|
|
1304
|
+
errorMessage,
|
|
1305
|
+
asUpdateErrorCode(e?.code) ?? 'DOWNLOAD_FAILED',
|
|
1306
|
+
{ cause: e }
|
|
1307
|
+
);
|
|
1308
|
+
log(errorMessage);
|
|
1309
|
+
if (!isMirrorRetryableCode(lastError.code)) {
|
|
1310
|
+
// The bytes arrived but could not be applied (patch/manifest
|
|
1311
|
+
// mismatch): every mirror serves the same artifact, so move
|
|
1312
|
+
// on to the next strategy instead of re-downloading it.
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1211
1316
|
}
|
|
1212
1317
|
}
|
|
1213
1318
|
if (succeeded) {
|
|
@@ -1278,7 +1383,19 @@ export class Pushy {
|
|
|
1278
1383
|
if (sharedState.toHash === hash) {
|
|
1279
1384
|
hashInfo.debugChannel = true;
|
|
1280
1385
|
}
|
|
1281
|
-
|
|
1386
|
+
// The version is "downloaded" only once its metadata is persisted: a
|
|
1387
|
+
// lost hash-info write would leave the next launch with a nameless
|
|
1388
|
+
// version and a switchVersion that the native side may refuse.
|
|
1389
|
+
try {
|
|
1390
|
+
await setLocalHashInfo(hash, hashInfo);
|
|
1391
|
+
} catch (e: any) {
|
|
1392
|
+
const err = toUpdateError(e, 'FILE_OPERATION_FAILED');
|
|
1393
|
+
this.emitError(err, 'errorUpdate', {
|
|
1394
|
+
message: err.message,
|
|
1395
|
+
data: { newVersion: hash },
|
|
1396
|
+
});
|
|
1397
|
+
throw err;
|
|
1398
|
+
}
|
|
1282
1399
|
sharedState.downloadedHash = hash;
|
|
1283
1400
|
return hash;
|
|
1284
1401
|
};
|
package/src/core.ts
CHANGED
|
@@ -36,8 +36,15 @@ 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
|
-
export function setLocalHashInfo(
|
|
40
|
-
|
|
39
|
+
export function setLocalHashInfo(
|
|
40
|
+
hash: string,
|
|
41
|
+
info: Record<string, any>
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
// Always a promise so callers can await native persistence (the legacy
|
|
44
|
+
// bridge returns undefined when no promise is exposed).
|
|
45
|
+
return Promise.resolve(
|
|
46
|
+
PushyModule.setLocalHashInfo(hash, JSON.stringify(info))
|
|
47
|
+
);
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
const currentVersionInfoString: string = PushyConstants.currentVersionInfo;
|
|
@@ -49,7 +56,9 @@ if (currentVersionInfoString) {
|
|
|
49
56
|
if (_currentVersionInfo.debugChannel) {
|
|
50
57
|
isDebugChannel = true;
|
|
51
58
|
delete _currentVersionInfo.debugChannel;
|
|
52
|
-
setLocalHashInfo(currentVersion, _currentVersionInfo)
|
|
59
|
+
setLocalHashInfo(currentVersion, _currentVersionInfo).catch((e) => {
|
|
60
|
+
error('failed to persist version info', e);
|
|
61
|
+
});
|
|
53
62
|
}
|
|
54
63
|
} catch {
|
|
55
64
|
error(
|
|
@@ -58,6 +67,11 @@ if (currentVersionInfoString) {
|
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
export const currentVersionInfo = _currentVersionInfo;
|
|
70
|
+
/** SHA-256 of the running hot-update bundle from its install record; '' when unknown. */
|
|
71
|
+
export const currentBundleSha256: string =
|
|
72
|
+
typeof PushyConstants.currentBundleSha256 === 'string'
|
|
73
|
+
? PushyConstants.currentBundleSha256
|
|
74
|
+
: '';
|
|
61
75
|
|
|
62
76
|
export const isFirstTime: boolean = PushyConstants.isFirstTime;
|
|
63
77
|
export const isFirstTimeDebug: boolean = isFirstTime && isDebugChannel;
|
package/src/error.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type UpdateErrorCode =
|
|
|
13
13
|
| 'APPKEY_REQUIRED'
|
|
14
14
|
| 'NO_ENDPOINTS'
|
|
15
15
|
| 'HTTP_STATUS'
|
|
16
|
+
// A 2xx check response that is not a check verdict (schema gate).
|
|
17
|
+
| 'INVALID_RESPONSE'
|
|
16
18
|
| 'CHECK_FAILED'
|
|
17
19
|
| 'DOWNLOAD_FAILED'
|
|
18
20
|
| 'SWITCH_VERSION_FAILED'
|
|
@@ -43,6 +45,7 @@ const KNOWN_CODES = new Set<string>([
|
|
|
43
45
|
'APPKEY_REQUIRED',
|
|
44
46
|
'NO_ENDPOINTS',
|
|
45
47
|
'HTTP_STATUS',
|
|
48
|
+
'INVALID_RESPONSE',
|
|
46
49
|
'CHECK_FAILED',
|
|
47
50
|
'DOWNLOAD_FAILED',
|
|
48
51
|
'SWITCH_VERSION_FAILED',
|
package/src/index.ts
CHANGED
|
@@ -10,4 +10,12 @@ export {
|
|
|
10
10
|
export { PushyModule, UpdateModule } from './core';
|
|
11
11
|
export type { UpdateErrorCode } from './error';
|
|
12
12
|
export { UpdateError } from './error';
|
|
13
|
+
export type { CrashReporterLike, UpdateMetadata } from './metadata';
|
|
14
|
+
export {
|
|
15
|
+
attachToCrashlytics,
|
|
16
|
+
attachToSentry,
|
|
17
|
+
attachUpdateMetadata,
|
|
18
|
+
getUpdateMetadata,
|
|
19
|
+
updateMetadataTags,
|
|
20
|
+
} from './metadata';
|
|
13
21
|
export { PushyProvider, UpdateProvider } from './provider';
|
package/src/locales/en.ts
CHANGED
|
@@ -61,6 +61,9 @@ export default {
|
|
|
61
61
|
error_all_promises_rejected: 'All promises were rejected',
|
|
62
62
|
error_ping_failed: 'Ping failed',
|
|
63
63
|
error_ping_timeout: 'Ping timeout',
|
|
64
|
+
error_insecure_redirect:
|
|
65
|
+
'Request was redirected from https to plaintext http',
|
|
66
|
+
error_invalid_check_response: 'Update check response is not a valid verdict',
|
|
64
67
|
error_http_status: '{{status}} {{statusText}}',
|
|
65
68
|
error_apk_pending_install:
|
|
66
69
|
'The APK has been downloaded, please complete the installation in the system installer',
|
package/src/locales/zh.ts
CHANGED
|
@@ -58,6 +58,8 @@ export default {
|
|
|
58
58
|
error_all_promises_rejected: '所有请求都被拒绝',
|
|
59
59
|
error_ping_failed: 'Ping 失败',
|
|
60
60
|
error_ping_timeout: 'Ping 超时',
|
|
61
|
+
error_insecure_redirect: '请求被从 https 重定向到明文 http',
|
|
62
|
+
error_invalid_check_response: '更新检查响应不是有效结果',
|
|
61
63
|
error_http_status: '{{status}} {{statusText}}',
|
|
62
64
|
error_apk_pending_install: '安装包已下载完成,请在系统安装界面完成安装',
|
|
63
65
|
error_storage_permission_rejected: '存储权限被拒绝,无法下载安装包',
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildTime,
|
|
3
|
+
cInfo,
|
|
4
|
+
currentBundleSha256,
|
|
5
|
+
currentVersion,
|
|
6
|
+
currentVersionInfo,
|
|
7
|
+
getBundleHash,
|
|
8
|
+
isFirstTime,
|
|
9
|
+
isRolledBack,
|
|
10
|
+
packageVersion,
|
|
11
|
+
rolledBackVersion,
|
|
12
|
+
} from './core';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Everything a crash report needs to be mapped back to the exact JS bundle
|
|
16
|
+
* that was running: attach it as tags/context in Sentry, Crashlytics, Bugly…
|
|
17
|
+
* The `currentVersion` hash (empty = the binary's embedded bundle) is the
|
|
18
|
+
* key that selects the matching source map on the update platform.
|
|
19
|
+
*/
|
|
20
|
+
export interface UpdateMetadata {
|
|
21
|
+
/** react-native-update SDK version. */
|
|
22
|
+
sdkVersion: string;
|
|
23
|
+
/** Native binary version the update decision is keyed on. */
|
|
24
|
+
packageVersion: string;
|
|
25
|
+
/** Build time of the native binary (empty in debug). */
|
|
26
|
+
buildTime: string;
|
|
27
|
+
/** Hash of the running hot-update version; '' when the embedded bundle runs. */
|
|
28
|
+
currentVersion: string;
|
|
29
|
+
/** Publisher-facing name/description/metaInfo of the running version. */
|
|
30
|
+
versionName: string;
|
|
31
|
+
versionDescription: string;
|
|
32
|
+
metaInfo: string;
|
|
33
|
+
/** SHA-256 of the embedded bundle; '' until computed / unavailable. */
|
|
34
|
+
bundleHash: string;
|
|
35
|
+
/**
|
|
36
|
+
* SHA-256 of the running hot-update bundle from its install record
|
|
37
|
+
* (matches the `bundleSha256` the CLI archives with the source map); ''
|
|
38
|
+
* for the embedded bundle or a legacy install.
|
|
39
|
+
*/
|
|
40
|
+
bundleSha256: string;
|
|
41
|
+
/** First launch of `currentVersion` (crash protection still armed). */
|
|
42
|
+
isFirstTime: boolean;
|
|
43
|
+
/** This launch is the result of a rollback; `rolledBackVersion` names the culprit. */
|
|
44
|
+
isRolledBack: boolean;
|
|
45
|
+
rolledBackVersion: string;
|
|
46
|
+
/**
|
|
47
|
+
* How the running version got activated when not by the JS flow:
|
|
48
|
+
* 'forceBoot' — the server's per-version override applied by the native
|
|
49
|
+
* cold-start check; 'crashRescue' — activated by the crash-time rescue.
|
|
50
|
+
*/
|
|
51
|
+
rescueSource: 'forceBoot' | 'crashRescue' | null;
|
|
52
|
+
/** Stable per-install client id (gray-release bucketing key). */
|
|
53
|
+
uuid: string;
|
|
54
|
+
os: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getUpdateMetadata(): UpdateMetadata {
|
|
58
|
+
const info = currentVersionInfo || {};
|
|
59
|
+
return {
|
|
60
|
+
sdkVersion: cInfo.rnu,
|
|
61
|
+
packageVersion,
|
|
62
|
+
buildTime,
|
|
63
|
+
currentVersion: currentVersion || '',
|
|
64
|
+
versionName: typeof info.name === 'string' ? info.name : '',
|
|
65
|
+
versionDescription:
|
|
66
|
+
typeof info.description === 'string' ? info.description : '',
|
|
67
|
+
metaInfo: typeof info.metaInfo === 'string' ? info.metaInfo : '',
|
|
68
|
+
bundleHash: getBundleHash(),
|
|
69
|
+
bundleSha256: currentBundleSha256 || '',
|
|
70
|
+
isFirstTime,
|
|
71
|
+
isRolledBack,
|
|
72
|
+
rolledBackVersion: rolledBackVersion || '',
|
|
73
|
+
rescueSource: info.forceBootRescue
|
|
74
|
+
? 'forceBoot'
|
|
75
|
+
: info.crashRescue
|
|
76
|
+
? 'crashRescue'
|
|
77
|
+
: null,
|
|
78
|
+
uuid: cInfo.uuid,
|
|
79
|
+
os: cInfo.os,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The duck-typed surface of a crash reporter. Sentry exposes setTag/setContext,
|
|
85
|
+
* Firebase Crashlytics setAttributes/setAttribute; any object with one of
|
|
86
|
+
* those works.
|
|
87
|
+
*/
|
|
88
|
+
export interface CrashReporterLike {
|
|
89
|
+
setTag?: (key: string, value: string) => unknown;
|
|
90
|
+
setContext?: (
|
|
91
|
+
name: string,
|
|
92
|
+
context: Record<string, unknown> | null
|
|
93
|
+
) => unknown;
|
|
94
|
+
setAttributes?: (attributes: Record<string, string>) => unknown;
|
|
95
|
+
setAttribute?: (key: string, value: string) => unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Flat string tags (`pushy.currentVersion` …) for the running update. */
|
|
99
|
+
export function updateMetadataTags(
|
|
100
|
+
metadata: UpdateMetadata = getUpdateMetadata(),
|
|
101
|
+
prefix = 'pushy.'
|
|
102
|
+
): Record<string, string> {
|
|
103
|
+
const tags: Record<string, string> = {};
|
|
104
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
105
|
+
tags[`${prefix}${key}`] =
|
|
106
|
+
value === null || value === undefined ? '' : String(value);
|
|
107
|
+
}
|
|
108
|
+
return tags;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Attach the running update's metadata to a crash reporter so every event can
|
|
113
|
+
* be mapped to the exact bundle (and source map) it came from. Call once after
|
|
114
|
+
* the reporter is initialised; safe to call again (e.g. after markSuccess).
|
|
115
|
+
* Every reporter method is optional and failures are swallowed — a reporting
|
|
116
|
+
* hiccup must never break the app.
|
|
117
|
+
*/
|
|
118
|
+
export function attachUpdateMetadata(
|
|
119
|
+
reporter: CrashReporterLike,
|
|
120
|
+
options: { prefix?: string; contextName?: string | null } = {}
|
|
121
|
+
): UpdateMetadata {
|
|
122
|
+
const metadata = getUpdateMetadata();
|
|
123
|
+
const tags = updateMetadataTags(metadata, options.prefix ?? 'pushy.');
|
|
124
|
+
const swallow = (fn: () => unknown) => {
|
|
125
|
+
try {
|
|
126
|
+
const result = fn();
|
|
127
|
+
if (result && typeof (result as Promise<unknown>).catch === 'function') {
|
|
128
|
+
(result as Promise<unknown>).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// never let a reporter failure propagate
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
if (typeof reporter.setTag === 'function') {
|
|
135
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
136
|
+
swallow(() => reporter.setTag?.(key, value));
|
|
137
|
+
}
|
|
138
|
+
} else if (typeof reporter.setAttributes === 'function') {
|
|
139
|
+
swallow(() => reporter.setAttributes?.(tags));
|
|
140
|
+
} else if (typeof reporter.setAttribute === 'function') {
|
|
141
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
142
|
+
swallow(() => reporter.setAttribute?.(key, value));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
options.contextName !== null &&
|
|
147
|
+
typeof reporter.setContext === 'function'
|
|
148
|
+
) {
|
|
149
|
+
swallow(() =>
|
|
150
|
+
reporter.setContext?.(options.contextName ?? 'pushy', { ...metadata })
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return metadata;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** `attachUpdateMetadata` for `@sentry/react-native` (tags + a `pushy` context). */
|
|
157
|
+
export function attachToSentry(sentry: CrashReporterLike): UpdateMetadata {
|
|
158
|
+
return attachUpdateMetadata(sentry, { contextName: 'pushy' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** `attachUpdateMetadata` for `@react-native-firebase/crashlytics` (custom keys). */
|
|
162
|
+
export function attachToCrashlytics(
|
|
163
|
+
crashlytics: CrashReporterLike
|
|
164
|
+
): UpdateMetadata {
|
|
165
|
+
return attachUpdateMetadata(crashlytics, { contextName: null });
|
|
166
|
+
}
|