@aguacerowx/react-native 0.0.50 → 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 (56) 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 -275
  3. package/ios/SatelliteLayerView.swift +11 -4
  4. package/ios/WeatherFrameProcessorModule.swift +222 -188
  5. package/lib/commonjs/WeatherLayerManager.js +112 -48
  6. package/lib/commonjs/WeatherLayerManager.js.map +1 -1
  7. package/lib/commonjs/aguaceroCoreDebugHooks.js +144 -0
  8. package/lib/commonjs/aguaceroCoreDebugHooks.js.map +1 -0
  9. package/lib/commonjs/aguaceroRnDebug.js +358 -0
  10. package/lib/commonjs/aguaceroRnDebug.js.map +1 -0
  11. package/lib/commonjs/gridCdnAuth.js +64 -0
  12. package/lib/commonjs/gridCdnAuth.js.map +1 -0
  13. package/lib/commonjs/index.js +50 -0
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/nexrad/nexradAndroidController.js +38 -25
  16. package/lib/commonjs/nexrad/nexradAndroidController.js.map +1 -1
  17. package/lib/commonjs/nexrad/nexradDiag.js +31 -25
  18. package/lib/commonjs/nexrad/nexradDiag.js.map +1 -1
  19. package/lib/commonjs/satellite/satelliteAndroidController.js +24 -15
  20. package/lib/commonjs/satellite/satelliteAndroidController.js.map +1 -1
  21. package/lib/module/WeatherLayerManager.js +112 -48
  22. package/lib/module/WeatherLayerManager.js.map +1 -1
  23. package/lib/module/aguaceroCoreDebugHooks.js +136 -0
  24. package/lib/module/aguaceroCoreDebugHooks.js.map +1 -0
  25. package/lib/module/aguaceroRnDebug.js +341 -0
  26. package/lib/module/aguaceroRnDebug.js.map +1 -0
  27. package/lib/module/gridCdnAuth.js +56 -0
  28. package/lib/module/gridCdnAuth.js.map +1 -0
  29. package/lib/module/index.js +2 -0
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/nexrad/nexradAndroidController.js +38 -25
  32. package/lib/module/nexrad/nexradAndroidController.js.map +1 -1
  33. package/lib/module/nexrad/nexradDiag.js +31 -25
  34. package/lib/module/nexrad/nexradDiag.js.map +1 -1
  35. package/lib/module/satellite/satelliteAndroidController.js +24 -15
  36. package/lib/module/satellite/satelliteAndroidController.js.map +1 -1
  37. package/lib/typescript/WeatherLayerManager.d.ts.map +1 -1
  38. package/lib/typescript/aguaceroCoreDebugHooks.d.ts +10 -0
  39. package/lib/typescript/aguaceroCoreDebugHooks.d.ts.map +1 -0
  40. package/lib/typescript/aguaceroRnDebug.d.ts +97 -0
  41. package/lib/typescript/aguaceroRnDebug.d.ts.map +1 -0
  42. package/lib/typescript/gridCdnAuth.d.ts +24 -0
  43. package/lib/typescript/gridCdnAuth.d.ts.map +1 -0
  44. package/lib/typescript/index.d.ts +2 -0
  45. package/lib/typescript/nexrad/nexradAndroidController.d.ts.map +1 -1
  46. package/lib/typescript/nexrad/nexradDiag.d.ts.map +1 -1
  47. package/lib/typescript/satellite/satelliteAndroidController.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/WeatherLayerManager.js +2024 -1947
  50. package/src/aguaceroCoreDebugHooks.js +142 -0
  51. package/src/aguaceroRnDebug.js +335 -0
  52. package/src/gridCdnAuth.js +56 -0
  53. package/src/index.js +19 -7
  54. package/src/nexrad/nexradAndroidController.js +1078 -1068
  55. package/src/nexrad/nexradDiag.js +150 -144
  56. package/src/satellite/satelliteAndroidController.js +245 -236
