react-native-update 10.50.0 → 10.51.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 (49) hide show
  1. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  2. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  3. package/README-CN.md +14 -1
  4. package/README.md +14 -1
  5. package/android/jni/Android.mk +5 -1
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
  11. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  12. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
  15. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
  16. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  17. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  18. package/cpp/update_flow_core/flow_json.cpp +522 -0
  19. package/cpp/update_flow_core/flow_json.h +103 -0
  20. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  21. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  22. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  23. package/cpp/update_flow_core/update_flow_core.h +77 -0
  24. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  25. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  26. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
  27. package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
  28. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  29. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  30. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  31. package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
  32. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  33. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  34. package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
  35. package/harmony/pushy.har +0 -0
  36. package/ios/RCTPushy/RCTPushy.mm +779 -61
  37. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  38. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  39. package/package.json +3 -1
  40. package/react-native-update.podspec +2 -0
  41. package/src/NativePushy.ts +14 -0
  42. package/src/client.ts +341 -118
  43. package/src/endpoint.ts +7 -29
  44. package/src/provider.tsx +38 -2
  45. package/src/type.ts +26 -0
  46. package/src/updateFlowCore.ts +320 -0
  47. package/src/utils.ts +1 -12
  48. package/src/isInRollout.ts +0 -87
  49. package/src/resolveCheckResult.ts +0 -30
@@ -47,6 +47,17 @@ public class UpdateContext {
47
47
  private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
48
48
  private static final int STATE_OP_RESOLVE_LAUNCH = 6;
49
49
  private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
50
+ static final String VERSION_COMPLETE_FILE = ".pushy-complete";
51
+ // Bumped by resetToPackagedBundle. The cold-start check runs for minutes
52
+ // and may already hold a decision when the app resets to the packaged
53
+ // bundle; the orchestrator samples this counter and abandons activation
54
+ // (and its response cache) when the value moved, so an in-flight rescue
55
+ // can never resurrect the version the app just reset away from.
56
+ private static final java.util.concurrent.atomic.AtomicLong resetGeneration =
57
+ new java.util.concurrent.atomic.AtomicLong(0);
58
+ // Held by resetToPackagedBundle and by the cold-start check's commit, so
59
+ // the generation check and the writes it guards are one atomic step.
60
+ private static final Object commitLock = new Object();
50
61
 
51
62
  // Singleton instance
52
63
  private static volatile UpdateContext sInstance;
@@ -159,7 +170,9 @@ public class UpdateContext {
159
170
  });
160
171
  }
161
172
 
