@memberjunction/react-runtime 2.71.0 → 2.73.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.
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LibraryLoader = void 0;
4
+ const standard_libraries_1 = require("./standard-libraries");
5
+ class LibraryLoader {
6
+ static async loadAllLibraries() {
7
+ return this.loadLibraries({
8
+ loadCore: true,
9
+ loadUI: true,
10
+ loadCSS: true
11
+ });
12
+ }
13
+ static async loadLibraries(options) {
14
+ const { loadCore = true, loadUI = true, loadCSS = true, customLibraries = [] } = options;
15
+ const [React, ReactDOM, Babel] = await Promise.all([
16
+ this.loadScript(standard_libraries_1.STANDARD_LIBRARY_URLS.REACT, 'React'),
17
+ this.loadScript(standard_libraries_1.STANDARD_LIBRARY_URLS.REACT_DOM, 'ReactDOM'),
18
+ this.loadScript(standard_libraries_1.STANDARD_LIBRARY_URLS.BABEL, 'Babel')
19
+ ]);
20
+ if (loadCSS) {
21
+ (0, standard_libraries_1.getCSSUrls)().forEach(url => this.loadCSS(url));
22
+ }
23
+ const libraryPromises = [];
24
+ const libraryNames = [];
25
+ if (loadCore) {
26
+ const coreUrls = (0, standard_libraries_1.getCoreLibraryUrls)();
27
+ coreUrls.forEach(url => {
28
+ const name = this.getLibraryNameFromUrl(url);
29
+ libraryNames.push(name);
30
+ libraryPromises.push(this.loadScript(url, name));
31
+ });
32
+ }
33
+ if (loadUI) {
34
+ const uiUrls = (0, standard_libraries_1.getUILibraryUrls)();
35
+ uiUrls.forEach(url => {
36
+ const name = this.getLibraryNameFromUrl(url);
37
+ libraryNames.push(name);
38
+ libraryPromises.push(this.loadScript(url, name));
39
+ });
40
+ }
41
+ customLibraries.forEach(({ url, globalName }) => {
42
+ libraryNames.push(globalName);
43
+ libraryPromises.push(this.loadScript(url, globalName));
44
+ });
45
+ const loadedLibraries = await Promise.all(libraryPromises);
46
+ const libraries = {
47
+ _: undefined
48
+ };
49
+ libraryNames.forEach((name, index) => {
50
+ if (name === '_') {
51
+ libraries._ = loadedLibraries[index];
52
+ }
53
+ else {
54
+ libraries[name] = loadedLibraries[index];
55
+ }
56
+ });
57
+ if (!libraries._)
58
+ libraries._ = window._;
59
+ if (!libraries.d3)
60
+ libraries.d3 = window.d3;
61
+ if (!libraries.Chart)
62
+ libraries.Chart = window.Chart;
63
+ if (!libraries.dayjs)
64
+ libraries.dayjs = window.dayjs;
65
+ if (!libraries.antd)
66
+ libraries.antd = window.antd;
67
+ if (!libraries.ReactBootstrap)
68
+ libraries.ReactBootstrap = window.ReactBootstrap;
69
+ return {
70
+ React,
71
+ ReactDOM,
72
+ Babel,
73
+ libraries
74
+ };
75
+ }
76
+ static async loadScript(url, globalName) {
77
+ const existing = this.loadedResources.get(url);
78
+ if (existing) {
79
+ return existing.promise;
80
+ }
81
+ const promise = new Promise((resolve, reject) => {
82
+ const existingGlobal = window[globalName];
83
+ if (existingGlobal) {
84
+ resolve(existingGlobal);
85
+ return;
86
+ }
87
+ const existingScript = document.querySelector(`script[src="${url}"]`);
88
+ if (existingScript) {
89
+ this.waitForScriptLoad(existingScript, globalName, resolve, reject);
90
+ return;
91
+ }
92
+ const script = document.createElement('script');
93
+ script.src = url;
94
+ script.async = true;
95
+ script.crossOrigin = 'anonymous';
96
+ script.onload = () => {
97
+ const global = window[globalName];
98
+ if (global) {
99
+ resolve(global);
100
+ }
101
+ else {
102
+ setTimeout(() => {
103
+ const delayedGlobal = window[globalName];
104
+ if (delayedGlobal) {
105
+ resolve(delayedGlobal);
106
+ }
107
+ else {
108
+ reject(new Error(`${globalName} not found after script load`));
109
+ }
110
+ }, 100);
111
+ }
112
+ };
113
+ script.onerror = () => {
114
+ reject(new Error(`Failed to load script: ${url}`));
115
+ };
116
+ document.head.appendChild(script);
117
+ });
118
+ this.loadedResources.set(url, {
119
+ element: document.querySelector(`script[src="${url}"]`),
120
+ promise
121
+ });
122
+ return promise;
123
+ }
124
+ static loadCSS(url) {
125
+ if (this.loadedResources.has(url)) {
126
+ return;
127
+ }
128
+ const existingLink = document.querySelector(`link[href="${url}"]`);
129
+ if (existingLink) {
130
+ return;
131
+ }
132
+ const link = document.createElement('link');
133
+ link.rel = 'stylesheet';
134
+ link.href = url;
135
+ document.head.appendChild(link);
136
+ this.loadedResources.set(url, {
137
+ element: link,
138
+ promise: Promise.resolve()
139
+ });
140
+ }
141
+ static waitForScriptLoad(script, globalName, resolve, reject) {
142
+ const checkGlobal = () => {
143
+ const global = window[globalName];
144
+ if (global) {
145
+ resolve(global);
146
+ }
147
+ else {
148
+ setTimeout(() => {
149
+ const delayedGlobal = window[globalName];
150
+ if (delayedGlobal) {
151
+ resolve(delayedGlobal);
152
+ }
153
+ else {
154
+ reject(new Error(`${globalName} not found after script load`));
155
+ }
156
+ }, 100);
157
+ }
158
+ };
159
+ if (script.complete || script.readyState === 'complete') {
160
+ checkGlobal();
161
+ return;
162
+ }
163
+ const loadHandler = () => {
164
+ script.removeEventListener('load', loadHandler);
165
+ checkGlobal();
166
+ };
167
+ script.addEventListener('load', loadHandler);
168
+ }
169
+ static getLibraryNameFromUrl(url) {
170
+ if (url.includes('lodash'))
171
+ return '_';
172
+ if (url.includes('d3'))
173
+ return 'd3';
174
+ if (url.includes('Chart.js') || url.includes('chart'))
175
+ return 'Chart';
176
+ if (url.includes('dayjs'))
177
+ return 'dayjs';
178
+ if (url.includes('antd'))
179
+ return 'antd';
180
+ if (url.includes('react-bootstrap'))
181
+ return 'ReactBootstrap';
182
+ const match = url.match(/\/([^\/]+)(?:\.min)?\.js$/);
183
+ return match ? match[1] : 'unknown';
184
+ }
185
+ static getLoadedResources() {
186
+ return this.loadedResources;
187
+ }
188
+ static clearCache() {
189
+ this.loadedResources.clear();
190
+ }
191
+ }
192
+ exports.LibraryLoader = LibraryLoader;
193
+ LibraryLoader.loadedResources = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/react-runtime",
3
- "version": "2.71.0",
3
+ "version": "2.73.0",
4
4
  "description": "Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,10 +25,11 @@
