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,11 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.RenderCollector=void 0;var _constants=require("../core/constants.js");/**
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.RenderCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
2
9
  * RenderCollector.js
3
10
  *
4
11
  * Instruments: bugs #1, #6, #10, #14, #15, #16
@@ -25,16 +32,253 @@
25
32
  * Notifies:
26
33
  * ScrollCollector.onLoaf() — so scroll session can increment its loafCount
27
34
  * HookCollector.onLoaf() — so hook mount sessions can detect LoAF bursts
28
- */class RenderCollector{constructor(measure,getSessionContext,nativeToWeb){this._measure=measure;// getSessionContext() returns 'navigation'|'scroll'|'keyboard'|'none'
29
- // provided by index.js so RenderCollector doesn't import other collectors
30
- this._getSessionContext=getSessionContext;this._nativeToWeb=nativeToWeb??(t=>t);this._loafListeners=[];// [fn] called on every LoAF
31
- this._getInteraction=null;// set by index.js after construction
32
- }/** Called by index.js to wire up interaction session context. */setInteractionSource(fn){this._getInteraction=fn;}/** Called by index.js to wire up insights notifications. */setInsightsSource(fn){this._notifyInsights=fn;}/** Called by index.js to feed the normalized perf store. */setWaterfallSource(fn){this._notifyWaterfall=fn;}// Allow other collectors to subscribe to LoAF events
33
- addLoafListener(fn){this._loafListeners.push(fn);}init(){this._observeLoAF();this._observeLayoutShift();}// ─── Native MetricsMonitor events ────────────────────────────────────────
34
- onNativeEvent(event){const t=this._nativeToWeb(event.nativeTime);switch(event.type){case"fps-drop-episode":{const start=Math.max(0,this._nativeToWeb(event.startNativeTime));const end=Math.max(start,this._nativeToWeb(event.endNativeTime));const markName=`${_constants.PREFIX.FPS_DROP}:${Math.round(start)}`;performance.mark(markName,{startTime:start});const interactionId=this._getInteraction?.()?.interactionIdForRange(start,end)??null;if(interactionId)this._getInteraction?.()?.onFpsDrop();this._measure.emit(`${interactionId?`[${interactionId}] `:""}FPS drop: ${event.minFps.toFixed(1)}fps - ${Math.round(event.durationMs)}ms`,markName,end,{interactionId,minFps:event.minFps,avgFps:event.avgFps,durationMs:event.durationMs,sessionContext:this._getSessionContext(),thread:event.thread??null,simulatorValid:false},_constants.TRACK.RENDER,event.minFps<30?"error":"tertiary");this._notifyWaterfall?.({metric:"fps-drop",startTime:start,endTime:end,durationMs:event.durationMs,minFps:event.minFps,avgFps:event.avgFps,sessionId:interactionId});return{interactionId,startTime:start,endTime:end};}case"long-task":{const markName=`${_constants.PREFIX.LONG_TASK}:${Math.round(t)}`;const interactionId=this._getInteraction?.()?.interactionIdForRange(t,t+event.durationMs)??null;performance.mark(markName,{startTime:t});this._measure.emit(`${interactionId?`[${interactionId}] `:""}Long task: ${event.durationMs}ms`,markName,t+event.durationMs,{interactionId,durationMs:event.durationMs,sessionContext:this._getSessionContext(),thread:event.thread??null,simulatorValid:true},_constants.TRACK.RENDER,event.durationMs>100?"error":"tertiary");this._notifyWaterfall?.({metric:"long-task",startTime:t,endTime:t+event.durationMs,durationMs:event.durationMs,sessionId:interactionId});break;}case"memory-snapshot":{const jvmMark=`catalyst:mem/jvm:${Math.round(t)}`;performance.mark(jvmMark,{startTime:t});this._measure.emit(`JVM memory: ${event.jvmMb.toFixed(1)}MB`,jvmMark,t+1,{memoryId:`memory-jvm-${Math.round(t)}`,jvmMb:event.jvmMb,peakMb:event.peakMb,thread:event.thread??null,simulatorValid:false,...(event.coldStartMs!=null?{coldStartMs:event.coldStartMs}:{})},_constants.TRACK.MEMORY,"primary");const webviewMemoryId=`memory-webview-${Math.round(t)}`;const webviewMark=`catalyst:mem/webview:${Math.round(t)}`;performance.mark(webviewMark,{startTime:t});this._measure.emit(`WebView memory: ${event.webviewMb.toFixed(1)}MB`,webviewMark,t+1,{memoryId:webviewMemoryId,webviewMb:event.webviewMb,otherMb:event.otherMb,peakMb:event.peakMb,thread:event.thread??null,simulatorValid:false,...(event.coldStartMs!=null?{coldStartMs:event.coldStartMs}:{})},_constants.TRACK.MEMORY,"tertiary-dark");const totalMark=`${_constants.PREFIX.MEMORY_TOTAL}:${Math.round(t)}`;performance.mark(totalMark,{startTime:t});this._measure.emit(`Total memory: ${event.totalMb.toFixed(1)}MB`,totalMark,t+1,{memoryId:`memory-total-${Math.round(t)}`,totalMb:event.totalMb,jvmMb:event.jvmMb,webviewMb:event.webviewMb,otherMb:event.otherMb,peakMb:event.peakMb,thread:event.thread??null,simulatorValid:false,...(event.coldStartMs!=null?{coldStartMs:event.coldStartMs}:{})},_constants.TRACK.MEMORY,event.totalMb>400?"error":"secondary");this._notifyInsights?.({currentMb:event.webviewMb,startTime:t,endTime:t+1,memoryId:webviewMemoryId});this._notifyWaterfall?.({metric:"memory",startTime:t,endTime:t+1,durationMs:1,totalMb:event.totalMb,webviewMb:event.webviewMb});break;}}}// ─── Long Animation Frames ────────────────────────────────────────────────
35
- _observeLoAF(){if(!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame"))return;const obs=new PerformanceObserver(list=>{for(const entry of list.getEntries()){const start=entry.startTime;const end=start+entry.duration;const context=this._getSessionContext();// Register with shared Measure so all collectors get overlap detection
36
- this._measure.recordLoaf(start,end);// Notify subscribers (ScrollCollector, HookCollector)
37
- this._loafListeners.forEach(fn=>fn(start,end));const markName=`${_constants.PREFIX.LOAF}:${Math.round(start)}`;performance.mark(markName,{startTime:start});this._measure.emit(`${_constants.PREFIX.LOAF}|${Math.round(entry.duration)}ms`,markName,end,{blockingDuration:entry.blockingDuration,scriptCount:entry.scripts?.length??0,sessionContext:context,simulatorValid:false},_constants.TRACK.RENDER);this._notifyWaterfall?.({metric:"loaf",startTime:start,endTime:end,durationMs:entry.duration});}});obs.observe({type:"long-animation-frame",buffered:true});}// ─── Layout shift ─────────────────────────────────────────────────────────
38
- _observeLayoutShift(){if(!PerformanceObserver.supportedEntryTypes?.includes("layout-shift"))return;const obs=new PerformanceObserver(list=>{for(const entry of list.getEntries()){if(entry.hadRecentInput)continue;// user-initiated shift, ignore
39
- const context=this._getSessionContext();const markName=`catalyst:cls:${Math.round(entry.startTime)}`;performance.mark(markName,{startTime:entry.startTime});if(entry.value>0.1){this._notifyInsights?.({value:entry.value,sessionContext:context,startTime:entry.startTime,endTime:entry.startTime+1});}this._measure.emit(`${_constants.PREFIX.LAYOUT_SHIFT}|${entry.value.toFixed(4)}`,markName,entry.startTime+1,// point-in-time event, minimal duration
40
- {value:entry.value,sessionContext:context,simulatorValid:false},_constants.TRACK.RENDER);this._notifyWaterfall?.({metric:"layout-shift",startTime:entry.startTime,endTime:entry.startTime+1,durationMs:1,value:entry.value});}});obs.observe({type:"layout-shift",buffered:true});}}exports.RenderCollector=RenderCollector;
35
+ */
36
+
37
+ class RenderCollector {
38
+ constructor(measure, getSessionContext, nativeToWeb) {
39
+ this._measure = measure;
40
+ // getSessionContext() returns 'navigation'|'scroll'|'keyboard'|'none'
41
+ // provided by index.js so RenderCollector doesn't import other collectors
42
+ this._getSessionContext = getSessionContext;
43
+ this._nativeToWeb = nativeToWeb ?? (t => t);
44
+ this._loafListeners = []; // [fn] called on every LoAF
45
+ this._getInteraction = null; // set by index.js after construction
46
+ }
47
+
48
+ /** Called by index.js to wire up interaction session context. */
49
+ setInteractionSource(fn) {
50
+ this._getInteraction = fn;
51
+ }
52
+
53
+ /** Called by index.js to wire up insights notifications. */
54
+ setInsightsSource(fn) {
55
+ this._notifyInsights = fn;
56
+ }
57
+
58
+ /** Called by index.js to feed the normalized perf store. */
59
+ setWaterfallSource(fn) {
60
+ this._notifyWaterfall = fn;
61
+ }
62
+
63
+ // Allow other collectors to subscribe to LoAF events
64
+ addLoafListener(fn) {
65
+ this._loafListeners.push(fn);
66
+ }
67
+ init() {
68
+ this._observeLoAF();
69
+ this._observeLayoutShift();
70
+ }
71
+
72
+ // ─── Native MetricsMonitor events ────────────────────────────────────────
73
+
74
+ onNativeEvent(event) {
75
+ const t = this._nativeToWeb(event.nativeTime);
76
+ switch (event.type) {
77
+ case "fps-drop-episode":
78
+ {
79
+ const start = Math.max(0, this._nativeToWeb(event.startNativeTime));
80
+ const end = Math.max(start, this._nativeToWeb(event.endNativeTime));
81
+ const markName = `${_constants.PREFIX.FPS_DROP}:${Math.round(start)}`;
82
+ performance.mark(markName, {
83
+ startTime: start
84
+ });
85
+ const interactionId = this._getInteraction?.()?.interactionIdForRange(start, end) ?? null;
86
+ if (interactionId) this._getInteraction?.()?.onFpsDrop();
87
+ this._measure.emit(`${interactionId ? `[${interactionId}] ` : ""}FPS drop: ${event.minFps.toFixed(1)}fps - ${Math.round(event.durationMs)}ms`, markName, end, {
88
+ interactionId,
89
+ minFps: event.minFps,
90
+ avgFps: event.avgFps,
91
+ durationMs: event.durationMs,
92
+ sessionContext: this._getSessionContext(),
93
+ thread: event.thread ?? null,
94
+ simulatorValid: false
95
+ }, _constants.TRACK.RENDER, event.minFps < 30 ? "error" : "tertiary");
96
+ this._notifyWaterfall?.({
97
+ metric: "fps-drop",
98
+ startTime: start,
99
+ endTime: end,
100
+ durationMs: event.durationMs,
101
+ minFps: event.minFps,
102
+ avgFps: event.avgFps,
103
+ sessionId: interactionId
104
+ });
105
+ return {
106
+ interactionId,
107
+ startTime: start,
108
+ endTime: end
109
+ };
110
+ }
111
+ case "long-task":
112
+ {
113
+ const markName = `${_constants.PREFIX.LONG_TASK}:${Math.round(t)}`;
114
+ const interactionId = this._getInteraction?.()?.interactionIdForRange(t, t + event.durationMs) ?? null;
115
+ performance.mark(markName, {
116
+ startTime: t
117
+ });
118
+ this._measure.emit(`${interactionId ? `[${interactionId}] ` : ""}Long task: ${event.durationMs}ms`, markName, t + event.durationMs, {
119
+ interactionId,
120
+ durationMs: event.durationMs,
121
+ sessionContext: this._getSessionContext(),
122
+ thread: event.thread ?? null,
123
+ simulatorValid: true
124
+ }, _constants.TRACK.RENDER, event.durationMs > 100 ? "error" : "tertiary");
125
+ this._notifyWaterfall?.({
126
+ metric: "long-task",
127
+ startTime: t,
128
+ endTime: t + event.durationMs,
129
+ durationMs: event.durationMs,
130
+ sessionId: interactionId
131
+ });
132
+ break;
133
+ }
134
+ case "memory-snapshot":
135
+ {
136
+ const jvmMark = `catalyst:mem/jvm:${Math.round(t)}`;
137
+ performance.mark(jvmMark, {
138
+ startTime: t
139
+ });
140
+ this._measure.emit(`JVM memory: ${event.jvmMb.toFixed(1)}MB`, jvmMark, t + 1, {
141
+ memoryId: `memory-jvm-${Math.round(t)}`,
142
+ jvmMb: event.jvmMb,
143
+ peakMb: event.peakMb,
144
+ thread: event.thread ?? null,
145
+ simulatorValid: false,
146
+ ...(event.coldStartMs != null ? {
147
+ coldStartMs: event.coldStartMs
148
+ } : {})
149
+ }, _constants.TRACK.MEMORY, "primary");
150
+ const webviewMemoryId = `memory-webview-${Math.round(t)}`;
151
+ const webviewMark = `catalyst:mem/webview:${Math.round(t)}`;
152
+ performance.mark(webviewMark, {
153
+ startTime: t
154
+ });
155
+ this._measure.emit(`WebView memory: ${event.webviewMb.toFixed(1)}MB`, webviewMark, t + 1, {
156
+ memoryId: webviewMemoryId,
157
+ webviewMb: event.webviewMb,
158
+ otherMb: event.otherMb,
159
+ peakMb: event.peakMb,
160
+ thread: event.thread ?? null,
161
+ simulatorValid: false,
162
+ ...(event.coldStartMs != null ? {
163
+ coldStartMs: event.coldStartMs
164
+ } : {})
165
+ }, _constants.TRACK.MEMORY, "tertiary-dark");
166
+ const totalMark = `${_constants.PREFIX.MEMORY_TOTAL}:${Math.round(t)}`;
167
+ performance.mark(totalMark, {
168
+ startTime: t
169
+ });
170
+ this._measure.emit(`Total memory: ${event.totalMb.toFixed(1)}MB`, totalMark, t + 1, {
171
+ memoryId: `memory-total-${Math.round(t)}`,
172
+ totalMb: event.totalMb,
173
+ jvmMb: event.jvmMb,
174
+ webviewMb: event.webviewMb,
175
+ otherMb: event.otherMb,
176
+ peakMb: event.peakMb,
177
+ thread: event.thread ?? null,
178
+ simulatorValid: false,
179
+ ...(event.coldStartMs != null ? {
180
+ coldStartMs: event.coldStartMs
181
+ } : {})
182
+ }, _constants.TRACK.MEMORY, event.totalMb > 400 ? "error" : "secondary");
183
+ this._notifyInsights?.({
184
+ currentMb: event.webviewMb,
185
+ startTime: t,
186
+ endTime: t + 1,
187
+ memoryId: webviewMemoryId
188
+ });
189
+ this._notifyWaterfall?.({
190
+ metric: "memory",
191
+ startTime: t,
192
+ endTime: t + 1,
193
+ durationMs: 1,
194
+ totalMb: event.totalMb,
195
+ webviewMb: event.webviewMb
196
+ });
197
+ break;
198
+ }
199
+ }
200
+ }
201
+
202
+ // ─── Long Animation Frames ────────────────────────────────────────────────
203
+
204
+ _observeLoAF() {
205
+ if (!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) return;
206
+ const obs = new PerformanceObserver(list => {
207
+ for (const entry of list.getEntries()) {
208
+ const start = entry.startTime;
209
+ const end = start + entry.duration;
210
+ const context = this._getSessionContext();
211
+
212
+ // Register with shared Measure so all collectors get overlap detection
213
+ this._measure.recordLoaf(start, end);
214
+
215
+ // Notify subscribers (ScrollCollector, HookCollector)
216
+ this._loafListeners.forEach(fn => fn(start, end));
217
+ const markName = `${_constants.PREFIX.LOAF}:${Math.round(start)}`;
218
+ performance.mark(markName, {
219
+ startTime: start
220
+ });
221
+ this._measure.emit(`${_constants.PREFIX.LOAF}|${Math.round(entry.duration)}ms`, markName, end, {
222
+ blockingDuration: entry.blockingDuration,
223
+ scriptCount: entry.scripts?.length ?? 0,
224
+ sessionContext: context,
225
+ simulatorValid: false
226
+ }, _constants.TRACK.RENDER);
227
+ this._notifyWaterfall?.({
228
+ metric: "loaf",
229
+ startTime: start,
230
+ endTime: end,
231
+ durationMs: entry.duration
232
+ });
233
+ }
234
+ });
235
+ obs.observe({
236
+ type: "long-animation-frame",
237
+ buffered: true
238
+ });
239
+ }
240
+
241
+ // ─── Layout shift ─────────────────────────────────────────────────────────
242
+
243
+ _observeLayoutShift() {
244
+ if (!PerformanceObserver.supportedEntryTypes?.includes("layout-shift")) return;
245
+ const obs = new PerformanceObserver(list => {
246
+ for (const entry of list.getEntries()) {
247
+ if (entry.hadRecentInput) continue; // user-initiated shift, ignore
248
+
249
+ const context = this._getSessionContext();
250
+ const markName = `catalyst:cls:${Math.round(entry.startTime)}`;
251
+ performance.mark(markName, {
252
+ startTime: entry.startTime
253
+ });
254
+ if (entry.value > 0.1) {
255
+ this._notifyInsights?.({
256
+ value: entry.value,
257
+ sessionContext: context,
258
+ startTime: entry.startTime,
259
+ endTime: entry.startTime + 1
260
+ });
261
+ }
262
+ this._measure.emit(`${_constants.PREFIX.LAYOUT_SHIFT}|${entry.value.toFixed(4)}`, markName, entry.startTime + 1,
263
+ // point-in-time event, minimal duration
264
+ {
265
+ value: entry.value,
266
+ sessionContext: context,
267
+ simulatorValid: false
268
+ }, _constants.TRACK.RENDER);
269
+ this._notifyWaterfall?.({
270
+ metric: "layout-shift",
271
+ startTime: entry.startTime,
272
+ endTime: entry.startTime + 1,
273
+ durationMs: 1,
274
+ value: entry.value
275
+ });
276
+ }
277
+ });
278
+ obs.observe({
279
+ type: "layout-shift",
280
+ buffered: true
281
+ });
282
+ }
283
+ }
284
+ exports.RenderCollector = RenderCollector;
@@ -1,4 +1,12 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ScrollCollector=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.ScrollCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ var _session = require("../core/session.js");
9
+ /**
2
10
  * ScrollCollector.js
3
11
  *
4
12
  * Instruments: bugs #3, #13
@@ -15,11 +23,89 @@
15
23
  *
16
24
  * fps-drop-episode routed here (Option B) when scroll session is open:
17
25
  * index.js checks this._scroll._session.isOpen before calling onFpsDrop(event)
18
- */class ScrollCollector{constructor(measure,nativeToWeb){this._measure=measure;this._nativeToWeb=nativeToWeb;this._session=new _session.Session(_constants.PREFIX.SESSION_SCROLL,measure,_constants.TRACK.INPUT);this._insightsCb=null;// set by index.js via setInsightsSource
19
- this._frameDropCount=0;this._minFps=60;}init(){// No DOM listener — scroll events come from native GestureDetector
20
- }setInsightsSource(cb){this._insightsCb=cb;}setWaterfallSource(fn){this._notifyWaterfall=fn;}// Called by RenderCollector when a LoAF fires (shared notification, kept for compat)
21
- onLoaf(){if(this._session.isOpen){this._session.increment("loafCount");}}// ─── Native event handlers ────────────────────────────────────────────────
22
- onScrollStart(event){if(this._session.isOpen)return;// already tracking, ignore duplicate start
23
- const webNow=this._nativeToWeb(event.nativeTime);this._frameDropCount=0;this._minFps=60;this._session.open({loafCount:0},webNow);}onScrollEnd(event){if(!this._session.isOpen)return;const startTime=this._session.startTime;const webNow=this._nativeToWeb(event.nativeTime);const loafCount=this._session.get("loafCount")??0;const janky=this._frameDropCount>0||this._minFps<55;const durationMs=Math.round(webNow-startTime);this._session.close({loafCount,frameDropCount:this._frameDropCount,minFps:this._minFps,janky,simulatorValid:false},webNow);if(janky&&this._insightsCb){this._insightsCb({minFps:this._minFps,frameDropCount:this._frameDropCount,startTime,endTime:webNow});}this._notifyWaterfall?.({kind:"scroll",startTime,endTime:webNow,durationMs,loafCount,frameDropCount:this._frameDropCount,minFps:this._minFps,janky});this._frameDropCount=0;this._minFps=60;}// ─── fps-drop-episode correlation (Option B) ──────────────────────────────
24
- // Called from index.js _routeNativeEvent when scroll session is open.
25
- onFpsDrop(event){this._frameDropCount++;if(event.minFps!=null&&event.minFps<this._minFps){this._minFps=event.minFps;}}}exports.ScrollCollector=ScrollCollector;
26
+ */
27
+
28
+ class ScrollCollector {
29
+ constructor(measure, nativeToWeb) {
30
+ this._measure = measure;
31
+ this._nativeToWeb = nativeToWeb;
32
+ this._session = new _session.Session(_constants.PREFIX.SESSION_SCROLL, measure, _constants.TRACK.INPUT);
33
+ this._insightsCb = null; // set by index.js via setInsightsSource
34
+ this._frameDropCount = 0;
35
+ this._minFps = 60;
36
+ }
37
+ init() {
38
+ // No DOM listener — scroll events come from native GestureDetector
39
+ }
40
+ setInsightsSource(cb) {
41
+ this._insightsCb = cb;
42
+ }
43
+ setWaterfallSource(fn) {
44
+ this._notifyWaterfall = fn;
45
+ }
46
+
47
+ // Called by RenderCollector when a LoAF fires (shared notification, kept for compat)
48
+ onLoaf() {
49
+ if (this._session.isOpen) {
50
+ this._session.increment("loafCount");
51
+ }
52
+ }
53
+
54
+ // ─── Native event handlers ────────────────────────────────────────────────
55
+
56
+ onScrollStart(event) {
57
+ if (this._session.isOpen) return; // already tracking, ignore duplicate start
58
+ const webNow = this._nativeToWeb(event.nativeTime);
59
+ this._frameDropCount = 0;
60
+ this._minFps = 60;
61
+ this._session.open({
62
+ loafCount: 0
63
+ }, webNow);
64
+ }
65
+ onScrollEnd(event) {
66
+ if (!this._session.isOpen) return;
67
+ const startTime = this._session.startTime;
68
+ const webNow = this._nativeToWeb(event.nativeTime);
69
+ const loafCount = this._session.get("loafCount") ?? 0;
70
+ const janky = this._frameDropCount > 0 || this._minFps < 55;
71
+ const durationMs = Math.round(webNow - startTime);
72
+ this._session.close({
73
+ loafCount,
74
+ frameDropCount: this._frameDropCount,
75
+ minFps: this._minFps,
76
+ janky,
77
+ simulatorValid: false
78
+ }, webNow);
79
+ if (janky && this._insightsCb) {
80
+ this._insightsCb({
81
+ minFps: this._minFps,
82
+ frameDropCount: this._frameDropCount,
83
+ startTime,
84
+ endTime: webNow
85
+ });
86
+ }
87
+ this._notifyWaterfall?.({
88
+ kind: "scroll",
89
+ startTime,
90
+ endTime: webNow,
91
+ durationMs,
92
+ loafCount,
93
+ frameDropCount: this._frameDropCount,
94
+ minFps: this._minFps,
95
+ janky
96
+ });
97
+ this._frameDropCount = 0;
98
+ this._minFps = 60;
99
+ }
100
+
101
+ // ─── fps-drop-episode correlation (Option B) ──────────────────────────────
102
+ // Called from index.js _routeNativeEvent when scroll session is open.
103
+
104
+ onFpsDrop(event) {
105
+ this._frameDropCount++;
106
+ if (event.minFps != null && event.minFps < this._minFps) {
107
+ this._minFps = event.minFps;
108
+ }
109
+ }
110
+ }
111
+ exports.ScrollCollector = ScrollCollector;
@@ -1,4 +1,11 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ThreadCollector=void 0;var _constants=require("../core/constants.js");/**
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ThreadCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
2
9
  * ThreadCollector.js
3
10
  *
4
11
  * Instruments: thread visibility gap during cache serving.
@@ -20,8 +27,85 @@
20
27
  * Overlap detection:
21
28
  * If a LoAF overlaps a software-render window → notifyInsights so
22
29
  * InsightsCollector can fire an 'hw-accel-during-frame' critical insight.
23
- */class ThreadCollector{constructor(measure,nativeToWeb){this._measure=measure;this._nativeToWeb=nativeToWeb;// Track open software-render windows: { startMark, startTime, thread }
24
- this._openWindow=null;this._notifyInsights=null;}/** Called by index.js to wire up insights notifications. */setInsightsSource(fn){this._notifyInsights=fn;}init(){// No web-side observers — all events come from native via onNativeEvent()
25
- }onNativeEvent(event){if(event.type!=="hw-accel-change")return;const{state,trigger,thread,nativeTime}=event;const webTime=nativeTime!=null?this._nativeToWeb(nativeTime):performance.now();if(state==="off"){// Software-render window opens — record start, wait for 'on' to close span
26
- const markName=`${_constants.PREFIX.HW_ACCEL}/off:start:${Math.round(webTime)}`;performance.mark(markName,{startTime:webTime});this._openWindow={startMark:markName,startTime:webTime,thread:thread??null,trigger:trigger??null};}else if(state==="on"){const endTime=webTime;if(this._openWindow){const{startMark,startTime,thread:offThread,trigger:offTrigger}=this._openWindow;this._openWindow=null;const loafOverlap=this._measure.overlapsLoaf(startTime,endTime);this._measure.emit(`${_constants.PREFIX.HW_ACCEL}/sw-render-window|${offTrigger??"unknown"}`,startMark,endTime,{trigger:offTrigger??null,thread:offThread??null,loafOverlap,simulatorValid:false},_constants.TRACK.RENDER,loafOverlap?_constants.COLOR.ERROR:_constants.COLOR.SECONDARY_DARK);if(loafOverlap){this._notifyInsights?.({durationMs:endTime-startTime,thread:offThread,trigger:offTrigger,startTime,endTime});}}// Emit a point-in-time marker for hw-accel restored
27
- const onMark=`${_constants.PREFIX.HW_ACCEL}/on:${Math.round(endTime)}`;performance.mark(onMark,{startTime:endTime});this._measure.emit(`${_constants.PREFIX.HW_ACCEL}/restored`,onMark,endTime+1,{trigger:trigger??null,thread:thread??null,simulatorValid:false},_constants.TRACK.RENDER,_constants.COLOR.SECONDARY);}}}exports.ThreadCollector=ThreadCollector;
30
+ */
31
+
32
+ class ThreadCollector {
33
+ constructor(measure, nativeToWeb) {
34
+ this._measure = measure;
35
+ this._nativeToWeb = nativeToWeb;
36
+
37
+ // Track open software-render windows: { startMark, startTime, thread }
38
+ this._openWindow = null;
39
+ this._notifyInsights = null;
40
+ }
41
+
42
+ /** Called by index.js to wire up insights notifications. */
43
+ setInsightsSource(fn) {
44
+ this._notifyInsights = fn;
45
+ }
46
+ init() {
47
+ // No web-side observers — all events come from native via onNativeEvent()
48
+ }
49
+ onNativeEvent(event) {
50
+ if (event.type !== "hw-accel-change") return;
51
+ const {
52
+ state,
53
+ trigger,
54
+ thread,
55
+ nativeTime
56
+ } = event;
57
+ const webTime = nativeTime != null ? this._nativeToWeb(nativeTime) : performance.now();
58
+ if (state === "off") {
59
+ // Software-render window opens — record start, wait for 'on' to close span
60
+ const markName = `${_constants.PREFIX.HW_ACCEL}/off:start:${Math.round(webTime)}`;
61
+ performance.mark(markName, {
62
+ startTime: webTime
63
+ });
64
+ this._openWindow = {
65
+ startMark: markName,
66
+ startTime: webTime,
67
+ thread: thread ?? null,
68
+ trigger: trigger ?? null
69
+ };
70
+ } else if (state === "on") {
71
+ const endTime = webTime;
72
+ if (this._openWindow) {
73
+ const {
74
+ startMark,
75
+ startTime,
76
+ thread: offThread,
77
+ trigger: offTrigger
78
+ } = this._openWindow;
79
+ this._openWindow = null;
80
+ const loafOverlap = this._measure.overlapsLoaf(startTime, endTime);
81
+ this._measure.emit(`${_constants.PREFIX.HW_ACCEL}/sw-render-window|${offTrigger ?? "unknown"}`, startMark, endTime, {
82
+ trigger: offTrigger ?? null,
83
+ thread: offThread ?? null,
84
+ loafOverlap,
85
+ simulatorValid: false
86
+ }, _constants.TRACK.RENDER, loafOverlap ? _constants.COLOR.ERROR : _constants.COLOR.SECONDARY_DARK);
87
+ if (loafOverlap) {
88
+ this._notifyInsights?.({
89
+ durationMs: endTime - startTime,
90
+ thread: offThread,
91
+ trigger: offTrigger,
92
+ startTime,
93
+ endTime
94
+ });
95
+ }
96
+ }
97
+
98
+ // Emit a point-in-time marker for hw-accel restored
99
+ const onMark = `${_constants.PREFIX.HW_ACCEL}/on:${Math.round(endTime)}`;
100
+ performance.mark(onMark, {
101
+ startTime: endTime
102
+ });
103
+ this._measure.emit(`${_constants.PREFIX.HW_ACCEL}/restored`, onMark, endTime + 1, {
104
+ trigger: trigger ?? null,
105
+ thread: thread ?? null,
106
+ simulatorValid: false
107
+ }, _constants.TRACK.RENDER, _constants.COLOR.SECONDARY);
108
+ }
109
+ }
110
+ }
111
+ exports.ThreadCollector = ThreadCollector;
@@ -1,26 +1,97 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.TRACK_GROUP=exports.TRACK=exports.THRESHOLD=exports.PREFIX=exports.COLOR=void 0;/**
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TRACK_GROUP = exports.TRACK = exports.THRESHOLD = exports.PREFIX = exports.COLOR = void 0;
7
+ /**
2
8
  * constants.js
3
9
  *
4
10
  * Single source of truth for track names, thresholds, and color rules.
5
11
  * All collectors import from here — no magic strings scattered across files.
6
- */// ─── Chrome DevTools track group ─────────────────────────────────────────────
7
- const TRACK_GROUP=exports.TRACK_GROUP="Catalyst";const TRACK=exports.TRACK={NAVIGATION:"Navigation",SCROLL:"Scroll",INPUT:"Input",KEYBOARD:"Keyboard",// alias kept for backward compat — collectors use TRACK.INPUT
8
- RENDER:"Render",MEMORY:"Memory",CACHE:"Cache",NETWORK:"Network",HOOK:"Hook",INTERACTION:"Interaction",INSIGHTS:"Insights",NATIVE_API:"Native API"};// ─── Measure name prefixes ────────────────────────────────────────────────────
9
- const PREFIX=exports.PREFIX={SESSION_NAV:"catalyst:session/navigation",SESSION_SCROLL:"catalyst:session/scroll",SESSION_INPUT:"catalyst:session/input",SESSION_KEYBOARD:"catalyst:session/keyboard",// alias — KeyboardCollector still uses this
10
- LCP:"catalyst:lcp",FCP:"catalyst:fcp",LOAF:"catalyst:loaf",LAYOUT_SHIFT:"catalyst:layout-shift",VIEWPORT_RESIZE:"catalyst:viewport-resize",ROUTE_TRANSITION:"catalyst:route-transition",CACHE:"catalyst:cache",CACHE_GROUP:"catalyst:cache-group",NETWORK_TIMING:"catalyst:network",NATIVE_API:"catalyst:native-api",HOOK_MOUNT:"catalyst:hook/mount",HOOK_LAZY:"catalyst:hook/lazy",INTERACTION:"catalyst:interaction",SESSION_INTERACTION:"catalyst:session/interaction",LONG_PRESS:"catalyst:long-press",INSIGHT:"catalyst:insight",HW_ACCEL:"catalyst:hw-accel",// Native MetricsMonitor events
11
- PAGE_LOAD:"catalyst:page-load",COLD_START:"catalyst:cold-start",FPS_DROP:"catalyst:fps-drop",LONG_TASK:"catalyst:long-task",MEMORY_JVM:"catalyst:mem/jvm",MEMORY_NATIVE:"catalyst:mem/native",MEMORY_TOTAL:"catalyst:mem/total"};// ─── Thresholds ───────────────────────────────────────────────────────────────
12
- const THRESHOLD=exports.THRESHOLD={LCP_BAD_MS:2500,// LCP > 2.5s = poor (Core Web Vitals)
13
- NAV_SESSION_BAD_MS:2500,// navigation session > 2.5s = slow page
14
- FETCH_SLOW_MS:300,// network fetch > 300ms = slow
15
- SCROLL_IDLE_MS:150,// scroll ends after 150ms of no scroll events
16
- VIEWPORT_MIN_DELTA:50,// ignore viewport resize < 50px (not keyboard)
17
- CACHE_GROUP_IDLE_MS:300,// close cache group after 300ms idle
18
- LOAF_WINDOW:5,// keep last N LoAF entries for overlap detection
19
- LONG_PRESS_MS:500,// pointerdown held > 500ms = long press
20
- INTERACTION_SLOW_MS:100,// tap → paint > 100ms = slow response
21
- LAZY_LOAF_WINDOW_MS:2000,// watch for LoAF bursts up to 2s after interaction
22
- CLS_BAD:0.1// CLS > 0.1 per entry = significant shift
23
- };// ─── Colors ───────────────────────────────────────────────────────────────────
12
+ */
13
+
14
+ // ─── Chrome DevTools track group ─────────────────────────────────────────────
15
+
16
+ const TRACK_GROUP = exports.TRACK_GROUP = "Catalyst";
17
+ const TRACK = exports.TRACK = {
18
+ NAVIGATION: "Navigation",
19
+ SCROLL: "Scroll",
20
+ INPUT: "Input",
21
+ KEYBOARD: "Keyboard",
22
+ // alias kept for backward compat — collectors use TRACK.INPUT
23
+ RENDER: "Render",
24
+ MEMORY: "Memory",
25
+ CACHE: "Cache",
26
+ NETWORK: "Network",
27
+ HOOK: "Hook",
28
+ INTERACTION: "Interaction",
29
+ INSIGHTS: "Insights",
30
+ NATIVE_API: "Native API"
31
+ };
32
+
33
+ // ─── Measure name prefixes ────────────────────────────────────────────────────
34
+
35
+ const PREFIX = exports.PREFIX = {
36
+ SESSION_NAV: "catalyst:session/navigation",
37
+ SESSION_SCROLL: "catalyst:session/scroll",
38
+ SESSION_INPUT: "catalyst:session/input",
39
+ SESSION_KEYBOARD: "catalyst:session/keyboard",
40
+ // alias — KeyboardCollector still uses this
41
+ LCP: "catalyst:lcp",
42
+ FCP: "catalyst:fcp",
43
+ LOAF: "catalyst:loaf",
44
+ LAYOUT_SHIFT: "catalyst:layout-shift",
45
+ VIEWPORT_RESIZE: "catalyst:viewport-resize",
46
+ ROUTE_TRANSITION: "catalyst:route-transition",
47
+ CACHE: "catalyst:cache",
48
+ CACHE_GROUP: "catalyst:cache-group",
49
+ NETWORK_TIMING: "catalyst:network",
50
+ NATIVE_API: "catalyst:native-api",
51
+ HOOK_MOUNT: "catalyst:hook/mount",
52
+ HOOK_LAZY: "catalyst:hook/lazy",
53
+ INTERACTION: "catalyst:interaction",
54
+ SESSION_INTERACTION: "catalyst:session/interaction",
55
+ LONG_PRESS: "catalyst:long-press",
56
+ INSIGHT: "catalyst:insight",
57
+ HW_ACCEL: "catalyst:hw-accel",
58
+ // Native MetricsMonitor events
59
+ PAGE_LOAD: "catalyst:page-load",
60
+ COLD_START: "catalyst:cold-start",
61
+ FPS_DROP: "catalyst:fps-drop",
62
+ LONG_TASK: "catalyst:long-task",
63
+ MEMORY_JVM: "catalyst:mem/jvm",
64
+ MEMORY_NATIVE: "catalyst:mem/native",
65
+ MEMORY_TOTAL: "catalyst:mem/total"
66
+ };
67
+
68
+ // ─── Thresholds ───────────────────────────────────────────────────────────────
69
+
70
+ const THRESHOLD = exports.THRESHOLD = {
71
+ LCP_BAD_MS: 2500,
72
+ // LCP > 2.5s = poor (Core Web Vitals)
73
+ NAV_SESSION_BAD_MS: 2500,
74
+ // navigation session > 2.5s = slow page
75
+ FETCH_SLOW_MS: 300,
76
+ // network fetch > 300ms = slow
77
+ SCROLL_IDLE_MS: 150,
78
+ // scroll ends after 150ms of no scroll events
79
+ VIEWPORT_MIN_DELTA: 50,
80
+ // ignore viewport resize < 50px (not keyboard)
81
+ CACHE_GROUP_IDLE_MS: 300,
82
+ // close cache group after 300ms idle
83
+ LOAF_WINDOW: 5,
84
+ // keep last N LoAF entries for overlap detection
85
+ LONG_PRESS_MS: 500,
86
+ // pointerdown held > 500ms = long press
87
+ INTERACTION_SLOW_MS: 100,
88
+ // tap → paint > 100ms = slow response
89
+ LAZY_LOAF_WINDOW_MS: 2000,
90
+ // watch for LoAF bursts up to 2s after interaction
91
+ CLS_BAD: 0.1 // CLS > 0.1 per entry = significant shift
92
+ };
93
+
94
+ // ─── Colors ───────────────────────────────────────────────────────────────────
24
95
  // Chrome DevTools Extensibility API color values (all 9 values):
