pulse-updates 1.0.7 → 1.0.9
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 +19 -1
- package/android/src/main/java/app/pulse/updates/launcher/PulseAppLauncher.kt +97 -13
- package/ios/PulseUpdates/AppLauncher/PulseAppLauncher.swift +171 -39
- package/ios/PulseUpdates/PulseController.swift +148 -26
- package/ios/PulseUpdates/PulseUpdates.swift +2 -1
- package/lib/commonjs/assetResolver.js +0 -13
- package/lib/commonjs/assetResolver.js.map +1 -1
- package/lib/module/assetResolver.js +0 -13
- package/lib/module/assetResolver.js.map +1 -1
- package/lib/typescript/assetResolver.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-embedded-manifest.mjs +7 -0
- package/src/assetResolver.ts +0 -13
|
@@ -199,6 +199,9 @@ class PulseController private constructor() {
|
|
|
199
199
|
fun initializeWithoutStarting() {
|
|
200
200
|
if (isStarted) return
|
|
201
201
|
|
|
202
|
+
// Mark as started to prevent re-initialization during reload
|
|
203
|
+
isStarted = true
|
|
204
|
+
|
|
202
205
|
createDirectories()
|
|
203
206
|
initializeDatabase()
|
|
204
207
|
loadEmbeddedManifest()
|
|
@@ -538,8 +541,18 @@ class PulseController private constructor() {
|
|
|
538
541
|
result.fold(
|
|
539
542
|
onSuccess = { checkResult ->
|
|
540
543
|
if (checkResult.isAvailable && checkResult.manifest != null) {
|
|
544
|
+
// Compare commitTime: if embedded is newer, don't show update
|
|
545
|
+
val embeddedCommitTime = embeddedManifest?.commitTime ?: 0L
|
|
546
|
+
val otaCommitTime = checkResult.manifest.commitTime?.time ?: 0L
|
|
547
|
+
|
|
548
|
+
if (embeddedCommitTime > 0 && otaCommitTime > 0 && embeddedCommitTime >= otaCommitTime) {
|
|
549
|
+
pulseLog(TAG, "checkForUpdate: embedded is newer or same (embedded=$embeddedCommitTime >= ota=$otaCommitTime), no update needed")
|
|
550
|
+
callback(Result.success(CheckResult(isAvailable = false)))
|
|
551
|
+
return@checkForUpdate
|
|
552
|
+
}
|
|
553
|
+
|
|
541
554
|
lastCheckManifest = checkResult.manifest
|
|
542
|
-
pulseLog(TAG, "checkForUpdate: cached manifest for fetch")
|
|
555
|
+
pulseLog(TAG, "checkForUpdate: cached manifest for fetch (ota=$otaCommitTime > embedded=$embeddedCommitTime)")
|
|
543
556
|
}
|
|
544
557
|
},
|
|
545
558
|
onFailure = { }
|
|
@@ -746,6 +759,11 @@ class PulseController private constructor() {
|
|
|
746
759
|
bundleHash = update.bundleHash,
|
|
747
760
|
scopeKey = update.scopeKey
|
|
748
761
|
)
|
|
762
|
+
|
|
763
|
+
// Load manifest from database for metadata (build number, etc.)
|
|
764
|
+
update.manifest?.let { manifest ->
|
|
765
|
+
launchedManifestJson = manifest.toString()
|
|
766
|
+
}
|
|
749
767
|
}
|
|
750
768
|
}
|
|
751
769
|
|
|
@@ -114,6 +114,12 @@ class PulseAppLauncher(
|
|
|
114
114
|
try {
|
|
115
115
|
val assets = database.assetsForUpdate(update.updateId)
|
|
116
116
|
|
|
117
|
+
pulseLog(TAG, "ensureAllAssetsExist for updateId=${update.updateId.take(16)}... found ${assets.size} assets")
|
|
118
|
+
|
|
119
|
+
// Initialize asset map with embedded assets (these are referenced directly, not copied)
|
|
120
|
+
val mutableAssetFilesMap = buildEmbeddedAssetsMap().toMutableMap()
|
|
121
|
+
pulseLog(TAG, "Embedded assets map has ${mutableAssetFilesMap.size} entries")
|
|
122
|
+
|
|
117
123
|
if (assets.isEmpty()) {
|
|
118
124
|
// No assets to verify, just find the bundle
|
|
119
125
|
update.bundleHash?.let { hash ->
|
|
@@ -123,16 +129,59 @@ class PulseAppLauncher(
|
|
|
123
129
|
return
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
//
|
|
127
|
-
val
|
|
132
|
+
// Separate assets into those we need to verify vs those already in embedded
|
|
133
|
+
val assetsToVerify = mutableListOf<PulseAsset>()
|
|
134
|
+
|
|
135
|
+
for (asset in assets) {
|
|
136
|
+
if (asset.isLaunchAsset) {
|
|
137
|
+
// Always verify the launch asset (bundle) - it must be downloaded
|
|
138
|
+
assetsToVerify.add(asset)
|
|
139
|
+
} else {
|
|
140
|
+
val hashKey = asset.hash.lowercase()
|
|
141
|
+
// If asset hash is already in embedded map, we can use it directly
|
|
142
|
+
if (mutableAssetFilesMap.containsKey(hashKey)) {
|
|
143
|
+
// Already referenced from embedded, no action needed
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
// Check if downloaded version exists
|
|
147
|
+
val localFile = assetStorePath(asset.hash)
|
|
148
|
+
if (localFile.exists()) {
|
|
149
|
+
// Use downloaded version
|
|
150
|
+
val localUri = localFile.toURI().toString()
|
|
151
|
+
mutableAssetFilesMap[hashKey] = localUri
|
|
152
|
+
asset.key?.let { key ->
|
|
153
|
+
mutableAssetFilesMap[key] = localUri
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Need to verify/download this asset
|
|
157
|
+
assetsToVerify.add(asset)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pulseLog(TAG, "${assets.size - assetsToVerify.size} assets from embedded, ${assetsToVerify.size} to verify/download")
|
|
128
163
|
|
|
129
|
-
|
|
164
|
+
if (assetsToVerify.isEmpty()) {
|
|
165
|
+
// All assets accounted for, but we need the bundle
|
|
166
|
+
update.bundleHash?.let { hash ->
|
|
167
|
+
launchAssetFile = bundleStorePath(hash)
|
|
168
|
+
if (launchAssetFile?.exists() != true) {
|
|
169
|
+
callback(false, LauncherException("Bundle not found"))
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
assetFilesMap = mutableAssetFilesMap
|
|
174
|
+
callback(launchAssetFile != null, null)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
val totalAssets = assetsToVerify.size
|
|
130
179
|
val completedAssets = java.util.concurrent.atomic.AtomicInteger(0)
|
|
131
180
|
val launchError = AtomicReference<Exception?>(null)
|
|
132
181
|
val launchAssetFound = AtomicBoolean(false)
|
|
133
182
|
val latch = CountDownLatch(totalAssets)
|
|
134
183
|
|
|
135
|
-
for (asset in
|
|
184
|
+
for (asset in assetsToVerify) {
|
|
136
185
|
val localFile = assetStorePath(asset.hash)
|
|
137
186
|
|
|
138
187
|
Thread {
|
|
@@ -146,9 +195,8 @@ class PulseAppLauncher(
|
|
|
146
195
|
} else {
|
|
147
196
|
val localUri = localFile.toURI().toString()
|
|
148
197
|
synchronized(mutableAssetFilesMap) {
|
|
149
|
-
|
|
150
|
-
mutableAssetFilesMap[
|
|
151
|
-
// Also store by path-based key for Metro compatibility
|
|
198
|
+
val hashKey = asset.hash.lowercase()
|
|
199
|
+
mutableAssetFilesMap[hashKey] = localUri
|
|
152
200
|
asset.key?.let { key ->
|
|
153
201
|
mutableAssetFilesMap[key] = localUri
|
|
154
202
|
}
|
|
@@ -368,11 +416,25 @@ data class EmbeddedManifest(
|
|
|
368
416
|
fun fromJson(json: String): EmbeddedManifest? {
|
|
369
417
|
return try {
|
|
370
418
|
val root = org.json.JSONObject(json)
|
|
371
|
-
|
|
419
|
+
// Support both formats: wrapped in "manifest" key or directly at root
|
|
420
|
+
val manifest = root.optJSONObject("manifest") ?: root
|
|
372
421
|
|
|
373
422
|
val updateId = manifest.getString("updateId")
|
|
374
423
|
val runtimeVersion = manifest.getString("runtimeVersion")
|
|
375
|
-
|
|
424
|
+
|
|
425
|
+
// Support both commitTime (ms) and createdAt (ISO 8601)
|
|
426
|
+
val commitTime = if (manifest.has("commitTime")) {
|
|
427
|
+
manifest.optLong("commitTime", System.currentTimeMillis())
|
|
428
|
+
} else if (manifest.has("createdAt")) {
|
|
429
|
+
try {
|
|
430
|
+
java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US)
|
|
431
|
+
.parse(manifest.getString("createdAt"))?.time ?: System.currentTimeMillis()
|
|
432
|
+
} catch (e: Exception) {
|
|
433
|
+
System.currentTimeMillis()
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
System.currentTimeMillis()
|
|
437
|
+
}
|
|
376
438
|
|
|
377
439
|
val assets = mutableListOf<EmbeddedAsset>()
|
|
378
440
|
|
|
@@ -383,9 +445,21 @@ data class EmbeddedManifest(
|
|
|
383
445
|
}
|
|
384
446
|
}
|
|
385
447
|
|
|
386
|
-
// Parse launch asset
|
|
448
|
+
// Parse launch asset (either separate or from bundle field)
|
|
387
449
|
manifest.optJSONObject("launchAsset")?.let { launchAssetJson ->
|
|
388
450
|
EmbeddedAsset.fromJson(launchAssetJson, isLaunchAsset = true)?.let { assets.add(it) }
|
|
451
|
+
} ?: manifest.optJSONObject("bundle")?.let { bundleJson ->
|
|
452
|
+
val bundleHash = bundleJson.optString("hash", "")
|
|
453
|
+
if (bundleHash.isNotEmpty()) {
|
|
454
|
+
assets.add(EmbeddedAsset(
|
|
455
|
+
key = "bundle",
|
|
456
|
+
hash = bundleHash,
|
|
457
|
+
type = bundleJson.optString("contentType", null),
|
|
458
|
+
mainBundleDir = null,
|
|
459
|
+
mainBundleFilename = null,
|
|
460
|
+
isLaunchAsset = true
|
|
461
|
+
))
|
|
462
|
+
}
|
|
389
463
|
}
|
|
390
464
|
|
|
391
465
|
EmbeddedManifest(updateId, runtimeVersion, commitTime, assets)
|
|
@@ -440,10 +514,20 @@ data class PulseUpdateInfo(
|
|
|
440
514
|
)
|
|
441
515
|
|
|
442
516
|
class PulseDefaultSelectionPolicy : PulseSelectionPolicy {
|
|
517
|
+
/**
|
|
518
|
+
* Select the best update to launch
|
|
519
|
+
* Priority: 1) Ready updates (newest by commitTime), 2) Embedded updates (fallback)
|
|
520
|
+
*/
|
|
443
521
|
override fun selectUpdateToLaunch(updates: List<PulseUpdate>): PulseUpdate? {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
522
|
+
// First, try to find the newest ready (downloaded) update
|
|
523
|
+
val readyUpdates = updates.filter { it.status == PulseUpdateStatus.READY }
|
|
524
|
+
val newestReady = readyUpdates.maxByOrNull { it.commitTime.time }
|
|
525
|
+
if (newestReady != null) {
|
|
526
|
+
return newestReady
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Fall back to embedded update
|
|
530
|
+
return updates.find { it.status == PulseUpdateStatus.EMBEDDED }
|
|
447
531
|
}
|
|
448
532
|
|
|
449
533
|
override fun selectUpdatesToDelete(launchedUpdate: PulseUpdateInfo, allUpdates: List<PulseUpdateInfo>): List<PulseUpdateInfo> {
|
|
@@ -126,6 +126,9 @@ final class PulseAppLauncher {
|
|
|
126
126
|
do {
|
|
127
127
|
let assets = try database.assets(forUpdateId: update.updateId)
|
|
128
128
|
|
|
129
|
+
// Initialize asset map with embedded assets (these are referenced directly, not copied)
|
|
130
|
+
assetFilesMap = buildEmbeddedAssetsMap()
|
|
131
|
+
|
|
129
132
|
if assets.isEmpty {
|
|
130
133
|
// No assets to verify, just find the bundle
|
|
131
134
|
if let bundleHash = update.bundleHash {
|
|
@@ -135,15 +138,54 @@ final class PulseAppLauncher {
|
|
|
135
138
|
return
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
//
|
|
139
|
-
|
|
141
|
+
// Separate assets into those we need to verify vs those already in embedded
|
|
142
|
+
var assetsToVerify: [PulseAsset] = []
|
|
143
|
+
|
|
144
|
+
for asset in assets {
|
|
145
|
+
if asset.isLaunchAsset {
|
|
146
|
+
// Always verify the launch asset (bundle) - it must be downloaded
|
|
147
|
+
assetsToVerify.append(asset)
|
|
148
|
+
} else {
|
|
149
|
+
let hashKey = asset.hash.lowercased()
|
|
150
|
+
// If asset hash is already in embedded map, we can use it directly
|
|
151
|
+
if assetFilesMap[hashKey] != nil {
|
|
152
|
+
// Already referenced from embedded, no action needed
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
// Check if downloaded version exists
|
|
156
|
+
let localUrl = assetStorePath(hash: asset.hash)
|
|
157
|
+
if FileManager.default.fileExists(atPath: localUrl.path) {
|
|
158
|
+
// Use downloaded version
|
|
159
|
+
if let key = asset.key {
|
|
160
|
+
assetFilesMap[key] = localUrl.absoluteString
|
|
161
|
+
}
|
|
162
|
+
assetFilesMap[hashKey] = localUrl.absoluteString
|
|
163
|
+
} else {
|
|
164
|
+
// Need to verify/download this asset
|
|
165
|
+
assetsToVerify.append(asset)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if assetsToVerify.isEmpty {
|
|
171
|
+
// All assets accounted for, but we need the bundle
|
|
172
|
+
if let bundleHash = update.bundleHash {
|
|
173
|
+
launchAssetUrl = bundleStorePath(hash: bundleHash)
|
|
174
|
+
if !FileManager.default.fileExists(atPath: launchAssetUrl!.path) {
|
|
175
|
+
completion(false, PulseLauncherError.bundleNotFound)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
completion(launchAssetUrl != nil, nil)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
140
182
|
|
|
141
183
|
completedAssets = 0
|
|
142
184
|
launchError = nil
|
|
143
185
|
|
|
144
|
-
let totalAssets =
|
|
186
|
+
let totalAssets = assetsToVerify.count
|
|
145
187
|
|
|
146
|
-
for asset in
|
|
188
|
+
for asset in assetsToVerify {
|
|
147
189
|
let localUrl = assetStorePath(hash: asset.hash)
|
|
148
190
|
|
|
149
191
|
ensureAssetExists(asset: asset, localUrl: localUrl) { [weak self] exists in
|
|
@@ -155,8 +197,12 @@ final class PulseAppLauncher {
|
|
|
155
197
|
if exists {
|
|
156
198
|
if asset.isLaunchAsset {
|
|
157
199
|
self.launchAssetUrl = localUrl
|
|
158
|
-
} else
|
|
159
|
-
|
|
200
|
+
} else {
|
|
201
|
+
let hashKey = asset.hash.lowercased()
|
|
202
|
+
self.assetFilesMap[hashKey] = localUrl.absoluteString
|
|
203
|
+
if let key = asset.key {
|
|
204
|
+
self.assetFilesMap[key] = localUrl.absoluteString
|
|
205
|
+
}
|
|
160
206
|
}
|
|
161
207
|
}
|
|
162
208
|
|
|
@@ -229,22 +275,44 @@ final class PulseAppLauncher {
|
|
|
229
275
|
}
|
|
230
276
|
|
|
231
277
|
guard let match = matchingAsset,
|
|
232
|
-
let bundleFilename = match.nsBundleFilename
|
|
278
|
+
let bundleFilename = match.nsBundleFilename,
|
|
279
|
+
let assetType = match.type else {
|
|
233
280
|
completion(false)
|
|
234
281
|
return
|
|
235
282
|
}
|
|
236
283
|
|
|
237
|
-
// Get bundle path
|
|
284
|
+
// Get bundle path using FileManager (handles deep nested paths)
|
|
238
285
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
239
|
-
let bundlePath
|
|
286
|
+
let bundleRoot = Bundle.main.bundlePath
|
|
287
|
+
let fileManager = FileManager.default
|
|
288
|
+
let ext = assetType.components(separatedBy: "/").last ?? "png"
|
|
289
|
+
var sourcePath: String? = nil
|
|
290
|
+
|
|
291
|
+
// 1. Try in assets folder with subdirectory path
|
|
292
|
+
if let dir = match.nsBundleDir, !dir.isEmpty {
|
|
293
|
+
let path = "\(bundleRoot)/assets/\(dir)/\(bundleFilename).\(ext)"
|
|
294
|
+
if fileManager.fileExists(atPath: path) {
|
|
295
|
+
sourcePath = path
|
|
296
|
+
}
|
|
297
|
+
}
|
|
240
298
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
299
|
+
// 2. Try without assets/ prefix
|
|
300
|
+
if sourcePath == nil, let dir = match.nsBundleDir, !dir.isEmpty {
|
|
301
|
+
let path = "\(bundleRoot)/\(dir)/\(bundleFilename).\(ext)"
|
|
302
|
+
if fileManager.fileExists(atPath: path) {
|
|
303
|
+
sourcePath = path
|
|
304
|
+
}
|
|
245
305
|
}
|
|
246
306
|
|
|
247
|
-
|
|
307
|
+
// 3. Try at root
|
|
308
|
+
if sourcePath == nil {
|
|
309
|
+
let path = "\(bundleRoot)/\(bundleFilename).\(ext)"
|
|
310
|
+
if fileManager.fileExists(atPath: path) {
|
|
311
|
+
sourcePath = path
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
guard let finalSourcePath = sourcePath else {
|
|
248
316
|
self.launcherQueue.async {
|
|
249
317
|
completion(false)
|
|
250
318
|
}
|
|
@@ -260,13 +328,13 @@ final class PulseAppLauncher {
|
|
|
260
328
|
)
|
|
261
329
|
|
|
262
330
|
// Copy file
|
|
263
|
-
try FileManager.default.copyItem(atPath:
|
|
331
|
+
try FileManager.default.copyItem(atPath: finalSourcePath, toPath: localUrl.path)
|
|
264
332
|
|
|
265
333
|
self.launcherQueue.async {
|
|
266
334
|
completion(true)
|
|
267
335
|
}
|
|
268
336
|
} catch {
|
|
269
|
-
|
|
337
|
+
pulseLogWarn("Failed to copy asset from bundle: \(error)")
|
|
270
338
|
self.launcherQueue.async {
|
|
271
339
|
completion(false)
|
|
272
340
|
}
|
|
@@ -310,7 +378,7 @@ final class PulseAppLauncher {
|
|
|
310
378
|
completion(true, nil)
|
|
311
379
|
}
|
|
312
380
|
} catch {
|
|
313
|
-
|
|
381
|
+
pulseLogWarn("Failed to download asset: \(error)")
|
|
314
382
|
self.launcherQueue.async {
|
|
315
383
|
completion(false, error)
|
|
316
384
|
}
|
|
@@ -337,29 +405,59 @@ final class PulseAppLauncher {
|
|
|
337
405
|
|
|
338
406
|
// MARK: - Embedded Assets Map
|
|
339
407
|
|
|
340
|
-
/// Build map of embedded assets (key -> file URL)
|
|
408
|
+
/// Build map of embedded assets (hash -> file URL, key -> file URL)
|
|
409
|
+
/// Uses FileManager to find assets with deep nested paths
|
|
341
410
|
private func buildEmbeddedAssetsMap() -> [String: String] {
|
|
342
411
|
guard let embedded = embeddedManifest ?? loadEmbeddedManifest() else {
|
|
343
412
|
return [:]
|
|
344
413
|
}
|
|
345
414
|
|
|
346
415
|
var map: [String: String] = [:]
|
|
416
|
+
let bundleRoot = Bundle.main.bundlePath
|
|
417
|
+
let fileManager = FileManager.default
|
|
347
418
|
|
|
348
419
|
for asset in embedded.assets where !asset.isLaunchAsset {
|
|
349
|
-
guard let
|
|
350
|
-
let
|
|
420
|
+
guard let filename = asset.nsBundleFilename,
|
|
421
|
+
let assetType = asset.type else {
|
|
351
422
|
continue
|
|
352
423
|
}
|
|
353
424
|
|
|
354
|
-
|
|
425
|
+
// Get file extension from type (e.g., "image/png" -> "png")
|
|
426
|
+
let ext = assetType.components(separatedBy: "/").last ?? "png"
|
|
427
|
+
var bundlePath: String? = nil
|
|
428
|
+
|
|
429
|
+
// 1. Try in assets folder with subdirectory path (RN stores assets here)
|
|
355
430
|
if let dir = asset.nsBundleDir, !dir.isEmpty {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
431
|
+
let path = "\(bundleRoot)/assets/\(dir)/\(filename).\(ext)"
|
|
432
|
+
if fileManager.fileExists(atPath: path) {
|
|
433
|
+
bundlePath = path
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 2. Try without assets/ prefix
|
|
438
|
+
if bundlePath == nil, let dir = asset.nsBundleDir, !dir.isEmpty {
|
|
439
|
+
let path = "\(bundleRoot)/\(dir)/\(filename).\(ext)"
|
|
440
|
+
if fileManager.fileExists(atPath: path) {
|
|
441
|
+
bundlePath = path
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 3. Try at root
|
|
446
|
+
if bundlePath == nil {
|
|
447
|
+
let path = "\(bundleRoot)/\(filename).\(ext)"
|
|
448
|
+
if fileManager.fileExists(atPath: path) {
|
|
449
|
+
bundlePath = path
|
|
450
|
+
}
|
|
359
451
|
}
|
|
360
452
|
|
|
361
453
|
if let path = bundlePath {
|
|
362
|
-
|
|
454
|
+
let url = URL(fileURLWithPath: path).absoluteString
|
|
455
|
+
// Store by hash (lowercased for consistency)
|
|
456
|
+
map[asset.hash.lowercased()] = url
|
|
457
|
+
// Also store by key for Metro compatibility
|
|
458
|
+
if let key = asset.key {
|
|
459
|
+
map[key] = url
|
|
460
|
+
}
|
|
363
461
|
}
|
|
364
462
|
}
|
|
365
463
|
|
|
@@ -373,13 +471,16 @@ final class PulseAppLauncher {
|
|
|
373
471
|
// MARK: - Embedded Manifest
|
|
374
472
|
|
|
375
473
|
private func loadEmbeddedManifest() -> PulseEmbeddedManifest? {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
474
|
+
// Try pulse/ subdirectory first, then root
|
|
475
|
+
let paths = ["pulse/embedded-manifest.json", "embedded-manifest.json"]
|
|
476
|
+
for path in paths {
|
|
477
|
+
let url = Bundle.main.bundleURL.appendingPathComponent(path)
|
|
478
|
+
if let data = try? Data(contentsOf: url),
|
|
479
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
480
|
+
return PulseEmbeddedManifest(json: json)
|
|
481
|
+
}
|
|
380
482
|
}
|
|
381
|
-
|
|
382
|
-
return PulseEmbeddedManifest(json: json)
|
|
483
|
+
return nil
|
|
383
484
|
}
|
|
384
485
|
}
|
|
385
486
|
|
|
@@ -420,8 +521,10 @@ struct PulseEmbeddedManifest {
|
|
|
420
521
|
let assets: [PulseEmbeddedAsset]
|
|
421
522
|
|
|
422
523
|
init?(json: [String: Any]) {
|
|
423
|
-
|
|
424
|
-
|
|
524
|
+
// Support both formats: wrapped in "manifest" key or directly at root
|
|
525
|
+
let manifest = (json["manifest"] as? [String: Any]) ?? json
|
|
526
|
+
|
|
527
|
+
guard let updateId = manifest["updateId"] as? String,
|
|
425
528
|
let runtimeVersion = manifest["runtimeVersion"] as? String else {
|
|
426
529
|
return nil
|
|
427
530
|
}
|
|
@@ -431,6 +534,11 @@ struct PulseEmbeddedManifest {
|
|
|
431
534
|
|
|
432
535
|
if let commitTimeMs = manifest["commitTime"] as? Double {
|
|
433
536
|
self.commitTime = Date(timeIntervalSince1970: commitTimeMs / 1000)
|
|
537
|
+
} else if let createdAt = manifest["createdAt"] as? String {
|
|
538
|
+
// Parse ISO 8601 date
|
|
539
|
+
let formatter = ISO8601DateFormatter()
|
|
540
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
541
|
+
self.commitTime = formatter.date(from: createdAt) ?? Date()
|
|
434
542
|
} else {
|
|
435
543
|
self.commitTime = Date()
|
|
436
544
|
}
|
|
@@ -446,11 +554,22 @@ struct PulseEmbeddedManifest {
|
|
|
446
554
|
}
|
|
447
555
|
}
|
|
448
556
|
|
|
449
|
-
// Parse launch asset
|
|
557
|
+
// Parse launch asset (either separate or from bundle field)
|
|
450
558
|
if let launchAssetJson = manifest["launchAsset"] as? [String: Any],
|
|
451
559
|
var launchAsset = PulseEmbeddedAsset(json: launchAssetJson) {
|
|
452
560
|
launchAsset.isLaunchAsset = true
|
|
453
561
|
parsedAssets.append(launchAsset)
|
|
562
|
+
} else if let bundleJson = manifest["bundle"] as? [String: Any] {
|
|
563
|
+
// Create launch asset from bundle field
|
|
564
|
+
var bundleAsset = PulseEmbeddedAsset(
|
|
565
|
+
key: "bundle",
|
|
566
|
+
hash: (bundleJson["hash"] as? String) ?? "",
|
|
567
|
+
type: bundleJson["contentType"] as? String,
|
|
568
|
+
nsBundleDir: nil,
|
|
569
|
+
nsBundleFilename: nil
|
|
570
|
+
)
|
|
571
|
+
bundleAsset.isLaunchAsset = true
|
|
572
|
+
parsedAssets.append(bundleAsset)
|
|
454
573
|
}
|
|
455
574
|
|
|
456
575
|
self.assets = parsedAssets
|
|
@@ -465,6 +584,14 @@ struct PulseEmbeddedAsset {
|
|
|
465
584
|
let nsBundleFilename: String?
|
|
466
585
|
var isLaunchAsset: Bool = false
|
|
467
586
|
|
|
587
|
+
init(key: String?, hash: String, type: String?, nsBundleDir: String?, nsBundleFilename: String?) {
|
|
588
|
+
self.key = key
|
|
589
|
+
self.hash = hash
|
|
590
|
+
self.type = type
|
|
591
|
+
self.nsBundleDir = nsBundleDir
|
|
592
|
+
self.nsBundleFilename = nsBundleFilename
|
|
593
|
+
}
|
|
594
|
+
|
|
468
595
|
init?(json: [String: Any]) {
|
|
469
596
|
guard let hash = json["hash"] as? String else {
|
|
470
597
|
return nil
|
|
@@ -492,12 +619,17 @@ protocol PulseSelectionPolicy {
|
|
|
492
619
|
|
|
493
620
|
struct PulseDefaultSelectionPolicy: PulseSelectionPolicy {
|
|
494
621
|
|
|
495
|
-
/// Select the
|
|
622
|
+
/// Select the best update to launch
|
|
623
|
+
/// Priority: 1) Ready updates (newest by commitTime), 2) Embedded updates (fallback)
|
|
496
624
|
func selectUpdateToLaunch(from updates: [PulseUpdate]) -> PulseUpdate? {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
625
|
+
// First, try to find the newest ready (downloaded) update
|
|
626
|
+
let readyUpdates = updates.filter { $0.status == .ready }
|
|
627
|
+
if let newest = readyUpdates.sorted(by: { $0.commitTime > $1.commitTime }).first {
|
|
628
|
+
return newest
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Fall back to embedded update
|
|
632
|
+
return updates.first { $0.status == .embedded }
|
|
501
633
|
}
|
|
502
634
|
|
|
503
635
|
/// Select updates to delete - keeps launched update and one backup
|
|
@@ -70,6 +70,11 @@ public final class PulseController {
|
|
|
70
70
|
// Only valid for the immediate check->fetch sequence, cleared on use or new check
|
|
71
71
|
private var lastCheckManifest: PulseManifestModel?
|
|
72
72
|
|
|
73
|
+
// Flag to prevent concurrent fetch operations (with lock for thread safety)
|
|
74
|
+
private var isFetching = false
|
|
75
|
+
private var pendingFetchCallbacks: [(Result<PulseFetchResult, Error>) -> Void] = []
|
|
76
|
+
private let fetchLock = NSLock()
|
|
77
|
+
|
|
73
78
|
// MARK: - Native Config (from Info.plist, like expo-updates)
|
|
74
79
|
|
|
75
80
|
/// Load config from Info.plist (called before JS starts)
|
|
@@ -146,6 +151,9 @@ public final class PulseController {
|
|
|
146
151
|
public func initializeWithoutStarting() {
|
|
147
152
|
guard !isStarted else { return }
|
|
148
153
|
|
|
154
|
+
// Mark as started to prevent re-initialization during reload
|
|
155
|
+
isStarted = true
|
|
156
|
+
|
|
149
157
|
createDirectories()
|
|
150
158
|
initializeDatabase()
|
|
151
159
|
loadEmbeddedManifest()
|
|
@@ -296,17 +304,40 @@ public final class PulseController {
|
|
|
296
304
|
guard let manifest = embeddedManifest else { return [:] }
|
|
297
305
|
|
|
298
306
|
var assets: [String: String] = [:]
|
|
307
|
+
let bundleRoot = Bundle.main.bundlePath
|
|
308
|
+
let fileManager = FileManager.default
|
|
299
309
|
|
|
300
310
|
for asset in manifest.assets where !asset.isLaunchAsset {
|
|
301
|
-
guard let filename = asset.nsBundleFilename else {
|
|
311
|
+
guard let filename = asset.nsBundleFilename, let assetType = asset.type else {
|
|
302
312
|
continue
|
|
303
313
|
}
|
|
304
314
|
|
|
305
|
-
|
|
315
|
+
// Get file extension from type (e.g., "image/png" -> "png")
|
|
316
|
+
let ext = assetType.components(separatedBy: "/").last ?? "png"
|
|
317
|
+
var bundlePath: String? = nil
|
|
318
|
+
|
|
319
|
+
// 1. Try in assets folder with subdirectory path (RN stores assets here)
|
|
306
320
|
if let dir = asset.nsBundleDir, !dir.isEmpty {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
321
|
+
let path = "\(bundleRoot)/assets/\(dir)/\(filename).\(ext)"
|
|
322
|
+
if fileManager.fileExists(atPath: path) {
|
|
323
|
+
bundlePath = path
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 2. Try without assets/ prefix
|
|
328
|
+
if bundlePath == nil, let dir = asset.nsBundleDir, !dir.isEmpty {
|
|
329
|
+
let path = "\(bundleRoot)/\(dir)/\(filename).\(ext)"
|
|
330
|
+
if fileManager.fileExists(atPath: path) {
|
|
331
|
+
bundlePath = path
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 3. Try at root
|
|
336
|
+
if bundlePath == nil {
|
|
337
|
+
let path = "\(bundleRoot)/\(filename).\(ext)"
|
|
338
|
+
if fileManager.fileExists(atPath: path) {
|
|
339
|
+
bundlePath = path
|
|
340
|
+
}
|
|
310
341
|
}
|
|
311
342
|
|
|
312
343
|
if let path = bundlePath {
|
|
@@ -486,9 +517,25 @@ public final class PulseController {
|
|
|
486
517
|
config: config,
|
|
487
518
|
currentUpdateId: launchedUpdate?.updateId
|
|
488
519
|
) { [weak self] (result: Result<PulseCheckResult, Error>) in
|
|
520
|
+
guard let self = self else {
|
|
521
|
+
completion(result)
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
|
|
489
525
|
// Cache the manifest for subsequent fetchUpdate call
|
|
490
|
-
if case .success(let checkResult) = result, checkResult.isAvailable {
|
|
491
|
-
|
|
526
|
+
if case .success(let checkResult) = result, checkResult.isAvailable, let otaManifest = checkResult.manifest {
|
|
527
|
+
// Compare commitTime: if embedded is newer, don't show update
|
|
528
|
+
let embeddedCommitTime = self.embeddedManifest?.commitTime ?? Date.distantPast
|
|
529
|
+
let otaCommitTime = otaManifest.commitTime ?? Date.distantPast
|
|
530
|
+
|
|
531
|
+
if embeddedCommitTime >= otaCommitTime {
|
|
532
|
+
pulseLog("checkForUpdate: embedded is newer or same (embedded=\(embeddedCommitTime) >= ota=\(otaCommitTime)), no update needed")
|
|
533
|
+
completion(.success(PulseCheckResult(isAvailable: false)))
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
self.lastCheckManifest = otaManifest
|
|
538
|
+
pulseLog("checkForUpdate: cached manifest for fetch (ota=\(otaCommitTime) > embedded=\(embeddedCommitTime))")
|
|
492
539
|
}
|
|
493
540
|
completion(result)
|
|
494
541
|
}
|
|
@@ -507,6 +554,17 @@ public final class PulseController {
|
|
|
507
554
|
return
|
|
508
555
|
}
|
|
509
556
|
|
|
557
|
+
// Thread-safe check and set of isFetching flag
|
|
558
|
+
fetchLock.lock()
|
|
559
|
+
if isFetching {
|
|
560
|
+
pendingFetchCallbacks.append(completion)
|
|
561
|
+
fetchLock.unlock()
|
|
562
|
+
pulseLog("fetchUpdate: already fetching, queuing callback")
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
isFetching = true
|
|
566
|
+
fetchLock.unlock()
|
|
567
|
+
|
|
510
568
|
// Use cached manifest from recent checkForUpdate if available
|
|
511
569
|
var manifestToUse = cachedManifest
|
|
512
570
|
if manifestToUse == nil, let cached = lastCheckManifest {
|
|
@@ -521,9 +579,28 @@ public final class PulseController {
|
|
|
521
579
|
config: config,
|
|
522
580
|
database: database,
|
|
523
581
|
directory: directory,
|
|
524
|
-
cachedManifest: manifestToUse
|
|
525
|
-
|
|
526
|
-
|
|
582
|
+
cachedManifest: manifestToUse
|
|
583
|
+
) { [weak self] result in
|
|
584
|
+
guard let self = self else {
|
|
585
|
+
completion(result)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Thread-safe reset of isFetching and get pending callbacks
|
|
590
|
+
self.fetchLock.lock()
|
|
591
|
+
self.isFetching = false
|
|
592
|
+
let callbacks = self.pendingFetchCallbacks
|
|
593
|
+
self.pendingFetchCallbacks.removeAll()
|
|
594
|
+
self.fetchLock.unlock()
|
|
595
|
+
|
|
596
|
+
// Call original completion
|
|
597
|
+
completion(result)
|
|
598
|
+
|
|
599
|
+
// Call all queued callbacks with the same result
|
|
600
|
+
for callback in callbacks {
|
|
601
|
+
callback(result)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
527
604
|
}
|
|
528
605
|
|
|
529
606
|
/// Reload the app with the new update
|
|
@@ -586,11 +663,13 @@ public final class PulseController {
|
|
|
586
663
|
|
|
587
664
|
private func loadEmbeddedManifest() {
|
|
588
665
|
// Try pulse/ subdirectory first, then root
|
|
589
|
-
|
|
590
|
-
|
|
666
|
+
let urlInPulse = Bundle.main.url(forResource: "embedded-manifest", withExtension: "json", subdirectory: "pulse")
|
|
667
|
+
let urlInRoot = Bundle.main.url(forResource: "embedded-manifest", withExtension: "json")
|
|
668
|
+
|
|
669
|
+
guard let url = urlInPulse ?? urlInRoot,
|
|
591
670
|
let data = try? Data(contentsOf: url),
|
|
592
671
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
593
|
-
pulseLog("No embedded manifest found")
|
|
672
|
+
pulseLog("No embedded manifest found or failed to parse")
|
|
594
673
|
return
|
|
595
674
|
}
|
|
596
675
|
|
|
@@ -598,17 +677,50 @@ public final class PulseController {
|
|
|
598
677
|
|
|
599
678
|
// Cache embedded asset hashes
|
|
600
679
|
if let manifest = embeddedManifest {
|
|
680
|
+
var foundCount = 0
|
|
681
|
+
var notFoundCount = 0
|
|
682
|
+
let bundleRoot = Bundle.main.bundlePath
|
|
683
|
+
let fileManager = FileManager.default
|
|
684
|
+
|
|
601
685
|
for asset in manifest.assets {
|
|
602
|
-
if let filename = asset.nsBundleFilename {
|
|
603
|
-
|
|
686
|
+
if let filename = asset.nsBundleFilename, let assetType = asset.type {
|
|
687
|
+
// Get file extension from type (e.g., "image/png" -> "png")
|
|
688
|
+
let ext = assetType.components(separatedBy: "/").last ?? "png"
|
|
689
|
+
var bundlePath: String? = nil
|
|
690
|
+
|
|
691
|
+
// 1. Try in assets folder with subdirectory path (RN stores assets here)
|
|
604
692
|
if let dir = asset.nsBundleDir, !dir.isEmpty {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
693
|
+
let path = "\(bundleRoot)/assets/\(dir)/\(filename).\(ext)"
|
|
694
|
+
if fileManager.fileExists(atPath: path) {
|
|
695
|
+
bundlePath = path
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// 2. Try without assets/ prefix
|
|
700
|
+
if bundlePath == nil, let dir = asset.nsBundleDir, !dir.isEmpty {
|
|
701
|
+
let path = "\(bundleRoot)/\(dir)/\(filename).\(ext)"
|
|
702
|
+
if fileManager.fileExists(atPath: path) {
|
|
703
|
+
bundlePath = path
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 3. Try at root
|
|
708
|
+
if bundlePath == nil {
|
|
709
|
+
let path = "\(bundleRoot)/\(filename).\(ext)"
|
|
710
|
+
if fileManager.fileExists(atPath: path) {
|
|
711
|
+
bundlePath = path
|
|
712
|
+
}
|
|
608
713
|
}
|
|
609
714
|
|
|
610
715
|
if let path = bundlePath {
|
|
611
716
|
embeddedAssetHashes[asset.hash.lowercased()] = URL(fileURLWithPath: path)
|
|
717
|
+
foundCount += 1
|
|
718
|
+
} else {
|
|
719
|
+
notFoundCount += 1
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
if !asset.isLaunchAsset {
|
|
723
|
+
notFoundCount += 1
|
|
612
724
|
}
|
|
613
725
|
}
|
|
614
726
|
|
|
@@ -616,6 +728,7 @@ public final class PulseController {
|
|
|
616
728
|
embeddedBundleHash = asset.hash.lowercased()
|
|
617
729
|
}
|
|
618
730
|
}
|
|
731
|
+
pulseLog("Assets found in bundle: \(foundCount), not found: \(notFoundCount)")
|
|
619
732
|
}
|
|
620
733
|
|
|
621
734
|
pulseLog("Loaded embedded manifest: \(embeddedManifest?.updateId ?? "unknown"), embeddedAssetHashes count: \(embeddedAssetHashes.count)")
|
|
@@ -708,6 +821,13 @@ public final class PulseController {
|
|
|
708
821
|
bundleHash: update.bundleHash,
|
|
709
822
|
scopeKey: update.scopeKey
|
|
710
823
|
)
|
|
824
|
+
|
|
825
|
+
// Load manifest from database for metadata (build number, etc.)
|
|
826
|
+
if let manifest = update.manifest,
|
|
827
|
+
let data = try? JSONSerialization.data(withJSONObject: manifest, options: []),
|
|
828
|
+
let jsonString = String(data: data, encoding: .utf8) {
|
|
829
|
+
self.launchedManifestJson = jsonString
|
|
830
|
+
}
|
|
711
831
|
}
|
|
712
832
|
}
|
|
713
833
|
|
|
@@ -1133,9 +1253,16 @@ final class PulseRemoteLoader {
|
|
|
1133
1253
|
}
|
|
1134
1254
|
|
|
1135
1255
|
// Move to final destination
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1256
|
+
do {
|
|
1257
|
+
try FileManager.default.createDirectory(at: destination.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
1258
|
+
if FileManager.default.fileExists(atPath: destination.path) {
|
|
1259
|
+
try FileManager.default.removeItem(at: destination)
|
|
1260
|
+
}
|
|
1261
|
+
try FileManager.default.moveItem(at: tempPath, to: destination)
|
|
1262
|
+
} catch {
|
|
1263
|
+
completion(.failure(error))
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1139
1266
|
|
|
1140
1267
|
// Store in database as launch asset
|
|
1141
1268
|
storeBundleInDatabase()
|
|
@@ -1161,12 +1288,7 @@ final class PulseRemoteLoader {
|
|
|
1161
1288
|
|
|
1162
1289
|
let group = DispatchGroup()
|
|
1163
1290
|
var downloadError: Error?
|
|
1164
|
-
|
|
1165
1291
|
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
1292
|
|
|
1171
1293
|
for asset in assets {
|
|
1172
1294
|
group.enter()
|
|
@@ -98,7 +98,8 @@ public class PulseUpdates: RCTEventEmitter {
|
|
|
98
98
|
// Local assets map for expo-asset compatibility
|
|
99
99
|
// Key: asset key from manifest, Value: local file:// URL
|
|
100
100
|
// expo-updates exposes this as localAssets
|
|
101
|
-
|
|
101
|
+
let assets = controller.localAssets
|
|
102
|
+
state["localAssets"] = assets.isEmpty ? [:] : assets
|
|
102
103
|
|
|
103
104
|
return state
|
|
104
105
|
}
|
|
@@ -105,22 +105,9 @@ function resolveLocalAsset(asset) {
|
|
|
105
105
|
const keysToTry = buildAssetKeys(asset);
|
|
106
106
|
for (const key of keysToTry) {
|
|
107
107
|
if (localAssets[key]) {
|
|
108
|
-
// Debug: log successful resolution
|
|
109
|
-
if (__DEV__) {
|
|
110
|
-
console.log(`[PulseAssetResolver] Resolved ${asset.name}.${asset.type} with key=${key.substring(0, 16)}...`);
|
|
111
|
-
}
|
|
112
108
|
return localAssets[key];
|
|
113
109
|
}
|
|
114
110
|
}
|
|
115
|
-
|
|
116
|
-
// Debug: log failed resolution
|
|
117
|
-
if (__DEV__) {
|
|
118
|
-
console.log(`[PulseAssetResolver] MISS for ${asset.name}.${asset.type}`, {
|
|
119
|
-
triedKeys: keysToTry.map(k => k.substring(0, 16) + '...'),
|
|
120
|
-
localAssetsCount: Object.keys(localAssets).length,
|
|
121
|
-
localAssetsSample: Object.keys(localAssets).slice(0, 3).map(k => k.substring(0, 16) + '...')
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
111
|
return null;
|
|
125
112
|
}
|
|
126
113
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["localAssets","isPatched","originalDefaultAsset","applyPatch","AssetSourceResolver","require","default","prototype","defaultAsset","Object","keys","length","localUri","resolveLocalAsset","asset","fromSource","e","call","apply","initializeAssetResolver","assets","updateLocalAssets","keysToTry","buildAssetKeys","key","
|
|
1
|
+
{"version":3,"names":["localAssets","isPatched","originalDefaultAsset","applyPatch","AssetSourceResolver","require","default","prototype","defaultAsset","Object","keys","length","localUri","resolveLocalAsset","asset","fromSource","e","call","apply","initializeAssetResolver","assets","updateLocalAssets","keysToTry","buildAssetKeys","key","name","type","httpServerLocation","hash","fileHashes","push","values","forEach","h","location","startsWith","slice","scales","scale","scaleSuffix","_default","exports"],"sourceRoot":"../../src","sources":["assetResolver.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -97,22 +97,9 @@ function resolveLocalAsset(asset) {
|
|
|
97
97
|
const keysToTry = buildAssetKeys(asset);
|
|
98
98
|
for (const key of keysToTry) {
|
|
99
99
|
if (localAssets[key]) {
|
|
100
|
-
// Debug: log successful resolution
|
|
101
|
-
if (__DEV__) {
|
|
102
|
-
console.log(`[PulseAssetResolver] Resolved ${asset.name}.${asset.type} with key=${key.substring(0, 16)}...`);
|
|
103
|
-
}
|
|
104
100
|
return localAssets[key];
|
|
105
101
|
}
|
|
106
102
|
}
|
|
107
|
-
|
|
108
|
-
// Debug: log failed resolution
|
|
109
|
-
if (__DEV__) {
|
|
110
|
-
console.log(`[PulseAssetResolver] MISS for ${asset.name}.${asset.type}`, {
|
|
111
|
-
triedKeys: keysToTry.map(k => k.substring(0, 16) + '...'),
|
|
112
|
-
localAssetsCount: Object.keys(localAssets).length,
|
|
113
|
-
localAssetsSample: Object.keys(localAssets).slice(0, 3).map(k => k.substring(0, 16) + '...')
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
103
|
return null;
|
|
117
104
|
}
|
|
118
105
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["localAssets","isPatched","originalDefaultAsset","applyPatch","AssetSourceResolver","require","default","prototype","defaultAsset","Object","keys","length","localUri","resolveLocalAsset","asset","fromSource","e","call","apply","initializeAssetResolver","assets","updateLocalAssets","keysToTry","buildAssetKeys","key","
|
|
1
|
+
{"version":3,"names":["localAssets","isPatched","originalDefaultAsset","applyPatch","AssetSourceResolver","require","default","prototype","defaultAsset","Object","keys","length","localUri","resolveLocalAsset","asset","fromSource","e","call","apply","initializeAssetResolver","assets","updateLocalAssets","keysToTry","buildAssetKeys","key","name","type","httpServerLocation","hash","fileHashes","push","values","forEach","h","location","startsWith","slice","scales","scale","scaleSuffix"],"sourceRoot":"../../src","sources":["assetResolver.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assetResolver.d.ts","sourceRoot":"","sources":["../../src/assetResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA2DH;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAGnF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAE7E;;;;;
|
|
1
|
+
{"version":3,"file":"assetResolver.d.ts","sourceRoot":"","sources":["../../src/assetResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA2DH;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAGnF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAE7E;;;;;AAyFD,wBAGE"}
|
package/package.json
CHANGED
|
@@ -226,6 +226,13 @@ async function main() {
|
|
|
226
226
|
const isDrawable = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(type);
|
|
227
227
|
assetInfo.resourcesFolder = isDrawable ? 'drawable' : 'raw';
|
|
228
228
|
assetInfo.resourcesFilename = getAndroidResourceIdentifier(relative, name);
|
|
229
|
+
// Also set mainBundleDir/mainBundleFilename for Kotlin code compatibility
|
|
230
|
+
// Assets are copied to pulse/assets/<relative_path> in the APK
|
|
231
|
+
const relativeDir = path.dirname(relative);
|
|
232
|
+
assetInfo.mainBundleDir = relativeDir && relativeDir !== '.'
|
|
233
|
+
? `pulse/assets/${relativeDir}`
|
|
234
|
+
: 'pulse/assets';
|
|
235
|
+
assetInfo.mainBundleFilename = path.basename(relative);
|
|
229
236
|
}
|
|
230
237
|
|
|
231
238
|
assets.push(assetInfo);
|
package/src/assetResolver.ts
CHANGED
|
@@ -101,23 +101,10 @@ function resolveLocalAsset(asset: any): string | null {
|
|
|
101
101
|
|
|
102
102
|
for (const key of keysToTry) {
|
|
103
103
|
if (localAssets[key]) {
|
|
104
|
-
// Debug: log successful resolution
|
|
105
|
-
if (__DEV__) {
|
|
106
|
-
console.log(`[PulseAssetResolver] Resolved ${asset.name}.${asset.type} with key=${key.substring(0, 16)}...`);
|
|
107
|
-
}
|
|
108
104
|
return localAssets[key];
|
|
109
105
|
}
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
// Debug: log failed resolution
|
|
113
|
-
if (__DEV__) {
|
|
114
|
-
console.log(`[PulseAssetResolver] MISS for ${asset.name}.${asset.type}`, {
|
|
115
|
-
triedKeys: keysToTry.map(k => k.substring(0, 16) + '...'),
|
|
116
|
-
localAssetsCount: Object.keys(localAssets).length,
|
|
117
|
-
localAssetsSample: Object.keys(localAssets).slice(0, 3).map(k => k.substring(0, 16) + '...')
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
108
|
return null;
|
|
122
109
|
}
|
|
123
110
|
|