@memberjunction/react-runtime 2.75.0 → 2.76.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.
@@ -41,7 +41,8 @@ export function buildComponentProps(
41
41
  callbacks: ComponentCallbacks = {},
42
42
  components: Record<string, any> = {},
43
43
  styles?: ComponentStyles,
44
- options: PropBuilderOptions = {}
44
+ options: PropBuilderOptions = {},
45
+ onStateChanged?: (stateUpdate: Record<string, any>) => void
45
46
  ): ComponentProps {
46
47
  const {
47
48
  validate = true,
@@ -61,7 +62,8 @@ export function buildComponentProps(
61
62
  utilities,
62
63
  callbacks: normalizeCallbacks(callbacks, debounceUpdateUserState),
63
64
  components,
64
- styles: normalizeStyles(styles)
65
+ styles: normalizeStyles(styles),
66
+ onStateChanged
65
67
  };
66
68
 
67
69
  // Validate if enabled
@@ -124,74 +126,6 @@ export function normalizeCallbacks(callbacks: any, debounceMs: number = 3000): C
124
126
  normalized.OpenEntityRecord = callbacks.OpenEntityRecord;
125
127
  }
126
128
 
127
- if (callbacks.UpdateUserState && typeof callbacks.UpdateUserState === 'function') {
128
- // Create a debounced version of UpdateUserState with loop detection
129
- const originalCallback = callbacks.UpdateUserState;
130
-
131
- // Get or create a subject for this callback
132
- let subject = updateUserStateSubjects.get(originalCallback);
133
- if (!subject) {
134
- subject = new Subject<any>();
135
- updateUserStateSubjects.set(originalCallback, subject);
136
-
137
- // Subscribe to the subject with debounce
138
- const subscription = subject.pipe(
139
- debounceTime(debounceMs)
140
- ).subscribe(state => {
141
- console.log(`[Skip Component] UpdateUserState called after ${debounceMs}ms debounce`);
142
- originalCallback(state);
143
- });
144
-
145
- // Store the subscription for cleanup
146
- updateUserStateSubscriptions.set(originalCallback, subscription);
147
- }
148
-
149
- // Get or create loop detection state
150
- let loopState = loopDetectionStates.get(originalCallback);
151
- if (!loopState) {
152
- loopState = { count: 0, lastUpdate: 0, lastState: null };
153
- loopDetectionStates.set(originalCallback, loopState);
154
- }
155
-
156
- // Return a function that prevents redundant updates
157
- normalized.UpdateUserState = (state: any) => {
158
- // Check if this is a redundant update
159
- if (loopState!.lastState && deepEqual(state, loopState!.lastState)) {
160
- console.log('[Skip Component] Skipping redundant state update');
161
- return; // Don't process identical state updates
162
- }
163
-
164
- const now = Date.now();
165
- const timeSinceLastUpdate = now - loopState!.lastUpdate;
166
-
167
- // Check for rapid updates
168
- if (timeSinceLastUpdate < 100) {
169
- loopState!.count++;
170
-
171
- if (loopState!.count > 5) {
172
- console.error('[Skip Component] Rapid state updates detected - possible infinite loop');
173
- console.error('Updates in last 100ms:', loopState!.count);
174
- // Still process the update but warn
175
- }
176
- } else {
177
- // Reset counter if more than 100ms has passed
178
- loopState!.count = 0;
179
- }
180
-
181
- loopState!.lastUpdate = now;
182
- loopState!.lastState = JSON.parse(JSON.stringify(state)); // Deep clone to preserve state
183
-
184
- console.log('[Skip Component] Processing state update');
185
-
186
- // Push to debounce subject (which already has 3 second debounce)
187
- subject!.next(state);
188
- };
189
- }
190
-
191
- if (callbacks.NotifyEvent && typeof callbacks.NotifyEvent === 'function') {
192
- normalized.NotifyEvent = callbacks.NotifyEvent;
193
- }
194
-
195
129
  return normalized;
196
130
  }
197
131
 
@@ -284,34 +218,7 @@ export function mergeProps(...propsList: Partial<ComponentProps>[]): ComponentPr
284
218
 
285
219
  return merged;
286
220
  }
287
-
288
- /**
289
- * Cleanup function for prop builder resources
290
- * @param callbacks - The callbacks object that was used to build props
291
- */
292
- export function cleanupPropBuilder(callbacks: ComponentCallbacks): void {
293
- if (callbacks.UpdateUserState && typeof callbacks.UpdateUserState === 'function') {
294
- const originalCallback = callbacks.UpdateUserState;
295
-
296
- // Unsubscribe from the subject
297
- const subscription = updateUserStateSubscriptions.get(originalCallback);
298
- if (subscription) {
299
- subscription.unsubscribe();
300
- updateUserStateSubscriptions.delete(originalCallback);
301
- }
302
-
303
- // Complete and remove the subject
304
- const subject = updateUserStateSubjects.get(originalCallback);
305
- if (subject) {
306
- subject.complete();
307
- updateUserStateSubjects.delete(originalCallback);
308
- }
309
-
310
- // Clear loop detection state
311
- loopDetectionStates.delete(originalCallback);
312
- }
313
- }
314
-
221
+
315
222
  /**
316
223
  * Creates a props transformer function
317
224
  * @param transformations - Map of prop paths to transformer functions
@@ -372,20 +279,6 @@ export function wrapCallbacksWithLogging(
372
279
  };
373
280
  }
374
281
 
375
- if (callbacks.UpdateUserState) {
376
- wrapped.UpdateUserState = (state: any) => {
377
- console.log(`[${componentName}] UpdateUserState called:`, state);
378
- callbacks.UpdateUserState!(state);
379
- };
380
- }
381
-
382
- if (callbacks.NotifyEvent) {
383
- wrapped.NotifyEvent = (event: string, data: any) => {
384
- console.log(`[${componentName}] NotifyEvent called:`, { event, data });
385
- callbacks.NotifyEvent!(event, data);
386
- };
387
- }
388
-
389
282
  return wrapped;
390
283
  }
391
284
 
@@ -114,6 +114,8 @@ export interface ComponentProps {
114
114
  components?: Record<string, any>;
115
115
  /** Component styles */
