react-native-update 10.44.0 → 10.46.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 (37) hide show
  1. package/android/jni/Android.mk +0 -1
  2. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  3. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  4. package/android/lib/x86/librnupdate.so +0 -0
  5. package/android/lib/x86_64/librnupdate.so +0 -0
  6. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +10 -1
  7. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +24 -0
  8. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +2 -1
  9. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +2 -1
  10. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +19 -4
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +15 -15
  13. package/cpp/patch_core/error_codes.h +43 -0
  14. package/cpp/patch_core/patch_core.cpp +16 -1
  15. package/cpp/patch_core/patch_core.h +6 -0
  16. package/cpp/patch_core/tests/patch_core_test.cpp +77 -0
  17. package/harmony/pushy/src/main/cpp/pushy.cpp +1 -93
  18. package/harmony/pushy/src/main/ets/DownloadTask.ts +13 -10
  19. package/harmony/pushy/src/main/ets/NativePatchCore.ts +0 -4
  20. package/harmony/pushy/src/main/ets/UpdateContext.ts +20 -2
  21. package/harmony/pushy.har +0 -0
  22. package/ios/RCTPushy/RCTPushy.mm +223 -132
  23. package/ios/RCTPushy/RCTPushyDownloader.mm +22 -1
  24. package/package.json +7 -3
  25. package/src/client.ts +151 -53
  26. package/src/context.ts +21 -7
  27. package/src/core.ts +8 -2
  28. package/src/endpoint.ts +4 -2
  29. package/src/error.ts +68 -0
  30. package/src/index.ts +10 -1
  31. package/src/locales/en.ts +5 -1
  32. package/src/locales/zh.ts +3 -1
  33. package/src/provider.tsx +101 -54
  34. package/src/type.ts +6 -0
  35. package/src/utils.ts +56 -16
  36. package/android/jni/DownloadTask.c +0 -56
  37. package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
package/src/provider.tsx CHANGED
@@ -2,6 +2,7 @@ import React, {
2
2
  ReactNode,
3
3
  useCallback,
4
4
  useEffect,
5
+ useMemo,
5
6
  useRef,
6
7
  useState,
7
8
  } from 'react';
@@ -24,7 +25,7 @@ import {
24
25
  ProgressData,
25
26
  UpdateTestPayload,
26
27
  } from './type';
27
- import { UpdateContext } from './context';
28
+ import { ProgressContext, UpdateContext } from './context';
28
29
  import { URL } from 'react-native-url-polyfill';
29
30
  import { resolveCheckResult } from './resolveCheckResult';
30
31
  import { assertWeb, log, noop } from './utils';