162
- private String computeBundleHash() {
173
+ // Package-private: also the native cold-start check's request input
174
+ // (NativeCheckOrchestrator). Blocking — call off the main thread.
175
+ String computeBundleHash() {
163
176
  String cachePrefix = getPackageVersion() + "|" + getPackageLastUpdateTime() + "|";
164
177
  String cached = sp.getString(KEY_BUNDLE_HASH_CACHE, null);
165
178
  if (cached != null && cached.startsWith(cachePrefix)) {
@@ -238,6 +251,12 @@ public class UpdateContext {
238
251
  }
239
252
 
240
253
  public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) {
254
+ downloadFullUpdate(url, hash, listener, 0);
255
+ }
256
+
257
+ void downloadFullUpdate(
258
+ String url, String hash, DownloadFileListener listener, long deadlineNanos
259
+ ) {
241
260
  if (rejectUnsafeComponent(hash, listener)) {
242
261
  return;
243
262
  }
@@ -246,6 +265,7 @@ public class UpdateContext {
246
265
  params.url = url;
247
266
  params.hash = hash;
248
267
  params.listener = listener;
268
+ params.deadlineNanos = deadlineNanos;
249
269
  params.targetFile = new File(rootDir, hash + ".ppk");
250
270
  params.unzipDirectory = new File(rootDir, hash);
251
271
  enqueue(params);
@@ -273,6 +293,12 @@ public class UpdateContext {
273
293
  }
274
294
 
275
295
  public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) {
296
+ downloadPatchFromApk(url, hash, listener, 0);
297
+ }
298
+
299
+ void downloadPatchFromApk(
300
+ String url, String hash, DownloadFileListener listener, long deadlineNanos
301
+ ) {
276
302
  if (rejectUnsafeComponent(hash, listener)) {
277
303
  return;
278
304
  }
@@ -281,12 +307,23 @@ public class UpdateContext {
281
307
  params.url = url;
282
308
  params.hash = hash;
283
309
  params.listener = listener;
310
+ params.deadlineNanos = deadlineNanos;
284
311
  params.targetFile = new File(rootDir, hash + ".apk.patch");
285
312
  params.unzipDirectory = new File(rootDir, hash);
286
313
  enqueue(params);
287
314
  }
288
315
 
289
316
  public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
317
+ downloadPatchFromPpk(url, hash, originHash, listener, 0);
318
+ }
319
+
320
+ void downloadPatchFromPpk(
321
+ String url,
322
+ String hash,
323
+ String originHash,
324
+ DownloadFileListener listener,
325
+ long deadlineNanos
326
+ ) {
290
327
  if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
291
328
  return;
292
329
  }
@@ -296,6 +333,7 @@ public class UpdateContext {
296
333
  params.hash = hash;
297
334
  params.originHash = originHash;
298
335
  params.listener = listener;
336
+ params.deadlineNanos = deadlineNanos;
299
337
  params.targetFile = new File(rootDir, originHash + "-" + hash + ".ppk.patch");
300
338
  params.unzipDirectory = new File(rootDir, hash);
301
339
  params.originDirectory = new File(rootDir, originHash);
@@ -348,10 +386,21 @@ public class UpdateContext {
348
386
  if (!isSafePathComponent(hash)) {
349
387
  throw new IllegalArgumentException("Invalid hash: " + hash);
350
388
  }
351
- if (!new File(rootDir, hash+"/index.bundlejs").exists()) {
389
+ File versionDir = new File(rootDir, hash);
390
+ File bundleFile = new File(versionDir, "index.bundlejs");
391
+ if (!bundleFile.isFile()) {
352
392
  throw new IllegalStateException("Bundle version " + hash + " not found.");
353
393
  }
354
394
  StateCoreResult currentState = getStateSnapshot();
395
+ boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
396
+ || hash.equals(currentState.lastVersion);
397
+ if (!new File(versionDir, VERSION_COMPLETE_FILE).isFile()
398
+ && !isLegacyActivatedVersion) {
399
+ // Versions activated before completion markers were introduced are
400
+ // explicitly grandfathered through current/last state. An arbitrary
401
+ // markerless directory may be a crash-left partial install.
402
+ throw new IllegalStateException("Bundle version " + hash + " is incomplete.");
403
+ }
355
404
  StateCoreResult nextState = runStateCore(
356
405
  STATE_OP_SWITCH_VERSION,
357
406
  currentState,
@@ -445,6 +494,16 @@ public class UpdateContext {
445
494
  * for gray release bucketing and must not change on reset.
446
495
  */
447
496
  public void resetToPackagedBundle() {
497
+ synchronized (commitLock) {
498
+ resetToPackagedBundleLocked();
499
+ }
500
+ }
501
+
502
+ private void resetToPackagedBundleLocked() {
503
+ // Invalidate any in-flight cold-start round before clearing state: a
504
+ // round committing under the same lock afterwards sees the new
505
+ // generation and drops its result.
506
+ resetGeneration.incrementAndGet();
448
507
  StateCoreResult resetState = new StateCoreResult();
449
508
  resetState.packageVersion = getPackageVersion();
450
509
  resetState.buildTime = getBuildTime();
@@ -459,6 +518,8 @@ public class UpdateContext {
459
518
  }
460
519
  persistEditor(editor, "reset to packaged bundle");
461
520
  ignoreRollback = false;
521
+ // editor.clear() above already dropped the cached check response; it
522
+ // still advertised the version this reset removed.
462
523
  Log.i(TAG, "Reset to packaged bundle");
463
524
 
464
525
  DownloadTaskParams params = new DownloadTaskParams();
@@ -530,55 +591,112 @@ public class UpdateContext {
530
591
 
531
592
  public String getBundleUrl(String defaultAssetsUrl) {
532
593
  isUsingBundleUrl = true;
533
- StateCoreResult currentState = getStateSnapshot();
534
- StateCoreResult launchState = runStateCore(
535
- STATE_OP_RESOLVE_LAUNCH,
536
- currentState,
537
- null,
538
- ignoreRollback,
539
- true
540
- );
541
- if (launchState.didRollback) {
542
- // The crash-protection rollback: the new version never called
543
- // markSuccess. Keep this visible in release logs.
544
- Log.e(TAG, "Version " + currentState.currentVersion
545
- + " was not marked as successful, rolling back to "
546
- + launchState.currentVersion);
547
- }
548
- if (launchState.didRollback || launchState.consumedFirstTime) {
549
- SharedPreferences.Editor editor = sp.edit();
550
- applyState(editor, launchState);
594
+ String nativeCheckRolledBackVersion = null;
595
+ try {
596
+ StateCoreResult currentState = getStateSnapshot();
597
+ StateCoreResult launchState = runStateCore(
598
+ STATE_OP_RESOLVE_LAUNCH,
599
+ currentState,
600
+ null,
601
+ ignoreRollback,
602
+ true
603
+ );
604
+ nativeCheckRolledBackVersion = launchState.rolledBackVersion;
605
+ if (launchState.didRollback) {
606
+ // The crash-protection rollback: the new version never called
607
+ // markSuccess. Keep this visible in release logs.
608
+ Log.e(TAG, "Version " + currentState.currentVersion
609
+ + " was not marked as successful, rolling back to "
610
+ + launchState.currentVersion);
611
+ }
612
+ if (launchState.didRollback || launchState.consumedFirstTime) {
613
+ SharedPreferences.Editor editor = sp.edit();
614
+ applyState(editor, launchState);
615
+ if (launchState.consumedFirstTime) {
616
+ editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
617
+ }
618
+ persistEditor(editor, "resolve launch");
619
+ }
551
620
  if (launchState.consumedFirstTime) {
552
- editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
621
+ // bundleURL may be resolved multiple times in one process.
622
+ ignoreRollback = true;
623
+ }
624
+
625
+ String currentVersion = launchState.loadVersion;
626
+ if (currentVersion == null) {
627
+ return defaultAssetsUrl;
628
+ }
629
+
630
+ // Guard the rollback chain against cycles: a corrupted state returning
631
+ // an already-visited version would otherwise spin this loop forever on
632
+ // the main thread.
633
+ java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
634
+ while (currentVersion != null && visitedVersions.add(currentVersion)) {
635
+ File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
636
+ if (!bundleFile.exists()) {
637
+ Log.e(TAG, "Bundle version " + currentVersion + " not found.");
638
+ currentVersion = this.rollBack();
639
+ nativeCheckRolledBackVersion = rolledBackVersion();
640
+ continue;
641
+ }
642
+ launchVersion = currentVersion;
643
+ nativeCheckRolledBackVersion = rolledBackVersion();
644
+ return bundleFile.toString();
553
645
  }
554
- persistEditor(editor, "resolve launch");
555
- }
556
- if (launchState.consumedFirstTime) {
557
- // bundleURL may be resolved multiple times in one process.
558
- ignoreRollback = true;
559
- }
560
646
 
561
- String currentVersion = launchState.loadVersion;
562
- if (currentVersion == null) {
647
+ nativeCheckRolledBackVersion = rolledBackVersion();
563
648
  return defaultAssetsUrl;
649
+ } finally {
650
+ // Even corrupted state or a state-core exception must not disable
651
+ // the next-launch rescue check. A null snapshot simply omits the
652
+ // rollback guard for this exceptional launch.
653
+ NativeCheckOrchestrator.schedule(this, nativeCheckRolledBackVersion);
564
654
  }
655
+ }
565
656
 
566
- // Guard the rollback chain against cycles: a corrupted state returning
567
- // an already-visited version would otherwise spin this loop forever on
568
- // the main thread.
569
- java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
570
- while (currentVersion != null && visitedVersions.add(currentVersion)) {
571
- File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
572
- if (!bundleFile.exists()) {
573
- Log.e(TAG, "Bundle version " + currentVersion + " not found.");
574
- currentVersion = this.rollBack();
575
- continue;
657
+ /** Sampled/compared by the native check orchestrator; see resetGeneration. */
658
+ static long getResetGeneration() {
659
+ return resetGeneration.get();
660
+ }
661
+
662
+ /**
663
+ * Commit everything a cold-start round persists — version info, the
664
+ * activation, the response cache — under one lock that first re-checks the
665
+ * reset generation. resetToPackagedBundle takes the same lock, so there is
666
+ * no compare-and-act window: either the whole round lands, or the reset
667
+ * wins and none of it does. Returns whether the round was committed.
668
+ */
669
+ boolean commitNativeCheckResult(
670
+ long expectedGeneration,
671
+ String hash,
672
+ String hashInfoJson,
673
+ boolean activate,
674
+ String responseCacheJson
675
+ ) {
676
+ synchronized (commitLock) {
677
+ if (resetGeneration.get() != expectedGeneration) {
678
+ return false;
679
+ }
680
+ if (hash != null && hashInfoJson != null) {
681
+ setKv("hash_" + hash, hashInfoJson);
682
+ }
683
+ if (activate && hash != null) {
684
+ switchVersion(hash);
576
685
  }
577
- launchVersion = currentVersion;
578
- return bundleFile.toString();
686
+ if (responseCacheJson != null) {
687
+ setKv(NativeCheckOrchestrator.KEY_RESP_CACHE, responseCacheJson);
688
+ }
689
+ return true;
579
690
  }
691
+ }
580
692
 
581
- return defaultAssetsUrl;
693
+ boolean hasCompletedVersion(String hash) {
694
+ if (!isSafePathComponent(hash)) {
695
+ return false;
696
+ }
697
+ File versionDir = new File(rootDir, hash);
698
+ return new File(versionDir, "index.bundlejs").isFile()
699
+ && new File(versionDir, VERSION_COMPLETE_FILE).isFile();
582
700
  }
583
701
 
584
702
  private String rollBack() {
@@ -224,10 +224,54 @@ public class UpdateModuleImpl {
224
224
  });
225
225
  }
226
226
 
227
+ /**
228
+ * Raw response cached by the native cold-start check, for the JS side to
229
+ * reuse instead of re-checking (§10.3). Empty string when absent; never
230
+ * rejects.
231
+ */
232
+ public static void getNativeCheckCache(
233
+ final UpdateContext updateContext,
234
+ final Promise promise
235
+ ) {
236
+ String cached = updateContext.getKv(NativeCheckOrchestrator.KEY_RESP_CACHE);
237
+ promise.resolve(cached == null ? "" : cached);
238
+ }
239
+
227
240
  private static void setUuidInternal(UpdateContext updateContext, String uuid) {
228
241
  updateContext.setKv("uuid", uuid);
229
242
  }
230
243
 
244
+ /**
245
+ * Provisioning for the native cold-start update check
246
+ * (NATIVE_CHECKUPDATE_DESIGN §10.1): the raw JSON persists as-is and is
247
+ * parsed on read by the orchestrator; absent config = check disabled.
248
+ * Validated at write time — a corrupt config would otherwise silently
249
+ * disable the native check forever with no signal.
250
+ */
251
+ public static void syncNativeConfig(
252
+ final UpdateContext updateContext,
253
+ final String config,
254
+ final Promise promise
255
+ ) {
256
+ if (config == null || config.isEmpty()) {
257
+ promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a JSON object string");
258
+ return;
259
+ }
260
+ try {
261
+ new JSONObject(config);
262
+ } catch (JSONException e) {
263
+ promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a JSON object string", e);
264
+ return;
265
+ }
266
+ StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "syncNativeConfig", new StateSerialRunner.Operation() {
267
+ @Override
268
+ public void run() {
269
+ updateContext.setKv(NativeCheckOrchestrator.KEY_CONFIG, config);
270
+ promise.resolve(true);
271
+ }
272
+ });
273
+ }
274
+
231
275
  public static void setUuid(
232
276
  final UpdateContext updateContext,
233
277
  final String uuid,
@@ -88,6 +88,16 @@ public class UpdateModule extends NativePushySpec {
88
88
  UpdateModuleImpl.setUuid(updateContext, uuid, promise);
89
89
  }
90
90
 
91
+ @Override
92
+ public void syncNativeConfig(String config, Promise promise) {
93
+ UpdateModuleImpl.syncNativeConfig(updateContext, config, promise);
94
+ }
95
+
96
+ @Override
97
+ public void getNativeCheckCache(Promise promise) {
98
+ UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
99
+ }
100
+
91
101
  @Override
92
102
  public void setLocalHashInfo(String hash, String info, Promise promise) {
93
103
  UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info, promise);
@@ -90,6 +90,16 @@ public class UpdateModule extends ReactContextBaseJavaModule {
90
90
  UpdateModuleImpl.setUuid(updateContext, uuid);
91
91
  }
92
92
 
93
+ @ReactMethod
94
+ public void syncNativeConfig(String config, Promise promise) {
95
+ UpdateModuleImpl.syncNativeConfig(updateContext, config, promise);
96
+ }
97
+
98
+ @ReactMethod
99
+ public void getNativeCheckCache(Promise promise) {
100
+ UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
101
+ }
102
+
93
103
  @ReactMethod
94
104
  public void setLocalHashInfo(String hash, String info) {
95
105
  UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info);