@memberjunction/react-runtime 2.76.0 → 2.77.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/registry/component-registry.d.ts +1 -0
- package/dist/registry/component-registry.d.ts.map +1 -1
- package/dist/registry/component-registry.js +6 -3
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -1
- package/dist/runtime/react-root-manager.d.ts +26 -0
- package/dist/runtime/react-root-manager.d.ts.map +1 -0
- package/dist/runtime/react-root-manager.js +122 -0
- package/dist/utilities/cache-manager.d.ts +38 -0
- package/dist/utilities/cache-manager.d.ts.map +1 -0
- package/dist/utilities/cache-manager.js +156 -0
- package/dist/utilities/index.d.ts +2 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +2 -0
- package/dist/utilities/library-loader.d.ts.map +1 -1
- package/dist/utilities/library-loader.js +25 -8
- package/dist/utilities/resource-manager.d.ts +34 -0
- package/dist/utilities/resource-manager.d.ts.map +1 -0
- package/dist/utilities/resource-manager.js +174 -0
- package/package.json +4 -4
- package/src/index.ts +18 -0
- package/src/registry/component-registry.ts +14 -4
- package/src/runtime/index.ts +7 -1
- package/src/runtime/react-root-manager.ts +218 -0
- package/src/utilities/cache-manager.ts +253 -0
- package/src/utilities/index.ts +3 -1
- package/src/utilities/library-loader.ts +66 -21
- package/src/utilities/resource-manager.ts +305 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cache management with TTL and size limits
|
|
3
|
+
* @module @memberjunction/react-runtime/utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CacheEntry<T> {
|
|
7
|
+
value: T;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
size?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CacheOptions {
|
|
13
|
+
maxSize?: number; // Maximum number of entries
|
|
14
|
+
maxMemory?: number; // Maximum memory in bytes (estimated)
|
|
15
|
+
defaultTTL?: number; // Default TTL in milliseconds
|
|
16
|
+
cleanupInterval?: number; // Cleanup interval in milliseconds
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cache manager with TTL and size limits.
|
|
21
|
+
* Provides automatic cleanup and memory management.
|
|
22
|
+
*/
|
|
23
|
+
export class CacheManager<T = any> {
|
|
24
|
+
private cache = new Map<string, CacheEntry<T>>();
|
|
25
|
+
private memoryUsage = 0;
|
|
26
|
+
private cleanupTimer?: number;
|
|
27
|
+
private readonly options: Required<CacheOptions>;
|
|
28
|
+
|
|
29
|
+
constructor(options: CacheOptions = {}) {
|
|
30
|
+
this.options = {
|
|
31
|
+
maxSize: options.maxSize || 1000,
|
|
32
|
+
maxMemory: options.maxMemory || 50 * 1024 * 1024, // 50MB default
|
|
33
|
+
defaultTTL: options.defaultTTL || 5 * 60 * 1000, // 5 minutes default
|
|
34
|
+
cleanupInterval: options.cleanupInterval || 60 * 1000 // 1 minute default
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (this.options.cleanupInterval > 0) {
|
|
38
|
+
this.startCleanupTimer();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set a value in the cache
|
|
44
|
+
*/
|
|
45
|
+
set(key: string, value: T, ttl?: number): void {
|
|
46
|
+
const size = this.estimateSize(value);
|
|
47
|
+
const entry: CacheEntry<T> = {
|
|
48
|
+
value,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
size
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Check if we need to evict entries
|
|
54
|
+
if (this.cache.size >= this.options.maxSize) {
|
|
55
|
+
this.evictLRU();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check memory usage
|
|
59
|
+
if (this.memoryUsage + size > this.options.maxMemory) {
|
|
60
|
+
this.evictByMemory(size);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remove old entry if exists
|
|
64
|
+
const oldEntry = this.cache.get(key);
|
|
65
|
+
if (oldEntry) {
|
|
66
|
+
this.memoryUsage -= oldEntry.size || 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.cache.set(key, entry);
|
|
70
|
+
this.memoryUsage += size;
|
|
71
|
+
|
|
72
|
+
// Schedule removal if TTL is set
|
|
73
|
+
if (ttl || this.options.defaultTTL) {
|
|
74
|
+
const timeout = ttl || this.options.defaultTTL;
|
|
75
|
+
setTimeout(() => this.delete(key), timeout);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get a value from the cache
|
|
81
|
+
*/
|
|
82
|
+
get(key: string): T | undefined {
|
|
83
|
+
const entry = this.cache.get(key);
|
|
84
|
+
if (!entry) return undefined;
|
|
85
|
+
|
|
86
|
+
// Check if expired
|
|
87
|
+
if (this.isExpired(entry)) {
|
|
88
|
+
this.delete(key);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Update timestamp for LRU
|
|
93
|
+
entry.timestamp = Date.now();
|
|
94
|
+
return entry.value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a key exists and is not expired
|
|
99
|
+
*/
|
|
100
|
+
has(key: string): boolean {
|
|
101
|
+
const entry = this.cache.get(key);
|
|
102
|
+
if (!entry) return false;
|
|
103
|
+
|
|
104
|
+
if (this.isExpired(entry)) {
|
|
105
|
+
this.delete(key);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delete a key from the cache
|
|
114
|
+
*/
|
|
115
|
+
delete(key: string): boolean {
|
|
116
|
+
const entry = this.cache.get(key);
|
|
117
|
+
if (entry) {
|
|
118
|
+
this.memoryUsage -= entry.size || 0;
|
|
119
|
+
return this.cache.delete(key);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Clear all entries
|
|
126
|
+
*/
|
|
127
|
+
clear(): void {
|
|
128
|
+
this.cache.clear();
|
|
129
|
+
this.memoryUsage = 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get cache statistics
|
|
134
|
+
*/
|
|
135
|
+
getStats(): {
|
|
136
|
+
size: number;
|
|
137
|
+
memoryUsage: number;
|
|
138
|
+
maxSize: number;
|
|
139
|
+
maxMemory: number;
|
|
140
|
+
} {
|
|
141
|
+
return {
|
|
142
|
+
size: this.cache.size,
|
|
143
|
+
memoryUsage: this.memoryUsage,
|
|
144
|
+
maxSize: this.options.maxSize,
|
|
145
|
+
maxMemory: this.options.maxMemory
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Manually trigger cleanup
|
|
151
|
+
*/
|
|
152
|
+
cleanup(): number {
|
|
153
|
+
let removed = 0;
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
|
|
156
|
+
for (const [key, entry] of this.cache) {
|
|
157
|
+
if (this.isExpired(entry, now)) {
|
|
158
|
+
this.delete(key);
|
|
159
|
+
removed++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return removed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Destroy the cache and stop cleanup timer
|
|
168
|
+
*/
|
|
169
|
+
destroy(): void {
|
|
170
|
+
this.stopCleanupTimer();
|
|
171
|
+
this.clear();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if an entry is expired
|
|
176
|
+
*/
|
|
177
|
+
private isExpired(entry: CacheEntry<T>, now?: number): boolean {
|
|
178
|
+
if (!this.options.defaultTTL) return false;
|
|
179
|
+
const currentTime = now || Date.now();
|
|
180
|
+
return currentTime - entry.timestamp > this.options.defaultTTL;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Evict least recently used entry
|
|
185
|
+
*/
|
|
186
|
+
private evictLRU(): void {
|
|
187
|
+
let lruKey: string | undefined;
|
|
188
|
+
let lruTime = Infinity;
|
|
189
|
+
|
|
190
|
+
for (const [key, entry] of this.cache) {
|
|
191
|
+
if (entry.timestamp < lruTime) {
|
|
192
|
+
lruTime = entry.timestamp;
|
|
193
|
+
lruKey = key;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (lruKey) {
|
|
198
|
+
this.delete(lruKey);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Evict entries to make room for new memory
|
|
204
|
+
*/
|
|
205
|
+
private evictByMemory(requiredSize: number): void {
|
|
206
|
+
const entries = Array.from(this.cache.entries())
|
|
207
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
208
|
+
|
|
209
|
+
let freedMemory = 0;
|
|
210
|
+
for (const [key, entry] of entries) {
|
|
211
|
+
if (freedMemory >= requiredSize) break;
|
|
212
|
+
freedMemory += entry.size || 0;
|
|
213
|
+
this.delete(key);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Estimate size of a value
|
|
219
|
+
*/
|
|
220
|
+
private estimateSize(value: T): number {
|
|
221
|
+
if (typeof value === 'string') {
|
|
222
|
+
return value.length * 2; // 2 bytes per character
|
|
223
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
224
|
+
// Rough estimation for objects
|
|
225
|
+
try {
|
|
226
|
+
return JSON.stringify(value).length * 2;
|
|
227
|
+
} catch {
|
|
228
|
+
return 1024; // Default 1KB for objects that can't be stringified
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
return 8; // Default for primitives
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Start the cleanup timer
|
|
237
|
+
*/
|
|
238
|
+
private startCleanupTimer(): void {
|
|
239
|
+
this.cleanupTimer = window.setInterval(() => {
|
|
240
|
+
this.cleanup();
|
|
241
|
+
}, this.options.cleanupInterval);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Stop the cleanup timer
|
|
246
|
+
*/
|
|
247
|
+
private stopCleanupTimer(): void {
|
|
248
|
+
if (this.cleanupTimer) {
|
|
249
|
+
window.clearInterval(this.cleanupTimer);
|
|
250
|
+
this.cleanupTimer = undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
package/src/utilities/index.ts
CHANGED
|
@@ -7,4 +7,6 @@ export * from './runtime-utilities';
|
|
|
7
7
|
export * from './component-styles';
|
|
8
8
|
export * from './standard-libraries';
|
|
9
9
|
export * from './library-loader';
|
|
10
|
-
export * from './component-error-analyzer';
|
|
10
|
+
export * from './component-error-analyzer';
|
|
11
|
+
export * from './resource-manager';
|
|
12
|
+
export * from './cache-manager';
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
} from './standard-libraries';
|
|
11
11
|
import { LibraryConfiguration, ExternalLibraryConfig, LibraryLoadOptions as ConfigLoadOptions } from '../types/library-config';
|
|
12
12
|
import { getCoreRuntimeLibraries, isCoreRuntimeLibrary } from './core-libraries';
|
|
13
|
+
import { resourceManager } from './resource-manager';
|
|
14
|
+
|
|
15
|
+
// Unique component ID for resource tracking
|
|
16
|
+
const LIBRARY_LOADER_COMPONENT_ID = 'mj-react-runtime-library-loader-singleton';
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Represents a loaded script or CSS resource
|
|
@@ -214,28 +218,46 @@ export class LibraryLoader {
|
|
|
214
218
|
script.async = true;
|
|
215
219
|
script.crossOrigin = 'anonymous';
|
|
216
220
|
|
|
217
|
-
|
|
221
|
+
const cleanup = () => {
|
|
222
|
+
script.removeEventListener('load', onLoad);
|
|
223
|
+
script.removeEventListener('error', onError);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const onLoad = () => {
|
|
227
|
+
cleanup();
|
|
218
228
|
const global = (window as any)[globalName];
|
|
219
229
|
if (global) {
|
|
220
230
|
resolve(global);
|
|
221
231
|
} else {
|
|
222
232
|
// Some libraries may take a moment to initialize
|
|
223
|
-
setTimeout(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
const timeoutId = resourceManager.setTimeout(
|
|
234
|
+
LIBRARY_LOADER_COMPONENT_ID,
|
|
235
|
+
() => {
|
|
236
|
+
const delayedGlobal = (window as any)[globalName];
|
|
237
|
+
if (delayedGlobal) {
|
|
238
|
+
resolve(delayedGlobal);
|
|
239
|
+
} else {
|
|
240
|
+
reject(new Error(`${globalName} not found after script load`));
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
100,
|
|
244
|
+
{ url, globalName }
|
|
245
|
+
);
|
|
231
246
|
}
|
|
232
247
|
};
|
|
233
248
|
|
|
234
|
-
|
|
249
|
+
const onError = () => {
|
|
250
|
+
cleanup();
|
|
235
251
|
reject(new Error(`Failed to load script: ${url}`));
|
|
236
252
|
};
|
|
237
253
|
|
|
254
|
+
script.addEventListener('load', onLoad);
|
|
255
|
+
script.addEventListener('error', onError);
|
|
256
|
+
|
|
238
257
|
document.head.appendChild(script);
|
|
258
|
+
|
|
259
|
+
// Register the script element for cleanup
|
|
260
|
+
resourceManager.registerDOMElement(LIBRARY_LOADER_COMPONENT_ID, script);
|
|
239
261
|
});
|
|
240
262
|
|
|
241
263
|
this.loadedResources.set(url, {
|
|
@@ -263,6 +285,9 @@ export class LibraryLoader {
|
|
|
263
285
|
link.rel = 'stylesheet';
|
|
264
286
|
link.href = url;
|
|
265
287
|
document.head.appendChild(link);
|
|
288
|
+
|
|
289
|
+
// Register the link element for cleanup
|
|
290
|
+
resourceManager.registerDOMElement(LIBRARY_LOADER_COMPONENT_ID, link);
|
|
266
291
|
|
|
267
292
|
this.loadedResources.set(url, {
|
|
268
293
|
element: link,
|
|
@@ -285,14 +310,19 @@ export class LibraryLoader {
|
|
|
285
310
|
resolve(global);
|
|
286
311
|
} else {
|
|
287
312
|
// Retry after a short delay
|
|
288
|
-
setTimeout(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
313
|
+
resourceManager.setTimeout(
|
|
314
|
+
LIBRARY_LOADER_COMPONENT_ID,
|
|
315
|
+
() => {
|
|
316
|
+
const delayedGlobal = (window as any)[globalName];
|
|
317
|
+
if (delayedGlobal) {
|
|
318
|
+
resolve(delayedGlobal);
|
|
319
|
+
} else {
|
|
320
|
+
reject(new Error(`${globalName} not found after script load`));
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
100,
|
|
324
|
+
{ context: 'waitForScriptLoad', globalName }
|
|
325
|
+
);
|
|
296
326
|
}
|
|
297
327
|
};
|
|
298
328
|
|
|
@@ -304,10 +334,15 @@ export class LibraryLoader {
|
|
|
304
334
|
|
|
305
335
|
// Wait for load
|
|
306
336
|
const loadHandler = () => {
|
|
307
|
-
script.removeEventListener('load', loadHandler);
|
|
308
337
|
checkGlobal();
|
|
309
338
|
};
|
|
310
|
-
|
|
339
|
+
resourceManager.addEventListener(
|
|
340
|
+
LIBRARY_LOADER_COMPONENT_ID,
|
|
341
|
+
script,
|
|
342
|
+
'load',
|
|
343
|
+
loadHandler,
|
|
344
|
+
{ once: true }
|
|
345
|
+
);
|
|
311
346
|
}
|
|
312
347
|
|
|
313
348
|
|
|
@@ -319,9 +354,19 @@ export class LibraryLoader {
|
|
|
319
354
|
}
|
|
320
355
|
|
|
321
356
|
/**
|
|
322
|
-
* Clear loaded resources cache
|
|
357
|
+
* Clear loaded resources cache and cleanup DOM elements
|
|
323
358
|
*/
|
|
324
359
|
static clearCache(): void {
|
|
360
|
+
// Remove all script and link elements we added
|
|
361
|
+
this.loadedResources.forEach((resource, url) => {
|
|
362
|
+
if (resource.element && resource.element.parentNode) {
|
|
363
|
+
resource.element.parentNode.removeChild(resource.element);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
325
367
|
this.loadedResources.clear();
|
|
368
|
+
|
|
369
|
+
// Clean up any resources managed by resource manager
|
|
370
|
+
resourceManager.cleanupComponent(LIBRARY_LOADER_COMPONENT_ID);
|
|
326
371
|
}
|
|
327
372
|
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Centralized resource management for React Runtime
|
|
3
|
+
* Handles timers, DOM elements, event listeners, and other resources that need cleanup
|
|
4
|
+
* @module @memberjunction/react-runtime/utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ManagedResource {
|
|
8
|
+
type: 'timer' | 'interval' | 'animationFrame' | 'eventListener' | 'domElement' | 'observable' | 'reactRoot';
|
|
9
|
+
id: string | number;
|
|
10
|
+
cleanup: () => void;
|
|
11
|
+
metadata?: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ResourceManager provides centralized management of resources that need cleanup.
|
|
16
|
+
* This prevents memory leaks by ensuring all resources are properly disposed.
|
|
17
|
+
*/
|
|
18
|
+
export class ResourceManager {
|
|
19
|
+
private resources = new Map<string, Set<ManagedResource>>();
|
|
20
|
+
private globalResources = new Set<ManagedResource>();
|
|
21
|
+
private cleanupCallbacks = new Map<string, (() => void)[]>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register a timeout with automatic cleanup
|
|
25
|
+
*/
|
|
26
|
+
setTimeout(
|
|
27
|
+
componentId: string,
|
|
28
|
+
callback: () => void,
|
|
29
|
+
delay: number,
|
|
30
|
+
metadata?: Record<string, any>
|
|
31
|
+
): number {
|
|
32
|
+
const id = window.setTimeout(() => {
|
|
33
|
+
this.removeResource(componentId, 'timer', id);
|
|
34
|
+
callback();
|
|
35
|
+
}, delay);
|
|
36
|
+
|
|
37
|
+
this.addResource(componentId, {
|
|
38
|
+
type: 'timer',
|
|
39
|
+
id,
|
|
40
|
+
cleanup: () => window.clearTimeout(id),
|
|
41
|
+
metadata
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register an interval with automatic cleanup
|
|
49
|
+
*/
|
|
50
|
+
setInterval(
|
|
51
|
+
componentId: string,
|
|
52
|
+
callback: () => void,
|
|
53
|
+
delay: number,
|
|
54
|
+
metadata?: Record<string, any>
|
|
55
|
+
): number {
|
|
56
|
+
const id = window.setInterval(callback, delay);
|
|
57
|
+
|
|
58
|
+
this.addResource(componentId, {
|
|
59
|
+
type: 'interval',
|
|
60
|
+
id,
|
|
61
|
+
cleanup: () => window.clearInterval(id),
|
|
62
|
+
metadata
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Register an animation frame with automatic cleanup
|
|
70
|
+
*/
|
|
71
|
+
requestAnimationFrame(
|
|
72
|
+
componentId: string,
|
|
73
|
+
callback: FrameRequestCallback,
|
|
74
|
+
metadata?: Record<string, any>
|
|
75
|
+
): number {
|
|
76
|
+
const id = window.requestAnimationFrame((time) => {
|
|
77
|
+
this.removeResource(componentId, 'animationFrame', id);
|
|
78
|
+
callback(time);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.addResource(componentId, {
|
|
82
|
+
type: 'animationFrame',
|
|
83
|
+
id,
|
|
84
|
+
cleanup: () => window.cancelAnimationFrame(id),
|
|
85
|
+
metadata
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return id;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clear a specific timeout
|
|
93
|
+
*/
|
|
94
|
+
clearTimeout(componentId: string, id: number): void {
|
|
95
|
+
window.clearTimeout(id);
|
|
96
|
+
this.removeResource(componentId, 'timer', id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear a specific interval
|
|
101
|
+
*/
|
|
102
|
+
clearInterval(componentId: string, id: number): void {
|
|
103
|
+
window.clearInterval(id);
|
|
104
|
+
this.removeResource(componentId, 'interval', id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Cancel a specific animation frame
|
|
109
|
+
*/
|
|
110
|
+
cancelAnimationFrame(componentId: string, id: number): void {
|
|
111
|
+
window.cancelAnimationFrame(id);
|
|
112
|
+
this.removeResource(componentId, 'animationFrame', id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Register an event listener with automatic cleanup
|
|
117
|
+
*/
|
|
118
|
+
addEventListener(
|
|
119
|
+
componentId: string,
|
|
120
|
+
target: EventTarget,
|
|
121
|
+
type: string,
|
|
122
|
+
listener: EventListener,
|
|
123
|
+
options?: AddEventListenerOptions
|
|
124
|
+
): void {
|
|
125
|
+
target.addEventListener(type, listener, options);
|
|
126
|
+
|
|
127
|
+
const resourceId = `${type}-${Date.now()}-${Math.random()}`;
|
|
128
|
+
this.addResource(componentId, {
|
|
129
|
+
type: 'eventListener',
|
|
130
|
+
id: resourceId,
|
|
131
|
+
cleanup: () => target.removeEventListener(type, listener, options),
|
|
132
|
+
metadata: { target, type, options }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Register a DOM element that needs cleanup
|
|
138
|
+
*/
|
|
139
|
+
registerDOMElement(
|
|
140
|
+
componentId: string,
|
|
141
|
+
element: HTMLElement,
|
|
142
|
+
cleanup?: () => void
|
|
143
|
+
): void {
|
|
144
|
+
const resourceId = `dom-${Date.now()}-${Math.random()}`;
|
|
145
|
+
this.addResource(componentId, {
|
|
146
|
+
type: 'domElement',
|
|
147
|
+
id: resourceId,
|
|
148
|
+
cleanup: () => {
|
|
149
|
+
if (cleanup) {
|
|
150
|
+
cleanup();
|
|
151
|
+
}
|
|
152
|
+
if (element.parentNode) {
|
|
153
|
+
element.parentNode.removeChild(element);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
metadata: { element }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Register a React root for cleanup
|
|
162
|
+
*/
|
|
163
|
+
registerReactRoot(
|
|
164
|
+
componentId: string,
|
|
165
|
+
root: any,
|
|
166
|
+
unmountFn: () => void
|
|
167
|
+
): void {
|
|
168
|
+
this.addResource(componentId, {
|
|
169
|
+
type: 'reactRoot',
|
|
170
|
+
id: `react-root-${componentId}`,
|
|
171
|
+
cleanup: unmountFn,
|
|
172
|
+
metadata: { root }
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register a generic cleanup callback for a component
|
|
178
|
+
*/
|
|
179
|
+
registerCleanup(componentId: string, cleanup: () => void): void {
|
|
180
|
+
if (!this.cleanupCallbacks.has(componentId)) {
|
|
181
|
+
this.cleanupCallbacks.set(componentId, []);
|
|
182
|
+
}
|
|
183
|
+
this.cleanupCallbacks.get(componentId)!.push(cleanup);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Register a global resource (not tied to a specific component)
|
|
188
|
+
*/
|
|
189
|
+
registerGlobalResource(resource: ManagedResource): void {
|
|
190
|
+
this.globalResources.add(resource);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add a resource to be managed
|
|
195
|
+
*/
|
|
196
|
+
private addResource(componentId: string, resource: ManagedResource): void {
|
|
197
|
+
if (!this.resources.has(componentId)) {
|
|
198
|
+
this.resources.set(componentId, new Set());
|
|
199
|
+
}
|
|
200
|
+
this.resources.get(componentId)!.add(resource);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Remove a specific resource
|
|
205
|
+
*/
|
|
206
|
+
private removeResource(
|
|
207
|
+
componentId: string,
|
|
208
|
+
type: ManagedResource['type'],
|
|
209
|
+
id: string | number
|
|
210
|
+
): void {
|
|
211
|
+
const componentResources = this.resources.get(componentId);
|
|
212
|
+
if (componentResources) {
|
|
213
|
+
const toRemove = Array.from(componentResources).find(
|
|
214
|
+
r => r.type === type && r.id === id
|
|
215
|
+
);
|
|
216
|
+
if (toRemove) {
|
|
217
|
+
componentResources.delete(toRemove);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Clean up all resources for a specific component
|
|
224
|
+
*/
|
|
225
|
+
cleanupComponent(componentId: string): void {
|
|
226
|
+
// Clean up tracked resources
|
|
227
|
+
const componentResources = this.resources.get(componentId);
|
|
228
|
+
if (componentResources) {
|
|
229
|
+
componentResources.forEach(resource => {
|
|
230
|
+
try {
|
|
231
|
+
resource.cleanup();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`Error cleaning up ${resource.type} resource:`, error);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
this.resources.delete(componentId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Execute cleanup callbacks
|
|
240
|
+
const callbacks = this.cleanupCallbacks.get(componentId);
|
|
241
|
+
if (callbacks) {
|
|
242
|
+
callbacks.forEach(callback => {
|
|
243
|
+
try {
|
|
244
|
+
callback();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Error executing cleanup callback:', error);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
this.cleanupCallbacks.delete(componentId);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Clean up all global resources
|
|
255
|
+
*/
|
|
256
|
+
cleanupGlobal(): void {
|
|
257
|
+
this.globalResources.forEach(resource => {
|
|
258
|
+
try {
|
|
259
|
+
resource.cleanup();
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error(`Error cleaning up global ${resource.type} resource:`, error);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
this.globalResources.clear();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Clean up all resources (components and global)
|
|
269
|
+
*/
|
|
270
|
+
cleanupAll(): void {
|
|
271
|
+
// Clean up all component resources
|
|
272
|
+
for (const componentId of this.resources.keys()) {
|
|
273
|
+
this.cleanupComponent(componentId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Clean up global resources
|
|
277
|
+
this.cleanupGlobal();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get resource statistics for debugging
|
|
282
|
+
*/
|
|
283
|
+
getStats(): {
|
|
284
|
+
componentCount: number;
|
|
285
|
+
resourceCounts: Record<string, number>;
|
|
286
|
+
globalResourceCount: number;
|
|
287
|
+
} {
|
|
288
|
+
const resourceCounts: Record<string, number> = {};
|
|
289
|
+
|
|
290
|
+
for (const resources of this.resources.values()) {
|
|
291
|
+
resources.forEach(resource => {
|
|
292
|
+
resourceCounts[resource.type] = (resourceCounts[resource.type] || 0) + 1;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
componentCount: this.resources.size,
|
|
298
|
+
resourceCounts,
|
|
299
|
+
globalResourceCount: this.globalResources.size
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Singleton instance
|
|
305
|
+
export const resourceManager = new ResourceManager();
|