@memberjunction/react-runtime 2.76.0 → 2.78.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 +18 -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 +36 -0
  24. package/dist/utilities/resource-manager.d.ts.map +1 -0
  25. package/dist/utilities/resource-manager.js +225 -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 +371 -0
  35. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,371 @@
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
+ // Type alias for timer IDs that works in both browser and Node.js
8
+ export type TimerId = number | NodeJS.Timeout;
9
+
10
+ export interface ManagedResource {
11
+ type: 'timer' | 'interval' | 'animationFrame' | 'eventListener' | 'domElement' | 'observable' | 'reactRoot';
12
+ id: string | TimerId;
13
+ cleanup: () => void;
14
+ metadata?: Record<string, any>;
15
+ }
16
+
17
+ /**
18
+ * Environment-agnostic timer functions that work in both browser and Node.js
19
+ */
20
+ const getTimerFunctions = () => {
21
+ // Check if we're in a browser environment
22
+ if (typeof window !== 'undefined' && window.setTimeout) {
23
+ return {
24
+ setTimeout: window.setTimeout.bind(window),
25
+ clearTimeout: window.clearTimeout.bind(window),
26
+ setInterval: window.setInterval.bind(window),
27
+ clearInterval: window.clearInterval.bind(window),
28
+ requestAnimationFrame: window.requestAnimationFrame?.bind(window),
29
+ cancelAnimationFrame: window.cancelAnimationFrame?.bind(window)
30
+ };
31
+ }
32
+ // Node.js environment
33
+ else if (typeof global !== 'undefined' && global.setTimeout) {
34
+ return {
35
+ setTimeout: global.setTimeout,
36
+ clearTimeout: global.clearTimeout,
37
+ setInterval: global.setInterval,
38
+ clearInterval: global.clearInterval,
39
+ requestAnimationFrame: undefined, // Not available in Node.js
40
+ cancelAnimationFrame: undefined
41
+ };
42
+ }
43
+ // Fallback - return no-op functions
44
+ else {
45
+ const noop = () => {};
46
+ const noopWithReturn = () => 0;
47
+ return {
48
+ setTimeout: noopWithReturn,
49
+ clearTimeout: noop,
50
+ setInterval: noopWithReturn,
51
+ clearInterval: noop,
52
+ requestAnimationFrame: undefined,
53
+ cancelAnimationFrame: undefined
54
+ };
55
+ }
56
+ };
57
+
58
+ const timers = getTimerFunctions();
59
+
60
+ /**
61
+ * ResourceManager provides centralized management of resources that need cleanup.
62
+ * This prevents memory leaks by ensuring all resources are properly disposed.
63
+ */
64
+ export class ResourceManager {
65
+ private resources = new Map<string, Set<ManagedResource>>();
66
+ private globalResources = new Set<ManagedResource>();
67
+ private cleanupCallbacks = new Map<string, (() => void)[]>();
68
+
69
+ /**
70
+ * Register a timeout with automatic cleanup
71
+ */
72
+ setTimeout(
73
+ componentId: string,
74
+ callback: () => void,
75
+ delay: number,
76
+ metadata?: Record<string, any>
77
+ ): number {
78
+ const id = timers.setTimeout(() => {
79
+ this.removeResource(componentId, 'timer', id);
80
+ callback();
81
+ }, delay);
82
+
83
+ this.addResource(componentId, {
84
+ type: 'timer',
85
+ id,
86
+ cleanup: () => timers.clearTimeout(id),
87
+ metadata
88
+ });
89
+
90
+ return id as any;
91
+ }
92
+
93
+ /**
94
+ * Register an interval with automatic cleanup
95
+ */
96
+ setInterval(
97
+ componentId: string,
98
+ callback: () => void,
99
+ delay: number,
100
+ metadata?: Record<string, any>
101
+ ): number {
102
+ const id = timers.setInterval(callback, delay);
103
+
104
+ this.addResource(componentId, {
105
+ type: 'interval',
106
+ id,
107
+ cleanup: () => timers.clearInterval(id),
108
+ metadata
109
+ });
110
+
111
+ return id as any;
112
+ }
113
+
114
+ /**
115
+ * Register an animation frame with automatic cleanup
116
+ */
117
+ requestAnimationFrame(
118
+ componentId: string,
119
+ callback: FrameRequestCallback,
120
+ metadata?: Record<string, any>
121
+ ): TimerId {
122
+ if (!timers.requestAnimationFrame) {
123
+ // Fallback to setTimeout in non-browser environments
124
+ return this.setTimeout(componentId, () => callback(Date.now()), 16, metadata);
125
+ }
126
+
127
+ const id = timers.requestAnimationFrame((time) => {
128
+ this.removeResource(componentId, 'animationFrame', id);
129
+ callback(time);
130
+ });
131
+
132
+ this.addResource(componentId, {
133
+ type: 'animationFrame',
134
+ id,
135
+ cleanup: () => timers.cancelAnimationFrame?.(id),
136
+ metadata
137
+ });
138
+
139
+ return id as any;
140
+ }
141
+
142
+ /**
143
+ * Clear a specific timeout
144
+ */
145
+ clearTimeout(componentId: string, id: number): void {
146
+ timers.clearTimeout(id);
147
+ this.removeResource(componentId, 'timer', id);
148
+ }
149
+
150
+ /**
151
+ * Clear a specific interval
152
+ */
153
+ clearInterval(componentId: string, id: number): void {
154
+ timers.clearInterval(id);
155
+ this.removeResource(componentId, 'interval', id);
156
+ }
157
+
158
+ /**
159
+ * Cancel a specific animation frame
160
+ */
161
+ cancelAnimationFrame(componentId: string, id: TimerId): void {
162
+ if (timers.cancelAnimationFrame) {
163
+ timers.cancelAnimationFrame(id as number);
164
+ } else {
165
+ // If we fell back to setTimeout, use clearTimeout
166
+ timers.clearTimeout(id as any);
167
+ }
168
+ this.removeResource(componentId, 'animationFrame', id);
169
+ }
170
+
171
+ /**
172
+ * Register an event listener with automatic cleanup
173
+ */
174
+ addEventListener(
175
+ componentId: string,
176
+ target: EventTarget,
177
+ type: string,
178
+ listener: EventListener,
179
+ options?: AddEventListenerOptions
180
+ ): void {
181
+ // Only add event listeners if we have a valid EventTarget (browser environment)
182
+ if (target && typeof target.addEventListener === 'function') {
183
+ target.addEventListener(type, listener, options);
184
+
185
+ const resourceId = `${type}-${Date.now()}-${Math.random()}`;
186
+ this.addResource(componentId, {
187
+ type: 'eventListener',
188
+ id: resourceId,
189
+ cleanup: () => {
190
+ if (target && typeof target.removeEventListener === 'function') {
191
+ target.removeEventListener(type, listener, options);
192
+ }
193
+ },
194
+ metadata: { target, type, options }
195
+ });
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Register a DOM element that needs cleanup
201
+ */
202
+ registerDOMElement(
203
+ componentId: string,
204
+ element: any, // Use 'any' to avoid HTMLElement type in Node.js
205
+ cleanup?: () => void
206
+ ): void {
207
+ // Only register if we're in a browser environment with DOM support
208
+ if (typeof document !== 'undefined' && element && element.parentNode) {
209
+ const resourceId = `dom-${Date.now()}-${Math.random()}`;
210
+ this.addResource(componentId, {
211
+ type: 'domElement',
212
+ id: resourceId,
213
+ cleanup: () => {
214
+ if (cleanup) {
215
+ cleanup();
216
+ }
217
+ if (element && element.parentNode && typeof element.parentNode.removeChild === 'function') {
218
+ element.parentNode.removeChild(element);
219
+ }
220
+ },
221
+ metadata: { element }
222
+ });
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Register a React root for cleanup
228
+ */
229
+ registerReactRoot(
230
+ componentId: string,
231
+ root: any,
232
+ unmountFn: () => void
233
+ ): void {
234
+ this.addResource(componentId, {
235
+ type: 'reactRoot',
236
+ id: `react-root-${componentId}`,
237
+ cleanup: unmountFn,
238
+ metadata: { root }
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Register a generic cleanup callback for a component
244
+ */
245
+ registerCleanup(componentId: string, cleanup: () => void): void {
246
+ if (!this.cleanupCallbacks.has(componentId)) {
247
+ this.cleanupCallbacks.set(componentId, []);
248
+ }
249
+ this.cleanupCallbacks.get(componentId)!.push(cleanup);
250
+ }
251
+
252
+ /**
253
+ * Register a global resource (not tied to a specific component)
254
+ */
255
+ registerGlobalResource(resource: ManagedResource): void {
256
+ this.globalResources.add(resource);
257
+ }
258
+
259
+ /**
260
+ * Add a resource to be managed
261
+ */
262
+ private addResource(componentId: string, resource: ManagedResource): void {
263
+ if (!this.resources.has(componentId)) {
264
+ this.resources.set(componentId, new Set());
265
+ }
266
+ this.resources.get(componentId)!.add(resource);
267
+ }
268
+
269
+ /**
270
+ * Remove a specific resource
271
+ */
272
+ private removeResource(
273
+ componentId: string,
274
+ type: ManagedResource['type'],
275
+ id: string | number | NodeJS.Timeout
276
+ ): void {
277
+ const componentResources = this.resources.get(componentId);
278
+ if (componentResources) {
279
+ const toRemove = Array.from(componentResources).find(
280
+ r => r.type === type && r.id === id
281
+ );
282
+ if (toRemove) {
283
+ componentResources.delete(toRemove);
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Clean up all resources for a specific component
290
+ */
291
+ cleanupComponent(componentId: string): void {
292
+ // Clean up tracked resources
293
+ const componentResources = this.resources.get(componentId);
294
+ if (componentResources) {
295
+ componentResources.forEach(resource => {
296
+ try {
297
+ resource.cleanup();
298
+ } catch (error) {
299
+ console.error(`Error cleaning up ${resource.type} resource:`, error);
300
+ }
301
+ });
302
+ this.resources.delete(componentId);
303
+ }
304
+
305
+ // Execute cleanup callbacks
306
+ const callbacks = this.cleanupCallbacks.get(componentId);
307
+ if (callbacks) {
308
+ callbacks.forEach(callback => {
309
+ try {
310
+ callback();
311
+ } catch (error) {
312
+ console.error('Error executing cleanup callback:', error);
313
+ }
314
+ });
315
+ this.cleanupCallbacks.delete(componentId);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Clean up all global resources
321
+ */
322
+ cleanupGlobal(): void {
323
+ this.globalResources.forEach(resource => {
324
+ try {
325
+ resource.cleanup();
326
+ } catch (error) {
327
+ console.error(`Error cleaning up global ${resource.type} resource:`, error);
328
+ }
329
+ });
330
+ this.globalResources.clear();
331
+ }
332
+
333
+ /**
334
+ * Clean up all resources (components and global)
335
+ */
336
+ cleanupAll(): void {
337
+ // Clean up all component resources
338
+ for (const componentId of this.resources.keys()) {
339
+ this.cleanupComponent(componentId);
340
+ }
341
+
342
+ // Clean up global resources
343
+ this.cleanupGlobal();
344
+ }
345
+
346
+ /**
347
+ * Get resource statistics for debugging
348
+ */
349
+ getStats(): {
350
+ componentCount: number;
351
+ resourceCounts: Record<string, number>;
352
+ globalResourceCount: number;
353
+ } {
354
+ const resourceCounts: Record<string, number> = {};
355
+
356
+ for (const resources of this.resources.values()) {
357
+ resources.forEach(resource => {
358
+ resourceCounts[resource.type] = (resourceCounts[resource.type] || 0) + 1;
359
+ });
360
+ }
361
+
362
+ return {
363
+ componentCount: this.resources.size,
364
+ resourceCounts,
365
+ globalResourceCount: this.globalResources.size
366
+ };
367
+ }
368
+ }
369
+
370
+ // Singleton instance
371
+ export const resourceManager = new ResourceManager();