react-native-update 10.43.2 → 10.44.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 (27) hide show
  1. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +24 -12
  2. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +23 -8
  3. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
  4. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
  5. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +2 -2
  6. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +8 -8
  7. package/cpp/patch_core/archive_patch_core.cpp +16 -0
  8. package/cpp/patch_core/archive_patch_core.h +5 -0
  9. package/cpp/patch_core/jni_util.h +56 -0
  10. package/cpp/patch_core/patch_core_android.cpp +4 -37
  11. package/cpp/patch_core/state_ops.h +24 -0
  12. package/cpp/patch_core/tests/patch_core_test.cpp +32 -2
  13. package/cpp/patch_core/update_core_android.cpp +43 -69
  14. package/harmony/pushy/src/main/cpp/pushy.cpp +162 -35
  15. package/harmony/pushy/src/main/ets/DownloadTask.ts +59 -9
  16. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -2
  17. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +52 -8
  18. package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
  19. package/harmony/pushy.har +0 -0
  20. package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
  21. package/package.json +2 -1
  22. package/src/client.ts +130 -75
  23. package/src/provider.tsx +40 -26
  24. package/src/utils.ts +2 -2
  25. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  26. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  27. package/src/__tests__/setup.ts +0 -44
@@ -68,7 +68,7 @@ export class UpdateContext {
68
68
  */
69
69
  private trace(point: string): void {
70
70
  const snap = this.getStateSnapshot();
71
- logger.info(
71
+ logger.debug(
72
72
  'UpdateContext',
73
73
  `trace id=${this.instanceId} ${point}` +
74
74
  ` pkg=${snap.packageVersion} bt=${snap.buildTime}` +
@@ -91,7 +91,12 @@ export class UpdateContext {
91
91
  name: 'update',
92
92
  });
93
93
  } catch (e) {
94
+ // Fail fast: a missing preferences store means no state can be persisted,
95
+ // which disables rollback protection. Rethrow so the failure surfaces at
96
+ // construction time instead of later as an unrelated TypeError on the
97
+ // undefined `preferences` handle.
94
98
  console.error('Failed to init preferences:', e);
99
+ throw e;
95
100
  }
96
101
  }
97
102
 
@@ -179,17 +184,23 @@ export class UpdateContext {
179
184
  }
180
185
 
181
186
  private flushPreferences(reason: string): void {
182
- try {
183
- const flushablePreferences = this.preferences as FlushablePreferences;
184
- if (typeof flushablePreferences.flushSync === 'function') {
187
+ const flushablePreferences = this.preferences as FlushablePreferences;
188
+ if (typeof flushablePreferences.flushSync === 'function') {
189
+ try {
185
190
  flushablePreferences.flushSync();
186
191
  return;
192
+ } catch (error) {
193
+ console.error(`Failed to flushSync preferences for ${reason}:`, error);
194
+ // fall through to async flush rather than failing the whole operation
187
195
  }
188
- throw Error('preferences.flushSync is not available');
189
- } catch (error) {
190
- console.error(`Failed to flush preferences for ${reason}:`, error);
191
- throw error;
192
196
  }
197
+ // flushSync unavailable or failed: writes are already applied in memory via
198
+ // putSync/deleteSync; persist asynchronously as a best-effort so the state
199
+ // operation still succeeds instead of throwing (which is worse than a
200
+ // slightly delayed persist).
201
+ this.preferences.flush().catch((error: Object) => {
202
+ console.error(`Failed to flush preferences for ${reason}:`, error);
203
+ });
193
204
  }
194
205
 
195
206
  private putNullableString(key: string, value?: string): void {
@@ -509,12 +520,18 @@ export class UpdateContext {
509
520
 
510
521
  public cleanUp(): void {
511
522
  const state = this.getStateSnapshot();
523
+ // cleanupOldEntries now runs on a native worker thread (returns a Promise).
524
+ // Cleanup is best-effort background maintenance and no caller depends on its
525
+ // completion, so fire-and-forget it off the UI thread and just log failures
526
+ // instead of blocking the state operation (or cold start) on disk I/O.
512
527
  NativePatchCore.cleanupOldEntries(
513
528
  this.rootDir,
514
529
  state.currentVersion || '',
515
530
  state.lastVersion || '',
516
531
  3,
517
- );
532
+ ).catch((error: Object) => {
533
+ console.error('cleanupOldEntries failed:', error);
534
+ });
518
535
  }
519
536
 
520
537
  public getIsUsingBundleUrl(): boolean {
package/harmony/pushy.har CHANGED
Binary file
@@ -10,6 +10,8 @@ static NSString *const RCTPushyDownloaderErrorDomain = @"cn.reactnative.pushy";
10
10
  @property (copy) NSString *savePath;
11
11
  @property (nonatomic, strong) NSError *fileError;
12
12
  @property (nonatomic, assign) BOOL finished;
13
+ @property (nonatomic, assign) int lastReportedPercentage;
14
+ @property (nonatomic, assign) long long lastReportedBytes;
13
15
  @end
14
16
 
15
17
  @implementation RCTPushyDownloader
@@ -42,6 +44,10 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
42
44
  }
43
45
 
44
46
  NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
47
+ // Avoid hanging forever on a stalled connection (default resource timeout
48
+ // is 7 days). These are generous enough for large bundles on slow networks.
49
+ sessionConfig.timeoutIntervalForRequest = 30;
50
+ sessionConfig.timeoutIntervalForResource = 300;
45
51
  self.session = [NSURLSession sessionWithConfiguration:sessionConfig
46
52
  delegate:self
47
53
  delegateQueue:nil];
@@ -77,14 +83,47 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
77
83
  totalBytesWritten:(int64_t)totalBytesWritten
78
84
  totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
79
85
  {
80
- if (self.progressHandler) {
81
- self.progressHandler(totalBytesWritten ,totalBytesExpectedToWrite);
86
+ if (!self.progressHandler) {
87
+ return;
88
+ }
89
+ // Normalize an unknown total (NSURLSessionTransferSizeUnknown == -1) to 0 so
90
+ // the JS side does not compute a negative/NaN percentage.
91
+ long long total = totalBytesExpectedToWrite > 0 ? totalBytesExpectedToWrite : 0;
92
+ if (total > 0) {
93
+ int percentage = (int)((totalBytesWritten * 100.0 / total) + 0.5);
94
+ if (percentage <= self.lastReportedPercentage) {
95
+ return;
96
+ }
97
+ self.lastReportedPercentage = percentage;
98
+ } else {
99
+ // Total unknown: throttle by bytes to avoid flooding the bridge.
100
+ if (totalBytesWritten - self.lastReportedBytes < 256 * 1024) {
101
+ return;
102
+ }
103
+ self.lastReportedBytes = totalBytesWritten;
82
104
  }
105
+ self.progressHandler(totalBytesWritten, total);
83
106
  }
84
107
 
85
108
  - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
86
109
  didFinishDownloadingToURL:(NSURL *)location
87
110
  {
111
+ // A completed transfer does not imply success: 404/500 pages, CDN error
112
+ // bodies and captive-portal HTML all arrive here. Reject non-2xx responses
113
+ // before touching savePath so an existing valid package is not destroyed.
114
+ NSURLResponse *response = downloadTask.response;
115
+ if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
116
+ NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
117
+ if (statusCode < 200 || statusCode >= 300) {
118
+ self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
119
+ code:statusCode
120
+ userInfo:@{
121
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"unexpected http status code %ld", (long)statusCode],
122
+ }];
123
+ return;
124
+ }
125
+ }
126
+
88
127
  NSFileManager *fileManager = [NSFileManager defaultManager];
89
128
  [fileManager removeItemAtPath:self.savePath error:nil];
90
129
 
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.43.2",
3
+ "version": "10.44.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
+ "types": "src/index.ts",
6
7
  "scripts": {
7
8
  "prepublishOnly": "NODE_ENV=production bun scripts/prepublish.ts",
8
9
  "publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
package/src/client.ts CHANGED
@@ -30,7 +30,6 @@ import {
30
30
  assertWeb,
31
31
  computeProgress,
32
32
  DEFAULT_FETCH_TIMEOUT_MS,
33
- emptyObj,
34
33
  fetchWithTimeout,
35
34
  info,
36
35
  joinUrls,
@@ -87,6 +86,7 @@ const defaultClientOptions: ClientOptions = {
87
86
 
88
87
  export const sharedState: {
89
88
  progressHandlers: Record<string, EmitterSubscription>;
89
+ downloadingTasks: Record<string, Promise<string | undefined>>;
90
90
  downloadedHash?: string;
91
91
  toHash?: string;
92
92
  apkStatus: 'downloading' | 'downloaded' | null;
@@ -94,6 +94,7 @@ export const sharedState: {
94
94
  applyingUpdate: boolean;
95
95
  } = {
96
96
  progressHandlers: {},
97
+ downloadingTasks: {},
97
98
  downloadedHash: undefined,
98
99
  apkStatus: null,
99
100
  marked: false,
@@ -102,6 +103,7 @@ export const sharedState: {
102
103
 
103
104
  const assertHash = (hash: string) => {
104
105
  if (!sharedState.downloadedHash) {
106
+ log(`no downloaded hash yet, ignore switch to ${hash}`);
105
107
  return;
106
108
  }
107
109
  if (hash !== sharedState.downloadedHash) {
@@ -113,7 +115,7 @@ const assertHash = (hash: string) => {
113
115
 
114
116
  // for China users
115
117
  export class Pushy {
116
- options = defaultClientOptions;
118
+ options = { ...defaultClientOptions };
117
119
  clientType: 'Pushy' | 'Cresc' = 'Pushy';
118
120
  lastChecking?: number;
119
121
  lastRespJson?: Promise<CheckResult>;
@@ -134,7 +136,9 @@ export class Pushy {
134
136
  this.clientType = clientType || 'Pushy';
135
137
  this.options.server = cloneServerConfig(SERVER_PRESETS[this.clientType]);
136
138
 
137
- i18n.setLocale(options.locale ?? this.clientType === 'Pushy' ? 'zh' : 'en');
139
+ i18n.setLocale(
140
+ options.locale ?? (this.clientType === 'Pushy' ? 'zh' : 'en'),
141
+ );
138
142
 
139
143
  if (Platform.OS === 'ios' || Platform.OS === 'android') {
140
144
  if (!options.appKey) {
@@ -348,7 +352,14 @@ export class Pushy {
348
352
  sharedState.applyingUpdate = false;
349
353
  throw e;
350
354
  }
351
- return PushyModule.reloadUpdate({ hash });
355
+ try {
356
+ return await PushyModule.reloadUpdate({ hash });
357
+ } catch (e) {
358
+ // reloadUpdate can reject (e.g. bundle missing); reset the flag so a
359
+ // later retry is not permanently blocked by a stuck applyingUpdate.
360
+ sharedState.applyingUpdate = false;
361
+ throw e;
362
+ }
352
363
  }
353
364
  };
354
365
 
@@ -437,7 +448,10 @@ export class Pushy {
437
448
  });
438
449
  this.notifyAfterCheckUpdate({ status: 'error', error: e });
439
450
  this.throwIfEnabled(e);
440
- return previousRespJson ? await previousRespJson : emptyObj;
451
+ // Fall back to the previous successful response if we have one; otherwise
452
+ // return undefined so callers can distinguish "check failed" from a real
453
+ // empty result and avoid overwriting the last good updateInfo.
454
+ return previousRespJson ? await previousRespJson : undefined;
441
455
  }
442
456
  };
443
457
  getBackupEndpoints = async () => {
@@ -455,16 +469,7 @@ export class Pushy {
455
469
  updateInfo: CheckResult,
456
470
  onDownloadProgress?: (data: ProgressData) => void,
457
471
  ) => {
458
- const {
459
- hash,
460
- diff,
461
- pdiff,
462
- full,
463
- paths = [],
464
- name,
465
- description = '',
466
- metaInfo,
467
- } = updateInfo;
472
+ const { hash } = updateInfo;
468
473
  if (
469
474
  this.options.beforeDownloadUpdate &&
470
475
  (await this.options.beforeDownloadUpdate(updateInfo)) === false
@@ -487,6 +492,39 @@ export class Pushy {
487
492
  log(`duplicated downloaded hash ${sharedState.downloadedHash}, ignored`);
488
493
  return sharedState.downloadedHash;
489
494
  }
495
+ // Deduplicate concurrent downloads of the same hash regardless of whether a
496
+ // progress callback was passed: all callers await the single in-flight
497
+ // promise instead of triggering parallel native downloads.
498
+ const existingTask = sharedState.downloadingTasks[hash];
499
+ if (existingTask) {
500
+ log(`download for hash ${hash} already in progress, reusing it`);
501
+ return existingTask;
502
+ }
503
+ const task = this.performDownload(updateInfo, onDownloadProgress);
504
+ sharedState.downloadingTasks[hash] = task;
505
+ try {
506
+ return await task;
507
+ } finally {
508
+ delete sharedState.downloadingTasks[hash];
509
+ }
510
+ };
511
+ private performDownload = async (
512
+ updateInfo: CheckResult,
513
+ onDownloadProgress?: (data: ProgressData) => void,
514
+ ) => {
515
+ const {
516
+ hash,
517
+ diff,
518
+ pdiff,
519
+ full,
520
+ paths = [],
521
+ name,
522
+ description = '',
523
+ metaInfo,
524
+ } = updateInfo;
525
+ if (!hash) {
526
+ return;
527
+ }
490
528
  if (sharedState.progressHandlers[hash]) {
491
529
  return;
492
530
  }
@@ -525,6 +563,59 @@ export class Pushy {
525
563
  let lastError: any;
526
564
  const errorMessages: string[] = [];
527
565
 
566
+ // Ordered download strategies, tried in sequence until one succeeds. Each
567
+ // resolves its candidate URL lazily (testUrls) and runs the matching native
568
+ // download. diff/pdiff are incremental and skipped entirely in dev; full is
569
+ // attempted whenever a URL exists, and in dev with no URL it is treated as a
570
+ // no-op success so the flow can proceed.
571
+ type DownloadStrategy = {
572
+ name: string;
573
+ candidate: string | undefined;
574
+ errorKey: 'error_diff_failed' | 'error_pdiff_failed' | 'error_full_patch_failed';
575
+ skipInDev: boolean;
576
+ devNoopWhenNoUrl: boolean;
577
+ run: (url: string) => Promise<void>;
578
+ };
579
+ const strategies: DownloadStrategy[] = [
580
+ {
581
+ name: 'diff',
582
+ candidate: diff,
583
+ errorKey: 'error_diff_failed',
584
+ skipInDev: true,
585
+ devNoopWhenNoUrl: false,
586
+ run: url =>
587
+ PushyModule.downloadPatchFromPpk({
588
+ updateUrl: url,
589
+ hash,
590
+ originHash: currentVersion,
591
+ }),
592
+ },
593
+ {
594
+ name: 'pdiff',
595
+ candidate: pdiff,
596
+ errorKey: 'error_pdiff_failed',
597
+ skipInDev: true,
598
+ devNoopWhenNoUrl: false,
599
+ run: url =>
600
+ PushyModule.downloadPatchFromPackage({
601
+ updateUrl: url,
602
+ hash,
603
+ }),
604
+ },
605
+ {
606
+ name: 'full',
607
+ candidate: full,
608
+ errorKey: 'error_full_patch_failed',
609
+ skipInDev: false,
610
+ devNoopWhenNoUrl: true,
611
+ run: url =>
612
+ PushyModule.downloadFullUpdate({
613
+ updateUrl: url,
614
+ hash,
615
+ }),
616
+ },
617
+ ];
618
+
528
619
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
529
620
  if (attempt > 0) {
530
621
  const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000);
@@ -541,66 +632,27 @@ export class Pushy {
541
632
  attempt,
542
633
  },
543
634
  });
544
- const diffUrl = await testUrls(joinUrls(paths, diff));
545
- if (diffUrl && !__DEV__) {
546
- log('downloading diff');
547
- try {
548
- await PushyModule.downloadPatchFromPpk({
549
- updateUrl: diffUrl,
550
- hash,
551
- originHash: currentVersion,
552
- });
553
- succeeded = 'diff';
554
- } catch (e: any) {
555
- const errorMessage = this.t('error_diff_failed', {
556
- message: e.message,
557
- });
558
- errorMessages.push(errorMessage);
559
- lastError = Error(errorMessage);
560
- log(errorMessage);
561
- }
562
- }
563
- if (!succeeded) {
564
- const pdiffUrl = await testUrls(joinUrls(paths, pdiff));
565
- if (pdiffUrl && !__DEV__) {
566
- log('downloading pdiff');
567
- try {
568
- await PushyModule.downloadPatchFromPackage({
569
- updateUrl: pdiffUrl,
570
- hash,
571
- });
572
- succeeded = 'pdiff';
573
- } catch (e: any) {
574
- const errorMessage = this.t('error_pdiff_failed', {
575
- message: e.message,
576
- });
577
- errorMessages.push(errorMessage);
578
- lastError = Error(errorMessage);
579
- log(errorMessage);
580
- }
635
+ for (const strategy of strategies) {
636
+ if (succeeded) {
637
+ break;
581
638
  }
582
- }
583
- if (!succeeded) {
584
- const fullUrl = await testUrls(joinUrls(paths, full));
585
- if (fullUrl) {
586
- log('downloading full patch');
639
+ const url = await testUrls(joinUrls(paths, strategy.candidate));
640
+ if (url && !(strategy.skipInDev && __DEV__)) {
641
+ log(`downloading ${strategy.name}`);
587
642
  try {
588
- await PushyModule.downloadFullUpdate({
589
- updateUrl: fullUrl,
590
- hash,
591
- });
592
- succeeded = 'full';
643
+ await strategy.run(url);
644
+ succeeded = strategy.name;
593
645
  } catch (e: any) {
594
- const errorMessage = this.t('error_full_patch_failed', {
646
+ const errorMessage = this.t(strategy.errorKey, {
595
647
  message: e.message,
596
648
  });
597
649
  errorMessages.push(errorMessage);
598
650
  lastError = Error(errorMessage);
599
651
  log(errorMessage);
600
652
  }
601
- } else if (__DEV__) {
653
+ } else if (!url && strategy.devNoopWhenNoUrl && __DEV__) {
602
654
  log(this.t('dev_incremental_update_disabled'));
603
- succeeded = 'full';
655
+ succeeded = strategy.name;
604
656
  }
605
657
  }
606
658
  if (succeeded) {
@@ -697,19 +749,22 @@ export class Pushy {
697
749
  },
698
750
  );
699
751
  }
700
- await PushyModule.downloadAndInstallApk({
701
- url,
702
- target: 'update.apk',
703
- hash: progressKey,
704
- }).catch(() => {
752
+ try {
753
+ await PushyModule.downloadAndInstallApk({
754
+ url,
755
+ target: 'update.apk',
756
+ hash: progressKey,
757
+ });
758
+ sharedState.apkStatus = 'downloaded';
759
+ } catch {
705
760
  sharedState.apkStatus = null;
706
761
  this.report({ type: 'errorDownloadAndInstallApk' });
707
762
  this.throwIfEnabled(Error('errorDownloadAndInstallApk'));
708
- });
709
- sharedState.apkStatus = 'downloaded';
710
- if (sharedState.progressHandlers[progressKey]) {
711
- sharedState.progressHandlers[progressKey].remove();
712
- delete sharedState.progressHandlers[progressKey];
763
+ } finally {
764
+ if (sharedState.progressHandlers[progressKey]) {
765
+ sharedState.progressHandlers[progressKey].remove();
766
+ delete sharedState.progressHandlers[progressKey];
767
+ }
713
768
  }
714
769
  };
715
770
  restartApp = async () => {
package/src/provider.tsx CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  import { UpdateContext } from './context';
28
28
  import { URL } from 'react-native-url-polyfill';
29
29
  import { resolveCheckResult } from './resolveCheckResult';
30
- import { assertWeb, log } from './utils';
30
+ import { assertWeb, log, noop } from './utils';
31
31
 
32
32
  export const UpdateProvider = ({
33
33
  client,
@@ -108,7 +108,6 @@ export const UpdateProvider = ({
108
108
  if (!hash) {
109
109
  return false;
110
110
  }
111
- stateListener.current && stateListener.current.remove();
112
111
 
113
112
  if (
114
113
  options.afterDownloadUpdate &&
@@ -170,7 +169,7 @@ export const UpdateProvider = ({
170
169
  lastChecking.current = now;
171
170
  let rootInfo: CheckResult | undefined;
172
171
  try {
173
- rootInfo = { ...(await client.checkUpdate(extra)) };
172
+ rootInfo = await client.checkUpdate(extra);
174
173
  } catch (e: any) {
175
174
  setLastError(e);
176
175
  alertError(client.t('error_update_check_failed'), e.message);
@@ -178,6 +177,8 @@ export const UpdateProvider = ({
178
177
  return;
179
178
  }
180
179
  if (!rootInfo) {
180
+ // Check was skipped or failed with no cached result; keep the last
181
+ // known updateInfo instead of overwriting it with an empty object.
181
182
  return;
182
183
  }
183
184
  const info = resolveCheckResult(rootInfo);
@@ -198,7 +199,7 @@ export const UpdateProvider = ({
198
199
  if (downloadUrl && sharedState.apkStatus === null) {
199
200
  if (options.updateStrategy === 'silentAndNow') {
200
201
  if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
201
- downloadAndInstallApk(downloadUrl);
202
+ downloadAndInstallApk(downloadUrl).catch(noop);
202
203
  } else {
203
204
  Linking.openURL(downloadUrl);
204
205
  }
@@ -215,7 +216,7 @@ export const UpdateProvider = ({
215
216
  Platform.OS === 'android' &&
216
217
  downloadUrl.endsWith('.apk')
217
218
  ) {
218
- downloadAndInstallApk(downloadUrl);
219
+ downloadAndInstallApk(downloadUrl).catch(noop);
219
220
  } else {
220
221
  Linking.openURL(downloadUrl);
221
222
  }
@@ -229,7 +230,7 @@ export const UpdateProvider = ({
229
230
  options.updateStrategy === 'silentAndNow' ||
230
231
  options.updateStrategy === 'silentAndLater'
231
232
  ) {
232
- downloadUpdate(info);
233
+ downloadUpdate(info).catch(noop);
233
234
  return info;
234
235
  }
235
236
  alertUpdate(
@@ -244,7 +245,7 @@ export const UpdateProvider = ({
244
245
  text: client.t('alert_confirm'),
245
246
  style: 'default',
246
247
  onPress: () => {
247
- downloadUpdate();
248
+ downloadUpdate().catch(noop);
248
249
  },
249
250
  },
250
251
  ],
@@ -272,7 +273,7 @@ export const UpdateProvider = ({
272
273
  if (!assertWeb()) {
273
274
  return;
274
275
  }
275
- const { checkStrategy, dismissErrorAfter, autoMarkSuccess } = options;
276
+ const { checkStrategy, autoMarkSuccess } = options;
276
277
  if (autoMarkSuccess) {
277
278
  setTimeout(() => {
278
279
  markSuccess();
@@ -283,26 +284,35 @@ export const UpdateProvider = ({
283
284
  'change',
284
285
  nextAppState => {
285
286
  if (nextAppState === 'active') {
286
- checkUpdate();
287
+ checkUpdate().catch(noop);
287
288
  }
288
289
  },
289
290
  );
290
291
  }
291
292
  if (checkStrategy === 'both' || checkStrategy === 'onAppStart') {
292
- checkUpdate();
293
- }
294
- let dismissErrorTimer: ReturnType<typeof setTimeout>;
295
- if (typeof dismissErrorAfter === 'number' && dismissErrorAfter > 0) {
296
- dismissErrorTimer = setTimeout(() => {
297
- dismissError();
298
- }, dismissErrorAfter);
293
+ checkUpdate().catch(noop);
299
294
  }
300
295
  return () => {
301
296
  stateListener.current && stateListener.current.remove();
302
- clearTimeout(dismissErrorTimer);
303
297
  };
304
298
  }, [checkUpdate, options, dismissError, markSuccess, client]);
305
299
 
300
+ useEffect(() => {
301
+ const { dismissErrorAfter } = options;
302
+ if (
303
+ lastError &&
304
+ typeof dismissErrorAfter === 'number' &&
305
+ dismissErrorAfter > 0
306
+ ) {
307
+ const dismissErrorTimer = setTimeout(() => {
308
+ dismissError();
309
+ }, dismissErrorAfter);
310
+ return () => {
311
+ clearTimeout(dismissErrorTimer);
312
+ };
313
+ }
314
+ }, [lastError, options, dismissError]);
315
+
306
316
  const parseTestPayload = useCallback(
307
317
  (payload: UpdateTestPayload) => {
308
318
  if (payload && payload.type && payload.type.startsWith('__rnPushy')) {
@@ -314,15 +324,19 @@ export const UpdateProvider = ({
314
324
  if (payload.type === '__rnPushyVersionHash') {
315
325
  const toHash = payload.data;
316
326
  sharedState.toHash = toHash;
317
- checkUpdate({ extra: { toHash } }).then(() => {
318
- if (updateInfoRef.current && updateInfoRef.current.upToDate) {
319
- Alert.alert(
320
- client.t('alert_info'),
321
- client.t('alert_no_update_wait'),
322
- );
323
- }
324
- options.logger = logger;
325
- });
327
+ checkUpdate({ extra: { toHash } })
328
+ .then(() => {
329
+ if (updateInfoRef.current && updateInfoRef.current.upToDate) {
330
+ Alert.alert(
331
+ client.t('alert_info'),
332
+ client.t('alert_no_update_wait'),
333
+ );
334
+ }
335
+ })
336
+ .catch(noop)
337
+ .finally(() => {
338
+ options.logger = logger;
339
+ });
326
340
  }
327
341
  return true;
328
342
  }
package/src/utils.ts CHANGED
@@ -84,7 +84,7 @@ export function joinUrls(paths: string[], fileName?: string) {
84
84
  }
85
85
  }
86
86
 
87
- export const testUrls = async (urls?: string[]) => {
87
+ export const testUrls = async (urls?: string[]): Promise<string | null> => {
88
88
  if (!urls?.length) {
89
89
  return null;
90
90
  }
@@ -93,7 +93,7 @@ export const testUrls = async (urls?: string[]) => {
93
93
  const ret = await promiseAny(urls.map(ping));
94
94
  if (ret) {
95
95
  log('ping success, use url:', ret);
96
- return ret;
96
+ return ret as string;
97
97
  }
98
98
  } catch {}
99
99
  log('all ping failed, use first url:', urls[0]);