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
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;
|
|
@@ -28,33 +31,6 @@ const normalizeError = (error: unknown) => {
|
|
|
28
31
|
return new Error(String(error));
|
|
29
32
|
};
|
|
30
33
|
|
|
31
|
-
export const dedupeEndpoints = (
|
|
32
|
-
endpoints: Array<string | null | undefined>
|
|
33
|
-
): string[] => {
|
|
34
|
-
const result: string[] = [];
|
|
35
|
-
const visited = new Set<string>();
|
|
36
|
-
|
|
37
|
-
for (const endpoint of endpoints) {
|
|
38
|
-
if (!endpoint || visited.has(endpoint)) {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
visited.add(endpoint);
|
|
42
|
-
result.push(endpoint);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return result;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const pickRandomEndpoint = (
|
|
49
|
-
endpoints: string[],
|
|
50
|
-
random: () => number = Math.random
|
|
51
|
-
) => {
|
|
52
|
-
if (!endpoints.length) {
|
|
53
|
-
throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
|
|
54
|
-
}
|
|
55
|
-
return endpoints[Math.floor(random() * endpoints.length)];
|
|
56
|
-
};
|
|
57
|
-
|
|
58
34
|
export const DEFAULT_HEDGE_DELAY_MS = 250;
|
|
59
35
|
|
|
60
36
|
/**
|
|
@@ -165,13 +141,15 @@ export async function executeEndpointFallback<T>({
|
|
|
165
141
|
onFirstFailure,
|
|
166
142
|
}: ExecuteEndpointFallbackOptions<T>): Promise<EndpointAttemptSuccess<T>> {
|
|
167
143
|
const excludedEndpoints = new Set<string>();
|
|
168
|
-
|
|
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());
|
|
169
147
|
|
|
170
148
|
if (!candidates.length) {
|
|
171
149
|
throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
|
|
172
150
|
}
|
|
173
151
|
|
|
174
|
-
const firstEndpoint =
|
|
152
|
+
const firstEndpoint = candidates[0];
|
|
175
153
|
|
|
176
154
|
try {
|
|
177
155
|
return {
|
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 = ({
|
|
@@ -222,7 +224,41 @@ export const UpdateProvider = ({
|
|
|
222
224
|
// known updateInfo instead of overwriting it with an empty object.
|
|
223
225
|
return;
|
|
224
226
|
}
|
|
225
|
-
|
|
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
|
+
}
|
|
226
262
|
if (info.update) {
|
|
227
263
|
info.description = info.description ?? '';
|
|
228
264
|
}
|
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;
|
|
@@ -169,6 +177,24 @@ export interface ClientOptions {
|
|
|
169
177
|
* the version health view in the console. Default: false (enabled).
|
|
170
178
|
*/
|
|
171
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;
|
|
172
198
|
}
|
|
173
199
|
|
|
174
200
|
export interface UpdateTestPayload {
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type { CheckResult } from './type';
|
|
2
|
+
|
|
3
|
+
// 更新流程的纯决策层:无 IO、不依赖 react-native、无模块级状态,所有输入均由
|
|
4
|
+
// 参数传入(仅允许 type-only import)。这一层必须保持可在裸 JS 引擎中求值——
|
|
5
|
+
// 它就是未来 guardian bundle 的编译单元(NATIVE_CHECKUPDATE_DESIGN §5),
|
|
6
|
+
// IO(HTTP/下载/落盘)由调用方(现在是 client.ts,将来是原生编排)执行。
|
|
7
|
+
|
|
8
|
+
/* eslint-disable no-fallthrough */
|
|
9
|
+
/* eslint-disable no-bitwise */
|
|
10
|
+
export function murmurhash3_32_gc(key: string, seed = 0) {
|
|
11
|
+
let remainder: number,
|
|
12
|
+
bytes: number,
|
|
13
|
+
h1: number,
|
|
14
|
+
h1b: number,
|
|
15
|
+
c1: number,
|
|
16
|
+
c2: number,
|
|
17
|
+
k1: number,
|
|
18
|
+
i: number;
|
|
19
|
+
|
|
20
|
+
remainder = key.length & 3; // key.length % 4
|
|
21
|
+
bytes = key.length - remainder;
|
|
22
|
+
h1 = seed;
|
|
23
|
+
c1 = 0xcc9e2d51;
|
|
24
|
+
c2 = 0x1b873593;
|
|
25
|
+
i = 0;
|
|
26
|
+
|
|
27
|
+
while (i < bytes) {
|
|
28
|
+
k1 =
|
|
29
|
+
(key.charCodeAt(i) & 0xff) |
|
|
30
|
+
((key.charCodeAt(++i) & 0xff) << 8) |
|
|
31
|
+
((key.charCodeAt(++i) & 0xff) << 16) |
|
|
32
|
+
((key.charCodeAt(++i) & 0xff) << 24);
|
|
33
|
+
++i;
|
|
34
|
+
|
|
35
|
+
k1 =
|
|
36
|
+
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
|
|
37
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
38
|
+
k1 =
|
|
39
|
+
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
|
|
40
|
+
|
|
41
|
+
h1 ^= k1;
|
|
42
|
+
h1 = (h1 << 13) | (h1 >>> 19);
|
|
43
|
+
h1b =
|
|
44
|
+
((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
|
|
45
|
+
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
k1 = 0;
|
|
49
|
+
|
|
50
|
+
switch (remainder) {
|
|
51
|
+
// biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
|
|
52
|
+
case 3:
|
|
53
|
+
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
|
|
54
|
+
// biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
|
|
55
|
+
case 2:
|
|
56
|
+
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
|
|
57
|
+
case 1:
|
|
58
|
+
k1 ^= key.charCodeAt(i) & 0xff;
|
|
59
|
+
|
|
60
|
+
k1 =
|
|
61
|
+
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
|
|
62
|
+
0xffffffff;
|
|
63
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
64
|
+
k1 =
|
|
65
|
+
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
|
|
66
|
+
0xffffffff;
|
|
67
|
+
h1 ^= k1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
h1 ^= key.length;
|
|
71
|
+
|
|
72
|
+
h1 ^= h1 >>> 16;
|
|
73
|
+
h1 =
|
|
74
|
+
((h1 & 0xffff) * 0x85ebca6b +
|
|
75
|
+
((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
|
|
76
|
+
0xffffffff;
|
|
77
|
+
h1 ^= h1 >>> 13;
|
|
78
|
+
h1 =
|
|
79
|
+
((h1 & 0xffff) * 0xc2b2ae35 +
|
|
80
|
+
((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
|
|
81
|
+
0xffffffff;
|
|
82
|
+
h1 ^= h1 >>> 16;
|
|
83
|
+
|
|
84
|
+
return h1 >>> 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isInRollout(rollout: number, uuid: string) {
|
|
88
|
+
return murmurhash3_32_gc(uuid) % 100 < rollout;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function joinUrls(paths: string[], fileName?: string) {
|
|
92
|
+
if (fileName) {
|
|
93
|
+
return paths.map((path) => {
|
|
94
|
+
const normalizedPath = path.replace(/\/+$/, '');
|
|
95
|
+
// Keep explicit http(s) URLs for local/self-hosted update sources.
|
|
96
|
+
const baseUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(normalizedPath)
|
|
97
|
+
? normalizedPath
|
|
98
|
+
: `https://${normalizedPath}`;
|
|
99
|
+
return `${baseUrl}/${fileName}`;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const dedupeEndpoints = (
|
|
105
|
+
endpoints: Array<string | null | undefined>
|
|
106
|
+
): string[] => {
|
|
107
|
+
const result: string[] = [];
|
|
108
|
+
const visited = new Set<string>();
|
|
109
|
+
|
|
110
|
+
for (const endpoint of endpoints) {
|
|
111
|
+
if (!endpoint || visited.has(endpoint)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
visited.add(endpoint);
|
|
115
|
+
result.push(endpoint);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The endpoint plan: dedupe, then move the sampled pick to the front (load
|
|
123
|
+
* spreading), keeping the rest in configured order as the fallback sequence.
|
|
124
|
+
* `randomSample` ∈ [0, 1) is injected by the caller — this layer cannot draw
|
|
125
|
+
* randomness itself.
|
|
126
|
+
*/
|
|
127
|
+
export function orderEndpointCandidates(
|
|
128
|
+
endpoints: Array<string | null | undefined>,
|
|
129
|
+
randomSample = 0
|
|
130
|
+
): string[] {
|
|
131
|
+
const deduped = dedupeEndpoints(endpoints);
|
|
132
|
+
if (deduped.length < 2) {
|
|
133
|
+
return deduped;
|
|
134
|
+
}
|
|
135
|
+
const first = Math.max(
|
|
136
|
+
0,
|
|
137
|
+
Math.min(
|
|
138
|
+
Number.isFinite(randomSample)
|
|
139
|
+
? Math.floor(randomSample * deduped.length)
|
|
140
|
+
: 0,
|
|
141
|
+
deduped.length - 1
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
return [
|
|
145
|
+
deduped[first],
|
|
146
|
+
...deduped.slice(0, first),
|
|
147
|
+
...deduped.slice(first + 1),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface CheckRequestInput {
|
|
152
|
+
/** Effective native package version (overridePackageVersion already applied). */
|
|
153
|
+
packageVersion: string;
|
|
154
|
+
/** Hash of the currently running JS version ('' when on the packaged bundle). */
|
|
155
|
+
currentVersion?: string;
|
|
156
|
+
buildTime: string;
|
|
157
|
+
cInfo: Record<string, string>;
|
|
158
|
+
supportedDiffVersion?: number;
|
|
159
|
+
/** '' or undefined while unknown — the field is then omitted. */
|
|
160
|
+
bundleHash?: string;
|
|
161
|
+
isDev?: boolean;
|
|
162
|
+
extra?: Record<string, any>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function buildCheckRequestBody({
|
|
166
|
+
packageVersion,
|
|
167
|
+
currentVersion,
|
|
168
|
+
buildTime,
|
|
169
|
+
cInfo,
|
|
170
|
+
supportedDiffVersion,
|
|
171
|
+
bundleHash,
|
|
172
|
+
isDev,
|
|
173
|
+
extra,
|
|
174
|
+
}: CheckRequestInput): Record<string, any> {
|
|
175
|
+
const body: Record<string, any> = {
|
|
176
|
+
packageVersion,
|
|
177
|
+
hash: currentVersion,
|
|
178
|
+
buildTime,
|
|
179
|
+
cInfo,
|
|
180
|
+
// 可消费的 diff 轨道版本(2 = hdiffv2 轨道),服务端据此门控下发
|
|
181
|
+
...(supportedDiffVersion ? { diffV: supportedDiffVersion } : {}),
|
|
182
|
+
...(bundleHash ? { bundleHash } : {}),
|
|
183
|
+
...extra,
|
|
184
|
+
};
|
|
185
|
+
if (isDev) {
|
|
186
|
+
delete body.buildTime;
|
|
187
|
+
}
|
|
188
|
+
return body;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface UpdateIdentity {
|
|
192
|
+
packageVersion: string;
|
|
193
|
+
currentVersion?: string;
|
|
194
|
+
/** Stable client uuid — the gray-release bucketing key. */
|
|
195
|
+
uuid: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resolveCheckResult(
|
|
199
|
+
rootInfo: CheckResult,
|
|
200
|
+
identity: UpdateIdentity,
|
|
201
|
+
log: (...args: any[]) => void = () => {}
|
|
202
|
+
): CheckResult {
|
|
203
|
+
const { expVersion, ...rootResult } = rootInfo;
|
|
204
|
+
const rollout = expVersion?.config?.rollout?.[identity.packageVersion];
|
|
205
|
+
if (rootResult.update && expVersion && typeof rollout === 'number') {
|
|
206
|
+
if (isInRollout(rollout, identity.uuid)) {
|
|
207
|
+
log(`${expVersion.name} in ${rollout}% rollout, continue`);
|
|
208
|
+
if (
|
|
209
|
+
typeof expVersion.hash === 'string' &&
|
|
210
|
+
expVersion.hash.length > 0 &&
|
|
211
|
+
expVersion.hash === identity.currentVersion
|
|
212
|
+
) {
|
|
213
|
+
return { upToDate: true };
|
|
214
|
+
}
|
|
215
|
+
const info: CheckResult = {
|
|
216
|
+
update: true,
|
|
217
|
+
...expVersion,
|
|
218
|
+
};
|
|
219
|
+
if (rootResult.paths) {
|
|
220
|
+
info.paths = rootResult.paths;
|
|
221
|
+
}
|
|
222
|
+
return info;
|
|
223
|
+
}
|
|
224
|
+
log(`${expVersion.name} not in ${rollout}% rollout, ignored`);
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
rootResult.update &&
|
|
228
|
+
typeof rootResult.hash === 'string' &&
|
|
229
|
+
rootResult.hash.length > 0 &&
|
|
230
|
+
rootResult.hash === identity.currentVersion
|
|
231
|
+
) {
|
|
232
|
+
return { upToDate: true };
|
|
233
|
+
}
|
|
234
|
+
return rootResult;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Whether the native orchestrator should activate a downloaded version for
|
|
239
|
+
* the next launch: either the client opted in via its silent strategies
|
|
240
|
+
* (afterDownload === 'setNeedUpdate'), or the server marked this version
|
|
241
|
+
* `config.forceBoot` — the per-version remote override that closes the
|
|
242
|
+
* brick-rescue gap for alert-strategy apps (a bricked device never runs JS,
|
|
243
|
+
* so activation cannot wait for it). Native-only: the JS side's interactive
|
|
244
|
+
* strategies are not consulted and not affected. The device-local
|
|
245
|
+
* rolledBackVersion guard in decideDownload still wins over forceBoot, and
|
|
246
|
+
* the activated version keeps the first_time crash-protection rollback.
|
|
247
|
+
*/
|
|
248
|
+
export function shouldActivateAfterDownload(
|
|
249
|
+
info: CheckResult,
|
|
250
|
+
afterDownload?: string
|
|
251
|
+
): boolean {
|
|
252
|
+
return afterDownload === 'setNeedUpdate' || !!info?.config?.forceBoot;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export type DownloadStrategyType = 'diff' | 'pdiff' | 'full';
|
|
256
|
+
|
|
257
|
+
export interface DownloadAttempt {
|
|
258
|
+
type: DownloadStrategyType;
|
|
259
|
+
/** Candidate URLs (paths × file name); the executor probes and picks one. */
|
|
260
|
+
urls: string[];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface DownloadPlan {
|
|
264
|
+
hash: string;
|
|
265
|
+
/** Ordered attempts: incremental first, full as the last resort. */
|
|
266
|
+
attempts: DownloadAttempt[];
|
|
267
|
+
/** Dev-only: nothing to fetch — treat as an immediate no-op success. */
|
|
268
|
+
devNoop: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type DownloadDecision =
|
|
272
|
+
| {
|
|
273
|
+
action: 'none';
|
|
274
|
+
reason: 'noUpdate' | 'alreadyCurrent' | 'rolledBack' | 'noArtifact';
|
|
275
|
+
}
|
|
276
|
+
| ({ action: 'download' } & DownloadPlan);
|
|
277
|
+
|
|
278
|
+
export function decideDownload(
|
|
279
|
+
info: CheckResult,
|
|
280
|
+
identity: { currentVersion?: string; rolledBackVersion?: string },
|
|
281
|
+
isDev = false
|
|
282
|
+
): DownloadDecision {
|
|
283
|
+
const { hash, diff, pdiff, full, paths = [] } = info;
|
|
284
|
+
if (!info.update || !hash) {
|
|
285
|
+
return { action: 'none', reason: 'noUpdate' };
|
|
286
|
+
}
|
|
287
|
+
if (hash === identity.currentVersion) {
|
|
288
|
+
return { action: 'none', reason: 'alreadyCurrent' };
|
|
289
|
+
}
|
|
290
|
+
if (identity.rolledBackVersion && hash === identity.rolledBackVersion) {
|
|
291
|
+
return { action: 'none', reason: 'rolledBack' };
|
|
292
|
+
}
|
|
293
|
+
const attempts: DownloadAttempt[] = [];
|
|
294
|
+
// Incremental artifacts only exist against release builds; dev goes
|
|
295
|
+
// straight to full (or the no-op below when there is none).
|
|
296
|
+
if (!isDev) {
|
|
297
|
+
const diffUrls = joinUrls(paths, diff);
|
|
298
|
+
if (diffUrls?.length) {
|
|
299
|
+
attempts.push({ type: 'diff', urls: diffUrls });
|
|
300
|
+
}
|
|
301
|
+
const pdiffUrls = joinUrls(paths, pdiff);
|
|
302
|
+
if (pdiffUrls?.length) {
|
|
303
|
+
attempts.push({ type: 'pdiff', urls: pdiffUrls });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const fullUrls = joinUrls(paths, full);
|
|
307
|
+
if (fullUrls?.length) {
|
|
308
|
+
attempts.push({ type: 'full', urls: fullUrls });
|
|
309
|
+
}
|
|
310
|
+
const devNoop = !!isDev && !fullUrls?.length;
|
|
311
|
+
if (!attempts.length && !devNoop) {
|
|
312
|
+
return { action: 'none', reason: 'noArtifact' };
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
action: 'download',
|
|
316
|
+
hash,
|
|
317
|
+
attempts,
|
|
318
|
+
devNoop,
|
|
319
|
+
};
|
|
320
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -76,18 +76,7 @@ const ping = isWeb
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
export
|
|
80
|
-
if (fileName) {
|
|
81
|
-
return paths.map((path) => {
|
|
82
|
-
const normalizedPath = path.replace(/\/+$/, '');
|
|
83
|
-
// Keep explicit http(s) URLs for local/self-hosted update sources.
|
|
84
|
-
const baseUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(normalizedPath)
|
|
85
|
-
? normalizedPath
|
|
86
|
-
: `https://${normalizedPath}`;
|
|
87
|
-
return `${baseUrl}/${fileName}`;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
79
|
+
export { joinUrls } from './updateFlowCore';
|
|
91
80
|
|
|
92
81
|
export const testUrls = async (urls?: string[]): Promise<string | null> => {
|
|
93
82
|
if (!urls?.length) {
|
package/src/isInRollout.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-fallthrough */
|
|
2
|
-
|
|
3
|
-
import { cInfo } from './core';
|
|
4
|
-
|
|
5
|
-
/* eslint-disable no-bitwise */
|
|
6
|
-
export function murmurhash3_32_gc(key: string, seed = 0) {
|
|
7
|
-
let remainder: number,
|
|
8
|
-
bytes: number,
|
|
9
|
-
h1: number,
|
|
10
|
-
h1b: number,
|
|
11
|
-
c1: number,
|
|
12
|
-
c2: number,
|
|
13
|
-
k1: number,
|
|
14
|
-
i: number;
|
|
15
|
-
|
|
16
|
-
remainder = key.length & 3; // key.length % 4
|
|
17
|
-
bytes = key.length - remainder;
|
|
18
|
-
h1 = seed;
|
|
19
|
-
c1 = 0xcc9e2d51;
|
|
20
|
-
c2 = 0x1b873593;
|
|
21
|
-
i = 0;
|
|
22
|
-
|
|
23
|
-
while (i < bytes) {
|
|
24
|
-
k1 =
|
|
25
|
-
(key.charCodeAt(i) & 0xff) |
|
|
26
|
-
((key.charCodeAt(++i) & 0xff) << 8) |
|
|
27
|
-
((key.charCodeAt(++i) & 0xff) << 16) |
|
|
28
|
-
((key.charCodeAt(++i) & 0xff) << 24);
|
|
29
|
-
++i;
|
|
30
|
-
|
|
31
|
-
k1 =
|
|
32
|
-
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
|
|
33
|
-
k1 = (k1 << 15) | (k1 >>> 17);
|
|
34
|
-
k1 =
|
|
35
|
-
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
|
|
36
|
-
|
|
37
|
-
h1 ^= k1;
|
|
38
|
-
h1 = (h1 << 13) | (h1 >>> 19);
|
|
39
|
-
h1b =
|
|
40
|
-
((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
|
|
41
|
-
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
k1 = 0;
|
|
45
|
-
|
|
46
|
-
switch (remainder) {
|
|
47
|
-
// biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
|
|
48
|
-
case 3:
|
|
49
|
-
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
|
|
50
|
-
// biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
|
|
51
|
-
case 2:
|
|
52
|
-
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
|
|
53
|
-
case 1:
|
|
54
|
-
k1 ^= key.charCodeAt(i) & 0xff;
|
|
55
|
-
|
|
56
|
-
k1 =
|
|
57
|
-
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
|
|
58
|
-
0xffffffff;
|
|
59
|
-
k1 = (k1 << 15) | (k1 >>> 17);
|
|
60
|
-
k1 =
|
|
61
|
-
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
|
|
62
|
-
0xffffffff;
|
|
63
|
-
h1 ^= k1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
h1 ^= key.length;
|
|
67
|
-
|
|
68
|
-
h1 ^= h1 >>> 16;
|
|
69
|
-
h1 =
|
|
70
|
-
((h1 & 0xffff) * 0x85ebca6b +
|
|
71
|
-
((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
|
|
72
|
-
0xffffffff;
|
|
73
|
-
h1 ^= h1 >>> 13;
|
|
74
|
-
h1 =
|
|
75
|
-
((h1 & 0xffff) * 0xc2b2ae35 +
|
|
76
|
-
((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
|
|
77
|
-
0xffffffff;
|
|
78
|
-
h1 ^= h1 >>> 16;
|
|
79
|
-
|
|
80
|
-
return h1 >>> 0;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const intForUUID = murmurhash3_32_gc(cInfo.uuid);
|
|
84
|
-
|
|
85
|
-
export function isInRollout(rollout: number) {
|
|
86
|
-
return intForUUID % 100 < rollout;
|
|
87
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { currentVersion, packageVersion } from './core';
|
|
2
|
-
import { isInRollout } from './isInRollout';
|
|
3
|
-
import type { CheckResult } from './type';
|
|
4
|
-
import { log } from './utils';
|
|
5
|
-
|
|
6
|
-
export function resolveCheckResult(rootInfo: CheckResult): CheckResult {
|
|
7
|
-
const { expVersion, ...rootResult } = rootInfo;
|
|
8
|
-
const rollout = expVersion?.config?.rollout?.[packageVersion];
|
|
9
|
-
if (rootResult.update && expVersion && typeof rollout === 'number') {
|
|
10
|
-
if (isInRollout(rollout)) {
|
|
11
|
-
log(`${expVersion.name} in ${rollout}% rollout, continue`);
|
|
12
|
-
if (expVersion.hash === currentVersion) {
|
|
13
|
-
return { upToDate: true };
|
|
14
|
-
}
|
|
15
|
-
const info: CheckResult = {
|
|
16
|
-
update: true,
|
|
17
|
-
...expVersion,
|
|
18
|
-
};
|
|
19
|
-
if (rootResult.paths) {
|
|
20
|
-
info.paths = rootResult.paths;
|
|
21
|
-
}
|
|
22
|
-
return info;
|
|
23
|
-
}
|
|
24
|
-
log(`${expVersion.name} not in ${rollout}% rollout, ignored`);
|
|
25
|
-
}
|
|
26
|
-
if (rootResult.update && rootResult.hash === currentVersion) {
|
|
27
|
-
return { upToDate: true };
|
|
28
|
-
}
|
|
29
|
-
return rootResult;
|
|
30
|
-
}
|