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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/babel.native.config.cjs +8 -0
  2. package/bin/catalyst.js +49 -105
  3. package/changelog.md +8 -0
  4. package/dist/caching.js +1 -1
  5. package/dist/index.jsx +8 -0
  6. package/dist/logger.js +74 -3
  7. package/dist/native/androidProject/app/build.gradle.kts +1 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
  19. package/dist/native/androidSetup.js +300 -8
  20. package/dist/native/bridge/WebBridge.js +481 -59
  21. package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
  22. package/dist/native/bridge/errors.js +250 -26
  23. package/dist/native/bridge/hooks/index.js +125 -1
  24. package/dist/native/bridge/hooks/useCamera.js +347 -9
  25. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  26. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  27. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  28. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  29. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  30. package/dist/native/bridge/hooks/useIntent.js +95 -1
  31. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  32. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  33. package/dist/native/bridge/hooks/useNotification.js +318 -2
  34. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  35. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  36. package/dist/native/bridge/hooks/utils.js +27 -1
  37. package/dist/native/bridge/hooks.js +16 -1
  38. package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
  39. package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
  40. package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
  41. package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
  42. package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
  43. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
  44. package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
  45. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
  46. package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
  47. package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
  48. package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
  49. package/dist/native/bridge/perf/core/constants.js +126 -0
  50. package/dist/native/bridge/perf/core/measure.js +189 -0
  51. package/dist/native/bridge/perf/core/session.js +100 -0
  52. package/dist/native/bridge/perf/core/store.js +554 -0
  53. package/dist/native/bridge/perf/core/summary.js +174 -0
  54. package/dist/native/bridge/perf/core/traceExport.js +240 -0
  55. package/dist/native/bridge/perf/core/waterfalls.js +146 -0
  56. package/dist/native/bridge/perf/index.js +402 -0
  57. package/dist/native/bridge/safeArea.js +53 -5
  58. package/dist/native/bridge/standardizedHooks.js +595 -82
  59. package/dist/native/bridge/useBaseHook.js +274 -38
  60. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  61. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  62. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  63. package/dist/native/buildAndroid/assets.js +510 -13
  64. package/dist/native/buildAndroid/build.js +296 -19
  65. package/dist/native/buildAndroid/config.js +87 -1
  66. package/dist/native/buildAndroid/index.js +374 -1
  67. package/dist/native/buildAppAndroid.js +34 -2
  68. package/dist/native/buildAppIos.js +62 -2
  69. package/dist/native/buildIos/assets.js +609 -8
  70. package/dist/native/buildIos/build.js +639 -20
  71. package/dist/native/buildIos/config.js +294 -12
  72. package/dist/native/buildIos/index.js +343 -6
  73. package/dist/native/buildIos/plugins.js +343 -16
  74. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  75. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  76. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  77. package/dist/native/internalPluginUtils.js +232 -7
  78. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
  89. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
  90. package/dist/native/package.json +3 -0
  91. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  92. package/dist/native/pluginComposerAndroid.js +407 -23
  93. package/dist/native/pluginComposerIos.js +435 -11
  94. package/dist/native/renameAndroidProject.js +1094 -88
  95. package/dist/native/setupEmulatorIos.js +289 -22
  96. package/dist/native/setupServer.js +160 -19
  97. package/dist/native/terminalProgress.js +199 -11
  98. package/dist/native/utils.js +193 -15
  99. package/dist/offline/catalyst-sw.js +145 -1
  100. package/dist/offline/registerServiceWorker.js +33 -1
  101. package/dist/otel.js +375 -46
  102. package/dist/router/ClientRouter.js +14 -2
  103. package/dist/router/ServerRouter.js +9 -1
  104. package/dist/router.js +1 -1
  105. package/dist/scripts/build.js +112 -3
  106. package/dist/scripts/checkVersion.js +3 -1
  107. package/dist/scripts/cleanCache.js +30 -1
  108. package/dist/scripts/generateOfflineManifest.js +116 -1
  109. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  110. package/dist/scripts/plugins.js +309 -1
  111. package/dist/scripts/preServerInit.js +13 -0
  112. package/dist/scripts/scriptUtils.js +54 -3
  113. package/dist/scripts/serve.js +51 -4
  114. package/dist/scripts/start.js +47 -10
  115. package/dist/scripts/validator.js +149 -1
  116. package/dist/sentry.cjs +127 -0
  117. package/dist/sentry.js +125 -5
  118. package/dist/server/expressServer.js +250 -5
  119. package/dist/server/manifestCache.js +36 -0
  120. package/dist/server/renderer/ChunkExtractor.js +144 -0
  121. package/dist/server/renderer/document/Body.jsx +59 -0
  122. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  123. package/dist/server/renderer/document/Head.jsx +60 -0
  124. package/dist/server/renderer/document/index.jsx +2 -0
  125. package/dist/server/renderer/extract.js +149 -31
  126. package/dist/server/renderer/handler.jsx +433 -0
  127. package/dist/server/renderer/index.js +8 -1
  128. package/dist/server/renderer/render.js +47 -0
  129. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  130. package/dist/server/utils/cjsRequire.js +10 -0
  131. package/dist/server/utils/userAgentUtil.js +87 -9
  132. package/dist/server/utils/validator.js +164 -1
  133. package/dist/vite/FastRefresh.jsx +20 -0
  134. package/dist/vite/inject-cache-key-plugin.js +276 -0
  135. package/dist/vite/loadCustomViteConfig.js +22 -0
  136. package/dist/vite/manifest-categorization-plugin.js +455 -0
  137. package/dist/vite/node-loader.mjs +458 -0
  138. package/dist/vite/scssParams.js +21 -0
  139. package/dist/vite/vite.config.client.js +130 -0
  140. package/dist/vite/vite.config.js +422 -0
  141. package/dist/vite/vite.config.server.js +84 -0
  142. package/dist/web-router/components/MetaTag.jsx +34 -0
  143. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  144. package/dist/web-router/components/Split.jsx +200 -0
  145. package/dist/web-router/components/SplitInview.jsx +106 -0
  146. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  147. package/dist/web-router/context.jsx +12 -0
  148. package/dist/web-router/hooks.jsx +72 -0
  149. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  150. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  151. package/dist/web-router/types/context.d.ts +14 -0
  152. package/dist/web-router/types/hooks.d.ts +1 -0
  153. package/dist/web-router/types/index.d.ts +3 -0
  154. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  155. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  156. package/mcp_v2/conversion-tasks.json +35 -17
  157. package/mcp_v2/knowledge-base.json +124 -13
  158. package/mcp_v2/lib/helpers.js +10 -0
  159. package/mcp_v2/mcp.js +4 -4
  160. package/mcp_v2/package.json +1 -0
  161. package/mcp_v2/tools/build.js +116 -20
  162. package/mcp_v2/tools/conversion.js +106 -5
  163. package/mcp_v2/tools/tasks.js +18 -14
  164. package/package.json +35 -68
  165. package/babel.config.js +0 -32
  166. package/dist/index.js +0 -1
  167. package/dist/scripts/devBuild.js +0 -3
  168. package/dist/scripts/devServe.js +0 -3
  169. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  170. package/dist/scripts/registerAliases.js +0 -6
  171. package/dist/server/renderer/document/Body.js +0 -11
  172. package/dist/server/renderer/document/Head.js +0 -9
  173. package/dist/server/renderer/document/index.js +0 -1
  174. package/dist/server/renderer/handler.js +0 -47
  175. package/dist/server/startServer.js +0 -57
  176. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  177. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  178. package/dist/webpack/babel.config.client.js +0 -1
  179. package/dist/webpack/babel.config.ssr.js +0 -1
  180. package/dist/webpack/base.babel.js +0 -7
  181. package/dist/webpack/development.client.babel.js +0 -10
  182. package/dist/webpack/production.client.babel.js +0 -4
  183. package/dist/webpack/production.ssr.babel.js +0 -8
  184. package/dist/webpack/scssParams.js +0 -9
