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.
- package/babel.native.config.cjs +8 -0
- package/bin/catalyst.js +49 -105
- package/changelog.md +8 -0
- package/dist/caching.js +1 -1
- package/dist/index.jsx +8 -0
- package/dist/logger.js +74 -3
- package/dist/native/androidProject/app/build.gradle.kts +1 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
- package/dist/native/androidSetup.js +300 -8
- package/dist/native/bridge/WebBridge.js +481 -59
- package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
- package/dist/native/bridge/errors.js +250 -26
- package/dist/native/bridge/hooks/index.js +125 -1
- package/dist/native/bridge/hooks/useCamera.js +347 -9
- package/dist/native/bridge/hooks/useDataProtection.js +200 -2
- package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
- package/dist/native/bridge/hooks/useFilePicker.js +429 -2
- package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
- package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
- package/dist/native/bridge/hooks/useIntent.js +95 -1
- package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
- package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
- package/dist/native/bridge/hooks/useNotification.js +318 -2
- package/dist/native/bridge/hooks/useSafeArea.js +84 -2
- package/dist/native/bridge/hooks/useVideoStream.js +434 -6
- package/dist/native/bridge/hooks/utils.js +27 -1
- package/dist/native/bridge/hooks.js +16 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
- package/dist/native/bridge/perf/core/constants.js +126 -0
- package/dist/native/bridge/perf/core/measure.js +189 -0
- package/dist/native/bridge/perf/core/session.js +100 -0
- package/dist/native/bridge/perf/core/store.js +554 -0
- package/dist/native/bridge/perf/core/summary.js +174 -0
- package/dist/native/bridge/perf/core/traceExport.js +240 -0
- package/dist/native/bridge/perf/core/waterfalls.js +146 -0
- package/dist/native/bridge/perf/index.js +402 -0
- package/dist/native/bridge/safeArea.js +53 -5
- package/dist/native/bridge/standardizedHooks.js +595 -82
- package/dist/native/bridge/useBaseHook.js +274 -38
- package/dist/native/bridge/utils/CameraUtils.js +201 -56
- package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
- package/dist/native/bridge/utils/NativeBridge.js +425 -141
- package/dist/native/buildAndroid/assets.js +510 -13
- package/dist/native/buildAndroid/build.js +296 -19
- package/dist/native/buildAndroid/config.js +87 -1
- package/dist/native/buildAndroid/index.js +374 -1
- package/dist/native/buildAppAndroid.js +34 -2
- package/dist/native/buildAppIos.js +62 -2
- package/dist/native/buildIos/assets.js +609 -8
- package/dist/native/buildIos/build.js +639 -20
- package/dist/native/buildIos/config.js +294 -12
- package/dist/native/buildIos/index.js +343 -6
- package/dist/native/buildIos/plugins.js +343 -16
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
- package/dist/native/internalPluginUtils.js +232 -7
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
- package/dist/native/package.json +3 -0
- package/dist/native/plugin-bridge/PluginBridge.js +227 -1
- package/dist/native/pluginComposerAndroid.js +407 -23
- package/dist/native/pluginComposerIos.js +435 -11
- package/dist/native/renameAndroidProject.js +1094 -88
- package/dist/native/setupEmulatorIos.js +289 -22
- package/dist/native/setupServer.js +160 -19
- package/dist/native/terminalProgress.js +199 -11
- package/dist/native/utils.js +193 -15
- package/dist/offline/catalyst-sw.js +145 -1
- package/dist/offline/registerServiceWorker.js +33 -1
- package/dist/otel.js +375 -46
- package/dist/router/ClientRouter.js +14 -2
- package/dist/router/ServerRouter.js +9 -1
- package/dist/router.js +1 -1
- package/dist/scripts/build.js +112 -3
- package/dist/scripts/checkVersion.js +3 -1
- package/dist/scripts/cleanCache.js +30 -1
- package/dist/scripts/generateOfflineManifest.js +116 -1
- package/dist/scripts/loadEnvironmentVariables.js +32 -6
- package/dist/scripts/plugins.js +309 -1
- package/dist/scripts/preServerInit.js +13 -0
- package/dist/scripts/scriptUtils.js +54 -3
- package/dist/scripts/serve.js +51 -4
- package/dist/scripts/start.js +47 -10
- package/dist/scripts/validator.js +149 -1
- package/dist/sentry.cjs +127 -0
- package/dist/sentry.js +125 -5
- package/dist/server/expressServer.js +250 -5
- package/dist/server/manifestCache.js +36 -0
- package/dist/server/renderer/ChunkExtractor.js +144 -0
- package/dist/server/renderer/document/Body.jsx +59 -0
- package/dist/server/renderer/document/FastRefresh.jsx +20 -0
- package/dist/server/renderer/document/Head.jsx +60 -0
- package/dist/server/renderer/document/index.jsx +2 -0
- package/dist/server/renderer/extract.js +149 -31
- package/dist/server/renderer/handler.jsx +433 -0
- package/dist/server/renderer/index.js +8 -1
- package/dist/server/renderer/render.js +47 -0
- package/dist/server/utils/botDetectionMiddleware.js +29 -0
- package/dist/server/utils/cjsRequire.js +10 -0
- package/dist/server/utils/userAgentUtil.js +87 -9
- package/dist/server/utils/validator.js +164 -1
- package/dist/vite/FastRefresh.jsx +20 -0
- package/dist/vite/inject-cache-key-plugin.js +276 -0
- package/dist/vite/loadCustomViteConfig.js +22 -0
- package/dist/vite/manifest-categorization-plugin.js +455 -0
- package/dist/vite/node-loader.mjs +458 -0
- package/dist/vite/scssParams.js +21 -0
- package/dist/vite/vite.config.client.js +130 -0
- package/dist/vite/vite.config.js +422 -0
- package/dist/vite/vite.config.server.js +84 -0
- package/dist/web-router/components/MetaTag.jsx +34 -0
- package/dist/web-router/components/RouterDataProvider.jsx +332 -0
- package/dist/web-router/components/Split.jsx +200 -0
- package/dist/web-router/components/SplitInview.jsx +106 -0
- package/dist/web-router/components/SsrRequestContext.jsx +14 -0
- package/dist/web-router/context.jsx +12 -0
- package/dist/web-router/hooks.jsx +72 -0
- package/dist/web-router/types/components/MetaTag.d.ts +2 -0
- package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
- package/dist/web-router/types/context.d.ts +14 -0
- package/dist/web-router/types/hooks.d.ts +1 -0
- package/dist/web-router/types/index.d.ts +3 -0
- package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
- package/dist/web-router/utils/metaDataUtils.jsx +119 -0
- package/mcp_v2/conversion-tasks.json +35 -17
- package/mcp_v2/knowledge-base.json +124 -13
- package/mcp_v2/lib/helpers.js +10 -0
- package/mcp_v2/mcp.js +4 -4
- package/mcp_v2/package.json +1 -0
- package/mcp_v2/tools/build.js +116 -20
- package/mcp_v2/tools/conversion.js +106 -5
- package/mcp_v2/tools/tasks.js +18 -14
- package/package.json +35 -68
- package/babel.config.js +0 -32
- package/dist/index.js +0 -1
- package/dist/scripts/devBuild.js +0 -3
- package/dist/scripts/devServe.js +0 -3
- package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
- package/dist/scripts/registerAliases.js +0 -6
- package/dist/server/renderer/document/Body.js +0 -11
- package/dist/server/renderer/document/Head.js +0 -9
- package/dist/server/renderer/document/index.js +0 -1
- package/dist/server/renderer/handler.js +0 -47
- package/dist/server/startServer.js +0 -57
- package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
- package/dist/webpack/babel.config.client.js +0 -1
- package/dist/webpack/babel.config.ssr.js +0 -1
- package/dist/webpack/base.babel.js +0 -7
- package/dist/webpack/development.client.babel.js +0 -10
- package/dist/webpack/production.client.babel.js +0 -4
- package/dist/webpack/production.ssr.babel.js +0 -8
- package/dist/webpack/scssParams.js +0 -9
|
@@ -31,6 +31,10 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
31
31
|
|
|
32
32
|
let totalTime = (CFAbsoluteTimeGetCurrent() - start) * 1000
|
|
33
33
|
logWithTimestamp("🏗️ WebView init completed (total: \(String(format: "%.2f", totalTime))ms, protocol: \(String(format: "%.2f", protocolTime))ms)")
|
|
34
|
+
CatalystPerf.add([
|
|
35
|
+
"type": "boot-activity-created",
|
|
36
|
+
"durationMs": totalTime,
|
|
37
|
+
])
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
public func makeUIView(context: Context) -> WKWebView {
|
|
@@ -54,12 +58,33 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
54
58
|
preferences.allowsContentJavaScript = true
|
|
55
59
|
configuration.defaultWebpagePreferences = preferences
|
|
56
60
|
|
|
61
|
+
#if DEBUG
|
|
62
|
+
if ConfigConstants.Profiler.enabled {
|
|
63
|
+
configuration.userContentController.addUserScript(
|
|
64
|
+
WKUserScript(
|
|
65
|
+
source: "window.__CATALYST_PROFILER_ENABLED = true;",
|
|
66
|
+
injectionTime: .atDocumentStart,
|
|
67
|
+
forMainFrameOnly: true
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
#endif
|
|
72
|
+
|
|
57
73
|
let webViewCreateStart = CFAbsoluteTimeGetCurrent()
|
|
58
74
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
|
59
75
|
let webViewCreateTime = (CFAbsoluteTimeGetCurrent() - webViewCreateStart) * 1000
|
|
60
76
|
logWithTimestamp("📦 WKWebView created (took \(String(format: "%.2f", webViewCreateTime))ms)")
|
|
77
|
+
CatalystPerf.add([
|
|
78
|
+
"type": "boot-webview-constructed",
|
|
79
|
+
"durationMs": webViewCreateTime,
|
|
80
|
+
])
|
|
61
81
|
|
|
62
82
|
webView.navigationDelegate = navigationDelegate
|
|
83
|
+
#if DEBUG
|
|
84
|
+
if ConfigConstants.Profiler.enabled {
|
|
85
|
+
webView.scrollView.delegate = context.coordinator
|
|
86
|
+
}
|
|
87
|
+
#endif
|
|
63
88
|
webView.allowsBackForwardNavigationGestures = true
|
|
64
89
|
webView.isOpaque = false
|
|
65
90
|
webView.backgroundColor = .clear
|
|
@@ -101,6 +126,10 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
101
126
|
let request = URLRequest(url: url)
|
|
102
127
|
logWithTimestamp("🚀 Calling webView.load()")
|
|
103
128
|
let loadStart = CFAbsoluteTimeGetCurrent()
|
|
129
|
+
CatalystPerf.add([
|
|
130
|
+
"type": "boot-load-url",
|
|
131
|
+
"url": url.absoluteString,
|
|
132
|
+
])
|
|
104
133
|
webView.load(request)
|
|
105
134
|
let loadTime = (CFAbsoluteTimeGetCurrent() - loadStart) * 1000
|
|
106
135
|
logWithTimestamp("✅ webView.load() returned (took \(String(format: "%.2f", loadTime))ms)")
|
|
@@ -118,6 +147,11 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
118
147
|
|
|
119
148
|
let makeUIViewTime = (CFAbsoluteTimeGetCurrent() - makeUIViewStart) * 1000
|
|
120
149
|
logWithTimestamp("🔨 makeUIView() completed (took \(String(format: "%.2f", makeUIViewTime))ms)")
|
|
150
|
+
#if DEBUG
|
|
151
|
+
if ConfigConstants.Profiler.enabled {
|
|
152
|
+
context.coordinator.startKeyboardPerfTracking(webView)
|
|
153
|
+
}
|
|
154
|
+
#endif
|
|
121
155
|
|
|
122
156
|
return webView
|
|
123
157
|
}
|
|
@@ -145,6 +179,9 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
145
179
|
}
|
|
146
180
|
|
|
147
181
|
// Clean up native bridge
|
|
182
|
+
coordinator.stopPerfMonitoring()
|
|
183
|
+
coordinator.stopKeyboardPerfTracking()
|
|
184
|
+
webView.scrollView.delegate = nil
|
|
148
185
|
coordinator.nativeBridge?.unregister()
|
|
149
186
|
coordinator.nativeBridge = nil
|
|
150
187
|
coordinator.pluginBridge?.unregister()
|
|
@@ -163,6 +200,10 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
163
200
|
var pluginBridge: PluginBridge?
|
|
164
201
|
var hostingController: UIViewController?
|
|
165
202
|
var isObserverAdded = false
|
|
203
|
+
private weak var perfWebView: WKWebView?
|
|
204
|
+
private var keyboardObserverTokens: [NSObjectProtocol] = []
|
|
205
|
+
private var scrollSessionOpen = false
|
|
206
|
+
private var fpsMonitor: DisplayLinkPerfMonitor?
|
|
166
207
|
|
|
167
208
|
public init(_ parent: WebView) {
|
|
168
209
|
self.parent = parent
|
|
@@ -189,6 +230,17 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
189
230
|
pluginBridge.register()
|
|
190
231
|
self.nativeBridge = bridge
|
|
191
232
|
self.pluginBridge = pluginBridge
|
|
233
|
+
#if DEBUG
|
|
234
|
+
if ConfigConstants.Profiler.enabled {
|
|
235
|
+
self.fpsMonitor = DisplayLinkPerfMonitor(webView: webView)
|
|
236
|
+
self.fpsMonitor?.start()
|
|
237
|
+
}
|
|
238
|
+
#endif
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func stopPerfMonitoring() {
|
|
242
|
+
fpsMonitor?.stop()
|
|
243
|
+
fpsMonitor = nil
|
|
192
244
|
}
|
|
193
245
|
|
|
194
246
|
@objc func handleCameraPinch(_ gesture: UIPinchGestureRecognizer) {
|
|
@@ -218,9 +270,83 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
218
270
|
|
|
219
271
|
deinit {
|
|
220
272
|
// Ensure observer is removed even if dismantleUIView is skipped.
|
|
273
|
+
CatalystPerf.reset()
|
|
274
|
+
stopPerfMonitoring()
|
|
275
|
+
stopKeyboardPerfTracking()
|
|
221
276
|
if isObserverAdded, let webView = nativeBridge?.webView {
|
|
222
277
|
webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
|
|
223
278
|
}
|
|
224
279
|
}
|
|
225
280
|
}
|
|
226
281
|
}
|
|
282
|
+
|
|
283
|
+
extension WebView.Coordinator: UIScrollViewDelegate {
|
|
284
|
+
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
285
|
+
guard !scrollSessionOpen else { return }
|
|
286
|
+
scrollSessionOpen = true
|
|
287
|
+
CatalystPerf.emit([
|
|
288
|
+
"type": "scroll-start",
|
|
289
|
+
"nativeTime": CatalystPerf.nativeTimeMs(),
|
|
290
|
+
], to: perfWebView)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
294
|
+
if !decelerate {
|
|
295
|
+
closeScrollSession()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
300
|
+
closeScrollSession()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private func closeScrollSession() {
|
|
304
|
+
guard scrollSessionOpen else { return }
|
|
305
|
+
scrollSessionOpen = false
|
|
306
|
+
CatalystPerf.emit([
|
|
307
|
+
"type": "scroll-end",
|
|
308
|
+
"nativeTime": CatalystPerf.nativeTimeMs(),
|
|
309
|
+
], to: perfWebView)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func startKeyboardPerfTracking(_ webView: WKWebView) {
|
|
313
|
+
perfWebView = webView
|
|
314
|
+
stopKeyboardPerfTracking()
|
|
315
|
+
|
|
316
|
+
let center = NotificationCenter.default
|
|
317
|
+
keyboardObserverTokens = [
|
|
318
|
+
center.addObserver(
|
|
319
|
+
forName: UIResponder.keyboardWillShowNotification,
|
|
320
|
+
object: nil,
|
|
321
|
+
queue: .main
|
|
322
|
+
) { [weak self] notification in
|
|
323
|
+
self?.emitKeyboardEvent("keyboard-show", notification: notification)
|
|
324
|
+
},
|
|
325
|
+
center.addObserver(
|
|
326
|
+
forName: UIResponder.keyboardWillHideNotification,
|
|
327
|
+
object: nil,
|
|
328
|
+
queue: .main
|
|
329
|
+
) { [weak self] notification in
|
|
330
|
+
self?.emitKeyboardEvent("keyboard-hide", notification: notification)
|
|
331
|
+
},
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func stopKeyboardPerfTracking() {
|
|
336
|
+
let center = NotificationCenter.default
|
|
337
|
+
keyboardObserverTokens.forEach { center.removeObserver($0) }
|
|
338
|
+
keyboardObserverTokens.removeAll()
|
|
339
|
+
perfWebView = nil
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private func emitKeyboardEvent(_ type: String, notification: Notification) {
|
|
343
|
+
let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
|
|
344
|
+
let durationSeconds = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
|
|
345
|
+
CatalystPerf.emit([
|
|
346
|
+
"type": type,
|
|
347
|
+
"nativeTime": CatalystPerf.nativeTimeMs(),
|
|
348
|
+
"keyboardHeight": frame?.height ?? 0,
|
|
349
|
+
"durationMs": Int((durationSeconds ?? 0) * 1000),
|
|
350
|
+
], to: perfWebView)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -13,6 +13,9 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
13
13
|
private var lastTargetURL: URL?
|
|
14
14
|
private let initialURL: URL?
|
|
15
15
|
private weak var cameraManager: NativeCameraManager?
|
|
16
|
+
private var pageLoadStartMs: Int64?
|
|
17
|
+
private var didEmitColdStart = false
|
|
18
|
+
private var hasStartedProfilerNavigation = false
|
|
16
19
|
|
|
17
20
|
init(viewModel: WebViewModel, initialURL: URL?, cameraManager: NativeCameraManager? = nil) {
|
|
18
21
|
self.viewModel = viewModel
|
|
@@ -155,15 +158,26 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
if isCacheableMethod && CacheManager.shared.shouldCacheURL(url) {
|
|
158
|
-
|
|
161
|
+
logger.info("🎯 URL matches cache pattern: \(url.absoluteString)")
|
|
159
162
|
|
|
163
|
+
let cacheStartMs = CatalystPerf.nativeTimeMs()
|
|
160
164
|
let (cachedData, cacheState, mimeType) = await CacheManager.shared.getCachedResource(
|
|
161
165
|
for: navigationAction.request
|
|
162
166
|
)
|
|
167
|
+
let cacheDurationMs = CatalystPerf.nativeTimeMs() - cacheStartMs
|
|
163
168
|
|
|
164
169
|
switch cacheState {
|
|
165
170
|
case .fresh, .stale:
|
|
166
171
|
logger.info("✅ Serving fresh/stale cached content")
|
|
172
|
+
CatalystPerf.add([
|
|
173
|
+
"type": "cache-hit-memory",
|
|
174
|
+
"url": url.absoluteString,
|
|
175
|
+
"resourceType": "document",
|
|
176
|
+
"nativeTime": cacheStartMs,
|
|
177
|
+
"nativeStartMs": cacheStartMs,
|
|
178
|
+
"durationMs": cacheDurationMs,
|
|
179
|
+
"source": cacheState == .fresh ? "fresh" : "stale",
|
|
180
|
+
])
|
|
167
181
|
|
|
168
182
|
if let cachedData = cachedData,
|
|
169
183
|
let mimeType = mimeType {
|
|
@@ -182,6 +196,14 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
182
196
|
|
|
183
197
|
case .expired:
|
|
184
198
|
logger.info("♻️ Cache expired, fetching fresh content")
|
|
199
|
+
CatalystPerf.add([
|
|
200
|
+
"type": "cache-miss-fetch",
|
|
201
|
+
"url": url.absoluteString,
|
|
202
|
+
"resourceType": "document",
|
|
203
|
+
"nativeTime": cacheStartMs,
|
|
204
|
+
"nativeStartMs": cacheStartMs,
|
|
205
|
+
"durationMs": cacheDurationMs,
|
|
206
|
+
])
|
|
185
207
|
break
|
|
186
208
|
}
|
|
187
209
|
} else {
|
|
@@ -235,6 +257,25 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
235
257
|
|
|
236
258
|
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
|
237
259
|
logWithTimestamp("📡 didStartProvisionalNavigation - loading started")
|
|
260
|
+
if hasStartedProfilerNavigation {
|
|
261
|
+
CatalystPerf.reset()
|
|
262
|
+
}
|
|
263
|
+
hasStartedProfilerNavigation = true
|
|
264
|
+
let nativeNow = CatalystPerf.nativeTimeMs()
|
|
265
|
+
pageLoadStartMs = nativeNow
|
|
266
|
+
CatalystPerf.injectNativeTimeOffset(into: webView)
|
|
267
|
+
if let url = webView.url?.absoluteString ?? lastTargetURL?.absoluteString {
|
|
268
|
+
CatalystPerf.add([
|
|
269
|
+
"type": "boot-page-started",
|
|
270
|
+
"nativeTime": nativeNow,
|
|
271
|
+
"url": url,
|
|
272
|
+
])
|
|
273
|
+
CatalystPerf.add([
|
|
274
|
+
"type": "page-load-start",
|
|
275
|
+
"nativeTime": nativeNow,
|
|
276
|
+
"url": url,
|
|
277
|
+
])
|
|
278
|
+
}
|
|
238
279
|
// Stop camera on navigation — mirrors Android CustomWebview onPageStarted lambda
|
|
239
280
|
cameraManager?.stop()
|
|
240
281
|
Task { @MainActor in
|
|
@@ -261,6 +302,32 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
261
302
|
|
|
262
303
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
263
304
|
logWithTimestamp("🎉 didFinish - page fully loaded")
|
|
305
|
+
let nativeNow = CatalystPerf.nativeTimeMs()
|
|
306
|
+
let url = webView.url?.absoluteString ?? lastTargetURL?.absoluteString ?? ""
|
|
307
|
+
let durationMs = pageLoadStartMs.map { nativeNow - $0 } ?? 0
|
|
308
|
+
CatalystPerf.add([
|
|
309
|
+
"type": "boot-page-finished",
|
|
310
|
+
"nativeTime": nativeNow,
|
|
311
|
+
"url": url,
|
|
312
|
+
])
|
|
313
|
+
CatalystPerf.add([
|
|
314
|
+
"type": "page-load-end",
|
|
315
|
+
"nativeTime": nativeNow,
|
|
316
|
+
"url": url,
|
|
317
|
+
"durationMs": durationMs,
|
|
318
|
+
])
|
|
319
|
+
if !didEmitColdStart {
|
|
320
|
+
didEmitColdStart = true
|
|
321
|
+
CatalystPerf.add([
|
|
322
|
+
"type": "cold-start",
|
|
323
|
+
"nativeTime": nativeNow,
|
|
324
|
+
"url": url,
|
|
325
|
+
"durationMs": nativeNow - AppBoot.nativeStartMs,
|
|
326
|
+
])
|
|
327
|
+
}
|
|
328
|
+
CatalystPerf.memorySnapshot(to: nil, label: "page-finish")
|
|
329
|
+
CatalystPerf.scheduleFlush(webView)
|
|
330
|
+
|
|
264
331
|
Task { @MainActor in
|
|
265
332
|
if let url = webView.url {
|
|
266
333
|
if !isOfflinePageURL(url) {
|
|
@@ -287,6 +354,13 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
287
354
|
}
|
|
288
355
|
|
|
289
356
|
private func handleNavigationError(_ error: Error, webView: WKWebView?) {
|
|
357
|
+
CatalystPerf.emit([
|
|
358
|
+
"type": "page-load-error",
|
|
359
|
+
"nativeTime": CatalystPerf.nativeTimeMs(),
|
|
360
|
+
"url": webView?.url?.absoluteString ?? lastTargetURL?.absoluteString ?? "",
|
|
361
|
+
"description": error.localizedDescription,
|
|
362
|
+
], to: webView)
|
|
363
|
+
|
|
290
364
|
Task { @MainActor in
|
|
291
365
|
viewModel.reset()
|
|
292
366
|
logWithTimestamp("🔴 Navigation error: \(error.localizedDescription)")
|
package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift
CHANGED
|
@@ -386,6 +386,19 @@ final class BridgeMessageValidatorTests: XCTestCase {
|
|
|
386
386
|
XCTAssertEqual(result.command, "openCamera")
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
func testTraceExportAvailabilityRequiresDebugProfiler() {
|
|
390
|
+
XCTAssertTrue(CatalystConstants.Bridge.isTraceExportEnabled(debugBuild: true, profilerEnabled: true))
|
|
391
|
+
XCTAssertFalse(CatalystConstants.Bridge.isTraceExportEnabled(debugBuild: false, profilerEnabled: true))
|
|
392
|
+
XCTAssertFalse(CatalystConstants.Bridge.isTraceExportEnabled(debugBuild: true, profilerEnabled: false))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func testTraceExportAllowlistMatchesAvailabilityGate() {
|
|
396
|
+
XCTAssertEqual(
|
|
397
|
+
CatalystConstants.Bridge.validCommands.contains("exportCatalystTrace"),
|
|
398
|
+
CatalystConstants.Bridge.isTraceExportEnabled()
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
389
402
|
// ========================================
|
|
390
403
|
// MARK: - Helper Methods
|
|
391
404
|
// ========================================
|
|
@@ -1 +1,227 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
class PluginNativeBridge {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.handlers = new Map();
|
|
10
|
+
this.requestCount = 0;
|
|
11
|
+
this.isInitialized = false;
|
|
12
|
+
}
|
|
13
|
+
createRequestId = () => {
|
|
14
|
+
this.requestCount += 1;
|
|
15
|
+
return `plugin_${Date.now()}_${this.requestCount}`;
|
|
16
|
+
};
|
|
17
|
+
normalizeRequiredString = (value, fieldName) => {
|
|
18
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
19
|
+
throw new Error(`${fieldName} must be a non-empty string`);
|
|
20
|
+
}
|
|
21
|
+
return value.trim();
|
|
22
|
+
};
|
|
23
|
+
normalizeCommand = command => {
|
|
24
|
+
if (command == null) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return this.normalizeRequiredString(command, "command");
|
|
28
|
+
};
|
|
29
|
+
getHandlerKey = ({
|
|
30
|
+
pluginId,
|
|
31
|
+
eventName,
|
|
32
|
+
command = null
|
|
33
|
+
} = {}) => {
|
|
34
|
+
return JSON.stringify([this.normalizeRequiredString(pluginId, "pluginId"), this.normalizeRequiredString(eventName, "eventName"), this.normalizeCommand(command)]);
|
|
35
|
+
};
|
|
36
|
+
getHandlerSet = (key, create = false) => {
|
|
37
|
+
let handlers = this.handlers.get(key);
|
|
38
|
+
if (!handlers && create) {
|
|
39
|
+
handlers = new Set();
|
|
40
|
+
this.handlers.set(key, handlers);
|
|
41
|
+
}
|
|
42
|
+
return handlers || null;
|
|
43
|
+
};
|
|
44
|
+
addHandler = ({
|
|
45
|
+
pluginId,
|
|
46
|
+
eventName,
|
|
47
|
+
command
|
|
48
|
+
} = {}, handler) => {
|
|
49
|
+
if (typeof handler !== "function") {
|
|
50
|
+
throw new Error("handler must be a function");
|
|
51
|
+
}
|
|
52
|
+
this.getHandlerSet(this.getHandlerKey({
|
|
53
|
+
pluginId,
|
|
54
|
+
eventName,
|
|
55
|
+
command
|
|
56
|
+
}), true).add(handler);
|
|
57
|
+
};
|
|
58
|
+
removeHandler = ({
|
|
59
|
+
pluginId,
|
|
60
|
+
eventName,
|
|
61
|
+
command
|
|
62
|
+
} = {}, handler) => {
|
|
63
|
+
const key = this.getHandlerKey({
|
|
64
|
+
pluginId,
|
|
65
|
+
eventName,
|
|
66
|
+
command
|
|
67
|
+
});
|
|
68
|
+
const handlers = this.getHandlerSet(key, false);
|
|
69
|
+
if (!handlers) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (handler == null) {
|
|
73
|
+
handlers.clear();
|
|
74
|
+
this.handlers.delete(key);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const removed = handlers.delete(handler);
|
|
78
|
+
if (handlers.size === 0) {
|
|
79
|
+
this.handlers.delete(key);
|
|
80
|
+
}
|
|
81
|
+
return removed;
|
|
82
|
+
};
|
|
83
|
+
ensureInitialized = () => {
|
|
84
|
+
if (typeof window === "undefined" || this.isInitialized) {
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
window.PluginBridgeWeb = window.PluginBridgeWeb || {};
|
|
88
|
+
window.PluginBridgeWeb.callback = this.dispatchCallback;
|
|
89
|
+
window.PluginBridgeWeb.dispatch = message => {
|
|
90
|
+
let parsed = message;
|
|
91
|
+
if (typeof parsed === "string") {
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(parsed);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!parsed || typeof parsed !== "object") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const payload = Object.prototype.hasOwnProperty.call(parsed, "payload") ? parsed.payload : null;
|
|
102
|
+
return this.dispatchCallback(parsed.pluginId, parsed.eventName, payload, parsed.requestId ?? null, parsed.command ?? null);
|
|
103
|
+
};
|
|
104
|
+
this.isInitialized = true;
|
|
105
|
+
return this;
|
|
106
|
+
};
|
|
107
|
+
hasAndroidBridge = () => typeof window !== "undefined" && !!window.PluginBridge;
|
|
108
|
+
hasIOSBridge = () => typeof window !== "undefined" && !!window.webkit?.messageHandlers?.PluginBridge;
|
|
109
|
+
init = () => this.ensureInitialized();
|
|
110
|
+
assertInitialized = () => {
|
|
111
|
+
if (!this.isInitialized) {
|
|
112
|
+
throw new Error("PluginBridge.init() must be called before using emit() or register()");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
reportHandlerError = error => {
|
|
116
|
+
if (typeof queueMicrotask === "function") {
|
|
117
|
+
queueMicrotask(() => {
|
|
118
|
+
throw error;
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
throw error;
|
|
124
|
+
}, 0);
|
|
125
|
+
};
|
|
126
|
+
emit = ({
|
|
127
|
+
pluginId,
|
|
128
|
+
command,
|
|
129
|
+
data = null
|
|
130
|
+
} = {}) => {
|
|
131
|
+
const normalizedPluginId = this.normalizeRequiredString(pluginId, "pluginId");
|
|
132
|
+
const normalizedCommand = this.normalizeRequiredString(command, "command");
|
|
133
|
+
if (typeof window === "undefined") {
|
|
134
|
+
throw new Error("PluginBridge is not available in this environment");
|
|
135
|
+
}
|
|
136
|
+
this.assertInitialized();
|
|
137
|
+
const payload = {
|
|
138
|
+
pluginId: normalizedPluginId,
|
|
139
|
+
command: normalizedCommand,
|
|
140
|
+
data,
|
|
141
|
+
requestId: this.createRequestId()
|
|
142
|
+
};
|
|
143
|
+
if (this.hasAndroidBridge()) {
|
|
144
|
+
window.PluginBridge.emit(JSON.stringify(payload));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (this.hasIOSBridge()) {
|
|
148
|
+
window.webkit.messageHandlers.PluginBridge.postMessage(payload);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throw new Error("PluginBridge is not available in this environment");
|
|
152
|
+
};
|
|
153
|
+
register = ({
|
|
154
|
+
pluginId,
|
|
155
|
+
eventName,
|
|
156
|
+
command,
|
|
157
|
+
handler
|
|
158
|
+
} = {}) => {
|
|
159
|
+
this.assertInitialized();
|
|
160
|
+
this.addHandler({
|
|
161
|
+
pluginId,
|
|
162
|
+
eventName,
|
|
163
|
+
command
|
|
164
|
+
}, handler);
|
|
165
|
+
return () => this.removeHandler({
|
|
166
|
+
pluginId,
|
|
167
|
+
eventName,
|
|
168
|
+
command
|
|
169
|
+
}, handler);
|
|
170
|
+
};
|
|
171
|
+
unregister = ({
|
|
172
|
+
pluginId,
|
|
173
|
+
eventName,
|
|
174
|
+
command,
|
|
175
|
+
handler
|
|
176
|
+
} = {}) => {
|
|
177
|
+
if (typeof handler !== "function") {
|
|
178
|
+
throw new Error("handler must be a function");
|
|
179
|
+
}
|
|
180
|
+
return this.removeHandler({
|
|
181
|
+
pluginId,
|
|
182
|
+
eventName,
|
|
183
|
+
command
|
|
184
|
+
}, handler);
|
|
185
|
+
};
|
|
186
|
+
dispatchCallback = (pluginId, eventName, payload, requestId = null, command = null) => {
|
|
187
|
+
void requestId;
|
|
188
|
+
const normalizedPluginId = typeof pluginId === "string" && pluginId.trim() ? pluginId.trim() : null;
|
|
189
|
+
const normalizedEventName = typeof eventName === "string" && eventName.trim() ? eventName.trim() : null;
|
|
190
|
+
if (normalizedPluginId == null || normalizedEventName == null) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const normalizedCommand = typeof command === "string" && command.trim() ? command.trim() : null;
|
|
194
|
+
const toCall = new Set();
|
|
195
|
+
const collectHandlers = commandScope => {
|
|
196
|
+
const handlers = this.getHandlerSet(JSON.stringify([normalizedPluginId, normalizedEventName, commandScope]), false);
|
|
197
|
+
if (!handlers) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
handlers.forEach(handler => {
|
|
201
|
+
toCall.add(handler);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
if (normalizedCommand != null) {
|
|
205
|
+
collectHandlers(normalizedCommand);
|
|
206
|
+
}
|
|
207
|
+
collectHandlers(null);
|
|
208
|
+
if (toCall.size === 0) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
const meta = {
|
|
212
|
+
pluginId: normalizedPluginId,
|
|
213
|
+
eventName: normalizedEventName,
|
|
214
|
+
command: normalizedCommand
|
|
215
|
+
};
|
|
216
|
+
toCall.forEach(handler => {
|
|
217
|
+
try {
|
|
218
|
+
handler(payload, meta);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
this.reportHandlerError(error);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return true;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const pluginNativeBridge = new PluginNativeBridge();
|
|
227
|
+
var _default = exports.default = pluginNativeBridge;
|