react-native-update 10.49.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/BUNDLEHASH_MIGRATION.md +22 -2
- 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/BundledResourceCopier.java +42 -9
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
- 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/PatchFailedException.java +16 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
- 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/patch_core/digest.cpp +53 -0
- package/cpp/patch_core/digest.h +18 -0
- package/cpp/patch_core/patch_core.cpp +17 -1
- package/cpp/patch_core/patch_core.h +8 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +99 -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 +150 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -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 +16 -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 +152 -52
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +840 -68
- 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 +439 -122
- package/src/endpoint.ts +105 -80
- package/src/error.ts +4 -0
- package/src/locales/en.ts +4 -0
- package/src/locales/zh.ts +4 -0
- package/src/provider.tsx +74 -9
- package/src/telemetry.ts +8 -0
- package/src/type.ts +31 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +14 -12
- package/src/isInRollout.ts +0 -86
- package/src/resolveCheckResult.ts +0 -30
package/src/endpoint.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { UpdateError } from './error';
|
|
2
|
+
import { dedupeEndpoints, orderEndpointCandidates } from './updateFlowCore';
|
|
3
|
+
|
|
4
|
+
export { dedupeEndpoints };
|
|
2
5
|
|
|
3
6
|
export interface EndpointAttemptSuccess<T> {
|
|
4
7
|
endpoint: string;
|
|
@@ -14,9 +17,10 @@ export interface EndpointAttemptFailure {
|
|
|
14
17
|
export interface ExecuteEndpointFallbackOptions<T> {
|
|
15
18
|
configuredEndpoints: string[];
|
|
16
19
|
getRemoteEndpoints?: () => Promise<string[]>;
|
|
17
|
-
tryEndpoint: (endpoint: string) => Promise<T>;
|
|
20
|
+
tryEndpoint: (endpoint: string, signal?: AbortSignal) => Promise<T>;
|
|
18
21
|
random?: () => number;
|
|
19
22
|
now?: () => number;
|
|
23
|
+
hedgeDelayMs?: number;
|
|
20
24
|
onFirstFailure?: (failure: EndpointAttemptFailure) => void | Promise<void>;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -27,87 +31,104 @@ const normalizeError = (error: unknown) => {
|
|
|
27
31
|
return new Error(String(error));
|
|
28
32
|
};
|
|
29
33
|
|
|
30
|
-
export const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
result.push(endpoint);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return result;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const pickRandomEndpoint = (
|
|
34
|
+
export const DEFAULT_HEDGE_DELAY_MS = 250;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hedged race over the candidate endpoints: the first candidate starts
|
|
38
|
+
* immediately, each following one is only started after `hedgeDelayMs` of
|
|
39
|
+
* silence (or immediately when a previous attempt failed). The first
|
|
40
|
+
* successful response wins and every other in-flight request is aborted, so
|
|
41
|
+
* one slow endpoint never gates a fast one, and healthy rounds send a single
|
|
42
|
+
* request instead of blasting every candidate at once.
|
|
43
|
+
*/
|
|
44
|
+
export function selectFastestSuccessfulEndpoint<T>(
|
|
48
45
|
endpoints: string[],
|
|
49
|
-
|
|
50
|
-
) =>
|
|
51
|
-
|
|
52
|
-
throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
|
|
53
|
-
}
|
|
54
|
-
return endpoints[Math.floor(random() * endpoints.length)];
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export async function selectFastestSuccessfulEndpoint<T>(
|
|
58
|
-
endpoints: string[],
|
|
59
|
-
tryEndpoint: (endpoint: string) => Promise<T>,
|
|
60
|
-
now: () => number = Date.now
|
|
46
|
+
tryEndpoint: (endpoint: string, signal?: AbortSignal) => Promise<T>,
|
|
47
|
+
now: () => number = Date.now,
|
|
48
|
+
hedgeDelayMs: number = DEFAULT_HEDGE_DELAY_MS
|
|
61
49
|
): Promise<{
|
|
62
|
-
|
|
50
|
+
success?: EndpointAttemptSuccess<T>;
|
|
63
51
|
failures: EndpointAttemptFailure[];
|
|
64
52
|
}> {
|
|
65
|
-
|
|
66
|
-
endpoints.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const value = await tryEndpoint(endpoint);
|
|
70
|
-
return {
|
|
71
|
-
ok: true as const,
|
|
72
|
-
endpoint,
|
|
73
|
-
value,
|
|
74
|
-
duration: now() - start,
|
|
75
|
-
};
|
|
76
|
-
} catch (error) {
|
|
77
|
-
return {
|
|
78
|
-
ok: false as const,
|
|
79
|
-
endpoint,
|
|
80
|
-
error: normalizeError(error),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const successes: EndpointAttemptSuccess<T>[] = [];
|
|
87
|
-
const failures: EndpointAttemptFailure[] = [];
|
|
88
|
-
|
|
89
|
-
for (const attempt of attempts) {
|
|
90
|
-
if (attempt.ok) {
|
|
91
|
-
successes.push({
|
|
92
|
-
endpoint: attempt.endpoint,
|
|
93
|
-
value: attempt.value,
|
|
94
|
-
duration: attempt.duration,
|
|
95
|
-
});
|
|
96
|
-
continue;
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
if (!endpoints.length) {
|
|
55
|
+
resolve({ failures: [] });
|
|
56
|
+
return;
|
|
97
57
|
}
|
|
98
58
|
|
|
99
|
-
failures
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
59
|
+
const failures: EndpointAttemptFailure[] = [];
|
|
60
|
+
const controllers: AbortController[] = [];
|
|
61
|
+
let nextIndex = 0;
|
|
62
|
+
let pending = 0;
|
|
63
|
+
let settled = false;
|
|
64
|
+
let hedgeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
65
|
+
|
|
66
|
+
const finish = (success?: EndpointAttemptSuccess<T>) => {
|
|
67
|
+
if (settled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
settled = true;
|
|
71
|
+
if (hedgeTimer) {
|
|
72
|
+
clearTimeout(hedgeTimer);
|
|
73
|
+
}
|
|
74
|
+
for (const controller of controllers) {
|
|
75
|
+
controller.abort();
|
|
76
|
+
}
|
|
77
|
+
resolve({ success, failures });
|
|
78
|
+
};
|
|
104
79
|
|
|
105
|
-
|
|
80
|
+
const launchNext = () => {
|
|
81
|
+
if (hedgeTimer) {
|
|
82
|
+
clearTimeout(hedgeTimer);
|
|
83
|
+
hedgeTimer = undefined;
|
|
84
|
+
}
|
|
85
|
+
if (settled || nextIndex >= endpoints.length) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const endpoint = endpoints[nextIndex++];
|
|
89
|
+
const controller =
|
|
90
|
+
typeof AbortController === 'undefined'
|
|
91
|
+
? undefined
|
|
92
|
+
: new AbortController();
|
|
93
|
+
if (controller) {
|
|
94
|
+
controllers.push(controller);
|
|
95
|
+
}
|
|
96
|
+
pending++;
|
|
97
|
+
const start = now();
|
|
98
|
+
tryEndpoint(endpoint, controller?.signal)
|
|
99
|
+
.then((value) => {
|
|
100
|
+
if (controller) {
|
|
101
|
+
// Only the losing requests get aborted, not the winner's own
|
|
102
|
+
// (already settled) one.
|
|
103
|
+
const index = controllers.indexOf(controller);
|
|
104
|
+
if (index >= 0) {
|
|
105
|
+
controllers.splice(index, 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
finish({ endpoint, value, duration: now() - start });
|
|
109
|
+
})
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
pending--;
|
|
112
|
+
if (settled) {
|
|
113
|
+
// Losers cancelled after a win must not count as failures.
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
failures.push({ endpoint, error: normalizeError(error) });
|
|
117
|
+
if (nextIndex < endpoints.length) {
|
|
118
|
+
// A failure frees its slot: hedge the next candidate right away
|
|
119
|
+
// instead of waiting out the stagger.
|
|
120
|
+
launchNext();
|
|
121
|
+
} else if (pending === 0) {
|
|
122
|
+
finish(undefined);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (!settled && nextIndex < endpoints.length) {
|
|
126
|
+
hedgeTimer = setTimeout(launchNext, hedgeDelayMs);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
106
129
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
failures,
|
|
110
|
-
};
|
|
130
|
+
launchNext();
|
|
131
|
+
});
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
export async function executeEndpointFallback<T>({
|
|
@@ -116,16 +137,19 @@ export async function executeEndpointFallback<T>({
|
|
|
116
137
|
tryEndpoint,
|
|
117
138
|
random = Math.random,
|
|
118
139
|
now = Date.now,
|
|
140
|
+
hedgeDelayMs = DEFAULT_HEDGE_DELAY_MS,
|
|
119
141
|
onFirstFailure,
|
|
120
142
|
}: ExecuteEndpointFallbackOptions<T>): Promise<EndpointAttemptSuccess<T>> {
|
|
121
143
|
const excludedEndpoints = new Set<string>();
|
|
122
|
-
|
|
144
|
+
// The candidate ordering (random first pick, configured order as fallback)
|
|
145
|
+
// is pure policy; this side only executes it.
|
|
146
|
+
let candidates = orderEndpointCandidates(configuredEndpoints, random());
|
|
123
147
|
|
|
124
148
|
if (!candidates.length) {
|
|
125
149
|
throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
const firstEndpoint =
|
|
152
|
+
const firstEndpoint = candidates[0];
|
|
129
153
|
|
|
130
154
|
try {
|
|
131
155
|
return {
|
|
@@ -154,14 +178,15 @@ export async function executeEndpointFallback<T>({
|
|
|
154
178
|
throw lastError;
|
|
155
179
|
}
|
|
156
180
|
|
|
157
|
-
const {
|
|
181
|
+
const { success, failures } = await selectFastestSuccessfulEndpoint(
|
|
158
182
|
candidates,
|
|
159
183
|
tryEndpoint,
|
|
160
|
-
now
|
|
184
|
+
now,
|
|
185
|
+
hedgeDelayMs
|
|
161
186
|
);
|
|
162
187
|
|
|
163
|
-
if (
|
|
164
|
-
return
|
|
188
|
+
if (success) {
|
|
189
|
+
return success;
|
|
165
190
|
}
|
|
166
191
|
|
|
167
192
|
for (const failure of failures) {
|
package/src/error.ts
CHANGED
|
@@ -24,6 +24,9 @@ export type UpdateErrorCode =
|
|
|
24
24
|
// A throw from a user-provided hook (e.g. beforeReload) — not an update
|
|
25
25
|
// pipeline failure, and excluded from server-side patch-health telemetry.
|
|
26
26
|
| 'USER_HOOK_ERROR'
|
|
27
|
+
// A second client/provider in the same process — a hard integration error;
|
|
28
|
+
// the SDK is a process-level singleton.
|
|
29
|
+
| 'SINGLETON_VIOLATION'
|
|
27
30
|
// Native codes (see cpp/patch_core/error_codes.h)
|
|
28
31
|
| 'INVALID_OPTIONS'
|
|
29
32
|
| 'PATCH_FAILED'
|
|
@@ -47,6 +50,7 @@ const KNOWN_CODES = new Set<string>([
|
|
|
47
50
|
'STORAGE_PERMISSION_ERROR',
|
|
48
51
|
'APK_DOWNLOAD_FAILED',
|
|
49
52
|
'USER_HOOK_ERROR',
|
|
53
|
+
'SINGLETON_VIOLATION',
|
|
50
54
|
'INVALID_OPTIONS',
|
|
51
55
|
'PATCH_FAILED',
|
|
52
56
|
'FILE_OPERATION_FAILED',
|
package/src/locales/en.ts
CHANGED
|
@@ -43,6 +43,10 @@ export default {
|
|
|
43
43
|
|
|
44
44
|
// Error messages
|
|
45
45
|
error_appkey_required: 'appKey is required',
|
|
46
|
+
error_client_singleton:
|
|
47
|
+
'A second update client was created: the SDK is a process-level singleton — create one client and reuse it everywhere (use client.setOptions to change options at runtime)',
|
|
48
|
+
error_provider_singleton:
|
|
49
|
+
'A second UpdateProvider was mounted: make sure the app renders exactly one UpdateProvider',
|
|
46
50
|
error_parse_version_info: 'Failed to parse currentVersionInfo: {{info}}',
|
|
47
51
|
warn_unknown_bundle:
|
|
48
52
|
'The bundle embedded in this binary is not registered on the update platform; incremental updates are degraded to full downloads. Upload the package with pushy uploadIpa / uploadApk / uploadApp.',
|
package/src/locales/zh.ts
CHANGED
|
@@ -41,6 +41,10 @@ export default {
|
|
|
41
41
|
|
|
42
42
|
// Error messages
|
|
43
43
|
error_appkey_required: '需要提供 appKey',
|
|
44
|
+
error_client_singleton:
|
|
45
|
+
'检测到重复创建热更新 client:SDK 是进程级单例,请只创建一个 client 并在全局复用(运行期改配置请调用 client.setOptions)',
|
|
46
|
+
error_provider_singleton:
|
|
47
|
+
'检测到同时挂载多个 UpdateProvider:请确保应用中只有一个 UpdateProvider',
|
|
44
48
|
error_parse_version_info: '解析 currentVersionInfo 失败: {{info}}',
|
|
45
49
|
warn_unknown_bundle:
|
|
46
50
|
'当前二进制内嵌 bundle 未在更新平台注册,增量更新已降级为全量。请使用 pushy uploadIpa / uploadApk / uploadApp 上传对应安装包。',
|
package/src/provider.tsx
CHANGED
|
@@ -18,13 +18,15 @@ import { URL } from 'react-native-url-polyfill';
|
|
|
18
18
|
import { type Cresc, type Pushy, sharedState } from './client';
|
|
19
19
|
import { ProgressContext, UpdateContext } from './context';
|
|
20
20
|
import {
|
|
21
|
+
cInfo,
|
|
21
22
|
currentVersion,
|
|
22
23
|
currentVersionInfo,
|
|
23
24
|
getCurrentVersionInfo,
|
|
24
25
|
packageVersion,
|
|
26
|
+
rolledBackVersion,
|
|
25
27
|
} from './core';
|
|
26
|
-
import { resolveCheckResult } from './resolveCheckResult';
|
|
27
28
|
import type { CheckResult, ProgressData, UpdateTestPayload } from './type';
|
|
29
|
+
import { decideDownload, resolveCheckResult } from './updateFlowCore';
|
|
28
30
|
import { assertWeb, log, noop } from './utils';
|
|
29
31
|
|
|
30
32
|
export const UpdateProvider = ({
|
|
@@ -37,6 +39,26 @@ export const UpdateProvider = ({
|
|
|
37
39
|
client = useRef(client).current;
|
|
38
40
|
const { options } = client;
|
|
39
41
|
|
|
42
|
+
// A second concurrently mounted provider is a hard integration error (the
|
|
43
|
+
// client is a process-level singleton); the client throws on the second
|
|
44
|
+
// claim. Also releases the claim on unmount.
|
|
45
|
+
useEffect(() => client.claimProviderMount?.(), [client]);
|
|
46
|
+
|
|
47
|
+
// options is mutated in place by client.setOptions (its identity never
|
|
48
|
+
// changes), so effects keyed on it would never re-run. The client bumps a
|
|
49
|
+
// version on every setOptions; mirroring it into state re-renders this
|
|
50
|
+
// provider and re-runs the effects that list it as a dependency.
|
|
51
|
+
const [optionsVersion, setOptionsVersion] = useState(
|
|
52
|
+
client.optionsVersion ?? 0
|
|
53
|
+
);
|
|
54
|
+
useEffect(
|
|
55
|
+
() =>
|
|
56
|
+
client.onOptionsChange?.(() => {
|
|
57
|
+
setOptionsVersion((version) => version + 1);
|
|
58
|
+
}),
|
|
59
|
+
[client]
|
|
60
|
+
);
|
|
61
|
+
|
|
40
62
|
const stateListener = useRef<NativeEventSubscription>(undefined);
|
|
41
63
|
const [updateInfo, setUpdateInfo] = useState<CheckResult>();
|
|
42
64
|
const updateInfoRef = useRef(updateInfo);
|
|
@@ -202,7 +224,41 @@ export const UpdateProvider = ({
|
|
|
202
224
|
// known updateInfo instead of overwriting it with an empty object.
|
|
203
225
|
return;
|
|
204
226
|
}
|
|
205
|
-
|
|
227
|
+
let info = resolveCheckResult(
|
|
228
|
+
rootInfo,
|
|
229
|
+
{
|
|
230
|
+
packageVersion: client.getEffectivePackageVersion(),
|
|
231
|
+
currentVersion,
|
|
232
|
+
uuid: cInfo.uuid,
|
|
233
|
+
},
|
|
234
|
+
log
|
|
235
|
+
);
|
|
236
|
+
if (
|
|
237
|
+
!info.expired &&
|
|
238
|
+
info.update &&
|
|
239
|
+
(typeof info.hash !== 'string' || info.hash.length === 0)
|
|
240
|
+
) {
|
|
241
|
+
// A malformed rollout/root entry must not produce an alert whose
|
|
242
|
+
// confirm button can never download anything. Surface it to the
|
|
243
|
+
// developer telemetry/logger and present it to the app as no update.
|
|
244
|
+
client.reportInvalidUpdateOnce('missingHash');
|
|
245
|
+
info = { upToDate: true };
|
|
246
|
+
}
|
|
247
|
+
if (info.update && !info.expired) {
|
|
248
|
+
const decision = decideDownload(
|
|
249
|
+
info,
|
|
250
|
+
{ currentVersion, rolledBackVersion },
|
|
251
|
+
__DEV__
|
|
252
|
+
);
|
|
253
|
+
if (decision.action === 'none') {
|
|
254
|
+
if (decision.reason === 'noArtifact') {
|
|
255
|
+
// Invalid server data is worth reporting; local rollout guards are
|
|
256
|
+
// expected no-ops and stay silent.
|
|
257
|
+
client.reportInvalidUpdateOnce('noArtifact', info.hash || '');
|
|
258
|
+
}
|
|
259
|
+
info = { upToDate: true };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
206
262
|
if (info.update) {
|
|
207
263
|
info.description = info.description ?? '';
|
|
208
264
|
}
|
|
@@ -279,6 +335,7 @@ export const UpdateProvider = ({
|
|
|
279
335
|
|
|
280
336
|
const markSuccess = client.markSuccess;
|
|
281
337
|
|
|
338
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
|
|
282
339
|
useEffect(() => {
|
|
283
340
|
if (!client.assertDebug('checkUpdate()')) {
|
|
284
341
|
return;
|
|
@@ -313,8 +370,11 @@ export const UpdateProvider = ({
|
|
|
313
370
|
}
|
|
314
371
|
stateListener.current?.remove();
|
|
315
372
|
};
|
|
316
|
-
|
|
373
|
+
// optionsVersion: checkStrategy/autoMarkSuccess are read from the
|
|
374
|
+
// in-place-mutated options object, so option changes must re-run this.
|
|
375
|
+
}, [checkUpdate, options, markSuccess, client, optionsVersion]);
|
|
317
376
|
|
|
377
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
|
|
318
378
|
useEffect(() => {
|
|
319
379
|
const { dismissErrorAfter } = options;
|
|
320
380
|
if (
|
|
@@ -329,17 +389,22 @@ export const UpdateProvider = ({
|
|
|
329
389
|
clearTimeout(dismissErrorTimer);
|
|
330
390
|
};
|
|
331
391
|
}
|
|
332
|
-
|
|
392
|
+
// optionsVersion: dismissErrorAfter changes must reschedule a running
|
|
393
|
+
// timer, not only apply to the next error.
|
|
394
|
+
}, [lastError, options, dismissError, optionsVersion]);
|
|
333
395
|
|
|
334
396
|
const parseTestPayload = useCallback(
|
|
335
397
|
(payload: UpdateTestPayload) => {
|
|
336
398
|
if (payload?.type?.startsWith('__rnPushy')) {
|
|
337
|
-
const logger = options.logger || (() => {});
|
|
338
|
-
options.logger = ({ type, data }) => {
|
|
339
|
-
logger({ type, data });
|
|
340
|
-
Alert.alert(type, JSON.stringify(data));
|
|
341
|
-
};
|
|
342
399
|
if (payload.type === '__rnPushyVersionHash') {
|
|
400
|
+
// Wrap the logger only for the duration of this test check; wrapping
|
|
401
|
+
// on any other __rnPushy* payload would never be undone (the finally
|
|
402
|
+
// below is the only restore point).
|
|
403
|
+
const logger = options.logger || (() => {});
|
|
404
|
+
options.logger = ({ type, data }) => {
|
|
405
|
+
logger({ type, data });
|
|
406
|
+
Alert.alert(type, JSON.stringify(data));
|
|
407
|
+
};
|
|
343
408
|
const toHash = payload.data;
|
|
344
409
|
sharedState.toHash = toHash;
|
|
345
410
|
checkUpdate({ extra: { toHash } })
|
package/src/telemetry.ts
CHANGED
|
@@ -53,6 +53,14 @@ export const resolveServerEventType = (
|
|
|
53
53
|
return 'mark_success';
|
|
54
54
|
case 'rollback':
|
|
55
55
|
return 'rollback';
|
|
56
|
+
case 'downloadFallback':
|
|
57
|
+
// An incremental strategy failed but a later one rescued the download.
|
|
58
|
+
// When the incremental patch itself failed to apply (e.g. pdiff
|
|
59
|
+
// copiesCrc mismatch on a rebuilt binary), that is still a patch-health
|
|
60
|
+
// signal even though the end user got the update via full. Plain
|
|
61
|
+
// download errors that full rescued stay local-only — they are network
|
|
62
|
+
// noise, not patch health.
|
|
63
|
+
return code === 'PATCH_FAILED' ? 'patch_fail' : undefined;
|
|
56
64
|
default:
|
|
57
65
|
return undefined;
|
|
58
66
|
}
|
package/src/type.ts
CHANGED
|
@@ -9,6 +9,14 @@ export interface VersionInfo {
|
|
|
9
9
|
rollout: {
|
|
10
10
|
[packageVersion: string]: number;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Server-set per-version override: the native cold-start check activates
|
|
14
|
+
* this version for the next launch regardless of the client's
|
|
15
|
+
* updateStrategy (the brick-rescue directive). Native-only — the JS
|
|
16
|
+
* interactive flow ignores it. The device-local rolledBack guard still
|
|
17
|
+
* wins, and first_time crash protection still applies.
|
|
18
|
+
*/
|
|
19
|
+
forceBoot?: boolean;
|
|
12
20
|
[key: string]: any;
|
|
13
21
|
};
|
|
14
22
|
pdiff?: string;
|
|
@@ -83,6 +91,11 @@ export type EventType =
|
|
|
83
91
|
// registered on the update platform (developer-facing only, local logger;
|
|
84
92
|
// not a server telemetry event).
|
|
85
93
|
| 'bundleMismatch'
|
|
94
|
+
// An incremental strategy (diff/pdiff) failed but a later strategy saved the
|
|
95
|
+
// download. Carries which strategy failed and why — the signal that watches
|
|
96
|
+
// pdiff copiesCrc verification failures (BUNDLEHASH_MIGRATION §4.2.1) in
|
|
97
|
+
// the field.
|
|
98
|
+
| 'downloadFallback'
|
|
86
99
|
| 'errorRestart'
|
|
87
100
|
| 'errorSwitchVersion'
|
|
88
101
|
| 'downloadingApk'
|
|
@@ -164,6 +177,24 @@ export interface ClientOptions {
|
|
|
164
177
|
* the version health view in the console. Default: false (enabled).
|
|
165
178
|
*/
|
|
166
179
|
disableTelemetry?: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Disable the native cold-start update check: the background check that runs
|
|
182
|
+
* a few seconds after every launch, independent of JS
|
|
183
|
+
* (NATIVE_CHECKUPDATE_DESIGN §10). Default: false (enabled).
|
|
184
|
+
*
|
|
185
|
+
* That check is what rescues a device bricked by a bad update — its JS never
|
|
186
|
+
* runs, so nothing else can pull the fix. Turning it off gives up that
|
|
187
|
+
* recovery path (and the response cache the JS check reuses) in exchange for
|
|
188
|
+
* one fewer background request per cold start; choose it only when the extra
|
|
189
|
+
* request is itself the problem (traffic/battery budgets, privacy manifests,
|
|
190
|
+
* consent-gated networking).
|
|
191
|
+
*
|
|
192
|
+
* Orthogonal to `checkStrategy`, which governs activation authority rather
|
|
193
|
+
* than whether the check runs: with `checkStrategy: null` the native check
|
|
194
|
+
* still downloads but never activates on its own — only the server's
|
|
195
|
+
* per-version forceBoot directive may.
|
|
196
|
+
*/
|
|
197
|
+
disableNativeCheck?: boolean;
|
|
167
198
|
}
|
|
168
199
|
|
|
169
200
|
export interface UpdateTestPayload {
|