pulse-updates 1.0.4 → 1.0.6

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.
@@ -99,6 +99,10 @@ class PulseController private constructor() {
99
99
  private var embeddedManifest: EmbeddedManifest? = null
100
100
  private val executor = Executors.newSingleThreadExecutor()
101
101
 
102
+ // Cached manifest from last check (to avoid duplicate requests in fetch)
103
+ // Only valid for the immediate check->fetch sequence, cleared on use or new check
104
+ private var lastCheckManifest: ManifestModel? = null
105
+
102
106
  // Embedded asset hashes set for download optimization
103
107
  internal val embeddedAssetHashes: Set<String>
104
108
  get() = embeddedManifest?.assets
@@ -526,11 +530,26 @@ class PulseController private constructor() {
526
530
  }
527
531
 
528
532
  executor.execute {
529
- PulseRemoteLoader.checkForUpdate(cfg, launchedUpdate?.updateId, callback)
533
+ // Clear any previous cached manifest before new check
534
+ lastCheckManifest = null
535
+
536
+ PulseRemoteLoader.checkForUpdate(cfg, launchedUpdate?.updateId) { result ->
537
+ // Cache the manifest if update is available (for use by fetchUpdate)
538
+ result.fold(
539
+ onSuccess = { checkResult ->
540
+ if (checkResult.isAvailable && checkResult.manifest != null) {
541
+ lastCheckManifest = checkResult.manifest
542
+ pulseLog(TAG, "checkForUpdate: cached manifest for fetch")
543
+ }
544
+ },
545
+ onFailure = { }
546
+ )
547
+ callback(result)
548
+ }
530
549
  }
531
550
  }
532
551
 
