react-native-update 10.47.0 → 10.48.1

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 (63) hide show
  1. package/android/jni/Android.mk +2 -0
  2. package/android/jni/hpatch.c +55 -20
  3. package/android/jni/hpatch.h +0 -6
  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/DownloadTask.java +29 -6
  9. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +2 -0
  10. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +1 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java +12 -0
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +47 -2
  13. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +12 -3
  14. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +13 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +11 -0
  16. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  17. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  18. package/cpp/patch_core/error_codes.h +3 -0
  19. package/cpp/patch_core/hbc_transform.cpp +147 -0
  20. package/cpp/patch_core/hbc_transform.h +53 -0
  21. package/cpp/patch_core/hbc_transform_wire.cpp +327 -0
  22. package/cpp/patch_core/hbc_transform_wire.h +52 -0
  23. package/cpp/patch_core/patch_core.cpp +126 -4
  24. package/cpp/patch_core/patch_core.h +5 -0
  25. package/cpp/patch_core/patch_core_android.cpp +3 -1
  26. package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
  27. package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
  28. package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
  29. package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
  30. package/cpp/patch_core/tests/fixtures/v96.meta.json +1 -0
  31. package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
  32. package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
  33. package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
  34. package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
  35. package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
  36. package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
  37. package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
  38. package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
  39. package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
  40. package/cpp/patch_core/tests/hbc_transform_test.cpp +384 -0
  41. package/cpp/patch_core/tests/patch_core_test.cpp +150 -1
  42. package/cpp/patch_core/update_core_android.cpp +10 -0
  43. package/harmony/pushy/src/main/cpp/CMakeLists.txt +33 -39
  44. package/harmony/pushy/src/main/cpp/pushy.cpp +31 -6
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +34 -2
  46. package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
  47. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
  48. package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -2
  49. package/harmony/pushy.har +0 -0
  50. package/ios/RCTPushy/RCTPushy.mm +93 -1
  51. package/ios/RCTPushy/RCTPushyDownloader.mm +11 -2
  52. package/package.json +1 -1
  53. package/react-native-update.podspec +2 -0
  54. package/src/NativePushy.ts +1 -0
  55. package/src/client.ts +167 -38
  56. package/src/context.ts +4 -0
  57. package/src/core.ts +4 -0
  58. package/src/error.ts +51 -5
  59. package/src/locales/en.ts +2 -0
  60. package/src/locales/zh.ts +2 -0
  61. package/src/provider.tsx +32 -15
  62. package/src/telemetry.ts +6 -1
  63. package/src/type.ts +3 -0
@@ -6,6 +6,7 @@
6
6
  #include <vector>
7
7
 
8
8
  #include "archive_patch_core.h"
9
+ #include "hbc_transform_wire.h"
9
10
  #include "patch_core.h"
10
11
  #include "state_core.h"
11
12
  #include "state_ops.h"
@@ -69,7 +70,9 @@ std::string GetString(napi_env env, napi_value value, bool* ok) {
69
70
  return std::string();
70
71
  }
71
72
 
72
- std::string result(length, '\0');
73
+ // length + 1 so NAPI's terminating '\0' lands inside owned storage instead
74
+ // of the string's past-the-end terminator slot (formally UB to write).
75
+ std::string result(length + 1, '\0');
73
76
  size_t written = 0;
