@fluxiapi/react 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1278 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/devtools/FluxDevTools.tsx
9
+ import { useState as useState3, useEffect as useEffect2, useCallback } from "react";
10
+
11
+ // src/context.tsx
12
+ import {
13
+ createContext,
14
+ useContext,
15
+ useEffect,
16
+ useRef,
17
+ useSyncExternalStore
18
+ } from "react";
19
+
20
+ // src/scanner-bridge.ts
21
+ var ScannerBridge = class {
22
+ constructor(config = {}) {
23
+ this._listeners = /* @__PURE__ */ new Set();
24
+ this._analyzeTimer = null;
25
+ this._tickTimer = null;
26
+ // Lazy-loaded scan engine classes
27
+ this._scanner = null;
28
+ this._analyzer = null;
29
+ this._config = {
30
+ network: config.network ?? "wifi",
31
+ analysisInterval: config.analysisInterval ?? 3e3,
32
+ autoStart: config.autoStart ?? true,
33
+ verbose: config.verbose ?? false
34
+ };
35
+ this._state = {
36
+ scanning: false,
37
+ requests: [],
38
+ report: null,
39
+ score: 100,
40
+ violations: [],
41
+ startTime: 0,
42
+ elapsed: 0,
43
+ network: this._config.network,
44
+ framework: null
45
+ };
46
+ }
47
+ // ─── State Management ───────────────────────────────────────
48
+ get state() {
49
+ return this._state;
50
+ }
51
+ subscribe(listener) {
52
+ this._listeners.add(listener);
53
+ return () => this._listeners.delete(listener);
54
+ }
55
+ _emit() {
56
+ const snapshot = { ...this._state };
57
+ this._listeners.forEach((fn) => fn(snapshot));
58
+ }
59
+ _update(partial) {
60
+ this._state = { ...this._state, ...partial };
61
+ this._emit();
62
+ }
63
+ // ─── Scan Lifecycle ─────────────────────────────────────────
64
+ async start() {
65
+ if (this._state.scanning) return;
66
+ try {
67
+ const scanModule = await import("@fluxiapi/scan");
68
+ this._scanner = new scanModule.FluxScanner({
69
+ duration: Infinity,
70
+ // We control the stop
71
+ network: this._config.network,
72
+ verbose: this._config.verbose
73
+ });
74
+ this._analyzer = new scanModule.FluxAnalyzer({
75
+ network: this._config.network
76
+ });
77
+ this._scanner.start();
78
+ this._update({
79
+ scanning: true,
80
+ startTime: Date.now(),
81
+ elapsed: 0,
82
+ requests: [],
83
+ report: null,
84
+ violations: [],
85
+ score: 100
86
+ });
87
+ this._tickTimer = setInterval(() => {
88
+ if (!this._scanner) return;
89
+ const requests = this._scanner.getRequests?.() ?? [];
90
+ this._update({
91
+ elapsed: Math.round((Date.now() - this._state.startTime) / 1e3),
92
+ requests
93
+ });
94
+ }, 500);
95
+ this._analyzeTimer = setInterval(() => {
96
+ this._runAnalysis();
97
+ }, this._config.analysisInterval);
98
+ if (this._config.verbose) {
99
+ console.log("[FluxAPI] Scanner started");
100
+ }
101
+ } catch (err) {
102
+ console.error("[FluxAPI] Failed to start scanner:", err);
103
+ }
104
+ }
105
+ stop() {
106
+ if (!this._state.scanning || !this._scanner) return null;
107
+ if (this._tickTimer) clearInterval(this._tickTimer);
108
+ if (this._analyzeTimer) clearInterval(this._analyzeTimer);
109
+ this._tickTimer = null;
110
+ this._analyzeTimer = null;
111
+ const session = this._scanner.stop();
112
+ const report = this._analyzer?.analyze(session) ?? null;
113
+ this._update({
114
+ scanning: false,
115
+ report,
116
+ score: report?.score?.overall ?? 100,
117
+ violations: report?.violations ?? [],
118
+ framework: session?.stack?.framework?.name ?? null
119
+ });
120
+ if (this._config.verbose) {
121
+ console.log("[FluxAPI] Scanner stopped. Score:", report?.score?.overall);
122
+ }
123
+ return report;
124
+ }
125
+ reset() {
126
+ this.stop();
127
+ this._update({
128
+ scanning: false,
129
+ requests: [],
130
+ report: null,
131
+ score: 100,
132
+ violations: [],
133
+ startTime: 0,
134
+ elapsed: 0,
135
+ framework: null
136
+ });
137
+ }
138
+ setNetwork(network) {
139
+ this._config.network = network;
140
+ this._update({ network });
141
+ if (this._analyzer) {
142
+ try {
143
+ const scanModule = __require("@fluxiapi/scan");
144
+ this._analyzer = new scanModule.FluxAnalyzer({ network });
145
+ } catch {
146
+ }
147
+ }
148
+ }
149
+ // ─── Analysis ───────────────────────────────────────────────
150
+ _runAnalysis() {
151
+ if (!this._scanner || !this._analyzer) return;
152
+ try {
153
+ const session = this._scanner.stop();
154
+ this._scanner.start();
155
+ const report = this._analyzer.analyze(session);
156
+ this._update({
157
+ report,
158
+ score: report?.score?.overall ?? 100,
159
+ violations: report?.violations ?? [],
160
+ framework: session?.stack?.framework?.name ?? null
161
+ });
162
+ } catch (err) {
163
+ if (this._config.verbose) {
164
+ console.error("[FluxAPI] Analysis error:", err);
165
+ }
166
+ }
167
+ }
168
+ // ─── TanStack Query Integration ─────────────────────────────
169
+ /**
170
+ * Capture a TanStack Query event. Called by FluxQueryClient wrapper.
171
+ */
172
+ captureQueryEvent(event) {
173
+ if (this._config.verbose) {
174
+ console.log("[FluxAPI] Query event:", event.type, event.queryKey);
175
+ }
176
+ }
177
+ /**
178
+ * Capture a SWR event.
179
+ */
180
+ captureSWREvent(event) {
181
+ if (this._config.verbose) {
182
+ console.log("[FluxAPI] SWR event:", event.type, event.key);
183
+ }
184
+ }
185
+ destroy() {
186
+ this.stop();
187
+ this._listeners.clear();
188
+ this._scanner = null;
189
+ this._analyzer = null;
190
+ }
191
+ };
192
+ var _globalBridge = null;
193
+ function getGlobalBridge(config) {
194
+ if (!_globalBridge) {
195
+ _globalBridge = new ScannerBridge(config);
196
+ }
197
+ return _globalBridge;
198
+ }
199
+ function resetGlobalBridge() {
200
+ _globalBridge?.destroy();
201
+ _globalBridge = null;
202
+ }
203
+
204
+ // src/context.tsx
205
+ import { jsx } from "react/jsx-runtime";
206
+ var FluxContext = createContext(null);
207
+ function FluxProvider({
208
+ children,
209
+ network,
210
+ analysisInterval,
211
+ autoStart = true,
212
+ verbose = false,
213
+ bridge: customBridge
214
+ }) {
215
+ const bridgeRef = useRef(
216
+ customBridge ?? getGlobalBridge({ network, analysisInterval, autoStart, verbose })
217
+ );
218
+ const bridge = bridgeRef.current;
219
+ const state = useSyncExternalStore(
220
+ (callback) => bridge.subscribe(callback),
221
+ () => bridge.state,
222
+ () => bridge.state
223
+ // server snapshot
224
+ );
225
+ useEffect(() => {
226
+ if (autoStart && !bridge.state.scanning) {
227
+ bridge.start();
228
+ }
229
+ return () => {
230
+ };
231
+ }, [autoStart, bridge]);
232
+ return /* @__PURE__ */ jsx(FluxContext.Provider, { value: { bridge, state }, children });
233
+ }
234
+ function useFlux() {
235
+ const ctx = useContext(FluxContext);
236
+ if (!ctx) {
237
+ throw new Error(
238
+ "useFlux must be used within a <FluxProvider>. Wrap your app with <FluxProvider> or use <FluxDevTools /> which includes it."
239
+ );
240
+ }
241
+ return ctx;
242
+ }
243
+ function useFluxBridge() {
244
+ return useFlux().bridge;
245
+ }
246
+ function useFluxState() {
247
+ return useFlux().state;
248
+ }
249
+
250
+ // src/devtools/styles.ts
251
+ var COLORS = {
252
+ bg: "#0f0f13",
253
+ bg2: "#16161d",
254
+ bg3: "#1e1e28",
255
+ border: "#2a2a3a",
256
+ fg: "#e2e2e6",
257
+ fg2: "#a0a0b0",
258
+ fg3: "#6a6a7a",
259
+ accent: "#7c6afc",
260
+ accent2: "#a78bfa",
261
+ green: "#22c55e",
262
+ blue: "#3b82f6",
263
+ orange: "#f59e0b",
264
+ red: "#ef4444",
265
+ cyan: "#06b6d4"
266
+ };
267
+ var FONTS = {
268
+ sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
269
+ mono: '"SF Mono", "Fira Code", "JetBrains Mono", Menlo, monospace'
270
+ };
271
+ var containerStyle = {
272
+ position: "fixed",
273
+ zIndex: 2147483647,
274
+ fontFamily: FONTS.sans,
275
+ fontSize: "12px",
276
+ lineHeight: "1.5",
277
+ color: COLORS.fg,
278
+ WebkitFontSmoothing: "antialiased"
279
+ };
280
+ var badgeStyle = (score, position) => {
281
+ const color = score >= 90 ? COLORS.green : score >= 70 ? COLORS.blue : score >= 50 ? COLORS.orange : COLORS.red;
282
+ const pos = parsePosition(position);
283
+ return {
284
+ ...pos,
285
+ width: "48px",
286
+ height: "48px",
287
+ borderRadius: "50%",
288
+ background: COLORS.bg,
289
+ border: `2px solid ${color}`,
290
+ cursor: "pointer",
291
+ display: "flex",
292
+ alignItems: "center",
293
+ justifyContent: "center",
294
+ flexDirection: "column",
295
+ boxShadow: `0 4px 20px rgba(0,0,0,0.5), 0 0 0 1px ${COLORS.border}`,
296
+ transition: "all 0.2s ease",
297
+ userSelect: "none"
298
+ };
299
+ };
300
+ var badgeScoreStyle = (score) => ({
301
+ fontSize: "14px",
302
+ fontWeight: 800,
303
+ lineHeight: "1",
304
+ color: score >= 90 ? COLORS.green : score >= 70 ? COLORS.blue : score >= 50 ? COLORS.orange : COLORS.red
305
+ });
306
+ var badgeLabelStyle = {
307
+ fontSize: "6px",
308
+ fontWeight: 600,
309
+ color: COLORS.fg3,
310
+ letterSpacing: "0.5px",
311
+ textTransform: "uppercase"
312
+ };
313
+ var scanningDotStyle = {
314
+ width: "10px",
315
+ height: "10px",
316
+ borderRadius: "50%",
317
+ background: COLORS.accent,
318
+ animation: "fluxPulse 1.5s ease-in-out infinite"
319
+ };
320
+ var panelStyle = (position) => {
321
+ const isRight = position.includes("right");
322
+ const isBottom = position.includes("bottom");
323
+ return {
324
+ position: "fixed",
325
+ [isBottom ? "bottom" : "top"]: "16px",
326
+ [isRight ? "right" : "left"]: "16px",
327
+ width: "380px",
328
+ maxHeight: "min(580px, calc(100vh - 40px))",
329
+ background: COLORS.bg,
330
+ border: `1px solid ${COLORS.border}`,
331
+ borderRadius: "12px",
332
+ boxShadow: "0 8px 40px rgba(0,0,0,0.6)",
333
+ display: "flex",
334
+ flexDirection: "column",
335
+ overflow: "hidden"
336
+ };
337
+ };
338
+ var panelHeaderStyle = {
339
+ display: "flex",
340
+ alignItems: "center",
341
+ justifyContent: "space-between",
342
+ padding: "10px 14px",
343
+ background: COLORS.bg2,
344
+ borderBottom: `1px solid ${COLORS.border}`,
345
+ cursor: "move"
346
+ };
347
+ var panelTitleStyle = {
348
+ display: "flex",
349
+ alignItems: "center",
350
+ gap: "8px",
351
+ fontSize: "13px",
352
+ fontWeight: 700,
353
+ color: COLORS.fg
354
+ };
355
+ var panelCloseBtnStyle = {
356
+ background: "none",
357
+ border: "none",
358
+ color: COLORS.fg3,
359
+ cursor: "pointer",
360
+ fontSize: "16px",
361
+ padding: "2px 4px",
362
+ borderRadius: "4px",
363
+ lineHeight: "1"
364
+ };
365
+ var tabBarStyle = {
366
+ display: "flex",
367
+ borderBottom: `1px solid ${COLORS.border}`,
368
+ background: COLORS.bg2
369
+ };
370
+ var tabStyle = (active) => ({
371
+ flex: 1,
372
+ padding: "7px 0",
373
+ fontSize: "10px",
374
+ fontWeight: 600,
375
+ textAlign: "center",
376
+ cursor: "pointer",
377
+ color: active ? COLORS.accent : COLORS.fg3,
378
+ borderBottom: active ? `2px solid ${COLORS.accent}` : "2px solid transparent",
379
+ background: "none",
380
+ border: "none",
381
+ borderBottomStyle: "solid",
382
+ transition: "all 0.15s",
383
+ textTransform: "uppercase",
384
+ letterSpacing: "0.5px"
385
+ });
386
+ var panelBodyStyle = {
387
+ flex: 1,
388
+ overflow: "auto",
389
+ padding: "12px"
390
+ };
391
+ var scoreRingContainerStyle = {
392
+ display: "flex",
393
+ alignItems: "center",
394
+ gap: "16px",
395
+ marginBottom: "14px"
396
+ };
397
+ var scoreRingStyle = {
398
+ width: "72px",
399
+ height: "72px",
400
+ position: "relative",
401
+ flexShrink: 0
402
+ };
403
+ var scoreNumberStyle = (color) => ({
404
+ position: "absolute",
405
+ top: "50%",
406
+ left: "50%",
407
+ transform: "translate(-50%, -55%)",
408
+ fontSize: "22px",
409
+ fontWeight: 800,
410
+ color,
411
+ lineHeight: "1"
412
+ });
413
+ var scoreGradeStyle = (color) => ({
414
+ position: "absolute",
415
+ top: "50%",
416
+ left: "50%",
417
+ transform: "translate(-50%, 50%)",
418
+ fontSize: "8px",
419
+ fontWeight: 600,
420
+ color,
421
+ textTransform: "uppercase",
422
+ letterSpacing: "0.5px"
423
+ });
424
+ var catRowStyle = {
425
+ display: "flex",
426
+ alignItems: "center",
427
+ gap: "8px",
428
+ marginBottom: "6px",
429
+ fontSize: "11px"
430
+ };
431
+ var catNameStyle = {
432
+ width: "70px",
433
+ color: COLORS.fg2
434
+ };
435
+ var catTrackStyle = {
436
+ flex: 1,
437
+ height: "6px",
438
+ borderRadius: "3px",
439
+ background: COLORS.bg3,
440
+ overflow: "hidden"
441
+ };
442
+ var catFillStyle = (pct, color) => ({
443
+ width: `${pct}%`,
444
+ height: "100%",
445
+ borderRadius: "3px",
446
+ background: color,
447
+ transition: "width 0.5s ease"
448
+ });
449
+ var catPctStyle = (color) => ({
450
+ width: "32px",
451
+ textAlign: "right",
452
+ fontWeight: 700,
453
+ fontFamily: FONTS.mono,
454
+ fontSize: "10px",
455
+ color
456
+ });
457
+ var violationCardStyle = {
458
+ background: COLORS.bg2,
459
+ border: `1px solid ${COLORS.border}`,
460
+ borderRadius: "8px",
461
+ padding: "10px",
462
+ marginBottom: "8px"
463
+ };
464
+ var violationHeaderStyle = {
465
+ display: "flex",
466
+ alignItems: "center",
467
+ gap: "6px",
468
+ marginBottom: "4px"
469
+ };
470
+ var severityDotStyle = (severity) => ({
471
+ width: "8px",
472
+ height: "8px",
473
+ borderRadius: "50%",
474
+ flexShrink: 0,
475
+ background: severity === "critical" ? COLORS.red : severity === "warning" ? COLORS.orange : COLORS.blue
476
+ });
477
+ var ruleIdBadgeStyle = {
478
+ fontSize: "9px",
479
+ fontWeight: 700,
480
+ fontFamily: FONTS.mono,
481
+ color: COLORS.accent,
482
+ padding: "1px 4px",
483
+ borderRadius: "3px",
484
+ background: "rgba(124, 106, 252, 0.12)"
485
+ };
486
+ var violationTitleStyle = {
487
+ fontSize: "11px",
488
+ fontWeight: 600,
489
+ color: COLORS.fg,
490
+ flex: 1
491
+ };
492
+ var violationDescStyle = {
493
+ fontSize: "10px",
494
+ color: COLORS.fg3,
495
+ lineHeight: "1.4"
496
+ };
497
+ var violationImpactStyle = {
498
+ display: "flex",
499
+ gap: "10px",
500
+ marginTop: "6px",
501
+ fontSize: "10px"
502
+ };
503
+ var impactItemStyle = (color) => ({
504
+ color,
505
+ fontWeight: 600,
506
+ fontFamily: FONTS.mono
507
+ });
508
+ var requestRowStyle = {
509
+ display: "flex",
510
+ alignItems: "center",
511
+ gap: "6px",
512
+ padding: "4px 0",
513
+ borderBottom: `1px solid ${COLORS.border}`,
514
+ fontSize: "10px"
515
+ };
516
+ var methodBadgeStyle = (method) => ({
517
+ fontSize: "9px",
518
+ fontWeight: 700,
519
+ fontFamily: FONTS.mono,
520
+ padding: "1px 4px",
521
+ borderRadius: "3px",
522
+ color: method === "GET" ? COLORS.green : method === "POST" ? COLORS.blue : COLORS.orange,
523
+ background: method === "GET" ? "rgba(34, 197, 94, 0.1)" : method === "POST" ? "rgba(59, 130, 246, 0.1)" : "rgba(245, 158, 11, 0.1)",
524
+ minWidth: "32px",
525
+ textAlign: "center"
526
+ });
527
+ var pathStyle = {
528
+ flex: 1,
529
+ color: COLORS.fg2,
530
+ fontFamily: FONTS.mono,
531
+ fontSize: "10px",
532
+ overflow: "hidden",
533
+ textOverflow: "ellipsis",
534
+ whiteSpace: "nowrap"
535
+ };
536
+ var durationStyle = (ms) => ({
537
+ fontFamily: FONTS.mono,
538
+ fontWeight: 600,
539
+ fontSize: "10px",
540
+ color: ms > 500 ? COLORS.red : ms > 200 ? COLORS.orange : COLORS.green,
541
+ minWidth: "40px",
542
+ textAlign: "right"
543
+ });
544
+ var statusStyle = (status) => ({
545
+ fontFamily: FONTS.mono,
546
+ fontSize: "9px",
547
+ fontWeight: 600,
548
+ color: status >= 500 ? COLORS.red : status >= 400 ? COLORS.orange : status >= 300 ? COLORS.blue : COLORS.green,
549
+ minWidth: "24px",
550
+ textAlign: "center"
551
+ });
552
+ var statsGridStyle = {
553
+ display: "grid",
554
+ gridTemplateColumns: "1fr 1fr 1fr",
555
+ gap: "8px",
556
+ marginBottom: "14px"
557
+ };
558
+ var statCardStyle = (color) => ({
559
+ background: COLORS.bg2,
560
+ border: `1px solid ${COLORS.border}`,
561
+ borderRadius: "8px",
562
+ padding: "8px",
563
+ textAlign: "center"
564
+ });
565
+ var statNumStyle = (color) => ({
566
+ fontSize: "18px",
567
+ fontWeight: 800,
568
+ color,
569
+ lineHeight: "1.2"
570
+ });
571
+ var statLabelStyle = {
572
+ fontSize: "9px",
573
+ color: COLORS.fg3,
574
+ textTransform: "uppercase",
575
+ letterSpacing: "0.3px"
576
+ };
577
+ var infoRowStyle = {
578
+ display: "flex",
579
+ alignItems: "center",
580
+ gap: "6px",
581
+ padding: "6px 10px",
582
+ background: COLORS.bg2,
583
+ borderRadius: "6px",
584
+ marginBottom: "8px",
585
+ fontSize: "10px",
586
+ color: COLORS.fg2
587
+ };
588
+ var emptyStateStyle = {
589
+ textAlign: "center",
590
+ padding: "24px",
591
+ color: COLORS.fg3,
592
+ fontSize: "12px"
593
+ };
594
+ function parsePosition(pos) {
595
+ switch (pos) {
596
+ case "bottom-right":
597
+ return { bottom: "16px", right: "16px" };
598
+ case "bottom-left":
599
+ return { bottom: "16px", left: "16px" };
600
+ case "top-right":
601
+ return { top: "16px", right: "16px" };
602
+ case "top-left":
603
+ return { top: "16px", left: "16px" };
604
+ default:
605
+ return { bottom: "16px", right: "16px" };
606
+ }
607
+ }
608
+ var _stylesInjected = false;
609
+ function injectKeyframes() {
610
+ if (_stylesInjected || typeof document === "undefined") return;
611
+ _stylesInjected = true;
612
+ const style = document.createElement("style");
613
+ style.textContent = `
614
+ @keyframes fluxPulse {
615
+ 0%, 100% { opacity: 1; transform: scale(1); }
616
+ 50% { opacity: 0.5; transform: scale(0.85); }
617
+ }
618
+ @keyframes fluxFadeIn {
619
+ from { opacity: 0; transform: translateY(8px); }
620
+ to { opacity: 1; transform: translateY(0); }
621
+ }
622
+ .flux-devtools * { box-sizing: border-box; }
623
+ .flux-devtools ::-webkit-scrollbar { width: 4px; }
624
+ .flux-devtools ::-webkit-scrollbar-track { background: transparent; }
625
+ .flux-devtools ::-webkit-scrollbar-thumb { background: #2a2a3a; border-radius: 2px; }
626
+ `;
627
+ document.head.appendChild(style);
628
+ }
629
+
630
+ // src/devtools/Badge.tsx
631
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
632
+ function Badge({ state, position, onClick }) {
633
+ const { score, scanning, violations } = state;
634
+ const critCount = violations.filter((v) => v.severity === "critical").length;
635
+ return /* @__PURE__ */ jsx2(
636
+ "div",
637
+ {
638
+ style: badgeStyle(score, position),
639
+ onClick,
640
+ onMouseEnter: (e) => {
641
+ e.currentTarget.style.transform = "scale(1.1)";
642
+ },
643
+ onMouseLeave: (e) => {
644
+ e.currentTarget.style.transform = "scale(1)";
645
+ },
646
+ title: scanning ? `Scanning... ${state.requests.length} requests` : `API Score: ${score}/100 \xB7 ${violations.length} issues`,
647
+ children: scanning ? /* @__PURE__ */ jsxs(Fragment, { children: [
648
+ /* @__PURE__ */ jsx2("div", { style: scanningDotStyle }),
649
+ /* @__PURE__ */ jsx2("div", { style: { ...badgeLabelStyle, marginTop: "2px" }, children: "SCAN" })
650
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
651
+ /* @__PURE__ */ jsx2("div", { style: badgeScoreStyle(score), children: score }),
652
+ /* @__PURE__ */ jsx2("div", { style: badgeLabelStyle, children: critCount > 0 ? `${critCount}!` : "API" })
653
+ ] })
654
+ }
655
+ );
656
+ }
657
+
658
+ // src/devtools/Panel.tsx
659
+ import { useState as useState2, useMemo } from "react";
660
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
661
+ var RULE_NAMES = {
662
+ E1: "Request Waterfall",
663
+ E2: "Duplicate Requests",
664
+ E3: "N+1 Pattern",
665
+ E4: "Over-fetching",
666
+ E5: "Batchable Requests",
667
+ C1: "No Cache",
668
+ C2: "Under-Caching",
669
+ C3: "Over-Caching",
670
+ C4: "Missing Revalidation",
671
+ P1: "Missing Prefetch",
672
+ P2: "Unnecessary Polling",
673
+ P3: "No Error Recovery",
674
+ P4: "Uncompressed"
675
+ };
676
+ function Panel({ state, bridge, position, onClose }) {
677
+ const [tab, setTab] = useState2("overview");
678
+ const { report, violations, requests, scanning, elapsed, score, framework } = state;
679
+ const apiRequests = useMemo(
680
+ () => requests.filter((r) => r.type === "api-rest" || r.type === "api-graphql"),
681
+ [requests]
682
+ );
683
+ return /* @__PURE__ */ jsxs2("div", { style: panelStyle(position), className: "flux-devtools", children: [
684
+ /* @__PURE__ */ jsxs2("div", { style: panelHeaderStyle, children: [
685
+ /* @__PURE__ */ jsxs2("div", { style: panelTitleStyle, children: [
686
+ /* @__PURE__ */ jsx3("span", { children: "\u26A1" }),
687
+ /* @__PURE__ */ jsx3("span", { children: "FluxAPI" }),
688
+ scanning && /* @__PURE__ */ jsx3("span", { style: { color: COLORS.accent, fontSize: "10px", fontWeight: 500 }, children: "\u25CF Recording" }),
689
+ !scanning && report && /* @__PURE__ */ jsxs2("span", { style: {
690
+ color: score >= 70 ? COLORS.green : score >= 50 ? COLORS.orange : COLORS.red,
691
+ fontSize: "10px",
692
+ fontWeight: 700
693
+ }, children: [
694
+ score,
695
+ "/100"
696
+ ] })
697
+ ] }),
698
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "4px" }, children: [
699
+ scanning ? /* @__PURE__ */ jsx3(
700
+ "button",
701
+ {
702
+ style: { ...panelCloseBtnStyle, color: COLORS.red, fontSize: "10px" },
703
+ onClick: () => bridge.stop(),
704
+ children: "\u23F9 Stop"
705
+ }
706
+ ) : /* @__PURE__ */ jsx3(
707
+ "button",
708
+ {
709
+ style: { ...panelCloseBtnStyle, color: COLORS.green, fontSize: "10px" },
710
+ onClick: () => bridge.start(),
711
+ children: "\u25B6 Scan"
712
+ }
713
+ ),
714
+ /* @__PURE__ */ jsx3("button", { style: panelCloseBtnStyle, onClick: onClose, children: "\u2715" })
715
+ ] })
716
+ ] }),
717
+ /* @__PURE__ */ jsx3("div", { style: tabBarStyle, children: ["overview", "violations", "requests"].map((t) => /* @__PURE__ */ jsxs2(
718
+ "button",
719
+ {
720
+ style: tabStyle(tab === t),
721
+ onClick: () => setTab(t),
722
+ children: [
723
+ t,
724
+ t === "violations" && violations.length > 0 && /* @__PURE__ */ jsx3("span", { style: {
725
+ marginLeft: "4px",
726
+ background: COLORS.red,
727
+ color: "#fff",
728
+ borderRadius: "8px",
729
+ padding: "0 5px",
730
+ fontSize: "9px",
731
+ fontWeight: 700
732
+ }, children: violations.length }),
733
+ t === "requests" && /* @__PURE__ */ jsx3("span", { style: {
734
+ marginLeft: "4px",
735
+ color: COLORS.fg3,
736
+ fontSize: "9px"
737
+ }, children: apiRequests.length })
738
+ ]
739
+ },
740
+ t
741
+ )) }),
742
+ /* @__PURE__ */ jsxs2("div", { style: panelBodyStyle, children: [
743
+ tab === "overview" && /* @__PURE__ */ jsx3(
744
+ OverviewTab,
745
+ {
746
+ state,
747
+ apiRequests,
748
+ violations
749
+ }
750
+ ),
751
+ tab === "violations" && /* @__PURE__ */ jsx3(ViolationsTab, { violations }),
752
+ tab === "requests" && /* @__PURE__ */ jsx3(RequestsTab, { requests: apiRequests })
753
+ ] })
754
+ ] });
755
+ }
756
+ function OverviewTab({
757
+ state,
758
+ apiRequests,
759
+ violations
760
+ }) {
761
+ const { report, score, scanning, elapsed, framework } = state;
762
+ const scoreObj = report?.score;
763
+ const color = score >= 90 ? COLORS.green : score >= 70 ? COLORS.blue : score >= 50 ? COLORS.orange : COLORS.red;
764
+ const dash = Math.round(score / 100 * 251);
765
+ const crits = violations.filter((v) => v.severity === "critical").length;
766
+ const warns = violations.filter((v) => v.severity === "warning").length;
767
+ const infos = violations.filter((v) => v.severity === "info").length;
768
+ const totalTimeSaved = violations.reduce((s, v) => s + (v.impact?.timeSavedMs ?? 0), 0);
769
+ const totalReqsSaved = violations.reduce((s, v) => s + (v.impact?.requestsEliminated ?? 0), 0);
770
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
771
+ /* @__PURE__ */ jsxs2("div", { style: scoreRingContainerStyle, children: [
772
+ /* @__PURE__ */ jsxs2("div", { style: scoreRingStyle, children: [
773
+ /* @__PURE__ */ jsxs2("svg", { viewBox: "0 0 100 100", style: { transform: "rotate(-90deg)" }, children: [
774
+ /* @__PURE__ */ jsx3("circle", { cx: "50", cy: "50", r: "40", fill: "none", stroke: COLORS.bg3, strokeWidth: "6" }),
775
+ /* @__PURE__ */ jsx3(
776
+ "circle",
777
+ {
778
+ cx: "50",
779
+ cy: "50",
780
+ r: "40",
781
+ fill: "none",
782
+ stroke: color,
783
+ strokeWidth: "6",
784
+ strokeDasharray: `${dash} 251`,
785
+ strokeLinecap: "round"
786
+ }
787
+ )
788
+ ] }),
789
+ /* @__PURE__ */ jsx3("div", { style: scoreNumberStyle(color), children: score }),
790
+ /* @__PURE__ */ jsx3("div", { style: scoreGradeStyle(color), children: score >= 90 ? "A+" : score >= 70 ? "B" : score >= 50 ? "C" : "F" })
791
+ ] }),
792
+ /* @__PURE__ */ jsxs2("div", { children: [
793
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "13px", fontWeight: 700, marginBottom: "4px" }, children: "API Health" }),
794
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: "10px", color: COLORS.fg3 }, children: [
795
+ apiRequests.length,
796
+ " API calls \xB7 ",
797
+ elapsed,
798
+ "s"
799
+ ] }),
800
+ framework && /* @__PURE__ */ jsxs2("div", { style: { fontSize: "10px", color: COLORS.accent, marginTop: "2px" }, children: [
801
+ "\u269B\uFE0F ",
802
+ framework
803
+ ] })
804
+ ] })
805
+ ] }),
806
+ /* @__PURE__ */ jsxs2("div", { style: statsGridStyle, children: [
807
+ /* @__PURE__ */ jsxs2("div", { style: statCardStyle(COLORS.red), children: [
808
+ /* @__PURE__ */ jsx3("div", { style: statNumStyle(COLORS.red), children: crits }),
809
+ /* @__PURE__ */ jsx3("div", { style: statLabelStyle, children: "Critical" })
810
+ ] }),
811
+ /* @__PURE__ */ jsxs2("div", { style: statCardStyle(COLORS.orange), children: [
812
+ /* @__PURE__ */ jsx3("div", { style: statNumStyle(COLORS.orange), children: warns }),
813
+ /* @__PURE__ */ jsx3("div", { style: statLabelStyle, children: "Warnings" })
814
+ ] }),
815
+ /* @__PURE__ */ jsxs2("div", { style: statCardStyle(COLORS.blue), children: [
816
+ /* @__PURE__ */ jsx3("div", { style: statNumStyle(COLORS.blue), children: apiRequests.length }),
817
+ /* @__PURE__ */ jsx3("div", { style: statLabelStyle, children: "API Calls" })
818
+ ] })
819
+ ] }),
820
+ (totalTimeSaved > 0 || totalReqsSaved > 0) && /* @__PURE__ */ jsxs2("div", { style: infoRowStyle, children: [
821
+ totalTimeSaved > 0 && /* @__PURE__ */ jsxs2("span", { style: { color: COLORS.blue, fontWeight: 600 }, children: [
822
+ "\u26A1 ",
823
+ fmtMs(totalTimeSaved),
824
+ " saveable"
825
+ ] }),
826
+ totalReqsSaved > 0 && /* @__PURE__ */ jsxs2("span", { style: { color: COLORS.green, fontWeight: 600 }, children: [
827
+ "\u{1F4C9} ",
828
+ totalReqsSaved,
829
+ " fewer requests"
830
+ ] })
831
+ ] }),
832
+ scoreObj && (() => {
833
+ const cats = scoreObj.categories ?? [];
834
+ const getCat = (cat) => cats.find((c) => c.category === cat)?.score ?? 100;
835
+ return /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "12px" }, children: [
836
+ /* @__PURE__ */ jsx3(CategoryBar, { icon: "\u26A1", name: "Efficiency", score: getCat("efficiency") }),
837
+ /* @__PURE__ */ jsx3(CategoryBar, { icon: "\u{1F4BE}", name: "Caching", score: getCat("caching") }),
838
+ /* @__PURE__ */ jsx3(CategoryBar, { icon: "\u{1F504}", name: "Patterns", score: getCat("patterns") })
839
+ ] });
840
+ })(),
841
+ violations.length > 0 && /* @__PURE__ */ jsxs2("div", { children: [
842
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "10px", fontWeight: 700, color: COLORS.fg2, marginBottom: "6px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: "Top Issues" }),
843
+ violations.slice(0, 3).map((v, i) => /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: "6px", padding: "3px 0", fontSize: "10px" }, children: [
844
+ /* @__PURE__ */ jsx3("span", { style: severityDotStyle(v.severity) }),
845
+ /* @__PURE__ */ jsx3("span", { style: ruleIdBadgeStyle, children: v.ruleId }),
846
+ /* @__PURE__ */ jsx3("span", { style: { color: COLORS.fg2, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: v.title }),
847
+ v.impact?.timeSavedMs > 0 && /* @__PURE__ */ jsxs2("span", { style: { color: COLORS.blue, fontSize: "9px", fontWeight: 600 }, children: [
848
+ "\u26A1",
849
+ fmtMs(v.impact.timeSavedMs)
850
+ ] })
851
+ ] }, i))
852
+ ] }),
853
+ violations.length === 0 && !scanning && apiRequests.length > 0 && /* @__PURE__ */ jsxs2("div", { style: emptyStateStyle, children: [
854
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u2728" }),
855
+ /* @__PURE__ */ jsx3("div", { style: { color: COLORS.green, fontWeight: 700 }, children: "No API issues found!" })
856
+ ] }),
857
+ scanning && apiRequests.length === 0 && /* @__PURE__ */ jsxs2("div", { style: emptyStateStyle, children: [
858
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u{1F4E1}" }),
859
+ "Waiting for API requests..."
860
+ ] })
861
+ ] });
862
+ }
863
+ function ViolationsTab({ violations }) {
864
+ const [expanded, setExpanded] = useState2(null);
865
+ if (violations.length === 0) {
866
+ return /* @__PURE__ */ jsxs2("div", { style: emptyStateStyle, children: [
867
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u2728" }),
868
+ "No violations detected"
869
+ ] });
870
+ }
871
+ return /* @__PURE__ */ jsx3(Fragment2, { children: violations.map((v, i) => /* @__PURE__ */ jsxs2("div", { style: violationCardStyle, children: [
872
+ /* @__PURE__ */ jsxs2(
873
+ "div",
874
+ {
875
+ style: { ...violationHeaderStyle, cursor: "pointer" },
876
+ onClick: () => setExpanded(expanded === i ? null : i),
877
+ children: [
878
+ /* @__PURE__ */ jsx3("span", { style: severityDotStyle(v.severity) }),
879
+ /* @__PURE__ */ jsx3("span", { style: ruleIdBadgeStyle, children: v.ruleId }),
880
+ /* @__PURE__ */ jsx3("span", { style: { ...violationTitleStyle, fontSize: "10px", color: COLORS.fg3 }, children: RULE_NAMES[v.ruleId] || v.ruleId }),
881
+ /* @__PURE__ */ jsx3("span", { style: { color: COLORS.fg3, fontSize: "10px" }, children: expanded === i ? "\u25BE" : "\u25B8" })
882
+ ]
883
+ }
884
+ ),
885
+ /* @__PURE__ */ jsx3("div", { style: { ...violationTitleStyle, marginTop: "2px" }, children: v.title }),
886
+ /* @__PURE__ */ jsxs2("div", { style: violationImpactStyle, children: [
887
+ v.impact?.timeSavedMs > 0 && /* @__PURE__ */ jsxs2("span", { style: impactItemStyle(COLORS.blue), children: [
888
+ "\u26A1 ",
889
+ fmtMs(v.impact.timeSavedMs)
890
+ ] }),
891
+ v.impact?.requestsEliminated > 0 && /* @__PURE__ */ jsxs2("span", { style: impactItemStyle(COLORS.green), children: [
892
+ "\u{1F4C9} ",
893
+ v.impact.requestsEliminated,
894
+ " reqs"
895
+ ] }),
896
+ v.impact?.bandwidthSavedBytes > 0 && /* @__PURE__ */ jsxs2("span", { style: impactItemStyle(COLORS.orange), children: [
897
+ "\u{1F4BE} ",
898
+ fmtBytes(v.impact.bandwidthSavedBytes)
899
+ ] })
900
+ ] }),
901
+ expanded === i && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "8px" }, children: [
902
+ /* @__PURE__ */ jsx3("div", { style: violationDescStyle, children: v.description }),
903
+ v.affectedEndpoints && v.affectedEndpoints.length > 0 && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "6px" }, children: [
904
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "9px", color: COLORS.fg3, fontWeight: 600, marginBottom: "3px" }, children: "ENDPOINTS" }),
905
+ v.affectedEndpoints.slice(0, 5).map((ep, j) => /* @__PURE__ */ jsx3("div", { style: { fontSize: "10px", fontFamily: FONTS.mono, color: COLORS.fg2, padding: "1px 0" }, children: ep }, j))
906
+ ] }),
907
+ v.metadata?.fix && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "6px" }, children: [
908
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "9px", color: COLORS.fg3, fontWeight: 600, marginBottom: "3px" }, children: "FIX" }),
909
+ /* @__PURE__ */ jsx3("pre", { style: {
910
+ fontSize: "9px",
911
+ fontFamily: FONTS.mono,
912
+ color: COLORS.accent2,
913
+ background: COLORS.bg,
914
+ borderRadius: "4px",
915
+ padding: "6px",
916
+ overflow: "auto",
917
+ maxHeight: "120px",
918
+ lineHeight: "1.4",
919
+ whiteSpace: "pre-wrap",
920
+ wordBreak: "break-all"
921
+ }, children: typeof v.metadata.fix === "string" ? v.metadata.fix : v.metadata.fix?.code ?? "" })
922
+ ] })
923
+ ] })
924
+ ] }, i)) });
925
+ }
926
+ function RequestsTab({ requests }) {
927
+ const sorted = useMemo(
928
+ () => [...requests].sort((a, b) => b.startTime - a.startTime),
929
+ [requests]
930
+ );
931
+ if (sorted.length === 0) {
932
+ return /* @__PURE__ */ jsxs2("div", { style: emptyStateStyle, children: [
933
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u{1F4E1}" }),
934
+ "No API requests captured yet"
935
+ ] });
936
+ }
937
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
938
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: "10px", color: COLORS.fg3, marginBottom: "6px" }, children: [
939
+ sorted.length,
940
+ " API requests (newest first)"
941
+ ] }),
942
+ sorted.slice(0, 50).map((r, i) => {
943
+ const path = r.urlParts?.pathname ?? new URL(r.url, "http://x").pathname;
944
+ const short = path.length > 40 ? "\u2026" + path.slice(-37) : path;
945
+ const status = r.response?.status ?? 0;
946
+ return /* @__PURE__ */ jsxs2("div", { style: requestRowStyle, children: [
947
+ /* @__PURE__ */ jsx3("span", { style: methodBadgeStyle(r.method), children: r.method }),
948
+ /* @__PURE__ */ jsx3("span", { style: pathStyle, title: r.url, children: short }),
949
+ status > 0 && /* @__PURE__ */ jsx3("span", { style: statusStyle(status), children: status }),
950
+ /* @__PURE__ */ jsxs2("span", { style: durationStyle(r.duration ?? 0), children: [
951
+ r.duration ?? 0,
952
+ "ms"
953
+ ] })
954
+ ] }, i);
955
+ })
956
+ ] });
957
+ }
958
+ function CategoryBar({ icon, name, score }) {
959
+ const color = score >= 70 ? COLORS.green : score >= 50 ? COLORS.orange : COLORS.red;
960
+ return /* @__PURE__ */ jsxs2("div", { style: catRowStyle, children: [
961
+ /* @__PURE__ */ jsx3("span", { children: icon }),
962
+ /* @__PURE__ */ jsx3("span", { style: catNameStyle, children: name }),
963
+ /* @__PURE__ */ jsx3("div", { style: catTrackStyle, children: /* @__PURE__ */ jsx3("div", { style: catFillStyle(score, color) }) }),
964
+ /* @__PURE__ */ jsxs2("span", { style: catPctStyle(color), children: [
965
+ score,
966
+ "%"
967
+ ] })
968
+ ] });
969
+ }
970
+ function fmtMs(ms) {
971
+ return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
972
+ }
973
+ function fmtBytes(bytes) {
974
+ if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)}MB`;
975
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
976
+ return `${bytes}B`;
977
+ }
978
+
979
+ // src/devtools/FluxDevTools.tsx
980
+ import { jsx as jsx4 } from "react/jsx-runtime";
981
+ function FluxDevTools({
982
+ position = "bottom-right",
983
+ network,
984
+ analysisInterval,
985
+ autoStart = true,
986
+ defaultOpen = false,
987
+ verbose = false,
988
+ bridge,
989
+ forceShow = false,
990
+ shortcut = "ctrl+shift+f"
991
+ }) {
992
+ if (!forceShow && typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
993
+ return null;
994
+ }
995
+ return /* @__PURE__ */ jsx4(
996
+ FluxProvider,
997
+ {
998
+ network,
999
+ analysisInterval,
1000
+ autoStart,
1001
+ verbose,
1002
+ bridge,
1003
+ children: /* @__PURE__ */ jsx4(
1004
+ DevToolsInner,
1005
+ {
1006
+ position,
1007
+ defaultOpen,
1008
+ shortcut
1009
+ }
1010
+ )
1011
+ }
1012
+ );
1013
+ }
1014
+ function DevToolsInner({
1015
+ position,
1016
+ defaultOpen,
1017
+ shortcut
1018
+ }) {
1019
+ const [open, setOpen] = useState3(defaultOpen);
1020
+ const { bridge, state } = useFlux();
1021
+ useEffect2(() => {
1022
+ injectKeyframes();
1023
+ }, []);
1024
+ useEffect2(() => {
1025
+ if (!shortcut) return;
1026
+ const parts = shortcut.toLowerCase().split("+");
1027
+ const key = parts.pop();
1028
+ const needCtrl = parts.includes("ctrl") || parts.includes("control");
1029
+ const needShift = parts.includes("shift");
1030
+ const needAlt = parts.includes("alt");
1031
+ const needMeta = parts.includes("meta") || parts.includes("cmd");
1032
+ function handleKeydown(e) {
1033
+ if (e.key.toLowerCase() === key && e.ctrlKey === needCtrl && e.shiftKey === needShift && e.altKey === needAlt && e.metaKey === needMeta) {
1034
+ e.preventDefault();
1035
+ setOpen((prev) => !prev);
1036
+ }
1037
+ }
1038
+ window.addEventListener("keydown", handleKeydown);
1039
+ return () => window.removeEventListener("keydown", handleKeydown);
1040
+ }, [shortcut]);
1041
+ const toggle = useCallback(() => setOpen((prev) => !prev), []);
1042
+ return /* @__PURE__ */ jsx4("div", { style: containerStyle, className: "flux-devtools", children: open ? /* @__PURE__ */ jsx4(
1043
+ Panel,
1044
+ {
1045
+ state,
1046
+ bridge,
1047
+ position,
1048
+ onClose: toggle
1049
+ }
1050
+ ) : /* @__PURE__ */ jsx4(
1051
+ Badge,
1052
+ {
1053
+ state,
1054
+ position,
1055
+ onClick: toggle
1056
+ }
1057
+ ) });
1058
+ }
1059
+
1060
+ // src/hooks/index.ts
1061
+ import { useMemo as useMemo2 } from "react";
1062
+ function useFluxScore() {
1063
+ const { state } = useFlux();
1064
+ return useMemo2(() => {
1065
+ const score = state.report?.score;
1066
+ const overall = score?.overall ?? 100;
1067
+ const cats = score?.categories ?? [];
1068
+ const getCat = (label) => cats.find((c) => c.category === label)?.score ?? 100;
1069
+ return {
1070
+ overall,
1071
+ grade: score?.grade ?? "excellent",
1072
+ efficiency: getCat("efficiency"),
1073
+ caching: getCat("caching"),
1074
+ patterns: getCat("patterns"),
1075
+ color: overall >= 90 ? "#22c55e" : overall >= 70 ? "#3b82f6" : overall >= 50 ? "#f59e0b" : "#ef4444"
1076
+ };
1077
+ }, [state.report?.score]);
1078
+ }
1079
+ function useFluxViolations(filter) {
1080
+ const { state } = useFlux();
1081
+ return useMemo2(() => {
1082
+ let violations = state.violations;
1083
+ if (filter?.severity) {
1084
+ violations = violations.filter((v) => v.severity === filter.severity);
1085
+ }
1086
+ if (filter?.ruleId) {
1087
+ violations = violations.filter((v) => v.ruleId === filter.ruleId);
1088
+ }
1089
+ if (filter?.category) {
1090
+ const prefixMap = {
1091
+ efficiency: ["E1", "E2", "E3", "E4", "E5"],
1092
+ caching: ["C1", "C2", "C3", "C4"],
1093
+ patterns: ["P1", "P2", "P3", "P4"]
1094
+ };
1095
+ const allowed = prefixMap[filter.category] ?? [];
1096
+ violations = violations.filter((v) => allowed.includes(v.ruleId));
1097
+ }
1098
+ return violations;
1099
+ }, [state.violations, filter?.severity, filter?.ruleId, filter?.category]);
1100
+ }
1101
+ function useFluxRequests(filter) {
1102
+ const { state } = useFlux();
1103
+ return useMemo2(() => {
1104
+ let requests = state.requests;
1105
+ if (filter?.type) {
1106
+ requests = requests.filter((r) => r.type === filter.type);
1107
+ }
1108
+ if (filter?.method) {
1109
+ requests = requests.filter((r) => r.method === filter.method);
1110
+ }
1111
+ if (filter?.minDuration) {
1112
+ requests = requests.filter((r) => (r.duration ?? 0) >= (filter.minDuration ?? 0));
1113
+ }
1114
+ return requests;
1115
+ }, [state.requests, filter?.type, filter?.method, filter?.minDuration]);
1116
+ }
1117
+ function useFluxReport() {
1118
+ const { state } = useFlux();
1119
+ return state.report;
1120
+ }
1121
+ function useFluxScanning() {
1122
+ const { bridge, state } = useFlux();
1123
+ return useMemo2(
1124
+ () => ({
1125
+ scanning: state.scanning,
1126
+ elapsed: state.elapsed,
1127
+ requestCount: state.requests.length,
1128
+ start: () => bridge.start(),
1129
+ stop: () => bridge.stop(),
1130
+ reset: () => bridge.reset()
1131
+ }),
1132
+ [bridge, state.scanning, state.elapsed, state.requests.length]
1133
+ );
1134
+ }
1135
+
1136
+ // src/integrations/tanstack.ts
1137
+ function wrapQueryClient(queryClient, bridge) {
1138
+ const _bridge = bridge ?? getGlobalBridge();
1139
+ try {
1140
+ const qc = queryClient;
1141
+ if (typeof qc.getQueryCache === "function") {
1142
+ const cache = qc.getQueryCache();
1143
+ if (typeof cache.subscribe === "function") {
1144
+ cache.subscribe((event) => {
1145
+ if (!event.query) return;
1146
+ const { query } = event;
1147
+ const eventType = event.type;
1148
+ if (eventType === "added" || eventType === "updated") {
1149
+ _bridge.captureQueryEvent({
1150
+ type: eventType === "added" ? "query-added" : "query-updated",
1151
+ queryKey: query.queryKey,
1152
+ state: {
1153
+ status: query.state.status,
1154
+ fetchStatus: query.state.fetchStatus,
1155
+ dataUpdatedAt: query.state.dataUpdatedAt
1156
+ },
1157
+ options: {
1158
+ staleTime: query.options.staleTime,
1159
+ gcTime: query.options.gcTime,
1160
+ refetchInterval: query.options.refetchInterval,
1161
+ enabled: query.options.enabled,
1162
+ retry: query.options.retry
1163
+ }
1164
+ });
1165
+ } else if (eventType === "removed") {
1166
+ _bridge.captureQueryEvent({
1167
+ type: "query-removed",
1168
+ queryKey: query.queryKey
1169
+ });
1170
+ }
1171
+ });
1172
+ }
1173
+ }
1174
+ } catch (err) {
1175
+ console.warn("[FluxAPI] Failed to instrument QueryClient:", err);
1176
+ }
1177
+ return queryClient;
1178
+ }
1179
+ function extractQueryMetrics(queryClient) {
1180
+ const metrics = {
1181
+ uniqueQueries: 0,
1182
+ queriesWithoutStaleTime: 0,
1183
+ pollingQueries: 0,
1184
+ noRetryQueries: 0,
1185
+ avgStaleTime: 0
1186
+ };
1187
+ try {
1188
+ if (typeof queryClient.getQueryCache !== "function") return metrics;
1189
+ const cache = queryClient.getQueryCache();
1190
+ const queries = cache.getAll?.() ?? [];
1191
+ metrics.uniqueQueries = queries.length;
1192
+ let totalStaleTime = 0;
1193
+ let staleTimeCount = 0;
1194
+ queries.forEach((q) => {
1195
+ const opts = q.options ?? {};
1196
+ if (!opts.staleTime || opts.staleTime === 0) {
1197
+ metrics.queriesWithoutStaleTime++;
1198
+ } else {
1199
+ totalStaleTime += opts.staleTime;
1200
+ staleTimeCount++;
1201
+ }
1202
+ if (opts.refetchInterval && opts.refetchInterval !== false) {
1203
+ metrics.pollingQueries++;
1204
+ }
1205
+ if (opts.retry === false || opts.retry === 0) {
1206
+ metrics.noRetryQueries++;
1207
+ }
1208
+ });
1209
+ metrics.avgStaleTime = staleTimeCount > 0 ? Math.round(totalStaleTime / staleTimeCount) : 0;
1210
+ } catch {
1211
+ }
1212
+ return metrics;
1213
+ }
1214
+
1215
+ // src/integrations/swr.ts
1216
+ function createFluxSWRMiddleware(bridge) {
1217
+ const _bridge = bridge ?? getGlobalBridge();
1218
+ return (useSWRNext) => {
1219
+ return (key, fetcher, config) => {
1220
+ const resolvedKey = typeof key === "function" ? (() => {
1221
+ try {
1222
+ return key();
1223
+ } catch {
1224
+ return null;
1225
+ }
1226
+ })() : key;
1227
+ const keyStr = resolvedKey ? typeof resolvedKey === "string" ? resolvedKey : JSON.stringify(resolvedKey) : "null";
1228
+ _bridge.captureSWREvent({
1229
+ type: "swr-request",
1230
+ key: keyStr,
1231
+ config: {
1232
+ refreshInterval: config.refreshInterval,
1233
+ dedupingInterval: config.dedupingInterval,
1234
+ revalidateOnFocus: config.revalidateOnFocus,
1235
+ errorRetryCount: config.errorRetryCount
1236
+ }
1237
+ });
1238
+ const instrumentedFetcher = fetcher ? async (...args) => {
1239
+ const start = Date.now();
1240
+ try {
1241
+ const result = await fetcher(...args);
1242
+ _bridge.captureSWREvent({
1243
+ type: "swr-success",
1244
+ key: keyStr
1245
+ });
1246
+ return result;
1247
+ } catch (err) {
1248
+ _bridge.captureSWREvent({
1249
+ type: "swr-error",
1250
+ key: keyStr
1251
+ });
1252
+ throw err;
1253
+ }
1254
+ } : fetcher;
1255
+ return useSWRNext(key, instrumentedFetcher, config);
1256
+ };
1257
+ };
1258
+ }
1259
+ var fluxSWRMiddleware = createFluxSWRMiddleware();
1260
+ export {
1261
+ FluxDevTools,
1262
+ FluxProvider,
1263
+ ScannerBridge,
1264
+ createFluxSWRMiddleware,
1265
+ extractQueryMetrics,
1266
+ fluxSWRMiddleware,
1267
+ getGlobalBridge,
1268
+ resetGlobalBridge,
1269
+ useFlux,
1270
+ useFluxBridge,
1271
+ useFluxReport,
1272
+ useFluxRequests,
1273
+ useFluxScanning,
1274
+ useFluxScore,
1275
+ useFluxState,
1276
+ useFluxViolations,
1277
+ wrapQueryClient
1278
+ };