25
25
  },
26
26
  "homepage": "https://github.com/MemberJunction/MJ#readme",
27
27
  "dependencies": {
28
- "@memberjunction/core": "2.71.0",
29
- "@memberjunction/global": "2.71.0",
30
- "@memberjunction/interactive-component-types": "2.71.0",
31
- "@babel/standalone": "^7.23.5"
28
+ "@memberjunction/core": "2.73.0",
29
+ "@memberjunction/global": "2.73.0",
30
+ "@memberjunction/interactive-component-types": "2.73.0",
31
+ "@babel/standalone": "^7.23.5",
32
+ "rxjs": "^7.8.1"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "20.10.0",
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ export {
53
53
 
54
54
  export {
55
55
  buildComponentProps,
56
+ cleanupPropBuilder,
56
57
  normalizeCallbacks,
57
58
  normalizeStyles,
58
59
  validateComponentProps,
@@ -94,6 +95,17 @@ export {
94
95
  createStandardLibraries
95
96
  } from './utilities/standard-libraries';
96
97
 
98
+ export {
99
+ LibraryLoader,
100
+ LibraryLoadOptions,
101
+ LibraryLoadResult
102
+ } from './utilities/library-loader';
103
+
104
+ export {
105
+ ComponentErrorAnalyzer,
106
+ FailedComponentInfo
107
+ } from './utilities/component-error-analyzer';
108
+
97
109
  // Version information
98
110
  export const VERSION = '2.69.1';
99
111
 
@@ -23,6 +23,7 @@ export {
23
23
 
24
24
  export {
25
25
  buildComponentProps,
26
+ cleanupPropBuilder,
26
27
  normalizeCallbacks,
27
28
  normalizeStyles,
28
29
  validateComponentProps,
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { ComponentProps, ComponentCallbacks, ComponentStyles } from '../types';
8
+ import { Subject, debounceTime, Subscription } from 'rxjs';
8
9
 
9
10
  /**
10
11
  * Options for building component props
@@ -18,6 +19,8 @@ export interface PropBuilderOptions {
18
19
  transformData?: (data: any) => any;
19
20
  /** Transform state before passing to component */
20
21
  transformState?: (state: any) => any;
22
+ /** Debounce time for UpdateUserState callback in milliseconds */
23
+ debounceUpdateUserState?: number;
21
24
  }
22
25
 
23
26
  /**
@@ -43,7 +46,8 @@ export function buildComponentProps(
43
46
  const {
44
47
  validate = true,
45
48
  transformData,
46
- transformState
49
+ transformState,
50
+ debounceUpdateUserState = 3000 // Default 3 seconds
47
51
  } = options;
48
52
 
49
53
  // Transform data if transformer provided
@@ -55,7 +59,7 @@ export function buildComponentProps(
55
59
  data: transformedData,
56
60
  userState: transformedState,
57
61
  utilities,
58
- callbacks: normalizeCallbacks(callbacks),
62
+ callbacks: normalizeCallbacks(callbacks, debounceUpdateUserState),
59
63
  components,
60
64
  styles: normalizeStyles(styles)
61
65
  };
@@ -68,12 +72,47 @@ export function buildComponentProps(
68
72
  return props;
69
73
  }
70
74
 
75
+ // Store subjects for debouncing per component instance
76
+ const updateUserStateSubjects = new WeakMap<Function, Subject<any>>();
77
+
78
+ // Store subscriptions for cleanup
79
+ const updateUserStateSubscriptions = new WeakMap<Function, Subscription>();
80
+
81
+ // Loop detection state
82
+ interface LoopDetectionState {
83
+ count: number;
84
+ lastUpdate: number;
85
+ lastState: any;
86
+ }
87
+ const loopDetectionStates = new WeakMap<Function, LoopDetectionState>();
88
+
89
+ // Deep equality check for objects
90
+ function deepEqual(obj1: any, obj2: any): boolean {
91
+ if (obj1 === obj2) return true;
92
+
93
+ if (!obj1 || !obj2) return false;
94
+ if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
95
+
96
+ const keys1 = Object.keys(obj1);
97
+ const keys2 = Object.keys(obj2);
98
+
99
+ if (keys1.length !== keys2.length) return false;
100
+
101
+ for (const key of keys1) {
102
+ if (!keys2.includes(key)) return false;
103
+ if (!deepEqual(obj1[key], obj2[key])) return false;
104
+ }
105
+
106
+ return true;
107
+ }
108
+
71
109
  /**
72
110
  * Normalizes component callbacks
73
111
  * @param callbacks - Raw callbacks object
112
+ * @param debounceMs - Debounce time for UpdateUserState in milliseconds
74
113
  * @returns Normalized callbacks
75
114
  */
76
- export function normalizeCallbacks(callbacks: any): ComponentCallbacks {
115
+ export function normalizeCallbacks(callbacks: any, debounceMs: number = 3000): ComponentCallbacks {
77
116
  const normalized: ComponentCallbacks = {};
78
117
 
79
118
  // Ensure all callbacks are functions
@@ -86,7 +125,67 @@ export function normalizeCallbacks(callbacks: any): ComponentCallbacks {
86
125
  }
87
126
 
88
127
  if (callbacks.UpdateUserState && typeof callbacks.UpdateUserState === 'function') {
89
- normalized.UpdateUserState = callbacks.UpdateUserState;
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
+ };
90
189
  }
91
190
 
92
191
  if (callbacks.NotifyEvent && typeof callbacks.NotifyEvent === 'function') {
@@ -186,6 +285,33 @@ export function mergeProps(...propsList: Partial<ComponentProps>[]): ComponentPr
186
285
  return merged;
187
286
  }
188
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
+
189
315
  /**
190
316
  * Creates a props transformer function
191
317
  * @param transformations - Map of prop paths to transformer functions