@memberjunction/react-runtime 2.75.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 (52) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/dist/compiler/component-compiler.d.ts +0 -1
  4. package/dist/compiler/component-compiler.d.ts.map +1 -1
  5. package/dist/compiler/component-compiler.js +34 -25
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +10 -2
  9. package/dist/registry/component-registry.d.ts +1 -0
  10. package/dist/registry/component-registry.d.ts.map +1 -1
  11. package/dist/registry/component-registry.js +6 -3
  12. package/dist/runtime/index.d.ts +2 -1
  13. package/dist/runtime/index.d.ts.map +1 -1
  14. package/dist/runtime/index.js +4 -2
  15. package/dist/runtime/prop-builder.d.ts +1 -2
  16. package/dist/runtime/prop-builder.d.ts.map +1 -1
  17. package/dist/runtime/prop-builder.js +4 -76
  18. package/dist/runtime/react-root-manager.d.ts +26 -0
  19. package/dist/runtime/react-root-manager.d.ts.map +1 -0
  20. package/dist/runtime/react-root-manager.js +122 -0
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/dist/utilities/cache-manager.d.ts +38 -0
  24. package/dist/utilities/cache-manager.d.ts.map +1 -0
  25. package/dist/utilities/cache-manager.js +156 -0
  26. package/dist/utilities/core-libraries.d.ts +5 -0
  27. package/dist/utilities/core-libraries.d.ts.map +1 -0
  28. package/dist/utilities/core-libraries.js +52 -0
  29. package/dist/utilities/index.d.ts +2 -0
  30. package/dist/utilities/index.d.ts.map +1 -1
  31. package/dist/utilities/index.js +2 -0
  32. package/dist/utilities/library-loader.d.ts +2 -2
  33. package/dist/utilities/library-loader.d.ts.map +1 -1
  34. package/dist/utilities/library-loader.js +52 -24
  35. package/dist/utilities/resource-manager.d.ts +34 -0
  36. package/dist/utilities/resource-manager.d.ts.map +1 -0
  37. package/dist/utilities/resource-manager.js +174 -0
  38. package/package.json +4 -4
  39. package/samples/entities-1.js +493 -0
  40. package/src/compiler/component-compiler.ts +64 -35
  41. package/src/index.ts +18 -1
  42. package/src/registry/component-registry.ts +14 -4
  43. package/src/runtime/index.ts +7 -2
  44. package/src/runtime/prop-builder.ts +5 -112
  45. package/src/runtime/react-root-manager.ts +218 -0
  46. package/src/types/index.ts +2 -0
  47. package/src/utilities/cache-manager.ts +253 -0
  48. package/src/utilities/core-libraries.ts +61 -0
  49. package/src/utilities/index.ts +3 -1
  50. package/src/utilities/library-loader.ts +111 -47
  51. package/src/utilities/resource-manager.ts +305 -0
  52. 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
+ }
@@ -0,0 +1,61 @@
1
+ import { ExternalLibraryConfig } from '../types/library-config';
2
+
3
+ /**
4
+ * Core runtime libraries required for the React runtime to function.
5
+ * These are not plugin libraries and are always loaded.
6
+ */
7
+ export const CORE_RUNTIME_LIBRARIES: ExternalLibraryConfig[] = [
8
+ {
9
+ id: 'react',
10
+ name: 'react',
11
+ displayName: 'React',
12
+ category: 'runtime',
13
+ globalVariable: 'React',
14
+ version: '18.2.0',
15
+ cdnUrl: 'https://unpkg.com/react@18.2.0/umd/react.production.min.js',
16
+ description: 'React core library',
17
+ isEnabled: true,
18
+ isCore: true,
19
+ isRuntimeOnly: true
20
+ },
21
+ {
22
+ id: 'react-dom',
23
+ name: 'react-dom',
24
+ displayName: 'ReactDOM',
25
+ category: 'runtime',
26
+ globalVariable: 'ReactDOM',
27
+ version: '18.2.0',
28
+ cdnUrl: 'https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js',
29
+ description: 'React DOM library',
30
+ isEnabled: true,
31
+ isCore: true,
32
+ isRuntimeOnly: true
33
+ },
34
+ {
35
+ id: 'babel-standalone',
36
+ name: '@babel/standalone',
37
+ displayName: 'Babel Standalone',
38
+ category: 'runtime',
39
+ globalVariable: 'Babel',
40
+ version: '7.24.4',
41
+ cdnUrl: 'https://unpkg.com/@babel/standalone@7.24.4/babel.min.js',
42
+ description: 'Babel compiler for JSX transformation',
43
+ isEnabled: true,
44
+ isCore: true,
45
+ isRuntimeOnly: true
46
+ }
47
+ ];
48
+
49
+ /**
50
+ * Get the core runtime libraries configuration
51
+ */
52
+ export function getCoreRuntimeLibraries(): ExternalLibraryConfig[] {
53
+ return CORE_RUNTIME_LIBRARIES;
54
+ }
55
+
56
+ /**
57
+ * Check if a library ID is a core runtime library
58
+ */
59
+ export function isCoreRuntimeLibrary(libraryId: string): boolean {
60
+ return CORE_RUNTIME_LIBRARIES.some(lib => lib.id === libraryId);
61
+ }
@@ -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';
@@ -9,6 +9,11 @@ import {
9
9
  StandardLibraryManager
10
10
  } from './standard-libraries';
