@aguacerowx/react-native 0.0.51 → 0.0.52

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.
Files changed (41) hide show
  1. package/android/src/main/java/com/aguacerowx/reactnative/SatelliteLayerView.java +11 -3
  2. package/android/src/main/java/com/aguacerowx/reactnative/WeatherFrameProcessorModule.java +315 -311
  3. package/ios/SatelliteLayerView.swift +11 -4
  4. package/ios/WeatherFrameProcessorModule.swift +222 -219
  5. package/lib/commonjs/WeatherLayerManager.js +61 -45
  6. package/lib/commonjs/WeatherLayerManager.js.map +1 -1
  7. package/lib/commonjs/aguaceroRnDebug.js +8 -1
  8. package/lib/commonjs/aguaceroRnDebug.js.map +1 -1
  9. package/lib/commonjs/gridCdnAuth.js +64 -0
  10. package/lib/commonjs/gridCdnAuth.js.map +1 -0
  11. package/lib/commonjs/index.js +13 -0
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/nexrad/nexradAndroidController.js +25 -25
  14. package/lib/commonjs/nexrad/nexradAndroidController.js.map +1 -1
  15. package/lib/commonjs/nexrad/nexradDiag.js +24 -24
  16. package/lib/commonjs/satellite/satelliteAndroidController.js +15 -15
  17. package/lib/module/WeatherLayerManager.js +61 -45
  18. package/lib/module/WeatherLayerManager.js.map +1 -1
  19. package/lib/module/aguaceroRnDebug.js +8 -1
  20. package/lib/module/aguaceroRnDebug.js.map +1 -1
  21. package/lib/module/gridCdnAuth.js +56 -0
  22. package/lib/module/gridCdnAuth.js.map +1 -0
  23. package/lib/module/index.js +1 -0
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/nexrad/nexradAndroidController.js +25 -25
  26. package/lib/module/nexrad/nexradAndroidController.js.map +1 -1
  27. package/lib/module/nexrad/nexradDiag.js +24 -24
  28. package/lib/module/satellite/satelliteAndroidController.js +15 -15
  29. package/lib/typescript/WeatherLayerManager.d.ts.map +1 -1
  30. package/lib/typescript/aguaceroRnDebug.d.ts.map +1 -1
  31. package/lib/typescript/gridCdnAuth.d.ts +24 -0
  32. package/lib/typescript/gridCdnAuth.d.ts.map +1 -0
  33. package/lib/typescript/index.d.ts +1 -0
  34. package/package.json +1 -1
  35. package/src/WeatherLayerManager.js +2024 -2004
  36. package/src/aguaceroRnDebug.js +8 -1
  37. package/src/gridCdnAuth.js +56 -0
  38. package/src/index.js +19 -15
  39. package/src/nexrad/nexradAndroidController.js +1078 -1078
  40. package/src/nexrad/nexradDiag.js +150 -150
  41. package/src/satellite/satelliteAndroidController.js +245 -245
