Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 68 additions & 9 deletions ios/DownloadManagerModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class DownloadManagerModule: RCTEventEmitter {
var bytesDownloaded: Int64
var status: String // pending, running, paused, completed, failed
var startedAt: Double
// v3 fields — mirrors Android WorkManager columns so JS hydration works on both platforms
var modelKey: String?
var modelType: String
var combinedTotalBytes: Int64
var metadataJson: String?
// Single-file download
var task: URLSessionDownloadTask?
var taskIdentifier: Int?
Expand Down Expand Up @@ -87,6 +92,10 @@ class DownloadManagerModule: RCTEventEmitter {
let bytesDownloaded: Int64
let status: String
let startedAt: Double
let modelKey: String?
let modelType: String
let combinedTotalBytes: Int64
let metadataJson: String?
let taskIdentifier: Int?
let localUri: String?
let multiFileDestDir: String?
Expand All @@ -95,7 +104,8 @@ class DownloadManagerModule: RCTEventEmitter {

enum CodingKeys: String, CodingKey {
case downloadId, fileName, modelId, totalBytes, bytesDownloaded, status,
startedAt, taskIdentifier, localUri, multiFileDestDir, isMultiFile, fileTasks
startedAt, modelKey, modelType, combinedTotalBytes, metadataJson,
taskIdentifier, localUri, multiFileDestDir, isMultiFile, fileTasks
}

init(
Expand All @@ -106,6 +116,10 @@ class DownloadManagerModule: RCTEventEmitter {
bytesDownloaded: Int64,
status: String,
startedAt: Double,
modelKey: String?,
modelType: String,
combinedTotalBytes: Int64,
metadataJson: String?,
taskIdentifier: Int?,
localUri: String?,
multiFileDestDir: String?,
Expand All @@ -119,6 +133,10 @@ class DownloadManagerModule: RCTEventEmitter {
self.bytesDownloaded = bytesDownloaded
self.status = status
self.startedAt = startedAt
self.modelKey = modelKey
self.modelType = modelType
self.combinedTotalBytes = combinedTotalBytes
self.metadataJson = metadataJson
self.taskIdentifier = taskIdentifier
self.localUri = localUri
self.multiFileDestDir = multiFileDestDir
Expand All @@ -135,6 +153,11 @@ class DownloadManagerModule: RCTEventEmitter {
bytesDownloaded = try container.decode(Int64.self, forKey: .bytesDownloaded)
status = try container.decode(String.self, forKey: .status)
startedAt = try container.decode(Double.self, forKey: .startedAt)
// Decode with defaults so old persisted state (pre-v3) deserialises without crashing
modelKey = try container.decodeIfPresent(String.self, forKey: .modelKey)
modelType = (try container.decodeIfPresent(String.self, forKey: .modelType)) ?? "text"
combinedTotalBytes = (try container.decodeIfPresent(Int64.self, forKey: .combinedTotalBytes)) ?? 0
metadataJson = try container.decodeIfPresent(String.self, forKey: .metadataJson)
taskIdentifier = try container.decodeIfPresent(Int.self, forKey: .taskIdentifier)
localUri = try container.decodeIfPresent(String.self, forKey: .localUri)
multiFileDestDir = try container.decodeIfPresent(String.self, forKey: .multiFileDestDir)
Expand Down Expand Up @@ -337,6 +360,10 @@ class DownloadManagerModule: RCTEventEmitter {
bytesDownloaded: info.bytesDownloaded,
status: info.status,
startedAt: info.startedAt,
modelKey: info.modelKey,
modelType: info.modelType,
combinedTotalBytes: info.combinedTotalBytes,
metadataJson: info.metadataJson,
taskIdentifier: info.taskIdentifier ?? info.task?.taskIdentifier,
localUri: info.localUri,
multiFileDestDir: info.multiFileDestDir,
Expand Down Expand Up @@ -368,6 +395,10 @@ class DownloadManagerModule: RCTEventEmitter {
bytesDownloaded: persisted.bytesDownloaded,
status: persisted.status,
startedAt: persisted.startedAt,
modelKey: persisted.modelKey,
modelType: persisted.modelType,
combinedTotalBytes: persisted.combinedTotalBytes,
metadataJson: persisted.metadataJson,
task: nil,
taskIdentifier: persisted.taskIdentifier,
localUri: persisted.localUri,
Expand Down Expand Up @@ -464,6 +495,10 @@ class DownloadManagerModule: RCTEventEmitter {
bytesDownloaded: 0,
status: self.statusString(from: downloadTask.state),
startedAt: Date().timeIntervalSince1970 * 1000,
modelKey: nil,
modelType: "text",
combinedTotalBytes: 0,
metadataJson: nil,
Comment on lines +498 to +501

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

These new fields are being initialized with hardcoded defaults during task restoration. If the app's persisted state in UserDefaults is lost or out of sync, but background URLSessionTasks are still active, the restored DownloadInfo will lose its original modelKey, modelType, combinedTotalBytes, and metadataJson. To ensure robustness and avoid swallowing the state-loss issue with defaults, these fields should be added to the TaskDescription struct so they can be recovered directly from the URLSessionDownloadTask.taskDescription property.

References
  1. Instead of swallowing errors by only logging them, functions that can fail should propagate the result (e.g., by returning a success/failure status or throwing an error) to allow callers to handle it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. Adding these fields to TaskDescription is the more robust solution for the edge case where URLSession tasks survive but UserDefaults is wiped. In practice these always travel together on iOS — if UserDefaults is cleared the session tasks are invalidated too. We've accepted this as a known limitation given the added complexity of expanding TaskDescription encoding. The decodeIfPresent fallbacks protect the common upgrade path.

task: nil,
taskIdentifier: nil,
localUri: nil,
Expand Down Expand Up @@ -519,11 +554,18 @@ extension DownloadManagerModule {
}

let totalBytes = (params["totalBytes"] as? NSNumber)?.int64Value ?? 0
let downloadId = String(nextDownloadId)
nextDownloadId += 1
let modelKey = params["modelKey"] as? String
let modelType = (params["modelType"] as? String) ?? "text"
let combinedTotalBytes = (params["combinedTotalBytes"] as? NSNumber)?.int64Value ?? 0
let metadataJson = params["metadataJson"] as? String
let downloadId = queue.sync(flags: .barrier) { () -> String in
let id = String(nextDownloadId)
nextDownloadId += 1
return id
}

NSLog("[DownloadManager] Starting download #%@: url=%@, fileName=%@, modelId=%@, totalBytes=%lld",
downloadId, urlString, fileName, modelId, totalBytes)
NSLog("[DownloadManager] Starting download #%@: url=%@, fileName=%@, modelId=%@, totalBytes=%lld, modelType=%@",
downloadId, urlString, fileName, modelId, totalBytes, modelType)

let task = session.downloadTask(with: url)
task.taskDescription = encodeTaskDescription(TaskDescription(
Expand All @@ -546,6 +588,10 @@ extension DownloadManagerModule {
bytesDownloaded: 0,
status: "running",
startedAt: Date().timeIntervalSince1970 * 1000,
modelKey: modelKey,
modelType: modelType,
combinedTotalBytes: combinedTotalBytes,
metadataJson: metadataJson,
task: task,
taskIdentifier: task.taskIdentifier,
localUri: nil,
Expand Down Expand Up @@ -588,8 +634,11 @@ extension DownloadManagerModule {
}

let totalBytes = (params["totalBytes"] as? NSNumber)?.int64Value ?? 0
let downloadId = String(nextDownloadId)
nextDownloadId += 1
let downloadId = queue.sync(flags: .barrier) { () -> String in
let id = String(nextDownloadId)
nextDownloadId += 1
return id
}

NSLog("[DownloadManager] Starting multi-file download #%@: %d files, totalBytes=%lld, dest=%@",
downloadId, filesArray.count, totalBytes, destinationDir)
Expand Down Expand Up @@ -647,6 +696,10 @@ extension DownloadManagerModule {
bytesDownloaded: 0,
status: "running",
startedAt: Date().timeIntervalSince1970 * 1000,
modelKey: params["modelKey"] as? String,
modelType: (params["modelType"] as? String) ?? "text",
combinedTotalBytes: (params["combinedTotalBytes"] as? NSNumber)?.int64Value ?? 0,
metadataJson: params["metadataJson"] as? String,
task: nil,
taskIdentifier: nil,
localUri: nil,
Expand Down Expand Up @@ -714,15 +767,21 @@ extension DownloadManagerModule {
let result = downloads.values.map { info -> [String: Any] in
NSLog("[DownloadManager] -> #%@: %@ status=%@ bytes=%lld/%lld",
info.downloadId, info.fileName, info.status, info.bytesDownloaded, info.totalBytes)
return [
var entry: [String: Any] = [
"downloadId": info.downloadId,
"fileName": info.fileName,
"modelId": info.modelId,
"status": info.status,
"bytesDownloaded": NSNumber(value: info.bytesDownloaded),
"totalBytes": NSNumber(value: info.totalBytes),
"startedAt": NSNumber(value: info.startedAt)
"startedAt": NSNumber(value: info.startedAt),
"modelType": info.modelType,
"combinedTotalBytes": NSNumber(value: info.combinedTotalBytes),
"createdAt": NSNumber(value: info.startedAt)
]
if let modelKey = info.modelKey { entry["modelKey"] = modelKey }
if let metadataJson = info.metadataJson { entry["metadataJson"] = metadataJson }
return entry
}
resolve(result)
}
Expand Down
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PODS:
- hermes-engine (0.14.0):
- hermes-engine/Pre-built (= 0.14.0)
- hermes-engine/Pre-built (0.14.0)
- llama-rn (0.12.0-rc.5):
- llama-rn (0.12.0-rc.9):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -3604,7 +3604,7 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 8c6be38f94b3bf8b864981980e64e55f08e467ec
llama-rn: 3ae5a64b3d08ff41f9e62b214ba5004e475b9561
llama-rn: 796fa53f37f89e2c77cd6c462ad1172ee96d4c80
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: 691b8363e8c591fb78a78254ff2517258891456b
op-sqlite: bafff369cecaee4fe65c89eec47deaba26f2db95
Expand Down
2 changes: 1 addition & 1 deletion src/services/backgroundDownloadService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class BackgroundDownloadService {
}
const downloads = await DownloadManagerModule.getActiveDownloads();
return downloads.map((d: any) => ({
downloadId: d.id,
downloadId: d.downloadId ?? d.id,
fileName: d.fileName,
modelId: d.modelId,
status: d.status as BackgroundDownloadStatus,
Expand Down
Loading