catalyst-core 0.2.0-beta.3 → 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 (160) 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/androidSetup.js +300 -8
  8. package/dist/native/bridge/WebBridge.js +481 -59
  9. package/dist/native/bridge/constants/NativeInterfaces.js +290 -45
  10. package/dist/native/bridge/errors.js +250 -26
  11. package/dist/native/bridge/hooks/index.js +125 -1
  12. package/dist/native/bridge/hooks/useCamera.js +347 -9
  13. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  14. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  15. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  16. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  17. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  18. package/dist/native/bridge/hooks/useIntent.js +95 -1
  19. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  20. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  21. package/dist/native/bridge/hooks/useNotification.js +318 -2
  22. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  23. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  24. package/dist/native/bridge/hooks/utils.js +27 -1
  25. package/dist/native/bridge/hooks.js +16 -1
  26. package/dist/native/bridge/perf/collectors/ApiCollector.js +66 -3
  27. package/dist/native/bridge/perf/collectors/CacheCollector.js +186 -11
  28. package/dist/native/bridge/perf/collectors/HookCollector.js +141 -19
  29. package/dist/native/bridge/perf/collectors/InsightsCollector.js +481 -23
  30. package/dist/native/bridge/perf/collectors/InteractionCollector.js +245 -10
  31. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +107 -8
  32. package/dist/native/bridge/perf/collectors/NavigationCollector.js +273 -32
  33. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +178 -2
  34. package/dist/native/bridge/perf/collectors/RenderCollector.js +258 -14
  35. package/dist/native/bridge/perf/collectors/ScrollCollector.js +95 -9
  36. package/dist/native/bridge/perf/collectors/ThreadCollector.js +90 -6
  37. package/dist/native/bridge/perf/core/constants.js +103 -20
  38. package/dist/native/bridge/perf/core/measure.js +179 -30
  39. package/dist/native/bridge/perf/core/session.js +89 -23
  40. package/dist/native/bridge/perf/core/store.js +554 -1
  41. package/dist/native/bridge/perf/core/summary.js +174 -1
  42. package/dist/native/bridge/perf/core/traceExport.js +240 -1
  43. package/dist/native/bridge/perf/core/waterfalls.js +146 -1
  44. package/dist/native/bridge/perf/index.js +359 -38
  45. package/dist/native/bridge/safeArea.js +53 -5
  46. package/dist/native/bridge/standardizedHooks.js +595 -82
  47. package/dist/native/bridge/useBaseHook.js +274 -38
  48. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  49. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  50. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  51. package/dist/native/buildAndroid/assets.js +510 -13
  52. package/dist/native/buildAndroid/build.js +296 -19
  53. package/dist/native/buildAndroid/config.js +87 -1
  54. package/dist/native/buildAndroid/index.js +374 -1
  55. package/dist/native/buildAppAndroid.js +34 -2
  56. package/dist/native/buildAppIos.js +62 -2
  57. package/dist/native/buildIos/assets.js +609 -8
  58. package/dist/native/buildIos/build.js +639 -20
  59. package/dist/native/buildIos/config.js +294 -12
  60. package/dist/native/buildIos/index.js +343 -6
  61. package/dist/native/buildIos/plugins.js +343 -16
  62. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  63. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  64. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  65. package/dist/native/internalPluginUtils.js +232 -7
  66. package/dist/native/package.json +3 -0
  67. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  68. package/dist/native/pluginComposerAndroid.js +407 -23
  69. package/dist/native/pluginComposerIos.js +435 -11
  70. package/dist/native/renameAndroidProject.js +1094 -88
  71. package/dist/native/setupEmulatorIos.js +289 -22
  72. package/dist/native/setupServer.js +160 -19
  73. package/dist/native/terminalProgress.js +199 -11
  74. package/dist/native/utils.js +193 -15
  75. package/dist/offline/catalyst-sw.js +145 -1
  76. package/dist/offline/registerServiceWorker.js +33 -1
  77. package/dist/otel.js +375 -46
  78. package/dist/router/ClientRouter.js +14 -2
  79. package/dist/router/ServerRouter.js +9 -1
  80. package/dist/router.js +1 -1
  81. package/dist/scripts/build.js +112 -3
  82. package/dist/scripts/checkVersion.js +3 -1
  83. package/dist/scripts/cleanCache.js +30 -1
  84. package/dist/scripts/generateOfflineManifest.js +116 -1
  85. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  86. package/dist/scripts/plugins.js +309 -1
  87. package/dist/scripts/preServerInit.js +13 -0
  88. package/dist/scripts/scriptUtils.js +54 -3
  89. package/dist/scripts/serve.js +51 -4
  90. package/dist/scripts/start.js +47 -10
  91. package/dist/scripts/validator.js +149 -1
  92. package/dist/sentry.cjs +127 -0
  93. package/dist/sentry.js +125 -5
  94. package/dist/server/expressServer.js +250 -5
  95. package/dist/server/manifestCache.js +36 -0
  96. package/dist/server/renderer/ChunkExtractor.js +144 -0
  97. package/dist/server/renderer/document/Body.jsx +59 -0
  98. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  99. package/dist/server/renderer/document/Head.jsx +60 -0
  100. package/dist/server/renderer/document/index.jsx +2 -0
  101. package/dist/server/renderer/extract.js +149 -31
  102. package/dist/server/renderer/handler.jsx +433 -0
  103. package/dist/server/renderer/index.js +8 -1
  104. package/dist/server/renderer/render.js +47 -0
  105. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  106. package/dist/server/utils/cjsRequire.js +10 -0
  107. package/dist/server/utils/userAgentUtil.js +87 -9
  108. package/dist/server/utils/validator.js +164 -1
  109. package/dist/vite/FastRefresh.jsx +20 -0
  110. package/dist/vite/inject-cache-key-plugin.js +276 -0
  111. package/dist/vite/loadCustomViteConfig.js +22 -0
  112. package/dist/vite/manifest-categorization-plugin.js +455 -0
  113. package/dist/vite/node-loader.mjs +458 -0
  114. package/dist/vite/scssParams.js +21 -0
  115. package/dist/vite/vite.config.client.js +130 -0
  116. package/dist/vite/vite.config.js +422 -0
  117. package/dist/vite/vite.config.server.js +84 -0
  118. package/dist/web-router/components/MetaTag.jsx +34 -0
  119. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  120. package/dist/web-router/components/Split.jsx +200 -0
  121. package/dist/web-router/components/SplitInview.jsx +106 -0
  122. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  123. package/dist/web-router/context.jsx +12 -0
  124. package/dist/web-router/hooks.jsx +72 -0
  125. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  126. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  127. package/dist/web-router/types/context.d.ts +14 -0
  128. package/dist/web-router/types/hooks.d.ts +1 -0
  129. package/dist/web-router/types/index.d.ts +3 -0
  130. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  131. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  132. package/mcp_v2/conversion-tasks.json +35 -17
  133. package/mcp_v2/knowledge-base.json +124 -13
  134. package/mcp_v2/lib/helpers.js +10 -0
  135. package/mcp_v2/mcp.js +4 -4
  136. package/mcp_v2/package.json +1 -0
  137. package/mcp_v2/tools/build.js +116 -20
  138. package/mcp_v2/tools/conversion.js +106 -5
  139. package/mcp_v2/tools/tasks.js +18 -14
  140. package/package.json +35 -68
  141. package/babel.config.js +0 -32
  142. package/dist/index.js +0 -1
  143. package/dist/scripts/devBuild.js +0 -3
  144. package/dist/scripts/devServe.js +0 -3
  145. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  146. package/dist/scripts/registerAliases.js +0 -6
  147. package/dist/server/renderer/document/Body.js +0 -11
  148. package/dist/server/renderer/document/Head.js +0 -9
  149. package/dist/server/renderer/document/index.js +0 -1
  150. package/dist/server/renderer/handler.js +0 -47
  151. package/dist/server/startServer.js +0 -57
  152. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  153. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  154. package/dist/webpack/babel.config.client.js +0 -1
  155. package/dist/webpack/babel.config.ssr.js +0 -1
  156. package/dist/webpack/base.babel.js +0 -7
  157. package/dist/webpack/development.client.babel.js +0 -10
  158. package/dist/webpack/production.client.babel.js +0 -4
  159. package/dist/webpack/production.ssr.babel.js +0 -8
  160. package/dist/webpack/scssParams.js +0 -9
