@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,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview React root lifecycle management for preventing memory leaks
|
|
3
|
+
* @module @memberjunction/react-runtime/runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resourceManager } from '../utilities/resource-manager';
|
|
7
|
+
|
|
8
|
+
export interface ManagedReactRoot {
|
|
9
|
+
id: string;
|
|
10
|
+
root: any; // React root instance
|
|
11
|
+
container: HTMLElement;
|
|
12
|
+
isRendering: boolean;
|
|
13
|
+
lastRenderTime?: Date;
|
|
14
|
+
componentId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Manages React root instances to prevent memory leaks and ensure proper cleanup.
|
|
19
|
+
* Handles safe rendering and unmounting with protection against concurrent operations.
|
|
20
|
+
*/
|
|
21
|
+
export class ReactRootManager {
|
|
22
|
+
private roots = new Map<string, ManagedReactRoot>();
|
|
23
|
+
private renderingRoots = new Set<string>();
|
|
24
|
+
private unmountQueue = new Map<string, () => void>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a new managed React root
|
|
28
|
+
* @param container - The DOM container element
|
|
29
|
+
* @param createRootFn - Function to create the React root (e.g., ReactDOM.createRoot)
|
|
30
|
+
* @param componentId - Optional component ID for resource tracking
|
|
31
|
+
* @returns The root ID for future operations
|
|
32
|
+
*/
|
|
33
|
+
createRoot(
|
|
34
|
+
container: HTMLElement,
|
|
35
|
+
createRootFn: (container: HTMLElement) => any,
|
|
36
|
+
componentId?: string
|
|
37
|
+
): string {
|
|
38
|
+
const rootId = `react-root-${Date.now()}-${Math.random()}`;
|
|
39
|
+
const root = createRootFn(container);
|
|
40
|
+
|
|
41
|
+
const managedRoot: ManagedReactRoot = {
|
|
42
|
+
id: rootId,
|
|
43
|
+
root,
|
|
44
|
+
container,
|
|
45
|
+
isRendering: false,
|
|
46
|
+
componentId
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.roots.set(rootId, managedRoot);
|
|
50
|
+
|
|
51
|
+
// Register with resource manager if component ID provided
|
|
52
|
+
if (componentId) {
|
|
53
|
+
resourceManager.registerReactRoot(
|
|
54
|
+
componentId,
|
|
55
|
+
root,
|
|
56
|
+
() => this.unmountRoot(rootId)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return rootId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Safely render content to a React root
|
|
65
|
+
* @param rootId - The root ID
|
|
66
|
+
* @param element - React element to render
|
|
67
|
+
* @param onComplete - Optional callback when render completes
|
|
68
|
+
*/
|
|
69
|
+
render(
|
|
70
|
+
rootId: string,
|
|
71
|
+
element: any,
|
|
72
|
+
onComplete?: () => void
|
|
73
|
+
): void {
|
|
74
|
+
const managedRoot = this.roots.get(rootId);
|
|
75
|
+
if (!managedRoot) {
|
|
76
|
+
console.warn(`React root ${rootId} not found`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Don't render if we're already unmounting
|
|
81
|
+
if (this.unmountQueue.has(rootId)) {
|
|
82
|
+
console.warn(`React root ${rootId} is being unmounted, skipping render`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Mark as rendering
|
|
87
|
+
managedRoot.isRendering = true;
|
|
88
|
+
this.renderingRoots.add(rootId);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
managedRoot.root.render(element);
|
|
92
|
+
managedRoot.lastRenderTime = new Date();
|
|
93
|
+
|
|
94
|
+
// Use microtask to ensure React has completed its work
|
|
95
|
+
Promise.resolve().then(() => {
|
|
96
|
+
managedRoot.isRendering = false;
|
|
97
|
+
this.renderingRoots.delete(rootId);
|
|
98
|
+
|
|
99
|
+
// Process any pending unmount
|
|
100
|
+
const pendingUnmount = this.unmountQueue.get(rootId);
|
|
101
|
+
if (pendingUnmount) {
|
|
102
|
+
this.unmountQueue.delete(rootId);
|
|
103
|
+
pendingUnmount();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (onComplete) {
|
|
107
|
+
onComplete();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Clean up rendering state on error
|
|
112
|
+
managedRoot.isRendering = false;
|
|
113
|
+
this.renderingRoots.delete(rootId);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Safely unmount a React root
|
|
120
|
+
* @param rootId - The root ID
|
|
121
|
+
* @param force - Force unmount even if rendering
|
|
122
|
+
*/
|
|
123
|
+
unmountRoot(rootId: string, force: boolean = false): Promise<void> {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const managedRoot = this.roots.get(rootId);
|
|
126
|
+
if (!managedRoot) {
|
|
127
|
+
resolve();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const performUnmount = () => {
|
|
132
|
+
try {
|
|
133
|
+
managedRoot.root.unmount();
|
|
134
|
+
this.roots.delete(rootId);
|
|
135
|
+
this.renderingRoots.delete(rootId);
|
|
136
|
+
|
|
137
|
+
// Clean up container to ensure no dangling references
|
|
138
|
+
if (managedRoot.container) {
|
|
139
|
+
// Clear React's internal references
|
|
140
|
+
delete (managedRoot.container as any)._reactRootContainer;
|
|
141
|
+
// Clear all event listeners
|
|
142
|
+
const newContainer = managedRoot.container.cloneNode(false) as HTMLElement;
|
|
143
|
+
managedRoot.container.parentNode?.replaceChild(newContainer, managedRoot.container);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
resolve();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`Error unmounting React root ${rootId}:`, error);
|
|
149
|
+
// Still clean up our tracking even if unmount failed
|
|
150
|
+
this.roots.delete(rootId);
|
|
151
|
+
this.renderingRoots.delete(rootId);
|
|
152
|
+
resolve();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// If not rendering or force unmount, unmount immediately
|
|
157
|
+
if (!managedRoot.isRendering || force) {
|
|
158
|
+
performUnmount();
|
|
159
|
+
} else {
|
|
160
|
+
// Queue unmount for after rendering completes
|
|
161
|
+
this.unmountQueue.set(rootId, () => {
|
|
162
|
+
performUnmount();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Unmount all roots associated with a component
|
|
170
|
+
* @param componentId - The component ID
|
|
171
|
+
*/
|
|
172
|
+
async unmountComponentRoots(componentId: string): Promise<void> {
|
|
173
|
+
const rootIds: string[] = [];
|
|
174
|
+
|
|
175
|
+
for (const [rootId, managedRoot] of this.roots) {
|
|
176
|
+
if (managedRoot.componentId === componentId) {
|
|
177
|
+
rootIds.push(rootId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await Promise.all(rootIds.map(id => this.unmountRoot(id)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a root is currently rendering
|
|
186
|
+
* @param rootId - The root ID
|
|
187
|
+
* @returns true if rendering
|
|
188
|
+
*/
|
|
189
|
+
isRendering(rootId: string): boolean {
|
|
190
|
+
return this.renderingRoots.has(rootId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get statistics about managed roots
|
|
195
|
+
*/
|
|
196
|
+
getStats(): {
|
|
197
|
+
totalRoots: number;
|
|
198
|
+
renderingRoots: number;
|
|
199
|
+
pendingUnmounts: number;
|
|
200
|
+
} {
|
|
201
|
+
return {
|
|
202
|
+
totalRoots: this.roots.size,
|
|
203
|
+
renderingRoots: this.renderingRoots.size,
|
|
204
|
+
pendingUnmounts: this.unmountQueue.size
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Clean up all roots (for testing or shutdown)
|
|
210
|
+
*/
|
|
211
|
+
async cleanup(): Promise<void> {
|
|
212
|
+
const allRootIds = Array.from(this.roots.keys());
|
|
213
|
+
await Promise.all(allRootIds.map(id => this.unmountRoot(id, true)));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Singleton instance
|
|
218
|
+
export const reactRootManager = new ReactRootManager();
|
|
@@ -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
|
}
|