@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +23 -0
- package/dist/compiler/component-compiler.d.ts +0 -1
- package/dist/compiler/component-compiler.d.ts.map +1 -1
- package/dist/compiler/component-compiler.js +34 -25
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- 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 +2 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -2
- package/dist/runtime/prop-builder.d.ts +1 -2
- package/dist/runtime/prop-builder.d.ts.map +1 -1
- package/dist/runtime/prop-builder.js +4 -76
- 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/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- 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/core-libraries.d.ts +5 -0
- package/dist/utilities/core-libraries.d.ts.map +1 -0
- package/dist/utilities/core-libraries.js +52 -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 +2 -2
- package/dist/utilities/library-loader.d.ts.map +1 -1
- package/dist/utilities/library-loader.js +52 -24
- package/dist/utilities/resource-manager.d.ts +34 -0
- package/dist/utilities/resource-manager.d.ts.map +1 -0
- package/dist/utilities/resource-manager.js +174 -0
- package/package.json +4 -4
- package/samples/entities-1.js +493 -0
- package/src/compiler/component-compiler.ts +64 -35
- package/src/index.ts +18 -1
- package/src/registry/component-registry.ts +14 -4
- package/src/runtime/index.ts +7 -2
- package/src/runtime/prop-builder.ts +5 -112
- package/src/runtime/react-root-manager.ts +218 -0
- package/src/types/index.ts +2 -0
- package/src/utilities/cache-manager.ts +253 -0
- package/src/utilities/core-libraries.ts +61 -0
- package/src/utilities/index.ts +3 -1
- package/src/utilities/library-loader.ts +111 -47
- package/src/utilities/resource-manager.ts +305 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -161,7 +161,11 @@ export class ComponentCompiler {
|
|
|
161
161
|
*/
|
|
162
162
|
private wrapComponentCode(componentCode: string, componentName: string): string {
|
|
163
163
|
return `
|
|
164
|
-
function createComponent(
|
|
164
|
+
function createComponent(
|
|
165
|
+
React, ReactDOM,
|
|
166
|
+
useState, useEffect, useCallback, useMemo, useRef, useContext, useReducer, useLayoutEffect,
|
|
167
|
+
libraries, styles, console
|
|
168
|
+
) {
|
|
165
169
|
${componentCode}
|
|
166
170
|
|
|
167
171
|
// Ensure the component exists
|
|
@@ -193,19 +197,17 @@ export class ComponentCompiler {
|
|
|
193
197
|
*/
|
|
194
198
|
private createComponentFactory(transpiledCode: string, componentName: string): Function {
|
|
195
199
|
try {
|
|
196
|
-
// Create the factory function
|
|
200
|
+
// Create the factory function with all React hooks
|
|
197
201
|
const factoryCreator = new Function(
|
|
198
|
-
'React', 'ReactDOM',
|
|
199
|
-
'
|
|
202
|
+
'React', 'ReactDOM',
|
|
203
|
+
'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext', 'useReducer', 'useLayoutEffect',
|
|
204
|
+
'libraries', 'styles', 'console',
|
|
200
205
|
`${transpiledCode}; return createComponent;`
|
|
201
206
|
);
|
|
202
207
|
|
|
203
208
|
// Return a function that executes the factory with runtime context
|
|
204
209
|
return (context: RuntimeContext, styles: any = {}) => {
|
|
205
210
|
const { React, ReactDOM, libraries = {} } = context;
|
|
206
|
-
|
|
207
|
-
// Create state updater utility
|
|
208
|
-
const createStateUpdater = this.createStateUpdaterUtility();
|
|
209
211
|
|
|
210
212
|
// Execute the factory creator to get the createComponent function
|
|
211
213
|
const createComponentFn = factoryCreator(
|
|
@@ -214,7 +216,11 @@ export class ComponentCompiler {
|
|
|
214
216
|
React.useState,
|
|
215
217
|
React.useEffect,
|
|
216
218
|
React.useCallback,
|
|
217
|
-
|
|
219
|
+
React.useMemo,
|
|
220
|
+
React.useRef,
|
|
221
|
+
React.useContext,
|
|
222
|
+
React.useReducer,
|
|
223
|
+
React.useLayoutEffect,
|
|
218
224
|
libraries,
|
|
219
225
|
styles,
|
|
220
226
|
console
|
|
@@ -227,7 +233,11 @@ export class ComponentCompiler {
|
|
|
227
233
|
React.useState,
|
|
228
234
|
React.useEffect,
|
|
229
235
|
React.useCallback,
|
|
230
|
-
|
|
236
|
+
React.useMemo,
|
|
237
|
+
React.useRef,
|
|
238
|
+
React.useContext,
|
|
239
|
+
React.useReducer,
|
|
240
|
+
React.useLayoutEffect,
|
|
231
241
|
libraries,
|
|
232
242
|
styles,
|
|
233
243
|
console
|
|
@@ -238,28 +248,6 @@ export class ComponentCompiler {
|
|
|
238
248
|
}
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
/**
|
|
242
|
-
* Creates the state updater utility function for nested components
|
|
243
|
-
* @returns State updater function
|
|
244
|
-
*/
|
|
245
|
-
private createStateUpdaterUtility(): Function {
|
|
246
|
-
return (statePath: string, parentStateUpdater: Function) => {
|
|
247
|
-
return (componentStateUpdate: any) => {
|
|
248
|
-
if (!statePath) {
|
|
249
|
-
// Root component - pass through directly
|
|
250
|
-
parentStateUpdater(componentStateUpdate);
|
|
251
|
-
} else {
|
|
252
|
-
// Sub-component - bubble up with path context
|
|
253
|
-
const pathParts = statePath.split('.');
|
|
254
|
-
const componentKey = pathParts[pathParts.length - 1];
|
|
255
|
-
|
|
256
|
-
parentStateUpdater({
|
|
257
|
-
[componentKey]: componentStateUpdate
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
251
|
|
|
264
252
|
/**
|
|
265
253
|
* Validates compilation options
|
|
@@ -267,21 +255,62 @@ export class ComponentCompiler {
|
|
|
267
255
|
* @throws Error if validation fails
|
|
268
256
|
*/
|
|
269
257
|
private validateCompileOptions(options: CompileOptions): void {
|
|
258
|
+
// Check if options object exists
|
|
259
|
+
if (!options) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
'Component compilation failed: No options provided.\n' +
|
|
262
|
+
'Expected an object with componentName and componentCode properties.\n' +
|
|
263
|
+
'Example: { componentName: "MyComponent", componentCode: "function MyComponent() { ... }" }'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check component name
|
|
270
268
|
if (!options.componentName) {
|
|
271
|
-
|
|
269
|
+
const providedKeys = Object.keys(options).join(', ');
|
|
270
|
+
throw new Error(
|
|
271
|
+
'Component compilation failed: Component name is required.\n' +
|
|
272
|
+
`Received options with keys: [${providedKeys}]\n` +
|
|
273
|
+
'Please ensure your component spec includes a "name" property.\n' +
|
|
274
|
+
'Example: { name: "MyComponent", code: "..." }'
|
|
275
|
+
);
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
// Check component code
|
|
274
279
|
if (!options.componentCode) {
|
|
275
|
-
throw new Error(
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Component compilation failed: Component code is required for "${options.componentName}".\n` +
|
|
282
|
+
'Please ensure your component spec includes a "code" property with the component source code.\n' +
|
|
283
|
+
'Example: { name: "MyComponent", code: "function MyComponent() { return <div>Hello</div>; }" }'
|
|
284
|
+
);
|
|
276
285
|
}
|
|
277
286
|
|
|
287
|
+
// Check code type
|
|
278
288
|
if (typeof options.componentCode !== 'string') {
|
|
279
|
-
|
|
289
|
+
const actualType = typeof options.componentCode;
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Component compilation failed: Component code must be a string for "${options.componentName}".\n` +
|
|
292
|
+
`Received type: ${actualType}\n` +
|
|
293
|
+
`Received value: ${JSON.stringify(options.componentCode).substring(0, 100)}...\n` +
|
|
294
|
+
'Please ensure the code property contains a string of JavaScript/JSX code.'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if code is empty or whitespace only
|
|
299
|
+
if (options.componentCode.trim().length === 0) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Component compilation failed: Component code is empty for "${options.componentName}".\n` +
|
|
302
|
+
'The code property must contain valid JavaScript/JSX code defining a React component.'
|
|
303
|
+
);
|
|
280
304
|
}
|
|
281
305
|
|
|
282
306
|
// Basic syntax check
|
|
283
307
|
if (!options.componentCode.includes(options.componentName)) {
|
|
284
|
-
throw new Error(
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Component compilation failed: Component code must define a component named "${options.componentName}".\n` +
|
|
310
|
+
'The function/component name in the code must match the componentName property.\n' +
|
|
311
|
+
`Expected to find: function ${options.componentName}(...) or const ${options.componentName} = ...\n` +
|
|
312
|
+
'Code preview: ' + options.componentCode.substring(0, 200) + '...'
|
|
313
|
+
);
|
|
285
314
|
}
|
|
286
315
|
}
|
|
287
316
|
|
package/src/index.ts
CHANGED
|
@@ -53,7 +53,6 @@ export {
|
|
|
53
53
|
|
|
54
54
|
export {
|
|
55
55
|
buildComponentProps,
|
|
56
|
-
cleanupPropBuilder,
|
|
57
56
|
normalizeCallbacks,
|
|
58
57
|
normalizeStyles,
|
|
59
58
|
validateComponentProps,
|
|
@@ -75,6 +74,12 @@ export {
|
|
|
75
74
|
HierarchyRegistrationOptions
|
|
76
75
|
} from './runtime';
|
|
77
76
|
|
|
77
|
+
export {
|
|
78
|
+
ReactRootManager,
|
|
79
|
+
reactRootManager,
|
|
80
|
+
ManagedReactRoot
|
|
81
|
+
} from './runtime';
|
|
82
|
+
|
|
78
83
|
// Export utilities
|
|
79
84
|
export {
|
|
80
85
|
RuntimeUtilities,
|
|
@@ -103,6 +108,18 @@ export {
|
|
|
103
108
|
FailedComponentInfo
|
|
104
109
|
} from './utilities/component-error-analyzer';
|
|
105
110
|
|
|
111
|
+
export {
|
|
112
|
+
ResourceManager,
|
|
113
|
+
resourceManager,
|
|
114
|
+
ManagedResource
|
|
115
|
+
} from './utilities/resource-manager';
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
CacheManager,
|
|
119
|
+
CacheEntry,
|
|
120
|
+
CacheOptions
|
|
121
|
+
} from './utilities/cache-manager';
|
|
122
|
+
|
|
106
123
|
// Version information
|
|
107
124
|
export const VERSION = '2.69.1';
|
|
108
125
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ComponentMetadata,
|
|
10
10
|
RegistryConfig
|
|
11
11
|
} from '../types';
|
|
12
|
+
import { resourceManager } from '../utilities/resource-manager';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Default registry configuration
|
|
@@ -28,6 +29,7 @@ export class ComponentRegistry {
|
|
|
28
29
|
private registry: Map<string, RegistryEntry>;
|
|
29
30
|
private config: RegistryConfig;
|
|
30
31
|
private cleanupTimer?: NodeJS.Timeout | number;
|
|
32
|
+
private registryId: string;
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Creates a new ComponentRegistry instance
|
|
@@ -36,6 +38,7 @@ export class ComponentRegistry {
|
|
|
36
38
|
constructor(config?: Partial<RegistryConfig>) {
|
|
37
39
|
this.config = { ...DEFAULT_REGISTRY_CONFIG, ...config };
|
|
38
40
|
this.registry = new Map();
|
|
41
|
+
this.registryId = `component-registry-${Date.now()}`;
|
|
39
42
|
|
|
40
43
|
// Start cleanup timer if configured
|
|
41
44
|
if (this.config.cleanupInterval > 0) {
|
|
@@ -297,6 +300,8 @@ export class ComponentRegistry {
|
|
|
297
300
|
destroy(): void {
|
|
298
301
|
this.stopCleanupTimer();
|
|
299
302
|
this.clear();
|
|
303
|
+
// Clean up any resources associated with this registry
|
|
304
|
+
resourceManager.cleanupComponent(this.registryId);
|
|
300
305
|
}
|
|
301
306
|
|
|
302
307
|
/**
|
|
@@ -362,9 +367,14 @@ export class ComponentRegistry {
|
|
|
362
367
|
* Starts the automatic cleanup timer
|
|
363
368
|
*/
|
|
364
369
|
private startCleanupTimer(): void {
|
|
365
|
-
this.cleanupTimer = setInterval(
|
|
366
|
-
this.
|
|
367
|
-
|
|
370
|
+
this.cleanupTimer = resourceManager.setInterval(
|
|
371
|
+
this.registryId,
|
|
372
|
+
() => {
|
|
373
|
+
this.cleanup();
|
|
374
|
+
},
|
|
375
|
+
this.config.cleanupInterval,
|
|
376
|
+
{ purpose: 'component-registry-cleanup' }
|
|
377
|
+
);
|
|
368
378
|
}
|
|
369
379
|
|
|
370
380
|
/**
|
|
@@ -372,7 +382,7 @@ export class ComponentRegistry {
|
|
|
372
382
|
*/
|
|
373
383
|
private stopCleanupTimer(): void {
|
|
374
384
|
if (this.cleanupTimer) {
|
|
375
|
-
clearInterval(this.cleanupTimer as
|
|
385
|
+
resourceManager.clearInterval(this.registryId, this.cleanupTimer as number);
|
|
376
386
|
this.cleanupTimer = undefined;
|
|
377
387
|
}
|
|
378
388
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -23,7 +23,6 @@ export {
|
|
|
23
23
|
|
|
24
24
|
export {
|
|
25
25
|
buildComponentProps,
|
|
26
|
-
cleanupPropBuilder,
|
|
27
26
|
normalizeCallbacks,
|
|
28
27
|
normalizeStyles,
|
|
29
28
|
validateComponentProps,
|
|
@@ -43,4 +42,10 @@ export {
|
|
|
43
42
|
HierarchyRegistrationResult,
|
|
44
43
|
ComponentRegistrationError,
|
|
45
44
|
HierarchyRegistrationOptions
|
|
46
|
-
} from './component-hierarchy';
|
|
45
|
+
} from './component-hierarchy';
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
ReactRootManager,
|
|
49
|
+
reactRootManager,
|
|
50
|
+
ManagedReactRoot
|
|
51
|
+
} from './react-root-manager';
|
|
@@ -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
|
|
|
@@ -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();
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
/**
|