@hot-updater/react-native 1.1.2 → 1.1.4

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.
@@ -1183,6 +1183,14 @@ class BundleFileStorageService(
1183
1183
  private fun rollbackPendingBundle(
1184
1184
  stagingBundleId: String,
1185
1185
  unfinishedLaunch: Boolean = false,
1186
+ ): Boolean =
1187
+ synchronized(releaseStateLock) {
1188
+ rollbackPendingBundleLocked(stagingBundleId, unfinishedLaunch)
1189
+ }
1190
+
1191
+ private fun rollbackPendingBundleLocked(
1192
+ stagingBundleId: String,
1193
+ unfinishedLaunch: Boolean,
1186
1194
  ): Boolean {
1187
1195
  val metadata = loadMetadataOrNull() ?: return false
1188
1196
  if (metadata.stagingBundleId != stagingBundleId) {
@@ -1231,18 +1239,22 @@ class BundleFileStorageService(
1231
1239
  }
1232
1240
 
1233
1241
  private fun applyPendingRecoveryIfNeeded(pendingRecovery: PendingCrashRecovery?) {
1234
- val metadata = loadMetadataOrNull() ?: return
1235
- val stagingBundleId = metadata.stagingBundleId ?: return
1242
+ synchronized(releaseStateLock) {
1243
+ val metadata = loadMetadataOrNull() ?: return
1244
+ val stagingBundleId = metadata.stagingBundleId ?: return
1236
1245
 
1237
- if (pendingRecovery?.shouldRollback == true &&
1238
- pendingRecovery.launchedBundleId == stagingBundleId &&
1239
- isVerificationPending(metadata)
1240
- ) {
1241
- rollbackPendingBundle(stagingBundleId)
1246
+ if (pendingRecovery?.shouldRollback == true &&
1247
+ pendingRecovery.launchedBundleId == stagingBundleId &&
1248
+ isVerificationPending(metadata)
1249
+ ) {
1250
+ rollbackPendingBundle(stagingBundleId)
1251
+ }
1242
1252
  }
1243
1253
  }
1244
1254
 
1245
- private fun selectLaunch(): LaunchSelection {
1255
+ private fun selectLaunch(): LaunchSelection = synchronized(releaseStateLock) { selectLaunchLocked() }
1256
+
1257
+ private fun selectLaunchLocked(): LaunchSelection {
1246
1258
  val metadata = loadMetadataOrNull()
1247
1259
  if (metadata == null) {
1248
1260
  return LaunchSelection(
@@ -1263,7 +1275,7 @@ class BundleFileStorageService(
1263
1275
  }
1264
1276
 
1265
1277
  if (metadata.verificationPending && rollbackPendingBundle(stagingBundleId)) {
1266
- return selectLaunch()
1278
+ return selectLaunchLocked()
1267
1279
  }
1268
1280
  }
1269
1281
 
@@ -1583,35 +1595,39 @@ class BundleFileStorageService(
1583
1595
 
1584
1596
  override fun markLaunchCompleted(currentBundleId: String?) {
1585
1597
  readyInterruptedLaunchRetry(currentBundleId)
1586
- val metadata = loadMetadataOrNull() ?: return
1587
- val stagingBundleId = metadata.stagingBundleId ?: return
1588
- if (!metadata.verificationPending || stagingBundleId != currentBundleId) {
1589
- return
1590
- }
1598
+ // A Release selection can commit at the same time, from a native update
1599
+ // check or JS; neither may save over the other's read.
1600
+ synchronized(releaseStateLock) {
1601
+ val metadata = loadMetadataOrNull() ?: return
1602
+ val stagingBundleId = metadata.stagingBundleId ?: return
1603
+ if (!metadata.verificationPending || stagingBundleId != currentBundleId) {
1604
+ return
1605
+ }
1591
1606
 
1592
- val transition = metadata.pendingTransition
1593
- val fromBundleId = transition?.fromBundleId ?: getKnownLaunchBundleId(metadata)
1594
- saveMetadata(
1595
- metadata.copy(
1596
- stableBundleId = null,
1597
- stableSelection = null,
1598
- pendingUpdateStrategy = null,
1599
- pendingTransition = null,
1600
- verificationPending = false,
1601
- launchInProgress = false,
1602
- updatedAt = System.currentTimeMillis(),
1603
- ),
1604
- )
1605
- saveLaunchReport(
1606
- LaunchReport(
1607
- status = "UPDATE_APPLIED",
1608
- fromReleaseId = transition?.fromReleaseId,
1609
- fromBundleId = fromBundleId,
1610
- toReleaseId = transition?.toReleaseId,
1611
- toBundleId = stagingBundleId,
1612
- updateStrategy = metadata.pendingUpdateStrategy ?: resolveUpdateStrategy(),
1613
- ),
1614
- )
1607
+ val transition = metadata.pendingTransition
1608
+ val fromBundleId = transition?.fromBundleId ?: getKnownLaunchBundleId(metadata)
1609
+ saveMetadata(
1610
+ metadata.copy(
1611
+ stableBundleId = null,
1612
+ stableSelection = null,
1613
+ pendingUpdateStrategy = null,
1614
+ pendingTransition = null,
1615
+ verificationPending = false,
1616
+ launchInProgress = false,
1617
+ updatedAt = System.currentTimeMillis(),
1618
+ ),
1619
+ )
1620
+ saveLaunchReport(
1621
+ LaunchReport(
1622
+ status = "UPDATE_APPLIED",
1623
+ fromReleaseId = transition?.fromReleaseId,
1624
+ fromBundleId = fromBundleId,
1625
+ toReleaseId = transition?.toReleaseId,
1626
+ toBundleId = stagingBundleId,
1627
+ updateStrategy = metadata.pendingUpdateStrategy ?: resolveUpdateStrategy(),
1628
+ ),
1629
+ )
1630
+ }
1615
1631
  }
1616
1632
 
1617
1633
  // MARK: - notifyAppReady
@@ -1688,17 +1704,19 @@ class BundleFileStorageService(
1688
1704
  // Cleared before the rollbacks below, so the launch reports their RECOVERED.
1689
1705
  saveLaunchReport(null)
1690
1706
  applyPendingRecoveryIfNeeded(pendingRecovery())
1691
- // Only a new storage instance consumes an unfinished launch. Repeated bundle
1692
- // lookups or bridge reloads in this process must not reject its own launch.
1693
- val metadata = loadMetadataOrNull()
1694
- if (metadata?.verificationPending == true && metadata.launchInProgress) {
1695
- metadata.stagingBundleId?.let { rollbackPendingBundle(it, unfinishedLaunch = true) }
1696
- }
1697
- // A pending bundle whose file is gone rolls back, as selectLaunch does.
1698
- val settled = loadMetadataOrNull()
1699
- settled?.stagingBundleId?.let { stagingBundleId ->
1700
- if (settled.verificationPending && getBundleUrlForId(stagingBundleId) == null) {
1701
- rollbackPendingBundle(stagingBundleId)
1707
+ synchronized(releaseStateLock) {
1708
+ // Only a new storage instance consumes an unfinished launch. Repeated bundle
1709
+ // lookups or bridge reloads in this process must not reject its own launch.
1710
+ val metadata = loadMetadataOrNull()
1711
+ if (metadata?.verificationPending == true && metadata.launchInProgress) {
1712
+ metadata.stagingBundleId?.let { rollbackPendingBundle(it, unfinishedLaunch = true) }
1713
+ }
1714
+ // A pending bundle whose file is gone rolls back, as selectLaunch does.
1715
+ val settled = loadMetadataOrNull()
1716
+ settled?.stagingBundleId?.let { stagingBundleId ->
1717
+ if (settled.verificationPending && getBundleUrlForId(stagingBundleId) == null) {
1718
+ rollbackPendingBundle(stagingBundleId)
1719
+ }
1702
1720
  }
1703
1721
  }
1704
1722
  }
@@ -2708,47 +2726,51 @@ class BundleFileStorageService(
2708
2726
 
2709
2727
  override suspend fun resetChannel(): Boolean =
2710
2728
  withContext(Dispatchers.IO) {
2711
- if (!setBundleURL(null)) {
2712
- return@withContext false
2713
- }
2729
+ synchronized(releaseStateLock) { resetChannelLocked() }
2730
+ }
2714
2731
 
2715
- val clearedMetadata =
2716
- (loadMetadataOrNull() ?: BundleMetadata()).copy(
2717
- isolationKey = isolationKey,
2718
- stableBundleId = null,
2719
- stagingBundleId = null,
2720
- stableSelection = null,
2721
- stagingSelection = null,
2722
- pendingUpdateStrategy = null,
2723
- pendingTransition = null,
2724
- verificationPending = false,
2725
- launchInProgress = false,
2726
- // Selections made before the reset no longer stage; the
2727
- // high-water marks stay, so an older catalog is still refused.
2728
- currentSelectionContexts = emptyMap(),
2729
- updatedAt = System.currentTimeMillis(),
2730
- )
2732
+ private fun resetChannelLocked(): Boolean {
2733
+ if (!setBundleURL(null)) {
2734
+ return false
2735
+ }
2731
2736
 
2732
- if (!saveMetadata(clearedMetadata)) {
2733
- return@withContext false
2734
- }
2737
+ val clearedMetadata =
2738
+ (loadMetadataOrNull() ?: BundleMetadata()).copy(
2739
+ isolationKey = isolationKey,
2740
+ stableBundleId = null,
2741
+ stagingBundleId = null,
2742
+ stableSelection = null,
2743
+ stagingSelection = null,
2744
+ pendingUpdateStrategy = null,
2745
+ pendingTransition = null,
2746
+ verificationPending = false,
2747
+ launchInProgress = false,
2748
+ // Selections made before the reset no longer stage; the
2749
+ // high-water marks stay, so an older catalog is still refused.
2750
+ currentSelectionContexts = emptyMap(),
2751
+ updatedAt = System.currentTimeMillis(),
2752
+ )
2735
2753
 
2736
- saveLaunchReport(null)
2754
+ if (!saveMetadata(clearedMetadata)) {
2755
+ return false
2756
+ }
2737
2757
 
2738
- getBundleStoreDir().listFiles()?.forEach { file ->
2739
- if (
2740
- file.name == BundleMetadata.METADATA_FILENAME ||
2741
- file.name == CrashedHistory.CRASHED_HISTORY_FILENAME ||
2742
- file.name == LaunchReport.LAUNCH_REPORT_FILENAME
2743
- ) {
2744
- return@forEach
2745
- }
2758
+ saveLaunchReport(null)
2746
2759
 
2747
- if (file.isDirectory) {
2748
- file.deleteRecursively()
2749
- }
2760
+ getBundleStoreDir().listFiles()?.forEach { file ->
2761
+ if (
2762
+ file.name == BundleMetadata.METADATA_FILENAME ||
2763
+ file.name == CrashedHistory.CRASHED_HISTORY_FILENAME ||
2764
+ file.name == LaunchReport.LAUNCH_REPORT_FILENAME
2765
+ ) {
2766
+ return@forEach
2750
2767
  }
2751
2768
 
2752
- true
2769
+ if (file.isDirectory) {
2770
+ file.deleteRecursively()
2771
+ }
2753
2772
  }
2773
+
2774
+ return true
2775
+ }
2754
2776
  }
@@ -421,6 +421,7 @@ class BundleFileStorageService: BundleStorageService {
421
421
  private let builtInBundleIdProvider: () -> String
422
422
  private let builtInAssetResolver: BuiltInAssetResolver
423
423
  private let updateStrategyProvider: () -> UpdateStrategy
424
+ private let metadataWriter: (BundleMetadata, URL) -> Bool
424
425
  private let releaseStateLock = NSRecursiveLock()
425
426
 
426
427
  /// App-wide: every instance's storage reads the same install identity
@@ -787,7 +788,10 @@ class BundleFileStorageService: BundleStorageService {
787
788
  builtInBundleIdProvider: @escaping () -> String = { "" },
788
789
  builtInAssetResolver: BuiltInAssetResolver? = nil,
789
790
  updateStrategyProvider: (() -> UpdateStrategy)? = nil,
790
- downloadRetryDelay: TimeInterval = 1) {
791
+ downloadRetryDelay: TimeInterval = 1,
792
+ metadataWriter: @escaping (BundleMetadata, URL) -> Bool = { metadata, file in
793
+ metadata.save(to: file)
794
+ }) {
791
795
 
792
796
  self.fileSystem = fileSystem
793
797
  self.downloadRetryDelay = downloadRetryDelay
@@ -802,6 +806,7 @@ class BundleFileStorageService: BundleStorageService {
802
806
  defaultBundle: { instance.bundle }
803
807
  )
804
808
  self.updateStrategyProvider = updateStrategyProvider ?? { instance.updateStrategy() }
809
+ self.metadataWriter = metadataWriter
805
810
 
806
811
  // Create queue for file operations
807
812
  self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
@@ -859,7 +864,7 @@ class BundleFileStorageService: BundleStorageService {
859
864
  }
860
865
  var updatedMetadata = metadata
861
866
  updatedMetadata.isolationKey = isolationKey
862
- return updatedMetadata.save(to: file)
867
+ return metadataWriter(updatedMetadata, file)
863
868
  }
864
869
 
865
870
  private func loadLaunchReport() -> LaunchReport? {
@@ -1368,6 +1373,8 @@ class BundleFileStorageService: BundleStorageService {
1368
1373
  /// ended before first content without a crash marker.
1369
1374
  @discardableResult
1370
1375
  private func rollbackPendingBundle(_ stagingId: String, unfinishedLaunch: Bool = false) -> Bool {
1376
+ releaseStateLock.lock()
1377
+ defer { releaseStateLock.unlock() }
1371
1378
  guard var metadata = loadMetadataOrNull(), metadata.stagingBundleId == stagingId else {
1372
1379
  return false
1373
1380
  }
@@ -1443,6 +1450,8 @@ class BundleFileStorageService: BundleStorageService {
1443
1450
  }
1444
1451
 
1445
1452
  private func applyPendingRecoveryIfNeeded(_ pendingRecovery: PendingCrashRecovery?) {
1453
+ releaseStateLock.lock()
1454
+ defer { releaseStateLock.unlock() }
1446
1455
  guard let metadata = loadMetadataOrNull(),
1447
1456
  let stagingBundleId = metadata.stagingBundleId,
1448
1457
  pendingRecovery?.shouldRollback == true,
@@ -2001,6 +2010,8 @@ class BundleFileStorageService: BundleStorageService {
2001
2010
  }
2002
2011
 
2003
2012
  private func selectLaunch(bundle: Bundle) -> LaunchSelection {
2013
+ releaseStateLock.lock()
2014
+ defer { releaseStateLock.unlock() }
2004
2015
  guard let metadata = loadMetadataOrNull() else {
2005
2016
  return LaunchSelection(
2006
2017
  bundleURL: getFallbackBundleURL(bundle: bundle),
@@ -2081,6 +2092,8 @@ class BundleFileStorageService: BundleStorageService {
2081
2092
  // Cleared before the rollbacks below, so the launch reports their RECOVERED.
2082
2093
  saveLaunchReport(nil)
2083
2094
  applyPendingRecoveryIfNeeded(pendingRecovery())
2095
+ releaseStateLock.lock()
2096
+ defer { releaseStateLock.unlock() }
2084
2097
  // Only a new storage instance consumes an unfinished launch. Repeated bundle
2085
2098
  // lookups or bridge reloads in this process must not reject its own launch.
2086
2099
  if let metadata = loadMetadataOrNull(),
@@ -3019,6 +3032,10 @@ class BundleFileStorageService: BundleStorageService {
3019
3032
  func markLaunchCompleted(bundleId: String?) {
3020
3033
  // Before the guard: content from the built-in bundle (nil) also counts.
3021
3034
  readyInterruptedLaunchRetry(bundleId)
3035
+ // A Release selection can commit at the same time, from a native update
3036
+ // check or JavaScript; neither may save over the other's read.
3037
+ releaseStateLock.lock()
3038
+ defer { releaseStateLock.unlock() }
3022
3039
  guard let bundleId,
3023
3040
  var metadata = loadMetadataOrNull(),
3024
3041
  metadata.verificationPending,
@@ -3200,6 +3217,8 @@ class BundleFileStorageService: BundleStorageService {
3200
3217
  }
3201
3218
 
3202
3219
  func resetChannel() -> Result<Bool, Error> {
3220
+ releaseStateLock.lock()
3221
+ defer { releaseStateLock.unlock() }
3203
3222
  guard case .success = setBundleURL(localPath: nil) else {
3204
3223
  return .failure(BundleStorageError.unknown(nil))
3205
3224
  }
@@ -4,5 +4,5 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.HOT_UPDATER_SDK_VERSION = void 0;
7
- const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "1.1.2";
7
+ const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "1.1.4";
8
8
  //# sourceMappingURL=sdkVersion.js.map
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
 
3
- export const HOT_UPDATER_SDK_VERSION = "1.1.2";
3
+ export const HOT_UPDATER_SDK_VERSION = "1.1.4";
4
4
  //# sourceMappingURL=sdkVersion.js.map
@@ -1,2 +1,2 @@
1
- export declare const HOT_UPDATER_SDK_VERSION = "1.1.2";
1
+ export declare const HOT_UPDATER_SDK_VERSION = "1.1.4";
2
2
  //# sourceMappingURL=sdkVersion.d.ts.map
@@ -1,2 +1,2 @@
1
- export declare const HOT_UPDATER_SDK_VERSION = "1.1.2";
1
+ export declare const HOT_UPDATER_SDK_VERSION = "1.1.4";
2
2
  //# sourceMappingURL=sdkVersion.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "React Native OTA solution for self-hosted",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/commonjs/index.js",
@@ -124,9 +124,9 @@
124
124
  },
125
125
  "dependencies": {
126
126
  "use-sync-external-store": "1.5.0",
127
- "@hot-updater/protocol": "1.1.2",
128
- "@hot-updater/plugin-remote-config": "1.1.2",
129
- "@hot-updater/plugin-insights": "1.1.2"
127
+ "@hot-updater/plugin-insights": "1.1.4",
128
+ "@hot-updater/plugin-remote-config": "1.1.4",
129
+ "@hot-updater/protocol": "1.1.4"
130
130
  },
131
131
  "scripts": {
132
132
  "build": "node ./scripts/sync-sdk-version.mjs run -- bob build",
package/src/sdkVersion.ts CHANGED
@@ -1 +1 @@
1
- export const HOT_UPDATER_SDK_VERSION = "1.1.2";
1
+ export const HOT_UPDATER_SDK_VERSION = "1.1.4";