@@ -44,16 +45,6 @@ export const UpdateProvider = ({
44
45
  const updateInfoRef = useRef(updateInfo);
45
46
  const [progress, setProgress] = useState<ProgressData>();
46
47
  const [lastError, setLastError] = useState<Error>();
47
- const lastChecking = useRef(0);
48
-
49
- const throwErrorIfEnabled = useCallback(
50
- (e: Error) => {
51
- if (options.throwError) {
52
- throw e;
53
- }
54
- },
55
- [options.throwError],
56
- );
57
48
 
58
49
  const dismissError = useCallback(() => {
59
50
  setLastError(undefined);
@@ -80,6 +71,25 @@ export const UpdateProvider = ({
80
71
  [options.updateStrategy],
81
72
  );
82
73
 
74
+ // All client errors flow through this single subscription (regardless of
75
+ // throwError), so the catches below only handle flow control and never
76
+ // duplicate the lastError/Alert surfacing.
77
+ useEffect(
78
+ () =>
79
+ client.onError((e, eventType) => {
80
+ setLastError(e);
81
+ alertError(
82
+ client.t(
83
+ eventType === 'errorChecking'
84
+ ? 'error_update_check_failed'
85
+ : 'update_failed',
86
+ ),
87
+ e.message,
88
+ );
89
+ }),
90
+ [client, alertError],
91
+ );
92
+
83
93
  const switchVersion = useCallback(
84
94
  async (info: CheckResult | undefined = updateInfoRef.current) => {
85
95
  if (info && info.hash) {
@@ -117,10 +127,11 @@ export const UpdateProvider = ({
117
127
  return false;
118
128
  }
119
129
  if (options.updateStrategy === 'silentAndNow') {
120
- client.switchVersion(hash);
130
+ // Failures are surfaced via the onError subscription above.
131
+ client.switchVersion(hash).catch(noop);
121
132
  return true;
122
133
  } else if (options.updateStrategy === 'silentAndLater') {
123
- client.switchVersionLater(hash);
134
+ client.switchVersionLater(hash).catch(noop);
124
135
  return true;
125
136
  }
126
137
  alertUpdate(client.t('alert_title'), client.t('alert_update_ready'), [
@@ -128,26 +139,33 @@ export const UpdateProvider = ({
128
139
  text: client.t('alert_next_time'),
129
140
  style: 'cancel',
130
141
  onPress: () => {
131
- client.switchVersionLater(hash);
142
+ client.switchVersionLater(hash).catch(noop);
132
143
  },
133
144
  },
134
145
  {
135
146
  text: client.t('alert_update_now'),
136
147
  style: 'default',
137
148
  onPress: () => {
138
- client.switchVersion(hash);
149
+ client.switchVersion(hash).catch(noop);
139
150
  },
140
151
  },
141
152
  ]);
142
153
  return true;
143
154
  } catch (e: any) {
144
- setLastError(e);
145
- alertError(client.t('update_failed'), e.message);
146
- throwErrorIfEnabled(e);
155
+ // Client pipeline errors carry a code and were already surfaced via
156
+ // the onError subscription; errors thrown by user hooks
157
+ // (afterDownloadUpdate) bypass the pipeline and are surfaced here.
158
+ if (!e?.code) {
159
+ setLastError(e);
160
+ alertError(client.t('update_failed'), e.message);
161
+ }
162
+ if (options.throwError) {
163
+ throw e;
164
+ }
147
165
  return false;
148
166
  }
149
167
  },
150
- [client, options, alertUpdate, alertError, throwErrorIfEnabled],
168
+ [client, options, alertUpdate, alertError],
151
169
  );
152
170
 
153
171
  const downloadAndInstallApk = useCallback(
@@ -161,19 +179,23 @@ export const UpdateProvider = ({
161
179
 
162
180
  const checkUpdate = useCallback(
163
181
  async ({ extra }: { extra?: Partial<{ toHash: string }> } = {}) => {
164
- const now = Date.now();
165
- if (lastChecking.current && now - lastChecking.current < 1000) {
166
- client.notifyAfterCheckUpdate({ status: 'skipped' });
167
- return;
168
- }
169
- lastChecking.current = now;
182
+ // No throttle here: the client already dedupes checks via its 5s
183
+ // response cache, and a second throttle layer silently returned
184
+ // undefined, indistinguishable from a failed check.
170
185
  let rootInfo: CheckResult | undefined;
171
186
  try {
172
187
  rootInfo = await client.checkUpdate(extra);
173
188
  } catch (e: any) {
174
- setLastError(e);
175
- alertError(client.t('error_update_check_failed'), e.message);
176
- throwErrorIfEnabled(e);
189
+ // Client pipeline errors carry a code and were already surfaced via
190
+ // the onError subscription; errors thrown by user hooks
191
+ // (beforeCheckUpdate) bypass the pipeline and are surfaced here.
192
+ if (!e?.code) {
193
+ setLastError(e);
194
+ alertError(client.t('error_update_check_failed'), e.message);
195
+ }
196
+ if (options.throwError) {
197
+ throw e;
198
+ }
177
199
  return;
178
200
  }
179
201
  if (!rootInfo) {
@@ -255,10 +277,9 @@ export const UpdateProvider = ({
255
277
  },
256
278
  [
257
279
  client,
258
- alertError,
259
- throwErrorIfEnabled,
260
280
  options,
261
281
  alertUpdate,
282
+ alertError,
262
283
  downloadAndInstallApk,
263
284
  downloadUpdate,
264
285
  ],
@@ -274,9 +295,11 @@ export const UpdateProvider = ({
274
295
  return;
275
296
  }
276
297
  const { checkStrategy, autoMarkSuccess } = options;
298
+ let markSuccessTimer: ReturnType<typeof setTimeout> | undefined;
277
299
  if (autoMarkSuccess) {
278
- setTimeout(() => {
279
- markSuccess();
300
+ markSuccessTimer = setTimeout(() => {
301
+ // Failures are reported and surfaced via the onError subscription.
302
+ Promise.resolve(markSuccess()).catch(noop);
280
303
  }, 1000);
281
304
  }
282
305
  if (checkStrategy === 'both' || checkStrategy === 'onAppResume') {
@@ -293,6 +316,9 @@ export const UpdateProvider = ({
293
316
  checkUpdate().catch(noop);
294
317
  }
295
318
  return () => {
319
+ if (markSuccessTimer) {
320
+ clearTimeout(markSuccessTimer);
321
+ }
296
322
  stateListener.current && stateListener.current.remove();
297
323
  };
298
324
  }, [checkUpdate, options, dismissError, markSuccess, client]);
@@ -350,7 +376,8 @@ export const UpdateProvider = ({
350
376
  try {
351
377
  const payload = typeof code === 'string' ? JSON.parse(code) : code;
352
378
  return parseTestPayload(payload);
353
- } catch {
379
+ } catch (e: any) {
380
+ log('parseTestQrCode: invalid payload', e?.message || e);
354
381
  return false;
355
382
  }
356
383
  },
@@ -391,28 +418,48 @@ export const UpdateProvider = ({
391
418
  };
392
419
  }, [parseTestPayload]);
393
420
 
421
+ // progress lives in its own context (see context.ts), so this value only
422
+ // changes when the update state itself changes, not on every progress tick.
423
+ const contextValue = useMemo(
424
+ () => ({
425
+ checkUpdate,
426
+ switchVersion,
427
+ switchVersionLater,
428
+ dismissError,
429
+ updateInfo,
430
+ lastError,
431
+ markSuccess,
432
+ client,
433
+ downloadUpdate,
434
+ packageVersion,
435
+ currentHash: currentVersion,
436
+ downloadAndInstallApk,
437
+ getCurrentVersionInfo,
438
+ currentVersionInfo,
439
+ parseTestQrCode,
440
+ restartApp,
441
+ }),
442
+ [
443
+ checkUpdate,
444
+ switchVersion,
445
+ switchVersionLater,
446
+ dismissError,
447
+ updateInfo,
448
+ lastError,
449
+ markSuccess,
450
+ client,
451
+ downloadUpdate,
452
+ downloadAndInstallApk,
453
+ parseTestQrCode,
454
+ restartApp,
455
+ ],
456
+ );
457
+
394
458
  return (
395
- <UpdateContext.Provider
396
- value={{
397
- checkUpdate,
398
- switchVersion,
399
- switchVersionLater,
400
- dismissError,
401
- updateInfo,
402
- lastError,
403
- markSuccess,
404
- client,
405
- downloadUpdate,
406
- packageVersion,
407
- currentHash: currentVersion,
408
- progress,
409
- downloadAndInstallApk,
410
- getCurrentVersionInfo,
411
- currentVersionInfo,
412
- parseTestQrCode,
413
- restartApp,
414
- }}>
415
- {children}
459
+ <UpdateContext.Provider value={contextValue}>
460
+ <ProgressContext.Provider value={progress}>
461
+ {children}
462
+ </ProgressContext.Provider>
416
463
  </UpdateContext.Provider>
417
464
  );
418
465
  };
package/src/type.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { UpdateErrorCode } from './error';
2
+
1
3
  export interface VersionInfo {
2
4
  name: string;
3
5
  hash: string;
@@ -52,6 +54,8 @@ export type EventType =
52
54
  | 'downloadSuccess'
53
55
  | 'errorUpdate'
54
56
  | 'markSuccess'
57
+ | 'errorMarkSuccess'
58
+ | 'errorSwitchVersion'
55
59
  | 'downloadingApk'
56
60
  | 'rejectStoragePermission'
57
61
  | 'errorStoragePermission'
@@ -59,6 +63,8 @@ export type EventType =
59
63
  | 'errorInstallApk';
60
64
 
61
65
  export interface EventData {
66
+ /** Stable machine-readable error code; present on error events */
67
+ code?: UpdateErrorCode;
62
68
  currentVersion: string;
63
69
  cInfo: {
64
70
  rnu: string;
package/src/utils.ts CHANGED
@@ -22,6 +22,10 @@ export const DEFAULT_FETCH_TIMEOUT_MS = 5000;
22
22
 
23
23
  export function promiseAny<T>(promises: Promise<T>[]) {
24
24
  return new Promise<T>((resolve, reject) => {
25
+ if (!promises.length) {
26
+ reject(Error(i18n.t('error_all_promises_rejected')));
27
+ return;
28
+ }
25
29
  let count = 0;
26
30
 
27
31
  promises.forEach(promise => {
@@ -118,21 +122,50 @@ export const fetchWithTimeout = (
118
122
  params: Parameters<typeof fetch>[1],
119
123
  timeoutMs = DEFAULT_FETCH_TIMEOUT_MS,
120
124
  ): Promise<Response> => {
121
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
122
-
123
- return Promise.race([
124
- enhancedFetch(url, params),
125
- new Promise<Response>((_, reject) => {
126
- timeoutId = setTimeout(() => {
127
- log('fetch timeout', url);
128
- reject(Error(i18n.t('error_ping_timeout')));
129
- }, timeoutMs);
130
- }),
131
- ]).finally(() => {
132
- if (timeoutId) {
125
+ // AbortController landed in the RN fetch polyfill around 0.60; we support
126
+ // older peers, so fall back to a plain timer race when it is unavailable
127
+ // (the losing fetch keeps running there — old runtimes can't do better).
128
+ if (typeof AbortController === 'undefined') {
129
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
130
+ return Promise.race([
131
+ enhancedFetch(url, params),
132
+ new Promise<Response>((_, reject) => {
133
+ timeoutId = setTimeout(() => {
134
+ log('fetch timeout', url);
135
+ reject(Error(i18n.t('error_ping_timeout')));
136
+ }, timeoutMs);
137
+ }),
138
+ ]).finally(() => {
139
+ if (timeoutId) {
140
+ clearTimeout(timeoutId);
141
+ }
142
+ });
143
+ }
144
+
145
+ // Abort the underlying request on timeout instead of racing a timer: with
146
+ // Promise.race the losing fetch kept running (and kept the connection busy)
147
+ // long after the caller had already moved on.
148
+ const controller = new AbortController();
149
+ const timeoutId = setTimeout(() => {
150
+ log('fetch timeout', url);
151
+ controller.abort();
152
+ }, timeoutMs);
153
+
154
+ return enhancedFetch(url, { ...params, signal: controller.signal })
155
+ .catch((e: any) => {
156
+ if (controller.signal.aborted) {
157
+ throw Error(i18n.t('error_ping_timeout'));
158
+ }
159
+ throw e;
160
+ })
161
+ .finally(() => {
133
162
  clearTimeout(timeoutId);
134
- }
135
- });
163
+ });
164
+ };
165
+
166
+ const isIdempotentRequest = (params: Parameters<typeof fetch>[1]) => {
167
+ const method = params?.method?.toUpperCase() ?? 'GET';
168
+ return method === 'GET' || method === 'HEAD';
136
169
  };
137
170
 
138
171
  export const enhancedFetch = async (
@@ -143,10 +176,17 @@ export const enhancedFetch = async (
143
176
  return fetch(url, params)
144
177
  .catch(e => {
145
178
  log('fetch error', url, e);
146
- if (isRetry) {
179
+ if (
180
+ isRetry ||
181
+ (params as any)?.signal?.aborted ||
182
+ !url.startsWith('https:') ||
183
+ // Never replay non-idempotent requests (e.g. the checkUpdate POST)
184
+ // over plaintext http: the server may have processed the original.
185
+ !isIdempotentRequest(params)
186
+ ) {
147
187
  throw e;
148
188
  }
149
189
  log('trying fallback to http');
150
- return enhancedFetch(url.replace('https', 'http'), params, true);
190
+ return enhancedFetch(url.replace(/^https:/, 'http:'), params, true);
151
191
  });
152
192
  };
@@ -1,56 +0,0 @@
1
- //
2
- // Created by DengYun on 3/31/16.
3
- //
4
-
5
- #include "cn_reactnative_modules_update_DownloadTask.h"
6
-
7
- #include "hpatch.h"
8
- #define _check(v,errInfo) do{ if (!(v)) { _isError=hpatch_TRUE; _errInfo=errInfo; goto _clear; } }while(0)
9
-
10
- JNIEXPORT jbyteArray JNICALL Java_cn_reactnative_modules_update_DownloadTask_hdiffPatch
11
- (JNIEnv *env, jobject self, jbyteArray origin, jbyteArray patch){
12
- hpatch_BOOL _isError=hpatch_FALSE;
13
- const char* _errInfo="";
14
-
15
- jbyte* originPtr = (*env)->GetByteArrayElements(env, origin, NULL);
16
- size_t originLength = (*env)->GetArrayLength(env, origin);
17
- jbyte* patchPtr = (*env)->GetByteArrayElements(env, patch, NULL);
18
- size_t patchLength = (*env)->GetArrayLength(env, patch);
19
- jbyteArray ret = NULL;
20
- jbyte* outPtr = NULL;
21
- size_t newsize = 0;
22
- hpatch_singleCompressedDiffInfo patInfo;
23
-
24
- _check(((originLength==0)||originPtr) && patchPtr && (patchLength>0),"Corrupt patch");
25
- _check(kHPatch_ok==hpatch_getInfo_by_mem(&patInfo,(const uint8_t*)patchPtr,patchLength),"Error info in hpatch");
26
- _check(originLength==patInfo.oldDataSize,"Error oldDataSize in hpatch");
27
- newsize=(size_t)patInfo.newDataSize;
28
- if (sizeof(size_t)!=sizeof(hpatch_StreamPos_t))
29
- _check(newsize==patInfo.newDataSize,"Error newDataSize in hpatch");
30
-
31
- ret = (*env)->NewByteArray(env,newsize);
32
- _check(ret,"Error JNIEnv::NewByteArray()");
33
- if (newsize>0) {
34
- outPtr = (*env)->GetByteArrayElements(env, ret, NULL);
35
- _check(outPtr,"Corrupt JNIEnv::GetByteArrayElements");
36
- }
37
-
38
- _check(kHPatch_ok==hpatch_by_mem((const uint8_t*)originPtr,originLength,(uint8_t*)outPtr,newsize,
39
- (const uint8_t*)patchPtr,patchLength,&patInfo),"hpacth");
40
-
41
- _clear:
42
- if (outPtr) (*env)->ReleaseByteArrayElements(env, ret, outPtr, (_isError?JNI_ABORT:0));
43
- if (originPtr) (*env)->ReleaseByteArrayElements(env, origin, originPtr, JNI_ABORT);
44
- if (patchPtr) (*env)->ReleaseByteArrayElements(env, patch, patchPtr, JNI_ABORT);
45
- if (_isError){
46
- jclass newExcCls = NULL;
47
- if (ret){
48
- (*env)->DeleteLocalRef(env, ret);
49
- ret = NULL;
50
- }
51
- newExcCls = (*env)->FindClass(env, "java/lang/Error");
52
- if (newExcCls != NULL) // Unable to find the new exception class, give up.
53
- (*env)->ThrowNew(env, newExcCls, _errInfo);
54
- }
55
- return ret;
56
- }
@@ -1,21 +0,0 @@
1
- /* DO NOT EDIT THIS FILE - it is machine generated */
2
- #include <jni.h>
3
- /* Header for class cn_reactnative_modules_update_DownloadTask */
4
-
5
- #ifndef _Included_cn_reactnative_modules_update_DownloadTask
6
- #define _Included_cn_reactnative_modules_update_DownloadTask
7
- #ifdef __cplusplus
8
- extern "C" {
9
- #endif
10
- /*
11
- * Class: cn_reactnative_modules_update_DownloadTask
12
- * Method: hdiffPatch
13
- * Signature: ([B[B)[B
14
- */
15
- JNIEXPORT jbyteArray JNICALL Java_cn_reactnative_modules_update_DownloadTask_hdiffPatch
16
- (JNIEnv *, jclass, jbyteArray, jbyteArray);
17
-
18
- #ifdef __cplusplus
19
- }
20
- #endif
21
- #endif