@@ -1,4 +1,12 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NavigationCollector=void 0;var _constants=require("../core/constants.js");var _session=require("../core/session.js");/**
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NavigationCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ var _session = require("../core/session.js");
9
+ /**
2
10
  * NavigationCollector.js
3
11
  *
4
12
  * Instruments: bugs #2, #7, #9, #11, #14
@@ -20,34 +28,267 @@
20
28
  * catalyst:boot/load-url — batch: WebView.loadUrl() call (Track: Navigation)
21
29
  * catalyst:boot/page-started — batch: onPageStarted callback (Track: Navigation)
22
30
  * catalyst:boot/page-finished — batch: onPageFinished callback (Track: Navigation)
23
- */class NavigationCollector{constructor(measure,nativeToWeb){this._measure=measure;this._nativeToWeb=nativeToWeb??(t=>t);this._session=new _session.Session(_constants.PREFIX.SESSION_NAV,measure,_constants.TRACK.NAVIGATION);this._pendingLcp=null;// { entry, label } — buffered until finalized
24
- this._lcpFinalized=false;// Track pending native page-load spans: url → { markName, startWeb }
25
- this._pendingPageLoads=new Map();}init(){this._session.open();this._observeLCP();this._observeRouteTransitions();this._setupLcpFinalization();}/** Called by index.js to feed the normalized perf store. */setWaterfallSource(fn){this._notifyWaterfall=fn;}// Called by other collectors to check if nav is still loading
26
- get isLoading(){return this._session.isOpen;}// ─── LCP ──────────────────────────────────────────────────────────────────
27
- _observeLCP(){const types=PerformanceObserver.supportedEntryTypes??[];const type=types.includes("largest-contentful-paint")?"largest-contentful-paint":types.includes("paint")?"paint":null;if(!type)return;const obs=new PerformanceObserver(list=>{for(const entry of list.getEntries()){if(type==="paint"&&entry.name!=="first-contentful-paint")continue;const label=type==="largest-contentful-paint"?"lcp":"fcp";this._pendingLcp={entry,label};// overwrite — keep latest candidate
28
- }});obs.observe({type,buffered:true});}_setupLcpFinalization(){// Browser stops updating LCP candidate on first user interaction or page hide
29
- const finalize=()=>{this._finalizeLcp();document.removeEventListener("pointerdown",finalize);document.removeEventListener("keydown",finalize);};document.addEventListener("pointerdown",finalize);document.addEventListener("keydown",finalize);document.addEventListener("visibilitychange",()=>{if(document.visibilityState==="hidden")this._finalizeLcp();});}_finalizeLcp(){if(this._lcpFinalized||!this._pendingLcp)return;this._lcpFinalized=true;const{entry,label}=this._pendingLcp;this._pendingLcp=null;const markName=`${_constants.PREFIX[label.toUpperCase()]}:mark`;performance.mark(markName,{startTime:0});this._measure.emit(`${_constants.PREFIX[label.toUpperCase()]}|${Math.round(entry.startTime)}ms`,markName,entry.startTime,{renderTime:entry.renderTime??null,loadTime:entry.loadTime??null,simulatorValid:false},_constants.TRACK.NAVIGATION);this._notifyWaterfall?.({label:label.toUpperCase(),startTime:0,endTime:entry.startTime,durationMs:Math.round(entry.startTime)});// LCP finalized = page is loaded — close the navigation session
30
- this._session.close({simulatorValid:false},entry.startTime);}// ─── Route transitions ────────────────────────────────────────────────────
31
- // Detects SPA route changes via popstate + hashchange.
32
- // Emits a span from event fire → next rAF (visual settle).
33
- //
34
- // Covers:
35
- // #7 — if rAF never fires cleanly, transition span shows the gap
36
- // #9 — span fires on back but not on some forward routes → missing spans = broken routes
37
- // #11 — nav session re-opens; layout-shift collector shows shift after session close
38
- _observeRouteTransitions(){let _fromPath=location.pathname+location.hash;const onTransition=e=>{const from=_fromPath;const to=location.pathname+location.hash;_fromPath=to;const startTime=performance.now();const markName=`${_constants.PREFIX.ROUTE_TRANSITION}:start:${Math.round(startTime)}`;performance.mark(markName,{startTime});// Re-open nav session for the SPA route change.
39
- // LCP only fires once per WebView load — it does not reset on pushState.
40
- // So we can't wait for LCP to close the session. Instead, close it after
41
- // the next rAF (visual settle) + a short timeout to catch deferred renders.
42
- if(!this._session.isOpen){this._lcpFinalized=false;this._session.open({},startTime);this._setupLcpFinalization();// Fallback: if LCP doesn't fire (SPA), close after rAF + 1s
43
- requestAnimationFrame(()=>{setTimeout(()=>{if(this._session.isOpen&&!this._lcpFinalized){this._session.close({simulatorValid:true},performance.now());}},1000);});}requestAnimationFrame(()=>{const endTime=performance.now();this._measure.emit(`${_constants.PREFIX.ROUTE_TRANSITION}|${from}→${to}`,markName,endTime,{from,to,type:e.type,// 'popstate' or 'hashchange'
44
- simulatorValid:true},_constants.TRACK.NAVIGATION);this._notifyWaterfall?.({label:`Route ${to}`,startTime,endTime,durationMs:Math.round(endTime-startTime)});});};window.addEventListener("popstate",onTransition);window.addEventListener("hashchange",onTransition);}// ─── Native page-load events (from CustomWebview.kt) ─────────────────────
45
- // page-load-start: fired by onPageStarted, carries nativeTime + url
46
- // page-load-end: fired by onPageFinished, carries nativeTime + url + durationMs
47
- // page-load-error: fired by onReceivedError, carries nativeTime + url + errorCode
48
- onNativeEvent(event){switch(event.type){case"page-load-start":{const startWeb=this._nativeToWeb(event.nativeTime);const markName=`${_constants.PREFIX.PAGE_LOAD}:start:${Math.round(startWeb)}`;performance.mark(markName,{startTime:startWeb});this._pendingPageLoads.set(event.url,{markName,startWeb});break;}case"page-load-end":{const pending=this._pendingPageLoads.get(event.url);if(!pending)break;this._pendingPageLoads.delete(event.url);const endWeb=this._nativeToWeb(event.nativeTime);const durMs=event.durationMs??Math.round(endWeb-pending.startWeb);const isSlow=durMs>2500;this._measure.emit(`${_constants.PREFIX.PAGE_LOAD}|${event.url.split("/").pop()||event.url}`,pending.markName,endWeb,{url:event.url,durationMs:durMs,simulatorValid:true},_constants.TRACK.NAVIGATION,isSlow?"error":"primary");break;}case"page-load-error":{const pending=this._pendingPageLoads.get(event.url);const startWeb=pending?pending.startWeb:this._nativeToWeb(event.nativeTime);const markName=pending?pending.markName:`${_constants.PREFIX.PAGE_LOAD}:err:${Math.round(startWeb)}`;if(!pending)performance.mark(markName,{startTime:startWeb});this._pendingPageLoads.delete(event.url);const errEnd=this._nativeToWeb(event.nativeTime)+1;this._measure.emit(`${_constants.PREFIX.PAGE_LOAD}/error|${event.errorCode??"?"}`,markName,errEnd,{url:event.url,errorCode:event.errorCode,description:event.description??"",simulatorValid:true},_constants.TRACK.NAVIGATION,"error");break;}case"cold-start":{// emitted by markAppStartComplete() — point-in-time span at page init
49
- const startWeb=this._nativeToWeb(event.nativeTime);const markName=`${_constants.PREFIX.COLD_START}:mark`;performance.mark(markName,{startTime:Math.max(0,startWeb-event.durationMs)});this._measure.emit(`${_constants.PREFIX.COLD_START}|${event.durationMs}ms`,markName,startWeb,{durationMs:event.durationMs,simulatorValid:true},_constants.TRACK.NAVIGATION,event.durationMs>3000?"error":"secondary");break;}}}// ─── Batch boot timing events ─────────────────────────────────────────────
50
- // Emitted from PerfEventBuffer flush after onPageFinished.
51
- // Each boot-* event is a point-in-time marker on Catalyst > Navigation.
52
- onBatchEvent(event){if(event.type==="navigation-back"){const t=this._nativeToWeb(event.nativeTime);const markName=`catalyst:navigation/back:${Math.round(t)}`;performance.mark(markName,{startTime:Math.max(0,t)});this._measure.emit("catalyst:navigation/back",markName,Math.max(0,t)+1,{simulatorValid:false},_constants.TRACK.NAVIGATION,_constants.COLOR.SECONDARY);return;}const bootTypeMap={"boot-activity-created":"activity-created","boot-webview-constructed":"webview-constructed","boot-load-url":"load-url","boot-page-started":"page-started","boot-page-finished":"page-finished"};const label=bootTypeMap[event.type];if(!label)return;const t=this._nativeToWeb(event.nativeTime);const markName=`catalyst:boot/${label}:${Math.round(t)}`;performance.mark(markName,{startTime:Math.max(0,t)});this._measure.emit(`catalyst:boot/${label}`,markName,Math.max(0,t)+1,// point span
53
- {nativeTime:event.nativeTime,thread:event.thread??null,simulatorValid:false},_constants.TRACK.NAVIGATION,_constants.COLOR.SECONDARY_LIGHT);}}exports.NavigationCollector=NavigationCollector;
31
+ */
32
+
33
+ class NavigationCollector {
34
+ constructor(measure, nativeToWeb) {
35
+ this._measure = measure;
36
+ this._nativeToWeb = nativeToWeb ?? (t => t);
37
+ this._session = new _session.Session(_constants.PREFIX.SESSION_NAV, measure, _constants.TRACK.NAVIGATION);
38
+ this._pendingLcp = null; // { entry, label } — buffered until finalized
39
+ this._lcpFinalized = false;
40
+ // Track pending native page-load spans: url → { markName, startWeb }
41
+ this._pendingPageLoads = new Map();
42
+ }
43
+ init() {
44
+ this._session.open();
45
+ this._observeLCP();
46
+ this._observeRouteTransitions();
47
+ this._setupLcpFinalization();
48
+ }
49
+
50
+ /** Called by index.js to feed the normalized perf store. */
51
+ setWaterfallSource(fn) {
52
+ this._notifyWaterfall = fn;
53
+ }
54
+
55
+ // Called by other collectors to check if nav is still loading
56
+ get isLoading() {
57
+ return this._session.isOpen;
58
+ }
59
+
60
+ // ─── LCP ──────────────────────────────────────────────────────────────────
61
+
62
+ _observeLCP() {
63
+ const types = PerformanceObserver.supportedEntryTypes ?? [];
64
+ const type = types.includes("largest-contentful-paint") ? "largest-contentful-paint" : types.includes("paint") ? "paint" : null;
65
+ if (!type) return;
66
+ const obs = new PerformanceObserver(list => {
67
+ for (const entry of list.getEntries()) {
68
+ if (type === "paint" && entry.name !== "first-contentful-paint") continue;
69
+ const label = type === "largest-contentful-paint" ? "lcp" : "fcp";
70
+ this._pendingLcp = {
71
+ entry,
72
+ label
73
+ }; // overwrite — keep latest candidate
74
+ }
75
+ });
76
+ obs.observe({
77
+ type,
78
+ buffered: true
79
+ });
80
+ }
81
+ _setupLcpFinalization() {
82
+ // Browser stops updating LCP candidate on first user interaction or page hide
83
+ const finalize = () => {
84
+ this._finalizeLcp();
85
+ document.removeEventListener("pointerdown", finalize);
86
+ document.removeEventListener("keydown", finalize);
87
+ };
88
+ document.addEventListener("pointerdown", finalize);
89
+ document.addEventListener("keydown", finalize);
90
+ document.addEventListener("visibilitychange", () => {
91
+ if (document.visibilityState === "hidden") this._finalizeLcp();
92
+ });
93
+ }
94
+ _finalizeLcp() {
95
+ if (this._lcpFinalized || !this._pendingLcp) return;
96
+ this._lcpFinalized = true;
97
+ const {
98
+ entry,
99
+ label
100
+ } = this._pendingLcp;
101
+ this._pendingLcp = null;
102
+ const markName = `${_constants.PREFIX[label.toUpperCase()]}:mark`;
103
+ performance.mark(markName, {
104
+ startTime: 0
105
+ });
106
+ this._measure.emit(`${_constants.PREFIX[label.toUpperCase()]}|${Math.round(entry.startTime)}ms`, markName, entry.startTime, {
107
+ renderTime: entry.renderTime ?? null,
108
+ loadTime: entry.loadTime ?? null,
109
+ simulatorValid: false
110
+ }, _constants.TRACK.NAVIGATION);
111
+ this._notifyWaterfall?.({
112
+ label: label.toUpperCase(),
113
+ startTime: 0,
114
+ endTime: entry.startTime,
115
+ durationMs: Math.round(entry.startTime)
116
+ });
117
+
118
+ // LCP finalized = page is loaded — close the navigation session
119
+ this._session.close({
120
+ simulatorValid: false
121
+ }, entry.startTime);
122
+ }
123
+
124
+ // ─── Route transitions ────────────────────────────────────────────────────
125
+ // Detects SPA route changes via popstate + hashchange.
126
+ // Emits a span from event fire → next rAF (visual settle).
127
+ //
128
+ // Covers:
129
+ // #7 — if rAF never fires cleanly, transition span shows the gap
130
+ // #9 — span fires on back but not on some forward routes → missing spans = broken routes
131
+ // #11 — nav session re-opens; layout-shift collector shows shift after session close
132
+
133
+ _observeRouteTransitions() {
134
+ let _fromPath = location.pathname + location.hash;
135
+ const onTransition = e => {
136
+ const from = _fromPath;
137
+ const to = location.pathname + location.hash;
138
+ _fromPath = to;
139
+ const startTime = performance.now();
140
+ const markName = `${_constants.PREFIX.ROUTE_TRANSITION}:start:${Math.round(startTime)}`;
141
+ performance.mark(markName, {
142
+ startTime
143
+ });
144
+
145
+ // Re-open nav session for the SPA route change.
146
+ // LCP only fires once per WebView load — it does not reset on pushState.
147
+ // So we can't wait for LCP to close the session. Instead, close it after
148
+ // the next rAF (visual settle) + a short timeout to catch deferred renders.
149
+ if (!this._session.isOpen) {
150
+ this._lcpFinalized = false;
151
+ this._session.open({}, startTime);
152
+ this._setupLcpFinalization();
153
+ // Fallback: if LCP doesn't fire (SPA), close after rAF + 1s
154
+ requestAnimationFrame(() => {
155
+ setTimeout(() => {
156
+ if (this._session.isOpen && !this._lcpFinalized) {
157
+ this._session.close({
158
+ simulatorValid: true
159
+ }, performance.now());
160
+ }
161
+ }, 1000);
162
+ });
163
+ }
164
+ requestAnimationFrame(() => {
165
+ const endTime = performance.now();
166
+ this._measure.emit(`${_constants.PREFIX.ROUTE_TRANSITION}|${from}→${to}`, markName, endTime, {
167
+ from,
168
+ to,
169
+ type: e.type,
170
+ // 'popstate' or 'hashchange'
171
+ simulatorValid: true
172
+ }, _constants.TRACK.NAVIGATION);
173
+ this._notifyWaterfall?.({
174
+ label: `Route ${to}`,
175
+ startTime,
176
+ endTime,
177
+ durationMs: Math.round(endTime - startTime)
178
+ });
179
+ });
180
+ };
181
+ window.addEventListener("popstate", onTransition);
182
+ window.addEventListener("hashchange", onTransition);
183
+ }
184
+
185
+ // ─── Native page-load events (from CustomWebview.kt) ─────────────────────
186
+ // page-load-start: fired by onPageStarted, carries nativeTime + url
187
+ // page-load-end: fired by onPageFinished, carries nativeTime + url + durationMs
188
+ // page-load-error: fired by onReceivedError, carries nativeTime + url + errorCode
189
+
190
+ onNativeEvent(event) {
191
+ switch (event.type) {
192
+ case "page-load-start":
193
+ {
194
+ const startWeb = this._nativeToWeb(event.nativeTime);
195
+ const markName = `${_constants.PREFIX.PAGE_LOAD}:start:${Math.round(startWeb)}`;
196
+ performance.mark(markName, {
197
+ startTime: startWeb
198
+ });
199
+ this._pendingPageLoads.set(event.url, {
200
+ markName,
201
+ startWeb
202
+ });
203
+ break;
204
+ }
205
+ case "page-load-end":
206
+ {
207
+ const pending = this._pendingPageLoads.get(event.url);
208
+ if (!pending) break;
209
+ this._pendingPageLoads.delete(event.url);
210
+ const endWeb = this._nativeToWeb(event.nativeTime);
211
+ const durMs = event.durationMs ?? Math.round(endWeb - pending.startWeb);
212
+ const isSlow = durMs > 2500;
213
+ this._measure.emit(`${_constants.PREFIX.PAGE_LOAD}|${event.url.split("/").pop() || event.url}`, pending.markName, endWeb, {
214
+ url: event.url,
215
+ durationMs: durMs,
216
+ simulatorValid: true
217
+ }, _constants.TRACK.NAVIGATION, isSlow ? "error" : "primary");
218
+ break;
219
+ }
220
+ case "page-load-error":
221
+ {
222
+ const pending = this._pendingPageLoads.get(event.url);
223
+ const startWeb = pending ? pending.startWeb : this._nativeToWeb(event.nativeTime);
224
+ const markName = pending ? pending.markName : `${_constants.PREFIX.PAGE_LOAD}:err:${Math.round(startWeb)}`;
225
+ if (!pending) performance.mark(markName, {
226
+ startTime: startWeb
227
+ });
228
+ this._pendingPageLoads.delete(event.url);
229
+ const errEnd = this._nativeToWeb(event.nativeTime) + 1;
230
+ this._measure.emit(`${_constants.PREFIX.PAGE_LOAD}/error|${event.errorCode ?? "?"}`, markName, errEnd, {
231
+ url: event.url,
232
+ errorCode: event.errorCode,
233
+ description: event.description ?? "",
234
+ simulatorValid: true
235
+ }, _constants.TRACK.NAVIGATION, "error");
236
+ break;
237
+ }
238
+ case "cold-start":
239
+ {
240
+ // emitted by markAppStartComplete() — point-in-time span at page init
241
+ const startWeb = this._nativeToWeb(event.nativeTime);
242
+ const markName = `${_constants.PREFIX.COLD_START}:mark`;
243
+ performance.mark(markName, {
244
+ startTime: Math.max(0, startWeb - event.durationMs)
245
+ });
246
+ this._measure.emit(`${_constants.PREFIX.COLD_START}|${event.durationMs}ms`, markName, startWeb, {
247
+ durationMs: event.durationMs,
248
+ simulatorValid: true
249
+ }, _constants.TRACK.NAVIGATION, event.durationMs > 3000 ? "error" : "secondary");
250
+ break;
251
+ }
252
+ }
253
+ }
254
+
255
+ // ─── Batch boot timing events ─────────────────────────────────────────────
256
+ // Emitted from PerfEventBuffer flush after onPageFinished.
257
+ // Each boot-* event is a point-in-time marker on Catalyst > Navigation.
258
+
259
+ onBatchEvent(event) {
260
+ if (event.type === "navigation-back") {
261
+ const t = this._nativeToWeb(event.nativeTime);
262
+ const markName = `catalyst:navigation/back:${Math.round(t)}`;
263
+ performance.mark(markName, {
264
+ startTime: Math.max(0, t)
265
+ });
266
+ this._measure.emit("catalyst:navigation/back", markName, Math.max(0, t) + 1, {
267
+ simulatorValid: false
268
+ }, _constants.TRACK.NAVIGATION, _constants.COLOR.SECONDARY);
269
+ return;
270
+ }
271
+ const bootTypeMap = {
272
+ "boot-activity-created": "activity-created",
273
+ "boot-webview-constructed": "webview-constructed",
274
+ "boot-load-url": "load-url",
275
+ "boot-page-started": "page-started",
276
+ "boot-page-finished": "page-finished"
277
+ };
278
+ const label = bootTypeMap[event.type];
279
+ if (!label) return;
280
+ const t = this._nativeToWeb(event.nativeTime);
281
+ const markName = `catalyst:boot/${label}:${Math.round(t)}`;
282
+ performance.mark(markName, {
283
+ startTime: Math.max(0, t)
284
+ });
285
+ this._measure.emit(`catalyst:boot/${label}`, markName, Math.max(0, t) + 1,
286
+ // point span
287
+ {
288
+ nativeTime: event.nativeTime,
289
+ thread: event.thread ?? null,
290
+ simulatorValid: false
291
+ }, _constants.TRACK.NAVIGATION, _constants.COLOR.SECONDARY_LIGHT);
292
+ }
293
+ }
294
+ exports.NavigationCollector = NavigationCollector;
@@ -1,7 +1,183 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NetworkTimingCollector=void 0;var _constants=require("../core/constants.js");/**
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NetworkTimingCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
2
9
  * NetworkTimingCollector.js
3
10
  *
4
11
  * Adds lightweight Catalyst timeline spans for fetch() and XMLHttpRequest.
5
12
  * Chrome already has detailed network timing; these spans exist to attach
6
13
  * network work to Catalyst interaction sessions via sessionId.
7
- */const FETCH_WRAPPED="__catalystNetworkTimingFetchWrapped";const XHR_WRAPPED="__catalystNetworkTimingXhrWrapped";const XHR_META="__catalystNetworkTimingMeta";const MAX_LABEL_LENGTH=72;class NetworkTimingCollector{constructor(measure){this._measure=measure;this._getInteraction=null;this._requestSeq=0;}setInteractionSource(fn){this._getInteraction=fn;}setWaterfallSource(fn){this._notifyWaterfall=fn;}init(){if(typeof window==="undefined")return;this._wrapFetch();this._wrapXhr();}_wrapFetch(){const originalFetch=window.fetch;if(typeof originalFetch!=="function"||originalFetch[FETCH_WRAPPED])return;const collector=this;function catalystFetch(input,init){const span=collector._start("fetch",init?.method??input?.method??"GET",init?.url??input?.url??input);try{return originalFetch.apply(this,arguments).then(response=>{collector._finish(span,{status:response?.status??null,ok:response?.ok??null});return response;},error=>{collector._finish(span,{outcome:"error",ok:false});throw error;});}catch(error){collector._finish(span,{outcome:"error",ok:false});throw error;}}catalystFetch[FETCH_WRAPPED]=true;window.fetch=catalystFetch;}_wrapXhr(){const proto=window.XMLHttpRequest?.prototype;if(!proto||proto[XHR_WRAPPED])return;if(typeof proto.open!=="function"||typeof proto.send!=="function")return;const originalOpen=proto.open;const originalSend=proto.send;const collector=this;proto.open=function catalystXhrOpen(method,url){this[XHR_META]={method,url};return originalOpen.apply(this,arguments);};proto.send=function catalystXhrSend(){const meta=this[XHR_META]??{};const span=collector._start("xhr",meta.method??"GET",meta.url??"unknown");let emitted=false;const finish=(outcome="complete")=>{if(emitted)return;emitted=true;const status=this.status;collector._finish(span,{status,ok:collector._statusOk(status),outcome});};this.addEventListener?.("loadend",()=>finish(),{once:true});try{return originalSend.apply(this,arguments);}catch(error){finish("error");throw error;}};proto[XHR_WRAPPED]=true;}_start(api,method,rawUrl){const startTime=performance.now();const normalizedMethod=String(method||"GET").toUpperCase();const url=this._normalizeUrl(rawUrl);const label=this._labelForUrl(url);const requestId=`${api}:${++this._requestSeq}`;const startMark=`${_constants.PREFIX.NETWORK_TIMING}:${requestId}:start`;performance.mark(startMark,{startTime});const interaction=this._getInteraction?.();const interactionId=interaction?.activeInteractionId??null;if(interactionId)interaction?.onNetworkCall?.();return{api,method:normalizedMethod,url,label,requestId,startMark,startTime,interactionId};}_finish(span,result={}){const endTime=performance.now();const durationMs=Math.round(endTime-span.startTime);const outcome=result.outcome??"complete";const ok=result.ok??this._statusOk(result.status);const color=outcome!=="complete"||ok===false||durationMs>_constants.THRESHOLD.FETCH_SLOW_MS?_constants.COLOR.ERROR:_constants.COLOR.SECONDARY;const detail={api:span.api,method:span.method,url:span.url,status:result.status??null,ok,outcome,durationMs,sessionId:span.interactionId,requestId:span.requestId,simulatorValid:true};this._notifyWaterfall?.({...detail,startTime:span.startTime,endTime});this._measure.emit(`${span.interactionId?`[${span.interactionId}] `:""}Network: ${span.method} ${span.label} - ${durationMs}ms`,span.startMark,endTime,{interactionId:span.interactionId,...detail,sessionId:undefined},_constants.TRACK.NETWORK,color);}_normalizeUrl(rawUrl){const value=rawUrl?.url??rawUrl??"unknown";try{const parsed=new URL(String(value),window.location?.href);return`${parsed.origin}${parsed.pathname}`;}catch{return String(value).split("?")[0].split("#")[0];}}_labelForUrl(url){try{const parsed=new URL(url,window.location?.href);const label=parsed.origin===window.location?.origin?parsed.pathname:`${parsed.host}${parsed.pathname}`;return(label||"/").slice(0,MAX_LABEL_LENGTH);}catch{return String(url||"unknown").slice(0,MAX_LABEL_LENGTH);}}_statusOk(status){return typeof status==="number"&&status>=200&&status<400;}}exports.NetworkTimingCollector=NetworkTimingCollector;
14
+ */
15
+
16
+ const FETCH_WRAPPED = "__catalystNetworkTimingFetchWrapped";
17
+ const XHR_WRAPPED = "__catalystNetworkTimingXhrWrapped";
18
+ const XHR_META = "__catalystNetworkTimingMeta";
19
+ const MAX_LABEL_LENGTH = 72;
20
+ class NetworkTimingCollector {
21
+ constructor(measure) {
22
+ this._measure = measure;
23
+ this._getInteraction = null;
24
+ this._requestSeq = 0;
25
+ }
26
+ setInteractionSource(fn) {
27
+ this._getInteraction = fn;
28
+ }
29
+ setWaterfallSource(fn) {
30
+ this._notifyWaterfall = fn;
31
+ }
32
+ init() {
33
+ if (typeof window === "undefined") return;
34
+ this._wrapFetch();
35
+ this._wrapXhr();
36
+ }
37
+ _wrapFetch() {
38
+ const originalFetch = window.fetch;
39
+ if (typeof originalFetch !== "function" || originalFetch[FETCH_WRAPPED]) return;
40
+ const collector = this;
41
+ function catalystFetch(input, init) {
42
+ const span = collector._start("fetch", init?.method ?? input?.method ?? "GET", init?.url ?? input?.url ?? input);
43
+ try {
44
+ return originalFetch.apply(this, arguments).then(response => {
45
+ collector._finish(span, {
46
+ status: response?.status ?? null,
47
+ ok: response?.ok ?? null
48
+ });
49
+ return response;
50
+ }, error => {
51
+ collector._finish(span, {
52
+ outcome: "error",
53
+ ok: false
54
+ });
55
+ throw error;
56
+ });
57
+ } catch (error) {
58
+ collector._finish(span, {
59
+ outcome: "error",
60
+ ok: false
61
+ });
62
+ throw error;
63
+ }
64
+ }
65
+ catalystFetch[FETCH_WRAPPED] = true;
66
+ window.fetch = catalystFetch;
67
+ }
68
+ _wrapXhr() {
69
+ const proto = window.XMLHttpRequest?.prototype;
70
+ if (!proto || proto[XHR_WRAPPED]) return;
71
+ if (typeof proto.open !== "function" || typeof proto.send !== "function") return;
72
+ const originalOpen = proto.open;
73
+ const originalSend = proto.send;
74
+ const collector = this;
75
+ proto.open = function catalystXhrOpen(method, url) {
76
+ this[XHR_META] = {
77
+ method,
78
+ url
79
+ };
80
+ return originalOpen.apply(this, arguments);
81
+ };
82
+ proto.send = function catalystXhrSend() {
83
+ const meta = this[XHR_META] ?? {};
84
+ const span = collector._start("xhr", meta.method ?? "GET", meta.url ?? "unknown");
85
+ let emitted = false;
86
+ const finish = (outcome = "complete") => {
87
+ if (emitted) return;
88
+ emitted = true;
89
+ const status = this.status;
90
+ collector._finish(span, {
91
+ status,
92
+ ok: collector._statusOk(status),
93
+ outcome
94
+ });
95
+ };
96
+ this.addEventListener?.("loadend", () => finish(), {
97
+ once: true
98
+ });
99
+ try {
100
+ return originalSend.apply(this, arguments);
101
+ } catch (error) {
102
+ finish("error");
103
+ throw error;
104
+ }
105
+ };
106
+ proto[XHR_WRAPPED] = true;
107
+ }
108
+ _start(api, method, rawUrl) {
109
+ const startTime = performance.now();
110
+ const normalizedMethod = String(method || "GET").toUpperCase();
111
+ const url = this._normalizeUrl(rawUrl);
112
+ const label = this._labelForUrl(url);
113
+ const requestId = `${api}:${++this._requestSeq}`;
114
+ const startMark = `${_constants.PREFIX.NETWORK_TIMING}:${requestId}:start`;
115
+ performance.mark(startMark, {
116
+ startTime
117
+ });
118
+ const interaction = this._getInteraction?.();
119
+ const interactionId = interaction?.activeInteractionId ?? null;
120
+ if (interactionId) interaction?.onNetworkCall?.();
121
+ return {
122
+ api,
123
+ method: normalizedMethod,
124
+ url,
125
+ label,
126
+ requestId,
127
+ startMark,
128
+ startTime,
129
+ interactionId
130
+ };
131
+ }
132
+ _finish(span, result = {}) {
133
+ const endTime = performance.now();
134
+ const durationMs = Math.round(endTime - span.startTime);
135
+ const outcome = result.outcome ?? "complete";
136
+ const ok = result.ok ?? this._statusOk(result.status);
137
+ const color = outcome !== "complete" || ok === false || durationMs > _constants.THRESHOLD.FETCH_SLOW_MS ? _constants.COLOR.ERROR : _constants.COLOR.SECONDARY;
138
+ const detail = {
139
+ api: span.api,
140
+ method: span.method,
141
+ url: span.url,
142
+ status: result.status ?? null,
143
+ ok,
144
+ outcome,
145
+ durationMs,
146
+ sessionId: span.interactionId,
147
+ requestId: span.requestId,
148
+ simulatorValid: true
149
+ };
150
+ this._notifyWaterfall?.({
151
+ ...detail,
152
+ startTime: span.startTime,
153
+ endTime
154
+ });
155
+ this._measure.emit(`${span.interactionId ? `[${span.interactionId}] ` : ""}Network: ${span.method} ${span.label} - ${durationMs}ms`, span.startMark, endTime, {
156
+ interactionId: span.interactionId,
157
+ ...detail,
158
+ sessionId: undefined
159
+ }, _constants.TRACK.NETWORK, color);
160
+ }
161
+ _normalizeUrl(rawUrl) {
162
+ const value = rawUrl?.url ?? rawUrl ?? "unknown";
163
+ try {
164
+ const parsed = new URL(String(value), window.location?.href);
165
+ return `${parsed.origin}${parsed.pathname}`;
166
+ } catch {
167
+ return String(value).split("?")[0].split("#")[0];
168
+ }
169
+ }
170
+ _labelForUrl(url) {
171
+ try {
172
+ const parsed = new URL(url, window.location?.href);
173
+ const label = parsed.origin === window.location?.origin ? parsed.pathname : `${parsed.host}${parsed.pathname}`;
174
+ return (label || "/").slice(0, MAX_LABEL_LENGTH);
175
+ } catch {
176
+ return String(url || "unknown").slice(0, MAX_LABEL_LENGTH);
177
+ }
178
+ }
179
+ _statusOk(status) {
180
+ return typeof status === "number" && status >= 200 && status < 400;
181
+ }
182
+ }
183
+ exports.NetworkTimingCollector = NetworkTimingCollector;