@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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +10 -1
  6. package/dist/registry/component-registry.d.ts +1 -0
  7. package/dist/registry/component-registry.d.ts.map +1 -1
  8. package/dist/registry/component-registry.js +6 -3
  9. package/dist/runtime/index.d.ts +1 -0
  10. package/dist/runtime/index.d.ts.map +1 -1
  11. package/dist/runtime/index.js +4 -1
  12. package/dist/runtime/react-root-manager.d.ts +26 -0
  13. package/dist/runtime/react-root-manager.d.ts.map +1 -0
  14. package/dist/runtime/react-root-manager.js +122 -0
  15. package/dist/utilities/cache-manager.d.ts +38 -0
  16. package/dist/utilities/cache-manager.d.ts.map +1 -0
  17. package/dist/utilities/cache-manager.js +156 -0
  18. package/dist/utilities/index.d.ts +2 -0
  19. package/dist/utilities/index.d.ts.map +1 -1
  20. package/dist/utilities/index.js +2 -0
  21. package/dist/utilities/library-loader.d.ts.map +1 -1
  22. package/dist/utilities/library-loader.js +25 -8
  23. package/dist/utilities/resource-manager.d.ts +34 -0
  24. package/dist/utilities/resource-manager.d.ts.map +1 -0
  25. package/dist/utilities/resource-manager.js +174 -0
  26. package/package.json +4 -4
  27. package/src/index.ts +18 -0
  28. package/src/registry/component-registry.ts +14 -4
  29. package/src/runtime/index.ts +7 -1
  30. package/src/runtime/react-root-manager.ts +218 -0
  31. package/src/utilities/cache-manager.ts +253 -0
  32. package/src/utilities/index.ts +3 -1
  33. package/src/utilities/library-loader.ts +66 -21
  34. package/src/utilities/resource-manager.ts +305 -0
  35. 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
+ }
@@ -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
- script.onload = () => {
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
- const delayedGlobal = (window as any)[globalName];
225
- if (delayedGlobal) {
226
- resolve(delayedGlobal);
227
- } else {
228
- reject(new Error(`${globalName} not found after script load`));
229
- }
230
- }, 100);
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
- script.onerror = () => {
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
- const delayedGlobal = (window as any)[globalName];
290
- if (delayedGlobal) {
291
- resolve(delayedGlobal);
292
- } else {
293
- reject(new Error(`${globalName} not found after script load`));
294
- }
295
- }, 100);
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
- script.addEventListener('load', loadHandler);
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();