catalyst-core 0.2.0-beta.2 → 0.3.0-beta.1

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 (184) hide show
  1. package/babel.native.config.cjs +8 -0
  2. package/bin/catalyst.js +49 -105
  3. package/changelog.md +8 -0
  4. package/dist/caching.js +1 -1
  5. package/dist/index.jsx +8 -0
  6. package/dist/logger.js +74 -3
  7. package/dist/native/androidProject/app/build.gradle.kts +1 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
  19. package/dist/native/androidSetup.js +300 -8
  20. package/dist/native/bridge/WebBridge.js +481 -59
  21. package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
  22. package/dist/native/bridge/errors.js +250 -26
  23. package/dist/native/bridge/hooks/index.js +125 -1
  24. package/dist/native/bridge/hooks/useCamera.js +347 -9
  25. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  26. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  27. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  28. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  29. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  30. package/dist/native/bridge/hooks/useIntent.js +95 -1
  31. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  32. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  33. package/dist/native/bridge/hooks/useNotification.js +318 -2
  34. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  35. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  36. package/dist/native/bridge/hooks/utils.js +27 -1
  37. package/dist/native/bridge/hooks.js +16 -1
  38. package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
  39. package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
  40. package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
  41. package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
  42. package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
  43. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
  44. package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
  45. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
  46. package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
  47. package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
  48. package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
  49. package/dist/native/bridge/perf/core/constants.js +126 -0
  50. package/dist/native/bridge/perf/core/measure.js +189 -0
  51. package/dist/native/bridge/perf/core/session.js +100 -0
  52. package/dist/native/bridge/perf/core/store.js +554 -0
  53. package/dist/native/bridge/perf/core/summary.js +174 -0
  54. package/dist/native/bridge/perf/core/traceExport.js +240 -0
  55. package/dist/native/bridge/perf/core/waterfalls.js +146 -0
  56. package/dist/native/bridge/perf/index.js +402 -0
  57. package/dist/native/bridge/safeArea.js +53 -5
  58. package/dist/native/bridge/standardizedHooks.js +595 -82
  59. package/dist/native/bridge/useBaseHook.js +274 -38
  60. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  61. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  62. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  63. package/dist/native/buildAndroid/assets.js +510 -13
  64. package/dist/native/buildAndroid/build.js +296 -19
  65. package/dist/native/buildAndroid/config.js +87 -1
  66. package/dist/native/buildAndroid/index.js +374 -1
  67. package/dist/native/buildAppAndroid.js +34 -2
  68. package/dist/native/buildAppIos.js +62 -2
  69. package/dist/native/buildIos/assets.js +609 -8
  70. package/dist/native/buildIos/build.js +639 -20
  71. package/dist/native/buildIos/config.js +294 -12
  72. package/dist/native/buildIos/index.js +343 -6
  73. package/dist/native/buildIos/plugins.js +343 -16
  74. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  75. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  76. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  77. package/dist/native/internalPluginUtils.js +232 -7
  78. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
  89. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
  90. package/dist/native/package.json +3 -0
  91. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  92. package/dist/native/pluginComposerAndroid.js +407 -23
  93. package/dist/native/pluginComposerIos.js +435 -11
  94. package/dist/native/renameAndroidProject.js +1094 -88
  95. package/dist/native/setupEmulatorIos.js +289 -22
  96. package/dist/native/setupServer.js +160 -19
  97. package/dist/native/terminalProgress.js +199 -11
  98. package/dist/native/utils.js +193 -15
  99. package/dist/offline/catalyst-sw.js +145 -1
  100. package/dist/offline/registerServiceWorker.js +33 -1
  101. package/dist/otel.js +375 -46
  102. package/dist/router/ClientRouter.js +14 -2
  103. package/dist/router/ServerRouter.js +9 -1
  104. package/dist/router.js +1 -1
  105. package/dist/scripts/build.js +112 -3
  106. package/dist/scripts/checkVersion.js +3 -1
  107. package/dist/scripts/cleanCache.js +30 -1
  108. package/dist/scripts/generateOfflineManifest.js +116 -1
  109. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  110. package/dist/scripts/plugins.js +309 -1
  111. package/dist/scripts/preServerInit.js +13 -0
  112. package/dist/scripts/scriptUtils.js +54 -3
  113. package/dist/scripts/serve.js +51 -4
  114. package/dist/scripts/start.js +47 -10
  115. package/dist/scripts/validator.js +149 -1
  116. package/dist/sentry.cjs +127 -0
  117. package/dist/sentry.js +125 -5
  118. package/dist/server/expressServer.js +250 -5
  119. package/dist/server/manifestCache.js +36 -0
  120. package/dist/server/renderer/ChunkExtractor.js +144 -0
  121. package/dist/server/renderer/document/Body.jsx +59 -0
  122. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  123. package/dist/server/renderer/document/Head.jsx +60 -0
  124. package/dist/server/renderer/document/index.jsx +2 -0
  125. package/dist/server/renderer/extract.js +149 -31
  126. package/dist/server/renderer/handler.jsx +433 -0
  127. package/dist/server/renderer/index.js +8 -1
  128. package/dist/server/renderer/render.js +47 -0
  129. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  130. package/dist/server/utils/cjsRequire.js +10 -0
  131. package/dist/server/utils/userAgentUtil.js +87 -9
  132. package/dist/server/utils/validator.js +164 -1
  133. package/dist/vite/FastRefresh.jsx +20 -0
  134. package/dist/vite/inject-cache-key-plugin.js +276 -0
  135. package/dist/vite/loadCustomViteConfig.js +22 -0
  136. package/dist/vite/manifest-categorization-plugin.js +455 -0
  137. package/dist/vite/node-loader.mjs +458 -0
  138. package/dist/vite/scssParams.js +21 -0
  139. package/dist/vite/vite.config.client.js +130 -0
  140. package/dist/vite/vite.config.js +422 -0
  141. package/dist/vite/vite.config.server.js +84 -0
  142. package/dist/web-router/components/MetaTag.jsx +34 -0
  143. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  144. package/dist/web-router/components/Split.jsx +200 -0
  145. package/dist/web-router/components/SplitInview.jsx +106 -0
  146. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  147. package/dist/web-router/context.jsx +12 -0
  148. package/dist/web-router/hooks.jsx +72 -0
  149. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  150. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  151. package/dist/web-router/types/context.d.ts +14 -0
  152. package/dist/web-router/types/hooks.d.ts +1 -0
  153. package/dist/web-router/types/index.d.ts +3 -0
  154. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  155. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  156. package/mcp_v2/conversion-tasks.json +35 -17
  157. package/mcp_v2/knowledge-base.json +124 -13
  158. package/mcp_v2/lib/helpers.js +10 -0
  159. package/mcp_v2/mcp.js +4 -4
  160. package/mcp_v2/package.json +1 -0
  161. package/mcp_v2/tools/build.js +116 -20
  162. package/mcp_v2/tools/conversion.js +106 -5
  163. package/mcp_v2/tools/tasks.js +18 -14
  164. package/package.json +35 -68
  165. package/babel.config.js +0 -32
  166. package/dist/index.js +0 -1
  167. package/dist/scripts/devBuild.js +0 -3
  168. package/dist/scripts/devServe.js +0 -3
  169. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  170. package/dist/scripts/registerAliases.js +0 -6
  171. package/dist/server/renderer/document/Body.js +0 -11
  172. package/dist/server/renderer/document/Head.js +0 -9
  173. package/dist/server/renderer/document/index.js +0 -1
  174. package/dist/server/renderer/handler.js +0 -47
  175. package/dist/server/startServer.js +0 -57
  176. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  177. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  178. package/dist/webpack/babel.config.client.js +0 -1
  179. package/dist/webpack/babel.config.ssr.js +0 -1
  180. package/dist/webpack/base.babel.js +0 -7
  181. package/dist/webpack/development.client.babel.js +0 -10
  182. package/dist/webpack/production.client.babel.js +0 -4
  183. package/dist/webpack/production.ssr.babel.js +0 -8
  184. package/dist/webpack/scssParams.js +0 -9