@@ -381,10 +381,17 @@ public final class SatelliteLayerView: UIView {
381
381
  if !bundleId.isEmpty {
382
382
  req.setValue(bundleId, forHTTPHeaderField: "x-app-identifier")
383
383
  }
384
- // Match WeatherFrameProcessorModule + AguaceroCore grid fetch (CloudFront allowlists often require these on RN).
385
- if !gridRequestSiteOrigin.isEmpty {
386
- req.setValue(gridRequestSiteOrigin, forHTTPHeaderField: "Origin")
387
- req.setValue("\(gridRequestSiteOrigin)/", forHTTPHeaderField: "Referer")
384
+ // Match WeatherFrameProcessorModule + AguaceroCore grid fetch (CloudFront returns 403 without Origin on RN).
385
+ var gridOrigin = gridRequestSiteOrigin.trimmingCharacters(in: .whitespacesAndNewlines)
386
+ if gridOrigin.isEmpty {
387
+ gridOrigin = "https://localhost"
388
+ }
389
+ while gridOrigin.hasSuffix("/") {
390
+ gridOrigin.removeLast()
391
+ }
392
+ if !gridOrigin.isEmpty {
393
+ req.setValue(gridOrigin, forHTTPHeaderField: "Origin")
394
+ req.setValue("\(gridOrigin)/", forHTTPHeaderField: "Referer")
388
395
  }
389
396
 
390
397
  let sem = DispatchSemaphore(value: 0)
@@ -1,220 +1,223 @@
1
- import Foundation
2
- import React
3
-
4
- @objc(WeatherFrameProcessorModule)
5
- class WeatherFrameProcessorModule: NSObject {
6
-
7
- private var currentRunToken = 0
8
- private let session = URLSession(configuration: .default)
9
-
10
- // MARK: - Performance Helper
11
- private func getMemoryUsage() -> String {
12
- var taskInfo = mach_task_basic_info()
13
- var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
14
- let kerr: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
15
- $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
16
- task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
17
- }
18
- }
19
- if kerr == KERN_SUCCESS {
20
- let usedMB = Float(taskInfo.resident_size) / 1024.0 / 1024.0
21
- return String(format: "%.2f MB", usedMB)
22
- }
23
- return "Unknown"
24
- }
25
-
26
- @objc(cancelAllFrames)
27
- func cancelAllFrames() {
28
- currentRunToken += 1
29
- print("🔍 [WeatherFrameProcessor] cancelAllFrames → new runToken=\(currentRunToken) (in-flight fetches will reject with E_CANCELLED). RAM: \(getMemoryUsage())")
30
- }
31
-
32
- @objc(processFrame:resolver:rejecter:)
33
- func processFrame(options: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
34
- let taskToken = self.currentRunToken
35
- let startTime = CFAbsoluteTimeGetCurrent()
36
-
37
- guard let urlString = options["url"] as? String, let url = URL(string: urlString) else {
38
- print("🔍 [WeatherFrameProcessor] processFrame REJECT invalid url options=\(options.keys.sorted())")
39
- reject("INVALID_URL", "The provided URL is not valid.", nil)
40
- return
41
- }
42
-
43
- // Extract filename for log identification
44
- let fileId = url.lastPathComponent
45
- print("🔍 [WeatherFrameProcessor] processFrame START fileId=\(fileId) host=\(url.host ?? "?") runToken=\(taskToken) RAM=\(getMemoryUsage())")
46
-
47
- let apiKey = options["apiKey"] as? String ?? ""
48
- let bundleId = options["bundleId"] as? String
49
- let debug = (options["debug"] as? Bool) == true
50
- let gridOriginOpt = options["gridRequestSiteOrigin"] as? String
51
-
52
- var request = URLRequest(url: url)
53
- request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
54
- if let bundleId = bundleId, !bundleId.isEmpty {
55
- request.setValue(bundleId, forHTTPHeaderField: "x-app-identifier")
56
- }
57
- if let site = gridOriginOpt {
58
- var origin = site.trimmingCharacters(in: .whitespacesAndNewlines)
59
- while origin.hasSuffix("/") {
60
- origin.removeLast()
61
- }
62
- if !origin.isEmpty {
63
- request.setValue(origin, forHTTPHeaderField: "Origin")
64
- request.setValue("\(origin)/", forHTTPHeaderField: "Referer")
65
- }
66
- }
67
-
68
- if debug {
69
- let redacted = urlString.replacingOccurrences(
70
- of: #"([?&])apiKey=[^&]*"#,
71
- with: "$1apiKey=(redacted)",
72
- options: .regularExpression
73
- )
74
- let bundleStr = (bundleId?.isEmpty == false) ? bundleId! : "(none)"
75
- let originStr = (gridOriginOpt?.isEmpty == false) ? gridOriginOpt! : "(none)"
76
- print("[AguaceroRN][debug][FrameProcessor] REQUEST url=\(redacted) apiKey.len=\(apiKey.count) bundleId=\(bundleStr) gridOrigin=\(originStr)")
77
- }
78
-
79
- let task = session.dataTask(with: request) { data, response, error in
80
- let fetchDuration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
81
-
82
- // Must reject so JS `await processFrame` does not hang forever (otherwise WeatherLayerManager
83
- // leaves hasPreloadedRef stuck and grid data never loads after cancelAllFrames).
84
- let finishCancelled = {
85
- print("⚡️ [PERF] [WeatherFrameProcessor] CANCELLED: \(fileId) after \(String(format: "%.2f", fetchDuration))ms")
86
- DispatchQueue.main.async {
87
- reject("E_CANCELLED", "Weather frame fetch superseded", nil)
88
- }
89
- }
90
-
91
- if self.currentRunToken != taskToken {
92
- print("🔍 [WeatherFrameProcessor] \(fileId) aborted: runToken mismatch (task=\(taskToken) current=\(self.currentRunToken)) → E_CANCELLED")
93
- finishCancelled()
94
- return
95
- }
96
-
97
- if let error = error {
98
- print("🔍 [WeatherFrameProcessor] \(fileId) NETWORK_ERROR: \(error.localizedDescription)")
99
- reject("NETWORK_ERROR", "Failed to fetch frame data: \(error.localizedDescription)", error)
100
- return
101
- }
102
-
103
- guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
104
- let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
105
- if debug {
106
- let redacted = urlString.replacingOccurrences(
107
- of: #"([?&])apiKey=[^&]*"#,
108
- with: "$1apiKey=(redacted)",
109
- options: .regularExpression
110
- )
111
- var bodySnippet = ""
112
- if let data = data, let text = String(data: data, encoding: .utf8) {
113
- bodySnippet = text.count > 512 ? String(text.prefix(512)) + "…" : text
114
- }
115
- let bundleStr = (bundleId?.isEmpty == false) ? bundleId! : "(none)"
116
- let originStr = (gridOriginOpt?.isEmpty == false) ? gridOriginOpt! : "(none)"
117
- print("[AguaceroRN][debug][FrameProcessor] HTTP_ERROR status=\(statusCode) url=\(redacted) apiKey.len=\(apiKey.count) bundleId=\(bundleStr) gridOrigin=\(originStr) body=\(bodySnippet)")
118
- if statusCode == 403 {
119
- print("[AguaceroRN][debug][FrameProcessor] 403 hint: verify API key, allowlisted bundleId (x-app-identifier), and gridRequestSiteOrigin (Origin/Referer) match your web app.")
120
- }
121
- } else {
122
- print("🔍 [WeatherFrameProcessor] \(fileId) HTTP_ERROR status=\(statusCode)")
123
- }
124
- reject("HTTP_ERROR", "HTTP request failed with status code: \(statusCode)", nil)
125
- return
126
- }
127
-
128
- guard let data = data else {
129
- print("🔍 [WeatherFrameProcessor] \(fileId) NO_DATA")
130
- reject("NO_DATA", "The server returned no data.", nil)
131
- return
132
- }
133
-
134
- print("⚡️ [PERF] [WeatherFrameProcessor] NETWORK DONE: \(fileId) | Time: \(String(format: "%.2f", fetchDuration))ms | Size: \(data.count / 1024)KB | RAM: \(self.getMemoryUsage())")
135
-
136
- do {
137
- // 1. JSON Parsing
138
- let jsonStart = CFAbsoluteTimeGetCurrent()
139
- guard let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
140
- print("🔍 [WeatherFrameProcessor] \(fileId) INVALID_RESPONSE: top-level JSON not object")
141
- reject("INVALID_RESPONSE", "The API response format was invalid JSON.", nil)
142
- return
143
- }
144
- let jsonTime = (CFAbsoluteTimeGetCurrent() - jsonStart) * 1000
145
-
146
- guard let b64CompressedData = jsonResponse["data"] as? String,
147
- let encoding = jsonResponse["encoding"] as? [String: Any],
148
- let scale = encoding["scale"] as? NSNumber,
149
- let offset = encoding["offset"] as? NSNumber,
150
- let missing = encoding["missing_quantized"] as? NSNumber else {
151
- let encKeys = (jsonResponse["encoding"] as? [String: Any])?.keys.sorted() ?? []
152
- print("🔍 [WeatherFrameProcessor] \(fileId) INVALID_RESPONSE: keys=\(Array(jsonResponse.keys).sorted()) encodingKeys=\(encKeys)")
153
- reject("INVALID_RESPONSE", "The API response format was invalid structure.", nil)
154
- return
155
- }
156
-
157
- // 2. Base64 Decoding
158
- let decodeStart = CFAbsoluteTimeGetCurrent()
159
- guard let compressedData = Data(base64Encoded: b64CompressedData) else {
160
- print("🔍 [WeatherFrameProcessor] \(fileId) DECODING_ERROR: base64 decode failed (b64 len=\(b64CompressedData.count))")
161
- reject("DECODING_ERROR", "Failed to decode base64 data string.", nil)
162
- return
163
- }
164
- let decodeTime = (CFAbsoluteTimeGetCurrent() - decodeStart) * 1000
165
-
166
- // 3. Disk Write
167
- let writeStart = CFAbsoluteTimeGetCurrent()
168
- let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
169
- let fileName = "frame_\(urlString.hashValue).zst"
170
- let dataFile = cacheDir.appendingPathComponent(fileName)
171
-
172
- try compressedData.write(to: dataFile)
173
- let writeTime = (CFAbsoluteTimeGetCurrent() - writeStart) * 1000
174
-
175
- var responseMap: [String: Any] = [
176
- "filePath": dataFile.path,
177
- "scale": scale,
178
- "offset": offset,
179
- "missing": missing,
180
- ]
181
-
182
- if let scaleType = encoding["scale_type"] as? String {
183
- responseMap["scaleType"] = scaleType
184
- }
185
-
186
- let totalTime = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
187
-
188
- print("""
189
- ⚡️ [PERF] [WeatherFrameProcessor] FINISHED: \(fileId)
190
- - Total: \(String(format: "%.2f", totalTime))ms
191
- - JSON: \(String(format: "%.2f", jsonTime))ms
192
- - B64 Decode: \(String(format: "%.2f", decodeTime))ms
193
- - Disk Write: \(String(format: "%.2f", writeTime))ms
194
- - RAM End: \(self.getMemoryUsage())
195
- """)
196
-
197
- if self.currentRunToken != taskToken {
198
- print("🔍 [WeatherFrameProcessor] \(fileId) discarded after disk write: runToken mismatch before resolve")
199
- finishCancelled()
200
- return
201
- }
202
- DispatchQueue.main.async {
203
- print("🔍 [WeatherFrameProcessor] \(fileId) RESOLVE ok path=\(dataFile.path)")
204
- resolve(responseMap)
205
- }
206
-
207
- } catch {
208
- print("🔍 [WeatherFrameProcessor] \(fileId) PROCESSING_ERROR: \(error.localizedDescription)")
209
- reject("PROCESSING_ERROR", "Failed to process the frame data: \(error.localizedDescription)", error)
210
- }
211
- }
212
- print("🔍 [WeatherFrameProcessor] \(fileId) URLSession.dataTask.resume() (runToken=\(taskToken))")
213
- task.resume()
214
- }
215
-
216
- @objc
217
- static func requiresMainQueueSetup() -> Bool {
218
- return false
219
- }
1
+ import Foundation
2
+ import React
3
+
4
+ @objc(WeatherFrameProcessorModule)
5
+ class WeatherFrameProcessorModule: NSObject {
6
+
7
+ private var currentRunToken = 0
8
+ private let session = URLSession(configuration: .default)
9
+
10
+ // MARK: - Performance Helper
11
+ private func getMemoryUsage() -> String {
12
+ var taskInfo = mach_task_basic_info()
13
+ var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
14
+ let kerr: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
15
+ $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
16
+ task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
17
+ }
18
+ }
19
+ if kerr == KERN_SUCCESS {
20
+ let usedMB = Float(taskInfo.resident_size) / 1024.0 / 1024.0
21
+ return String(format: "%.2f MB", usedMB)
22
+ }
23
+ return "Unknown"
24
+ }
25
+
26
+ @objc(cancelAllFrames)
27
+ func cancelAllFrames() {
28
+ currentRunToken += 1
29
+ print("🔍 [WeatherFrameProcessor] cancelAllFrames → new runToken=\(currentRunToken) (in-flight fetches will reject with E_CANCELLED). RAM: \(getMemoryUsage())")
30
+ }
31
+
32
+ @objc(processFrame:resolver:rejecter:)
33
+ func processFrame(options: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
34
+ let taskToken = self.currentRunToken
35
+ let startTime = CFAbsoluteTimeGetCurrent()
36
+
37
+ guard let urlString = options["url"] as? String, let url = URL(string: urlString) else {
38
+ print("🔍 [WeatherFrameProcessor] processFrame REJECT invalid url options=\(options.keys.sorted())")
39
+ reject("INVALID_URL", "The provided URL is not valid.", nil)
40
+ return
41
+ }
42
+
43
+ // Extract filename for log identification
44
+ let fileId = url.lastPathComponent
45
+ print("🔍 [WeatherFrameProcessor] processFrame START fileId=\(fileId) host=\(url.host ?? "?") runToken=\(taskToken) RAM=\(getMemoryUsage())")
46
+
47
+ let apiKey = options["apiKey"] as? String ?? ""
48
+ let bundleId = options["bundleId"] as? String
49
+ let debug = (options["debug"] as? Bool) == true
50
+ let gridOriginOpt = options["gridRequestSiteOrigin"] as? String
51
+ let fallbackGridOrigin = "https://localhost"
52
+
53
+ var request = URLRequest(url: url)
54
+ request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
55
+ if let bundleId = bundleId, !bundleId.isEmpty {
56
+ request.setValue(bundleId, forHTTPHeaderField: "x-app-identifier")
57
+ }
58
+ var originCandidate = (gridOriginOpt ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
59
+ if originCandidate.isEmpty {
60
+ originCandidate = fallbackGridOrigin
61
+ }
62
+ var origin = originCandidate
63
+ while origin.hasSuffix("/") {
64
+ origin.removeLast()
65
+ }
66
+ if !origin.isEmpty {
67
+ request.setValue(origin, forHTTPHeaderField: "Origin")
68
+ request.setValue("\(origin)/", forHTTPHeaderField: "Referer")
69
+ }
70
+
71
+ if debug {
72
+ let redacted = urlString.replacingOccurrences(
73
+ of: #"([?&])apiKey=[^&]*"#,
74
+ with: "$1apiKey=(redacted)",
75
+ options: .regularExpression
76
+ )
77
+ let bundleStr = (bundleId?.isEmpty == false) ? bundleId! : "(none)"
78
+ let originStr = (gridOriginOpt?.isEmpty == false) ? gridOriginOpt! : "(none)"
79
+ print("[AguaceroRN][debug][FrameProcessor] REQUEST url=\(redacted) apiKey.len=\(apiKey.count) bundleId=\(bundleStr) gridOrigin=\(originStr)")
80
+ }
81
+
82
+ let task = session.dataTask(with: request) { data, response, error in
83
+ let fetchDuration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
84
+
85
+ // Must reject so JS `await processFrame` does not hang forever (otherwise WeatherLayerManager
86
+ // leaves hasPreloadedRef stuck and grid data never loads after cancelAllFrames).
87
+ let finishCancelled = {
88
+ print("⚡️ [PERF] [WeatherFrameProcessor] CANCELLED: \(fileId) after \(String(format: "%.2f", fetchDuration))ms")
89
+ DispatchQueue.main.async {
90
+ reject("E_CANCELLED", "Weather frame fetch superseded", nil)
91
+ }
92
+ }
93
+
94
+ if self.currentRunToken != taskToken {
95
+ print("🔍 [WeatherFrameProcessor] \(fileId) aborted: runToken mismatch (task=\(taskToken) current=\(self.currentRunToken)) → E_CANCELLED")
96
+ finishCancelled()
97
+ return
98
+ }
99
+
100
+ if let error = error {
101
+ print("🔍 [WeatherFrameProcessor] \(fileId) NETWORK_ERROR: \(error.localizedDescription)")
102
+ reject("NETWORK_ERROR", "Failed to fetch frame data: \(error.localizedDescription)", error)
103
+ return
104
+ }
105
+
106
+ guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
107
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
108
+ if debug {
109
+ let redacted = urlString.replacingOccurrences(
110
+ of: #"([?&])apiKey=[^&]*"#,
111
+ with: "$1apiKey=(redacted)",
112
+ options: .regularExpression
113
+ )
114
+ var bodySnippet = ""
115
+ if let data = data, let text = String(data: data, encoding: .utf8) {
116
+ bodySnippet = text.count > 512 ? String(text.prefix(512)) + "" : text
117
+ }
118
+ let bundleStr = (bundleId?.isEmpty == false) ? bundleId! : "(none)"
119
+ let originStr = (gridOriginOpt?.isEmpty == false) ? gridOriginOpt! : "(none)"
120
+ print("[AguaceroRN][debug][FrameProcessor] HTTP_ERROR status=\(statusCode) url=\(redacted) apiKey.len=\(apiKey.count) bundleId=\(bundleStr) gridOrigin=\(originStr) body=\(bodySnippet)")
121
+ if statusCode == 403 {
122
+ print("[AguaceroRN][debug][FrameProcessor] 403 hint: verify API key, allowlisted bundleId (x-app-identifier), and gridRequestSiteOrigin (Origin/Referer) match your web app.")
123
+ }
124
+ } else {
125
+ print("🔍 [WeatherFrameProcessor] \(fileId) HTTP_ERROR status=\(statusCode)")
126
+ }
127
+ reject("HTTP_ERROR", "HTTP request failed with status code: \(statusCode)", nil)
128
+ return
129
+ }
130
+
131
+ guard let data = data else {
132
+ print("🔍 [WeatherFrameProcessor] \(fileId) NO_DATA")
133
+ reject("NO_DATA", "The server returned no data.", nil)
134
+ return
135
+ }
136
+
137
+ print("⚡️ [PERF] [WeatherFrameProcessor] NETWORK DONE: \(fileId) | Time: \(String(format: "%.2f", fetchDuration))ms | Size: \(data.count / 1024)KB | RAM: \(self.getMemoryUsage())")
138
+
139
+ do {
140
+ // 1. JSON Parsing
141
+ let jsonStart = CFAbsoluteTimeGetCurrent()
142
+ guard let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
143
+ print("🔍 [WeatherFrameProcessor] \(fileId) INVALID_RESPONSE: top-level JSON not object")
144
+ reject("INVALID_RESPONSE", "The API response format was invalid JSON.", nil)
145
+ return
146
+ }
147
+ let jsonTime = (CFAbsoluteTimeGetCurrent() - jsonStart) * 1000
148
+
149
+ guard let b64CompressedData = jsonResponse["data"] as? String,
150
+ let encoding = jsonResponse["encoding"] as? [String: Any],
151
+ let scale = encoding["scale"] as? NSNumber,
152
+ let offset = encoding["offset"] as? NSNumber,
153
+ let missing = encoding["missing_quantized"] as? NSNumber else {
154
+ let encKeys = (jsonResponse["encoding"] as? [String: Any])?.keys.sorted() ?? []
155
+ print("🔍 [WeatherFrameProcessor] \(fileId) INVALID_RESPONSE: keys=\(Array(jsonResponse.keys).sorted()) encodingKeys=\(encKeys)")
156
+ reject("INVALID_RESPONSE", "The API response format was invalid structure.", nil)
157
+ return
158
+ }
159
+
160
+ // 2. Base64 Decoding
161
+ let decodeStart = CFAbsoluteTimeGetCurrent()
162
+ guard let compressedData = Data(base64Encoded: b64CompressedData) else {
163
+ print("🔍 [WeatherFrameProcessor] \(fileId) DECODING_ERROR: base64 decode failed (b64 len=\(b64CompressedData.count))")
164
+ reject("DECODING_ERROR", "Failed to decode base64 data string.", nil)
165
+ return
166
+ }
167
+ let decodeTime = (CFAbsoluteTimeGetCurrent() - decodeStart) * 1000
168
+
169
+ // 3. Disk Write
170
+ let writeStart = CFAbsoluteTimeGetCurrent()
171
+ let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
172
+ let fileName = "frame_\(urlString.hashValue).zst"
173
+ let dataFile = cacheDir.appendingPathComponent(fileName)
174
+
175
+ try compressedData.write(to: dataFile)
176
+ let writeTime = (CFAbsoluteTimeGetCurrent() - writeStart) * 1000
177
+
178
+ var responseMap: [String: Any] = [
179
+ "filePath": dataFile.path,
180
+ "scale": scale,
181
+ "offset": offset,
182
+ "missing": missing,
183
+ ]
184
+
185
+ if let scaleType = encoding["scale_type"] as? String {
186
+ responseMap["scaleType"] = scaleType
187
+ }
188
+
189
+ let totalTime = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
190
+
191
+ print("""
192
+ ⚡️ [PERF] [WeatherFrameProcessor] FINISHED: \(fileId)
193
+ - Total: \(String(format: "%.2f", totalTime))ms
194
+ - JSON: \(String(format: "%.2f", jsonTime))ms
195
+ - B64 Decode: \(String(format: "%.2f", decodeTime))ms
196
+ - Disk Write: \(String(format: "%.2f", writeTime))ms
197
+ - RAM End: \(self.getMemoryUsage())
198
+ """)
199
+
200
+ if self.currentRunToken != taskToken {
201
+ print("🔍 [WeatherFrameProcessor] \(fileId) discarded after disk write: runToken mismatch before resolve")
202
+ finishCancelled()
203
+ return
204
+ }
205
+ DispatchQueue.main.async {
206
+ print("🔍 [WeatherFrameProcessor] \(fileId) RESOLVE ok path=\(dataFile.path)")
207
+ resolve(responseMap)
208
+ }
209
+
210
+ } catch {
211
+ print("🔍 [WeatherFrameProcessor] \(fileId) PROCESSING_ERROR: \(error.localizedDescription)")
212
+ reject("PROCESSING_ERROR", "Failed to process the frame data: \(error.localizedDescription)", error)
213
+ }
214
+ }
215
+ print("🔍 [WeatherFrameProcessor] \(fileId) URLSession.dataTask.resume() (runToken=\(taskToken))")
216
+ task.resume()
217
+ }
218
+
219
+ @objc
220
+ static func requiresMainQueueSetup() -> Bool {
221
+ return false
222
+ }
220
223
  }