@hot-updater/react-native 0.27.0 → 0.28.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.
- package/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +9 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +170 -204
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +39 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
- package/android/src/newarch/HotUpdaterModule.kt +2 -8
- package/android/src/oldarch/HotUpdaterModule.kt +2 -8
- package/android/src/oldarch/HotUpdaterSpec.kt +1 -1
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +189 -203
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +265 -11
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +293 -9
- package/lib/commonjs/native.js +18 -21
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +86 -0
- package/lib/commonjs/native.spec.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +4 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/native.js +17 -20
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +85 -0
- package/lib/module/native.spec.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +5 -6
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +4 -15
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.spec.d.ts +2 -0
- package/lib/typescript/commonjs/native.spec.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +4 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +2 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +2 -5
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +4 -15
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/native.spec.d.ts +2 -0
- package/lib/typescript/module/native.spec.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +4 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +2 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +2 -5
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/native.spec.ts +84 -0
- package/src/native.ts +20 -19
- package/src/specs/NativeHotUpdater.ts +4 -6
- package/src/types.ts +2 -3
- package/src/wrap.tsx +7 -11
|
@@ -153,17 +153,11 @@ class HotUpdaterModule internal constructor(
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
156
|
-
override fun notifyAppReady(
|
|
157
|
-
val bundleId = params.getString("bundleId")
|
|
156
|
+
override fun notifyAppReady(): String {
|
|
158
157
|
val result = JSONObject()
|
|
159
158
|
|
|
160
|
-
if (bundleId == null) {
|
|
161
|
-
result.put("status", "STABLE")
|
|
162
|
-
return result.toString()
|
|
163
|
-
}
|
|
164
|
-
|
|
165
159
|
val impl = getInstance()
|
|
166
|
-
val statusMap = impl.notifyAppReady(
|
|
160
|
+
val statusMap = impl.notifyAppReady()
|
|
167
161
|
|
|
168
162
|
result.put("status", statusMap["status"] as? String ?: "STABLE")
|
|
169
163
|
statusMap["crashedBundleId"]?.let {
|
|
@@ -103,13 +103,14 @@ public protocol BundleStorageService {
|
|
|
103
103
|
func setBundleURL(localPath: String?) -> Result<Void, Error>
|
|
104
104
|
func getCachedBundleURL() -> URL?
|
|
105
105
|
func getFallbackBundleURL(bundle: Bundle) -> URL? // Synchronous as it's lightweight
|
|
106
|
-
func
|
|
106
|
+
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
107
107
|
|
|
108
108
|
// Bundle update
|
|
109
109
|
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
110
110
|
|
|
111
111
|
// Rollback support
|
|
112
|
-
func
|
|
112
|
+
func markLaunchCompleted(bundleId: String?)
|
|
113
|
+
func notifyAppReady() -> [String: Any]
|
|
113
114
|
func getCrashHistory() -> CrashedHistory
|
|
114
115
|
func clearCrashHistory() -> Bool
|
|
115
116
|
|
|
@@ -139,8 +140,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
139
140
|
|
|
140
141
|
private var activeTasks: [URLSessionTask] = []
|
|
141
142
|
|
|
142
|
-
|
|
143
|
-
private var sessionRollbackBundleId: String?
|
|
143
|
+
private var currentLaunchReport: LaunchReport?
|
|
144
144
|
|
|
145
145
|
public init(fileSystem: FileSystemService,
|
|
146
146
|
downloadService: DownloadService,
|
|
@@ -182,6 +182,13 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
182
182
|
return URL(fileURLWithPath: storeDir).appendingPathComponent(CrashedHistory.crashedHistoryFilename)
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
private func launchReportFileURL() -> URL? {
|
|
186
|
+
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
189
|
+
return URL(fileURLWithPath: storeDir).appendingPathComponent(LaunchReport.launchReportFilename)
|
|
190
|
+
}
|
|
191
|
+
|
|
185
192
|
// MARK: - Metadata Operations
|
|
186
193
|
|
|
187
194
|
private func loadMetadataOrNull() -> BundleMetadata? {
|
|
@@ -200,6 +207,51 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
200
207
|
return updatedMetadata.save(to: file)
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
private func loadLaunchReport() -> LaunchReport? {
|
|
211
|
+
if let currentLaunchReport {
|
|
212
|
+
return currentLaunchReport
|
|
213
|
+
}
|
|
214
|
+
guard let file = launchReportFileURL(),
|
|
215
|
+
let report = LaunchReport.load(from: file) else {
|
|
216
|
+
return nil
|
|
217
|
+
}
|
|
218
|
+
currentLaunchReport = report
|
|
219
|
+
return report
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private func saveLaunchReport(_ report: LaunchReport?) {
|
|
223
|
+
currentLaunchReport = report
|
|
224
|
+
guard let file = launchReportFileURL() else {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
guard let report else {
|
|
229
|
+
if FileManager.default.fileExists(atPath: file.path) {
|
|
230
|
+
try? FileManager.default.removeItem(at: file)
|
|
231
|
+
}
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_ = report.save(to: file)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func createInitialMetadata() -> BundleMetadata {
|
|
239
|
+
let currentBundleId = getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
240
|
+
return BundleMetadata(
|
|
241
|
+
isolationKey: isolationKey,
|
|
242
|
+
stableBundleId: nil,
|
|
243
|
+
stagingBundleId: currentBundleId,
|
|
244
|
+
verificationPending: false
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private func getCurrentVerifiedBundleId(_ metadata: BundleMetadata) -> String? {
|
|
249
|
+
if let stagingBundleId = metadata.stagingBundleId, !metadata.verificationPending {
|
|
250
|
+
return stagingBundleId
|
|
251
|
+
}
|
|
252
|
+
return metadata.stableBundleId
|
|
253
|
+
}
|
|
254
|
+
|
|
203
255
|
/**
|
|
204
256
|
* Checks if isolationKey has changed and cleans up old bundles if needed.
|
|
205
257
|
* This handles migration when isolationKey format changes.
|
|
@@ -289,100 +341,78 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
289
341
|
return metadata.verificationPending && metadata.stagingBundleId != nil
|
|
290
342
|
}
|
|
291
343
|
|
|
292
|
-
private func
|
|
293
|
-
|
|
344
|
+
private func prepareMetadataForNewStagingBundle(_ metadata: BundleMetadata, bundleId: String) -> BundleMetadata {
|
|
345
|
+
let currentVerifiedBundleId = getCurrentVerifiedBundleId(metadata).flatMap { $0 == bundleId ? nil : $0 }
|
|
346
|
+
return BundleMetadata(
|
|
347
|
+
isolationKey: isolationKey,
|
|
348
|
+
stableBundleId: currentVerifiedBundleId,
|
|
349
|
+
stagingBundleId: bundleId,
|
|
350
|
+
verificationPending: true,
|
|
351
|
+
updatedAt: Date().timeIntervalSince1970 * 1000
|
|
352
|
+
)
|
|
294
353
|
}
|
|
295
354
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
355
|
+
@discardableResult
|
|
356
|
+
private func rollbackPendingBundle(_ stagingId: String) -> Bool {
|
|
357
|
+
guard var metadata = loadMetadataOrNull(), metadata.stagingBundleId == stagingId else {
|
|
358
|
+
return false
|
|
299
359
|
}
|
|
300
|
-
metadata.verificationAttemptedAt = Date().timeIntervalSince1970 * 1000
|
|
301
|
-
let _ = saveMetadata(metadata)
|
|
302
|
-
NSLog("[BundleStorage] Marked verification attempted for staging bundle: \(metadata.stagingBundleId ?? "nil")")
|
|
303
|
-
}
|
|
304
360
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
metadata.stagingExecutionCount = (metadata.stagingExecutionCount ?? 0) + 1
|
|
310
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
311
|
-
let _ = saveMetadata(metadata)
|
|
312
|
-
NSLog("[BundleStorage] Incremented staging execution count to: \(metadata.stagingExecutionCount ?? 0)")
|
|
313
|
-
}
|
|
361
|
+
var crashedHistory = loadCrashedHistory()
|
|
362
|
+
crashedHistory.addEntry(stagingId)
|
|
363
|
+
let _ = saveCrashedHistory(crashedHistory)
|
|
314
364
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
365
|
+
let fallbackBundleId = metadata.stableBundleId.flatMap { candidate in
|
|
366
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
367
|
+
let stableBundleDir = (storeDir as NSString).appendingPathComponent(candidate)
|
|
368
|
+
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), bundlePath != nil {
|
|
369
|
+
return candidate
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return nil
|
|
322
373
|
}
|
|
323
374
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
375
|
+
metadata = BundleMetadata(
|
|
376
|
+
isolationKey: isolationKey,
|
|
377
|
+
stableBundleId: nil,
|
|
378
|
+
stagingBundleId: fallbackBundleId,
|
|
379
|
+
verificationPending: false,
|
|
380
|
+
updatedAt: Date().timeIntervalSince1970 * 1000
|
|
381
|
+
)
|
|
331
382
|
|
|
332
|
-
|
|
333
|
-
|
|
383
|
+
guard saveMetadata(metadata) else {
|
|
384
|
+
return false
|
|
385
|
+
}
|
|
334
386
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
387
|
+
if let fallbackBundleId,
|
|
388
|
+
case .success(let storeDir) = bundleStoreDir() {
|
|
389
|
+
let fallbackBundleDir = (storeDir as NSString).appendingPathComponent(fallbackBundleId)
|
|
390
|
+
if case .success(let bundlePath) = findBundleFile(in: fallbackBundleDir), let bundlePath {
|
|
391
|
+
let _ = setBundleURL(localPath: bundlePath)
|
|
338
392
|
}
|
|
393
|
+
} else {
|
|
394
|
+
let _ = setBundleURL(localPath: nil)
|
|
339
395
|
}
|
|
340
|
-
}
|
|
341
396
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
guard let stagingId = metadata.stagingBundleId else {
|
|
347
|
-
NSLog("[BundleStorage] No staging bundle to rollback from")
|
|
348
|
-
return
|
|
397
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
398
|
+
let stagingDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
399
|
+
try? fileSystem.removeItem(atPath: stagingDir)
|
|
349
400
|
}
|
|
350
401
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let _ = saveCrashedHistory(crashedHistory)
|
|
355
|
-
NSLog("[BundleStorage('\(id)')] Added bundle '\(stagingId)' to crashed history")
|
|
356
|
-
|
|
357
|
-
// Save rollback info to session variable (memory only)
|
|
358
|
-
self.sessionRollbackBundleId = stagingId
|
|
359
|
-
|
|
360
|
-
// Clear staging
|
|
361
|
-
metadata.stagingBundleId = nil
|
|
362
|
-
metadata.verificationPending = false
|
|
363
|
-
metadata.verificationAttemptedAt = nil
|
|
364
|
-
metadata.stagingExecutionCount = nil
|
|
365
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
366
|
-
|
|
367
|
-
if saveMetadata(metadata) {
|
|
368
|
-
NSLog("[BundleStorage] Rolled back to stable bundle: \(metadata.stableBundleId ?? "fallback")")
|
|
369
|
-
|
|
370
|
-
// Update HotUpdaterBundleURL to point to stable bundle
|
|
371
|
-
if let stableId = metadata.stableBundleId {
|
|
372
|
-
if case .success(let storeDir) = bundleStoreDir() {
|
|
373
|
-
let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
|
|
374
|
-
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), let path = bundlePath {
|
|
375
|
-
let _ = setBundleURL(localPath: path)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
// Reset to fallback
|
|
380
|
-
let _ = setBundleURL(localPath: nil)
|
|
381
|
-
}
|
|
402
|
+
saveLaunchReport(LaunchReport(status: "RECOVERED", crashedBundleId: stagingId))
|
|
403
|
+
return true
|
|
404
|
+
}
|
|
382
405
|
|
|
383
|
-
|
|
384
|
-
|
|
406
|
+
private func applyPendingRecoveryIfNeeded(_ pendingRecovery: PendingCrashRecovery?) {
|
|
407
|
+
guard let metadata = loadMetadataOrNull(),
|
|
408
|
+
let stagingBundleId = metadata.stagingBundleId,
|
|
409
|
+
pendingRecovery?.shouldRollback == true,
|
|
410
|
+
pendingRecovery?.launchedBundleId == stagingBundleId,
|
|
411
|
+
isVerificationPending(metadata) else {
|
|
412
|
+
return
|
|
385
413
|
}
|
|
414
|
+
|
|
415
|
+
_ = rollbackPendingBundle(stagingBundleId)
|
|
386
416
|
}
|
|
387
417
|
|
|
388
418
|
// MARK: - Directory Management
|
|
@@ -599,66 +629,55 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
599
629
|
func getFallbackBundleURL(bundle: Bundle) -> URL? {
|
|
600
630
|
return bundle.url(forResource: "main", withExtension: "jsbundle")
|
|
601
631
|
}
|
|
602
|
-
|
|
603
|
-
public func getBundleURL(bundle: Bundle) -> URL? {
|
|
604
|
-
// Try to load metadata
|
|
605
|
-
let metadata = loadMetadataOrNull()
|
|
606
|
-
|
|
607
|
-
// If no metadata exists, use legacy behavior (backwards compatible)
|
|
608
|
-
guard let metadata = metadata else {
|
|
609
|
-
let cached = getCachedBundleURL()
|
|
610
|
-
return cached ?? getFallbackBundleURL(bundle: bundle)
|
|
611
|
-
}
|
|
612
632
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
incrementStagingExecutionCount()
|
|
621
|
-
// Don't mark verificationAttempted yet!
|
|
622
|
-
} else if wasVerificationAttempted(metadata) {
|
|
623
|
-
// Already executed once and verificationAttempted is set → crash!
|
|
624
|
-
NSLog("[BundleStorage] Crash detected: staging bundle executed but didn't call notifyAppReady")
|
|
625
|
-
rollbackToStable()
|
|
626
|
-
} else {
|
|
627
|
-
// Second execution - now mark verification attempted
|
|
628
|
-
NSLog("[BundleStorage] Second execution of staging bundle, marking verification attempted")
|
|
629
|
-
markVerificationAttempted()
|
|
630
|
-
}
|
|
633
|
+
private func selectLaunch(bundle: Bundle) -> LaunchSelection {
|
|
634
|
+
guard let metadata = loadMetadataOrNull() else {
|
|
635
|
+
return LaunchSelection(
|
|
636
|
+
bundleURL: getCachedBundleURL() ?? getFallbackBundleURL(bundle: bundle),
|
|
637
|
+
launchedBundleId: getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent,
|
|
638
|
+
shouldRollbackOnCrash: false
|
|
639
|
+
)
|
|
631
640
|
}
|
|
632
641
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
642
|
+
if let stagingId = metadata.stagingBundleId,
|
|
643
|
+
case .success(let storeDir) = bundleStoreDir() {
|
|
644
|
+
let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
645
|
+
if case .success(let bundlePath) = findBundleFile(in: stagingBundleDir), let bundlePath {
|
|
646
|
+
return LaunchSelection(
|
|
647
|
+
bundleURL: URL(fileURLWithPath: bundlePath),
|
|
648
|
+
launchedBundleId: stagingId,
|
|
649
|
+
shouldRollbackOnCrash: metadata.verificationPending
|
|
650
|
+
)
|
|
651
|
+
}
|
|
637
652
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if case .success(let storeDir) = bundleStoreDir() {
|
|
641
|
-
let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
642
|
-
if case .success(let bundlePath) = findBundleFile(in: stagingBundleDir), let path = bundlePath {
|
|
643
|
-
NSLog("[BundleStorage] Returning staging bundle URL: \(path)")
|
|
644
|
-
return URL(fileURLWithPath: path)
|
|
645
|
-
}
|
|
653
|
+
if metadata.verificationPending, rollbackPendingBundle(stagingId) {
|
|
654
|
+
return selectLaunch(bundle: bundle)
|
|
646
655
|
}
|
|
647
656
|
}
|
|
648
657
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
658
|
+
if let stableId = metadata.stableBundleId,
|
|
659
|
+
case .success(let storeDir) = bundleStoreDir() {
|
|
660
|
+
let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
|
|
661
|
+
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), let bundlePath {
|
|
662
|
+
return LaunchSelection(
|
|
663
|
+
bundleURL: URL(fileURLWithPath: bundlePath),
|
|
664
|
+
launchedBundleId: stableId,
|
|
665
|
+
shouldRollbackOnCrash: false
|
|
666
|
+
)
|
|
657
667
|
}
|
|
658
668
|
}
|
|
659
669
|
|
|
660
|
-
|
|
661
|
-
|
|
670
|
+
return LaunchSelection(
|
|
671
|
+
bundleURL: getFallbackBundleURL(bundle: bundle),
|
|
672
|
+
launchedBundleId: getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent,
|
|
673
|
+
shouldRollbackOnCrash: false
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
|
|
678
|
+
saveLaunchReport(nil)
|
|
679
|
+
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
680
|
+
return selectLaunch(bundle: bundle)
|
|
662
681
|
}
|
|
663
682
|
|
|
664
683
|
// MARK: - Bundle Update
|
|
@@ -691,6 +710,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
691
710
|
let setResult = self.setBundleURL(localPath: nil)
|
|
692
711
|
switch setResult {
|
|
693
712
|
case .success:
|
|
713
|
+
let _ = self.saveMetadata(self.createInitialMetadata())
|
|
714
|
+
self.saveLaunchReport(nil)
|
|
694
715
|
let cleanupResult = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
|
|
695
716
|
switch cleanupResult {
|
|
696
717
|
case .success:
|
|
@@ -727,19 +748,13 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
727
748
|
let setResult = self.setBundleURL(localPath: bundlePath)
|
|
728
749
|
switch setResult {
|
|
729
750
|
case .success:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
metadata.verificationPending = true
|
|
734
|
-
metadata.verificationAttemptedAt = nil
|
|
735
|
-
metadata.stagingExecutionCount = 0
|
|
736
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
737
|
-
let _ = self.saveMetadata(metadata)
|
|
751
|
+
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
752
|
+
let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
|
|
753
|
+
let _ = self.saveMetadata(updatedMetadata)
|
|
738
754
|
NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
|
|
739
755
|
|
|
740
|
-
// Clean up old bundles, preserving
|
|
741
|
-
let
|
|
742
|
-
let bundleIdsToKeep = [stableId, bundleId].compactMap { $0 }
|
|
756
|
+
// Clean up old bundles, preserving the fallback and new staging bundle.
|
|
757
|
+
let bundleIdsToKeep = [updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
743
758
|
if bundleIdsToKeep.count > 0 {
|
|
744
759
|
let _ = self.cleanupOldBundles(currentBundleId: bundleIdsToKeep.first, bundleId: bundleIdsToKeep.count > 1 ? bundleIdsToKeep[1] : nil)
|
|
745
760
|
}
|
|
@@ -1045,21 +1060,16 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1045
1060
|
NSLog("[BundleStorage] Successfully set bundle URL: \(finalBundlePath)")
|
|
1046
1061
|
|
|
1047
1062
|
// 13) Set staging metadata for rollback support
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
metadata.verificationAttemptedAt = nil
|
|
1052
|
-
metadata.stagingExecutionCount = 0
|
|
1053
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1054
|
-
let _ = self.saveMetadata(metadata)
|
|
1063
|
+
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
1064
|
+
let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
|
|
1065
|
+
let _ = self.saveMetadata(updatedMetadata)
|
|
1055
1066
|
NSLog("[BundleStorage] Set staging bundle: \(bundleId), verificationPending: true")
|
|
1056
1067
|
|
|
1057
1068
|
// 14) Clean up the temporary directory
|
|
1058
1069
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
1059
1070
|
|
|
1060
|
-
// 15) Clean up old bundles, preserving
|
|
1061
|
-
let
|
|
1062
|
-
let bundleIdsToKeep = [stableId, bundleId].compactMap { $0 }
|
|
1071
|
+
// 15) Clean up old bundles, preserving the fallback and new staging bundle.
|
|
1072
|
+
let bundleIdsToKeep = [updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
1063
1073
|
if bundleIdsToKeep.count > 0 {
|
|
1064
1074
|
let _ = self.cleanupOldBundles(currentBundleId: bundleIdsToKeep.first, bundleId: bundleIdsToKeep.count > 1 ? bundleIdsToKeep[1] : nil)
|
|
1065
1075
|
}
|
|
@@ -1110,57 +1120,31 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1110
1120
|
// MARK: - Rollback Support
|
|
1111
1121
|
|
|
1112
1122
|
/**
|
|
1113
|
-
*
|
|
1114
|
-
* If the bundle matches the staging bundle, promotes it to stable.
|
|
1115
|
-
* @param bundleId The ID of the currently running bundle
|
|
1116
|
-
* @return true if promotion was successful or no action was needed
|
|
1123
|
+
* Marks the current launch as successful after the first content appeared.
|
|
1117
1124
|
*/
|
|
1118
|
-
func
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
return
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Check if there was a recent rollback (session variable)
|
|
1127
|
-
if let crashedBundleId = self.sessionRollbackBundleId {
|
|
1128
|
-
NSLog("[BundleStorage] notifyAppReady: Detected rollback recovery from '\(crashedBundleId)'")
|
|
1129
|
-
|
|
1130
|
-
// Clear rollback info (one-time read)
|
|
1131
|
-
self.sessionRollbackBundleId = nil
|
|
1132
|
-
|
|
1133
|
-
return [
|
|
1134
|
-
"status": "RECOVERED",
|
|
1135
|
-
"crashedBundleId": crashedBundleId
|
|
1136
|
-
]
|
|
1125
|
+
func markLaunchCompleted(bundleId: String?) {
|
|
1126
|
+
guard let bundleId,
|
|
1127
|
+
var metadata = loadMetadataOrNull(),
|
|
1128
|
+
metadata.verificationPending,
|
|
1129
|
+
metadata.stagingBundleId == bundleId else {
|
|
1130
|
+
return
|
|
1137
1131
|
}
|
|
1138
1132
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
return ["status": "PROMOTED"]
|
|
1144
|
-
}
|
|
1133
|
+
metadata.verificationPending = false
|
|
1134
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1135
|
+
let _ = saveMetadata(metadata)
|
|
1136
|
+
}
|
|
1145
1137
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
// Already stable, clear any pending verification state
|
|
1149
|
-
if metadata.verificationPending {
|
|
1150
|
-
metadata.verificationPending = false
|
|
1151
|
-
metadata.verificationAttemptedAt = nil
|
|
1152
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1153
|
-
let _ = saveMetadata(metadata)
|
|
1154
|
-
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' is stable, cleared pending verification")
|
|
1155
|
-
} else {
|
|
1156
|
-
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' is already stable")
|
|
1157
|
-
}
|
|
1138
|
+
func notifyAppReady() -> [String: Any] {
|
|
1139
|
+
guard let report = loadLaunchReport() else {
|
|
1158
1140
|
return ["status": "STABLE"]
|
|
1159
1141
|
}
|
|
1160
1142
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1143
|
+
var result: [String: Any] = ["status": report.status]
|
|
1144
|
+
if let crashedBundleId = report.crashedBundleId {
|
|
1145
|
+
result["crashedBundleId"] = crashedBundleId
|
|
1146
|
+
}
|
|
1147
|
+
return result
|
|
1164
1148
|
}
|
|
1165
1149
|
|
|
1166
1150
|
/**
|
|
@@ -1190,8 +1174,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1190
1174
|
let metadata = loadMetadataOrNull()
|
|
1191
1175
|
let activeBundleId: String?
|
|
1192
1176
|
|
|
1193
|
-
// Prefer staging bundle
|
|
1194
|
-
if let
|
|
1177
|
+
// Prefer the current staging bundle regardless of verification state.
|
|
1178
|
+
if let staging = metadata?.stagingBundleId {
|
|
1195
1179
|
activeBundleId = staging
|
|
1196
1180
|
} else if let stable = metadata?.stableBundleId {
|
|
1197
1181
|
activeBundleId = stable
|
|
@@ -1240,22 +1224,24 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1240
1224
|
isolationKey: isolationKey,
|
|
1241
1225
|
stableBundleId: nil,
|
|
1242
1226
|
stagingBundleId: nil,
|
|
1243
|
-
verificationPending: false
|
|
1244
|
-
verificationAttemptedAt: nil,
|
|
1245
|
-
stagingExecutionCount: nil
|
|
1227
|
+
verificationPending: false
|
|
1246
1228
|
)
|
|
1247
1229
|
|
|
1248
1230
|
guard saveMetadata(clearedMetadata) else {
|
|
1249
1231
|
return .failure(BundleStorageError.unknown(nil))
|
|
1250
1232
|
}
|
|
1251
1233
|
|
|
1234
|
+
saveLaunchReport(nil)
|
|
1235
|
+
|
|
1252
1236
|
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
1253
1237
|
return .failure(BundleStorageError.unknown(nil))
|
|
1254
1238
|
}
|
|
1255
1239
|
|
|
1256
1240
|
do {
|
|
1257
1241
|
for item in try fileSystem.contentsOfDirectory(atPath: storeDir) {
|
|
1258
|
-
if item == BundleMetadata.metadataFilename ||
|
|
1242
|
+
if item == BundleMetadata.metadataFilename ||
|
|
1243
|
+
item == CrashedHistory.crashedHistoryFilename ||
|
|
1244
|
+
item == LaunchReport.launchReportFilename {
|
|
1259
1245
|
continue
|
|
1260
1246
|
}
|
|
1261
1247
|
|
|
@@ -12,8 +12,6 @@ public struct BundleMetadata: Codable {
|
|
|
12
12
|
var stableBundleId: String?
|
|
13
13
|
var stagingBundleId: String?
|
|
14
14
|
var verificationPending: Bool
|
|
15
|
-
var verificationAttemptedAt: Double?
|
|
16
|
-
var stagingExecutionCount: Int?
|
|
17
15
|
var updatedAt: Double
|
|
18
16
|
|
|
19
17
|
enum CodingKeys: String, CodingKey {
|
|
@@ -22,8 +20,6 @@ public struct BundleMetadata: Codable {
|
|
|
22
20
|
case stableBundleId = "stable_bundle_id"
|
|
23
21
|
case stagingBundleId = "staging_bundle_id"
|
|
24
22
|
case verificationPending = "verification_pending"
|
|
25
|
-
case verificationAttemptedAt = "verification_attempted_at"
|
|
26
|
-
case stagingExecutionCount = "staging_execution_count"
|
|
27
23
|
case updatedAt = "updated_at"
|
|
28
24
|
}
|
|
29
25
|
|
|
@@ -33,8 +29,6 @@ public struct BundleMetadata: Codable {
|
|
|
33
29
|
stableBundleId: String? = nil,
|
|
34
30
|
stagingBundleId: String? = nil,
|
|
35
31
|
verificationPending: Bool = false,
|
|
36
|
-
verificationAttemptedAt: Double? = nil,
|
|
37
|
-
stagingExecutionCount: Int? = nil,
|
|
38
32
|
updatedAt: Double = Date().timeIntervalSince1970 * 1000
|
|
39
33
|
) {
|
|
40
34
|
self.schema = schema
|
|
@@ -42,8 +36,6 @@ public struct BundleMetadata: Codable {
|
|
|
42
36
|
self.stableBundleId = stableBundleId
|
|
43
37
|
self.stagingBundleId = stagingBundleId
|
|
44
38
|
self.verificationPending = verificationPending
|
|
45
|
-
self.verificationAttemptedAt = verificationAttemptedAt
|
|
46
|
-
self.stagingExecutionCount = stagingExecutionCount
|
|
47
39
|
self.updatedAt = updatedAt
|
|
48
40
|
}
|
|
49
41
|
|
|
@@ -191,3 +183,64 @@ public struct CrashedHistory: Codable {
|
|
|
191
183
|
bundles.removeAll()
|
|
192
184
|
}
|
|
193
185
|
}
|
|
186
|
+
|
|
187
|
+
public struct PendingCrashRecovery {
|
|
188
|
+
let launchedBundleId: String?
|
|
189
|
+
let shouldRollback: Bool
|
|
190
|
+
|
|
191
|
+
static func from(json: [String: Any]) -> PendingCrashRecovery {
|
|
192
|
+
PendingCrashRecovery(
|
|
193
|
+
launchedBundleId: (json["bundleId"] as? String).flatMap { $0.isEmpty ? nil : $0 },
|
|
194
|
+
shouldRollback: json["shouldRollback"] as? Bool ?? false
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public struct LaunchSelection {
|
|
200
|
+
let bundleURL: URL?
|
|
201
|
+
let launchedBundleId: String?
|
|
202
|
+
let shouldRollbackOnCrash: Bool
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public struct LaunchReport: Codable {
|
|
206
|
+
static let launchReportFilename = "launch-report.json"
|
|
207
|
+
|
|
208
|
+
let status: String
|
|
209
|
+
let crashedBundleId: String?
|
|
210
|
+
|
|
211
|
+
init(status: String = "STABLE", crashedBundleId: String? = nil) {
|
|
212
|
+
self.status = status
|
|
213
|
+
self.crashedBundleId = crashedBundleId
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static func load(from file: URL) -> LaunchReport? {
|
|
217
|
+
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
218
|
+
return nil
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
do {
|
|
222
|
+
let data = try Data(contentsOf: file)
|
|
223
|
+
return try JSONDecoder().decode(LaunchReport.self, from: data)
|
|
224
|
+
} catch {
|
|
225
|
+
print("[LaunchReport] Failed to load launch report: \(error)")
|
|
226
|
+
return nil
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func save(to file: URL) -> Bool {
|
|
231
|
+
do {
|
|
232
|
+
let encoder = JSONEncoder()
|
|
233
|
+
encoder.outputFormatting = .prettyPrinted
|
|
234
|
+
let data = try encoder.encode(self)
|
|
235
|
+
let directory = file.deletingLastPathComponent()
|
|
236
|
+
if !FileManager.default.fileExists(atPath: directory.path) {
|
|
237
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
238
|
+
}
|
|
239
|
+
try data.write(to: file)
|
|
240
|
+
return true
|
|
241
|
+
} catch {
|
|
242
|
+
print("[LaunchReport] Failed to save launch report: \(error)")
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|