react-native-update 10.50.0 → 10.51.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/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
- package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
- package/README-CN.md +14 -1
- package/README.md +14 -1
- package/android/jni/Android.mk +5 -1
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/cpp/update_flow_core/flow_json.cpp +522 -0
- package/cpp/update_flow_core/flow_json.h +103 -0
- package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
- package/cpp/update_flow_core/update_flow_core.cpp +301 -0
- package/cpp/update_flow_core/update_flow_core.h +77 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
- package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
- package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +779 -61
- package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
- package/package.json +3 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +14 -0
- package/src/client.ts +341 -118
- package/src/endpoint.ts +7 -29
- package/src/provider.tsx +38 -2
- package/src/type.ts +26 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +1 -12
- package/src/isInRollout.ts +0 -87
- package/src/resolveCheckResult.ts +0 -30
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
@interface RCTPushyDownloader : NSObject
|
|
4
4
|
|
|
5
5
|
+ (void)download:(NSString *)downloadPath savePath:(NSString *)savePath
|
|
6
|
+
timeoutInterval:(NSTimeInterval)timeoutInterval
|
|
6
7
|
progressHandler:(void (^)(long long, long long))progressHandler
|
|
7
8
|
completionHandler:(void (^)(NSString *path, NSError *error))completionHandler;
|
|
8
9
|
|
|
@@ -17,6 +17,7 @@ static NSString *const RCTPushyDownloaderErrorDomain = @"cn.reactnative.pushy";
|
|
|
17
17
|
@implementation RCTPushyDownloader
|
|
18
18
|
|
|
19
19
|
+ (void)download:(NSString *)downloadPath savePath:(NSString *)savePath
|
|
20
|
+
timeoutInterval:(NSTimeInterval)timeoutInterval
|
|
20
21
|
progressHandler:(void (^)(long long receivedBytes, long long totalBytes))progressHandler
|
|
21
22
|
completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
22
23
|
{
|
|
@@ -28,10 +29,10 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
28
29
|
downloader.completionHandler = completionHandler;
|
|
29
30
|
downloader.savePath = savePath;
|
|
30
31
|
|
|
31
|
-
[downloader startDownload:downloadPath];
|
|
32
|
+
[downloader startDownload:downloadPath timeoutInterval:timeoutInterval];
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
- (void)startDownload:(NSString *)path
|
|
35
|
+
- (void)startDownload:(NSString *)path timeoutInterval:(NSTimeInterval)timeoutInterval
|
|
35
36
|
{
|
|
36
37
|
NSURL *url = [NSURL URLWithString:path];
|
|
37
38
|
if (url == nil) {
|
|
@@ -50,7 +51,7 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
50
51
|
// Android's 10min callTimeout — 300s made a 30MB full package on a slow
|
|
51
52
|
// (<100KB/s) network fail on iOS while succeeding on Android.
|
|
52
53
|
sessionConfig.timeoutIntervalForRequest = 30;
|
|
53
|
-
sessionConfig.timeoutIntervalForResource =
|
|
54
|
+
sessionConfig.timeoutIntervalForResource = MAX(1, timeoutInterval);
|
|
54
55
|
self.session = [NSURLSession sessionWithConfiguration:sessionConfig
|
|
55
56
|
delegate:self
|
|
56
57
|
delegateQueue:nil];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-update",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.51.0",
|
|
4
4
|
"description": "react-native hot update",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"submodule": "git submodule update --init --recursive",
|
|
15
15
|
"test": "bun test src/__tests__",
|
|
16
16
|
"test:patch-core": "./scripts/test-patch-core.sh",
|
|
17
|
+
"test:flow-core": "./scripts/test-update-flow-core.sh",
|
|
18
|
+
"generate:flow-vectors": "bun scripts/generate-flow-vectors.ts",
|
|
17
19
|
"build:harmony-har": "node scripts/build-harmony-har.js",
|
|
18
20
|
"build:harmony-e2e": "bash Example/e2etest/scripts/build-harmony-e2e.sh",
|
|
19
21
|
"build:so": "bun submodule && bash scripts/build-android-so.sh",
|
|
@@ -110,6 +110,8 @@ Pod::Spec.new do |s|
|
|
|
110
110
|
|
|
111
111
|
s.subspec 'RCTPushy' do |ss|
|
|
112
112
|
ss.source_files = ['ios/RCTPushy/*.{h,m,mm}',
|
|
113
|
+
'cpp/update_flow_core/flow_json.{h,cpp}',
|
|
114
|
+
'cpp/update_flow_core/update_flow_core.{h,cpp}',
|
|
113
115
|
'cpp/patch_core/archive_patch_core.{h,cpp}',
|
|
114
116
|
'cpp/patch_core/digest.{h,cpp}',
|
|
115
117
|
'cpp/patch_core/hbc_transform.{h,cpp}',
|
package/src/NativePushy.ts
CHANGED
|
@@ -16,6 +16,20 @@ export interface Spec extends TurboModule {
|
|
|
16
16
|
setLocalHashInfo(hash: string, info: string): Promise<void>;
|
|
17
17
|
getLocalHashInfo(hash: string): Promise<string>;
|
|
18
18
|
setUuid(uuid: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Persist the config subset the native cold-start update check consumes
|
|
21
|
+
* (appKey, endpoints, afterDownload policy; NATIVE_CHECKUPDATE_DESIGN
|
|
22
|
+
* §10.1). Stored as a raw JSON string, parsed natively on read. JS is the
|
|
23
|
+
* single config source — a native side without persisted config silently
|
|
24
|
+
* skips its check, which doubles as the feature's rollout gate.
|
|
25
|
+
*/
|
|
26
|
+
syncNativeConfig(config: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Raw response cached by the native cold-start check, including the request
|
|
29
|
+
* and config fingerprints that scope reuse (§10.3). Resolves to
|
|
30
|
+
* an empty string when absent; never rejects.
|
|
31
|
+
*/
|
|
32
|
+
getNativeCheckCache(): Promise<string>;
|
|
19
33
|
reloadUpdate(options: { hash: string }): Promise<void>;
|
|
20
34
|
restartApp(): Promise<void>;
|
|
21
35
|
setNeedUpdate(options: { hash: string }): Promise<void>;
|
package/src/client.ts
CHANGED
|
@@ -41,13 +41,18 @@ import type {
|
|
|
41
41
|
UpdateCheckState,
|
|
42
42
|
UpdateServerConfig,
|
|
43
43
|
} from './type';
|
|
44
|
+
import {
|
|
45
|
+
buildCheckRequestBody,
|
|
46
|
+
type DownloadPlan,
|
|
47
|
+
type DownloadStrategyType,
|
|
48
|
+
decideDownload,
|
|
49
|
+
} from './updateFlowCore';
|
|
44
50
|
import {
|
|
45
51
|
assertWeb,
|
|
46
52
|
computeProgress,
|
|
47
53
|
DEFAULT_FETCH_TIMEOUT_MS,
|
|
48
54
|
fetchWithTimeout,
|
|
49
55
|
info,
|
|
50
|
-
joinUrls,
|
|
51
56
|
log,
|
|
52
57
|
noop,
|
|
53
58
|
promiseAny,
|
|
@@ -91,6 +96,11 @@ const cloneServerConfig = (server: UpdateServerConfig): UpdateServerConfig => ({
|
|
|
91
96
|
queryUrls: server.queryUrls ? [...server.queryUrls] : undefined,
|
|
92
97
|
});
|
|
93
98
|
|
|
99
|
+
// Persist an object (rather than an empty string) so every native bridge keeps
|
|
100
|
+
// accepting the payload while the orchestrators read `disabled` and skip the
|
|
101
|
+
// cold-start check entirely.
|
|
102
|
+
const NATIVE_CONFIG_DISABLED_JSON = '{"disabled":true}';
|
|
103
|
+
|
|
94
104
|
const excludeConfiguredEndpoints = (
|
|
95
105
|
endpoints: string[],
|
|
96
106
|
configuredEndpoints: string[]
|
|
@@ -161,6 +171,10 @@ export class Pushy {
|
|
|
161
171
|
// Endpoint that most recently served a successful checkUpdate; telemetry
|
|
162
172
|
// reuses it instead of re-running the fallback race.
|
|
163
173
|
private lastWorkingEndpoint?: string;
|
|
174
|
+
private syncedNativeConfigJson?: string;
|
|
175
|
+
private pendingNativeConfigJson?: string;
|
|
176
|
+
private nativeConfigSyncInFlight = false;
|
|
177
|
+
private reportedInvalidUpdates = new Set<string>();
|
|
164
178
|
|
|
165
179
|
version = cInfo.rnu;
|
|
166
180
|
loggerPromise = (() => {
|
|
@@ -259,6 +273,164 @@ export class Pushy {
|
|
|
259
273
|
log('onOptionsChange listener error:', e?.message || e);
|
|
260
274
|
}
|
|
261
275
|
}
|
|
276
|
+
this.syncNativeConfig();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** Build the subset of options used by the native cold-start check. */
|
|
280
|
+
private getNativeConfig = (): Record<string, unknown> | undefined => {
|
|
281
|
+
if (
|
|
282
|
+
Platform.OS === 'web' ||
|
|
283
|
+
typeof PushyModule.syncNativeConfig !== 'function'
|
|
284
|
+
) {
|
|
285
|
+
// Older natives lack the method; on web PushyModule is a noop Proxy
|
|
286
|
+
// and the feature-detect would false-positive.
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const {
|
|
290
|
+
appKey,
|
|
291
|
+
server,
|
|
292
|
+
updateStrategy,
|
|
293
|
+
checkStrategy,
|
|
294
|
+
disableNativeCheck,
|
|
295
|
+
} = this.options;
|
|
296
|
+
if (disableNativeCheck) {
|
|
297
|
+
// Explicit opt-out: keep the identity fields so the persisted config
|
|
298
|
+
// still describes this app, and let the orchestrators bail on `disabled`
|
|
299
|
+
// before any IO.
|
|
300
|
+
return { disabled: true, appKey, rnu: cInfo.rnu, rn: cInfo.rn };
|
|
301
|
+
}
|
|
302
|
+
if (!appKey || !server?.main?.length) {
|
|
303
|
+
// Unusable rather than merely absent: the JS check would fail on these
|
|
304
|
+
// options too (NO_ENDPOINTS / APPKEY_REQUIRED). The caller turns this
|
|
305
|
+
// into an explicit disabled state.
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
// An app that turned automatic checks off (checkStrategy: null) must not
|
|
309
|
+
// be handed a version switch it never asked for. The cold-start check
|
|
310
|
+
// still runs and still downloads — that is what keeps a bricked device
|
|
311
|
+
// rescuable — but activation waits for the JS side, or for the server's
|
|
312
|
+
// explicit per-version forceBoot directive (shouldActivateAfterDownload).
|
|
313
|
+
const autoCheckEnabled = checkStrategy != null;
|
|
314
|
+
return {
|
|
315
|
+
appKey,
|
|
316
|
+
packageVersion: this.getEffectivePackageVersion(),
|
|
317
|
+
endpoints: server.main,
|
|
318
|
+
queryUrls: server.queryUrls ?? [],
|
|
319
|
+
// The native check may activate a downloaded version (next launch)
|
|
320
|
+
// only under the silent strategies; alert-style strategies keep
|
|
321
|
+
// activation with the JS side (§6/§10.1).
|
|
322
|
+
afterDownload:
|
|
323
|
+
autoCheckEnabled &&
|
|
324
|
+
(updateStrategy === 'silentAndNow' ||
|
|
325
|
+
updateStrategy === 'silentAndLater')
|
|
326
|
+
? 'setNeedUpdate'
|
|
327
|
+
: 'none',
|
|
328
|
+
rnu: cInfo.rnu,
|
|
329
|
+
rn: cInfo.rn,
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
private getNativeConfigJson = (): string | undefined => {
|
|
334
|
+
const config = this.getNativeConfig();
|
|
335
|
+
return config ? JSON.stringify(config) : undefined;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
private flushNativeConfig = () => {
|
|
339
|
+
if (this.nativeConfigSyncInFlight) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const configJson = this.pendingNativeConfigJson;
|
|
343
|
+
this.pendingNativeConfigJson = undefined;
|
|
344
|
+
if (!configJson || configJson === this.syncedNativeConfigJson) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.nativeConfigSyncInFlight = true;
|
|
348
|
+
let syncResult: Promise<void>;
|
|
349
|
+
try {
|
|
350
|
+
syncResult = Promise.resolve(PushyModule.syncNativeConfig(configJson));
|
|
351
|
+
} catch (e: any) {
|
|
352
|
+
this.nativeConfigSyncInFlight = false;
|
|
353
|
+
log('syncNativeConfig failed:', e?.message || e);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
syncResult
|
|
357
|
+
.then(() => {
|
|
358
|
+
this.syncedNativeConfigJson = configJson;
|
|
359
|
+
})
|
|
360
|
+
.catch((e: any) => {
|
|
361
|
+
log('syncNativeConfig failed:', e?.message || e);
|
|
362
|
+
})
|
|
363
|
+
.finally(() => {
|
|
364
|
+
this.nativeConfigSyncInFlight = false;
|
|
365
|
+
this.flushNativeConfig();
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
private syncNativeConfig = () => {
|
|
370
|
+
if (
|
|
371
|
+
Platform.OS === 'web' ||
|
|
372
|
+
typeof PushyModule.syncNativeConfig !== 'function'
|
|
373
|
+
) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const configJson =
|
|
377
|
+
this.getNativeConfigJson() ?? NATIVE_CONFIG_DISABLED_JSON;
|
|
378
|
+
// Always record the latest desired value, even when it matches the last
|
|
379
|
+
// completed write. Example: A synced -> B in flight -> options revert to
|
|
380
|
+
// A. Comparing only with synced(A) would drop the revert and leave native
|
|
381
|
+
// storage at B after that in-flight write completes.
|
|
382
|
+
// Coalesce rapid setOptions calls, but serialize bridge writes so an older
|
|
383
|
+
// completion can never overwrite the newest desired configuration.
|
|
384
|
+
this.pendingNativeConfigJson = configJson;
|
|
385
|
+
this.flushNativeConfig();
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/** Package version used by every server-side update decision. */
|
|
389
|
+
getEffectivePackageVersion = () =>
|
|
390
|
+
this.options.overridePackageVersion || packageVersion;
|
|
391
|
+
|
|
392
|
+
private jsonValuesEqual = (left: unknown, right: unknown): boolean => {
|
|
393
|
+
const compare = (a: unknown, b: unknown): boolean => {
|
|
394
|
+
if (a === b) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
398
|
+
return (
|
|
399
|
+
Array.isArray(a) &&
|
|
400
|
+
Array.isArray(b) &&
|
|
401
|
+
a.length === b.length &&
|
|
402
|
+
a.every((value, index) => compare(value, b[index]))
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (
|
|
406
|
+
a === null ||
|
|
407
|
+
b === null ||
|
|
408
|
+
typeof a !== 'object' ||
|
|
409
|
+
typeof b !== 'object'
|
|
410
|
+
) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
const leftObject = a as Record<string, unknown>;
|
|
414
|
+
const rightObject = b as Record<string, unknown>;
|
|
415
|
+
// Match JSON.stringify semantics for request extras: object properties
|
|
416
|
+
// whose value is undefined are omitted from the wire fingerprint.
|
|
417
|
+
const leftKeys = Object.keys(leftObject)
|
|
418
|
+
.filter((key) => leftObject[key] !== undefined)
|
|
419
|
+
.sort();
|
|
420
|
+
const rightKeys = Object.keys(rightObject)
|
|
421
|
+
.filter((key) => rightObject[key] !== undefined)
|
|
422
|
+
.sort();
|
|
423
|
+
return (
|
|
424
|
+
leftKeys.length === rightKeys.length &&
|
|
425
|
+
leftKeys.every(
|
|
426
|
+
(key, index) =>
|
|
427
|
+
key === rightKeys[index] &&
|
|
428
|
+
compare(leftObject[key], rightObject[key])
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
return compare(left, right);
|
|
262
434
|
};
|
|
263
435
|
|
|
264
436
|
private providerMounted = false;
|
|
@@ -291,6 +463,25 @@ export class Pushy {
|
|
|
291
463
|
return i18n.t(key as any, values);
|
|
292
464
|
};
|
|
293
465
|
|
|
466
|
+
reportInvalidUpdateOnce = (
|
|
467
|
+
reason: 'missingHash' | 'noArtifact',
|
|
468
|
+
hash = ''
|
|
469
|
+
) => {
|
|
470
|
+
const key = `${this.options.appKey}:${reason}:${hash}`;
|
|
471
|
+
if (this.reportedInvalidUpdates.has(key)) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.reportedInvalidUpdates.add(key);
|
|
475
|
+
this.report({
|
|
476
|
+
type: 'errorUpdate',
|
|
477
|
+
message:
|
|
478
|
+
reason === 'missingHash'
|
|
479
|
+
? 'update response is missing a version hash'
|
|
480
|
+
: 'update response contains no downloadable artifact',
|
|
481
|
+
...(hash ? { data: { newVersion: hash } } : {}),
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
|
|
294
485
|
report = async ({
|
|
295
486
|
type,
|
|
296
487
|
message = '',
|
|
@@ -385,8 +576,7 @@ export class Pushy {
|
|
|
385
576
|
body: JSON.stringify({
|
|
386
577
|
type: payloadType,
|
|
387
578
|
hash,
|
|
388
|
-
packageVersion:
|
|
389
|
-
this.options.overridePackageVersion || packageVersion,
|
|
579
|
+
packageVersion: this.getEffectivePackageVersion(),
|
|
390
580
|
cInfo,
|
|
391
581
|
detail: truncateDetail(detail),
|
|
392
582
|
}),
|
|
@@ -577,6 +767,57 @@ export class Pushy {
|
|
|
577
767
|
this.lastWorkingEndpoint = endpoint;
|
|
578
768
|
return value;
|
|
579
769
|
};
|
|
770
|
+
/**
|
|
771
|
+
* Reuse the native cold-start check's cached response when fresh
|
|
772
|
+
* (NATIVE_CHECKUPDATE_DESIGN §10.3) instead of re-checking. Returns
|
|
773
|
+
* undefined whenever the cache is absent, stale, or unreadable — any
|
|
774
|
+
* failure falls through to a normal network check.
|
|
775
|
+
*/
|
|
776
|
+
private readNativeCheckCache = async (
|
|
777
|
+
requestBody: Record<string, any>
|
|
778
|
+
): Promise<CheckResult | undefined> => {
|
|
779
|
+
try {
|
|
780
|
+
if (__DEV__ || typeof PushyModule.getNativeCheckCache !== 'function') {
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
783
|
+
const raw = await Promise.resolve(PushyModule.getNativeCheckCache());
|
|
784
|
+
if (!raw || typeof raw !== 'string') {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
const entry = JSON.parse(raw);
|
|
788
|
+
const config = this.getNativeConfig();
|
|
789
|
+
const cachedRequest =
|
|
790
|
+
typeof entry?.request === 'string'
|
|
791
|
+
? JSON.parse(entry.request)
|
|
792
|
+
: undefined;
|
|
793
|
+
const cachedConfig =
|
|
794
|
+
typeof entry?.config === 'string'
|
|
795
|
+
? JSON.parse(entry.config)
|
|
796
|
+
: undefined;
|
|
797
|
+
if (
|
|
798
|
+
typeof entry?.ts !== 'number' ||
|
|
799
|
+
typeof entry?.body !== 'string' ||
|
|
800
|
+
!this.jsonValuesEqual(cachedRequest, requestBody) ||
|
|
801
|
+
!config ||
|
|
802
|
+
!this.jsonValuesEqual(cachedConfig, config)
|
|
803
|
+
) {
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
const ageSeconds = Date.now() / 1000 - entry.ts;
|
|
807
|
+
if (ageSeconds < 0 || ageSeconds > 120) {
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
const result = JSON.parse(entry.body);
|
|
811
|
+
if (!result || typeof result !== 'object') {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
log('reusing native check response cache');
|
|
815
|
+
return result as CheckResult;
|
|
816
|
+
} catch {
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
580
821
|
assertDebug = (matter: string) => {
|
|
581
822
|
if (__DEV__ && !this.options.debug) {
|
|
582
823
|
info(this.t('dev_debug_disabled', { matter }));
|
|
@@ -697,19 +938,16 @@ export class Pushy {
|
|
|
697
938
|
}
|
|
698
939
|
}
|
|
699
940
|
this.lastChecking = now;
|
|
700
|
-
const fetchBody
|
|
701
|
-
packageVersion: this.
|
|
702
|
-
|
|
941
|
+
const fetchBody = buildCheckRequestBody({
|
|
942
|
+
packageVersion: this.getEffectivePackageVersion(),
|
|
943
|
+
currentVersion,
|
|
703
944
|
buildTime,
|
|
704
945
|
cInfo,
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
};
|
|
710
|
-
if (__DEV__) {
|
|
711
|
-
delete fetchBody.buildTime;
|
|
712
|
-
}
|
|
946
|
+
supportedDiffVersion,
|
|
947
|
+
bundleHash,
|
|
948
|
+
isDev: __DEV__,
|
|
949
|
+
extra,
|
|
950
|
+
});
|
|
713
951
|
const stringifyBody = JSON.stringify(fetchBody);
|
|
714
952
|
// harmony fetch body is not string
|
|
715
953
|
let body: any = fetchBody;
|
|
@@ -730,7 +968,18 @@ export class Pushy {
|
|
|
730
968
|
type: 'checking',
|
|
731
969
|
message: `${this.options.appKey}: ${stringifyBody}`,
|
|
732
970
|
});
|
|
733
|
-
|
|
971
|
+
// The native cold-start check may have a fresh response on disk
|
|
972
|
+
// (§10.3); reuse it instead of re-checking. The read happens INSIDE
|
|
973
|
+
// the promise so no await lands between the dedup window above and the
|
|
974
|
+
// lastRespJson assignment below (the JS2-1 double-send lesson).
|
|
975
|
+
const respJsonPromise = (async (): Promise<CheckResult> => {
|
|
976
|
+
// While bundleHash prefetch is still pending, the JS request omits
|
|
977
|
+
// that key whereas the native request always includes its synchronously
|
|
978
|
+
// computed value. That narrow first-launch window intentionally misses
|
|
979
|
+
// the cache rather than delaying checkUpdate for hashing.
|
|
980
|
+
const cached = await this.readNativeCheckCache(fetchBody);
|
|
981
|
+
return cached ?? (await this.fetchCheckResult(fetchPayload));
|
|
982
|
+
})();
|
|
734
983
|
this.lastRespJson = respJsonPromise;
|
|
735
984
|
const result: CheckResult = await respJsonPromise;
|
|
736
985
|
|
|
@@ -766,7 +1015,6 @@ export class Pushy {
|
|
|
766
1015
|
updateInfo: CheckResult,
|
|
767
1016
|
onDownloadProgress?: (data: ProgressData) => void
|
|
768
1017
|
) => {
|
|
769
|
-
const { hash } = updateInfo;
|
|
770
1018
|
if (
|
|
771
1019
|
this.options.beforeDownloadUpdate &&
|
|
772
1020
|
(await this.options.beforeDownloadUpdate(updateInfo)) === false
|
|
@@ -774,17 +1022,26 @@ export class Pushy {
|
|
|
774
1022
|
log('beforeDownloadUpdate returned false, skipping download');
|
|
775
1023
|
return;
|
|
776
1024
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1025
|
+
const decision = decideDownload(
|
|
1026
|
+
updateInfo,
|
|
1027
|
+
{ currentVersion, rolledBackVersion },
|
|
1028
|
+
__DEV__
|
|
1029
|
+
);
|
|
1030
|
+
if (decision.action === 'none') {
|
|
1031
|
+
if (decision.reason === 'alreadyCurrent') {
|
|
1032
|
+
log(`current hash ${currentVersion}, ignored`);
|
|
1033
|
+
} else if (decision.reason === 'rolledBack') {
|
|
1034
|
+
log(`rolledback hash ${rolledBackVersion}, ignored`);
|
|
1035
|
+
} else if (decision.reason === 'noArtifact') {
|
|
1036
|
+
// A server response that advertises an update but provides no usable
|
|
1037
|
+
// artifact is a bad release signal, not an ordinary no-update result.
|
|
1038
|
+
// Keep the user flow silent and report at most once per bad release in
|
|
1039
|
+
// this process: repeated checks must not inflate download_fail health.
|
|
1040
|
+
this.reportInvalidUpdateOnce('noArtifact', updateInfo.hash || '');
|
|
1041
|
+
}
|
|
786
1042
|
return;
|
|
787
1043
|
}
|
|
1044
|
+
const { hash } = decision;
|
|
788
1045
|
if (sharedState.downloadedHash === hash) {
|
|
789
1046
|
log(`duplicated downloaded hash ${sharedState.downloadedHash}, ignored`);
|
|
790
1047
|
return sharedState.downloadedHash;
|
|
@@ -801,7 +1058,7 @@ export class Pushy {
|
|
|
801
1058
|
}
|
|
802
1059
|
return existingTask;
|
|
803
1060
|
}
|
|
804
|
-
const task = this.performDownload(updateInfo, onDownloadProgress);
|
|
1061
|
+
const task = this.performDownload(updateInfo, decision, onDownloadProgress);
|
|
805
1062
|
sharedState.downloadingTasks[hash] = task;
|
|
806
1063
|
try {
|
|
807
1064
|
return await task;
|
|
@@ -811,21 +1068,11 @@ export class Pushy {
|
|
|
811
1068
|
};
|
|
812
1069
|
private performDownload = async (
|
|
813
1070
|
updateInfo: CheckResult,
|
|
1071
|
+
plan: DownloadPlan,
|
|
814
1072
|
onDownloadProgress?: (data: ProgressData) => void
|
|
815
1073
|
) => {
|
|
816
|
-
const {
|
|
817
|
-
|
|
818
|
-
diff,
|
|
819
|
-
pdiff,
|
|
820
|
-
full,
|
|
821
|
-
paths = [],
|
|
822
|
-
name,
|
|
823
|
-
description = '',
|
|
824
|
-
metaInfo,
|
|
825
|
-
} = updateInfo;
|
|
826
|
-
if (!hash) {
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
1074
|
+
const { name, description = '', metaInfo } = updateInfo;
|
|
1075
|
+
const { hash, attempts, devNoop } = plan;
|
|
829
1076
|
const patchStartTime = Date.now();
|
|
830
1077
|
// One native listener per hash dispatching to a callback set, so
|
|
831
1078
|
// concurrent callers deduped onto this task can each observe progress
|
|
@@ -870,61 +1117,35 @@ export class Pushy {
|
|
|
870
1117
|
let lastError: any;
|
|
871
1118
|
const errorMessages: string[] = [];
|
|
872
1119
|
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
// download
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1120
|
+
// The ordered attempts come from decideDownload (the pure decision layer);
|
|
1121
|
+
// this side only executes them: probe candidate URLs, run the matching
|
|
1122
|
+
// native download, fall through to the next attempt on failure.
|
|
1123
|
+
const runners: Record<
|
|
1124
|
+
DownloadStrategyType,
|
|
1125
|
+
(url: string) => Promise<void>
|
|
1126
|
+
> = {
|
|
1127
|
+
diff: (url) =>
|
|
1128
|
+
PushyModule.downloadPatchFromPpk({
|
|
1129
|
+
updateUrl: url,
|
|
1130
|
+
hash,
|
|
1131
|
+
originHash: currentVersion,
|
|
1132
|
+
}),
|
|
1133
|
+
pdiff: (url) =>
|
|
1134
|
+
PushyModule.downloadPatchFromPackage({
|
|
1135
|
+
updateUrl: url,
|
|
1136
|
+
hash,
|
|
1137
|
+
}),
|
|
1138
|
+
full: (url) =>
|
|
1139
|
+
PushyModule.downloadFullUpdate({
|
|
1140
|
+
updateUrl: url,
|
|
1141
|
+
hash,
|
|
1142
|
+
}),
|
|
888
1143
|
};
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
skipInDev: true,
|
|
895
|
-
devNoopWhenNoUrl: false,
|
|
896
|
-
run: (url) =>
|
|
897
|
-
PushyModule.downloadPatchFromPpk({
|
|
898
|
-
updateUrl: url,
|
|
899
|
-
hash,
|
|
900
|
-
originHash: currentVersion,
|
|
901
|
-
}),
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
name: 'pdiff',
|
|
905
|
-
candidate: pdiff,
|
|
906
|
-
errorKey: 'error_pdiff_failed',
|
|
907
|
-
skipInDev: true,
|
|
908
|
-
devNoopWhenNoUrl: false,
|
|
909
|
-
run: (url) =>
|
|
910
|
-
PushyModule.downloadPatchFromPackage({
|
|
911
|
-
updateUrl: url,
|
|
912
|
-
hash,
|
|
913
|
-
}),
|
|
914
|
-
},
|
|
915
|
-
{
|
|
916
|
-
name: 'full',
|
|
917
|
-
candidate: full,
|
|
918
|
-
errorKey: 'error_full_patch_failed',
|
|
919
|
-
skipInDev: false,
|
|
920
|
-
devNoopWhenNoUrl: true,
|
|
921
|
-
run: (url) =>
|
|
922
|
-
PushyModule.downloadFullUpdate({
|
|
923
|
-
updateUrl: url,
|
|
924
|
-
hash,
|
|
925
|
-
}),
|
|
926
|
-
},
|
|
927
|
-
];
|
|
1144
|
+
const errorKeys = {
|
|
1145
|
+
diff: 'error_diff_failed',
|
|
1146
|
+
pdiff: 'error_pdiff_failed',
|
|
1147
|
+
full: 'error_full_patch_failed',
|
|
1148
|
+
} as const;
|
|
928
1149
|
|
|
929
1150
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
930
1151
|
if (attempt > 0) {
|
|
@@ -942,34 +1163,36 @@ export class Pushy {
|
|
|
942
1163
|
attempt,
|
|
943
1164
|
},
|
|
944
1165
|
});
|
|
945
|
-
|
|
1166
|
+
if (devNoop) {
|
|
1167
|
+
log(this.t('dev_incremental_update_disabled'));
|
|
1168
|
+
succeeded = 'full';
|
|
1169
|
+
}
|
|
1170
|
+
for (const { type, urls } of attempts) {
|
|
946
1171
|
if (succeeded) {
|
|
947
1172
|
break;
|
|
948
1173
|
}
|
|
949
|
-
const url = await testUrls(
|
|
950
|
-
if (url
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
)
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
log(this.t('dev_incremental_update_disabled'));
|
|
972
|
-
succeeded = strategy.name;
|
|
1174
|
+
const url = await testUrls(urls);
|
|
1175
|
+
if (!url) {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
log(`downloading ${type}`);
|
|
1179
|
+
try {
|
|
1180
|
+
await runners[type](url);
|
|
1181
|
+
succeeded = type;
|
|
1182
|
+
} catch (e: any) {
|
|
1183
|
+
const errorMessage = this.t(errorKeys[type], {
|
|
1184
|
+
message: e.message,
|
|
1185
|
+
});
|
|
1186
|
+
errorMessages.push(errorMessage);
|
|
1187
|
+
// Keep the i18n message for display, but preserve the native
|
|
1188
|
+
// rejection's stable code (e.g. PATCH_FAILED vs DOWNLOAD_FAILED —
|
|
1189
|
+
// telemetry classifies on it) and the original error as cause.
|
|
1190
|
+
lastError = new UpdateError(
|
|
1191
|
+
errorMessage,
|
|
1192
|
+
asUpdateErrorCode(e?.code) ?? 'DOWNLOAD_FAILED',
|
|
1193
|
+
{ cause: e }
|
|
1194
|
+
);
|
|
1195
|
+
log(errorMessage);
|
|
973
1196
|
}
|
|
974
1197
|
}
|
|
975
1198
|
if (succeeded) {
|