74
77
  if (napi_get_value_string_utf8(
75
78
  env, value, result.data(), length + 1, &written) != napi_ok) {
@@ -686,6 +689,7 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
686
689
  std::string bundle_patch_path;
687
690
  std::string bundle_output_path;
688
691
  std::string merge_source_subdir;
692
+ std::string bundle_hbc_transform_meta;
689
693
  bool enable_merge = true;
690
694
 
691
695
  if (!GetOptionalStringArrayProperty(env, args[0], "copyFroms", &copy_froms) ||
@@ -697,6 +701,8 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
697
701
  !GetOptionalStringProperty(env, args[0], "bundlePatchPath", &bundle_patch_path) ||
698
702
  !GetOptionalStringProperty(env, args[0], "bundleOutputPath", &bundle_output_path) ||
699
703
  !GetOptionalStringProperty(env, args[0], "mergeSourceSubdir", &merge_source_subdir) ||
704
+ !GetOptionalStringProperty(
705
+ env, args[0], "bundleHbcTransformMeta", &bundle_hbc_transform_meta) ||
700
706
  !GetOptionalBoolProperty(env, args[0], "enableMerge", true, &enable_merge)) {
701
707
  return nullptr;
702
708
  }
@@ -713,6 +719,7 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
713
719
  work_data->options.bundle_patch_path = bundle_patch_path;
714
720
  work_data->options.bundle_output_path = bundle_output_path;
715
721
  work_data->options.merge_source_subdir = merge_source_subdir;
722
+ work_data->options.bundle_hbc_transform_meta = bundle_hbc_transform_meta;
716
723
  work_data->options.enable_merge = enable_merge;
717
724
 
718
725
  napi_value promise = nullptr;
@@ -733,9 +740,13 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
733
740
  auto* w = static_cast<ApplyPatchWork*>(data);
734
741
  w->status = pushy::patch::ApplyPatchFromFileSource(w->options);
735
742
  },