25
96
  //
26
97
  // primary / primary-light / primary-dark → blue tones
@@ -40,4 +111,16 @@ CLS_BAD:0.1// CLS > 0.1 per entry = significant shift
40
111
  // tertiary = network fetch after miss (expected but slow)
41
112
  // tertiary-light = small CLS, long-press (low signal)
42
113
  // tertiary-dark = insight warning (non-critical)
43
- const COLOR=exports.COLOR={ERROR:"error",PRIMARY:"primary",PRIMARY_LIGHT:"primary-light",PRIMARY_DARK:"primary-dark",SECONDARY:"secondary",SECONDARY_LIGHT:"secondary-light",SECONDARY_DARK:"secondary-dark",TERTIARY:"tertiary",TERTIARY_LIGHT:"tertiary-light",TERTIARY_DARK:"tertiary-dark"};
114
+
115
+ const COLOR = exports.COLOR = {
116
+ ERROR: "error",
117
+ PRIMARY: "primary",
118
+ PRIMARY_LIGHT: "primary-light",
119
+ PRIMARY_DARK: "primary-dark",
120
+ SECONDARY: "secondary",
121
+ SECONDARY_LIGHT: "secondary-light",
122
+ SECONDARY_DARK: "secondary-dark",
123
+ TERTIARY: "tertiary",
124
+ TERTIARY_LIGHT: "tertiary-light",
125
+ TERTIARY_DARK: "tertiary-dark"
126
+ };