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.CacheCollector=void 0;var _constants=require("../core/constants.js");/**
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CacheCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
2
9
  * CacheCollector.js
3
10
  *
4
11
  * Instruments: bugs #1, #2, #10
@@ -28,13 +35,181 @@
28
35
  * Blue-light = cache HIT disk — good, slightly slower
29
36
  * Red = cache MISS — matched pattern but not cached
30
37
  * Yellow/Orange = network fetch — orange if slow >300ms
31
- */const TRACK_CACHE_DETAIL="Cache > Detail";class CacheCollector{constructor(measure,nativeToWeb){this._measure=measure;this._nativeToWeb=nativeToWeb;this._notifyInsights=null;// set by index.js after construction
32
- }/** Called by index.js to wire up insights notifications for cache-summary. */setInsightsSource(fn){this._notifyInsights=fn;}init(){// No web-side observers — all events come from native
33
- }// ── Live path (individual events routed from index.js) ──────────────────
34
- onNativeEvent(event){const{type}=event;if(!type?.startsWith("cache-")&&type!=="network-fetch-complete")return;this._emitCacheSpan(event,_constants.TRACK.CACHE);}// ── Batch path (native buffer flush) ────────────────────────────────────
35
- onBatchEvent(event){const{type}=event;if(type==="cache-summary"){this._emitSummarySpan(event);return;}if(!type?.startsWith("cache-")&&type!=="network-fetch-complete")return;// Individual cache spans go on the Detail sub-track
36
- this._emitCacheSpan(event,TRACK_CACHE_DETAIL);}// ── Internal ─────────────────────────────────────────────────────────────
37
- _emitCacheSpan(event,track){const{type,url,durationMs,nativeStartMs,resourceType,thread,statusCode}=event;const resType=resourceType??"other";const filename=this._filename(url);const endTime=nativeStartMs!=null?this._nativeToWeb(nativeStartMs)+(durationMs??0):performance.now();const startTime=endTime-(durationMs??0);let measureName,color,detail;const interceptThread=event.interceptThread??null;const emitThread=event.emitThread??null;if(type==="cache-hit-memory"){measureName=`${_constants.PREFIX.CACHE}/hit|memory|${resType}|${filename}`;color=_constants.COLOR.PRIMARY;detail={url,source:"memory",resourceType:resType,durationMs,thread:thread??null,interceptThread,emitThread,simulatorValid:false};}else if(type==="cache-hit-disk"){measureName=`${_constants.PREFIX.CACHE}/hit|disk|${resType}|${filename}`;color=_constants.COLOR.PRIMARY_LIGHT;detail={url,source:"disk",resourceType:resType,durationMs,thread:thread??null,interceptThread,emitThread,simulatorValid:false};}else if(type==="cache-miss-fetch"){measureName=`${_constants.PREFIX.CACHE}/miss|${resType}|${filename}`;color=_constants.COLOR.ERROR;detail={url,resourceType:resType,durationMs,thread:thread??null,interceptThread,emitThread,simulatorValid:false};}else if(type==="network-fetch-complete"){const slow=(durationMs??0)>_constants.THRESHOLD.FETCH_SLOW_MS;measureName=`${_constants.PREFIX.CACHE}/fetch|${resType}|${filename}`;color=slow?_constants.COLOR.TERTIARY_DARK:_constants.COLOR.TERTIARY;detail={url,resourceType:resType,durationMs,statusCode:statusCode??null,thread:thread??null,emitThread,slow,simulatorValid:false};}else{return;}const startMark=`${measureName}:start:${Math.round(startTime)}`;performance.mark(startMark,{startTime});this._measure.emit(measureName,startMark,endTime,detail,track,color);}_emitSummarySpan(event){const{hits,misses,fetches,total,hitRatePct,avgFetchMs,topSlowest,nativeTime}=event;if(total===0)return;// Place summary span at current time (post-load, idle phase)
38
- const startTime=this._nativeToWeb(nativeTime??0)||performance.now();const endTime=startTime+1;// point span
39
- const color=hitRatePct>=80?_constants.COLOR.PRIMARY_DARK:hitRatePct>=50?_constants.COLOR.SECONDARY:_constants.COLOR.ERROR;const topNames=Array.isArray(topSlowest)?topSlowest.map(e=>`${e.filename}(${e.durationMs}ms)`).join(", "):"";const measureName=`${_constants.PREFIX.CACHE}/summary`;const startMark=`${measureName}:start:${Math.round(startTime)}`;performance.mark(startMark,{startTime});this._measure.emit(measureName,startMark,endTime,{hits,misses,fetches,total,hitRatePct,avgFetchMs,topSlowest:topNames,simulatorValid:false},_constants.TRACK.CACHE,color);// Notify InsightsCollector so it can flag low hit rates
40
- this._notifyInsights?.({hitRatePct,total,avgFetchMs,startTime,endTime});}_filename(url){try{const path=new URL(url).pathname;const name=path.split("/").filter(Boolean).pop()??path;return name.slice(0,40);}catch{return String(url).slice(0,40);}}}exports.CacheCollector=CacheCollector;
38
+ */
39
+
40
+ const TRACK_CACHE_DETAIL = "Cache > Detail";
41
+ class CacheCollector {
42
+ constructor(measure, nativeToWeb) {
43
+ this._measure = measure;
44
+ this._nativeToWeb = nativeToWeb;
45
+ this._notifyInsights = null; // set by index.js after construction
46
+ }
47
+
48
+ /** Called by index.js to wire up insights notifications for cache-summary. */
49
+ setInsightsSource(fn) {
50
+ this._notifyInsights = fn;
51
+ }
52
+ init() {
53
+ // No web-side observers — all events come from native
54
+ }
55
+
56
+ // ── Live path (individual events routed from index.js) ──────────────────
57
+
58
+ onNativeEvent(event) {
59
+ const {
60
+ type
61
+ } = event;
62
+ if (!type?.startsWith("cache-") && type !== "network-fetch-complete") return;
63
+ this._emitCacheSpan(event, _constants.TRACK.CACHE);
64
+ }
65
+
66
+ // ── Batch path (native buffer flush) ────────────────────────────────────
67
+
68
+ onBatchEvent(event) {
69
+ const {
70
+ type
71
+ } = event;
72
+ if (type === "cache-summary") {
73
+ this._emitSummarySpan(event);
74
+ return;
75
+ }
76
+ if (!type?.startsWith("cache-") && type !== "network-fetch-complete") return;
77
+ // Individual cache spans go on the Detail sub-track
78
+ this._emitCacheSpan(event, TRACK_CACHE_DETAIL);
79
+ }
80
+
81
+ // ── Internal ─────────────────────────────────────────────────────────────
82
+
83
+ _emitCacheSpan(event, track) {
84
+ const {
85
+ type,
86
+ url,
87
+ durationMs,
88
+ nativeStartMs,
89
+ resourceType,
90
+ thread,
91
+ statusCode
92
+ } = event;
93
+ const resType = resourceType ?? "other";
94
+ const filename = this._filename(url);
95
+ const endTime = nativeStartMs != null ? this._nativeToWeb(nativeStartMs) + (durationMs ?? 0) : performance.now();
96
+ const startTime = endTime - (durationMs ?? 0);
97
+ let measureName, color, detail;
98
+ const interceptThread = event.interceptThread ?? null;
99
+ const emitThread = event.emitThread ?? null;
100
+ if (type === "cache-hit-memory") {
101
+ measureName = `${_constants.PREFIX.CACHE}/hit|memory|${resType}|${filename}`;
102
+ color = _constants.COLOR.PRIMARY;
103
+ detail = {
104
+ url,
105
+ source: "memory",
106
+ resourceType: resType,
107
+ durationMs,
108
+ thread: thread ?? null,
109
+ interceptThread,
110
+ emitThread,
111
+ simulatorValid: false
112
+ };
113
+ } else if (type === "cache-hit-disk") {
114
+ measureName = `${_constants.PREFIX.CACHE}/hit|disk|${resType}|${filename}`;
115
+ color = _constants.COLOR.PRIMARY_LIGHT;
116
+ detail = {
117
+ url,
118
+ source: "disk",
119
+ resourceType: resType,
120
+ durationMs,
121
+ thread: thread ?? null,
122
+ interceptThread,
123
+ emitThread,
124
+ simulatorValid: false
125
+ };
126
+ } else if (type === "cache-miss-fetch") {
127
+ measureName = `${_constants.PREFIX.CACHE}/miss|${resType}|${filename}`;
128
+ color = _constants.COLOR.ERROR;
129
+ detail = {
130
+ url,
131
+ resourceType: resType,
132
+ durationMs,
133
+ thread: thread ?? null,
134
+ interceptThread,
135
+ emitThread,
136
+ simulatorValid: false
137
+ };
138
+ } else if (type === "network-fetch-complete") {
139
+ const slow = (durationMs ?? 0) > _constants.THRESHOLD.FETCH_SLOW_MS;
140
+ measureName = `${_constants.PREFIX.CACHE}/fetch|${resType}|${filename}`;
141
+ color = slow ? _constants.COLOR.TERTIARY_DARK : _constants.COLOR.TERTIARY;
142
+ detail = {
143
+ url,
144
+ resourceType: resType,
145
+ durationMs,
146
+ statusCode: statusCode ?? null,
147
+ thread: thread ?? null,
148
+ emitThread,
149
+ slow,
150
+ simulatorValid: false
151
+ };
152
+ } else {
153
+ return;
154
+ }
155
+ const startMark = `${measureName}:start:${Math.round(startTime)}`;
156
+ performance.mark(startMark, {
157
+ startTime
158
+ });
159
+ this._measure.emit(measureName, startMark, endTime, detail, track, color);
160
+ }
161
+ _emitSummarySpan(event) {
162
+ const {
163
+ hits,
164
+ misses,
165
+ fetches,
166
+ total,
167
+ hitRatePct,
168
+ avgFetchMs,
169
+ topSlowest,
170
+ nativeTime
171
+ } = event;
172
+ if (total === 0) return;
173
+
174
+ // Place summary span at current time (post-load, idle phase)
175
+ const startTime = this._nativeToWeb(nativeTime ?? 0) || performance.now();
176
+ const endTime = startTime + 1; // point span
177
+
178
+ const color = hitRatePct >= 80 ? _constants.COLOR.PRIMARY_DARK : hitRatePct >= 50 ? _constants.COLOR.SECONDARY : _constants.COLOR.ERROR;
179
+ const topNames = Array.isArray(topSlowest) ? topSlowest.map(e => `${e.filename}(${e.durationMs}ms)`).join(", ") : "";
180
+ const measureName = `${_constants.PREFIX.CACHE}/summary`;
181
+ const startMark = `${measureName}:start:${Math.round(startTime)}`;
182
+ performance.mark(startMark, {
183
+ startTime
184
+ });
185
+ this._measure.emit(measureName, startMark, endTime, {
186
+ hits,
187
+ misses,
188
+ fetches,
189
+ total,
190
+ hitRatePct,
191
+ avgFetchMs,
192
+ topSlowest: topNames,
193
+ simulatorValid: false
194
+ }, _constants.TRACK.CACHE, color);
195
+
196
+ // Notify InsightsCollector so it can flag low hit rates
197
+ this._notifyInsights?.({
198
+ hitRatePct,
199
+ total,
200
+ avgFetchMs,
201
+ startTime,
202
+ endTime
203
+ });
204
+ }
205
+ _filename(url) {
206
+ try {
207
+ const path = new URL(url).pathname;
208
+ const name = path.split("/").filter(Boolean).pop() ?? path;
209
+ return name.slice(0, 40);
210
+ } catch {
211
+ return String(url).slice(0, 40);
212
+ }
213
+ }
214
+ }
215
+ exports.CacheCollector = CacheCollector;
@@ -1,4 +1,11 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.HookCollector=void 0;var _constants=require("../core/constants.js");/**
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.HookCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
2
9
  * HookCollector.js — Option A (symptom detection only)
3
10
  *
4
11
  * Instruments: bugs #13, #15, #16
@@ -23,21 +30,136 @@
23
30
  * TODO (Option B — future):
24
31
  * Expose window.catalyst.markComponentMount(name) / markComponentReady(name)
25
32
  * for precise hook-lifecycle spans without symptom inference.
26
- */const LOOP_LOAF_COUNT=3;// ≥3 LoAFs in the loop window = computation loop
27
- const LOOP_WINDOW_MS=5000;// rolling window to count sustained LoAFs
28
- class HookCollector{constructor(measure){this._measure=measure;// Lazy-load detection state
29
- this._pendingLazy=null;// { startMark, startTime, target, loafCount, totalBlocking, timer }
30
- // Computation loop detection state
31
- this._loopLoafs=[];// [{ start, end }] in rolling LOOP_WINDOW_MS window
32
- }init(){// Watch for taps that might trigger lazy component loads
33
- document.addEventListener("pointerdown",this._onPointerDown.bind(this),{passive:true});}/** Called by index.js to feed the normalized perf store. */setWaterfallSource(fn){this._notifyWaterfall=fn;}// Called by RenderCollector via addLoafListener() on every LoAF
34
- onLoaf(start,end){// ── Lazy-load window ────────────────────────────────────────────────
35
- if(this._pendingLazy){const p=this._pendingLazy;p.loafCount++;p.totalBlocking+=end-start;// Reset the close timer — extend window on each LoAF
36
- clearTimeout(p.timer);p.timer=setTimeout(()=>this._closeLazySpan(),_constants.THRESHOLD.LAZY_LOAF_WINDOW_MS);}// ── Computation loop detection ───────────────────────────────────────
37
- const now=performance.now();this._loopLoafs.push({start,end});// Evict entries outside the rolling window
38
- this._loopLoafs=this._loopLoafs.filter(e=>now-e.start<LOOP_WINDOW_MS);if(this._loopLoafs.length>=LOOP_LOAF_COUNT){this._emitLoopSpan();this._loopLoafs=[];// reset after emitting to avoid duplicate spans
39
- }}// ─── Lazy-load span ───────────────────────────────────────────────────────
40
- _onPointerDown(e){// If a lazy span is already open, let it close naturally
41
- if(this._pendingLazy)return;const target=e.target?.tagName?.toLowerCase()??"unknown";const startTime=performance.now();const startMark=`${_constants.PREFIX.HOOK_LAZY}:start:${Math.round(startTime)}`;performance.mark(startMark,{startTime});const timer=setTimeout(()=>this._closeLazySpan(),_constants.THRESHOLD.LAZY_LOAF_WINDOW_MS);this._pendingLazy={startMark,startTime,target,loafCount:0,totalBlocking:0,timer};}_closeLazySpan(){const p=this._pendingLazy;if(!p)return;this._pendingLazy=null;clearTimeout(p.timer);// Only emit if LoAF actually fired — otherwise it was a clean interaction
42
- if(p.loafCount===0)return;const endTime=performance.now();this._measure.emit(`${_constants.PREFIX.HOOK_LAZY}|${p.target}`,p.startMark,endTime,{loafCount:p.loafCount,totalBlockingMs:Math.round(p.totalBlocking),triggerTarget:p.target,likelyCause:"lazy-load",simulatorValid:false},_constants.TRACK.HOOK);this._notifyWaterfall?.({hookType:"lazy-load",label:`Hook lazy-load ${p.target}`,startTime:p.startTime,endTime,durationMs:Math.round(endTime-p.startTime),loafCount:p.loafCount,totalBlockingMs:Math.round(p.totalBlocking),likelyCause:"lazy-load"});}// ─── Computation loop span ────────────────────────────────────────────────
43
- _emitLoopSpan(){const first=this._loopLoafs[0];const last=this._loopLoafs[this._loopLoafs.length-1];const markName=`${_constants.PREFIX.HOOK_MOUNT}:loop:${Math.round(first.start)}`;performance.mark(markName,{startTime:first.start});const totalBlocking=this._loopLoafs.reduce((sum,e)=>sum+(e.end-e.start),0);this._measure.emit(`${_constants.PREFIX.HOOK_MOUNT}|loop`,markName,last.end,{loafCount:this._loopLoafs.length,windowMs:Math.round(last.end-first.start),totalBlockingMs:Math.round(totalBlocking),likelyCause:"computation-loop",simulatorValid:false},_constants.TRACK.HOOK);this._notifyWaterfall?.({hookType:"computation-loop",label:"Hook computation loop",startTime:first.start,endTime:last.end,durationMs:Math.round(last.end-first.start),loafCount:this._loopLoafs.length,totalBlockingMs:Math.round(totalBlocking),likelyCause:"computation-loop"});}}exports.HookCollector=HookCollector;
33
+ */
34
+
35
+ const LOOP_LOAF_COUNT = 3; // ≥3 LoAFs in the loop window = computation loop
36
+ const LOOP_WINDOW_MS = 5000; // rolling window to count sustained LoAFs
37
+
38
+ class HookCollector {
39
+ constructor(measure) {
40
+ this._measure = measure;
41
+
42
+ // Lazy-load detection state
43
+ this._pendingLazy = null; // { startMark, startTime, target, loafCount, totalBlocking, timer }
44
+
45
+ // Computation loop detection state
46
+ this._loopLoafs = []; // [{ start, end }] in rolling LOOP_WINDOW_MS window
47
+ }
48
+ init() {
49
+ // Watch for taps that might trigger lazy component loads
50
+ document.addEventListener("pointerdown", this._onPointerDown.bind(this), {
51
+ passive: true
52
+ });
53
+ }
54
+
55
+ /** Called by index.js to feed the normalized perf store. */
56
+ setWaterfallSource(fn) {
57
+ this._notifyWaterfall = fn;
58
+ }
59
+
60
+ // Called by RenderCollector via addLoafListener() on every LoAF
61
+ onLoaf(start, end) {
62
+ // ── Lazy-load window ────────────────────────────────────────────────
63
+ if (this._pendingLazy) {
64
+ const p = this._pendingLazy;
65
+ p.loafCount++;
66
+ p.totalBlocking += end - start;
67
+
68
+ // Reset the close timer — extend window on each LoAF
69
+ clearTimeout(p.timer);
70
+ p.timer = setTimeout(() => this._closeLazySpan(), _constants.THRESHOLD.LAZY_LOAF_WINDOW_MS);
71
+ }
72
+
73
+ // ── Computation loop detection ───────────────────────────────────────
74
+ const now = performance.now();
75
+ this._loopLoafs.push({
76
+ start,
77
+ end
78
+ });
79
+ // Evict entries outside the rolling window
80
+ this._loopLoafs = this._loopLoafs.filter(e => now - e.start < LOOP_WINDOW_MS);
81
+ if (this._loopLoafs.length >= LOOP_LOAF_COUNT) {
82
+ this._emitLoopSpan();
83
+ this._loopLoafs = []; // reset after emitting to avoid duplicate spans
84
+ }
85
+ }
86
+
87
+ // ─── Lazy-load span ───────────────────────────────────────────────────────
88
+
89
+ _onPointerDown(e) {
90
+ // If a lazy span is already open, let it close naturally
91
+ if (this._pendingLazy) return;
92
+ const target = e.target?.tagName?.toLowerCase() ?? "unknown";
93
+ const startTime = performance.now();
94
+ const startMark = `${_constants.PREFIX.HOOK_LAZY}:start:${Math.round(startTime)}`;
95
+ performance.mark(startMark, {
96
+ startTime
97
+ });
98
+ const timer = setTimeout(() => this._closeLazySpan(), _constants.THRESHOLD.LAZY_LOAF_WINDOW_MS);
99
+ this._pendingLazy = {
100
+ startMark,
101
+ startTime,
102
+ target,
103
+ loafCount: 0,
104
+ totalBlocking: 0,
105
+ timer
106
+ };
107
+ }
108
+ _closeLazySpan() {
109
+ const p = this._pendingLazy;
110
+ if (!p) return;
111
+ this._pendingLazy = null;
112
+ clearTimeout(p.timer);
113
+
114
+ // Only emit if LoAF actually fired — otherwise it was a clean interaction
115
+ if (p.loafCount === 0) return;
116
+ const endTime = performance.now();
117
+ this._measure.emit(`${_constants.PREFIX.HOOK_LAZY}|${p.target}`, p.startMark, endTime, {
118
+ loafCount: p.loafCount,
119
+ totalBlockingMs: Math.round(p.totalBlocking),
120
+ triggerTarget: p.target,
121
+ likelyCause: "lazy-load",
122
+ simulatorValid: false
123
+ }, _constants.TRACK.HOOK);
124
+ this._notifyWaterfall?.({
125
+ hookType: "lazy-load",
126
+ label: `Hook lazy-load ${p.target}`,
127
+ startTime: p.startTime,
128
+ endTime,
129
+ durationMs: Math.round(endTime - p.startTime),
130
+ loafCount: p.loafCount,
131
+ totalBlockingMs: Math.round(p.totalBlocking),
132
+ likelyCause: "lazy-load"
133
+ });
134
+ }
135
+
136
+ // ─── Computation loop span ────────────────────────────────────────────────
137
+
138
+ _emitLoopSpan() {
139
+ const first = this._loopLoafs[0];
140
+ const last = this._loopLoafs[this._loopLoafs.length - 1];
141
+ const markName = `${_constants.PREFIX.HOOK_MOUNT}:loop:${Math.round(first.start)}`;
142
+ performance.mark(markName, {
143
+ startTime: first.start
144
+ });
145
+ const totalBlocking = this._loopLoafs.reduce((sum, e) => sum + (e.end - e.start), 0);
146
+ this._measure.emit(`${_constants.PREFIX.HOOK_MOUNT}|loop`, markName, last.end, {
147
+ loafCount: this._loopLoafs.length,
148
+ windowMs: Math.round(last.end - first.start),
149
+ totalBlockingMs: Math.round(totalBlocking),
150
+ likelyCause: "computation-loop",
151
+ simulatorValid: false
152
+ }, _constants.TRACK.HOOK);
153
+ this._notifyWaterfall?.({
154
+ hookType: "computation-loop",
155
+ label: "Hook computation loop",
156
+ startTime: first.start,
157
+ endTime: last.end,
158
+ durationMs: Math.round(last.end - first.start),
159
+ loafCount: this._loopLoafs.length,
160
+ totalBlockingMs: Math.round(totalBlocking),
161
+ likelyCause: "computation-loop"
162
+ });
163
+ }
164
+ }
165
+ exports.HookCollector = HookCollector;