@@ -0,0 +1,215 @@
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
+ /**
9
+ * CacheCollector.js
10
+ *
11
+ * Instruments: bugs #1, #2, #10
12
+ * #1 page slowness OTC pages → cache miss → slow fetch → resource blocks render
13
+ * #2 back-forward reloads screen → back navigation triggers a burst of cache misses
14
+ * #10 footer flash on Drug/OTC → cache miss for footer chunk → layout shift after load
15
+ *
16
+ * Native events received in two modes:
17
+ *
18
+ * 1. Live (legacy path — only used if buffer disabled):
19
+ * cache-hit-memory / cache-hit-disk / cache-miss-fetch / network-fetch-complete
20
+ * → Each emits immediately to Catalyst > Cache track via onNativeEvent()
21
+ *
22
+ * 2. Batched (primary path via __catalystPerfBatch drain in index.js):
23
+ * Same event types arrive as part of the native buffer flush after onPageFinished.
24
+ * JS drains via requestIdleCallback → onBatchEvent() → emits spans on Catalyst > Cache > Detail
25
+ *
26
+ * cache-summary { hits, misses, fetches, total, hitRatePct, avgFetchMs, topSlowest[] }
27
+ * → One summary span emitted on main Catalyst > Cache track for InsightsCollector
28
+ *
29
+ * Spans emitted:
30
+ * Catalyst > Cache > Detail individual spans (one per file) during idle drain
31
+ * Catalyst > Cache summary span (hit rate, avg fetch, top slowest)
32
+ *
33
+ * Reading the track:
34
+ * Blue = cache HIT memory — fastest path
35
+ * Blue-light = cache HIT disk — good, slightly slower
36
+ * Red = cache MISS — matched pattern but not cached
37
+ * Yellow/Orange = network fetch — orange if slow >300ms
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;
@@ -0,0 +1,165 @@
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
+ /**
9
+ * HookCollector.js — Option A (symptom detection only)
10
+ *
11
+ * Instruments: bugs #13, #15, #16
12
+ * #13 cart computation loop jank → LoAF bursts sustained over time, not tied to a single interaction
13
+ * detected as: LoAF count > threshold within a rolling window
14
+ * #15 quantity selector slow first → interaction → first LoAF burst → long gap → paint
15
+ * detected as: LoAF fires within LAZY_LOAF_WINDOW_MS after tap
16
+ * #16 prescription screen touch lag → same pattern as #15
17
+ *
18
+ * Option A strategy (no React hooks needed):
19
+ * - Watch for LoAF bursts within 2s of a pointerdown (taps that might trigger lazy load)
20
+ * - If LoAF count > 0 in that window → emit hook/lazy span showing the blocked period
21
+ * - Sustained LoAF (3+ frames in 5s with no user interaction) → emit hook/mount span
22
+ * flagging a background computation loop
23
+ *
24
+ * Spans emitted:
25
+ * catalyst:hook/lazy|<label> — LoAF burst after interaction (Track: Hook)
26
+ * detail: loafCount, totalBlockingMs, triggerTarget
27
+ * catalyst:hook/mount|loop — sustained background LoAF (Track: Hook)
28
+ * detail: loafCount, windowMs, likelyCause='computation-loop'
29
+ *
30
+ * TODO (Option B — future):
31
+ * Expose window.catalyst.markComponentMount(name) / markComponentReady(name)
32
+ * for precise hook-lifecycle spans without symptom inference.
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;