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.
- package/android/src/main/java/app/pulse/updates/PulseController.kt +62 -9
- package/android/src/main/java/app/pulse/updates/launcher/PulseAppLauncher.kt +11 -6
- package/ios/PulseUpdates/AppLauncher/PulseAppLauncher.swift +2 -2
- package/ios/PulseUpdates/PulseController.swift +54 -7
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|