533
- fun fetchUpdate(callback: (Result<FetchResult>) -> Unit) {
552
+ fun fetchUpdate(cachedManifest: ManifestModel? = null, callback: (Result<FetchResult>) -> Unit) {
534
553
  val cfg = config
535
554
  val ctx = context
536
555
  val dir = directory
@@ -545,8 +564,18 @@ class PulseController private constructor() {
545
564
  return
546
565
  }
547
566
 
567
+ // Use cached manifest from recent check if available
568
+ var manifestToUse = cachedManifest
569
+ if (manifestToUse == null && lastCheckManifest != null) {
570
+ pulseLog(TAG, "fetchUpdate: using cached manifest from recent check")
571
+ manifestToUse = lastCheckManifest
572
+ }
573
+
574
+ // Clear the cache after using it
575
+ lastCheckManifest = null
576
+
548
577
  executor.execute {
549
- PulseRemoteLoader.fetchUpdate(cfg, database, dir, callback)
578
+ PulseRemoteLoader.fetchUpdate(cfg, database, dir, manifestToUse, callback)
550
579
  }
551
580
  }
552
581
 
@@ -609,13 +638,19 @@ class PulseController private constructor() {
609
638
  private fun loadEmbeddedManifest() {
610
639
  val ctx = context ?: return
611
640
 
612
- try {
613
- val json = ctx.assets.open("embedded-manifest.json").bufferedReader().use { it.readText() }
614
- embeddedManifest = EmbeddedManifest.fromJson(json)
615
- pulseLog(TAG, "Loaded embedded manifest: ${embeddedManifest?.updateId}")
616
- } catch (e: Exception) {
617
- pulseLog(TAG, "No embedded manifest found")
641
+ // Try pulse/ subdirectory first, then root
642
+ val paths = listOf("pulse/embedded-manifest.json", "embedded-manifest.json")
643
+ for (path in paths) {
644
+ try {
645
+ val json = ctx.assets.open(path).bufferedReader().use { it.readText() }
646
+ embeddedManifest = EmbeddedManifest.fromJson(json)
647
+ pulseLog(TAG, "Loaded embedded manifest from $path: ${embeddedManifest?.updateId}")
648
+ return
649
+ } catch (e: Exception) {
650
+ // Try next path
651
+ }
618
652
  }
653
+ pulseLog(TAG, "No embedded manifest found")
619
654
  }
620
655
 
621
656
  private fun seedEmbeddedUpdateIfNeeded() {
@@ -1135,8 +1170,22 @@ object PulseRemoteLoader {
1135
1170
  config: PulseUpdatesConfig,
1136
1171
  database: PulseDatabase?,
1137
1172
  directory: File,
1173
+ cachedManifest: ManifestModel? = null,
1138
1174
  callback: (Result<FetchResult>) -> Unit
1139
1175
  ) {
1176
+ // If we already have a manifest from a previous check, use it directly
1177
+ if (cachedManifest != null) {
1178
+ pulseLog(TAG, "fetchUpdate: using cached manifest, skipping check")
1179
+ downloadUpdate(cachedManifest, config, database, directory) { downloadResult ->
1180
+ downloadResult.fold(
1181
+ onSuccess = { callback(Result.success(FetchResult(isNew = true, manifest = cachedManifest))) },
1182
+ onFailure = { callback(Result.failure(it)) }
1183
+ )
1184
+ }
1185
+ return
1186
+ }
1187
+
1188
+ // No cached manifest, need to check first
1140
1189
  checkForUpdate(config, PulseController.getInstance().launchedUpdate?.updateId) { result ->
1141
1190
  result.fold(
1142
1191
  onSuccess = { checkResult ->
@@ -1259,6 +1308,10 @@ object PulseRemoteLoader {
1259
1308
 
1260
1309
  // Download assets and link them to the update
1261
1310
  val embeddedHashes = PulseController.getInstance().embeddedAssetHashes
1311
+ pulseLog(TAG, "downloadUpdate: embeddedHashes count=${embeddedHashes.size}")
1312
+ if (embeddedHashes.isNotEmpty()) {
1313
+ pulseLog(TAG, "downloadUpdate: first few embedded hashes: ${embeddedHashes.take(3).map { it.take(16) }}")
1314
+ }
1262
1315
 
1263
1316
  for (asset in manifest.assets) {
1264
1317
  val assetHash = asset.hash.lowercase()
@@ -323,13 +323,18 @@ class PulseAppLauncher(
323
323
  // MARK: - Embedded Manifest
324
324
 
325
325
  private fun loadEmbeddedManifest(): EmbeddedManifest? {
326
- return try {
327
- val json = context.assets.open("embedded-manifest.json").bufferedReader().use { it.readText() }
328
- EmbeddedManifest.fromJson(json)
329
- } catch (e: Exception) {
330
- pulseLogWarn(TAG, "No embedded manifest found: ${e.message}")
331
- null
326
+ // Try pulse/ subdirectory first, then root
327
+ val paths = listOf("pulse/embedded-manifest.json", "embedded-manifest.json")
328
+ for (path in paths) {
329
+ try {
330
+ val json = context.assets.open(path).bufferedReader().use { it.readText() }
331
+ return EmbeddedManifest.fromJson(json)
332
+ } catch (e: Exception) {
333
+ // Try next path
334
+ }
332
335
  }
336
+ pulseLogWarn(TAG, "No embedded manifest found")
337
+ return null
333
338
  }
334
339
 
335
340
  // MARK: - Crypto
@@ -473,8 +473,8 @@ struct PulseEmbeddedAsset {
473
473
  self.hash = hash
474
474
  self.key = json["key"] as? String
475
475
  self.type = json["type"] as? String ?? json["contentType"] as? String
476
- self.nsBundleDir = json["nsBundleDir"] as? String
477
- self.nsBundleFilename = json["nsBundleFilename"] as? String
476
+ self.nsBundleDir = json["mainBundleDir"] as? String ?? json["nsBundleDir"] as? String
477
+ self.nsBundleFilename = json["mainBundleFilename"] as? String ?? json["nsBundleFilename"] as? String
478
478
  }
479
479
  }
480
480
 
@@ -66,6 +66,10 @@ public final class PulseController {
66
66
  internal var embeddedAssetHashes: [String: URL] = [:]
67
67
  private var embeddedBundleHash: String?
68
68
 
69
+ // Cached manifest from last check (to avoid duplicate requests in fetch)
70
+ // Only valid for the immediate check->fetch sequence, cleared on use or new check
71
+ private var lastCheckManifest: PulseManifest?
72
+
69
73
  // MARK: - Native Config (from Info.plist, like expo-updates)
70
74
 
71
75
  /// Load config from Info.plist (called before JS starts)
@@ -475,15 +479,24 @@ public final class PulseController {
475
479
  return
476
480
  }
477
481
 
482
+ // Clear any previous cached manifest before new check
483
+ lastCheckManifest = nil
484
+
478
485
  PulseRemoteLoader.checkForUpdate(
479
486
  config: config,
480
- currentUpdateId: launchedUpdate?.updateId,
481
- completion: completion
482
- )
487
+ currentUpdateId: launchedUpdate?.updateId
488
+ ) { [weak self] result in
489
+ // Cache the manifest for subsequent fetchUpdate call
490
+ if case .success(let checkResult) = result, checkResult.isAvailable {
491
+ self?.lastCheckManifest = checkResult.manifest
492
+ }
493
+ completion(result)
494
+ }
483
495
  }
484
496
 
485
497
  /// Fetch and download an update
486
- public func fetchUpdate(completion: @escaping (Result<PulseFetchResult, Error>) -> Void) {
498
+ /// - Parameter cachedManifest: Optional manifest from a previous checkForUpdate call to avoid duplicate request
499
+ public func fetchUpdate(cachedManifest: PulseManifest? = nil, completion: @escaping (Result<PulseFetchResult, Error>) -> Void) {
487
500
  guard let config = config else {
488
501
  completion(.failure(PulseUpdatesError.notConfigured))
489
502
  return
@@ -494,10 +507,21 @@ public final class PulseController {
494
507
  return
495
508
  }
496
509
 
510
+ // Use cached manifest from recent checkForUpdate if available
511
+ var manifestToUse = cachedManifest
512
+ if manifestToUse == nil, let cached = lastCheckManifest {
513
+ pulseLog("fetchUpdate: using cached manifest from recent check")
514
+ manifestToUse = cached
515
+ }
516
+
517
+ // Clear cache after use
518
+ lastCheckManifest = nil
519
+
497
520
  PulseRemoteLoader.fetchUpdate(
498
521
  config: config,
499
522
  database: database,
500
523
  directory: directory,
524
+ cachedManifest: manifestToUse,
501
525
  completion: completion
502
526
  )
503
527
  }
@@ -561,7 +585,9 @@ public final class PulseController {
561
585
  }
562
586
 
563
587
  private func loadEmbeddedManifest() {
564
- guard let url = Bundle.main.url(forResource: "embedded-manifest", withExtension: "json"),
588
+ // Try pulse/ subdirectory first, then root
589
+ guard let url = Bundle.main.url(forResource: "embedded-manifest", withExtension: "json", subdirectory: "pulse")
590
+ ?? Bundle.main.url(forResource: "embedded-manifest", withExtension: "json"),
565
591
  let data = try? Data(contentsOf: url),
566
592
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
567
593
  pulseLog("No embedded manifest found")
@@ -592,7 +618,7 @@ public final class PulseController {
592
618
  }
593
619
  }
594
620
 
595
- pulseLog("Loaded embedded manifest: \(embeddedManifest?.updateId ?? "unknown")")
621
+ pulseLog("Loaded embedded manifest: \(embeddedManifest?.updateId ?? "unknown"), embeddedAssetHashes count: \(embeddedAssetHashes.count)")
596
622
  }
597
623
 
598
624
  private func seedEmbeddedUpdateIfNeeded() {
@@ -929,9 +955,24 @@ final class PulseRemoteLoader {
929
955
  config: PulseUpdatesConfig,
930
956
  database: PulseDatabase?,
931
957
  directory: URL,
958
+ cachedManifest: PulseManifest? = nil,
932
959
  completion: @escaping (Result<PulseFetchResult, Error>) -> Void
933
960
  ) {
934
- // First check for update
961
+ // If we already have a manifest from a previous check, use it directly
962
+ if let manifest = cachedManifest {
963
+ pulseLog("fetchUpdate: using cached manifest, skipping check")
964
+ downloadUpdate(manifest: manifest, config: config, database: database, directory: directory) { downloadResult in
965
+ switch downloadResult {
966
+ case .failure(let error):
967
+ completion(.failure(error))
968
+ case .success:
969
+ completion(.success(PulseFetchResult(isNew: true, manifest: manifest)))
970
+ }
971
+ }
972
+ return
973
+ }
974
+
975
+ // No cached manifest, need to check first
935
976
  checkForUpdate(config: config, currentUpdateId: PulseController.shared.launchedUpdate?.updateId) { result in
936
977
  switch result {
937
978
  case .failure(let error):
@@ -1121,6 +1162,12 @@ final class PulseRemoteLoader {
1121
1162
  let group = DispatchGroup()
1122
1163
  var downloadError: Error?
1123
1164
 
1165
+ let embeddedHashes = PulseController.shared.embeddedAssetHashes
1166
+ pulseLog("downloadAssets: embeddedAssetHashes count=\(embeddedHashes.count)")
1167
+ if !embeddedHashes.isEmpty {
1168
+ pulseLog("downloadAssets: first few embedded hashes: \(Array(embeddedHashes.keys.prefix(3)).map { String($0.prefix(16)) })")
1169
+ }
1170
+
1124
1171
  for asset in assets {
1125
1172
  group.enter()
1126
1173
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-updates",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "OTA updates for React Native - lightweight alternative to expo-updates",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",