736
- [](napi_env cb_env, napi_status, void* data) {
743
+ [](napi_env cb_env, napi_status status, void* data) {
737
744
  auto* w = static_cast<ApplyPatchWork*>(data);
738
- if (w->status.ok) {
745
+ if (status != napi_ok) {
746
+ // Cancelled/aborted before execute ran: w->status is
747
+ // meaningless; still settle the promise so it never hangs.
748
+ RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
749
+ } else if (w->status.ok) {
739
750
  napi_value undefined_value = nullptr;
740
751
  napi_get_undefined(cb_env, &undefined_value);
741
752
  napi_resolve_deferred(cb_env, w->deferred, undefined_value);
@@ -821,9 +832,11 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
821
832
  w->status = pushy::patch::CleanupOldEntries(
822
833
  w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
823
834
  },
824
- [](napi_env cb_env, napi_status, void* data) {
835
+ [](napi_env cb_env, napi_status status, void* data) {
825
836
  auto* w = static_cast<CleanupWork*>(data);
826
- if (w->status.ok) {
837
+ if (status != napi_ok) {
838
+ RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
839
+ } else if (w->status.ok) {
827
840
  napi_value undefined_value = nullptr;
828
841
  napi_get_undefined(cb_env, &undefined_value);
829
842
  napi_resolve_deferred(cb_env, w->deferred, undefined_value);
@@ -876,13 +889,25 @@ bool ExportFunction(
876
889
 
877
890
  } // namespace
878
891
 
892
+
893
+ // 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道)
894
+ static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
895
+ napi_value result = nullptr;
896
+ napi_create_uint32(
897
+ env,
898
+ static_cast<uint32_t>(pushy::hbc::kSupportedDiffVersion),
899
+ &result);
900
+ return result;
901
+ }
902
+
879
903
  napi_value Init(napi_env env, napi_value exports) {
880
904
  if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
881
905
  !ExportFunction(env, exports, "runStateCore", RunStateCore) ||
882
906
  !ExportFunction(env, exports, "buildArchivePatchPlan", BuildArchivePatchPlan) ||
883
907
  !ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
884
908
  !ExportFunction(env, exports, "applyPatchFromFileSource", ApplyPatchFromFileSource) ||
885
- !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries)) {
909
+ !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
910
+ !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion)) {
886
911
  return nullptr;
887
912
  }
888
913
  return exports;
@@ -16,6 +16,9 @@ export interface PatchManifestArrays {
16
16
  copyFroms: string[];
17
17
  copyTos: string[];
18
18
  deletes: string[];
19
+ // __diff.json 中对应 bundle patch 条目的 hbcTransform 元数据(原始 JSON
20
+ // 字符串);为空时 native 走现状路径
21
+ hbcTransformMeta: string;
19
22
  }
20
23
 
21
24
  export function parseManifestToArrays(
@@ -44,10 +47,23 @@ export function parseManifestToArrays(
44
47
  copyTos.push(to);
45
48
  }
46
49
 
50
+ const hbcTransform = manifest.hbcTransform as
51
+ | Record<string, object>
52
+ | undefined;
53
+ const hbcTransformEntry =
54
+ hbcTransform && typeof hbcTransform === 'object'
55
+ ? hbcTransform[HARMONY_BUNDLE_PATCH_ENTRY]
56
+ : undefined;
57
+ const hbcTransformMeta =
58
+ hbcTransformEntry && typeof hbcTransformEntry === 'object'
59
+ ? JSON.stringify(hbcTransformEntry)
60
+ : '';
61
+
47
62
  return {
48
63
  copyFroms,
49
64
  copyTos,
50
65
  deletes,
66
+ hbcTransformMeta,
51
67
  };
52
68
  }
53
69
 
@@ -214,6 +230,7 @@ export class DownloadTask {
214
230
  copyFroms: [],
215
231
  copyTos: [],
216
232
  deletes: [],
233
+ hbcTransformMeta: '',
217
234
  };
218
235
  }
219
236
 
@@ -229,6 +246,7 @@ export class DownloadTask {
229
246
  workingDirectory: string,
230
247
  bundlePatchPath: string,
231
248
  outputFile: string,
249
+ hbcTransformMeta = '',
232
250
  ): Promise<void> {
233
251
  const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
234
252
  try {
@@ -243,6 +261,7 @@ export class DownloadTask {
243
261
  bundlePatchPath,
244
262
  bundleOutputPath: outputFile,
245
263
  enableMerge: false,
264
+ bundleHbcTransformMeta: hbcTransformMeta,
246
265
  });
247
266
  } catch (error: any) {
248
267
  error.message = `Failed to process bundle patch: ${error.message}`;
@@ -376,8 +395,15 @@ export class DownloadTask {
376
395
  resolve();
377
396
  })
378
397
  .catch(async error => {
379
- await closeWriter();
398
+ // reject 必须先于 closeWriter:此时 watchdog 已清除,若 close
399
+ // 也失败(同一磁盘故障的常见连锁)而 reject 未执行,下载 Promise
400
+ // 将永久挂起——正是 HM-2 要消灭的症状。
380
401
  reject(error);
402
+ try {
403
+ await closeWriter();
404
+ } catch (closeErr: any) {
405
+ console.error('closeWriter failed after write error:', closeErr);
406
+ }
381
407
  });
382
408
  });
383
409
  });
@@ -442,7 +468,6 @@ export class DownloadTask {
442
468
  },
443
469
  );
444
470
 