@@ -0,0 +1,373 @@
1
+ import Foundation
2
+ import WebKit
3
+ import MachO
4
+
5
+ enum CatalystPerf {
6
+ private static let maxBufferSize = 500
7
+ private static let flushDelayMs: UInt64 = 250
8
+ private static let flushRetryDelayMs: UInt64 = 250
9
+ private static let maxFlushRetries = 5
10
+ private static let periodicFlushIntervalMs: UInt64 = 4_000
11
+ private static let lock = NSLock()
12
+
13
+ private static var buffer: [[String: Any]] = []
14
+ private static var pendingCalls: [String: (startMs: Int64, method: String)] = [:]
15
+ private static var flushed = false
16
+ private static var flushGeneration = 0
17
+ private static var batchSequence = 0
18
+ private static var inFlight: PendingBatch?
19
+ private static weak var periodicWebView: WKWebView?
20
+ private static var periodicTask: Task<Void, Never>?
21
+
22
+ private static var cacheHits = 0
23
+ private static var cacheMisses = 0
24
+ private static var cacheFetches = 0
25
+ private static var cacheTotalMs: Int64 = 0
26
+ private static var cacheTopSlow: [(durationMs: Int64, filename: String)] = []
27
+
28
+ private struct PendingBatch {
29
+ let id: String
30
+ let events: [[String: Any]]
31
+ let generation: Int
32
+ let retryCount: Int
33
+ }
34
+
35
+ static func nativeTimeMs() -> Int64 {
36
+ Int64(ProcessInfo.processInfo.systemUptime * 1000)
37
+ }
38
+
39
+ static func add(_ event: [String: Any]) {
40
+ #if DEBUG
41
+ guard ConfigConstants.Profiler.enabled else { return }
42
+ var payload = event
43
+ if payload["nativeTime"] == nil {
44
+ payload["nativeTime"] = nativeTimeMs()
45
+ }
46
+
47
+ lock.lock()
48
+ if buffer.count >= maxBufferSize {
49
+ buffer.removeFirst()
50
+ }
51
+ updateCacheSummaryLocked(payload)
52
+ buffer.append(payload)
53
+ lock.unlock()
54
+ #endif
55
+ }
56
+
57
+ static func emit(_ event: [String: Any], to webView: WKWebView?) {
58
+ #if DEBUG
59
+ guard ConfigConstants.Profiler.enabled else { return }
60
+ guard let webView else {
61
+ add(event)
62
+ return
63
+ }
64
+
65
+ var payload = event
66
+ if payload["nativeTime"] == nil {
67
+ payload["nativeTime"] = nativeTimeMs()
68
+ }
69
+
70
+ DispatchQueue.main.async {
71
+ guard let json = jsonString(payload) else { return }
72
+ let script = "window.__catalystPerfEvent && window.__catalystPerfEvent(\(javaScriptStringLiteral(json)));"
73
+ webView.evaluateJavaScript(script)
74
+ }
75
+ #endif
76
+ }
77
+
78
+ static func bridgeCallReceived(callId: String, method: String) {
79
+ #if DEBUG
80
+ guard ConfigConstants.Profiler.enabled else { return }
81
+ lock.lock()
82
+ pendingCalls[callId] = (nativeTimeMs(), method)
83
+ lock.unlock()
84
+ #endif
85
+ }
86
+
87
+ static func bridgeCallDispatched(callId: String) {
88
+ #if DEBUG
89
+ guard ConfigConstants.Profiler.enabled else { return }
90
+ lock.lock()
91
+ guard let pending = pendingCalls.removeValue(forKey: callId) else {
92
+ lock.unlock()
93
+ return
94
+ }
95
+ lock.unlock()
96
+
97
+ let endMs = nativeTimeMs()
98
+ add([
99
+ "type": "api-call",
100
+ "callId": callId,
101
+ "method": pending.method,
102
+ "nativeStartMs": pending.startMs,
103
+ "nativeEndMs": endMs,
104
+ "durationMs": endMs - pending.startMs,
105
+ "thread": Thread.current.name ?? "unknown",
106
+ ])
107
+ #endif
108
+ }
109
+
110
+ static func scheduleFlush(_ webView: WKWebView?) {
111
+ #if DEBUG
112
+ guard ConfigConstants.Profiler.enabled else { return }
113
+ guard let webView else { return }
114
+
115
+ lock.lock()
116
+ if flushed {
117
+ lock.unlock()
118
+ return
119
+ }
120
+ flushed = true
121
+ lock.unlock()
122
+
123
+ Task { @MainActor in
124
+ try? await Task.sleep(nanoseconds: flushDelayMs * 1_000_000)
125
+ beginFlush(webView, includeCacheSummary: true)
126
+ startPeriodicFlush(webView)
127
+ }
128
+ #endif
129
+ }
130
+
131
+ static func flushNow(_ webView: WKWebView?) {
132
+ #if DEBUG
133
+ guard ConfigConstants.Profiler.enabled else { return }
134
+ guard let webView else { return }
135
+ Task { @MainActor in
136
+ beginFlush(webView, includeCacheSummary: false)
137
+ }
138
+ #endif
139
+ }
140
+
141
+ static func reset() {
142
+ #if DEBUG
143
+ stopPeriodicFlush()
144
+ lock.lock()
145
+ buffer.removeAll()
146
+ flushGeneration += 1
147
+ inFlight = nil
148
+ pendingCalls.removeAll()
149
+ flushed = false
150
+ cacheHits = 0
151
+ cacheMisses = 0
152
+ cacheFetches = 0
153
+ cacheTotalMs = 0
154
+ cacheTopSlow.removeAll()
155
+ lock.unlock()
156
+ #endif
157
+ }
158
+
159
+ static func memorySnapshot(to webView: WKWebView?, label: String? = nil) {
160
+ #if DEBUG
161
+ guard ConfigConstants.Profiler.enabled else { return }
162
+ let currentMb = currentResidentMemoryMb()
163
+ let event: [String: Any] = [
164
+ "type": "memory-snapshot",
165
+ "nativeTime": nativeTimeMs(),
166
+ "jvmMb": 0,
167
+ "webviewMb": currentMb,
168
+ "nativeMb": currentMb,
169
+ "totalMb": currentMb,
170
+ "otherMb": 0,
171
+ "peakMb": currentMb,
172
+ "thread": Thread.current.name ?? "unknown",
173
+ "label": label ?? "ios-process",
174
+ ]
175
+
176
+ if let webView {
177
+ emit(event, to: webView)
178
+ } else {
179
+ add(event)
180
+ }
181
+ #endif
182
+ }
183
+
184
+ static func injectNativeTimeOffset(into webView: WKWebView?) {
185
+ #if DEBUG
186
+ guard ConfigConstants.Profiler.enabled else { return }
187
+ guard let webView else { return }
188
+ let nativeNow = nativeTimeMs()
189
+ DispatchQueue.main.async {
190
+ webView.evaluateJavaScript("window.__NATIVE_TIME_OFFSET = \(nativeNow) - performance.now();")
191
+ }
192
+ #endif
193
+ }
194
+
195
+ private static func startPeriodicFlush(_ webView: WKWebView) {
196
+ periodicTask?.cancel()
197
+ periodicWebView = webView
198
+ periodicTask = Task { @MainActor in
199
+ while !Task.isCancelled {
200
+ try? await Task.sleep(nanoseconds: periodicFlushIntervalMs * 1_000_000)
201
+ guard let webView = periodicWebView else { return }
202
+ lock.lock()
203
+ let pendingBatch = inFlight
204
+ lock.unlock()
205
+ if let pendingBatch {
206
+ deliver(pendingBatch, to: webView)
207
+ } else {
208
+ beginFlush(webView, includeCacheSummary: false)
209
+ }
210
+ memorySnapshot(to: webView, label: "periodic")
211
+ }
212
+ }
213
+ }
214
+
215
+ private static func stopPeriodicFlush() {
216
+ periodicTask?.cancel()
217
+ periodicTask = nil
218
+ periodicWebView = nil
219
+ }
220
+
221
+ @MainActor
222
+ private static func beginFlush(_ webView: WKWebView, includeCacheSummary: Bool) {
223
+ lock.lock()
224
+ guard inFlight == nil else {
225
+ lock.unlock()
226
+ return
227
+ }
228
+ var events = buffer
229
+ buffer.removeAll()
230
+ if includeCacheSummary, let summary = buildCacheSummaryEventLocked() {
231
+ events.append(summary)
232
+ }
233
+ guard !events.isEmpty else {
234
+ lock.unlock()
235
+ return
236
+ }
237
+ batchSequence += 1
238
+ let batch = PendingBatch(
239
+ id: "catalyst-\(batchSequence)",
240
+ events: events,
241
+ generation: flushGeneration,
242
+ retryCount: 0
243
+ )
244
+ inFlight = batch
245
+ lock.unlock()
246
+
247
+ deliver(batch, to: webView)
248
+ }
249
+
250
+ @MainActor
251
+ private static func deliver(_ batch: PendingBatch, to webView: WKWebView) {
252
+ lock.lock()
253
+ let isCurrent = inFlight?.id == batch.id && batch.generation == flushGeneration
254
+ lock.unlock()
255
+ guard isCurrent, let json = jsonString(batch.events) else { return }
256
+
257
+ let script = "window.__catalystPerfBatch ? window.__catalystPerfBatch(\(javaScriptStringLiteral(batch.id)), \(javaScriptStringLiteral(json))) : false"
258
+ webView.evaluateJavaScript(script) { result, _ in
259
+ if (result as? Bool) == true {
260
+ acknowledge(batch)
261
+ } else {
262
+ retry(batch, to: webView)
263
+ }
264
+ }
265
+ }
266
+
267
+ private static func acknowledge(_ batch: PendingBatch) {
268
+ lock.lock()
269
+ if inFlight?.id == batch.id && batch.generation == flushGeneration {
270
+ inFlight = nil
271
+ }
272
+ lock.unlock()
273
+ }
274
+
275
+ private static func retry(_ batch: PendingBatch, to webView: WKWebView) {
276
+ lock.lock()
277
+ guard inFlight?.id == batch.id && batch.generation == flushGeneration else {
278
+ lock.unlock()
279
+ return
280
+ }
281
+ guard batch.retryCount < maxFlushRetries else {
282
+ lock.unlock()
283
+ return
284
+ }
285
+ let retryBatch = PendingBatch(
286
+ id: batch.id,
287
+ events: batch.events,
288
+ generation: batch.generation,
289
+ retryCount: batch.retryCount + 1
290
+ )
291
+ inFlight = retryBatch
292
+ lock.unlock()
293
+
294
+ Task { @MainActor in
295
+ try? await Task.sleep(nanoseconds: flushRetryDelayMs * 1_000_000)
296
+ deliver(retryBatch, to: webView)
297
+ }
298
+ }
299
+
300
+ private static func updateCacheSummaryLocked(_ event: [String: Any]) {
301
+ guard let type = event["type"] as? String else { return }
302
+ let durationMs = event["durationMs"] as? Int64 ?? Int64(event["durationMs"] as? Int ?? 0)
303
+ let url = event["url"] as? String ?? ""
304
+
305
+ switch type {
306
+ case "cache-hit-memory", "cache-hit-disk":
307
+ cacheHits += 1
308
+ case "cache-miss-fetch":
309
+ cacheMisses += 1
310
+ case "network-fetch-complete":
311
+ cacheFetches += 1
312
+ cacheTotalMs += durationMs
313
+ cacheTopSlow.append((durationMs, filename(from: url)))
314
+ cacheTopSlow = Array(cacheTopSlow.sorted { $0.durationMs > $1.durationMs }.prefix(5))
315
+ default:
316
+ break
317
+ }
318
+ }
319
+
320
+ private static func buildCacheSummaryEventLocked() -> [String: Any]? {
321
+ let total = cacheHits + cacheMisses
322
+ guard total > 0 else { return nil }
323
+ let hitRate = Double(cacheHits) * 100.0 / Double(total)
324
+ let avgFetchMs = cacheFetches > 0 ? cacheTotalMs / Int64(cacheFetches) : 0
325
+ return [
326
+ "type": "cache-summary",
327
+ "hits": cacheHits,
328
+ "misses": cacheMisses,
329
+ "fetches": cacheFetches,
330
+ "total": total,
331
+ "hitRatePct": (hitRate * 10).rounded() / 10,
332
+ "avgFetchMs": avgFetchMs,
333
+ "topSlowest": cacheTopSlow.map { ["filename": $0.filename, "durationMs": $0.durationMs] },
334
+ "nativeTime": nativeTimeMs(),
335
+ ]
336
+ }
337
+
338
+ private static func currentResidentMemoryMb() -> Int {
339
+ var info = mach_task_basic_info()
340
+ var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
341
+ let result = withUnsafeMutablePointer(to: &info) {
342
+ $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
343
+ task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
344
+ }
345
+ }
346
+ guard result == KERN_SUCCESS else { return 0 }
347
+ return Int(info.resident_size / 1024 / 1024)
348
+ }
349
+
350
+ private static func filename(from url: String) -> String {
351
+ URL(string: url)?.lastPathComponent.prefix(40).description ?? String(url.prefix(40))
352
+ }
353
+
354
+ private static func jsonString(_ object: Any) -> String? {
355
+ guard JSONSerialization.isValidJSONObject(object),
356
+ let data = try? JSONSerialization.data(withJSONObject: object),
357
+ let json = String(data: data, encoding: .utf8) else {
358
+ return nil
359
+ }
360
+ return json
361
+ }
362
+
363
+ private static func javaScriptStringLiteral(_ value: String) -> String {
364
+ let escaped = value
365
+ .replacingOccurrences(of: "\\", with: "\\\\")
366
+ .replacingOccurrences(of: "'", with: "\\'")
367
+ .replacingOccurrences(of: "\n", with: "\\n")
368
+ .replacingOccurrences(of: "\r", with: "\\r")
369
+ .replacingOccurrences(of: "\u{2028}", with: "\\u2028")
370
+ .replacingOccurrences(of: "\u{2029}", with: "\\u2029")
371
+ return "'\(escaped)'"
372
+ }
373
+ }
@@ -48,6 +48,40 @@ class BridgeFileHandler: NSObject {
48
48
 
49
49
  // MARK: - File Intent Operations
50
50
 
51
+ func exportCatalystTrace(params: Any?) {
52
+ guard let payload = params as? [String: Any],
53
+ let filename = payload["filename"] as? String,
54
+ let trace = payload["trace"] as? String,
55
+ !filename.isEmpty,
56
+ !trace.isEmpty else {
57
+ delegate?.sendErrorCallback(eventName: "ON_INTENT_ERROR", error: "Invalid Catalyst trace export payload", code: "INVALID_PARAMETERS")
58
+ return
59
+ }
60
+
61
+ let safeFilename = filename.replacingOccurrences(of: "/", with: "-")
62
+ let exportsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
63
+ .appendingPathComponent("CatalystTraces", isDirectory: true)
64
+ let fileURL = exportsURL.appendingPathComponent(safeFilename)
65
+
66
+ do {
67
+ try FileManager.default.createDirectory(at: exportsURL, withIntermediateDirectories: true)
68
+ try trace.write(to: fileURL, atomically: true, encoding: .utf8)
69
+ DispatchQueue.main.async { [weak self] in
70
+ self?.presentSharingSheet(for: fileURL)
71
+ }
72
+ delegate?.sendJSONCallback(eventName: "ON_INTENT_SUCCESS", data: [
73
+ "message": "Catalyst trace exported",
74
+ "filePath": fileURL.path,
75
+ "filename": safeFilename,
76
+ "timestamp": ISO8601DateFormatter().string(from: Date()),
77
+ "platform": "ios"
78
+ ])
79
+ } catch {
80
+ fileLogger.error("Failed to export Catalyst trace: \(error.localizedDescription)")
81
+ delegate?.sendErrorCallback(eventName: "ON_INTENT_ERROR", error: "Failed to export Catalyst trace", code: "TRACE_EXPORT_ERROR")
82
+ }
83
+ }
84
+
51
85
  // Open file with external app using intent (iOS equivalent of Android intent)
52
86
  func openFileWithIntent(params: Any?) {
53
87
  fileLogger.debug("openFileWithIntent called with params: \(String(describing: params))")
@@ -337,4 +371,4 @@ extension BridgeFileHandler: UIDocumentInteractionControllerDelegate {
337
371
  ])
338
372
  }