11
11
  import { LibraryConfiguration, ExternalLibraryConfig, LibraryLoadOptions as ConfigLoadOptions } from '../types/library-config';
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';
12
17
 
13
18
  /**
14
19
  * Represents a loaded script or CSS resource
@@ -52,12 +57,30 @@ export class LibraryLoader {
52
57
  /**
53
58
  * Load all standard libraries (core + UI + CSS)
54
59
  * This is the main method that should be used by test harness and Angular wrapper
60
+ * @param config Optional full library configuration to replace the default
61
+ * @param additionalLibraries Optional additional libraries to merge with the configuration
55
62
  */
56
- static async loadAllLibraries(config?: LibraryConfiguration): Promise<LibraryLoadResult> {
63
+ static async loadAllLibraries(
64
+ config?: LibraryConfiguration,
65
+ additionalLibraries?: ExternalLibraryConfig[]
66
+ ): Promise<LibraryLoadResult> {
57
67
  if (config) {
58
68
  StandardLibraryManager.setConfiguration(config);
59
69
  }
60
70
 
71
+ // If additional libraries are provided, merge them with the current configuration
72
+ if (additionalLibraries && additionalLibraries.length > 0) {
73
+ const currentConfig = StandardLibraryManager.getConfiguration();
74
+ const mergedConfig: LibraryConfiguration = {
75
+ libraries: [...currentConfig.libraries, ...additionalLibraries],
76
+ metadata: {
77
+ ...currentConfig.metadata,
78
+ lastUpdated: new Date().toISOString()
79
+ }
80
+ };
81
+ StandardLibraryManager.setConfiguration(mergedConfig);
82
+ }
83
+
61
84
  return this.loadLibrariesFromConfig();
62
85
  }
63
86
 
@@ -65,55 +88,55 @@ export class LibraryLoader {
65
88
  * Load libraries based on the current configuration
66
89
  */
67
90
  static async loadLibrariesFromConfig(options?: ConfigLoadOptions): Promise<LibraryLoadResult> {
91
+ // Always load core runtime libraries first
92
+ const coreLibraries = getCoreRuntimeLibraries();
93
+ const corePromises = coreLibraries.map(lib =>
94
+ this.loadScript(lib.cdnUrl, lib.globalVariable)
95
+ );
96
+
97
+ const coreResults = await Promise.all(corePromises);
98
+ const React = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'React');
99
+ const ReactDOM = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'ReactDOM');
100
+ const Babel = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'Babel');
101
+
102
+ // Now load plugin libraries from configuration
68
103
  const config = StandardLibraryManager.getConfiguration();
69
104
  const enabledLibraries = StandardLibraryManager.getEnabledLibraries();
70
105
 
106
+ // Filter out any core runtime libraries from plugin configuration
107
+ let pluginLibraries = enabledLibraries.filter(lib => !isCoreRuntimeLibrary(lib.id));
108
+
71
109
  // Apply options filters if provided
