react-native-update 10.55.0 → 10.56.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 +35 -0
- package/README.md +37 -0
- package/android/build.gradle +53 -27
- package/android/jni/Application.mk +6 -1
- package/android/jni/hpatch.c +59 -4
- 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/proguard.pro +70 -14
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
- package/cpp/patch_core/archive_limits.h +6 -0
- package/cpp/patch_core/jni_util.h +6 -2
- package/cpp/patch_core/patch_core.cpp +237 -84
- package/cpp/patch_core/patch_core.h +11 -0
- package/cpp/patch_core/patch_core_android.cpp +49 -33
- package/cpp/patch_core/update_core_android.cpp +134 -103
- package/cpp/update_flow_core/flow_json.cpp +150 -17
- package/cpp/update_flow_core/flow_json.h +13 -1
- package/cpp/update_flow_core/update_flow_core.cpp +78 -8
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
- package/harmony/hvigor-plugin.ts +168 -21
- package/harmony/pushy/src/README.md +74 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
- package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
- package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
- package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
- package/harmony/pushy/src/main/ets/Logger.ts +42 -16
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
- package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
- package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
- package/harmony/pushy/src/main/module.json5 +5 -1
- package/harmony/pushy.har +0 -0
- package/ios/PrivacyInfo.xcprivacy +79 -0
- package/ios/RCTPushy/RCTPushy.mm +406 -170
- package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
- package/package.json +14 -24
- package/react-native-update.podspec +36 -3
- package/src/client.ts +244 -86
- package/src/context.ts +40 -27
- package/src/core.ts +41 -16
- package/src/endpoint.ts +2 -1
- package/src/error.ts +49 -11
- package/src/index.ts +14 -0
- package/src/locales/en.ts +1 -23
- package/src/locales/zh.ts +1 -22
- package/src/provider.tsx +159 -111
- package/src/type.ts +0 -2
- package/src/updateFlowCore.ts +10 -2
- package/src/utils.ts +61 -4
- package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
package/src/client.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from './core';
|
|
21
21
|
import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
|
|
22
22
|
import {
|
|
23
|
-
|
|
23
|
+
readErrorCode,
|
|
24
24
|
toUpdateError,
|
|
25
25
|
UpdateError,
|
|
26
26
|
type UpdateErrorCode,
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
log,
|
|
66
66
|
noop,
|
|
67
67
|
promiseAny,
|
|
68
|
+
setDebugLogging,
|
|
68
69
|
testUrls,
|
|
69
70
|
warn,
|
|
70
71
|
} from './utils';
|
|
@@ -105,10 +106,13 @@ const cloneServerConfig = (server: UpdateServerConfig): UpdateServerConfig => ({
|
|
|
105
106
|
queryUrls: server.queryUrls ? [...server.queryUrls] : undefined,
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
// How long after a resolved reloadUpdate the process may still be alive before
|
|
110
|
+
// the switch is considered not to have restarted the app (see switchVersion).
|
|
111
|
+
const RELOAD_WATCHDOG_MS = 5000;
|
|
112
|
+
|
|
113
|
+
// Non-2xx check responses are usually HTML error pages; keep only the head
|
|
114
|
+
// for the error message (it can end up in an alert) and the logger.
|
|
115
|
+
const MAX_HTTP_ERROR_TEXT = 200;
|
|
112
116
|
|
|
113
117
|
const excludeConfiguredEndpoints = (
|
|
114
118
|
endpoints: string[],
|
|
@@ -153,6 +157,59 @@ export const sharedState: {
|
|
|
153
157
|
applyingUpdate: false,
|
|
154
158
|
};
|
|
155
159
|
|
|
160
|
+
// A reset/new switch invalidates ownership of older async completions and
|
|
161
|
+
// watchdogs before the process-wide applyingUpdate flag can be reused.
|
|
162
|
+
let applyingUpdateGeneration = 0;
|
|
163
|
+
let reloadWatchdogTimer: ReturnType<typeof setTimeout> | undefined;
|
|
164
|
+
|
|
165
|
+
const clearReloadWatchdog = () => {
|
|
166
|
+
if (reloadWatchdogTimer !== undefined) {
|
|
167
|
+
clearTimeout(reloadWatchdogTimer);
|
|
168
|
+
reloadWatchdogTimer = undefined;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const beginApplyingUpdate = (): number => {
|
|
173
|
+
clearReloadWatchdog();
|
|
174
|
+
sharedState.applyingUpdate = true;
|
|
175
|
+
return ++applyingUpdateGeneration;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const ownsApplyingUpdate = (generation: number): boolean =>
|
|
179
|
+
generation === applyingUpdateGeneration && sharedState.applyingUpdate;
|
|
180
|
+
|
|
181
|
+
const finishApplyingUpdate = (generation: number): boolean => {
|
|
182
|
+
if (!ownsApplyingUpdate(generation)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
clearReloadWatchdog();
|
|
186
|
+
sharedState.applyingUpdate = false;
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const invalidateApplyingUpdate = () => {
|
|
191
|
+
applyingUpdateGeneration++;
|
|
192
|
+
clearReloadWatchdog();
|
|
193
|
+
sharedState.applyingUpdate = false;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const armReloadWatchdog = (generation: number, onTimeout: () => void) => {
|
|
197
|
+
if (!ownsApplyingUpdate(generation)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
clearReloadWatchdog();
|
|
201
|
+
const timer = setTimeout(() => {
|
|
202
|
+
// A cancelled callback may already be queued; both identities must match.
|
|
203
|
+
if (reloadWatchdogTimer !== timer || !ownsApplyingUpdate(generation)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
reloadWatchdogTimer = undefined;
|
|
207
|
+
sharedState.applyingUpdate = false;
|
|
208
|
+
onTimeout();
|
|
209
|
+
}, RELOAD_WATCHDOG_MS);
|
|
210
|
+
reloadWatchdogTimer = timer;
|
|
211
|
+
};
|
|
212
|
+
|
|
156
213
|
// The SDK is a process-level singleton: module-level sharedState, the global
|
|
157
214
|
// i18n locale and the native update state are all per-process, so a second
|
|
158
215
|
// client would silently share (and fight over) them. Constructing one is a
|
|
@@ -226,13 +283,11 @@ export class Pushy {
|
|
|
226
283
|
options.locale ?? (this.clientType === 'Pushy' ? 'zh' : 'en')
|
|
227
284
|
);
|
|
228
285
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
);
|
|
235
|
-
}
|
|
286
|
+
// Every native platform (Harmony included) needs the appKey; without it
|
|
287
|
+
// the check URL is `/checkUpdate/` and the failure would surface as an
|
|
288
|
+
// opaque 404 instead of this explicit code.
|
|
289
|
+
if (Platform.OS !== 'web' && !options.appKey) {
|
|
290
|
+
throw new UpdateError(i18n.t('error_appkey_required'), 'APPKEY_REQUIRED');
|
|
236
291
|
}
|
|
237
292
|
|
|
238
293
|
this.setOptions(options);
|
|
@@ -276,8 +331,15 @@ export class Pushy {
|
|
|
276
331
|
if (key === 'logger') {
|
|
277
332
|
this.loggerPromise.resolve();
|
|
278
333
|
}
|
|
334
|
+
if (key === 'locale') {
|
|
335
|
+
// The constructor applies the initial locale; a runtime switch (or
|
|
336
|
+
// the fast-refresh re-creation path, which only goes through here)
|
|
337
|
+
// must apply just the same.
|
|
338
|
+
i18n.setLocale(value as 'zh' | 'en');
|
|
339
|
+
}
|
|
279
340
|
}
|
|
280
341
|
}
|
|
342
|
+
setDebugLogging(!!this.options.debug);
|
|
281
343
|
this.optionsVersion++;
|
|
282
344
|
for (const listener of this.optionsListeners) {
|
|
283
345
|
try {
|
|
@@ -424,8 +486,16 @@ export class Pushy {
|
|
|
424
486
|
) {
|
|
425
487
|
return;
|
|
426
488
|
}
|
|
427
|
-
const configJson =
|
|
428
|
-
|
|
489
|
+
const configJson = this.getNativeConfigJson();
|
|
490
|
+
if (!configJson) {
|
|
491
|
+
// No usable config (empty appKey / no endpoints) is almost always a
|
|
492
|
+
// transient state while options are being assembled. Persisting a
|
|
493
|
+
// disabled config over the last good one would switch off the
|
|
494
|
+
// cold-start rescue for every later launch, so keep what native has.
|
|
495
|
+
// (Web and older natives never reach this point — no method.)
|
|
496
|
+
log('native check config unusable, keeping the last synced config');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
429
499
|
// Always record the latest desired value, even when it matches the last
|
|
430
500
|
// completed write. Example: A synced -> B in flight -> options revert to
|
|
431
501
|
// A. Comparing only with synced(A) would drop the revert and leave native
|
|
@@ -661,10 +731,14 @@ export class Pushy {
|
|
|
661
731
|
// report fires in the constructor before the user configures one), but
|
|
662
732
|
// give up after a bound instead of retaining the closure forever when
|
|
663
733
|
// no logger is ever provided.
|
|
734
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
664
735
|
await Promise.race([
|
|
665
736
|
this.loggerPromise.promise,
|
|
666
|
-
new Promise((resolve) =>
|
|
737
|
+
new Promise((resolve) => {
|
|
738
|
+
timer = setTimeout(resolve, 10 * 1000);
|
|
739
|
+
}),
|
|
667
740
|
]);
|
|
741
|
+
clearTimeout(timer);
|
|
668
742
|
}
|
|
669
743
|
const { logger = noop, appKey } = this.options;
|
|
670
744
|
const overridePackageVersion = this.options.overridePackageVersion;
|
|
@@ -850,23 +924,39 @@ export class Pushy {
|
|
|
850
924
|
return [];
|
|
851
925
|
}
|
|
852
926
|
try {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
927
|
+
// Race the parsed lists, not the raw responses: fetch resolves for
|
|
928
|
+
// 404/403/5xx and anti-bot HTML too, and this path only runs once the
|
|
929
|
+
// main endpoints are already failing — a mirror that answers first
|
|
930
|
+
// with an error page must not beat the healthy one.
|
|
931
|
+
const remoteEndpoints = await promiseAny(
|
|
932
|
+
server.queryUrls.map(async (queryUrl) => {
|
|
933
|
+
const resp = await fetchWithTimeout(
|
|
934
|
+
queryUrl,
|
|
935
|
+
{},
|
|
936
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
937
|
+
);
|
|
938
|
+
if (!resp.ok) {
|
|
939
|
+
throw Error(`${queryUrl}: ${resp.status}`);
|
|
940
|
+
}
|
|
941
|
+
const list: unknown = await resp.json();
|
|
942
|
+
if (!Array.isArray(list)) {
|
|
943
|
+
throw Error(`${queryUrl}: not an endpoint list`);
|
|
944
|
+
}
|
|
945
|
+
return list;
|
|
946
|
+
})
|
|
857
947
|
);
|
|
858
|
-
const remoteEndpoints = await resp.json();
|
|
859
948
|
log('fetch endpoints:', remoteEndpoints);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
)
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
)
|
|
869
|
-
|
|
949
|
+
return excludeConfiguredEndpoints(
|
|
950
|
+
dedupeEndpoints(
|
|
951
|
+
remoteEndpoints.filter(
|
|
952
|
+
// The list is fetched from a public mirror; it may add
|
|
953
|
+
// endpoints but never move the check outside TLS.
|
|
954
|
+
(endpoint): endpoint is string =>
|
|
955
|
+
typeof endpoint === 'string' && /^https:\/\//i.test(endpoint)
|
|
956
|
+
)
|
|
957
|
+
),
|
|
958
|
+
this.getConfiguredCheckEndpoints()
|
|
959
|
+
);
|
|
870
960
|
} catch (e) {
|
|
871
961
|
log('failed to fetch endpoints from: ', server.queryUrls, e);
|
|
872
962
|
}
|
|
@@ -884,7 +974,7 @@ export class Pushy {
|
|
|
884
974
|
);
|
|
885
975
|
|
|
886
976
|
if (!resp.ok) {
|
|
887
|
-
const respText = await resp.text();
|
|
977
|
+
const respText = (await resp.text()).slice(0, MAX_HTTP_ERROR_TEXT);
|
|
888
978
|
throw new UpdateError(
|
|
889
979
|
this.t('error_http_status', {
|
|
890
980
|
status: resp.status,
|
|
@@ -977,12 +1067,14 @@ export class Pushy {
|
|
|
977
1067
|
if (ageSeconds < 0 || ageSeconds > 120) {
|
|
978
1068
|
return undefined;
|
|
979
1069
|
}
|
|
980
|
-
const result = JSON.parse(entry.body);
|
|
981
|
-
|
|
1070
|
+
const result: unknown = JSON.parse(entry.body);
|
|
1071
|
+
// Same schema gate as the network path: a native old enough to have
|
|
1072
|
+
// cached a 200 `{"error": ...}` must not turn it into "no update".
|
|
1073
|
+
if (!isValidCheckResult(result)) {
|
|
982
1074
|
return undefined;
|
|
983
1075
|
}
|
|
984
1076
|
log('reusing native check response cache');
|
|
985
|
-
return result
|
|
1077
|
+
return result;
|
|
986
1078
|
} catch {
|
|
987
1079
|
return undefined;
|
|
988
1080
|
}
|
|
@@ -1017,42 +1109,69 @@ export class Pushy {
|
|
|
1017
1109
|
this.report({ type: 'crashRescue' });
|
|
1018
1110
|
}
|
|
1019
1111
|
};
|
|
1020
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* Reload into a downloaded version. Resolves true once the native reload
|
|
1114
|
+
* was requested, false when the call was ignored (not the downloaded hash,
|
|
1115
|
+
* a switch already in progress, beforeReload declined, dev without debug).
|
|
1116
|
+
*/
|
|
1117
|
+
switchVersion = async (hash: string): Promise<boolean> => {
|
|
1021
1118
|
if (!this.assertDebug('switchVersion()')) {
|
|
1022
|
-
return;
|
|
1119
|
+
return false;
|
|
1023
1120
|
}
|
|
1024
|
-
if (assertHash(hash)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const err = toUpdateError(e, 'USER_HOOK_ERROR');
|
|
1038
|
-
this.emitError(err, 'errorSwitchVersion', {
|
|
1039
|
-
data: { newVersion: hash },
|
|
1040
|
-
});
|
|
1041
|
-
throw err;
|
|
1042
|
-
}
|
|
1043
|
-
try {
|
|
1044
|
-
return await PushyModule.reloadUpdate({ hash });
|
|
1045
|
-
} catch (e) {
|
|
1046
|
-
// reloadUpdate can reject (e.g. bundle missing); reset the flag so a
|
|
1047
|
-
// later retry is not permanently blocked by a stuck applyingUpdate.
|
|
1048
|
-
sharedState.applyingUpdate = false;
|
|
1049
|
-
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
1050
|
-
this.emitError(err, 'errorSwitchVersion', {
|
|
1051
|
-
data: { newVersion: hash },
|
|
1052
|
-
});
|
|
1053
|
-
throw err;
|
|
1121
|
+
if (!assertHash(hash)) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
if (sharedState.applyingUpdate) {
|
|
1125
|
+
log(`switchVersion: ${hash} ignored, a switch is already in progress`);
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
log(`switchVersion: ${hash}`);
|
|
1129
|
+
const applyingGeneration = beginApplyingUpdate();
|
|
1130
|
+
try {
|
|
1131
|
+
if (!(await this.runBeforeReload({ type: 'switchVersion', hash }))) {
|
|
1132
|
+
finishApplyingUpdate(applyingGeneration);
|
|
1133
|
+
return false;
|
|
1054
1134
|
}
|
|
1135
|
+
} catch (e) {
|
|
1136
|
+
finishApplyingUpdate(applyingGeneration);
|
|
1137
|
+
// A throw from the user's beforeReload hook is business-code failure,
|
|
1138
|
+
// not an update-pipeline one: give it a distinct code so telemetry
|
|
1139
|
+
// excludes it from the server-side patch-health stats.
|
|
1140
|
+
const err = toUpdateError(e, 'USER_HOOK_ERROR');
|
|
1141
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
1142
|
+
data: { newVersion: hash },
|
|
1143
|
+
});
|
|
1144
|
+
throw err;
|
|
1145
|
+
}
|
|
1146
|
+
// resetToPackagedBundle may have completed while beforeReload awaited.
|
|
1147
|
+
if (!ownsApplyingUpdate(applyingGeneration)) {
|
|
1148
|
+
return false;
|
|
1055
1149
|
}
|
|
1150
|
+
try {
|
|
1151
|
+
await PushyModule.reloadUpdate({ hash });
|
|
1152
|
+
} catch (e) {
|
|
1153
|
+
// Do not let a superseded rejection clear a newer switch.
|
|
1154
|
+
finishApplyingUpdate(applyingGeneration);
|
|
1155
|
+
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
1156
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
1157
|
+
data: { newVersion: hash },
|
|
1158
|
+
});
|
|
1159
|
+
throw err;
|
|
1160
|
+
}
|
|
1161
|
+
// A resolved reloadUpdate normally tears this JS context down right away.
|
|
1162
|
+
// iOS has been seen resolving without restarting (89c638e); a stuck
|
|
1163
|
+
// applyingUpdate would then silently swallow every later switchVersion,
|
|
1164
|
+
// so release it once the process has outlived the reload and tell the
|
|
1165
|
+
// logger (RESTART_FAILED: restart mechanics, not patch health).
|
|
1166
|
+
armReloadWatchdog(applyingGeneration, () => {
|
|
1167
|
+
this.report({
|
|
1168
|
+
type: 'errorSwitchVersion',
|
|
1169
|
+
code: 'RESTART_FAILED',
|
|
1170
|
+
message: 'reloadUpdate resolved but the app did not restart',
|
|
1171
|
+
data: { newVersion: hash },
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
return true;
|
|
1056
1175
|
};
|
|
1057
1176
|
|
|
1058
1177
|
switchVersionLater = async (hash: string) => {
|
|
@@ -1153,6 +1272,7 @@ export class Pushy {
|
|
|
1153
1272
|
},
|
|
1154
1273
|
body,
|
|
1155
1274
|
};
|
|
1275
|
+
let respJsonPromise: Promise<CheckResult> | undefined;
|
|
1156
1276
|
try {
|
|
1157
1277
|
this.report({
|
|
1158
1278
|
type: 'checking',
|
|
@@ -1162,7 +1282,7 @@ export class Pushy {
|
|
|
1162
1282
|
// (§10.3); reuse it instead of re-checking. The read happens INSIDE
|
|
1163
1283
|
// the promise so no await lands between the dedup window above and the
|
|
1164
1284
|
// lastRespJson assignment below (the JS2-1 double-send lesson).
|
|
1165
|
-
|
|
1285
|
+
respJsonPromise = (async (): Promise<CheckResult> => {
|
|
1166
1286
|
// While bundleHash prefetch is still pending, the JS request omits
|
|
1167
1287
|
// that key whereas the native request always includes its synchronously
|
|
1168
1288
|
// computed value. That narrow first-launch window intentionally misses
|
|
@@ -1190,9 +1310,12 @@ export class Pushy {
|
|
|
1190
1310
|
return result;
|
|
1191
1311
|
} catch (e: any) {
|
|
1192
1312
|
// A failed check must not keep vouching for an older response: clear
|
|
1193
|
-
// the dedup slot so the next call re-asks the server
|
|
1194
|
-
this
|
|
1195
|
-
this.
|
|
1313
|
+
// the dedup slot so the next call re-asks the server — unless a newer
|
|
1314
|
+
// check already owns the slot, which this failure says nothing about.
|
|
1315
|
+
if (this.lastRespJson === respJsonPromise) {
|
|
1316
|
+
this.lastRespJson = undefined;
|
|
1317
|
+
this.lastCheckFingerprint = undefined;
|
|
1318
|
+
}
|
|
1196
1319
|
const err = toUpdateError(e, 'CHECK_FAILED');
|
|
1197
1320
|
this.emitError(err, 'errorChecking', {
|
|
1198
1321
|
message: err.message || this.t('error_cannot_connect_server'),
|
|
@@ -1291,6 +1414,14 @@ export class Pushy {
|
|
|
1291
1414
|
) => {
|
|
1292
1415
|
const { name, description = '', metaInfo } = updateInfo;
|
|
1293
1416
|
const { hash, attempts, devNoop } = plan;
|
|
1417
|
+
if (devNoop) {
|
|
1418
|
+
// Dev without a full package URL: nothing can be fetched, let alone
|
|
1419
|
+
// installed. Say so and deliver nothing — faking a success here used to
|
|
1420
|
+
// hand the caller a hash whose switchVersion the native side (rightly)
|
|
1421
|
+
// rejects with SWITCH_VERSION_FAILED.
|
|
1422
|
+
log(this.t('dev_incremental_update_disabled'));
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1294
1425
|
const patchStartTime = Date.now();
|
|
1295
1426
|
// One native listener per hash dispatching to a callback set, so
|
|
1296
1427
|
// concurrent callers deduped onto this task can each observe progress
|
|
@@ -1342,8 +1473,17 @@ export class Pushy {
|
|
|
1342
1473
|
}
|
|
1343
1474
|
const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
|
|
1344
1475
|
let succeeded = '';
|
|
1345
|
-
let lastError:
|
|
1476
|
+
let lastError: UpdateError | undefined;
|
|
1346
1477
|
const errorMessages: string[] = [];
|
|
1478
|
+
// A strategy whose bytes arrived but could not be applied fails the same
|
|
1479
|
+
// way on every retry (same artifact, same base): remember it — with its
|
|
1480
|
+
// message, so the final report still names it — and skip it on later
|
|
1481
|
+
// attempts instead of downloading and patching it again.
|
|
1482
|
+
const exhaustedStrategies = new Map<DownloadStrategyType, string>();
|
|
1483
|
+
// Whether any strategy, in any attempt, failed at the patch stage. The
|
|
1484
|
+
// downloadFallback report carries that as its code even when a later,
|
|
1485
|
+
// unrelated transport failure happened to be the last error seen.
|
|
1486
|
+
let patchFailed = false;
|
|
1347
1487
|
|
|
1348
1488
|
// The ordered attempts come from decideDownload (the pure decision layer);
|
|
1349
1489
|
// this side only executes them: probe candidate URLs, run the matching
|
|
@@ -1386,6 +1526,7 @@ export class Pushy {
|
|
|
1386
1526
|
log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
|
|
1387
1527
|
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1388
1528
|
errorMessages.length = 0;
|
|
1529
|
+
errorMessages.push(...exhaustedStrategies.values());
|
|
1389
1530
|
lastError = undefined;
|
|
1390
1531
|
succeeded = '';
|
|
1391
1532
|
}
|
|
@@ -1396,14 +1537,13 @@ export class Pushy {
|
|
|
1396
1537
|
attempt,
|
|
1397
1538
|
},
|
|
1398
1539
|
});
|
|
1399
|
-
if (devNoop) {
|
|
1400
|
-
log(this.t('dev_incremental_update_disabled'));
|
|
1401
|
-
succeeded = 'full';
|
|
1402
|
-
}
|
|
1403
1540
|
for (const { type, urls } of attempts) {
|
|
1404
1541
|
if (succeeded) {
|
|
1405
1542
|
break;
|
|
1406
1543
|
}
|
|
1544
|
+
if (exhaustedStrategies.has(type)) {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1407
1547
|
// The HEAD race only ranks mirrors; it is not proof that a mirror can
|
|
1408
1548
|
// serve the whole artifact (CDNs answer HEAD and GET differently, and
|
|
1409
1549
|
// a transfer can die mid-stream). Every mirror of the same artifact
|
|
@@ -1416,30 +1556,43 @@ export class Pushy {
|
|
|
1416
1556
|
await runners[type](url);
|
|
1417
1557
|
succeeded = type;
|
|
1418
1558
|
break;
|
|
1419
|
-
} catch (e:
|
|
1559
|
+
} catch (e: unknown) {
|
|
1560
|
+
// The native rejection's stable code (e.g. PATCH_FAILED vs
|
|
1561
|
+
// DOWNLOAD_FAILED — telemetry classifies on it) may sit on the
|
|
1562
|
+
// `code` property or, from bridges that cannot set it, as a
|
|
1563
|
+
// `[CODE] ` message prefix. Either way it survives into the
|
|
1564
|
+
// i18n-wrapped error together with the original as cause.
|
|
1565
|
+
const { code, message } = readErrorCode(e);
|
|
1420
1566
|
const errorMessage = this.t(errorKeys[type], {
|
|
1421
|
-
message: e
|
|
1567
|
+
message: message || String(e ?? ''),
|
|
1422
1568
|
});
|
|
1423
1569
|
errorMessages.push(errorMessage);
|
|
1424
|
-
// Keep the i18n message for display, but preserve the native
|
|
1425
|
-
// rejection's stable code (e.g. PATCH_FAILED vs DOWNLOAD_FAILED —
|
|
1426
|
-
// telemetry classifies on it) and the original error as cause.
|
|
1427
1570
|
lastError = new UpdateError(
|
|
1428
1571
|
errorMessage,
|
|
1429
|
-
|
|
1430
|
-
{
|
|
1572
|
+
code ?? 'DOWNLOAD_FAILED',
|
|
1573
|
+
{
|
|
1574
|
+
cause: e,
|
|
1575
|
+
}
|
|
1431
1576
|
);
|
|
1432
1577
|
log(errorMessage);
|
|
1578
|
+
if (lastError.code === 'PATCH_FAILED') {
|
|
1579
|
+
patchFailed = true;
|
|
1580
|
+
}
|
|
1433
1581
|
if (!isMirrorRetryableCode(lastError.code)) {
|
|
1434
1582
|
// The bytes arrived but could not be applied (patch/manifest
|
|
1435
1583
|
// mismatch): every mirror serves the same artifact, so move
|
|
1436
1584
|
// on to the next strategy instead of re-downloading it.
|
|
1585
|
+
exhaustedStrategies.set(type, errorMessage);
|
|
1437
1586
|
break;
|
|
1438
1587
|
}
|
|
1439
1588
|
}
|
|
1440
1589
|
}
|
|
1441
1590
|
}
|
|
1442
|
-
if (
|
|
1591
|
+
if (
|
|
1592
|
+
succeeded ||
|
|
1593
|
+
attempts.every(({ type }) => exhaustedStrategies.has(type))
|
|
1594
|
+
) {
|
|
1595
|
+
// Nothing left that a retry could change.
|
|
1443
1596
|
break;
|
|
1444
1597
|
}
|
|
1445
1598
|
}
|
|
@@ -1455,9 +1608,10 @@ export class Pushy {
|
|
|
1455
1608
|
// platform that incremental delivery is failing for this binary.
|
|
1456
1609
|
this.report({
|
|
1457
1610
|
type: 'downloadFallback',
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1460
|
-
|
|
1611
|
+
// The patch-health signal wins over whatever transport failure
|
|
1612
|
+
// happened to come last (diff PATCH_FAILED, then pdiff network
|
|
1613
|
+
// error, then full succeeded is still a patch_fail server-side).
|
|
1614
|
+
code: patchFailed ? 'PATCH_FAILED' : lastError?.code,
|
|
1461
1615
|
data: {
|
|
1462
1616
|
newVersion: hash,
|
|
1463
1617
|
succeeded,
|
|
@@ -1643,6 +1797,10 @@ export class Pushy {
|
|
|
1643
1797
|
sharedState.downloadedHash = undefined;
|
|
1644
1798
|
sharedState.toHash = undefined;
|
|
1645
1799
|
sharedState.marked = false;
|
|
1800
|
+
// Invalidate the private owner too: old promises/timers must not touch
|
|
1801
|
+
// a switch that starts after this reset.
|
|
1802
|
+
invalidateApplyingUpdate();
|
|
1803
|
+
sharedState.apkStatus = null;
|
|
1646
1804
|
this.report({ type: 'reset' });
|
|
1647
1805
|
if (options?.restart) {
|
|
1648
1806
|
try {
|
package/src/context.ts
CHANGED
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo } from 'react';
|
|
2
2
|
import type { Cresc, Pushy } from './client';
|
|
3
3
|
import i18n from './i18n';
|
|
4
|
-
import type { CheckResult, ProgressData } from './type';
|
|
4
|
+
import type { CheckResult, ProgressData, UpdateTestPayload } from './type';
|
|
5
5
|
|
|
6
6
|
const noop = () => {};
|
|
7
7
|
const asyncNoop = () => Promise.resolve(undefined);
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
currentVersionInfo: {},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const UpdateContext = createContext<{
|
|
27
|
-
checkUpdate: () => Promise<undefined | CheckResult>;
|
|
28
|
-
switchVersion: () => Promise<void>;
|
|
29
|
-
switchVersionLater: () => Promise<void>;
|
|
30
|
-
markSuccess: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* What UpdateProvider puts into UpdateContext (and useUpdate() returns, plus
|
|
11
|
+
* `progress`). The signatures mirror the provider's callbacks: the `info`
|
|
12
|
+
* parameters default to the last check result, `checkUpdate` accepts request
|
|
13
|
+
* extras, and the mutations resolve once the client is done.
|
|
14
|
+
*/
|
|
15
|
+
export interface UpdateContextValue {
|
|
16
|
+
checkUpdate: (params?: {
|
|
17
|
+
extra?: Partial<{ toHash: string }>;
|
|
18
|
+
}) => Promise<undefined | CheckResult>;
|
|
19
|
+
/** Resolves true once the native reload was requested; false when skipped. */
|
|
20
|
+
switchVersion: (info?: CheckResult) => Promise<boolean | undefined>;
|
|
21
|
+
switchVersionLater: (info?: CheckResult) => Promise<void>;
|
|
22
|
+
markSuccess: () => Promise<void>;
|
|
31
23
|
dismissError: () => void;
|
|
32
|
-
downloadUpdate: () => Promise<boolean | undefined>;
|
|
24
|
+
downloadUpdate: (info?: CheckResult) => Promise<boolean | undefined>;
|
|
33
25
|
downloadAndInstallApk: (url: string) => Promise<void>;
|
|
34
26
|
// @deprecated use currentVersionInfo instead
|
|
35
27
|
getCurrentVersionInfo: () => Promise<{
|
|
@@ -42,7 +34,7 @@ export const UpdateContext = createContext<{
|
|
|
42
34
|
description?: string;
|
|
43
35
|
metaInfo?: string;
|
|
44
36
|
} | null;
|
|
45
|
-
parseTestQrCode: (code: string) => boolean;
|
|
37
|
+
parseTestQrCode: (code: string | UpdateTestPayload) => boolean;
|
|
46
38
|
restartApp: () => Promise<void>;
|
|
47
39
|
resetToPackagedBundle: (options?: {
|
|
48
40
|
restart?: boolean;
|
|
@@ -52,7 +44,26 @@ export const UpdateContext = createContext<{
|
|
|
52
44
|
client?: Pushy | Cresc;
|
|
53
45
|
updateInfo?: CheckResult;
|
|
54
46
|
lastError?: Error;
|
|
55
|
-
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const defaultContext: UpdateContextValue = {
|
|
50
|
+
checkUpdate: asyncNoop,
|
|
51
|
+
switchVersion: asyncNoop,
|
|
52
|
+
switchVersionLater: asyncNoop,
|
|
53
|
+
markSuccess: asyncNoop,
|
|
54
|
+
dismissError: noop,
|
|
55
|
+
downloadUpdate: asyncNoop,
|
|
56
|
+
downloadAndInstallApk: asyncNoop,
|
|
57
|
+
restartApp: asyncNoop,
|
|
58
|
+
resetToPackagedBundle: asyncNoop,
|
|
59
|
+
getCurrentVersionInfo: () => Promise.resolve({}),
|
|
60
|
+
parseTestQrCode: () => false,
|
|
61
|
+
currentHash: '',
|
|
62
|
+
packageVersion: '',
|
|
63
|
+
currentVersionInfo: {},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const UpdateContext = createContext<UpdateContextValue>(defaultContext);
|
|
56
67
|
|
|
57
68
|
// Download progress ticks at high frequency, so it lives in its own context;
|
|
58
69
|
// otherwise every tick would re-render all useUpdate() consumers even when
|
|
@@ -68,7 +79,9 @@ export const ProgressContext = createContext<ProgressData | undefined>(
|
|
|
68
79
|
*/
|
|
69
80
|
export const useUpdateProgress = () => useContext(ProgressContext);
|
|
70
81
|
|
|
71
|
-
export const useUpdate = ()
|
|
82
|
+
export const useUpdate = (): UpdateContextValue & {
|
|
83
|
+
progress?: ProgressData;
|
|
84
|
+
} => {
|
|
72
85
|
const context = useContext(UpdateContext);
|
|
73
86
|
const progress = useContext(ProgressContext);
|
|
74
87
|
|