@usefy/use-memory-monitor 0.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1077 @@
1
+ // src/useMemoryMonitor.ts
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useSyncExternalStore
8
+ } from "react";
9
+
10
+ // src/constants.ts
11
+ var DEFAULT_INTERVAL = 5e3;
12
+ var DEFAULT_HISTORY_SIZE = 50;
13
+ var DEFAULT_WARNING_THRESHOLD = 70;
14
+ var DEFAULT_CRITICAL_THRESHOLD = 90;
15
+ var DEFAULT_LEAK_WINDOW_SIZE = 10;
16
+ var MIN_LEAK_DETECTION_SAMPLES = 5;
17
+ var LEAK_SENSITIVITY_CONFIG = {
18
+ low: {
19
+ minSlope: 1e5,
20
+ // 100KB/sample
21
+ minR2: 0.7,
22
+ probabilityMultiplier: 0.8
23
+ },
24
+ medium: {
25
+ minSlope: 5e4,
26
+ // 50KB/sample
27
+ minR2: 0.6,
28
+ probabilityMultiplier: 1
29
+ },
30
+ high: {
31
+ minSlope: 1e4,
32
+ // 10KB/sample
33
+ minR2: 0.5,
34
+ probabilityMultiplier: 1.2
35
+ }
36
+ };
37
+ var TREND_THRESHOLDS = {
38
+ increasing: 0.01,
39
+ // Slope > 0.01 = increasing
40
+ decreasing: -0.01
41
+ // Slope < -0.01 = decreasing
42
+ };
43
+ var DEFAULT_OPTIONS = {
44
+ interval: DEFAULT_INTERVAL,
45
+ autoStart: true,
46
+ enabled: true,
47
+ enableHistory: false,
48
+ historySize: DEFAULT_HISTORY_SIZE,
49
+ thresholds: {
50
+ warning: DEFAULT_WARNING_THRESHOLD,
51
+ critical: DEFAULT_CRITICAL_THRESHOLD
52
+ },
53
+ leakDetection: {
54
+ enabled: false,
55
+ sensitivity: "medium",
56
+ windowSize: DEFAULT_LEAK_WINDOW_SIZE,
57
+ threshold: void 0
58
+ },
59
+ devMode: false,
60
+ trackDOMNodes: false,
61
+ trackEventListeners: false,
62
+ logToConsole: false,
63
+ disableInProduction: false,
64
+ fallbackStrategy: "dom-only"
65
+ };
66
+ var DEFAULT_SEVERITY = "normal";
67
+ var DEFAULT_TREND = "stable";
68
+ var SSR_INITIAL_STATE = {
69
+ memory: null,
70
+ domNodes: null,
71
+ eventListeners: null,
72
+ isMonitoring: false,
73
+ severity: DEFAULT_SEVERITY,
74
+ lastUpdated: 0
75
+ };
76
+ var SSR_FORMATTED_MEMORY = {
77
+ heapUsed: "N/A",
78
+ heapTotal: "N/A",
79
+ heapLimit: "N/A",
80
+ domNodes: void 0,
81
+ eventListeners: void 0
82
+ };
83
+ var BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"];
84
+ var BYTES_PER_UNIT = 1024;
85
+
86
+ // src/utils/detection.ts
87
+ function isServer() {
88
+ return typeof window === "undefined";
89
+ }
90
+ function isClient() {
91
+ return typeof window !== "undefined";
92
+ }
93
+ function hasLegacyMemoryAPI() {
94
+ if (isServer()) return false;
95
+ return "memory" in performance;
96
+ }
97
+ function hasPreciseMemoryAPI() {
98
+ if (isServer()) return false;
99
+ return "measureUserAgentSpecificMemory" in performance;
100
+ }
101
+ function isSecureContext() {
102
+ if (isServer()) return false;
103
+ return window.isSecureContext ?? false;
104
+ }
105
+ function isCrossOriginIsolated() {
106
+ if (isServer()) return false;
107
+ return window.crossOriginIsolated ?? false;
108
+ }
109
+ function detectBrowser() {
110
+ if (isServer()) return void 0;
111
+ const userAgent = navigator.userAgent;
112
+ if (userAgent.includes("Chrome") && !userAgent.includes("Edg")) {
113
+ return "Chrome";
114
+ }
115
+ if (userAgent.includes("Edg")) {
116
+ return "Edge";
117
+ }
118
+ if (userAgent.includes("Firefox")) {
119
+ return "Firefox";
120
+ }
121
+ if (userAgent.includes("Safari") && !userAgent.includes("Chrome")) {
122
+ return "Safari";
123
+ }
124
+ return void 0;
125
+ }
126
+ function determineSupportLevel() {
127
+ if (isServer()) return "none";
128
+ if (hasLegacyMemoryAPI()) {
129
+ return "full";
130
+ }
131
+ return "partial";
132
+ }
133
+ function getAvailableMetrics() {
134
+ if (isServer()) return [];
135
+ const metrics = [];
136
+ if (hasLegacyMemoryAPI()) {
137
+ metrics.push("heapUsed", "heapTotal", "heapLimit");
138
+ }
139
+ metrics.push("domNodes");
140
+ metrics.push("eventListeners");
141
+ return metrics;
142
+ }
143
+ function getLimitations() {
144
+ const limitations = [];
145
+ if (isServer()) {
146
+ limitations.push("Running in server-side environment");
147
+ return limitations;
148
+ }
149
+ if (!hasLegacyMemoryAPI()) {
150
+ limitations.push("performance.memory API not available (not Chrome/Edge)");
151
+ }
152
+ if (!isSecureContext()) {
153
+ limitations.push("Not running in secure context (HTTPS required for precise API)");
154
+ }
155
+ if (!isCrossOriginIsolated()) {
156
+ limitations.push(
157
+ "Not cross-origin isolated (COOP/COEP headers required for precise API)"
158
+ );
159
+ }
160
+ return limitations;
161
+ }
162
+ function detectSupport() {
163
+ return {
164
+ level: determineSupportLevel(),
165
+ availableMetrics: getAvailableMetrics(),
166
+ limitations: getLimitations(),
167
+ isSecureContext: isSecureContext(),
168
+ isCrossOriginIsolated: isCrossOriginIsolated(),
169
+ hasPreciseMemoryAPI: hasPreciseMemoryAPI()
170
+ };
171
+ }
172
+ function getUnsupportedReason() {
173
+ if (isServer()) {
174
+ return "server-side";
175
+ }
176
+ if (!isSecureContext() && hasPreciseMemoryAPI()) {
177
+ return "insecure-context";
178
+ }
179
+ if (!hasLegacyMemoryAPI() && !hasPreciseMemoryAPI()) {
180
+ return "no-api";
181
+ }
182
+ return "browser-restriction";
183
+ }
184
+ function getAvailableFallbacks() {
185
+ if (isServer()) {
186
+ return ["none"];
187
+ }
188
+ const fallbacks = ["none"];
189
+ fallbacks.push("dom-only");
190
+ fallbacks.push("estimation");
191
+ return fallbacks;
192
+ }
193
+ function createUnsupportedInfo() {
194
+ return {
195
+ reason: getUnsupportedReason(),
196
+ browser: detectBrowser(),
197
+ availableFallbacks: getAvailableFallbacks()
198
+ };
199
+ }
200
+ function canMonitorMemory() {
201
+ return isClient() && (hasLegacyMemoryAPI() || hasPreciseMemoryAPI());
202
+ }
203
+
204
+ // src/store.ts
205
+ function createMemoryStore() {
206
+ let state = { ...SSR_INITIAL_STATE };
207
+ const listeners = /* @__PURE__ */ new Set();
208
+ function subscribe(listener) {
209
+ listeners.add(listener);
210
+ return () => {
211
+ listeners.delete(listener);
212
+ };
213
+ }
214
+ function getSnapshot() {
215
+ return state;
216
+ }
217
+ function getServerSnapshot() {
218
+ return SSR_INITIAL_STATE;
219
+ }
220
+ function notify() {
221
+ listeners.forEach((listener) => listener());
222
+ }
223
+ function updateMemory(memory) {
224
+ if (state.memory === memory) return;
225
+ state = {
226
+ ...state,
227
+ memory,
228
+ lastUpdated: Date.now()
229
+ };
230
+ notify();
231
+ }
232
+ function updateDOMNodes(count) {
233
+ if (state.domNodes === count) return;
234
+ state = {
235
+ ...state,
236
+ domNodes: count,
237
+ lastUpdated: Date.now()
238
+ };
239
+ notify();
240
+ }
241
+ function updateEventListeners(count) {
242
+ if (state.eventListeners === count) return;
243
+ state = {
244
+ ...state,
245
+ eventListeners: count,
246
+ lastUpdated: Date.now()
247
+ };
248
+ notify();
249
+ }
250
+ function updateMonitoringStatus(isMonitoring) {
251
+ if (state.isMonitoring === isMonitoring) return;
252
+ state = {
253
+ ...state,
254
+ isMonitoring,
255
+ lastUpdated: Date.now()
256
+ };
257
+ notify();
258
+ }
259
+ function updateSeverity(severity) {
260
+ if (state.severity === severity) return;
261
+ state = {
262
+ ...state,
263
+ severity,
264
+ lastUpdated: Date.now()
265
+ };
266
+ notify();
267
+ }
268
+ function batchUpdate(updates) {
269
+ const newState = { ...state, ...updates, lastUpdated: Date.now() };
270
+ const hasChanges = Object.keys(updates).some(
271
+ (key) => state[key] !== updates[key]
272
+ );
273
+ if (!hasChanges) return;
274
+ state = newState;
275
+ notify();
276
+ }
277
+ function reset() {
278
+ state = { ...SSR_INITIAL_STATE };
279
+ notify();
280
+ }
281
+ function getSubscriberCount() {
282
+ return listeners.size;
283
+ }
284
+ return {
285
+ subscribe,
286
+ getSnapshot,
287
+ getServerSnapshot,
288
+ updateMemory,
289
+ updateDOMNodes,
290
+ updateEventListeners,
291
+ updateMonitoringStatus,
292
+ updateSeverity,
293
+ batchUpdate,
294
+ reset,
295
+ getSubscriberCount
296
+ };
297
+ }
298
+ function createStore() {
299
+ return createMemoryStore();
300
+ }
301
+ function readMemoryFromAPI() {
302
+ if (isServer()) return null;
303
+ const memory = performance.memory;
304
+ if (!memory) return null;
305
+ return {
306
+ heapUsed: Math.max(0, memory.usedJSHeapSize),
307
+ heapTotal: Math.max(0, memory.totalJSHeapSize),
308
+ heapLimit: Math.max(0, memory.jsHeapSizeLimit),
309
+ timestamp: Date.now()
310
+ };
311
+ }
312
+ function countDOMNodes() {
313
+ if (isServer()) return null;
314
+ try {
315
+ return document.querySelectorAll("*").length;
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
320
+ function estimateEventListeners() {
321
+ if (isServer()) return null;
322
+ try {
323
+ const interactiveElements = document.querySelectorAll(
324
+ "button, a, input, select, textarea, [onclick], [onchange], [onkeydown], [onkeyup], [onmouseover], [onmouseout], [onfocus], [onblur], [tabindex]"
325
+ );
326
+ return Math.round(interactiveElements.length * 1.5);
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+
332
+ // src/utils/formatting.ts
333
+ function formatBytes(bytes, decimals = 2) {
334
+ if (bytes === null || bytes === void 0) {
335
+ return "N/A";
336
+ }
337
+ if (bytes === 0) {
338
+ return "0 B";
339
+ }
340
+ if (bytes < 0) {
341
+ return `-${formatBytes(Math.abs(bytes), decimals)}`;
342
+ }
343
+ const unitIndex = Math.max(
344
+ 0,
345
+ Math.min(
346
+ Math.floor(Math.log(bytes) / Math.log(BYTES_PER_UNIT)),
347
+ BYTE_UNITS.length - 1
348
+ )
349
+ );
350
+ const value = bytes / Math.pow(BYTES_PER_UNIT, unitIndex);
351
+ const unit = BYTE_UNITS[unitIndex];
352
+ const formatted = value.toFixed(decimals);
353
+ const trimmed = parseFloat(formatted).toString();
354
+ return `${trimmed} ${unit}`;
355
+ }
356
+ function formatPercentage(percentage, decimals = 1) {
357
+ if (percentage === null || percentage === void 0) {
358
+ return "N/A";
359
+ }
360
+ return `${percentage.toFixed(decimals)}%`;
361
+ }
362
+ function formatNumber(value) {
363
+ if (value === null || value === void 0) {
364
+ return "N/A";
365
+ }
366
+ return value.toLocaleString();
367
+ }
368
+ function createFormattedMemory(memory, domNodes, eventListeners) {
369
+ return {
370
+ heapUsed: formatBytes(memory?.heapUsed),
371
+ heapTotal: formatBytes(memory?.heapTotal),
372
+ heapLimit: formatBytes(memory?.heapLimit),
373
+ domNodes: domNodes != null ? formatNumber(domNodes) : void 0,
374
+ eventListeners: eventListeners != null ? formatNumber(eventListeners) : void 0
375
+ };
376
+ }
377
+ function calculateUsagePercentage(heapUsed, heapLimit) {
378
+ if (heapUsed == null || heapLimit == null || heapLimit <= 0) {
379
+ return null;
380
+ }
381
+ const percentage = heapUsed / heapLimit * 100;
382
+ return Math.min(100, Math.max(0, percentage));
383
+ }
384
+ function formatDuration(ms) {
385
+ if (ms < 1e3) {
386
+ return `${ms}ms`;
387
+ }
388
+ if (ms < 6e4) {
389
+ return `${(ms / 1e3).toFixed(1)}s`;
390
+ }
391
+ if (ms < 36e5) {
392
+ return `${(ms / 6e4).toFixed(1)}m`;
393
+ }
394
+ return `${(ms / 36e5).toFixed(1)}h`;
395
+ }
396
+
397
+ // src/utils/circularBuffer.ts
398
+ var CircularBuffer = class {
399
+ /**
400
+ * Create a new circular buffer with the specified capacity
401
+ *
402
+ * @param capacity - Maximum number of items the buffer can hold
403
+ * @throws Error if capacity is less than 1
404
+ */
405
+ constructor(capacity) {
406
+ this.head = 0;
407
+ this.tail = 0;
408
+ this._size = 0;
409
+ if (capacity < 1) {
410
+ throw new Error("CircularBuffer capacity must be at least 1");
411
+ }
412
+ this._capacity = capacity;
413
+ this.buffer = new Array(capacity);
414
+ }
415
+ /**
416
+ * Add an item to the buffer.
417
+ * If the buffer is full, the oldest item will be overwritten.
418
+ *
419
+ * @param item - Item to add
420
+ */
421
+ push(item) {
422
+ this.buffer[this.tail] = item;
423
+ this.tail = (this.tail + 1) % this._capacity;
424
+ if (this._size < this._capacity) {
425
+ this._size++;
426
+ } else {
427
+ this.head = (this.head + 1) % this._capacity;
428
+ }
429
+ }
430
+ /**
431
+ * Get all items in the buffer as an array, from oldest to newest.
432
+ *
433
+ * @returns Array of items in insertion order (oldest first)
434
+ */
435
+ toArray() {
436
+ const result = [];
437
+ for (let i = 0; i < this._size; i++) {
438
+ const index = (this.head + i) % this._capacity;
439
+ result.push(this.buffer[index]);
440
+ }
441
+ return result;
442
+ }
443
+ /**
444
+ * Get the most recent N items from the buffer.
445
+ *
446
+ * @param count - Number of items to retrieve
447
+ * @returns Array of most recent items (oldest first within the slice)
448
+ */
449
+ getRecent(count) {
450
+ const actualCount = Math.min(count, this._size);
451
+ const result = [];
452
+ const startOffset = this._size - actualCount;
453
+ for (let i = 0; i < actualCount; i++) {
454
+ const index = (this.head + startOffset + i) % this._capacity;
455
+ result.push(this.buffer[index]);
456
+ }
457
+ return result;
458
+ }
459
+ /**
460
+ * Get the most recently added item.
461
+ *
462
+ * @returns The most recent item, or undefined if buffer is empty
463
+ */
464
+ get last() {
465
+ if (this._size === 0) {
466
+ return void 0;
467
+ }
468
+ const lastIndex = (this.tail - 1 + this._capacity) % this._capacity;
469
+ return this.buffer[lastIndex];
470
+ }
471
+ /**
472
+ * Get the oldest item in the buffer.
473
+ *
474
+ * @returns The oldest item, or undefined if buffer is empty
475
+ */
476
+ get first() {
477
+ if (this._size === 0) {
478
+ return void 0;
479
+ }
480
+ return this.buffer[this.head];
481
+ }
482
+ /**
483
+ * Get an item at a specific index (0 = oldest).
484
+ *
485
+ * @param index - Index of the item to retrieve
486
+ * @returns The item at the index, or undefined if out of bounds
487
+ */
488
+ at(index) {
489
+ if (index < 0 || index >= this._size) {
490
+ return void 0;
491
+ }
492
+ const bufferIndex = (this.head + index) % this._capacity;
493
+ return this.buffer[bufferIndex];
494
+ }
495
+ /**
496
+ * Current number of items in the buffer.
497
+ */
498
+ get size() {
499
+ return this._size;
500
+ }
501
+ /**
502
+ * Maximum capacity of the buffer.
503
+ */
504
+ get capacity() {
505
+ return this._capacity;
506
+ }
507
+ /**
508
+ * Check if the buffer is empty.
509
+ */
510
+ get isEmpty() {
511
+ return this._size === 0;
512
+ }
513
+ /**
514
+ * Check if the buffer is full.
515
+ */
516
+ get isFull() {
517
+ return this._size === this._capacity;
518
+ }
519
+ /**
520
+ * Clear all items from the buffer.
521
+ */
522
+ clear() {
523
+ this.buffer = new Array(this._capacity);
524
+ this.head = 0;
525
+ this.tail = 0;
526
+ this._size = 0;
527
+ }
528
+ /**
529
+ * Iterate over all items in the buffer (oldest to newest).
530
+ */
531
+ *[Symbol.iterator]() {
532
+ for (let i = 0; i < this._size; i++) {
533
+ const index = (this.head + i) % this._capacity;
534
+ yield this.buffer[index];
535
+ }
536
+ }
537
+ /**
538
+ * Apply a function to each item in the buffer.
539
+ *
540
+ * @param callback - Function to call for each item
541
+ */
542
+ forEach(callback) {
543
+ for (let i = 0; i < this._size; i++) {
544
+ const bufferIndex = (this.head + i) % this._capacity;
545
+ callback(this.buffer[bufferIndex], i);
546
+ }
547
+ }
548
+ /**
549
+ * Map items to a new array.
550
+ *
551
+ * @param callback - Function to transform each item
552
+ * @returns Array of transformed items
553
+ */
554
+ map(callback) {
555
+ const result = [];
556
+ for (let i = 0; i < this._size; i++) {
557
+ const bufferIndex = (this.head + i) % this._capacity;
558
+ result.push(callback(this.buffer[bufferIndex], i));
559
+ }
560
+ return result;
561
+ }
562
+ /**
563
+ * Filter items based on a predicate.
564
+ *
565
+ * @param predicate - Function to test each item
566
+ * @returns Array of items that pass the test
567
+ */
568
+ filter(predicate) {
569
+ const result = [];
570
+ for (let i = 0; i < this._size; i++) {
571
+ const bufferIndex = (this.head + i) % this._capacity;
572
+ const item = this.buffer[bufferIndex];
573
+ if (predicate(item, i)) {
574
+ result.push(item);
575
+ }
576
+ }
577
+ return result;
578
+ }
579
+ };
580
+
581
+ // src/utils/leakDetection.ts
582
+ function linearRegression(points) {
583
+ const n = points.length;
584
+ if (n < 2) {
585
+ return { slope: 0, intercept: 0, rSquared: 0 };
586
+ }
587
+ let sumX = 0;
588
+ let sumY = 0;
589
+ let sumXY = 0;
590
+ let sumX2 = 0;
591
+ let sumY2 = 0;
592
+ for (const [x, y] of points) {
593
+ sumX += x;
594
+ sumY += y;
595
+ sumXY += x * y;
596
+ sumX2 += x * x;
597
+ sumY2 += y * y;
598
+ }
599
+ const denominator = n * sumX2 - sumX * sumX;
600
+ if (denominator === 0) {
601
+ return { slope: 0, intercept: sumY / n, rSquared: 0 };
602
+ }
603
+ const slope = (n * sumXY - sumX * sumY) / denominator;
604
+ const intercept = (sumY - slope * sumX) / n;
605
+ const meanY = sumY / n;
606
+ let ssTotal = 0;
607
+ let ssResidual = 0;
608
+ for (const [x, y] of points) {
609
+ const predicted = slope * x + intercept;
610
+ ssTotal += Math.pow(y - meanY, 2);
611
+ ssResidual += Math.pow(y - predicted, 2);
612
+ }
613
+ const rSquared = ssTotal === 0 ? 0 : 1 - ssResidual / ssTotal;
614
+ return {
615
+ slope,
616
+ intercept,
617
+ rSquared: Math.max(0, Math.min(1, rSquared))
618
+ // Clamp to [0, 1]
619
+ };
620
+ }
621
+ function calculateTrend(samples) {
622
+ if (samples.length < 2) {
623
+ return "stable";
624
+ }
625
+ const points = samples.map((s, i) => [i, s.heapUsed]);
626
+ const { slope } = linearRegression(points);
627
+ const avgHeap = samples.reduce((sum, s) => sum + s.heapUsed, 0) / samples.length;
628
+ const normalizedSlope = avgHeap > 0 ? slope / avgHeap : 0;
629
+ if (normalizedSlope > TREND_THRESHOLDS.increasing) {
630
+ return "increasing";
631
+ }
632
+ if (normalizedSlope < TREND_THRESHOLDS.decreasing) {
633
+ return "decreasing";
634
+ }
635
+ return "stable";
636
+ }
637
+ function calculateAverageGrowth(samples) {
638
+ if (samples.length < 2) {
639
+ return 0;
640
+ }
641
+ const points = samples.map((s, i) => [i, s.heapUsed]);
642
+ const { slope } = linearRegression(points);
643
+ return slope;
644
+ }
645
+ function generateRecommendation(probability, trend, averageGrowth) {
646
+ if (probability < 30) {
647
+ return void 0;
648
+ }
649
+ if (probability >= 80) {
650
+ return `Critical: High probability of memory leak detected. Memory is growing at ${formatGrowthRate(averageGrowth)} per sample. Consider profiling with browser DevTools.`;
651
+ }
652
+ if (probability >= 60) {
653
+ return `Warning: Possible memory leak detected. Memory trend is ${trend}. Monitor closely and check for retained references.`;
654
+ }
655
+ if (probability >= 30 && trend === "increasing") {
656
+ return `Note: Memory usage is trending upward. This may be normal for your application, but consider monitoring.`;
657
+ }
658
+ return void 0;
659
+ }
660
+ function formatGrowthRate(bytesPerSample) {
661
+ const absBytes = Math.abs(bytesPerSample);
662
+ if (absBytes >= 1024 * 1024) {
663
+ return `${(bytesPerSample / (1024 * 1024)).toFixed(2)} MB`;
664
+ }
665
+ if (absBytes >= 1024) {
666
+ return `${(bytesPerSample / 1024).toFixed(2)} KB`;
667
+ }
668
+ return `${bytesPerSample.toFixed(0)} bytes`;
669
+ }
670
+ function analyzeLeakProbability(samples, sensitivity = "medium", customThreshold) {
671
+ if (samples.length < MIN_LEAK_DETECTION_SAMPLES) {
672
+ return {
673
+ isLeaking: false,
674
+ probability: 0,
675
+ trend: calculateTrend(samples),
676
+ averageGrowth: calculateAverageGrowth(samples),
677
+ rSquared: 0,
678
+ samples,
679
+ recommendation: void 0
680
+ };
681
+ }
682
+ const config = LEAK_SENSITIVITY_CONFIG[sensitivity];
683
+ const threshold = customThreshold ?? config.minSlope;
684
+ const points = samples.map((s, i) => [i, s.heapUsed]);
685
+ const { slope, rSquared } = linearRegression(points);
686
+ let probability = 0;
687
+ if (slope > 0 && rSquared >= config.minR2) {
688
+ const slopeRatio = slope / threshold;
689
+ probability = Math.min(100, slopeRatio * 50 * rSquared * config.probabilityMultiplier);
690
+ }
691
+ const trend = calculateTrend(samples);
692
+ const averageGrowth = slope;
693
+ if (trend === "increasing" && rSquared > 0.7 && probability < 60) {
694
+ probability = Math.max(probability, 40);
695
+ }
696
+ probability = Math.min(100, Math.max(0, probability));
697
+ const isLeaking = probability > 50;
698
+ return {
699
+ isLeaking,
700
+ probability: Math.round(probability),
701
+ trend,
702
+ averageGrowth,
703
+ rSquared,
704
+ samples,
705
+ recommendation: generateRecommendation(probability, trend, averageGrowth)
706
+ };
707
+ }
708
+
709
+ // src/useMemoryMonitor.ts
710
+ function useMemoryMonitor(options = {}) {
711
+ const mergedOptions = useMemo(
712
+ () => ({
713
+ ...DEFAULT_OPTIONS,
714
+ ...options,
715
+ thresholds: {
716
+ ...DEFAULT_OPTIONS.thresholds,
717
+ ...options.thresholds
718
+ },
719
+ leakDetection: {
720
+ ...DEFAULT_OPTIONS.leakDetection,
721
+ ...options.leakDetection
722
+ }
723
+ }),
724
+ [options]
725
+ );
726
+ const {
727
+ interval,
728
+ autoStart,
729
+ enabled,
730
+ enableHistory,
731
+ historySize,
732
+ thresholds,
733
+ leakDetection,
734
+ devMode,
735
+ trackDOMNodes,
736
+ trackEventListeners,
737
+ logToConsole,
738
+ disableInProduction,
739
+ fallbackStrategy
740
+ } = mergedOptions;
741
+ const shouldDisable = disableInProduction && typeof globalThis.process !== "undefined" && globalThis.process?.env?.NODE_ENV === "production";
742
+ const onUpdateRef = useRef(options.onUpdate);
743
+ const onWarningRef = useRef(options.onWarning);
744
+ const onCriticalRef = useRef(options.onCritical);
745
+ const onLeakDetectedRef = useRef(options.onLeakDetected);
746
+ const onUnsupportedRef = useRef(options.onUnsupported);
747
+ useEffect(() => {
748
+ onUpdateRef.current = options.onUpdate;
749
+ onWarningRef.current = options.onWarning;
750
+ onCriticalRef.current = options.onCritical;
751
+ onLeakDetectedRef.current = options.onLeakDetected;
752
+ onUnsupportedRef.current = options.onUnsupported;
753
+ }, [options.onUpdate, options.onWarning, options.onCritical, options.onLeakDetected, options.onUnsupported]);
754
+ const browserSupport = useMemo(() => detectSupport(), []);
755
+ const storeRef = useRef(null);
756
+ if (!storeRef.current) {
757
+ storeRef.current = createStore();
758
+ }
759
+ const store = storeRef.current;
760
+ const storeState = useSyncExternalStore(
761
+ store.subscribe,
762
+ store.getSnapshot,
763
+ store.getServerSnapshot
764
+ );
765
+ const historyBufferRef = useRef(null);
766
+ if (enableHistory && !historyBufferRef.current) {
767
+ historyBufferRef.current = new CircularBuffer(historySize);
768
+ }
769
+ const snapshotsRef = useRef(/* @__PURE__ */ new Map());
770
+ const intervalIdRef = useRef(null);
771
+ const prevSeverityRef = useRef(DEFAULT_SEVERITY);
772
+ const prevLeakDetectedRef = useRef(false);
773
+ const isMonitoringRef = useRef(false);
774
+ const isSupported = useMemo(() => {
775
+ if (isServer()) return false;
776
+ if (shouldDisable) return false;
777
+ if (fallbackStrategy === "none") {
778
+ return hasLegacyMemoryAPI();
779
+ }
780
+ return true;
781
+ }, [shouldDisable, fallbackStrategy]);
782
+ const supportLevel = useMemo(() => {
783
+ if (!isSupported) return "none";
784
+ return browserSupport.level;
785
+ }, [isSupported, browserSupport.level]);
786
+ const availableMetrics = useMemo(() => {
787
+ if (!isSupported) return [];
788
+ return browserSupport.availableMetrics;
789
+ }, [isSupported, browserSupport.availableMetrics]);
790
+ const memory = storeState.memory;
791
+ const heapUsed = memory?.heapUsed ?? null;
792
+ const heapTotal = memory?.heapTotal ?? null;
793
+ const heapLimit = memory?.heapLimit ?? null;
794
+ const usagePercentage = calculateUsagePercentage(heapUsed, heapLimit);
795
+ const domNodes = storeState.domNodes;
796
+ const eventListeners = storeState.eventListeners;
797
+ const severity = useMemo(() => {
798
+ if (usagePercentage === null) return DEFAULT_SEVERITY;
799
+ if (usagePercentage >= (thresholds.critical ?? 90)) {
800
+ return "critical";
801
+ }
802
+ if (usagePercentage >= (thresholds.warning ?? 70)) {
803
+ return "warning";
804
+ }
805
+ return "normal";
806
+ }, [usagePercentage, thresholds.critical, thresholds.warning]);
807
+ const history = useMemo(() => {
808
+ if (!enableHistory || !historyBufferRef.current) return [];
809
+ return historyBufferRef.current.toArray();
810
+ }, [enableHistory, storeState.lastUpdated]);
811
+ const trend = useMemo(() => {
812
+ if (!enableHistory || history.length < 3) return DEFAULT_TREND;
813
+ return calculateTrend(history);
814
+ }, [enableHistory, history]);
815
+ const leakAnalysis = useMemo(() => {
816
+ if (!leakDetection.enabled || !enableHistory) return null;
817
+ const windowSize = leakDetection.windowSize ?? 10;
818
+ const samples = historyBufferRef.current?.getRecent(windowSize) ?? [];
819
+ if (samples.length < 5) return null;
820
+ return analyzeLeakProbability(
821
+ samples,
822
+ leakDetection.sensitivity ?? "medium",
823
+ leakDetection.threshold
824
+ );
825
+ }, [
826
+ leakDetection.enabled,
827
+ leakDetection.sensitivity,
828
+ leakDetection.windowSize,
829
+ leakDetection.threshold,
830
+ enableHistory,
831
+ history
832
+ ]);
833
+ const isLeakDetected = leakAnalysis?.isLeaking ?? false;
834
+ const leakProbability = leakAnalysis?.probability ?? 0;
835
+ const formatted = useMemo(() => {
836
+ if (!isSupported) return SSR_FORMATTED_MEMORY;
837
+ return createFormattedMemory(memory, domNodes, eventListeners);
838
+ }, [isSupported, memory, domNodes, eventListeners]);
839
+ const poll = useCallback(() => {
840
+ if (!isSupported || !enabled) return;
841
+ const newMemory = readMemoryFromAPI();
842
+ if (newMemory) {
843
+ store.updateMemory(newMemory);
844
+ if (enableHistory && historyBufferRef.current) {
845
+ historyBufferRef.current.push(newMemory);
846
+ }
847
+ onUpdateRef.current?.(newMemory);
848
+ if (logToConsole && devMode) {
849
+ console.log("[useMemoryMonitor]", {
850
+ heapUsed: newMemory.heapUsed,
851
+ heapTotal: newMemory.heapTotal,
852
+ heapLimit: newMemory.heapLimit
853
+ });
854
+ }
855
+ }
856
+ if (trackDOMNodes) {
857
+ const nodeCount = countDOMNodes();
858
+ store.updateDOMNodes(nodeCount);
859
+ }
860
+ if (trackEventListeners) {
861
+ const listenerCount = estimateEventListeners();
862
+ store.updateEventListeners(listenerCount);
863
+ }
864
+ }, [
865
+ isSupported,
866
+ enabled,
867
+ store,
868
+ enableHistory,
869
+ trackDOMNodes,
870
+ trackEventListeners,
871
+ logToConsole,
872
+ devMode
873
+ ]);
874
+ const start = useCallback(() => {
875
+ if (!isSupported || !enabled || isMonitoringRef.current) return;
876
+ isMonitoringRef.current = true;
877
+ store.updateMonitoringStatus(true);
878
+ poll();
879
+ intervalIdRef.current = setInterval(poll, interval);
880
+ if (devMode && logToConsole) {
881
+ console.log("[useMemoryMonitor] Monitoring started");
882
+ }
883
+ }, [isSupported, enabled, store, poll, interval, devMode, logToConsole]);
884
+ const stop = useCallback(() => {
885
+ if (!isMonitoringRef.current) return;
886
+ isMonitoringRef.current = false;
887
+ store.updateMonitoringStatus(false);
888
+ if (intervalIdRef.current) {
889
+ clearInterval(intervalIdRef.current);
890
+ intervalIdRef.current = null;
891
+ }
892
+ if (devMode && logToConsole) {
893
+ console.log("[useMemoryMonitor] Monitoring stopped");
894
+ }
895
+ }, [store, devMode, logToConsole]);
896
+ const takeSnapshot = useCallback(
897
+ (id) => {
898
+ if (!isSupported || !memory) return null;
899
+ const snapshot = {
900
+ id,
901
+ memory: { ...memory },
902
+ domNodes: domNodes ?? void 0,
903
+ eventListeners: eventListeners ?? void 0,
904
+ timestamp: Date.now()
905
+ };
906
+ snapshotsRef.current.set(id, snapshot);
907
+ return snapshot;
908
+ },
909
+ [isSupported, memory, domNodes, eventListeners]
910
+ );
911
+ const compareSnapshots = useCallback(
912
+ (id1, id2) => {
913
+ const snapshot1 = snapshotsRef.current.get(id1);
914
+ const snapshot2 = snapshotsRef.current.get(id2);
915
+ if (!snapshot1 || !snapshot2) return null;
916
+ return {
917
+ heapDelta: snapshot2.memory.heapUsed - snapshot1.memory.heapUsed,
918
+ heapPercentChange: snapshot1.memory.heapUsed > 0 ? (snapshot2.memory.heapUsed - snapshot1.memory.heapUsed) / snapshot1.memory.heapUsed * 100 : 0,
919
+ domNodesDelta: snapshot1.domNodes != null && snapshot2.domNodes != null ? snapshot2.domNodes - snapshot1.domNodes : void 0,
920
+ eventListenersDelta: snapshot1.eventListeners != null && snapshot2.eventListeners != null ? snapshot2.eventListeners - snapshot1.eventListeners : void 0,
921
+ timeDelta: snapshot2.timestamp - snapshot1.timestamp
922
+ };
923
+ },
924
+ []
925
+ );
926
+ const clearHistory = useCallback(() => {
927
+ if (historyBufferRef.current) {
928
+ historyBufferRef.current.clear();
929
+ }
930
+ }, []);
931
+ const requestGC = useCallback(() => {
932
+ if (isServer()) return;
933
+ try {
934
+ const pressure = new Array(1e6).fill(0);
935
+ pressure.length = 0;
936
+ } catch {
937
+ }
938
+ if (devMode && logToConsole) {
939
+ console.log("[useMemoryMonitor] GC hint requested");
940
+ }
941
+ }, [devMode, logToConsole]);
942
+ useEffect(() => {
943
+ if (autoStart && isSupported && enabled && !shouldDisable) {
944
+ start();
945
+ }
946
+ return () => {
947
+ stop();
948
+ };
949
+ }, [autoStart, isSupported, enabled, shouldDisable, start, stop]);
950
+ useEffect(() => {
951
+ if (isServer()) return;
952
+ const handleVisibilityChange = () => {
953
+ if (document.hidden) {
954
+ stop();
955
+ } else if (autoStart && isSupported && enabled) {
956
+ start();
957
+ }
958
+ };
959
+ document.addEventListener("visibilitychange", handleVisibilityChange);
960
+ return () => {
961
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
962
+ };
963
+ }, [autoStart, isSupported, enabled, start, stop]);
964
+ useEffect(() => {
965
+ if (severity === prevSeverityRef.current) return;
966
+ const prevSeverity = prevSeverityRef.current;
967
+ prevSeverityRef.current = severity;
968
+ if (!memory || !usagePercentage) return;
969
+ const callbackData = {
970
+ memory,
971
+ usagePercentage,
972
+ threshold: severity === "critical" ? thresholds.critical ?? 90 : thresholds.warning ?? 70,
973
+ timestamp: Date.now()
974
+ };
975
+ if (severity === "warning" && prevSeverity === "normal") {
976
+ onWarningRef.current?.(callbackData);
977
+ } else if (severity === "critical") {
978
+ onCriticalRef.current?.(callbackData);
979
+ }
980
+ }, [severity, memory, usagePercentage, thresholds.warning, thresholds.critical]);
981
+ useEffect(() => {
982
+ if (!leakAnalysis) return;
983
+ if (isLeakDetected && !prevLeakDetectedRef.current) {
984
+ onLeakDetectedRef.current?.(leakAnalysis);
985
+ }
986
+ prevLeakDetectedRef.current = isLeakDetected;
987
+ }, [isLeakDetected, leakAnalysis]);
988
+ useEffect(() => {
989
+ if (!isSupported && onUnsupportedRef.current) {
990
+ const unsupportedInfo = createUnsupportedInfo();
991
+ onUnsupportedRef.current(unsupportedInfo);
992
+ }
993
+ }, [isSupported]);
994
+ if (isServer()) {
995
+ return {
996
+ memory: null,
997
+ heapUsed: null,
998
+ heapTotal: null,
999
+ heapLimit: null,
1000
+ usagePercentage: null,
1001
+ domNodes: null,
1002
+ eventListeners: null,
1003
+ isSupported: false,
1004
+ isMonitoring: false,
1005
+ isLeakDetected: false,
1006
+ severity: DEFAULT_SEVERITY,
1007
+ supportLevel: "none",
1008
+ availableMetrics: [],
1009
+ history: [],
1010
+ trend: DEFAULT_TREND,
1011
+ leakProbability: 0,
1012
+ start: () => {
1013
+ },
1014
+ stop: () => {
1015
+ },
1016
+ takeSnapshot: () => null,
1017
+ compareSnapshots: () => null,
1018
+ clearHistory: () => {
1019
+ },
1020
+ requestGC: () => {
1021
+ },
1022
+ formatted: SSR_FORMATTED_MEMORY
1023
+ };
1024
+ }
1025
+ return {
1026
+ // Current State
1027
+ memory,
1028
+ heapUsed,
1029
+ heapTotal,
1030
+ heapLimit,
1031
+ usagePercentage,
1032
+ // DOM Related
1033
+ domNodes,
1034
+ eventListeners,
1035
+ // Status Flags
1036
+ isSupported,
1037
+ isMonitoring: storeState.isMonitoring,
1038
+ isLeakDetected,
1039
+ severity,
1040
+ // Support Details
1041
+ supportLevel,
1042
+ availableMetrics,
1043
+ // Analysis Data
1044
+ history,
1045
+ trend,
1046
+ leakProbability,
1047
+ // Actions
1048
+ start,
1049
+ stop,
1050
+ takeSnapshot,
1051
+ compareSnapshots,
1052
+ clearHistory,
1053
+ requestGC,
1054
+ // Formatting
1055
+ formatted
1056
+ };
1057
+ }
1058
+ export {
1059
+ CircularBuffer,
1060
+ analyzeLeakProbability,
1061
+ calculateTrend,
1062
+ calculateUsagePercentage,
1063
+ canMonitorMemory,
1064
+ detectBrowser,
1065
+ detectSupport,
1066
+ formatBytes,
1067
+ formatDuration,
1068
+ formatNumber,
1069
+ formatPercentage,
1070
+ hasLegacyMemoryAPI,
1071
+ hasPreciseMemoryAPI,
1072
+ isClient,
1073
+ isServer,
1074
+ linearRegression,
1075
+ useMemoryMonitor
1076
+ };
1077
+ //# sourceMappingURL=index.mjs.map