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.
Files changed (41) hide show
  1. package/BUNDLEHASH_MIGRATION.md +355 -0
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +192 -0
  3. package/android/jni/Android.mk +1 -0
  4. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  5. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  6. package/android/lib/x86/librnupdate.so +0 -0
  7. package/android/lib/x86_64/librnupdate.so +0 -0
  8. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +42 -9
  9. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +22 -1
  10. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +125 -0
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +23 -3
  13. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  14. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  15. package/cpp/patch_core/digest.cpp +230 -0
  16. package/cpp/patch_core/digest.h +61 -0
  17. package/cpp/patch_core/patch_core.cpp +17 -1
  18. package/cpp/patch_core/patch_core.h +8 -0
  19. package/cpp/patch_core/tests/patch_core_test.cpp +169 -0
  20. package/harmony/pushy/src/main/cpp/CMakeLists.txt +1 -0
  21. package/harmony/pushy/src/main/cpp/pushy.cpp +94 -0
  22. package/harmony/pushy/src/main/ets/DownloadTask.ts +85 -0
  23. package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
  24. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -0
  25. package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -4
  26. package/harmony/pushy.har +0 -0
  27. package/ios/RCTPushy/RCTPushy.mm +116 -7
  28. package/package.json +1 -1
  29. package/react-native-update.podspec +1 -0
  30. package/src/NativePushy.ts +6 -0
  31. package/src/client.ts +119 -7
  32. package/src/core.ts +27 -0
  33. package/src/endpoint.ts +98 -51
  34. package/src/error.ts +4 -0
  35. package/src/isInRollout.ts +2 -1
  36. package/src/locales/en.ts +6 -0
  37. package/src/locales/zh.ts +6 -0
  38. package/src/provider.tsx +36 -7
  39. package/src/telemetry.ts +8 -0
  40. package/src/type.ts +34 -3
  41. 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',
@@ -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
- ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
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
- }, [checkUpdate, options, markSuccess, client]);
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
- }, [lastError, options, dismissError]);
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?: true;
21
- expired?: true;
20
+ upToDate?: boolean;
21
+ expired?: boolean;
22
22
  downloadUrl?: string;
23
- update?: true;
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
  }