@@ -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,189 +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
-
50
- var request = URLRequest(url: url)
51
- request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
52
- if let bundleId = bundleId, !bundleId.isEmpty {
53
- request.setValue(bundleId, forHTTPHeaderField: "x-app-identifier")
54
- }
55
- if let site = options["gridRequestSiteOrigin"] as? String {
56
- var origin = site.trimmingCharacters(in: .whitespacesAndNewlines)
57
- while origin.hasSuffix("/") {
58
- origin.removeLast()
59
- }
60
- if !origin.isEmpty {
61
- request.setValue(origin, forHTTPHeaderField: "Origin")
62
- request.setValue("\(origin)/", forHTTPHeaderField: "Referer")
63
- }
64
- }
65
-
66
- let task = session.dataTask(with: request) { data, response, error in
67
- let fetchDuration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
68
-
69
- // Must reject so JS `await processFrame` does not hang forever (otherwise WeatherLayerManager
70
- // leaves hasPreloadedRef stuck and grid data never loads after cancelAllFrames).
71
- let finishCancelled = {
72
- print("⚡️ [PERF] [WeatherFrameProcessor] CANCELLED: \(fileId) after \(String(format: "%.2f", fetchDuration))ms")
73
- DispatchQueue.main.async {
74
- reject("E_CANCELLED", "Weather frame fetch superseded", nil)
75
- }
76
- }
77
-
78
- if self.currentRunToken != taskToken {
79
- print("🔍 [WeatherFrameProcessor] \(fileId) aborted: runToken mismatch (task=\(taskToken) current=\(self.currentRunToken)) → E_CANCELLED")
80
- finishCancelled()
81
- return
82
- }
83
-
84
- if let error = error {
85
- print("🔍 [WeatherFrameProcessor] \(fileId) NETWORK_ERROR: \(error.localizedDescription)")
86
- reject("NETWORK_ERROR", "Failed to fetch frame data: \(error.localizedDescription)", error)
87
- return
88
- }
89
-
90
- guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
91
- let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
92
- print("🔍 [WeatherFrameProcessor] \(fileId) HTTP_ERROR status=\(statusCode)")
93
- reject("HTTP_ERROR", "HTTP request failed with status code: \(statusCode)", nil)
94
- return
95
- }
96
-
97
- guard let data = data else {
98
- print("🔍 [WeatherFrameProcessor] \(fileId) NO_DATA")
99
- reject("NO_DATA", "The server returned no data.", nil)
100
- return
101
- }
102
-
103
- print("⚡️ [PERF] [WeatherFrameProcessor] NETWORK DONE: \(fileId) | Time: \(String(format: "%.2f", fetchDuration))ms | Size: \(data.count / 1024)KB | RAM: \(self.getMemoryUsage())")
104
-
105
- do {
106
- // 1. JSON Parsing
107
- let jsonStart = CFAbsoluteTimeGetCurrent()
108
- guard let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
109
- print("🔍 [WeatherFrameProcessor] \(fileId) INVALID_RESPONSE: top-level JSON not object")
110
- reject("INVALID_RESPONSE", "The API response format was invalid JSON.", nil)
111
- return
112
- }
113
- let jsonTime = (CFAbsoluteTimeGetCurrent() - jsonStart) * 1000
114
-
115
- guard let b64CompressedData = jsonResponse["data"] as? String,
116
- let encoding = jsonResponse["encoding"] as? [String: Any],
117
- let scale = encoding["scale"] as? NSNumber,
118
- let offset = encoding["offset"] as? NSNumber,
119
- let missing = encoding["missing_quantized"] as? NSNumber else {
120
- let encKeys = (jsonResponse["encoding"] as? [String: Any])?.keys.sorted() ?? []
121
- print("🔍 [WeatherFrameProcessor] \(fileId) INVALID_RESPONSE: keys=\(Array(jsonResponse.keys).sorted()) encodingKeys=\(encKeys)")
122
- reject("INVALID_RESPONSE", "The API response format was invalid structure.", nil)
123
- return
124
- }
125
-
126
- // 2. Base64 Decoding
127
- let decodeStart = CFAbsoluteTimeGetCurrent()
128
- guard let compressedData = Data(base64Encoded: b64CompressedData) else {
129
- print("🔍 [WeatherFrameProcessor] \(fileId) DECODING_ERROR: base64 decode failed (b64 len=\(b64CompressedData.count))")
130
- reject("DECODING_ERROR", "Failed to decode base64 data string.", nil)
131
- return
132
- }
133
- let decodeTime = (CFAbsoluteTimeGetCurrent() - decodeStart) * 1000
134
-
135
- // 3. Disk Write
136
- let writeStart = CFAbsoluteTimeGetCurrent()
137
- let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
138
- let fileName = "frame_\(urlString.hashValue).zst"
139
- let dataFile = cacheDir.appendingPathComponent(fileName)
140
-
141
- try compressedData.write(to: dataFile)
142
- let writeTime = (CFAbsoluteTimeGetCurrent() - writeStart) * 1000
143
-
144
- var responseMap: [String: Any] = [
145
- "filePath": dataFile.path,
146
- "scale": scale,
147
- "offset": offset,
148
- "missing": missing,
149
- ]
150
-
151
- if let scaleType = encoding["scale_type"] as? String {
152
- responseMap["scaleType"] = scaleType
153
- }
154
-
155
- let totalTime = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
156
-
157
- print("""
158
- ⚡️ [PERF] [WeatherFrameProcessor] FINISHED: \(fileId)
159
- - Total: \(String(format: "%.2f", totalTime))ms
160
- - JSON: \(String(format: "%.2f", jsonTime))ms
161
- - B64 Decode: \(String(format: "%.2f", decodeTime))ms
162
- - Disk Write: \(String(format: "%.2f", writeTime))ms
163
- - RAM End: \(self.getMemoryUsage())
164
- """)
165
-
166
- if self.currentRunToken != taskToken {
167
- print("🔍 [WeatherFrameProcessor] \(fileId) discarded after disk write: runToken mismatch before resolve")
168
- finishCancelled()
169
- return
170
- }
171
- DispatchQueue.main.async {
172
- print("🔍 [WeatherFrameProcessor] \(fileId) RESOLVE ok path=\(dataFile.path)")
173
- resolve(responseMap)
174
- }
175
-
176
- } catch {
177
- print("🔍 [WeatherFrameProcessor] \(fileId) PROCESSING_ERROR: \(error.localizedDescription)")
178
- reject("PROCESSING_ERROR", "Failed to process the frame data: \(error.localizedDescription)", error)
179
- }
180
- }
181
- print("🔍 [WeatherFrameProcessor] \(fileId) URLSession.dataTask.resume() (runToken=\(taskToken))")
182
- task.resume()
183
- }
184
-
185
- @objc
186
- static func requiresMainQueueSetup() -> Bool {
187
- return false
188
- }
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
+ }
189
223
  }