react-native-update 10.48.3 → 10.50.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 +355 -0
- package/NATIVE_CHECKUPDATE_DESIGN.md +192 -0
- package/android/jni/Android.mk +1 -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/BundledResourceCopier.java +42 -9
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +22 -1
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +125 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +23 -3
- 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/digest.cpp +230 -0
- package/cpp/patch_core/digest.h +61 -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 +169 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +1 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +94 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +85 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -4
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +116 -7
- package/package.json +1 -1
- package/react-native-update.podspec +1 -0
- package/src/NativePushy.ts +6 -0
- package/src/client.ts +119 -7
- package/src/core.ts +27 -0
- package/src/endpoint.ts +98 -51
- package/src/error.ts +4 -0
- package/src/isInRollout.ts +2 -1
- package/src/locales/en.ts +6 -0
- package/src/locales/zh.ts +6 -0
- package/src/provider.tsx +36 -7
- package/src/telemetry.ts +8 -0
- package/src/type.ts +34 -3
- package/src/utils.ts +13 -0
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/isInRollout.ts
CHANGED
|
@@ -28,7 +28,8 @@ export function murmurhash3_32_gc(key: string, seed = 0) {
|
|
|
28
28
|
((key.charCodeAt(++i) & 0xff) << 24);
|
|
29
29
|
++i;
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
k1 =
|
|
32
|
+
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
|
|
32
33
|
k1 = (k1 << 15) | (k1 >>> 17);
|
|
33
34
|
k1 =
|
|
34
35
|
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
|
package/src/locales/en.ts
CHANGED
|
@@ -43,7 +43,13 @@ 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}}',
|
|
51
|
+
warn_unknown_bundle:
|
|
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.',
|
|
47
53
|
error_update_check_failed: 'Update check failed',
|
|
48
54
|
error_cannot_connect_server:
|
|
49
55
|
'Can not connect to update server. Please check your network.',
|
package/src/locales/zh.ts
CHANGED
|
@@ -41,7 +41,13 @@ 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}}',
|
|
49
|
+
warn_unknown_bundle:
|
|
50
|
+
'当前二进制内嵌 bundle 未在更新平台注册,增量更新已降级为全量。请使用 pushy uploadIpa / uploadApk / uploadApp 上传对应安装包。',
|
|
45
51
|
error_update_check_failed: '更新检查失败',
|
|
46
52
|
error_cannot_connect_server: '无法连接到更新服务器。请检查网络连接。',
|
|
47
53
|
error_cannot_connect_backup:
|
package/src/provider.tsx
CHANGED
|
@@ -37,6 +37,26 @@ export const UpdateProvider = ({
|
|
|
37
37
|
client = useRef(client).current;
|
|
38
38
|
const { options } = client;
|
|
39
39
|
|
|
40
|
+
// A second concurrently mounted provider is a hard integration error (the
|
|
41
|
+
// client is a process-level singleton); the client throws on the second
|
|
42
|
+
// claim. Also releases the claim on unmount.
|
|
43
|
+
useEffect(() => client.claimProviderMount?.(), [client]);
|
|
44
|
+
|
|
45
|
+
// options is mutated in place by client.setOptions (its identity never
|
|
46
|
+
// changes), so effects keyed on it would never re-run. The client bumps a
|
|
47
|
+
// version on every setOptions; mirroring it into state re-renders this
|
|
48
|
+
// provider and re-runs the effects that list it as a dependency.
|
|
49
|
+
const [optionsVersion, setOptionsVersion] = useState(
|
|
50
|
+
client.optionsVersion ?? 0
|
|
51
|
+
);
|
|
52
|
+
useEffect(
|
|
53
|
+
() =>
|
|
54
|
+
client.onOptionsChange?.(() => {
|
|
55
|
+
setOptionsVersion((version) => version + 1);
|
|
56
|
+
}),
|
|
57
|
+
[client]
|
|
58
|
+
);
|
|
59
|
+
|
|
40
60
|
const stateListener = useRef<NativeEventSubscription>(undefined);
|
|
41
61
|
const [updateInfo, setUpdateInfo] = useState<CheckResult>();
|
|
42
62
|
const updateInfoRef = useRef(updateInfo);
|
|
@@ -279,6 +299,7 @@ export const UpdateProvider = ({
|
|
|
279
299
|
|
|
280
300
|
const markSuccess = client.markSuccess;
|
|
281
301
|
|
|
302
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
|
|
282
303
|
useEffect(() => {
|
|
283
304
|
if (!client.assertDebug('checkUpdate()')) {
|
|
284
305
|
return;
|
|
@@ -313,8 +334,11 @@ export const UpdateProvider = ({
|
|
|
313
334
|
}
|
|
314
335
|
stateListener.current?.remove();
|
|
315
336
|
};
|
|
316
|
-
|
|
337
|
+
// optionsVersion: checkStrategy/autoMarkSuccess are read from the
|
|
338
|
+
// in-place-mutated options object, so option changes must re-run this.
|
|
339
|
+
}, [checkUpdate, options, markSuccess, client, optionsVersion]);
|
|
317
340
|
|
|
341
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
|
|
318
342
|
useEffect(() => {
|
|
319
343
|
const { dismissErrorAfter } = options;
|
|
320
344
|
if (
|
|
@@ -329,17 +353,22 @@ export const UpdateProvider = ({
|
|
|
329
353
|
clearTimeout(dismissErrorTimer);
|
|
330
354
|
};
|
|
331
355
|
}
|
|
332
|
-
|
|
356
|
+
// optionsVersion: dismissErrorAfter changes must reschedule a running
|
|
357
|
+
// timer, not only apply to the next error.
|
|
358
|
+
}, [lastError, options, dismissError, optionsVersion]);
|
|
333
359
|
|
|
334
360
|
const parseTestPayload = useCallback(
|
|
335
361
|
(payload: UpdateTestPayload) => {
|
|
336
362
|
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
363
|
if (payload.type === '__rnPushyVersionHash') {
|
|
364
|
+
// Wrap the logger only for the duration of this test check; wrapping
|
|
365
|
+
// on any other __rnPushy* payload would never be undone (the finally
|
|
366
|
+
// below is the only restore point).
|
|
367
|
+
const logger = options.logger || (() => {});
|
|
368
|
+
options.logger = ({ type, data }) => {
|
|
369
|
+
logger({ type, data });
|
|
370
|
+
Alert.alert(type, JSON.stringify(data));
|
|
371
|
+
};
|
|
343
372
|
const toHash = payload.data;
|
|
344
373
|
sharedState.toHash = toHash;
|
|
345
374
|
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
|
@@ -17,15 +17,37 @@ export interface VersionInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface RootResult {
|
|
20
|
-
upToDate?:
|
|
21
|
-
expired?:
|
|
20
|
+
upToDate?: boolean;
|
|
21
|
+
expired?: boolean;
|
|
22
22
|
downloadUrl?: string;
|
|
23
|
-
update?:
|
|
23
|
+
update?: boolean;
|
|
24
24
|
paused?: 'app' | 'package';
|
|
25
25
|
message?: string;
|
|
26
26
|
paths?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Server verdict on the reported bundleHash: 'matched' — the installed
|
|
29
|
+
* binary is registered; 'rebuiltSameJs' — repackaged binary with identical
|
|
30
|
+
* JS (no upload needed); 'unknownBundle' — unregistered binary, incremental
|
|
31
|
+
* updates degraded to full downloads until the package is uploaded.
|
|
32
|
+
*/
|
|
33
|
+
bundleStatus?: 'matched' | 'rebuiltSameJs' | 'unknownBundle';
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
export type UpToDateCheckResult = RootResult & {
|
|
37
|
+
upToDate: true;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type UpdateAvailableCheckResult = RootResult &
|
|
41
|
+
Partial<VersionInfo> & {
|
|
42
|
+
update: true;
|
|
43
|
+
expVersion?: VersionInfo;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ExpiredCheckResult = RootResult & {
|
|
47
|
+
expired: true;
|
|
48
|
+
downloadUrl: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
29
51
|
export type CheckResult = RootResult &
|
|
30
52
|
Partial<VersionInfo> & {
|
|
31
53
|
expVersion?: VersionInfo;
|
|
@@ -57,6 +79,15 @@ export type EventType =
|
|
|
57
79
|
| 'errorMarkSuccess'
|
|
58
80
|
| 'reset'
|
|
59
81
|
| 'errorReset'
|
|
82
|
+
// Server reported bundleStatus: unknownBundle — the installed binary is not
|
|
83
|
+
// registered on the update platform (developer-facing only, local logger;
|
|
84
|
+
// not a server telemetry event).
|
|
85
|
+
| 'bundleMismatch'
|
|
86
|
+
// An incremental strategy (diff/pdiff) failed but a later strategy saved the
|
|
87
|
+
// download. Carries which strategy failed and why — the signal that watches
|
|
88
|
+
// pdiff copiesCrc verification failures (BUNDLEHASH_MIGRATION §4.2.1) in
|
|
89
|
+
// the field.
|
|
90
|
+
| 'downloadFallback'
|
|
60
91
|
| 'errorRestart'
|
|
61
92
|
| 'errorSwitchVersion'
|
|
62
93
|
| 'downloadingApk'
|
package/src/utils.ts
CHANGED
|
@@ -147,6 +147,15 @@ export const fetchWithTimeout = (
|
|
|
147
147
|
// Promise.race the losing fetch kept running (and kept the connection busy)
|
|
148
148
|
// long after the caller had already moved on.
|
|
149
149
|
const controller = new AbortController();
|
|
150
|
+
// The timeout controller replaces params.signal on the fetch call, so a
|
|
151
|
+
// caller-provided signal (e.g. the hedged endpoint race cancelling losers)
|
|
152
|
+
// must be chained onto it manually.
|
|
153
|
+
const externalSignal = (params as any)?.signal as AbortSignal | undefined;
|
|
154
|
+
if (externalSignal?.aborted) {
|
|
155
|
+
controller.abort();
|
|
156
|
+
} else if (externalSignal) {
|
|
157
|
+
externalSignal.addEventListener('abort', () => controller.abort());
|
|
158
|
+
}
|
|
150
159
|
const timeoutId = setTimeout(() => {
|
|
151
160
|
log('fetch timeout', url);
|
|
152
161
|
controller.abort();
|
|
@@ -154,6 +163,10 @@ export const fetchWithTimeout = (
|
|
|
154
163
|
|
|
155
164
|
return enhancedFetch(url, { ...params, signal: controller.signal as any })
|
|
156
165
|
.catch((e: any) => {
|
|
166
|
+
if (externalSignal?.aborted) {
|
|
167
|
+
// Cancelled by the caller, not timed out; keep the abort semantics.
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
157
170
|
if (controller.signal.aborted) {
|
|
158
171
|
throw Error(i18n.t('error_ping_timeout'));
|
|
159
172
|
}
|