react-native-update 10.43.3 → 10.45.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/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/BundledResourceCopier.java +24 -12
  7. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +33 -9
  8. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
  9. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
  10. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +8 -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 +8 -8
  13. package/cpp/patch_core/archive_patch_core.cpp +16 -0
  14. package/cpp/patch_core/archive_patch_core.h +5 -0
  15. package/cpp/patch_core/jni_util.h +56 -0
  16. package/cpp/patch_core/patch_core.cpp +16 -1
  17. package/cpp/patch_core/patch_core.h +6 -0
  18. package/cpp/patch_core/patch_core_android.cpp +4 -37
  19. package/cpp/patch_core/state_ops.h +24 -0
  20. package/cpp/patch_core/tests/patch_core_test.cpp +109 -2
  21. package/cpp/patch_core/update_core_android.cpp +43 -69
  22. package/harmony/pushy/src/main/cpp/pushy.cpp +163 -128
  23. package/harmony/pushy/src/main/ets/DownloadTask.ts +72 -19
  24. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -6
  25. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +10 -10
  26. package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
  27. package/harmony/pushy.har +0 -0
  28. package/ios/RCTPushy/RCTPushy.mm +142 -111
  29. package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
  30. package/package.json +7 -3
  31. package/src/client.ts +161 -101
  32. package/src/context.ts +21 -7
  33. package/src/core.ts +5 -1
  34. package/src/index.ts +7 -1
  35. package/src/provider.tsx +91 -56
  36. package/src/utils.ts +58 -18
  37. package/android/jni/DownloadTask.c +0 -56
  38. package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
  39. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  40. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  41. package/src/__tests__/setup.ts +0 -44
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) {
@@ -187,23 +191,39 @@ export class Pushy {
187
191
  data?: Record<string, string | number>;
188
192
  }) => {
189
193
  log(`${type} ${message}`);
190
- await this.loggerPromise.promise;
194
+ if (this.options.logger === noop) {
195
+ // Wait briefly for a logger to arrive via setOptions (e.g. the rollback
196
+ // report fires in the constructor before the user configures one), but
197
+ // give up after a bound instead of retaining the closure forever when
198
+ // no logger is ever provided.
199
+ await Promise.race([
200
+ this.loggerPromise.promise,
201
+ new Promise(resolve => setTimeout(resolve, 10 * 1000)),
202
+ ]);
203
+ }
191
204
  const { logger = noop, appKey } = this.options;
192
205
  const overridePackageVersion = this.options.overridePackageVersion;
193
- logger({
194
- type,
195
- data: {
196
- appKey,
197
- currentVersion,
198
- cInfo,
199
- packageVersion,
200
- overridePackageVersion,
201
- buildTime,
202
- message,
203
- ...currentVersionInfo,
204
- ...data,
205
- },
206
- });
206
+ try {
207
+ logger({
208
+ type,
209
+ data: {
210
+ appKey,
211
+ currentVersion,
212
+ cInfo,
213
+ packageVersion,
214
+ overridePackageVersion,
215
+ buildTime,
216
+ message,
217
+ ...currentVersionInfo,
218
+ ...data,
219
+ },
220
+ });
221
+ } catch (e: any) {
222
+ // A user-provided logger must never break the update flow, and report()
223
+ // calls are fire-and-forget so a throw here would be an unhandled
224
+ // rejection.
225
+ log('logger error:', e?.message || e);
226
+ }
207
227
  };
208
228
  throwIfEnabled = (e: Error) => {
209
229
  if (this.options.throwError) {
@@ -348,7 +368,14 @@ export class Pushy {
348
368
  sharedState.applyingUpdate = false;
349
369
  throw e;
350
370
  }
351
- return PushyModule.reloadUpdate({ hash });
371
+ try {
372
+ return await PushyModule.reloadUpdate({ hash });
373
+ } catch (e) {
374
+ // reloadUpdate can reject (e.g. bundle missing); reset the flag so a
375
+ // later retry is not permanently blocked by a stuck applyingUpdate.
376
+ sharedState.applyingUpdate = false;
377
+ throw e;
378
+ }
352
379
  }
353
380
  };
