react-native-update 10.53.1 → 10.55.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 +59 -31
- package/README.md +62 -32
- 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 +288 -47
- package/src/core.ts +17 -3
- package/src/error.ts +3 -0
- package/src/errorReporting.ts +136 -0
- package/src/index.ts +14 -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 +29 -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
|
@@ -25,6 +25,13 @@ import {
|
|
|
25
25
|
UpdateError,
|
|
26
26
|
type UpdateErrorCode,
|
|
27
27
|
} from './error';
|
|
28
|
+
import {
|
|
29
|
+
type ErrorReportContext,
|
|
30
|
+
type ErrorReportingOptions,
|
|
31
|
+
installGlobalErrorHandler,
|
|
32
|
+
type SerializedException,
|
|
33
|
+
serializeException,
|
|
34
|
+
} from './errorReporting';
|
|
28
35
|
import i18n from './i18n';
|
|
29
36
|
import {
|
|
30
37
|
resolveServerEventHash,
|
|
@@ -41,10 +48,13 @@ import type {
|
|
|
41
48
|
UpdateServerConfig,
|
|
42
49
|
} from './type';
|
|
43
50
|
import {
|
|
51
|
+
buildCheckFingerprint,
|
|
44
52
|
buildCheckRequestBody,
|
|
45
53
|
type DownloadPlan,
|
|
46
54
|
type DownloadStrategyType,
|
|
47
55
|
decideDownload,
|
|
56
|
+
isMirrorRetryableCode,
|
|
57
|
+
isValidCheckResult,
|
|
48
58
|
} from './updateFlowCore';
|
|
49
59
|
import {
|
|
50
60
|
assertWeb,
|
|
@@ -75,10 +85,6 @@ const SERVER_PRESETS = {
|
|
|
75
85
|
main: [
|
|
76
86
|
'https://update.react-native.cn/api',
|
|
77
87
|
'https://update.reactnative.cn/api',
|
|
78
|
-
'https://1.rnupdate.online/api',
|
|
79
|
-
'https://2.rnupdate.online/api',
|
|
80
|
-
'https://3.rnupdate.online/api',
|
|
81
|
-
'https://4.rnupdate.online/api',
|
|
82
88
|
],
|
|
83
89
|
queryUrls: [
|
|
84
90
|
'https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json',
|
|
@@ -122,6 +128,7 @@ const createDefaultClientOptions = (): ClientOptions => ({
|
|
|
122
128
|
logger: noop,
|
|
123
129
|
debug: false,
|
|
124
130
|
throwError: false,
|
|
131
|
+
disableErrorReporting: false,
|
|
125
132
|
});
|
|
126
133
|
|
|
127
134
|
export const sharedState: {
|
|
@@ -171,6 +178,7 @@ export class Pushy {
|
|
|
171
178
|
clientType: 'Pushy' | 'Cresc' = 'Pushy';
|
|
172
179
|
lastChecking?: number;
|
|
173
180
|
lastRespJson?: Promise<CheckResult>;
|
|
181
|
+
lastCheckFingerprint?: string;
|
|
174
182
|
// Endpoint that most recently served a successful checkUpdate; telemetry
|
|
175
183
|
// reuses it instead of re-running the fallback race.
|
|
176
184
|
private lastWorkingEndpoint?: string;
|
|
@@ -178,6 +186,8 @@ export class Pushy {
|
|
|
178
186
|
private pendingNativeConfigJson?: string;
|
|
179
187
|
private nativeConfigSyncInFlight = false;
|
|
180
188
|
private reportedInvalidUpdates = new Set<string>();
|
|
189
|
+
private reportedExceptions = new WeakSet<object>();
|
|
190
|
+
private globalErrorCleanup?: () => void;
|
|
181
191
|
|
|
182
192
|
version = cInfo.rnu;
|
|
183
193
|
loggerPromise = (() => {
|
|
@@ -276,9 +286,18 @@ export class Pushy {
|
|
|
276
286
|
log('onOptionsChange listener error:', e?.message || e);
|
|
277
287
|
}
|
|
278
288
|
}
|
|
289
|
+
this.syncAutomaticErrorReporting();
|
|
279
290
|
this.syncNativeConfig();
|
|
280
291
|
};
|
|
281
292
|
|
|
293
|
+
private syncAutomaticErrorReporting = (): void => {
|
|
294
|
+
if (this.options.disableTelemetry || this.options.disableErrorReporting) {
|
|
295
|
+
this.globalErrorCleanup?.();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.enableErrorReporting();
|
|
299
|
+
};
|
|
300
|
+
|
|
282
301
|
/** Build the subset of options used by the native cold-start check. */
|
|
283
302
|
private getNativeConfig = (): Record<string, unknown> | undefined => {
|
|
284
303
|
if (
|
|
@@ -369,6 +388,35 @@ export class Pushy {
|
|
|
369
388
|
});
|
|
370
389
|
};
|
|
371
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Tell the native cold-start orchestrator that this process already holds a
|
|
393
|
+
* valid check response for the current config, so its delayed round (5s
|
|
394
|
+
* after launch) does not repeat the request. Fire-and-forget: a failure
|
|
395
|
+
* costs one duplicate check. Never called on a failed check — the native
|
|
396
|
+
* rescue must keep running when JS could not reach the server.
|
|
397
|
+
*/
|
|
398
|
+
private markJsCheckCompleted = () => {
|
|
399
|
+
if (
|
|
400
|
+
Platform.OS === 'web' ||
|
|
401
|
+
typeof PushyModule.markJsCheckCompleted !== 'function'
|
|
402
|
+
) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const configJson = this.getNativeConfigJson();
|
|
406
|
+
if (!configJson) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
Promise.resolve(PushyModule.markJsCheckCompleted(configJson)).catch(
|
|
411
|
+
(e: any) => {
|
|
412
|
+
log('markJsCheckCompleted failed:', e?.message || e);
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
} catch (e: any) {
|
|
416
|
+
log('markJsCheckCompleted failed:', e?.message || e);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
372
420
|
private syncNativeConfig = () => {
|
|
373
421
|
if (
|
|
374
422
|
Platform.OS === 'web' ||
|
|
@@ -485,6 +533,115 @@ export class Pushy {
|
|
|
485
533
|
});
|
|
486
534
|
};
|
|
487
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Best-effort JS exception reporting for the currently running OTA release.
|
|
538
|
+
* It is intentionally synchronous to call and never throws; the transport
|
|
539
|
+
* runs in the background and preserves the app's existing error flow.
|
|
540
|
+
*/
|
|
541
|
+
captureException = (
|
|
542
|
+
error: unknown,
|
|
543
|
+
context: ErrorReportContext = {}
|
|
544
|
+
): void => {
|
|
545
|
+
try {
|
|
546
|
+
if (
|
|
547
|
+
__DEV__ ||
|
|
548
|
+
this.options.disableTelemetry ||
|
|
549
|
+
this.options.disableErrorReporting
|
|
550
|
+
) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (
|
|
554
|
+
error !== null &&
|
|
555
|
+
(typeof error === 'object' || typeof error === 'function')
|
|
556
|
+
) {
|
|
557
|
+
const object = error as object;
|
|
558
|
+
if (this.reportedExceptions.has(object)) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.reportedExceptions.add(object);
|
|
562
|
+
}
|
|
563
|
+
this.reportExceptionToServer(serializeException(error, context));
|
|
564
|
+
} catch (e: any) {
|
|
565
|
+
log('exception telemetry error:', e?.message || e);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Ensure uncaught JS exception capture is installed and return a runtime
|
|
571
|
+
* cleanup function. Capture is installed automatically by default; this
|
|
572
|
+
* method remains an idempotent compatibility hook. The wrapper always
|
|
573
|
+
* chains the handler that was installed before it and only restores that
|
|
574
|
+
* handler when no later integration has replaced ours.
|
|
575
|
+
*/
|
|
576
|
+
enableErrorReporting = (
|
|
577
|
+
options: ErrorReportingOptions = {}
|
|
578
|
+
): (() => void) => {
|
|
579
|
+
if (
|
|
580
|
+
options.captureGlobal === false ||
|
|
581
|
+
this.options.disableTelemetry ||
|
|
582
|
+
this.options.disableErrorReporting
|
|
583
|
+
) {
|
|
584
|
+
return () => {};
|
|
585
|
+
}
|
|
586
|
+
if (this.globalErrorCleanup) {
|
|
587
|
+
return this.globalErrorCleanup;
|
|
588
|
+
}
|
|
589
|
+
const uninstall = installGlobalErrorHandler((error, isFatal) => {
|
|
590
|
+
this.captureException(error, { fatal: isFatal === true });
|
|
591
|
+
});
|
|
592
|
+
let active = true;
|
|
593
|
+
const cleanup = () => {
|
|
594
|
+
if (!active) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
active = false;
|
|
598
|
+
uninstall();
|
|
599
|
+
if (this.globalErrorCleanup === cleanup) {
|
|
600
|
+
this.globalErrorCleanup = undefined;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
this.globalErrorCleanup = cleanup;
|
|
604
|
+
return cleanup;
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
private reportExceptionToServer = (error: SerializedException): void => {
|
|
608
|
+
try {
|
|
609
|
+
if (
|
|
610
|
+
__DEV__ ||
|
|
611
|
+
this.options.disableTelemetry ||
|
|
612
|
+
this.options.disableErrorReporting ||
|
|
613
|
+
!currentVersion
|
|
614
|
+
) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const { appKey } = this.options;
|
|
618
|
+
const endpoint =
|
|
619
|
+
this.lastWorkingEndpoint || this.options.server?.main?.[0];
|
|
620
|
+
if (!appKey || !endpoint) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
fetchWithTimeout(
|
|
624
|
+
`${endpoint}/report/${appKey}`,
|
|
625
|
+
{
|
|
626
|
+
method: 'POST',
|
|
627
|
+
headers: { 'Content-Type': 'application/json' },
|
|
628
|
+
body: JSON.stringify({
|
|
629
|
+
type: 'js_exception',
|
|
630
|
+
hash: currentVersion,
|
|
631
|
+
packageVersion: this.getEffectivePackageVersion(),
|
|
632
|
+
cInfo,
|
|
633
|
+
error,
|
|
634
|
+
}),
|
|
635
|
+
},
|
|
636
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
637
|
+
).catch((e: any) => {
|
|
638
|
+
log('exception telemetry report failed:', e?.message || e);
|
|
639
|
+
});
|
|
640
|
+
} catch (e: any) {
|
|
641
|
+
log('exception telemetry error:', e?.message || e);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
488
645
|
report = async ({
|
|
489
646
|
type,
|
|
490
647
|
message = '',
|
|
@@ -738,7 +895,17 @@ export class Pushy {
|
|
|
738
895
|
);
|
|
739
896
|
}
|
|
740
897
|
|
|
741
|
-
|
|
898
|
+
const result: unknown = await resp.json();
|
|
899
|
+
if (!isValidCheckResult(result)) {
|
|
900
|
+
// A 2xx that is not a verdict (`{"error": ...}`, a captive-portal page
|
|
901
|
+
// that parsed, ...) is a failed endpoint: throwing keeps the fallback
|
|
902
|
+
// moving to the next one instead of treating it as "no update".
|
|
903
|
+
throw new UpdateError(
|
|
904
|
+
this.t('error_invalid_check_response'),
|
|
905
|
+
'INVALID_RESPONSE'
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
return result;
|
|
742
909
|
};
|
|
743
910
|
fetchCheckResult = async (fetchPayload: Parameters<typeof fetch>[1]) => {
|
|
744
911
|
const { endpoint, value } = await executeEndpointFallback<CheckResult>({
|
|
@@ -927,10 +1094,33 @@ export class Pushy {
|
|
|
927
1094
|
// buildTime 启发式),下一次检查自然带上——绝不为它 await、拖慢或复杂化
|
|
928
1095
|
// 检查流程。
|
|
929
1096
|
const bundleHash = __DEV__ ? '' : getBundleHash();
|
|
1097
|
+
const fetchBody = buildCheckRequestBody({
|
|
1098
|
+
packageVersion: this.getEffectivePackageVersion(),
|
|
1099
|
+
currentVersion,
|
|
1100
|
+
buildTime,
|
|
1101
|
+
cInfo,
|
|
1102
|
+
supportedDiffVersion,
|
|
1103
|
+
bundleHash,
|
|
1104
|
+
isDev: __DEV__,
|
|
1105
|
+
extra,
|
|
1106
|
+
});
|
|
1107
|
+
const stringifyBody = JSON.stringify(fetchBody);
|
|
1108
|
+
// Identity of this check: only a check with the exact same request (same
|
|
1109
|
+
// extra / test hash / bundleHash / binary identity) against the same
|
|
1110
|
+
// endpoint set may share an in-flight or just-settled response. Two
|
|
1111
|
+
// checks 2s apart with different `extra` are different questions.
|
|
1112
|
+
const fingerprint = buildCheckFingerprint({
|
|
1113
|
+
appKey: this.options.appKey,
|
|
1114
|
+
endpoints: this.options.server?.main,
|
|
1115
|
+
queryUrls: this.options.server?.queryUrls,
|
|
1116
|
+
// The client uuid travels inside cInfo, i.e. inside `body`.
|
|
1117
|
+
body: stringifyBody,
|
|
1118
|
+
});
|
|
930
1119
|
const now = Date.now();
|
|
931
1120
|
if (
|
|
932
1121
|
this.lastRespJson &&
|
|
933
1122
|
this.lastChecking &&
|
|
1123
|
+
this.lastCheckFingerprint === fingerprint &&
|
|
934
1124
|
now - this.lastChecking < 1000 * 5
|
|
935
1125
|
) {
|
|
936
1126
|
try {
|
|
@@ -949,17 +1139,7 @@ export class Pushy {
|
|
|
949
1139
|
}
|
|
950
1140
|
}
|
|
951
1141
|
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);
|
|
1142
|
+
this.lastCheckFingerprint = fingerprint;
|
|
963
1143
|
// harmony fetch body is not string
|
|
964
1144
|
let body: any = fetchBody;
|
|
965
1145
|
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
@@ -973,7 +1153,6 @@ export class Pushy {
|
|
|
973
1153
|
},
|
|
974
1154
|
body,
|
|
975
1155
|
};
|
|
976
|
-
const previousRespJson = this.lastRespJson;
|
|
977
1156
|
try {
|
|
978
1157
|
this.report({
|
|
979
1158
|
type: 'checking',
|
|
@@ -1007,19 +1186,25 @@ export class Pushy {
|
|
|
1007
1186
|
}
|
|
1008
1187
|
|
|
1009
1188
|
this.notifyAfterCheckUpdate({ status: 'completed', result });
|
|
1189
|
+
this.markJsCheckCompleted();
|
|
1010
1190
|
return result;
|
|
1011
1191
|
} catch (e: any) {
|
|
1012
|
-
|
|
1192
|
+
// 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.lastRespJson = undefined;
|
|
1195
|
+
this.lastCheckFingerprint = undefined;
|
|
1013
1196
|
const err = toUpdateError(e, 'CHECK_FAILED');
|
|
1014
1197
|
this.emitError(err, 'errorChecking', {
|
|
1015
1198
|
message: err.message || this.t('error_cannot_connect_server'),
|
|
1016
1199
|
});
|
|
1017
1200
|
this.notifyAfterCheckUpdate({ status: 'error', error: err });
|
|
1018
1201
|
this.throwIfEnabled(err);
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1202
|
+
// No silent fallback to a previous successful response: a stale result
|
|
1203
|
+
// would let a server outage look like "still updatable" and drive the
|
|
1204
|
+
// provider's auto-download off it. Return undefined so callers can
|
|
1205
|
+
// distinguish "check failed" from a real empty result and keep their
|
|
1206
|
+
// last good updateInfo.
|
|
1207
|
+
return undefined;
|
|
1023
1208
|
}
|
|
1024
1209
|
};
|
|
1025
1210
|
downloadUpdate = async (
|
|
@@ -1077,6 +1262,28 @@ export class Pushy {
|
|
|
1077
1262
|
delete sharedState.downloadingTasks[hash];
|
|
1078
1263
|
}
|
|
1079
1264
|
};
|
|
1265
|
+
/**
|
|
1266
|
+
* Orders candidate mirrors for one artifact: the HEAD-race winner first
|
|
1267
|
+
* (its post-redirect URL), then the remaining mirrors in configured order.
|
|
1268
|
+
* Empty when there is nothing to try.
|
|
1269
|
+
*/
|
|
1270
|
+
private rankDownloadUrls = async (urls?: string[]): Promise<string[]> => {
|
|
1271
|
+
if (!urls?.length) {
|
|
1272
|
+
return [];
|
|
1273
|
+
}
|
|
1274
|
+
const winner = await testUrls(urls);
|
|
1275
|
+
const ordered: string[] = [];
|
|
1276
|
+
const push = (url?: string | null) => {
|
|
1277
|
+
if (url && !ordered.includes(url)) {
|
|
1278
|
+
ordered.push(url);
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
push(winner);
|
|
1282
|
+
for (const url of urls) {
|
|
1283
|
+
push(url);
|
|
1284
|
+
}
|
|
1285
|
+
return ordered;
|
|
1286
|
+
};
|
|
1080
1287
|
private performDownload = async (
|
|
1081
1288
|
updateInfo: CheckResult,
|
|
1082
1289
|
plan: DownloadPlan,
|
|
@@ -1103,7 +1310,13 @@ export class Pushy {
|
|
|
1103
1310
|
progress: computeProgress(data.received, data.total),
|
|
1104
1311
|
};
|
|
1105
1312
|
callbacks.forEach((callback) => {
|
|
1106
|
-
|
|
1313
|
+
// One subscriber throwing must not starve the others or abort the
|
|
1314
|
+
// download: business callbacks are isolated, the error is logged.
|
|
1315
|
+
try {
|
|
1316
|
+
callback(payload);
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
warn('onDownloadProgress callback threw', e);
|
|
1319
|
+
}
|
|
1107
1320
|
});
|
|
1108
1321
|
};
|
|
1109
1322
|
// RN >= 0.87 types native event listeners as `(...args: readonly Object[])`,
|
|
@@ -1164,7 +1377,12 @@ export class Pushy {
|
|
|
1164
1377
|
|
|
1165
1378
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1166
1379
|
if (attempt > 0) {
|
|
1167
|
-
|
|
1380
|
+
// Exponential backoff with jitter: a bad CDN edge otherwise gets
|
|
1381
|
+
// every client back at the same 1/2/4s marks.
|
|
1382
|
+
const backoffMs = Math.round(
|
|
1383
|
+
Math.min(1000 * 2 ** (attempt - 1), 10000) *
|
|
1384
|
+
(0.75 + Math.random() * 0.5)
|
|
1385
|
+
);
|
|
1168
1386
|
log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
|
|
1169
1387
|
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1170
1388
|
errorMessages.length = 0;
|
|
@@ -1186,28 +1404,39 @@ export class Pushy {
|
|
|
1186
1404
|
if (succeeded) {
|
|
1187
1405
|
break;
|
|
1188
1406
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1407
|
+
// The HEAD race only ranks mirrors; it is not proof that a mirror can
|
|
1408
|
+
// serve the whole artifact (CDNs answer HEAD and GET differently, and
|
|
1409
|
+
// a transfer can die mid-stream). Every mirror of the same artifact
|
|
1410
|
+
// gets a real download attempt before the strategy is abandoned —
|
|
1411
|
+
// the native cold-start path already behaves this way.
|
|
1412
|
+
const orderedUrls = await this.rankDownloadUrls(urls);
|
|
1413
|
+
for (const url of orderedUrls) {
|
|
1414
|
+
log(`downloading ${type} from ${url}`);
|
|
1415
|
+
try {
|
|
1416
|
+
await runners[type](url);
|
|
1417
|
+
succeeded = type;
|
|
1418
|
+
break;
|
|
1419
|
+
} catch (e: any) {
|
|
1420
|
+
const errorMessage = this.t(errorKeys[type], {
|
|
1421
|
+
message: e.message,
|
|
1422
|
+
});
|
|
1423
|
+
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
|
+
lastError = new UpdateError(
|
|
1428
|
+
errorMessage,
|
|
1429
|
+
asUpdateErrorCode(e?.code) ?? 'DOWNLOAD_FAILED',
|
|
1430
|
+
{ cause: e }
|
|
1431
|
+
);
|
|
1432
|
+
log(errorMessage);
|
|
1433
|
+
if (!isMirrorRetryableCode(lastError.code)) {
|
|
1434
|
+
// The bytes arrived but could not be applied (patch/manifest
|
|
1435
|
+
// mismatch): every mirror serves the same artifact, so move
|
|
1436
|
+
// on to the next strategy instead of re-downloading it.
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1211
1440
|
}
|
|
1212
1441
|
}
|
|
1213
1442
|
if (succeeded) {
|
|
@@ -1278,7 +1507,19 @@ export class Pushy {
|
|
|
1278
1507
|
if (sharedState.toHash === hash) {
|
|
1279
1508
|
hashInfo.debugChannel = true;
|
|
1280
1509
|
}
|
|
1281
|
-
|
|
1510
|
+
// The version is "downloaded" only once its metadata is persisted: a
|
|
1511
|
+
// lost hash-info write would leave the next launch with a nameless
|
|
1512
|
+
// version and a switchVersion that the native side may refuse.
|
|
1513
|
+
try {
|
|
1514
|
+
await setLocalHashInfo(hash, hashInfo);
|
|
1515
|
+
} catch (e: any) {
|
|
1516
|
+
const err = toUpdateError(e, 'FILE_OPERATION_FAILED');
|
|
1517
|
+
this.emitError(err, 'errorUpdate', {
|
|
1518
|
+
message: err.message,
|
|
1519
|
+
data: { newVersion: hash },
|
|
1520
|
+
});
|
|
1521
|
+
throw err;
|
|
1522
|
+
}
|
|
1282
1523
|
sharedState.downloadedHash = hash;
|
|
1283
1524
|
return hash;
|
|
1284
1525
|
};
|
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',
|