72
- let librariesToLoad = enabledLibraries;
73
110
  if (options) {
74
111
  if (options.categories) {
75
- librariesToLoad = librariesToLoad.filter(lib =>
112
+ pluginLibraries = pluginLibraries.filter(lib =>
76
113
  options.categories!.includes(lib.category)
77
114
  );
78
115
  }
79
116
  if (options.excludeRuntimeOnly) {
80
- librariesToLoad = librariesToLoad.filter(lib => !lib.isRuntimeOnly);
117
+ pluginLibraries = pluginLibraries.filter(lib => !lib.isRuntimeOnly);
81
118
  }
82
119
  }
83
120
 
84
- // Separate runtime and component libraries
85
- const runtimeLibs = librariesToLoad.filter(lib => lib.category === 'runtime');
86
- const componentLibs = librariesToLoad.filter(lib => lib.category !== 'runtime');
87
-
88
- // Load runtime libraries first (React, ReactDOM, Babel)
89
- const runtimePromises = runtimeLibs.map(lib =>
90
- this.loadScript(lib.cdnUrl, lib.globalVariable)
91
- );
92
-
93
- const runtimeResults = await Promise.all(runtimePromises);
94
- const React = runtimeResults.find((_, i) => runtimeLibs[i].globalVariable === 'React');
95
- const ReactDOM = runtimeResults.find((_, i) => runtimeLibs[i].globalVariable === 'ReactDOM');
96
- const Babel = runtimeResults.find((_, i) => runtimeLibs[i].globalVariable === 'Babel');
97
-
98
- // Load CSS files (non-blocking)
99
- componentLibs.forEach(lib => {
121
+ // Load CSS files for plugin libraries (non-blocking)
122
+ pluginLibraries.forEach(lib => {
100
123
  if (lib.cdnCssUrl) {
101
124
  this.loadCSS(lib.cdnCssUrl);
102
125
  }
103
126
  });
104
127
 
105
- // Load component libraries
106
- const componentPromises = componentLibs.map(lib =>
128
+ // Load plugin libraries
129
+ const pluginPromises = pluginLibraries.map(lib =>
107
130
  this.loadScript(lib.cdnUrl, lib.globalVariable)
108
131
  );
109
132
 
110
- const componentResults = await Promise.all(componentPromises);
133
+ const pluginResults = await Promise.all(pluginPromises);
111
134
 
112
- // Build libraries object
135
+ // Build libraries object (only contains plugin libraries)
113
136
  const libraries: StandardLibraries = {};
114
137
 
115
- componentLibs.forEach((lib, index) => {
116
- libraries[lib.globalVariable] = componentResults[index];
138
+ pluginLibraries.forEach((lib, index) => {
139
+ libraries[lib.globalVariable] = pluginResults[index];
117
140
  });
118
141
 
119
142
  return {
@@ -195,28 +218,46 @@ export class LibraryLoader {
195
218
  script.async = true;
196
219
  script.crossOrigin = 'anonymous';
197
220
 
198
- script.onload = () => {
221
+ const cleanup = () => {
222
+ script.removeEventListener('load', onLoad);
223
+ script.removeEventListener('error', onError);
224
+ };
225
+
226
+ const onLoad = () => {
227
+ cleanup();
199
228
  const global = (window as any)[globalName];
200
229
  if (global) {
201
230
  resolve(global);
202
231
  } else {
203
232
  // Some libraries may take a moment to initialize
204
- setTimeout(() => {
205
- const delayedGlobal = (window as any)[globalName];
206
- if (delayedGlobal) {
207
- resolve(delayedGlobal);
208
- } else {
209
- reject(new Error(`${globalName} not found after script load`));
210
- }
211
- }, 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
+ );
212
246
  }
213
247
  };
214
248
 
215
- script.onerror = () => {
249
+ const onError = () => {
250
+ cleanup();
216
251
  reject(new Error(`Failed to load script: ${url}`));
217
252
  };
218
253
 
254
+ script.addEventListener('load', onLoad);
255
+ script.addEventListener('error', onError);
256
+
219
257
  document.head.appendChild(script);
258
+
259
+ // Register the script element for cleanup
260
+ resourceManager.registerDOMElement(LIBRARY_LOADER_COMPONENT_ID, script);
220
261
  });
221
262
 
222
263
  this.loadedResources.set(url, {
@@ -244,6 +285,9 @@ export class LibraryLoader {
244
285
  link.rel = 'stylesheet';
245
286
  link.href = url;
246
287
  document.head.appendChild(link);
288
+
289
+ // Register the link element for cleanup
290
+ resourceManager.registerDOMElement(LIBRARY_LOADER_COMPONENT_ID, link);
247
291
 
248
292
  this.loadedResources.set(url, {
249
293
  element: link,
@@ -266,14 +310,19 @@ export class LibraryLoader {
266
310
  resolve(global);
267
311
  } else {
268
312
  // Retry after a short delay
269
- setTimeout(() => {
270
- const delayedGlobal = (window as any)[globalName];
271
- if (delayedGlobal) {
272
- resolve(delayedGlobal);
273
- } else {
274
- reject(new Error(`${globalName} not found after script load`));
275
- }
276
- }, 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
+ );
277
326
  }
278
327
  };
279
328
 
@@ -285,10 +334,15 @@ export class LibraryLoader {
285
334
 
286
335
  // Wait for load
287
336
  const loadHandler = () => {
288
- script.removeEventListener('load', loadHandler);
289
337
  checkGlobal();
290
338
  };
291
- script.addEventListener('load', loadHandler);
339
+ resourceManager.addEventListener(
340
+ LIBRARY_LOADER_COMPONENT_ID,
341
+ script,
342
+ 'load',
343
+ loadHandler,
344
+ { once: true }
345
+ );
292
346
  }
293
347
 
294
348
 
@@ -300,9 +354,19 @@ export class LibraryLoader {
300
354
  }
301
355
 
302
356
  /**
303
- * Clear loaded resources cache
357
+ * Clear loaded resources cache and cleanup DOM elements
304
358
  */
305
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
+
306
367
  this.loadedResources.clear();
368
+
369
+ // Clean up any resources managed by resource manager
370
+ resourceManager.cleanupComponent(LIBRARY_LOADER_COMPONENT_ID);
307
371
  }
308
372
  }