445
- refreshWatchdog();
446
471
  const responseCode = await httpRequest.requestInStream(params.url, {
447
472
  method: http.RequestMethod.GET,
448
473
  readTimeout: 60000,
@@ -455,6 +480,11 @@ export class DownloadTask {
455
480
  throw Error(`Server error: ${responseCode}`);
456
481
  }
457
482
 
483
+ // watchdog 到这里才首次启动:requestInStream 阶段已有 connect/readTimeout
484
+ // 覆盖;若提早启动,连接超过 60s 时 inactivityPromise 会在 race 尚无订阅者
485
+ // 时 reject(unhandled rejection),且 promise 一经 reject 无法复活——
486
+ // 即使随后数据正常流入,race 也必然以 "Download stalled" 失败。
487
+ refreshWatchdog();
458
488
  await Promise.race([dataEndPromise, inactivityPromise]);
459
489
  const stats = await fileIo.stat(params.targetFile);
460
490
  const fileSize = stats.size;
@@ -534,6 +564,7 @@ export class DownloadTask {
534
564
  params.unzipDirectory,
535
565
  bundlePatchPath,
536
566
  `${params.unzipDirectory}/bundle.harmony.js`,
567
+ manifestArrays.hbcTransformMeta,
537
568
  );
538
569
  await this.copyFromResource(
539
570
  NativePatchCore.buildCopyGroups(
@@ -580,6 +611,7 @@ export class DownloadTask {
580
611
  bundleOutputPath: `${params.unzipDirectory}/bundle.harmony.js`,
581
612
  mergeSourceSubdir: plan.mergeSourceSubdir,
582
613
  enableMerge: plan.enableMerge,
614
+ bundleHbcTransformMeta: manifestArrays.hbcTransformMeta,
583
615
  });
584
616
  console.info('Patch from PPK completed');
585
617
  try {
@@ -47,6 +47,8 @@ export interface FileSourcePatchRequest {
47
47
  bundleOutputPath: string;
48
48
  mergeSourceSubdir?: string;
49
49
  enableMerge?: boolean;
50
+ /** __diff.json 中该 bundle patch 条目的 hbcTransform 元数据(原始 JSON) */
51
+ bundleHbcTransformMeta?: string;
50
52
  }
51
53
 
52
54
  interface NativePatchCoreBindings {
@@ -78,6 +80,8 @@ interface NativePatchCoreBindings {
78
80
  keepPrevious: string,
79
81
  maxAgeDays: number,
80
82
  ): Promise<void>;
83
+ /** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */
84
+ getSupportedDiffVersion(): number;
81
85
  }
82
86
 
83
87
  export default NativeUpdateCore as unknown as NativePatchCoreBindings;
@@ -5,6 +5,7 @@ import {
5
5
  import common from '@ohos.app.ability.common';
6
6
  import { bundleManager } from '@kit.AbilityKit';
7
7
  import logger from './Logger';
8
+ import NativePatchCore from './NativePatchCore';
8
9
  import { UpdateContext } from './UpdateContext';
9
10
  import { EventHub } from './EventHub';
10
11
 
@@ -150,6 +151,7 @@ export class PushyTurboModule extends UITurboModule {
150
151
  isFirstTime,
151
152
  rolledBackVersion,
152
153
  uuid,
154
+ supportedDiffVersion: NativePatchCore.getSupportedDiffVersion(),
153
155
  };
154
156
  const logResult = {
155
157
  downloadRootDir: result.downloadRootDir,
@@ -231,6 +233,16 @@ export class PushyTurboModule extends UITurboModule {
231
233
  }
232
234
  }
233
235
 
236
+ async resetToPackagedBundle(): Promise<void> {
237
+ logger.debug(TAG, ',call resetToPackagedBundle');
238
+ try {
239
+ this.context.resetToPackagedBundle();
240
+ } catch (error) {
241
+ logger.error(TAG, `resetToPackagedBundle failed: ${getErrorMessage(error)}`);
242
+ throw Error(`resetToPackagedBundle failed: ${getErrorMessage(error)}`);
243
+ }
244
+ }
245
+
234
246
  async downloadPatchFromPpk(options: {
235
247
  updateUrl: string;
236
248
  hash: string;
@@ -27,6 +27,10 @@ export class UpdateContext {
27
27
  private static DEBUG: boolean = false;
28
28
  private static isUsingBundleUrl: boolean = false;
29
29
  private static ignoreRollback: boolean = false;
30
+ // 本进程实际加载的热更版本(getBundleUrl 解析成功时记录)。
31
+ // resetToPackagedBundle 不能删它的目录:热更包内的图片等资源是运行时按需
32
+ // 读盘的,静默(不重启)reset 若删掉会导致后续所有未加载过的资源失败。
33
+ private static launchVersion: string = '';
30
34
  private static cachedPackageVersion: string = '';
31
35
  private static cachedBuildTime: string = '';
32
36
  // 单例:确保 bundle provider 与 TurboModule 共用同一份 preferences 内存状态,
@@ -296,9 +300,26 @@ export class UpdateContext {
296
300
  return params;
297
301
  }
298
302
 
303
+ // 串行化下载/补丁任务与破坏性清理(reset 的全量删除):Android 靠单线程
304
+ // download executor 天然串行,Harmony 的 NAPI 任务跑在 libuv worker 池上,
305
+ // 若不排队,reset 的 RemovePathRecursively 可能与正在写入的解压/打补丁并发,
306
+ // 产出"bundle 在、资源半删"的目录且可能被后续 switchVersion 激活。
307
+ private taskChain: Promise<void> = Promise.resolve();
308
+
309
+ private enqueueSerialTask<T>(job: () => Promise<T>): Promise<T> {
310
+ const run = this.taskChain.then(job);
311
+ this.taskChain = run.then(
312
+ () => undefined,
313
+ () => undefined,
314
+ );
315
+ return run;
316
+ }
317
+
299
318
  private async executeTask(params: DownloadTaskParams): Promise<void> {
300
- const downloadTask = new DownloadTask(this.context);
301
- await downloadTask.execute(params);
319
+ await this.enqueueSerialTask(() => {
320
+ const downloadTask = new DownloadTask(this.context);
321
+ return downloadTask.execute(params);
322
+ });
302
323
  }
303
324
 
304
325
  public syncStateWithBinaryVersion(
@@ -372,6 +393,61 @@ export class UpdateContext {
372
393
  });
373
394
  }
374
395
 
396
+ /**
397
+ * 恢复到二进制内置包:清空整个更新状态机(下次启动即回内置 bundle)并删除
398
+ * 已下载版本——仅保留当前运行版本的目录(静默 reset 不能破坏运行中 bundle
399
+ * 的按需资源加载)。uuid 保留 —— 它标识安装实例、用于灰度分桶,reset 不应改变。
400
+ */
401
+ public resetToPackagedBundle(): void {
402
+ this.trace('resetToPackagedBundle:before');
403
+ // 实时读取二进制身份(与 Android/iOS 对齐),而非 preferences 快照:
404
+ // meta.json 读取失败时快照可能为空,reset 持久化空值会让下次启动误判
405
+ // binary 变更多做一轮 cleanUp+persist。
406
+ const resetState: StateCoreResult = {
407
+ packageVersion: this.getPackageVersion(),
408
+ buildTime: this.getBuildTime(),
409
+ currentVersion: '',
410
+ lastVersion: '',
411
+ firstTime: false,
412
+ firstTimeOk: true,
413
+ rolledBackVersion: '',
414
+ };
415
+ // 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
416
+ // uuid —— 见 syncStateWithBinaryVersion 的注释)。getAllSync 在旧 SDK 上
417
+ // 可能不存在,此时残留的 hash_* 只是无害孤儿数据,不影响 reset 语义。
418
+ const prefsWithGetAll = this.preferences as preferences.Preferences & {
419
+ getAllSync?: () => Record<string, unknown>;
420
+ };
421
+ if (typeof prefsWithGetAll.getAllSync === 'function') {
422
+ try {
423
+ const all = prefsWithGetAll.getAllSync();
424
+ for (const key of Object.keys(all)) {
425
+ if (key.startsWith('hash_')) {
426
+ this.preferences.deleteSync(key);
427
+ }
428
+ }
429
+ } catch (e: any) {
430
+ console.error('Failed to clear hash info on reset:', e);
431
+ }
432
+ }
433
+ this.persistState(resetState, { clearFirstLoadMarker: true });
434
+ UpdateContext.ignoreRollback = false;
435
+
436
+ // maxAgeDays=0:删除下载目录内容,仅保留当前运行版本的目录(残留目录由
437
+ // 下次常规清理回收)。挂到串行任务链尾,避免与在飞的解压/打补丁并发。
438
+ this.enqueueSerialTask(() =>
439
+ NativePatchCore.cleanupOldEntries(
440
+ this.rootDir,
441
+ UpdateContext.launchVersion,
442
+ '',
443
+ 0,
444
+ ),
445
+ ).catch((error: Object) => {
446
+ console.error('reset cleanup failed:', error);
447
+ });
448
+ this.trace('resetToPackagedBundle:after');
449
+ }
450
+
375
451
  public async downloadFullUpdate(url: string, hash: string): Promise<void> {
376
452
  try {
377
453
  const params = this.createTaskParams(
@@ -512,6 +588,7 @@ export class UpdateContext {
512
588
  version = this.rollBack();
513
589
  continue;
514
590
  }
591
+ UpdateContext.launchVersion = version;
515
592
  return bundleFile;
516
593
  } catch (e) {
517
594
  console.error('Failed to access bundle file:', e);
package/harmony/pushy.har CHANGED
Binary file
@@ -2,6 +2,7 @@
2
2
  #import "RCTPushyDownloader.h"
3
3
  #import "ZipArchive.h"
4
4
  #include "../../cpp/patch_core/archive_patch_core.h"
5
+ #include "../../cpp/patch_core/hbc_transform_wire.h"
5
6
  #include "../../cpp/patch_core/error_codes.h"
6
7
  #include "../../cpp/patch_core/patch_core.h"
7
8
  #include "../../cpp/patch_core/state_core.h"
@@ -63,6 +64,12 @@ typedef NS_ENUM(NSInteger, PushyType) {
63
64
  };
64
65
 
65
66
  static std::atomic<bool> ignoreRollback{false};
67
+ // The version whose bundle this process actually loaded (resolved in
68
+ // +bundleURL). resetToPackagedBundle must not delete its directory: update
69
+ // assets (images/fonts) are read from it on demand at runtime, so wiping it
70
+ // under a silent (no-restart) reset would break every image the running app
71
+ // has not loaded yet. Guarded by the state lock.
72
+ static NSString *pushyLaunchVersion = nil;
66
73
 
67
74
  // Serializes every read-modify-write of the persisted update state. The state
68
75
  // machine itself is a pure function (state_core), but callers run on different
@@ -304,6 +311,7 @@ RCT_EXPORT_MODULE(RCTPushy);
304
311
  [visitedVersions addObject:loadVersion];
305
312
  NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
306
313
  if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
314
+ pushyLaunchVersion = loadVersion;
307
315
  resolvedURL = [NSURL fileURLWithPath:bundlePath];
308
316
  return;
309
317
  } else {
@@ -347,6 +355,9 @@ RCT_EXPORT_MODULE(RCTPushy);
347
355
  ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
348
356
  ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
349
357
  ret[@"uuid"] = [defaults objectForKey:keyUuid];
358
+ // 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道),
359
+ // JS 随 checkUpdate 以 diffV 上报,服务端按能力门控下发
360
+ ret[@"supportedDiffVersion"] = @(pushy::hbc::kSupportedDiffVersion);
350
361
  NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
351
362
  NSString *currentVersion = [pushyInfo objectForKey:paramCurrentVersion];
352
363
  ret[@"currentVersion"] = currentVersion;
@@ -376,7 +387,16 @@ RCT_EXPORT_MODULE(RCTPushy);
376
387
  {
377
388
  self = [super init];
378
389
  if (self) {
379
- _fileQueue = dispatch_queue_create("cn.reactnative.pushy.file", DISPATCH_QUEUE_SERIAL);
390
+ // One process-wide serial queue, not per-instance: a bridge reload can
391
+ // briefly keep two RCTPushy instances alive, and destructive file work
392
+ // (resetToPackagedBundle's full cleanup) must stay serialized with the
393
+ // other instance's unzip/patch jobs.
394
+ static dispatch_queue_t sharedFileQueue;
395
+ static dispatch_once_t onceToken;
396
+ dispatch_once(&onceToken, ^{
397
+ sharedFileQueue = dispatch_queue_create("cn.reactnative.pushy.file", DISPATCH_QUEUE_SERIAL);
398
+ });
399
+ _fileQueue = sharedFileQueue;
380
400
  }
381
401
  return self;
382
402
  }
@@ -514,6 +534,56 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
514
534
  #endif
515
535
  }
516
536
 
537
+ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
538
+ rejecter:(RCTPromiseRejectBlock)reject)
539
+ {
540
+ // Reset to the bundle packaged in the binary: wipe the whole update state
541
+ // (so the next launch resolves to the built-in bundle) and delete the
542
+ // downloaded versions, keeping only the directory of the version this
543
+ // process is running from (a silent reset must not break its on-demand
544
+ // asset loads). Only the client uuid survives — it identifies the install
545
+ // for gray release bucketing and must not change on reset.
546
+ __block NSString *keepVersion = nil;
547
+ PushyWithStateLock(^{
548
+ NSUserDefaults *defaults = PushyDefaults();
549
+ keepVersion = pushyLaunchVersion;
550
+
551
+ // A default-constructed State is exactly the reset state (no current /
552
+ // last version, first_time=false, first_time_ok=true); keep the binary
553
+ // identity so the next launch does not re-trigger the package-updated
554
+ // sync path.
555
+ pushy::state::State state;
556
+ state.package_version = PushyToStdString([RCTPushy packageVersion]);
557
+ state.build_time = PushyToStdString([RCTPushy buildTime]);
558
+ PushyApplyStateToDefaults(defaults, state);
559
+
560
+ for (NSString *key in [defaults dictionaryRepresentation].allKeys) {
561
+ if ([key hasPrefix:keyHashInfo]) {
562
+ [defaults removeObjectForKey:key];
563
+ }
564
+ }
565
+ [defaults removeObjectForKey:keyFirstLoadMarked];
566
+ [defaults removeObjectForKey:KeyPackageUpdatedMarked];
567
+ ignoreRollback = false;
568
+ });
569
+
570
+ dispatch_async(_fileQueue, ^{
571
+ // maxAgeDays=0: remove every downloaded entry except the running
572
+ // version's directory (cleaned up by the next regular cleanup).
573
+ pushy::patch::Status status = pushy::patch::CleanupOldEntries(
574
+ PushyToStdString([RCTPushy downloadDir]),
575
+ PushyToStdString(keepVersion),
576
+ "",
577
+ 0
578
+ );
579
+ if (!status.ok) {
580
+ RCTLogWarn(@"Pushy reset cleanup error: %s", status.message.c_str());
581
+ }
582
+ });
583
+
584
+ resolve(@true);
585
+ }
586
+
517
587
 
518
588
 
519
589
  #pragma mark - private
@@ -742,6 +812,21 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
742
812
  return;
743
813
  }
744
814
 
815
+ // __diff.json 的 hbcTransform 元数据(HBC 变换域 patch,hdiffv2 轨道):
816
+ // 存在时透传给 patch 内核执行 T(origin) → hpatch → T⁻¹;缺失走现状路径。
817
+ NSDictionary *hbcTransform = json[@"hbcTransform"];
818
+ if ([hbcTransform isKindOfClass:[NSDictionary class]]) {
819
+ NSDictionary *meta = hbcTransform[BUNDLE_PATCH_NAME];
820
+ if ([meta isKindOfClass:[NSDictionary class]]) {
821
+ NSError *metaError = nil;
822
+ NSData *metaData = [NSJSONSerialization dataWithJSONObject:meta options:0 error:&metaError];
823
+ if (metaData != nil && metaError == nil) {
824
+ NSString *metaString = [[NSString alloc] initWithData:metaData encoding:NSUTF8StringEncoding];
825
+ options.bundle_hbc_transform_meta = PushyToStdString(metaString);
826
+ }
827
+ }
828
+ }
829
+
745
830
  pushy::patch::Status status = pushy::patch::ApplyPatchFromFileSource(options);
746
831
  if (!status.ok) {
747
832
  callback(PushyNSErrorFromStatus(status));
@@ -819,6 +904,13 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
819
904
  NSError *unzipError = error;
820
905
  if (!succeeded && unzipError == nil) {
821
906
  unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"unzip failed");
907
+ } else if (unzipError != nil && unzipError.userInfo[PushyErrorCodeKey] == nil) {
908
+ // SSZipArchive's own NSError (corrupt zip, bad magic, ...) has
909
+ // no stable code; without one, downloadUpdate's fallback would
910
+ // classify it as DOWNLOAD_FAILED even though the download
911
+ // succeeded — keep the classification deterministic.
912
+ unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed,
913
+ unzipError.localizedDescription ?: @"unzip failed");
822
914
  }
823
915
  completionHandler(unzipError);
824
916
  }];
@@ -45,9 +45,12 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
45
45
 
46
46
  NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
47
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.
48
+ // is 7 days). The 30s idle timeout matches Android's readTimeout and is
49
+ // what actually catches a stalled transfer; the total-duration cap matches
50
+ // Android's 10min callTimeout — 300s made a 30MB full package on a slow
51
+ // (<100KB/s) network fail on iOS while succeeding on Android.
49
52
  sessionConfig.timeoutIntervalForRequest = 30;
50
- sessionConfig.timeoutIntervalForResource = 300;
53
+ sessionConfig.timeoutIntervalForResource = 600;
51
54
  self.session = [NSURLSession sessionWithConfiguration:sessionConfig
52
55
  delegate:self
53
56
  delegateQueue:nil];
@@ -89,6 +92,12 @@ totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
89
92
  // Normalize an unknown total (NSURLSessionTransferSizeUnknown == -1) to 0 so
90
93
  // the JS side does not compute a negative/NaN percentage.
91
94
  long long total = totalBytesExpectedToWrite > 0 ? totalBytesExpectedToWrite : 0;
95
+ if (totalBytesWritten > total) {
96
+ // Encoded responses (gzip): the expected total counts compressed bytes
97
+ // while written counts decompressed ones, so the percentage would run
98
+ // past 100%. Treat the total as unknown instead.
99
+ total = 0;
100
+ }
92
101
  if (total > 0) {
93
102
  int percentage = (int)((totalBytesWritten * 100.0 / total) + 0.5);
94
103
  if (percentage <= self.lastReportedPercentage) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.47.0",
3
+ "version": "10.48.1",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
@@ -111,6 +111,8 @@ Pod::Spec.new do |s|
111
111
  s.subspec 'RCTPushy' do |ss|
112
112
  ss.source_files = ['ios/RCTPushy/*.{h,m,mm}',
113
113
  'cpp/patch_core/archive_patch_core.{h,cpp}',
114
+ 'cpp/patch_core/hbc_transform.{h,cpp}',
115
+ 'cpp/patch_core/hbc_transform_wire.{h,cpp}',
114
116
  'cpp/patch_core/patch_core.{h,cpp}',
115
117
  'cpp/patch_core/state_core.{h,cpp}',
116
118
  'android/jni/hpatch.{h,c}',
@@ -19,6 +19,7 @@ export interface Spec extends TurboModule {
19
19
  restartApp(): Promise<void>;
20
20
  setNeedUpdate(options: { hash: string }): Promise<void>;
21
21
  markSuccess(): Promise<void>;
22
+ resetToPackagedBundle(): Promise<void>;
22
23
  downloadPatchFromPpk(options: {
23
24
  updateUrl: string;
24
25
  hash: string;