354
381
 
@@ -437,34 +464,17 @@ export class Pushy {
437
464
  });
438
465
  this.notifyAfterCheckUpdate({ status: 'error', error: e });
439
466
  this.throwIfEnabled(e);
440
- return previousRespJson ? await previousRespJson : emptyObj;
467
+ // Fall back to the previous successful response if we have one; otherwise
468
+ // return undefined so callers can distinguish "check failed" from a real
469
+ // empty result and avoid overwriting the last good updateInfo.
470
+ return previousRespJson ? await previousRespJson : undefined;
441
471
  }
442
472
  };
443
- getBackupEndpoints = async () => {
444
- const { server } = this.options;
445
- if (!server) {
446
- return [];
447
- }
448
- const remoteEndpoints = await this.getRemoteEndpoints();
449
- return excludeConfiguredEndpoints(
450
- dedupeEndpoints(remoteEndpoints),
451
- this.getConfiguredCheckEndpoints(),
452
- );
453
- };
454
473
  downloadUpdate = async (
455
474
  updateInfo: CheckResult,
456
475
  onDownloadProgress?: (data: ProgressData) => void,
457
476
  ) => {
458
- const {
459
- hash,
460
- diff,
461
- pdiff,
462
- full,
463
- paths = [],
464
- name,
465
- description = '',
466
- metaInfo,
467
- } = updateInfo;
477
+ const { hash } = updateInfo;
468
478
  if (
469
479
  this.options.beforeDownloadUpdate &&
470
480
  (await this.options.beforeDownloadUpdate(updateInfo)) === false
@@ -487,6 +497,39 @@ export class Pushy {
487
497
  log(`duplicated downloaded hash ${sharedState.downloadedHash}, ignored`);
488
498
  return sharedState.downloadedHash;
489
499
  }
500
+ // Deduplicate concurrent downloads of the same hash regardless of whether a
501
+ // progress callback was passed: all callers await the single in-flight
502
+ // promise instead of triggering parallel native downloads.
503
+ const existingTask = sharedState.downloadingTasks[hash];
504
+ if (existingTask) {
505
+ log(`download for hash ${hash} already in progress, reusing it`);
506
+ return existingTask;
507
+ }
508
+ const task = this.performDownload(updateInfo, onDownloadProgress);
509
+ sharedState.downloadingTasks[hash] = task;
510
+ try {
511
+ return await task;
512
+ } finally {
513
+ delete sharedState.downloadingTasks[hash];
514
+ }
515
+ };
516
+ private performDownload = async (
517
+ updateInfo: CheckResult,
518
+ onDownloadProgress?: (data: ProgressData) => void,
519
+ ) => {
520
+ const {
521
+ hash,
522
+ diff,
523
+ pdiff,
524
+ full,
525
+ paths = [],
526
+ name,
527
+ description = '',
528
+ metaInfo,
529
+ } = updateInfo;
530
+ if (!hash) {
531
+ return;
532
+ }
490
533
  if (sharedState.progressHandlers[hash]) {
491
534
  return;
492
535
  }
@@ -525,6 +568,59 @@ export class Pushy {
525
568
  let lastError: any;
526
569
  const errorMessages: string[] = [];
527
570
 
571
+ // Ordered download strategies, tried in sequence until one succeeds. Each
572
+ // resolves its candidate URL lazily (testUrls) and runs the matching native
573
+ // download. diff/pdiff are incremental and skipped entirely in dev; full is
574
+ // attempted whenever a URL exists, and in dev with no URL it is treated as a
575
+ // no-op success so the flow can proceed.
576
+ type DownloadStrategy = {
577
+ name: string;
578
+ candidate: string | undefined;
579
+ errorKey: 'error_diff_failed' | 'error_pdiff_failed' | 'error_full_patch_failed';
580
+ skipInDev: boolean;
581
+ devNoopWhenNoUrl: boolean;
582
+ run: (url: string) => Promise<void>;
583
+ };
584
+ const strategies: DownloadStrategy[] = [
585
+ {
586
+ name: 'diff',
587
+ candidate: diff,
588
+ errorKey: 'error_diff_failed',
589
+ skipInDev: true,
590
+ devNoopWhenNoUrl: false,
591
+ run: url =>
592
+ PushyModule.downloadPatchFromPpk({
593
+ updateUrl: url,
594
+ hash,
595
+ originHash: currentVersion,
596
+ }),
597
+ },
598
+ {
599
+ name: 'pdiff',
600
+ candidate: pdiff,
601
+ errorKey: 'error_pdiff_failed',
602
+ skipInDev: true,
603
+ devNoopWhenNoUrl: false,
604
+ run: url =>
605
+ PushyModule.downloadPatchFromPackage({
606
+ updateUrl: url,
607
+ hash,
608
+ }),
609
+ },
610
+ {
611
+ name: 'full',
612
+ candidate: full,
613
+ errorKey: 'error_full_patch_failed',
614
+ skipInDev: false,
615
+ devNoopWhenNoUrl: true,
616
+ run: url =>
617
+ PushyModule.downloadFullUpdate({
618
+ updateUrl: url,
619
+ hash,
620
+ }),
621
+ },
622
+ ];
623
+
528
624
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
529
625
  if (attempt > 0) {
530
626
  const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000);
@@ -541,66 +637,27 @@ export class Pushy {
541
637
  attempt,
542
638
  },
543
639
  });
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);
640
+ for (const strategy of strategies) {
641
+ if (succeeded) {
642
+ break;
561
643
  }
562
- }
563
- if (!succeeded) {
564
- const pdiffUrl = await testUrls(joinUrls(paths, pdiff));
565
- if (pdiffUrl && !__DEV__) {
566
- log('downloading pdiff');
644
+ const url = await testUrls(joinUrls(paths, strategy.candidate));
645
+ if (url && !(strategy.skipInDev && __DEV__)) {
646
+ log(`downloading ${strategy.name}`);
567
647
  try {
568
- await PushyModule.downloadPatchFromPackage({
569
- updateUrl: pdiffUrl,
570
- hash,
571
- });
572
- succeeded = 'pdiff';
648
+ await strategy.run(url);
649
+ succeeded = strategy.name;
573
650
  } catch (e: any) {
574
- const errorMessage = this.t('error_pdiff_failed', {
651
+ const errorMessage = this.t(strategy.errorKey, {
575
652
  message: e.message,
576
653
  });
577
654
  errorMessages.push(errorMessage);
578
655
  lastError = Error(errorMessage);
579
656
  log(errorMessage);
580
657
  }
581
- }
582
- }
583
- if (!succeeded) {
584
- const fullUrl = await testUrls(joinUrls(paths, full));
585
- if (fullUrl) {
586
- log('downloading full patch');
587
- try {
588
- await PushyModule.downloadFullUpdate({
589
- updateUrl: fullUrl,
590
- hash,
591
- });
592
- succeeded = 'full';
593
- } catch (e: any) {
594
- const errorMessage = this.t('error_full_patch_failed', {
595
- message: e.message,
596
- });
597
- errorMessages.push(errorMessage);
598
- lastError = Error(errorMessage);
599
- log(errorMessage);
600
- }
601
- } else if (__DEV__) {
658
+ } else if (!url && strategy.devNoopWhenNoUrl && __DEV__) {
602
659
  log(this.t('dev_incremental_update_disabled'));
603
- succeeded = 'full';
660
+ succeeded = strategy.name;
604
661
  }
605
662
  }
606
663
  if (succeeded) {
@@ -697,19 +754,22 @@ export class Pushy {
697
754
  },
698
755
  );
699
756
  }
700
- await PushyModule.downloadAndInstallApk({
701
- url,
702
- target: 'update.apk',
703
- hash: progressKey,
704
- }).catch(() => {
757
+ try {
758
+ await PushyModule.downloadAndInstallApk({
759
+ url,
760
+ target: 'update.apk',
761
+ hash: progressKey,
762
+ });
763
+ sharedState.apkStatus = 'downloaded';
764
+ } catch {
705
765
  sharedState.apkStatus = null;
706
766
  this.report({ type: 'errorDownloadAndInstallApk' });
707
767
  this.throwIfEnabled(Error('errorDownloadAndInstallApk'));
708
- });
709
- sharedState.apkStatus = 'downloaded';
710
- if (sharedState.progressHandlers[progressKey]) {
711
- sharedState.progressHandlers[progressKey].remove();
712
- delete sharedState.progressHandlers[progressKey];
768
+ } finally {
769
+ if (sharedState.progressHandlers[progressKey]) {
770
+ sharedState.progressHandlers[progressKey].remove();
771
+ delete sharedState.progressHandlers[progressKey];
772
+ }
713
773
  }
714
774
  };
715
775
  restartApp = async () => {
package/src/context.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext } from 'react';
1
+ import { createContext, useContext, useMemo } from 'react';
2
2
  import { CheckResult, ProgressData } from './type';
3
3
  import { Pushy, Cresc } from './client';
4
4
  import i18n from './i18n';
@@ -46,21 +46,35 @@ export const UpdateContext = createContext<{
46
46
  currentHash: string;
47
47
  packageVersion: string;
48
48
  client?: Pushy | Cresc;
49
- progress?: ProgressData;
50
49
  updateInfo?: CheckResult;
51
50
  lastError?: Error;
52
51
  }>(defaultContext);
53
52
 
54
- export const useUpdate = __DEV__ ? () => {
53
+ // Download progress ticks at high frequency, so it lives in its own context;
54
+ // otherwise every tick would re-render all useUpdate() consumers even when
55
+ // they never read progress.
56
+ export const ProgressContext = createContext<ProgressData | undefined>(
57
+ undefined,
58
+ );
59
+
60
+ /**
61
+ * Subscribe to download progress only. Components that render a progress bar
62
+ * should prefer this over useUpdate() so the rest of the tree is not
63
+ * re-rendered on every progress event.
64
+ */
65
+ export const useUpdateProgress = () => useContext(ProgressContext);
66
+
67
+ export const useUpdate = () => {
55
68
  const context = useContext(UpdateContext);
69
+ const progress = useContext(ProgressContext);
56
70
 
57
- // 检查是否在 UpdateProvider 内部使用
58
- if (!context.client) {
71
+ if (__DEV__ && !context.client) {
72
+ // 检查是否在 UpdateProvider 内部使用
59
73
  throw new Error(i18n.t('error_use_update_outside_provider'));
60
74
  }
61
75
 
62
- return context;
63
- } : () => useContext(UpdateContext);
76
+ return useMemo(() => ({ ...context, progress }), [context, progress]);
77
+ };
64
78
 
65
79
  /** @deprecated Please use `useUpdate` instead */
66
80
  export const usePushy = useUpdate;
package/src/core.ts CHANGED
@@ -82,7 +82,11 @@ export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule);
82
82
 
83
83
  if (!uuid) {
84
84
  uuid = require('nanoid/non-secure').nanoid();
85
- PushyModule.setUuid(uuid);
85
+ // If persisting fails the uuid drifts on every launch, which skews gray
86
+ // release bucketing and inflates stats — log it instead of failing silently.
87
+ Promise.resolve(PushyModule.setUuid(uuid)).catch((e: any) => {
88
+ log('setUuid error:', e?.message || e);
89
+ });
86
90
  }
87
91
 
88
92
  export const cInfo = {
package/src/index.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  export { Pushy, Cresc } from './client';
2
- export { UpdateContext, usePushy, useUpdate } from './context';
2
+ export {
3
+ ProgressContext,
4
+ UpdateContext,
5
+ usePushy,
6
+ useUpdate,
7
+ useUpdateProgress,
8
+ } from './context';
3
9
  export { PushyProvider, UpdateProvider } from './provider';
4
10
  export { PushyModule, UpdateModule } from './core';