116
116
  styles?: ComponentStyles;
117
+ /** Standard state change handler for controlled components */
118
+ onStateChanged?: (stateUpdate: Record<string, any>) => void;
117
119
  }
118
120
 
119
121
  /**
@@ -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
+ }
@@ -9,6 +9,7 @@ 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';
12
13
 
13
14
  /**
14
15
  * Represents a loaded script or CSS resource
@@ -52,12 +53,30 @@ export class LibraryLoader {
52
53
  /**
53
54
  * Load all standard libraries (core + UI + CSS)
54
55
  * This is the main method that should be used by test harness and Angular wrapper
56
+ * @param config Optional full library configuration to replace the default
57
+ * @param additionalLibraries Optional additional libraries to merge with the configuration
55
58
  */
56
- static async loadAllLibraries(config?: LibraryConfiguration): Promise<LibraryLoadResult> {
59
+ static async loadAllLibraries(
60
+ config?: LibraryConfiguration,
61
+ additionalLibraries?: ExternalLibraryConfig[]
62
+ ): Promise<LibraryLoadResult> {
57
63
  if (config) {
58
64
  StandardLibraryManager.setConfiguration(config);
59
65
  }
60
66
 
67
+ // If additional libraries are provided, merge them with the current configuration
68
+ if (additionalLibraries && additionalLibraries.length > 0) {
69
+ const currentConfig = StandardLibraryManager.getConfiguration();
70
+ const mergedConfig: LibraryConfiguration = {
71
+ libraries: [...currentConfig.libraries, ...additionalLibraries],
72
+ metadata: {
73
+ ...currentConfig.metadata,
74
+ lastUpdated: new Date().toISOString()
75
+ }
76
+ };
77
+ StandardLibraryManager.setConfiguration(mergedConfig);
78
+ }
79
+
61
80
  return this.loadLibrariesFromConfig();
62
81
  }
63
82
 
@@ -65,55 +84,55 @@ export class LibraryLoader {
65
84
  * Load libraries based on the current configuration
66
85
  */
67
86
  static async loadLibrariesFromConfig(options?: ConfigLoadOptions): Promise<LibraryLoadResult> {
87
+ // Always load core runtime libraries first
88
+ const coreLibraries = getCoreRuntimeLibraries();
89
+ const corePromises = coreLibraries.map(lib =>
90
+ this.loadScript(lib.cdnUrl, lib.globalVariable)
91
+ );
92
+
93
+ const coreResults = await Promise.all(corePromises);
94
+ const React = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'React');
95
+ const ReactDOM = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'ReactDOM');
96
+ const Babel = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'Babel');
97
+
98
+ // Now load plugin libraries from configuration
68
99
  const config = StandardLibraryManager.getConfiguration();
69
100
  const enabledLibraries = StandardLibraryManager.getEnabledLibraries();
70
101
 
102
+ // Filter out any core runtime libraries from plugin configuration
103
+ let pluginLibraries = enabledLibraries.filter(lib => !isCoreRuntimeLibrary(lib.id));
104
+
71
105
  // Apply options filters if provided
72
- let librariesToLoad = enabledLibraries;
73
106
  if (options) {
74
107
  if (options.categories) {
75
- librariesToLoad = librariesToLoad.filter(lib =>
108
+ pluginLibraries = pluginLibraries.filter(lib =>
76
109
  options.categories!.includes(lib.category)
77
110
  );
78
111
  }
79
112
  if (options.excludeRuntimeOnly) {
80
- librariesToLoad = librariesToLoad.filter(lib => !lib.isRuntimeOnly);
113
+ pluginLibraries = pluginLibraries.filter(lib => !lib.isRuntimeOnly);
81
114
  }
82
115
  }
83
116
 
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 => {
117
+ // Load CSS files for plugin libraries (non-blocking)
118
+ pluginLibraries.forEach(lib => {
100
119
  if (lib.cdnCssUrl) {
101
120
  this.loadCSS(lib.cdnCssUrl);
102
121
  }
103
122
  });
104
123
 
105
- // Load component libraries
106
- const componentPromises = componentLibs.map(lib =>
124
+ // Load plugin libraries
125
+ const pluginPromises = pluginLibraries.map(lib =>
107
126
  this.loadScript(lib.cdnUrl, lib.globalVariable)
108
127
  );
109
128
 
110
- const componentResults = await Promise.all(componentPromises);
129
+ const pluginResults = await Promise.all(pluginPromises);
111
130
 
112
- // Build libraries object
131
+ // Build libraries object (only contains plugin libraries)
113
132
  const libraries: StandardLibraries = {};
114
133
 
115
- componentLibs.forEach((lib, index) => {
116
- libraries[lib.globalVariable] = componentResults[index];
134
+ pluginLibraries.forEach((lib, index) => {
135
+ libraries[lib.globalVariable] = pluginResults[index];
117
136
  });
118
137
 
119
138
  return {