react-native-update 10.42.3 → 10.42.5

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.
@@ -7,7 +7,6 @@ import { bundleManager } from '@kit.AbilityKit';
7
7
  import logger from './Logger';
8
8
  import { UpdateContext } from './UpdateContext';
9
9
  import { EventHub } from './EventHub';
10
- import { util } from '@kit.ArkTS';
11
10
 
12
11
  const TAG = 'PushyTurboModule';
13
12
 
@@ -47,33 +46,6 @@ export class PushyTurboModule extends UITurboModule {
47
46
  return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
48
47
  }
49
48
 
50
- private getPackageVersion(): string {
51
- try {
52
- const bundleInfo = bundleManager.getBundleInfoForSelfSync(
53
- this.getBundleFlags(),
54
- );
55
- return bundleInfo?.versionName || 'Unknown';
56
- } catch (error) {
57
- console.error('Failed to get bundle info:', error);
58
- return '';
59
- }
60
- }
61
-
62
- private getBuildTime(): string {
63
- try {
64
- const content = this.mUiCtx.resourceManager.getRawFileContentSync(
65
- 'meta.json',
66
- );
67
- const metaData = JSON.parse(
68
- new util.TextDecoder().decodeToString(content),
69
- ) as Record<string, string | number | boolean | null | undefined>;
70
- if (metaData.pushy_build_time) {
71
- return String(metaData.pushy_build_time);
72
- }
73
- } catch {}
74
- return '';
75
- }
76
-
77
49
  private requireHash(hash: string, methodName: string): string {
78
50
  if (!hash) {
79
51
  throw Error(`${methodName}: empty hash`);
@@ -95,15 +67,15 @@ export class PushyTurboModule extends UITurboModule {
95
67
 
96
68
  getConstants(): Object {
97
69
  logger.debug(TAG, ',call getConstants');
98
- const packageVersion = this.getPackageVersion();
99
- const buildTime = this.getBuildTime();
70
+ const packageVersion = this.context.getPackageVersion();
71
+ const buildTime = this.context.getBuildTime();
100
72
  this.context.syncStateWithBinaryVersion(packageVersion, buildTime);
101
73
 
102
74
  const currentVersion = this.context.getCurrentVersion();
103
75
  const currentVersionInfo = currentVersion
104
76
  ? this.context.getKv(`hash_${currentVersion}`)
105
77
  : '';
106
- const isFirstTime = this.context.isFirstTime();
78
+ const isFirstTime = this.context.consumeFirstLoadMarker();
107
79
  const rolledBackVersion = this.context.rolledBackVersion();
108
80
  const uuid = this.context.getKv('uuid');
109
81
  const isUsingBundleUrl = this.context.getIsUsingBundleUrl();
@@ -3,6 +3,8 @@ import fileIo from '@ohos.file.fs';
3
3
  import { DownloadTask } from './DownloadTask';
4
4
  import common from '@ohos.app.ability.common';
5
5
  import { DownloadTaskParams } from './DownloadTaskParams';
6
+ import { bundleManager } from '@kit.AbilityKit';
7
+ import { util } from '@kit.ArkTS';
6
8
  import NativePatchCore, {
7
9
  STATE_OP_CLEAR_FIRST_TIME,
8
10
  STATE_OP_CLEAR_ROLLBACK_MARK,
@@ -13,12 +15,17 @@ import NativePatchCore, {
13
15
  StateCoreResult,
14
16
  } from './NativePatchCore';
15
17
 
18
+ type FlushablePreferences = preferences.Preferences & {
19
+ flushSync?: () => void;
20
+ };
21
+
16
22
  export class UpdateContext {
17
23
  private context: common.UIAbilityContext;
18
24
  private rootDir: string;
19
25
  private preferences!: preferences.Preferences;
20
26
  private static DEBUG: boolean = false;
21
27
  private static isUsingBundleUrl: boolean = false;
28
+ private static ignoreRollback: boolean = false;
22
29
 
23
30
  constructor(context: common.UIAbilityContext) {
24
31
  this.context = context;
@@ -32,6 +39,10 @@ export class UpdateContext {
32
39
  console.error('Failed to create root directory:', e);
33
40
  }
34
41
  this.initPreferences();
42
+ this.syncStateWithBinaryVersion(
43
+ this.getPackageVersion(),
44
+ this.getBuildTime(),
45
+ );
35
46
  }
36
47
 
37
48
  private initPreferences() {
@@ -44,6 +55,36 @@ export class UpdateContext {
44
55
  }
45
56
  }
46
57
 
58
+ private getBundleFlags(): bundleManager.BundleFlag {
59
+ return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
60
+ }
61
+
62
+ public getPackageVersion(): string {
63
+ try {
64
+ const bundleInfo = bundleManager.getBundleInfoForSelfSync(
65
+ this.getBundleFlags(),
66
+ );
67
+ return bundleInfo?.versionName || 'Unknown';
68
+ } catch (error) {
69
+ console.error('Failed to get bundle info:', error);
70
+ return '';
71
+ }
72
+ }
73
+
74
+ public getBuildTime(): string {
75
+ try {
76
+ const content =
77
+ this.context.resourceManager.getRawFileContentSync('meta.json');
78
+ const metaData = JSON.parse(
79
+ new util.TextDecoder().decodeToString(content),
80
+ ) as Record<string, string | number | boolean | null | undefined>;
81
+ if (metaData.pushy_build_time) {
82
+ return String(metaData.pushy_build_time);
83
+ }
84
+ } catch {}
85
+ return '';
86
+ }
87
+
47
88
  private readString(key: string): string {
48
89
  const value = this.preferences.getSync(key, '') as
49
90
  | string
@@ -87,6 +128,20 @@ export class UpdateContext {
87
128
  return defaultValue;
88
129
  }
89
130
 
131
+ private flushPreferences(reason: string): void {
132
+ try {
133
+ const flushablePreferences = this.preferences as FlushablePreferences;
134
+ if (typeof flushablePreferences.flushSync === 'function') {
135
+ flushablePreferences.flushSync();
136
+ return;
137
+ }
138
+ throw Error('preferences.flushSync is not available');
139
+ } catch (error) {
140
+ console.error(`Failed to flush preferences for ${reason}:`, error);
141
+ throw error;
142
+ }
143
+ }
144
+
90
145
  private putNullableString(key: string, value?: string): void {
91
146
  if (value) {
92
147
  this.preferences.putSync(key, value);
@@ -127,6 +182,8 @@ export class UpdateContext {
127
182
  clearExisting?: boolean;
128
183
  removeStaleHash?: boolean;
129
184
  cleanUp?: boolean;
185
+ markFirstLoadMarker?: boolean;
186
+ clearFirstLoadMarker?: boolean;
130
187
  } = {},
131
188
  ): void {
132
189
  if (options.clearExisting) {
@@ -136,7 +193,13 @@ export class UpdateContext {
136
193
  if (options.removeStaleHash && state.staleVersionToDelete) {
137
194
  this.preferences.deleteSync(`hash_${state.staleVersionToDelete}`);
138
195
  }
139
- this.preferences.flush();
196
+ if (options.markFirstLoadMarker) {
197
+ this.preferences.putSync('firstLoadMarked', 'true');
198
+ }
199
+ if (options.clearFirstLoadMarker) {
200
+ this.preferences.deleteSync('firstLoadMarked');
201
+ }
202
+ this.flushPreferences('persist state');
140
203
  if (options.cleanUp) {
141
204
  this.cleanUp();
142
205
  }
@@ -148,6 +211,7 @@ export class UpdateContext {
148
211
  options: {
149
212
  removeStaleHash?: boolean;
150
213
  cleanUp?: boolean;
214
+ clearFirstLoadMarker?: boolean;
151
215
  } = {},
152
216
  ): StateCoreResult {
153
217
  const nextState = NativePatchCore.runStateCore(
@@ -190,13 +254,14 @@ export class UpdateContext {
190
254
  return;
191
255
  }
192
256
 
257
+ UpdateContext.ignoreRollback = false;
193
258
  this.cleanUp();
194
259
  this.persistState(nextState, { clearExisting: true });
195
260
  }
196
261
 
197
262
  public setKv(key: string, value: string): void {
198
263
  this.preferences.putSync(key, value);
199
- this.preferences.flush();
264
+ this.flushPreferences(`set key ${key}`);
200
265
  }
201
266
 
202
267
  public getKv(key: string): string {
@@ -223,7 +288,10 @@ export class UpdateContext {
223
288
  }
224
289
 
225
290
  public clearFirstTime(): void {
226
- this.runStateOperation(STATE_OP_CLEAR_FIRST_TIME, '', { cleanUp: true });
291
+ this.runStateOperation(STATE_OP_CLEAR_FIRST_TIME, '', {
292
+ cleanUp: true,
293
+ clearFirstLoadMarker: true,
294
+ });
227
295
  }
228
296
 
229
297
  public clearRollbackMark(): void {
@@ -306,23 +374,38 @@ export class UpdateContext {
306
374
  }
307
375
 
308
376
  this.runStateOperation(STATE_OP_SWITCH_VERSION, hash);
377
+ UpdateContext.ignoreRollback = false;
309
378
  } catch (e) {
310
379
  console.error('Failed to switch version:', e);
311
380
  throw e;
312
381
  }
313
382
  }
314
383
 
384
+ public consumeFirstLoadMarker(): boolean {
385
+ const marked = this.readString('firstLoadMarked') === 'true';
386
+ if (marked) {
387
+ this.preferences.deleteSync('firstLoadMarked');
388
+ this.flushPreferences('clear first load marker');
389
+ }
390
+ return marked;
391
+ }
392
+
315
393
  public getBundleUrl() {
316
394
  UpdateContext.isUsingBundleUrl = true;
317
395
  const launchState = NativePatchCore.runStateCore(
318
396
  STATE_OP_RESOLVE_LAUNCH,
319
397
  this.getStateSnapshot(),
320
398
  '',
321
- false,
322
- false,
399
+ UpdateContext.ignoreRollback,
400
+ true,
323
401
  );
324
402
  if (launchState.didRollback || launchState.consumedFirstTime) {
325
- this.persistState(launchState);
403
+ this.persistState(launchState, {
404
+ markFirstLoadMarker: launchState.consumedFirstTime,
405
+ });
406
+ }
407
+ if (launchState.consumedFirstTime) {
408
+ UpdateContext.ignoreRollback = true;
326
409
  }
327
410
 
328
411
  let version = launchState.loadVersion || '';
package/harmony/pushy.har CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.42.3",
3
+ "version": "10.42.5",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "scripts": {
package/src/client.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from './type';
29
29
  import {
30
30
  assertWeb,
31
+ computeProgress,
31
32
  DEFAULT_FETCH_TIMEOUT_MS,
32
33
  emptyObj,
33
34
  fetchWithTimeout,
@@ -491,13 +492,19 @@ export class Pushy {
491
492
  }
492
493
  const patchStartTime = Date.now();
493
494
  if (onDownloadProgress) {
495
+ const wrapProgress = (data: ProgressData) => {
496
+ onDownloadProgress({
497
+ ...data,
498
+ progress: computeProgress(data.received, data.total),
499
+ });
500
+ };
494
501
  // @ts-expect-error harmony not in existing platforms
495
502
  if (Platform.OS === 'harmony') {
496
503
  sharedState.progressHandlers[hash] = DeviceEventEmitter.addListener(
497
504
  'RCTPushyDownloadProgress',
498
- progressData => {
505
+ (progressData: ProgressData) => {
499
506
  if (progressData.hash === hash) {
500
- onDownloadProgress(progressData);
507
+ wrapProgress(progressData);
501
508
  }
502
509
  },
503
510
  );
@@ -505,54 +512,47 @@ export class Pushy {
505
512
  sharedState.progressHandlers[hash] =
506
513
  pushyNativeEventEmitter.addListener(
507
514
  'RCTPushyDownloadProgress',
508
- progressData => {
515
+ (progressData: ProgressData) => {
509
516
  if (progressData.hash === hash) {
510
- onDownloadProgress(progressData);
517
+ wrapProgress(progressData);
511
518
  }
512
519
  },
513
520
  );
514
521
  }
515
522
  }
523
+ const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
516
524
  let succeeded = '';
517
- this.report({
518
- type: 'downloading',
519
- data: {
520
- newVersion: hash,
521
- },
522
- });
523
525
  let lastError: any;
524
526
  const errorMessages: string[] = [];
525
- const diffUrl = await testUrls(joinUrls(paths, diff));
526
- if (diffUrl && !__DEV__) {
527
- log('downloading diff');
528
- try {
529
- await PushyModule.downloadPatchFromPpk({
530
- updateUrl: diffUrl,
531
- hash,
532
- originHash: currentVersion,
533
- });
534
- succeeded = 'diff';
535
- } catch (e: any) {
536
- const errorMessage = this.t('error_diff_failed', {
537
- message: e.message,
538
- });
539
- errorMessages.push(errorMessage);
540
- lastError = Error(errorMessage);
541
- log(errorMessage);
527
+
528
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
529
+ if (attempt > 0) {
530
+ const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000);
531
+ log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
532
+ await new Promise(r => setTimeout(r, backoffMs));
533
+ errorMessages.length = 0;
534
+ lastError = undefined;
535
+ succeeded = '';
542
536
  }
543
- }
544
- if (!succeeded) {
545
- const pdiffUrl = await testUrls(joinUrls(paths, pdiff));
546
- if (pdiffUrl && !__DEV__) {
547
- log('downloading pdiff');
537
+ this.report({
538
+ type: 'downloading',
539
+ data: {
540
+ newVersion: hash,
541
+ attempt,
542
+ },
543
+ });
544
+ const diffUrl = await testUrls(joinUrls(paths, diff));
545
+ if (diffUrl && !__DEV__) {
546
+ log('downloading diff');
548
547
  try {
549
- await PushyModule.downloadPatchFromPackage({
550
- updateUrl: pdiffUrl,
548
+ await PushyModule.downloadPatchFromPpk({
549
+ updateUrl: diffUrl,
551
550
  hash,
551
+ originHash: currentVersion,
552
552
  });
553
- succeeded = 'pdiff';
553
+ succeeded = 'diff';
554
554
  } catch (e: any) {
555
- const errorMessage = this.t('error_pdiff_failed', {
555
+ const errorMessage = this.t('error_diff_failed', {
556
556
  message: e.message,
557
557
  });
558
558
  errorMessages.push(errorMessage);
@@ -560,28 +560,51 @@ export class Pushy {
560
560
  log(errorMessage);
561
561
  }
562
562
  }
563
- }
564
- if (!succeeded) {
565
- const fullUrl = await testUrls(joinUrls(paths, full));
566
- if (fullUrl) {
567
- log('downloading full patch');
568
- try {
569
- await PushyModule.downloadFullUpdate({
570
- updateUrl: fullUrl,
571
- hash,
572
- });
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
+ }
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__) {
602
+ log(this.t('dev_incremental_update_disabled'));
573
603
  succeeded = 'full';
574
- } catch (e: any) {
575
- const errorMessage = this.t('error_full_patch_failed', {
576
- message: e.message,
577
- });
578
- errorMessages.push(errorMessage);
579
- lastError = Error(errorMessage);
580
- log(errorMessage);
581
604
  }
582
- } else if (__DEV__) {
583
- log(this.t('dev_incremental_update_disabled'));
584
- succeeded = 'full';
605
+ }
606
+ if (succeeded) {
607
+ break;
585
608
  }
586
609
  }
587
610
  if (sharedState.progressHandlers[hash]) {
@@ -0,0 +1,2 @@
1
+ /** React Native dev mode flag, injected by Metro bundler */
2
+ declare const __DEV__: boolean;
package/src/type.ts CHANGED
@@ -33,6 +33,8 @@ export interface ProgressData {
33
33
  hash: string;
34
34
  received: number;
35
35
  total: number;
36
+ /** Download progress percentage (0-100), computed from received / total and clamped to range. Only populated in downloadUpdate callbacks. */
37
+ progress?: number;
36
38
  }
37
39
 
38
40
  // 用于描述一次检查结束后的最终状态,便于业务侧感知成功、跳过或失败
@@ -119,6 +121,8 @@ export interface ClientOptions {
119
121
  ) => Promise<boolean | void> | boolean | void;
120
122
  onPackageExpired?: (info: CheckResult) => Promise<boolean> | boolean;
121
123
  overridePackageVersion?: string;
124
+ /** Maximum number of retry attempts for failed downloads (default: 3) */
125
+ maxRetries?: number;
122
126
  }
123
127
 
124
128
  export interface UpdateTestPayload {
package/src/utils.ts CHANGED
@@ -108,6 +108,11 @@ export const assertWeb = () => {
108
108
  return true;
109
109
  };
110
110
 
111
+ export const computeProgress = (received: number, total: number): number =>
112
+ total > 0
113
+ ? Math.min(100, Math.max(0, Math.floor((received / total) * 100)))
114
+ : 0;
115
+
111
116
  export const fetchWithTimeout = (
112
117
  url: string,
113
118
  params: Parameters<typeof fetch>[1],