339
373
  }
340
- }
374
+ }
@@ -208,6 +208,15 @@ class BridgeMessageValidator {
208
208
  "type": "object",
209
209
  "properties": [:],
210
210
  "additionalProperties": false
211
+ ],
212
+ "exportCatalystTrace": [
213
+ "type": "object",
214
+ "properties": [
215
+ "filename": ["type": "string"],
216
+ "trace": ["type": "string"]
217
+ ],
218
+ "required": ["filename", "trace"],
219
+ "additionalProperties": false
211
220
  ]
212
221
  ]
213
222
 
@@ -233,34 +242,38 @@ class BridgeMessageValidator {
233
242
  )
234
243
  }
235
244
 
236
- // Step 2: Validate message size
237
- if let messageData = try? JSONSerialization.data(withJSONObject: message.body, options: []),
238
- messageData.count > CatalystConstants.Bridge.maxMessageSize {
239
- logger.error("Message size exceeds limit: \(messageData.count) > \(CatalystConstants.Bridge.maxMessageSize)")
245
+ // Step 2: Validate message body structure
246
+ guard let body = message.body as? [String: Any] else {
247
+ logger.error("Invalid message format - body is not a dictionary")
240
248
  return BridgeValidationResult(
241
249
  isValid: false,
242
250
  command: nil,
243
251
  params: nil,
244
252
  body: nil,
245
253
  error: BridgeValidationError(
246
- message: "Message too large",
247
- code: "MESSAGE_TOO_LARGE",
254
+ message: "Invalid message format",
255
+ code: "INVALID_FORMAT",
248
256
  eventName: "BRIDGE_ERROR"
249
257
  )
250
258
  )
251
259
  }
252
260
 
253
- // Step 3: Validate message body structure
254
- guard let body = message.body as? [String: Any] else {
255
- logger.error("Invalid message format - body is not a dictionary")
261
+ // Step 3: Trace export gets a larger limit only when that debug profiler command is enabled.
262
+ let rawCommand = body["command"] as? String
263
+ let maxMessageSize = rawCommand == "exportCatalystTrace" && CatalystConstants.Bridge.isTraceExportEnabled()
264
+ ? 25 * 1024 * 1024
265
+ : CatalystConstants.Bridge.maxMessageSize
266
+ if let messageData = try? JSONSerialization.data(withJSONObject: message.body, options: []),
267
+ messageData.count > maxMessageSize {
268
+ logger.error("Message size exceeds limit: \(messageData.count) > \(maxMessageSize)")
256
269
  return BridgeValidationResult(
257
270
  isValid: false,
258
271
  command: nil,
259
272
  params: nil,
260
273
  body: nil,
261
274
  error: BridgeValidationError(
262
- message: "Invalid message format",
263
- code: "INVALID_FORMAT",
275
+ message: "Message too large",
276
+ code: "MESSAGE_TOO_LARGE",
264
277
  eventName: "BRIDGE_ERROR"
265
278
  )
266
279
  )
@@ -267,7 +267,10 @@ extension NativeBridge: WKScriptMessageHandler {
267
267
  logger.debug("Received validated command: \(command)")
268
268
 
269
269
  // Execute commands with proper error handling
270
+ let callId = "\(command):\(UUID().uuidString)"
271
+ CatalystPerf.bridgeCallReceived(callId: callId, method: command)
270
272
  executeCommand(command, params: params)
273
+ CatalystPerf.bridgeCallDispatched(callId: callId)
271
274
  }
272
275
 
273
276
  // MARK: - Command Execution
@@ -295,6 +298,12 @@ extension NativeBridge: WKScriptMessageHandler {
295
298
  commandHandler.pickFile(options: optionsString)
296
299
  case "openFileWithIntent":
297
300
  fileHandler.openFileWithIntent(params: params)
301
+ case "exportCatalystTrace":
302
+ guard CatalystConstants.Bridge.isTraceExportEnabled() else {
303
+ sendErrorCallback(eventName: "BRIDGE_ERROR", error: "Unsupported command: exportCatalystTrace", code: "UNSUPPORTED_COMMAND")
304
+ return
305
+ }
306
+ fileHandler.exportCatalystTrace(params: params)
298
307
  case "requestHapticFeedback":
299
308
  let feedbackType = delegateHandler.extractFeedbackType(from: params)
300
309
  commandHandler.requestHapticFeedback(feedbackType: feedbackType)