@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -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 +36 -0
- package/dist/utilities/resource-manager.d.ts.map +1 -0
- package/dist/utilities/resource-manager.js +225 -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 +371 -0
- 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();
|