react-native-update 10.59.1 → 10.60.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 +7 -7
- package/README.md +7 -6
- package/android/build.gradle +25 -33
- package/android/jni/Android.mk +16 -24
- package/android/lib/arm64-v8a/librnpushy.so +0 -0
- package/android/lib/armeabi-v7a/librnpushy.so +0 -0
- package/android/lib/x86/librnpushy.so +0 -0
- package/android/lib/x86_64/librnpushy.so +0 -0
- package/android/proguard.pro +9 -7
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +11 -12
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
- package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
- package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
- package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
- package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
- package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
- package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
- package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
- package/cpp/android_jni/archive_limits_jni.cpp +83 -0
- package/cpp/android_jni/install_record_jni.cpp +89 -0
- package/cpp/android_jni/jni_log.h +31 -0
- package/cpp/android_jni/jni_natives.h +40 -0
- package/cpp/android_jni/jni_registration.cpp +133 -0
- package/cpp/android_jni/jni_util.h +160 -0
- package/cpp/android_jni/update_core_android.cpp +450 -0
- package/cpp/android_jni/update_flow_jni.cpp +133 -0
- package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
- package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
- package/cpp/patch_core/archive_limits.cpp +194 -0
- package/cpp/patch_core/archive_limits.h +99 -10
- package/cpp/patch_core/archive_patch_core.cpp +96 -12
- package/cpp/patch_core/archive_patch_core.h +49 -0
- package/cpp/patch_core/digest.h +5 -4
- package/cpp/patch_core/error_codes.h +5 -3
- package/cpp/patch_core/install_record.cpp +430 -0
- package/cpp/patch_core/install_record.h +163 -9
- package/cpp/patch_core/patch_core.cpp +62 -16
- package/cpp/patch_core/patch_core.h +27 -7
- package/cpp/patch_core/patch_manifest.cpp +201 -0
- package/cpp/patch_core/patch_manifest.h +74 -0
- package/cpp/patch_core/state_ops.h +3 -3
- package/cpp/update_flow_core/flow_json.cpp +82 -8
- package/cpp/update_flow_core/flow_json.h +21 -0
- package/cpp/update_flow_core/native_config.cpp +456 -0
- package/cpp/update_flow_core/native_config.h +84 -0
- package/cpp/update_flow_core/native_round.cpp +326 -0
- package/cpp/update_flow_core/native_round.h +90 -0
- package/cpp/update_flow_core/update_flow_core.h +2 -2
- package/harmony/hvigor-plugin.ts +11 -19
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +19 -26
- package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
- package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
- package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
- package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
- package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
- package/harmony/pushy/src/main/ets/Logger.ts +20 -34
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
- package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
- package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
- package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
- package/harmony/pushy/src/main/ets/Texts.ts +0 -4
- package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
- package/ios/RCTPushy/RCTPushy.h +12 -0
- package/ios/RCTPushy/RCTPushy.mm +87 -2628
- package/ios/RCTPushy/RCTPushyCommon.h +34 -0
- package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
- package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
- package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
- package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
- package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
- package/ios/RCTPushy/RCTPushyInternal.h +144 -0
- package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
- package/ios/RCTPushy/RCTPushyState.mm +314 -0
- package/package.json +25 -23
- package/react-native-update.podspec +74 -83
- package/src/client.ts +361 -451
- package/src/context.ts +1 -1
- package/src/core.ts +35 -8
- package/src/endpoint.ts +0 -2
- package/src/error.ts +13 -32
- package/src/i18n.ts +18 -83
- package/src/locales/en.ts +0 -4
- package/src/locales/zh.ts +0 -4
- package/src/provider.tsx +60 -53
- package/src/telemetry.ts +0 -13
- package/src/type.ts +0 -15
- package/src/updateFlowCore.ts +116 -10
- package/src/utils.ts +49 -42
- package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
- package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
- package/cpp/patch_core/jni_natives.h +0 -27
- package/cpp/patch_core/jni_registration.cpp +0 -103
- package/cpp/patch_core/jni_util.h +0 -60
- package/cpp/patch_core/patch_core_android.cpp +0 -97
- package/cpp/patch_core/update_core_android.cpp +0 -485
- package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
package/src/client.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DeviceEventEmitter,
|
|
3
|
-
type EmitterSubscription,
|
|
4
|
-
Platform,
|
|
5
|
-
} from 'react-native';
|
|
1
|
+
import { DeviceEventEmitter, Platform } from 'react-native';
|
|
6
2
|
import {
|
|
7
3
|
buildTime,
|
|
8
4
|
cInfo,
|
|
@@ -11,6 +7,7 @@ import {
|
|
|
11
7
|
getBundleHash,
|
|
12
8
|
isFirstTime,
|
|
13
9
|
isRolledBack,
|
|
10
|
+
nativeMethod,
|
|
14
11
|
PushyModule,
|
|
15
12
|
packageVersion,
|
|
16
13
|
pushyNativeEventEmitter,
|
|
@@ -18,7 +15,7 @@ import {
|
|
|
18
15
|
setLocalHashInfo,
|
|
19
16
|
supportedDiffVersion,
|
|
20
17
|
} from './core';
|
|
21
|
-
import {
|
|
18
|
+
import { executeEndpointFallback } from './endpoint';
|
|
22
19
|
import {
|
|
23
20
|
readErrorCode,
|
|
24
21
|
toUpdateError,
|
|
@@ -53,8 +50,11 @@ import {
|
|
|
53
50
|
type DownloadPlan,
|
|
54
51
|
type DownloadStrategyType,
|
|
55
52
|
decideDownload,
|
|
53
|
+
dedupeEndpoints,
|
|
56
54
|
isMirrorRetryableCode,
|
|
57
55
|
isValidCheckResult,
|
|
56
|
+
jsonDeepEqual,
|
|
57
|
+
nativeConfigsEquivalent,
|
|
58
58
|
} from './updateFlowCore';
|
|
59
59
|
import {
|
|
60
60
|
assertWeb,
|
|
@@ -70,6 +70,14 @@ import {
|
|
|
70
70
|
warn,
|
|
71
71
|
} from './utils';
|
|
72
72
|
|
|
73
|
+
/** One report() event: the logger payload and the source of server telemetry. */
|
|
74
|
+
interface ReportEvent {
|
|
75
|
+
type: EventType;
|
|
76
|
+
message?: string;
|
|
77
|
+
code?: UpdateErrorCode;
|
|
78
|
+
data?: Record<string, string | number>;
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
/**
|
|
74
82
|
* Receives every error the client reports, alongside the report event type.
|
|
75
83
|
* The UpdateProvider subscribes to surface errors as lastError/Alert; user
|
|
@@ -114,14 +122,6 @@ const RELOAD_WATCHDOG_MS = 5000;
|
|
|
114
122
|
// for the error message (it can end up in an alert) and the logger.
|
|
115
123
|
const MAX_HTTP_ERROR_TEXT = 200;
|
|
116
124
|
|
|
117
|
-
const excludeConfiguredEndpoints = (
|
|
118
|
-
endpoints: string[],
|
|
119
|
-
configuredEndpoints: string[]
|
|
120
|
-
) => {
|
|
121
|
-
const configured = new Set(configuredEndpoints);
|
|
122
|
-
return endpoints.filter((endpoint) => !configured.has(endpoint));
|
|
123
|
-
};
|
|
124
|
-
|
|
125
125
|
assertWeb();
|
|
126
126
|
|
|
127
127
|
const createDefaultClientOptions = (): ClientOptions => ({
|
|
@@ -136,7 +136,6 @@ const createDefaultClientOptions = (): ClientOptions => ({
|
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
export const sharedState: {
|
|
139
|
-
progressHandlers: Record<string, EmitterSubscription>;
|
|
140
139
|
downloadingTasks: Record<string, Promise<string | undefined>>;
|
|
141
140
|
// Progress callbacks per hash: concurrent downloadUpdate callers of the
|
|
142
141
|
// same hash each register theirs here instead of the second one being
|
|
@@ -148,7 +147,6 @@ export const sharedState: {
|
|
|
148
147
|
marked: boolean;
|
|
149
148
|
applyingUpdate: boolean;
|
|
150
149
|
} = {
|
|
151
|
-
progressHandlers: {},
|
|
152
150
|
downloadingTasks: {},
|
|
153
151
|
progressCallbacks: {},
|
|
154
152
|
downloadedHash: undefined,
|
|
@@ -157,6 +155,19 @@ export const sharedState: {
|
|
|
157
155
|
applyingUpdate: false,
|
|
158
156
|
};
|
|
159
157
|
|
|
158
|
+
// The native cold-start check writes its response cache at most once per
|
|
159
|
+
// launch (and skips it once JS has checked), so once this process has seen it
|
|
160
|
+
// absent or expired, later reads are near-certain misses: skip the bridge
|
|
161
|
+
// call from then on. The rare miss costs one network check. A fresh entry
|
|
162
|
+
// that only mismatches the request or config does not set this: a later check
|
|
163
|
+
// with the original options (e.g. after `extra` reverts) can still reuse it.
|
|
164
|
+
let nativeCheckCacheExhausted = false;
|
|
165
|
+
const NATIVE_CHECK_CACHE_MAX_AGE_S = 120;
|
|
166
|
+
|
|
167
|
+
// In-flight native markSuccess shared by concurrent callers, so the native
|
|
168
|
+
// call and its telemetry happen once. Cleared once settled.
|
|
169
|
+
let markingPromise: Promise<void> | undefined;
|
|
170
|
+
|
|
160
171
|
// A reset/new switch invalidates ownership of older async completions and
|
|
161
172
|
// watchdogs before the process-wide applyingUpdate flag can be reused.
|
|
162
173
|
let applyingUpdateGeneration = 0;
|
|
@@ -242,6 +253,8 @@ export class Pushy {
|
|
|
242
253
|
private syncedNativeConfigJson?: string;
|
|
243
254
|
private pendingNativeConfigJson?: string;
|
|
244
255
|
private nativeConfigSyncInFlight = false;
|
|
256
|
+
// Config last acknowledged by markJsCheckCompleted.
|
|
257
|
+
private jsCheckCompletedConfigJson?: string;
|
|
245
258
|
private reportedInvalidUpdates = new Set<string>();
|
|
246
259
|
private reportedExceptions = new WeakSet<object>();
|
|
247
260
|
private globalErrorCleanup?: () => void;
|
|
@@ -362,12 +375,8 @@ export class Pushy {
|
|
|
362
375
|
|
|
363
376
|
/** Build the subset of options used by the native cold-start check. */
|
|
364
377
|
private getNativeConfig = (): Record<string, unknown> | undefined => {
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
typeof PushyModule.syncNativeConfig !== 'function'
|
|
368
|
-
) {
|
|
369
|
-
// Older natives lack the method; on web PushyModule is a noop Proxy
|
|
370
|
-
// and the feature-detect would false-positive.
|
|
378
|
+
if (!nativeMethod('syncNativeConfig')) {
|
|
379
|
+
// Web, or an older native without the native check.
|
|
371
380
|
return undefined;
|
|
372
381
|
}
|
|
373
382
|
const {
|
|
@@ -422,9 +431,11 @@ export class Pushy {
|
|
|
422
431
|
private flushNativeConfig = () => {
|
|
423
432
|
if (this.options.nativeConfigSource === 'native') {
|
|
424
433
|
this.pendingNativeConfigJson = undefined;
|
|
434
|
+
this.syncedNativeConfigJson = undefined;
|
|
425
435
|
return;
|
|
426
436
|
}
|
|
427
|
-
|
|
437
|
+
const sync = nativeMethod('syncNativeConfig');
|
|
438
|
+
if (!sync || this.nativeConfigSyncInFlight) {
|
|
428
439
|
return;
|
|
429
440
|
}
|
|
430
441
|
const configJson = this.pendingNativeConfigJson;
|
|
@@ -433,17 +444,14 @@ export class Pushy {
|
|
|
433
444
|
return;
|
|
434
445
|
}
|
|
435
446
|
this.nativeConfigSyncInFlight = true;
|
|
436
|
-
|
|
437
|
-
try {
|
|
438
|
-
syncResult = Promise.resolve(PushyModule.syncNativeConfig(configJson));
|
|
439
|
-
} catch (e: any) {
|
|
440
|
-
this.nativeConfigSyncInFlight = false;
|
|
441
|
-
log('syncNativeConfig failed:', e?.message || e);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
syncResult
|
|
447
|
+
sync(configJson)
|
|
445
448
|
.then(() => {
|
|
446
|
-
this
|
|
449
|
+
// A switch to 'native' while this write was in flight hands storage
|
|
450
|
+
// to the host; remembering the write would skip it after switching
|
|
451
|
+
// back.
|
|
452
|
+
if (this.options.nativeConfigSource !== 'native') {
|
|
453
|
+
this.syncedNativeConfigJson = configJson;
|
|
454
|
+
}
|
|
447
455
|
})
|
|
448
456
|
.catch((e: any) => {
|
|
449
457
|
log('syncNativeConfig failed:', e?.message || e);
|
|
@@ -462,36 +470,34 @@ export class Pushy {
|
|
|
462
470
|
* rescue must keep running when JS could not reach the server.
|
|
463
471
|
*/
|
|
464
472
|
private markJsCheckCompleted = () => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
typeof PushyModule.markJsCheckCompleted !== 'function'
|
|
468
|
-
) {
|
|
473
|
+
const markCompleted = nativeMethod('markJsCheckCompleted');
|
|
474
|
+
if (!markCompleted) {
|
|
469
475
|
return;
|
|
470
476
|
}
|
|
471
477
|
const configJson = this.getNativeConfigJson();
|
|
472
|
-
if (!configJson) {
|
|
478
|
+
if (!configJson || configJson === this.jsCheckCompletedConfigJson) {
|
|
473
479
|
return;
|
|
474
480
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
481
|
+
markCompleted(configJson).then(
|
|
482
|
+
() => {
|
|
483
|
+
this.jsCheckCompletedConfigJson = configJson;
|
|
484
|
+
},
|
|
485
|
+
(e: any) => {
|
|
486
|
+
log('markJsCheckCompleted failed:', e?.message || e);
|
|
487
|
+
}
|
|
488
|
+
);
|
|
484
489
|
};
|
|
485
490
|
|
|
486
491
|
private syncNativeConfig = () => {
|
|
487
492
|
if (this.options.nativeConfigSource === 'native') {
|
|
493
|
+
// The host owns the stored config now; forget what JS last wrote so a
|
|
494
|
+
// switch back to 'javascript' writes again instead of deduping
|
|
495
|
+
// against a value native storage no longer holds.
|
|
488
496
|
this.pendingNativeConfigJson = undefined;
|
|
497
|
+
this.syncedNativeConfigJson = undefined;
|
|
489
498
|
return;
|
|
490
499
|
}
|
|
491
|
-
if (
|
|
492
|
-
Platform.OS === 'web' ||
|
|
493
|
-
typeof PushyModule.syncNativeConfig !== 'function'
|
|
494
|
-
) {
|
|
500
|
+
if (!nativeMethod('syncNativeConfig')) {
|
|
495
501
|
return;
|
|
496
502
|
}
|
|
497
503
|
const configJson = this.getNativeConfigJson();
|
|
@@ -518,50 +524,6 @@ export class Pushy {
|
|
|
518
524
|
getEffectivePackageVersion = () =>
|
|
519
525
|
this.options.overridePackageVersion || packageVersion;
|
|
520
526
|
|
|
521
|
-
private jsonValuesEqual = (left: unknown, right: unknown): boolean => {
|
|
522
|
-
const compare = (a: unknown, b: unknown): boolean => {
|
|
523
|
-
if (a === b) {
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
527
|
-
return (
|
|
528
|
-
Array.isArray(a) &&
|
|
529
|
-
Array.isArray(b) &&
|
|
530
|
-
a.length === b.length &&
|
|
531
|
-
a.every((value, index) => compare(value, b[index]))
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
if (
|
|
535
|
-
a === null ||
|
|
536
|
-
b === null ||
|
|
537
|
-
typeof a !== 'object' ||
|
|
538
|
-
typeof b !== 'object'
|
|
539
|
-
) {
|
|
540
|
-
return false;
|
|
541
|
-
}
|
|
542
|
-
const leftObject = a as Record<string, unknown>;
|
|
543
|
-
const rightObject = b as Record<string, unknown>;
|
|
544
|
-
// Match JSON.stringify semantics for request extras: object properties
|
|
545
|
-
// whose value is undefined are omitted from the wire fingerprint.
|
|
546
|
-
const leftKeys = Object.keys(leftObject)
|
|
547
|
-
.filter((key) => leftObject[key] !== undefined)
|
|
548
|
-
.sort();
|
|
549
|
-
const rightKeys = Object.keys(rightObject)
|
|
550
|
-
.filter((key) => rightObject[key] !== undefined)
|
|
551
|
-
.sort();
|
|
552
|
-
return (
|
|
553
|
-
leftKeys.length === rightKeys.length &&
|
|
554
|
-
leftKeys.every(
|
|
555
|
-
(key, index) =>
|
|
556
|
-
key === rightKeys[index] &&
|
|
557
|
-
compare(leftObject[key], rightObject[key])
|
|
558
|
-
)
|
|
559
|
-
);
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
return compare(left, right);
|
|
563
|
-
};
|
|
564
|
-
|
|
565
527
|
private providerMounted = false;
|
|
566
528
|
/**
|
|
567
529
|
* Called by UpdateProvider on mount. A second concurrently mounted
|
|
@@ -624,7 +586,8 @@ export class Pushy {
|
|
|
624
586
|
if (
|
|
625
587
|
__DEV__ ||
|
|
626
588
|
this.options.disableTelemetry ||
|
|
627
|
-
this.options.disableErrorReporting
|
|
589
|
+
this.options.disableErrorReporting ||
|
|
590
|
+
!currentVersion
|
|
628
591
|
) {
|
|
629
592
|
return;
|
|
630
593
|
}
|
|
@@ -638,7 +601,11 @@ export class Pushy {
|
|
|
638
601
|
}
|
|
639
602
|
this.reportedExceptions.add(object);
|
|
640
603
|
}
|
|
641
|
-
this.
|
|
604
|
+
this.postReport({
|
|
605
|
+
type: 'js_exception',
|
|
606
|
+
hash: currentVersion,
|
|
607
|
+
error: serializeException(error, context),
|
|
608
|
+
});
|
|
642
609
|
} catch (e: any) {
|
|
643
610
|
log('exception telemetry error:', e?.message || e);
|
|
644
611
|
}
|
|
@@ -682,55 +649,7 @@ export class Pushy {
|
|
|
682
649
|
return cleanup;
|
|
683
650
|
};
|
|
684
651
|
|
|
685
|
-
|
|
686
|
-
try {
|
|
687
|
-
if (
|
|
688
|
-
__DEV__ ||
|
|
689
|
-
this.options.disableTelemetry ||
|
|
690
|
-
this.options.disableErrorReporting ||
|
|
691
|
-
!currentVersion
|
|
692
|
-
) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
const { appKey } = this.options;
|
|
696
|
-
const endpoint =
|
|
697
|
-
this.lastWorkingEndpoint || this.options.server?.main?.[0];
|
|
698
|
-
if (!appKey || !endpoint) {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
fetchWithTimeout(
|
|
702
|
-
`${endpoint}/report/${appKey}`,
|
|
703
|
-
{
|
|
704
|
-
method: 'POST',
|
|
705
|
-
headers: { 'Content-Type': 'application/json' },
|
|
706
|
-
body: JSON.stringify({
|
|
707
|
-
type: 'js_exception',
|
|
708
|
-
hash: currentVersion,
|
|
709
|
-
packageVersion: this.getEffectivePackageVersion(),
|
|
710
|
-
cInfo,
|
|
711
|
-
error,
|
|
712
|
-
}),
|
|
713
|
-
},
|
|
714
|
-
DEFAULT_FETCH_TIMEOUT_MS
|
|
715
|
-
).catch((e: any) => {
|
|
716
|
-
log('exception telemetry report failed:', e?.message || e);
|
|
717
|
-
});
|
|
718
|
-
} catch (e: any) {
|
|
719
|
-
log('exception telemetry error:', e?.message || e);
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
report = async ({
|
|
724
|
-
type,
|
|
725
|
-
message = '',
|
|
726
|
-
code,
|
|
727
|
-
data = {},
|
|
728
|
-
}: {
|
|
729
|
-
type: EventType;
|
|
730
|
-
message?: string;
|
|
731
|
-
code?: UpdateErrorCode;
|
|
732
|
-
data?: Record<string, string | number>;
|
|
733
|
-
}) => {
|
|
652
|
+
report = async ({ type, message = '', code, data = {} }: ReportEvent) => {
|
|
734
653
|
log(`${type} ${code ? `[${code}] ` : ''}${message}`);
|
|
735
654
|
// Fire-and-forget server telemetry; must not wait for the logger below.
|
|
736
655
|
this.reportToServer({ type, message, code, data });
|
|
@@ -782,15 +701,10 @@ export class Pushy {
|
|
|
782
701
|
*/
|
|
783
702
|
private reportToServer = ({
|
|
784
703
|
type,
|
|
785
|
-
message
|
|
704
|
+
message,
|
|
786
705
|
code,
|
|
787
706
|
data = {},
|
|
788
|
-
}: {
|
|
789
|
-
type: EventType;
|
|
790
|
-
message?: string;
|
|
791
|
-
code?: UpdateErrorCode;
|
|
792
|
-
data?: Record<string, string | number>;
|
|
793
|
-
}) => {
|
|
707
|
+
}: ReportEvent) => {
|
|
794
708
|
try {
|
|
795
709
|
if (__DEV__ || this.options.disableTelemetry) {
|
|
796
710
|
return;
|
|
@@ -799,45 +713,56 @@ export class Pushy {
|
|
|
799
713
|
if (!serverType) {
|
|
800
714
|
return;
|
|
801
715
|
}
|
|
802
|
-
const { appKey } = this.options;
|
|
803
|
-
const endpoint =
|
|
804
|
-
this.lastWorkingEndpoint || this.options.server?.main?.[0];
|
|
805
|
-
if (!appKey || !endpoint) {
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
716
|
const hash = resolveServerEventHash({ serverType, data, currentVersion });
|
|
809
717
|
if (!hash) {
|
|
810
718
|
return;
|
|
811
719
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
packageVersion: this.getEffectivePackageVersion(),
|
|
822
|
-
cInfo,
|
|
823
|
-
detail: truncateDetail(detail),
|
|
824
|
-
}),
|
|
825
|
-
},
|
|
826
|
-
DEFAULT_FETCH_TIMEOUT_MS
|
|
827
|
-
).catch((e: any) => {
|
|
828
|
-
log('telemetry report failed:', e?.message || e);
|
|
829
|
-
});
|
|
830
|
-
send(serverType, message || undefined);
|
|
831
|
-
// A download that only succeeded after an incremental patch failed is
|
|
832
|
-
// still a patch_fail signal server-side (diff quality), carried in
|
|
833
|
-
// data.error alongside the downloadSuccess event.
|
|
834
|
-
if (serverType === 'download_success' && data.error) {
|
|
835
|
-
send('patch_fail', String(data.error));
|
|
836
|
-
}
|
|
720
|
+
// A download rescued by a later strategy reports its patch-health
|
|
721
|
+
// signal through downloadFallback, which only counts patch-apply
|
|
722
|
+
// failures; downloadSuccess's data.error (any earlier failure, network
|
|
723
|
+
// errors included) stays local.
|
|
724
|
+
this.postReport({
|
|
725
|
+
type: serverType,
|
|
726
|
+
hash,
|
|
727
|
+
detail: truncateDetail(message),
|
|
728
|
+
});
|
|
837
729
|
} catch (e: any) {
|
|
838
730
|
log('telemetry error:', e?.message || e);
|
|
839
731
|
}
|
|
840
732
|
};
|
|
733
|
+
/**
|
|
734
|
+
* The single telemetry POST ({endpoint}/report/{appKey}) to the last known
|
|
735
|
+
* working endpoint; no retry, failures only logged.
|
|
736
|
+
*/
|
|
737
|
+
private postReport = ({
|
|
738
|
+
type,
|
|
739
|
+
hash,
|
|
740
|
+
...payload
|
|
741
|
+
}: {
|
|
742
|
+
type: string;
|
|
743
|
+
hash: string;
|
|
744
|
+
detail?: string;
|
|
745
|
+
error?: SerializedException;
|
|
746
|
+
}) => {
|
|
747
|
+
const { appKey } = this.options;
|
|
748
|
+
const endpoint = this.lastWorkingEndpoint || this.options.server?.main?.[0];
|
|
749
|
+
if (!appKey || !endpoint) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
fetchWithTimeout(`${endpoint}/report/${appKey}`, {
|
|
753
|
+
method: 'POST',
|
|
754
|
+
headers: { 'Content-Type': 'application/json' },
|
|
755
|
+
body: JSON.stringify({
|
|
756
|
+
type,
|
|
757
|
+
hash,
|
|
758
|
+
packageVersion: this.getEffectivePackageVersion(),
|
|
759
|
+
cInfo,
|
|
760
|
+
...payload,
|
|
761
|
+
}),
|
|
762
|
+
}).catch((e: any) => {
|
|
763
|
+
log('telemetry report failed:', e?.message || e);
|
|
764
|
+
});
|
|
765
|
+
};
|
|
841
766
|
throwIfEnabled = (e: Error) => {
|
|
842
767
|
if (this.options.throwError) {
|
|
843
768
|
throw e;
|
|
@@ -894,6 +819,32 @@ export class Pushy {
|
|
|
894
819
|
}
|
|
895
820
|
}
|
|
896
821
|
};
|
|
822
|
+
/**
|
|
823
|
+
* toUpdateError + emitError: how a caught failure leaves the client.
|
|
824
|
+
* Returns the error for the caller to throw or pass to throwIfEnabled.
|
|
825
|
+
* `fallbackMessage` is reported when the error has no message of its own.
|
|
826
|
+
*/
|
|
827
|
+
private fail = (
|
|
828
|
+
e: unknown,
|
|
829
|
+
code: UpdateErrorCode,
|
|
830
|
+
type: EventType,
|
|
831
|
+
{
|
|
832
|
+
message,
|
|
833
|
+
fallbackMessage,
|
|
834
|
+
data,
|
|
835
|
+
}: {
|
|
836
|
+
message?: string;
|
|
837
|
+
fallbackMessage?: string;
|
|
838
|
+
data?: Record<string, string | number>;
|
|
839
|
+
} = {}
|
|
840
|
+
): UpdateError => {
|
|
841
|
+
const err = toUpdateError(e, code);
|
|
842
|
+
this.emitError(err, type, {
|
|
843
|
+
message: message ?? (err.message || fallbackMessage),
|
|
844
|
+
data,
|
|
845
|
+
});
|
|
846
|
+
return err;
|
|
847
|
+
};
|
|
897
848
|
notifyAfterCheckUpdate = (state: UpdateCheckState) => {
|
|
898
849
|
const { afterCheckUpdate } = this.options;
|
|
899
850
|
if (!afterCheckUpdate) {
|
|
@@ -916,16 +867,9 @@ export class Pushy {
|
|
|
916
867
|
}
|
|
917
868
|
return true;
|
|
918
869
|
};
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
getConfiguredCheckEndpoints = () => {
|
|
923
|
-
const { server } = this.options;
|
|
924
|
-
if (!server) {
|
|
925
|
-
return [];
|
|
926
|
-
}
|
|
927
|
-
return dedupeEndpoints(server.main);
|
|
928
|
-
};
|
|
870
|
+
// server.main is deduplicated on assignment (cloneServerConfig), and the
|
|
871
|
+
// endpoint plan dedupes again anyway.
|
|
872
|
+
getConfiguredCheckEndpoints = () => this.options.server?.main ?? [];
|
|
929
873
|
getRemoteEndpoints = async () => {
|
|
930
874
|
const { server } = this.options;
|
|
931
875
|
if (!server?.queryUrls?.length) {
|
|
@@ -938,11 +882,7 @@ export class Pushy {
|
|
|
938
882
|
// with an error page must not beat the healthy one.
|
|
939
883
|
const remoteEndpoints = await promiseAny(
|
|
940
884
|
server.queryUrls.map(async (queryUrl) => {
|
|
941
|
-
const resp = await fetchWithTimeout(
|
|
942
|
-
queryUrl,
|
|
943
|
-
{},
|
|
944
|
-
DEFAULT_FETCH_TIMEOUT_MS
|
|
945
|
-
);
|
|
885
|
+
const resp = await fetchWithTimeout(queryUrl, {});
|
|
946
886
|
if (!resp.ok) {
|
|
947
887
|
throw Error(`${queryUrl}: ${resp.status}`);
|
|
948
888
|
}
|
|
@@ -954,16 +894,15 @@ export class Pushy {
|
|
|
954
894
|
})
|
|
955
895
|
);
|
|
956
896
|
log('fetch endpoints:', remoteEndpoints);
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
)
|
|
966
|
-
this.getConfiguredCheckEndpoints()
|
|
897
|
+
// Configured and already-failed endpoints are dropped by the endpoint
|
|
898
|
+
// fallback itself.
|
|
899
|
+
return dedupeEndpoints(
|
|
900
|
+
remoteEndpoints.filter(
|
|
901
|
+
// The list is fetched from a public mirror; it may add endpoints
|
|
902
|
+
// but never move the check outside TLS.
|
|
903
|
+
(endpoint): endpoint is string =>
|
|
904
|
+
typeof endpoint === 'string' && /^https:\/\//i.test(endpoint)
|
|
905
|
+
)
|
|
967
906
|
);
|
|
968
907
|
} catch (e) {
|
|
969
908
|
log('failed to fetch endpoints from: ', server.queryUrls, e);
|
|
@@ -974,54 +913,50 @@ export class Pushy {
|
|
|
974
913
|
endpoint: string,
|
|
975
914
|
fetchPayload: Parameters<typeof fetch>[1],
|
|
976
915
|
signal?: AbortSignal
|
|
977
|
-
) => {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (!resp.ok) {
|
|
986
|
-
const respText = (await resp.text()).slice(0, MAX_HTTP_ERROR_TEXT);
|
|
987
|
-
throw new UpdateError(
|
|
988
|
-
this.t('error_http_status', {
|
|
989
|
-
status: resp.status,
|
|
990
|
-
statusText: respText,
|
|
991
|
-
}),
|
|
992
|
-
'HTTP_STATUS',
|
|
993
|
-
{ extra: { status: resp.status } }
|
|
916
|
+
): Promise<CheckResult> => {
|
|
917
|
+
try {
|
|
918
|
+
const resp = await fetchWithTimeout(
|
|
919
|
+
`${endpoint}/checkUpdate/${this.options.appKey}`,
|
|
920
|
+
fetchPayload,
|
|
921
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
922
|
+
signal
|
|
994
923
|
);
|
|
995
|
-
}
|
|
996
924
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
925
|
+
if (!resp.ok) {
|
|
926
|
+
const respText = (await resp.text()).slice(0, MAX_HTTP_ERROR_TEXT);
|
|
927
|
+
throw new UpdateError(
|
|
928
|
+
this.t('error_http_status', {
|
|
929
|
+
status: resp.status,
|
|
930
|
+
statusText: respText,
|
|
931
|
+
}),
|
|
932
|
+
'HTTP_STATUS',
|
|
933
|
+
{ extra: { status: resp.status } }
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const result: unknown = await resp.json();
|
|
938
|
+
if (!isValidCheckResult(result)) {
|
|
939
|
+
// A 2xx that is not a verdict (`{"error": ...}`, a captive-portal
|
|
940
|
+
// page that parsed, ...) is a failed endpoint: throwing keeps the
|
|
941
|
+
// fallback moving to the next one instead of treating it as "no
|
|
942
|
+
// update".
|
|
943
|
+
throw new UpdateError(
|
|
944
|
+
this.t('error_invalid_check_response'),
|
|
945
|
+
'INVALID_RESPONSE'
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
return result;
|
|
949
|
+
} catch (e) {
|
|
950
|
+
log('check endpoint failed', endpoint, e);
|
|
951
|
+
throw e;
|
|
1006
952
|
}
|
|
1007
|
-
return result;
|
|
1008
953
|
};
|
|
1009
954
|
fetchCheckResult = async (fetchPayload: Parameters<typeof fetch>[1]) => {
|
|
1010
955
|
const { endpoint, value } = await executeEndpointFallback<CheckResult>({
|
|
1011
956
|
configuredEndpoints: this.getConfiguredCheckEndpoints(),
|
|
1012
957
|
getRemoteEndpoints: this.getRemoteEndpoints,
|
|
1013
|
-
tryEndpoint:
|
|
1014
|
-
|
|
1015
|
-
return await this.requestCheckResult(
|
|
1016
|
-
currentEndpoint,
|
|
1017
|
-
fetchPayload,
|
|
1018
|
-
signal
|
|
1019
|
-
);
|
|
1020
|
-
} catch (e) {
|
|
1021
|
-
log('check endpoint failed', currentEndpoint, e);
|
|
1022
|
-
throw e;
|
|
1023
|
-
}
|
|
1024
|
-
},
|
|
958
|
+
tryEndpoint: (currentEndpoint, signal) =>
|
|
959
|
+
this.requestCheckResult(currentEndpoint, fetchPayload, signal),
|
|
1025
960
|
onFirstFailure: ({ error }) => {
|
|
1026
961
|
this.report({
|
|
1027
962
|
type: 'errorChecking',
|
|
@@ -1046,34 +981,32 @@ export class Pushy {
|
|
|
1046
981
|
requestBody: Record<string, any>
|
|
1047
982
|
): Promise<CheckResult | undefined> => {
|
|
1048
983
|
try {
|
|
1049
|
-
|
|
984
|
+
const getCache = nativeMethod('getNativeCheckCache');
|
|
985
|
+
if (__DEV__ || nativeCheckCacheExhausted || !getCache) {
|
|
1050
986
|
return undefined;
|
|
1051
987
|
}
|
|
1052
|
-
const raw = await
|
|
988
|
+
const raw = await getCache();
|
|
1053
989
|
if (!raw || typeof raw !== 'string') {
|
|
990
|
+
nativeCheckCacheExhausted = true;
|
|
1054
991
|
return undefined;
|
|
1055
992
|
}
|
|
1056
993
|
const entry = JSON.parse(raw);
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
typeof entry?.
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const cachedConfig =
|
|
1063
|
-
typeof entry?.config === 'string'
|
|
1064
|
-
? JSON.parse(entry.config)
|
|
1065
|
-
: undefined;
|
|
1066
|
-
if (
|
|
1067
|
-
typeof entry?.ts !== 'number' ||
|
|
1068
|
-
typeof entry?.body !== 'string' ||
|
|
1069
|
-
!this.jsonValuesEqual(cachedRequest, requestBody) ||
|
|
1070
|
-
!config ||
|
|
1071
|
-
!this.jsonValuesEqual(cachedConfig, config)
|
|
1072
|
-
) {
|
|
994
|
+
// Age first: it is the common miss and needs no further parsing.
|
|
995
|
+
const ageSeconds =
|
|
996
|
+
typeof entry?.ts === 'number' ? Date.now() / 1000 - entry.ts : NaN;
|
|
997
|
+
if (!(ageSeconds >= 0 && ageSeconds <= NATIVE_CHECK_CACHE_MAX_AGE_S)) {
|
|
998
|
+
nativeCheckCacheExhausted = true;
|
|
1073
999
|
return undefined;
|
|
1074
1000
|
}
|
|
1075
|
-
const
|
|
1076
|
-
if (
|
|
1001
|
+
const configJson = this.getNativeConfigJson();
|
|
1002
|
+
if (
|
|
1003
|
+
typeof entry.body !== 'string' ||
|
|
1004
|
+
typeof entry.request !== 'string' ||
|
|
1005
|
+
typeof entry.config !== 'string' ||
|
|
1006
|
+
!configJson ||
|
|
1007
|
+
!nativeConfigsEquivalent(entry.config, configJson) ||
|
|
1008
|
+
!jsonDeepEqual(JSON.parse(entry.request), requestBody)
|
|
1009
|
+
) {
|
|
1077
1010
|
return undefined;
|
|
1078
1011
|
}
|
|
1079
1012
|
const result: unknown = JSON.parse(entry.body);
|
|
@@ -1100,12 +1033,23 @@ export class Pushy {
|
|
|
1100
1033
|
if (sharedState.marked || __DEV__ || !isFirstTime) {
|
|
1101
1034
|
return;
|
|
1102
1035
|
}
|
|
1036
|
+
// Concurrent callers (auto mark + an explicit call) would all pass the
|
|
1037
|
+
// check above before the first native call settles: share it.
|
|
1038
|
+
if (!markingPromise) {
|
|
1039
|
+
const promise = this.performMarkSuccess().finally(() => {
|
|
1040
|
+
if (markingPromise === promise) {
|
|
1041
|
+
markingPromise = undefined;
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
markingPromise = promise;
|
|
1045
|
+
}
|
|
1046
|
+
return markingPromise;
|
|
1047
|
+
};
|
|
1048
|
+
private performMarkSuccess = async () => {
|
|
1103
1049
|
try {
|
|
1104
1050
|
await Promise.resolve(PushyModule.markSuccess());
|
|
1105
1051
|
} catch (e) {
|
|
1106
|
-
|
|
1107
|
-
this.emitError(err, 'errorMarkSuccess');
|
|
1108
|
-
throw err;
|
|
1052
|
+
throw this.fail(e, 'MARK_SUCCESS_FAILED', 'errorMarkSuccess');
|
|
1109
1053
|
}
|
|
1110
1054
|
sharedState.marked = true;
|
|
1111
1055
|
this.report({ type: 'markSuccess' });
|
|
@@ -1142,15 +1086,14 @@ export class Pushy {
|
|
|
1142
1086
|
return false;
|
|
1143
1087
|
}
|
|
1144
1088
|
} catch (e) {
|
|
1089
|
+
// Release before emitting: an onError listener may retry right away.
|
|
1145
1090
|
finishApplyingUpdate(applyingGeneration);
|
|
1146
1091
|
// A throw from the user's beforeReload hook is business-code failure,
|
|
1147
1092
|
// not an update-pipeline one: give it a distinct code so telemetry
|
|
1148
1093
|
// excludes it from the server-side patch-health stats.
|
|
1149
|
-
|
|
1150
|
-
this.emitError(err, 'errorSwitchVersion', {
|
|
1094
|
+
throw this.fail(e, 'USER_HOOK_ERROR', 'errorSwitchVersion', {
|
|
1151
1095
|
data: { newVersion: hash },
|
|
1152
1096
|
});
|
|
1153
|
-
throw err;
|
|
1154
1097
|
}
|
|
1155
1098
|
// resetToPackagedBundle may have completed while beforeReload awaited.
|
|
1156
1099
|
if (!ownsApplyingUpdate(applyingGeneration)) {
|
|
@@ -1161,11 +1104,9 @@ export class Pushy {
|
|
|
1161
1104
|
} catch (e) {
|
|
1162
1105
|
// Do not let a superseded rejection clear a newer switch.
|
|
1163
1106
|
finishApplyingUpdate(applyingGeneration);
|
|
1164
|
-
|
|
1165
|
-
this.emitError(err, 'errorSwitchVersion', {
|
|
1107
|
+
throw this.fail(e, 'SWITCH_VERSION_FAILED', 'errorSwitchVersion', {
|
|
1166
1108
|
data: { newVersion: hash },
|
|
1167
1109
|
});
|
|
1168
|
-
throw err;
|
|
1169
1110
|
}
|
|
1170
1111
|
// A resolved reloadUpdate normally tears this JS context down right away.
|
|
1171
1112
|
// iOS has been seen resolving without restarting (89c638e); a stuck
|
|
@@ -1192,11 +1133,9 @@ export class Pushy {
|
|
|
1192
1133
|
try {
|
|
1193
1134
|
return await PushyModule.setNeedUpdate({ hash });
|
|
1194
1135
|
} catch (e) {
|
|
1195
|
-
|
|
1196
|
-
this.emitError(err, 'errorSwitchVersion', {
|
|
1136
|
+
throw this.fail(e, 'SWITCH_VERSION_FAILED', 'errorSwitchVersion', {
|
|
1197
1137
|
data: { newVersion: hash },
|
|
1198
1138
|
});
|
|
1199
|
-
throw err;
|
|
1200
1139
|
}
|
|
1201
1140
|
}
|
|
1202
1141
|
};
|
|
@@ -1241,7 +1180,6 @@ export class Pushy {
|
|
|
1241
1180
|
appKey: this.options.appKey,
|
|
1242
1181
|
endpoints: this.options.server?.main,
|
|
1243
1182
|
queryUrls: this.options.server?.queryUrls,
|
|
1244
|
-
// The client uuid travels inside cInfo, i.e. inside `body`.
|
|
1245
1183
|
body: stringifyBody,
|
|
1246
1184
|
});
|
|
1247
1185
|
const now = Date.now();
|
|
@@ -1325,9 +1263,8 @@ export class Pushy {
|
|
|
1325
1263
|
this.lastRespJson = undefined;
|
|
1326
1264
|
this.lastCheckFingerprint = undefined;
|
|
1327
1265
|
}
|
|
1328
|
-
const err =
|
|
1329
|
-
|
|
1330
|
-
message: err.message || this.t('error_cannot_connect_server'),
|
|
1266
|
+
const err = this.fail(e, 'CHECK_FAILED', 'errorChecking', {
|
|
1267
|
+
fallbackMessage: this.t('error_cannot_connect_server'),
|
|
1331
1268
|
});
|
|
1332
1269
|
this.notifyAfterCheckUpdate({ status: 'error', error: err });
|
|
1333
1270
|
this.throwIfEnabled(err);
|
|
@@ -1441,15 +1378,14 @@ export class Pushy {
|
|
|
1441
1378
|
}
|
|
1442
1379
|
sharedState.progressCallbacks[hash] = progressCallbacks;
|
|
1443
1380
|
const dispatchProgress = (data: ProgressData) => {
|
|
1444
|
-
|
|
1445
|
-
if (!callbacks || callbacks.size === 0) {
|
|
1381
|
+
if (progressCallbacks.size === 0) {
|
|
1446
1382
|
return;
|
|
1447
1383
|
}
|
|
1448
1384
|
const payload = {
|
|
1449
1385
|
...data,
|
|
1450
1386
|
progress: computeProgress(data.received, data.total),
|
|
1451
1387
|
};
|
|
1452
|
-
|
|
1388
|
+
progressCallbacks.forEach((callback) => {
|
|
1453
1389
|
// One subscriber throwing must not starve the others or abort the
|
|
1454
1390
|
// download: business callbacks are isolated, the error is logged.
|
|
1455
1391
|
try {
|
|
@@ -1469,17 +1405,10 @@ export class Pushy {
|
|
|
1469
1405
|
}
|
|
1470
1406
|
};
|
|
1471
1407
|
// @ts-expect-error harmony not in existing platforms
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
);
|
|
1477
|
-
} else {
|
|
1478
|
-
sharedState.progressHandlers[hash] = pushyNativeEventEmitter.addListener(
|
|
1479
|
-
'RCTPushyDownloadProgress',
|
|
1480
|
-
onNativeProgress
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1408
|
+
const isHarmony = Platform.OS === 'harmony';
|
|
1409
|
+
const progressSubscription = (
|
|
1410
|
+
isHarmony ? DeviceEventEmitter : pushyNativeEventEmitter
|
|
1411
|
+
).addListener('RCTPushyDownloadProgress', onNativeProgress);
|
|
1483
1412
|
const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
|
|
1484
1413
|
let succeeded = '';
|
|
1485
1414
|
let lastError: UpdateError | undefined;
|
|
@@ -1489,10 +1418,6 @@ export class Pushy {
|
|
|
1489
1418
|
// message, so the final report still names it — and skip it on later
|
|
1490
1419
|
// attempts instead of downloading and patching it again.
|
|
1491
1420
|
const exhaustedStrategies = new Map<DownloadStrategyType, string>();
|
|
1492
|
-
// Whether any strategy, in any attempt, failed at the patch stage. The
|
|
1493
|
-
// downloadFallback report carries that as its code even when a later,
|
|
1494
|
-
// unrelated transport failure happened to be the last error seen.
|
|
1495
|
-
let patchFailed = false;
|
|
1496
1421
|
|
|
1497
1422
|
// The ordered attempts come from decideDownload (the pure decision layer);
|
|
1498
1423
|
// this side only executes them: probe candidate URLs, run the matching
|
|
@@ -1518,98 +1443,112 @@ export class Pushy {
|
|
|
1518
1443
|
hash,
|
|
1519
1444
|
}),
|
|
1520
1445
|
};
|
|
1446
|
+
// Mirror ranking per strategy, kept across retry attempts: the HEAD race
|
|
1447
|
+
// runs once per strategy per download instead of once per attempt. A
|
|
1448
|
+
// winner that failed its download is demoted to the back for the next
|
|
1449
|
+
// attempt (every mirror is tried on every attempt anyway).
|
|
1450
|
+
const rankedUrls = new Map<DownloadStrategyType, string[]>();
|
|
1521
1451
|
const errorKeys = {
|
|
1522
1452
|
diff: 'error_diff_failed',
|
|
1523
1453
|
pdiff: 'error_pdiff_failed',
|
|
1524
1454
|
full: 'error_full_patch_failed',
|
|
1525
1455
|
} as const;
|
|
1526
1456
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
(
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
this.report({
|
|
1543
|
-
type: 'downloading',
|
|
1544
|
-
data: {
|
|
1545
|
-
newVersion: hash,
|
|
1546
|
-
attempt,
|
|
1547
|
-
},
|
|
1548
|
-
});
|
|
1549
|
-
for (const { type, urls } of attempts) {
|
|
1550
|
-
if (succeeded) {
|
|
1551
|
-
break;
|
|
1552
|
-
}
|
|
1553
|
-
if (exhaustedStrategies.has(type)) {
|
|
1554
|
-
continue;
|
|
1457
|
+
try {
|
|
1458
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1459
|
+
if (attempt > 0) {
|
|
1460
|
+
// Exponential backoff with jitter: a bad CDN edge otherwise gets
|
|
1461
|
+
// every client back at the same 1/2/4s marks.
|
|
1462
|
+
const backoffMs = Math.round(
|
|
1463
|
+
Math.min(1000 * 2 ** (attempt - 1), 10000) *
|
|
1464
|
+
(0.75 + Math.random() * 0.5)
|
|
1465
|
+
);
|
|
1466
|
+
log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
|
|
1467
|
+
await new Promise<void>((r) => setTimeout(() => r(), backoffMs));
|
|
1468
|
+
errorMessages.length = 0;
|
|
1469
|
+
errorMessages.push(...exhaustedStrategies.values());
|
|
1470
|
+
lastError = undefined;
|
|
1471
|
+
succeeded = '';
|
|
1555
1472
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
await runners[type](url);
|
|
1566
|
-
succeeded = type;
|
|
1473
|
+
this.report({
|
|
1474
|
+
type: 'downloading',
|
|
1475
|
+
data: {
|
|
1476
|
+
newVersion: hash,
|
|
1477
|
+
attempt,
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
for (const { type, urls } of attempts) {
|
|
1481
|
+
if (succeeded) {
|
|
1567
1482
|
break;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (lastError.code === 'PATCH_FAILED') {
|
|
1588
|
-
patchFailed = true;
|
|
1589
|
-
}
|
|
1590
|
-
if (!isMirrorRetryableCode(lastError.code)) {
|
|
1591
|
-
// The bytes arrived but could not be applied (patch/manifest
|
|
1592
|
-
// mismatch): every mirror serves the same artifact, so move
|
|
1593
|
-
// on to the next strategy instead of re-downloading it.
|
|
1594
|
-
exhaustedStrategies.set(type, errorMessage);
|
|
1483
|
+
}
|
|
1484
|
+
if (exhaustedStrategies.has(type)) {
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
// The HEAD race only ranks mirrors; it is not proof that a mirror can
|
|
1488
|
+
// serve the whole artifact (CDNs answer HEAD and GET differently, and
|
|
1489
|
+
// a transfer can die mid-stream). Every mirror of the same artifact
|
|
1490
|
+
// gets a real download attempt before the strategy is abandoned —
|
|
1491
|
+
// the native cold-start path already behaves this way.
|
|
1492
|
+
let orderedUrls = rankedUrls.get(type);
|
|
1493
|
+
if (!orderedUrls) {
|
|
1494
|
+
orderedUrls = await this.rankDownloadUrls(urls);
|
|
1495
|
+
rankedUrls.set(type, orderedUrls);
|
|
1496
|
+
}
|
|
1497
|
+
for (const url of orderedUrls) {
|
|
1498
|
+
log(`downloading ${type} from ${url}`);
|
|
1499
|
+
try {
|
|
1500
|
+
await runners[type](url);
|
|
1501
|
+
succeeded = type;
|
|
1595
1502
|
break;
|
|
1503
|
+
} catch (e: unknown) {
|
|
1504
|
+
// The native rejection's stable code (e.g. PATCH_FAILED vs
|
|
1505
|
+
// DOWNLOAD_FAILED — telemetry classifies on it) may sit on the
|
|
1506
|
+
// `code` property or, from bridges that cannot set it, as a
|
|
1507
|
+
// `[CODE] ` message prefix. Either way it survives into the
|
|
1508
|
+
// i18n-wrapped error together with the original as cause.
|
|
1509
|
+
const { code, message } = readErrorCode(e);
|
|
1510
|
+
if (url === orderedUrls[0] && orderedUrls.length > 1) {
|
|
1511
|
+
rankedUrls.set(type, [...orderedUrls.slice(1), url]);
|
|
1512
|
+
}
|
|
1513
|
+
const errorMessage = this.t(errorKeys[type], {
|
|
1514
|
+
message: message || String(e ?? ''),
|
|
1515
|
+
});
|
|
1516
|
+
errorMessages.push(errorMessage);
|
|
1517
|
+
lastError = new UpdateError(
|
|
1518
|
+
errorMessage,
|
|
1519
|
+
code ?? 'DOWNLOAD_FAILED',
|
|
1520
|
+
{
|
|
1521
|
+
cause: e,
|
|
1522
|
+
}
|
|
1523
|
+
);
|
|
1524
|
+
log(errorMessage);
|
|
1525
|
+
if (!isMirrorRetryableCode(lastError.code)) {
|
|
1526
|
+
// The bytes arrived but could not be applied (patch/manifest
|
|
1527
|
+
// mismatch): every mirror serves the same artifact, so move
|
|
1528
|
+
// on to the next strategy instead of re-downloading it.
|
|
1529
|
+
exhaustedStrategies.set(type, errorMessage);
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1596
1532
|
}
|
|
1597
1533
|
}
|
|
1598
1534
|
}
|
|
1535
|
+
if (
|
|
1536
|
+
succeeded ||
|
|
1537
|
+
attempts.every(({ type }) => exhaustedStrategies.has(type))
|
|
1538
|
+
) {
|
|
1539
|
+
// Nothing left that a retry could change.
|
|
1540
|
+
break;
|
|
1541
|
+
}
|
|
1599
1542
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
sharedState.progressHandlers[hash].remove();
|
|
1610
|
-
delete sharedState.progressHandlers[hash];
|
|
1611
|
-
}
|
|
1612
|
-
delete sharedState.progressCallbacks[hash];
|
|
1543
|
+
} finally {
|
|
1544
|
+
progressSubscription.remove();
|
|
1545
|
+
delete sharedState.progressCallbacks[hash];
|
|
1546
|
+
}
|
|
1547
|
+
// Only a patch-stage failure exhausts a strategy (isMirrorRetryableCode),
|
|
1548
|
+
// and an exhausted strategy is never retried: these are exactly the patch
|
|
1549
|
+
// failures, each once.
|
|
1550
|
+
const patchErrorMessages = [...exhaustedStrategies.values()];
|
|
1551
|
+
const patchFailed = patchErrorMessages.length > 0;
|
|
1613
1552
|
if (succeeded && errorMessages.length > 0) {
|
|
1614
1553
|
// An earlier strategy failed and a later one rescued the download
|
|
1615
1554
|
// (e.g. pdiff copiesCrc mismatch on a rebuilt binary → full). Surface
|
|
@@ -1617,50 +1556,40 @@ export class Pushy {
|
|
|
1617
1556
|
// platform that incremental delivery is failing for this binary.
|
|
1618
1557
|
this.report({
|
|
1619
1558
|
type: 'downloadFallback',
|
|
1620
|
-
// The patch-health signal
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1559
|
+
// The patch-health signal (any strategy, in any attempt, failing at
|
|
1560
|
+
// the patch stage) wins over whatever transport failure happened to
|
|
1561
|
+
// come last (diff PATCH_FAILED, then pdiff network error, then full
|
|
1562
|
+
// succeeded is still a patch_fail server-side).
|
|
1623
1563
|
code: patchFailed ? 'PATCH_FAILED' : lastError?.code,
|
|
1624
1564
|
data: {
|
|
1625
1565
|
newVersion: hash,
|
|
1626
1566
|
succeeded,
|
|
1627
1567
|
},
|
|
1628
|
-
|
|
1568
|
+
// Only the patch failures: a mirror's timeout alongside them would
|
|
1569
|
+
// otherwise be read as the cause server-side.
|
|
1570
|
+
message: (patchFailed ? patchErrorMessages : errorMessages).join(';'),
|
|
1629
1571
|
});
|
|
1630
1572
|
}
|
|
1631
1573
|
if (!succeeded) {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
message,
|
|
1637
|
-
data: { newVersion: hash },
|
|
1638
|
-
});
|
|
1639
|
-
throw err;
|
|
1640
|
-
}
|
|
1641
|
-
// No download URL was even attempted (e.g. dev without a full URL):
|
|
1642
|
-
// report for diagnostics but there is no error object to surface.
|
|
1643
|
-
this.report({
|
|
1644
|
-
type: 'errorUpdate',
|
|
1574
|
+
// Every attempt runs at least one download (a plan always has an
|
|
1575
|
+
// artifact URL, devNoop returned above), so a failure left lastError.
|
|
1576
|
+
throw this.fail(lastError, 'DOWNLOAD_FAILED', 'errorUpdate', {
|
|
1577
|
+
message: errorMessages.join(';'),
|
|
1645
1578
|
data: { newVersion: hash },
|
|
1646
|
-
message,
|
|
1647
|
-
});
|
|
1648
|
-
return;
|
|
1649
|
-
} else {
|
|
1650
|
-
const duration = Date.now() - patchStartTime;
|
|
1651
|
-
const data: Record<string, any> = {
|
|
1652
|
-
newVersion: hash,
|
|
1653
|
-
diff: succeeded,
|
|
1654
|
-
duration,
|
|
1655
|
-
};
|
|
1656
|
-
if (errorMessages.length > 0) {
|
|
1657
|
-
data.error = errorMessages.join(';');
|
|
1658
|
-
}
|
|
1659
|
-
this.report({
|
|
1660
|
-
type: 'downloadSuccess',
|
|
1661
|
-
data,
|
|
1662
1579
|
});
|
|
1663
1580
|
}
|
|
1581
|
+
const data: Record<string, any> = {
|
|
1582
|
+
newVersion: hash,
|
|
1583
|
+
diff: succeeded,
|
|
1584
|
+
duration: Date.now() - patchStartTime,
|
|
1585
|
+
};
|
|
1586
|
+
if (errorMessages.length > 0) {
|
|
1587
|
+
data.error = errorMessages.join(';');
|
|
1588
|
+
}
|
|
1589
|
+
this.report({
|
|
1590
|
+
type: 'downloadSuccess',
|
|
1591
|
+
data,
|
|
1592
|
+
});
|
|
1664
1593
|
log(`downloaded ${succeeded} hash:`, hash);
|
|
1665
1594
|
const hashInfo: Record<string, any> = {
|
|
1666
1595
|
name,
|
|
@@ -1676,12 +1605,9 @@ export class Pushy {
|
|
|
1676
1605
|
try {
|
|
1677
1606
|
await setLocalHashInfo(hash, hashInfo);
|
|
1678
1607
|
} catch (e: any) {
|
|
1679
|
-
|
|
1680
|
-
this.emitError(err, 'errorUpdate', {
|
|
1681
|
-
message: err.message,
|
|
1608
|
+
throw this.fail(e, 'FILE_OPERATION_FAILED', 'errorUpdate', {
|
|
1682
1609
|
data: { newVersion: hash },
|
|
1683
1610
|
});
|
|
1684
|
-
throw err;
|
|
1685
1611
|
}
|
|
1686
1612
|
sharedState.downloadedHash = hash;
|
|
1687
1613
|
return hash;
|
|
@@ -1708,12 +1634,8 @@ export class Pushy {
|
|
|
1708
1634
|
sharedState.apkStatus = 'downloading';
|
|
1709
1635
|
this.report({ type: 'downloadingApk' });
|
|
1710
1636
|
const progressKey = 'downloadingApk';
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
sharedState.progressHandlers[progressKey].remove();
|
|
1714
|
-
}
|
|
1715
|
-
sharedState.progressHandlers[progressKey] =
|
|
1716
|
-
pushyNativeEventEmitter.addListener(
|
|
1637
|
+
const progressSubscription = onDownloadProgress
|
|
1638
|
+
? pushyNativeEventEmitter.addListener(
|
|
1717
1639
|
'RCTPushyDownloadProgress',
|
|
1718
1640
|
(event: unknown) => {
|
|
1719
1641
|
const progressData = event as ProgressData;
|
|
@@ -1721,8 +1643,8 @@ export class Pushy {
|
|
|
1721
1643
|
onDownloadProgress(progressData);
|
|
1722
1644
|
}
|
|
1723
1645
|
}
|
|
1724
|
-
)
|
|
1725
|
-
|
|
1646
|
+
)
|
|
1647
|
+
: undefined;
|
|
1726
1648
|
try {
|
|
1727
1649
|
await PushyModule.downloadAndInstallApk({
|
|
1728
1650
|
url,
|
|
@@ -1733,16 +1655,13 @@ export class Pushy {
|
|
|
1733
1655
|
} catch (e) {
|
|
1734
1656
|
sharedState.apkStatus = null;
|
|
1735
1657
|
// Keep the native error (message/stack) instead of discarding it.
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1658
|
+
this.throwIfEnabled(
|
|
1659
|
+
this.fail(e, 'APK_DOWNLOAD_FAILED', 'errorDownloadAndInstallApk', {
|
|
1660
|
+
fallbackMessage: this.t('error_apk_download_failed'),
|
|
1661
|
+
})
|
|
1662
|
+
);
|
|
1741
1663
|
} finally {
|
|
1742
|
-
|
|
1743
|
-
sharedState.progressHandlers[progressKey].remove();
|
|
1744
|
-
delete sharedState.progressHandlers[progressKey];
|
|
1745
|
-
}
|
|
1664
|
+
progressSubscription?.remove();
|
|
1746
1665
|
}
|
|
1747
1666
|
};
|
|
1748
1667
|
restartApp = async () => {
|
|
@@ -1751,16 +1670,12 @@ export class Pushy {
|
|
|
1751
1670
|
return;
|
|
1752
1671
|
}
|
|
1753
1672
|
} catch (e) {
|
|
1754
|
-
|
|
1755
|
-
this.emitError(err, 'errorRestart');
|
|
1756
|
-
throw err;
|
|
1673
|
+
throw this.fail(e, 'USER_HOOK_ERROR', 'errorRestart');
|
|
1757
1674
|
}
|
|
1758
1675
|
try {
|
|
1759
1676
|
return await PushyModule.restartApp();
|
|
1760
1677
|
} catch (e) {
|
|
1761
|
-
|
|
1762
|
-
this.emitError(err, 'errorRestart');
|
|
1763
|
-
throw err;
|
|
1678
|
+
throw this.fail(e, 'RESTART_FAILED', 'errorRestart');
|
|
1764
1679
|
}
|
|
1765
1680
|
};
|
|
1766
1681
|
/**
|
|
@@ -1778,13 +1693,10 @@ export class Pushy {
|
|
|
1778
1693
|
restart?: boolean;
|
|
1779
1694
|
}): Promise<boolean> => {
|
|
1780
1695
|
if (!assertWeb()) {
|
|
1781
|
-
// On web PushyModule is a Proxy of noops, so the feature-detect below
|
|
1782
|
-
// would report a false success.
|
|
1783
1696
|
return false;
|
|
1784
1697
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
// native module predates this method.
|
|
1698
|
+
const reset = nativeMethod('resetToPackagedBundle');
|
|
1699
|
+
if (!reset) {
|
|
1788
1700
|
const err = new UpdateError(
|
|
1789
1701
|
this.t('error_reset_not_supported'),
|
|
1790
1702
|
'RESET_FAILED'
|
|
@@ -1794,11 +1706,9 @@ export class Pushy {
|
|
|
1794
1706
|
return false;
|
|
1795
1707
|
}
|
|
1796
1708
|
try {
|
|
1797
|
-
await
|
|
1709
|
+
await reset();
|
|
1798
1710
|
} catch (e) {
|
|
1799
|
-
|
|
1800
|
-
this.emitError(err, 'errorReset');
|
|
1801
|
-
this.throwIfEnabled(err);
|
|
1711
|
+
this.throwIfEnabled(this.fail(e, 'RESET_FAILED', 'errorReset'));
|
|
1802
1712
|
return false;
|
|
1803
1713
|
}
|
|
1804
1714
|
// The downloaded versions are gone; drop JS bookkeeping referring to them
|