@memberjunction/react-runtime 2.98.0 → 2.100.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 (71) hide show
  1. package/.turbo/turbo-build.log +15 -20
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +171 -1
  4. package/dist/compiler/component-compiler.d.ts.map +1 -1
  5. package/dist/compiler/component-compiler.js +59 -8
  6. package/dist/compiler/component-compiler.js.map +1 -1
  7. package/dist/component-manager/component-manager.d.ts +39 -0
  8. package/dist/component-manager/component-manager.d.ts.map +1 -0
  9. package/dist/component-manager/component-manager.js +474 -0
  10. package/dist/component-manager/component-manager.js.map +1 -0
  11. package/dist/component-manager/index.d.ts +3 -0
  12. package/dist/component-manager/index.d.ts.map +1 -0
  13. package/dist/component-manager/index.js +6 -0
  14. package/dist/component-manager/index.js.map +1 -0
  15. package/dist/component-manager/types.d.ts +62 -0
  16. package/dist/component-manager/types.d.ts.map +1 -0
  17. package/dist/component-manager/types.js +3 -0
  18. package/dist/component-manager/types.js.map +1 -0
  19. package/dist/index.d.ts +7 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +18 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/registry/component-registry-service.d.ts +16 -1
  24. package/dist/registry/component-registry-service.d.ts.map +1 -1
  25. package/dist/registry/component-registry-service.js +212 -10
  26. package/dist/registry/component-registry-service.js.map +1 -1
  27. package/dist/registry/component-registry.d.ts +1 -1
  28. package/dist/registry/component-registry.d.ts.map +1 -1
  29. package/dist/registry/component-registry.js.map +1 -1
  30. package/dist/registry/component-resolver.d.ts.map +1 -1
  31. package/dist/registry/component-resolver.js +122 -52
  32. package/dist/registry/component-resolver.js.map +1 -1
  33. package/dist/registry/index.d.ts +1 -1
  34. package/dist/registry/index.d.ts.map +1 -1
  35. package/dist/registry/index.js.map +1 -1
  36. package/dist/runtime/component-hierarchy.d.ts +4 -0
  37. package/dist/runtime/component-hierarchy.d.ts.map +1 -1
  38. package/dist/runtime/component-hierarchy.js +127 -12
  39. package/dist/runtime/component-hierarchy.js.map +1 -1
  40. package/dist/runtime.umd.js +535 -1
  41. package/dist/types/index.d.ts +1 -0
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/dist/types/index.js.map +1 -1
  44. package/dist/utilities/component-unwrapper.d.ts +7 -0
  45. package/dist/utilities/component-unwrapper.d.ts.map +1 -0
  46. package/dist/utilities/component-unwrapper.js +369 -0
  47. package/dist/utilities/component-unwrapper.js.map +1 -0
  48. package/dist/utilities/index.d.ts +1 -0
  49. package/dist/utilities/index.d.ts.map +1 -1
  50. package/dist/utilities/index.js +1 -0
  51. package/dist/utilities/index.js.map +1 -1
  52. package/dist/utilities/library-loader.d.ts +3 -0
  53. package/dist/utilities/library-loader.d.ts.map +1 -1
  54. package/dist/utilities/library-loader.js +101 -17
  55. package/dist/utilities/library-loader.js.map +1 -1
  56. package/examples/component-registry-integration.ts +191 -0
  57. package/package.json +6 -5
  58. package/src/compiler/component-compiler.ts +101 -23
  59. package/src/component-manager/component-manager.ts +736 -0
  60. package/src/component-manager/index.ts +13 -0
  61. package/src/component-manager/types.ts +204 -0
  62. package/src/index.ts +37 -1
  63. package/src/registry/component-registry-service.ts +315 -18
  64. package/src/registry/component-registry.ts +1 -1
  65. package/src/registry/component-resolver.ts +159 -67
  66. package/src/registry/index.ts +1 -1
  67. package/src/runtime/component-hierarchy.ts +124 -13
  68. package/src/types/index.ts +2 -0
  69. package/src/utilities/component-unwrapper.ts +481 -0
  70. package/src/utilities/index.ts +2 -1
  71. package/src/utilities/library-loader.ts +155 -22
@@ -0,0 +1,736 @@
1
+ /**
2
+ * @fileoverview Unified Component Manager implementation
3
+ * Handles all component operations efficiently with proper caching and registry tracking
4
+ */
5
+
6
+ import { ComponentSpec, ComponentLibraryDependency } from '@memberjunction/interactive-component-types';
7
+ import { UserInfo, Metadata, LogError } from '@memberjunction/core';
8
+ import { ComponentMetadataEngine, ComponentLibraryEntity } from '@memberjunction/core-entities';
9
+
10
+ import { ComponentCompiler } from '../compiler';
11
+ import { ComponentRegistry } from '../registry';
12
+ import { RuntimeContext, ComponentObject } from '../types';
13
+ import {
14
+ LoadOptions,
15
+ LoadResult,
16
+ HierarchyResult,
17
+ ComponentManagerConfig,
18
+ CacheEntry,
19
+ ResolutionMode
20
+ } from './types';
21
+
22
+ /**
23
+ * Unified component management system that handles all component operations
24
+ * efficiently with proper caching and registry tracking.
25
+ */
26
+ export class ComponentManager {
27
+ private compiler: ComponentCompiler;
28
+ private registry: ComponentRegistry;
29
+ private runtimeContext: RuntimeContext;
30
+ private config: ComponentManagerConfig;
31
+
32
+ // Caching
33
+ private fetchCache: Map<string, CacheEntry> = new Map();
34
+ private registryNotifications: Set<string> = new Set();
35
+ private loadingPromises: Map<string, Promise<LoadResult>> = new Map();
36
+
37
+ // Metadata engine
38
+ private componentEngine = ComponentMetadataEngine.Instance;
39
+ private graphQLClient: any = null;
40
+
41
+ constructor(
42
+ compiler: ComponentCompiler,
43
+ registry: ComponentRegistry,
44
+ runtimeContext: RuntimeContext,
45
+ config: ComponentManagerConfig = {}
46
+ ) {
47
+ this.compiler = compiler;
48
+ this.registry = registry;
49
+ this.runtimeContext = runtimeContext;
50
+ this.config = {
51
+ debug: false,
52
+ maxCacheSize: 100,
53
+ cacheTTL: 3600000, // 1 hour
54
+ enableUsageTracking: true,
55
+ dependencyBatchSize: 5,
56
+ fetchTimeout: 30000,
57
+ ...config
58
+ };
59
+
60
+ this.log('ComponentManager initialized', {
61
+ debug: this.config.debug,
62
+ cacheTTL: this.config.cacheTTL,
63
+ usageTracking: this.config.enableUsageTracking
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Main entry point - intelligently handles all component operations
69
+ */
70
+ async loadComponent(
71
+ spec: ComponentSpec,
72
+ options: LoadOptions = {}
73
+ ): Promise<LoadResult> {
74
+ const startTime = Date.now();
75
+ const componentKey = this.getComponentKey(spec, options);
76
+
77
+ this.log(`Loading component: ${spec.name}`, {
78
+ key: componentKey,
79
+ location: spec.location,
80
+ registry: spec.registry,
81
+ forceRefresh: options.forceRefresh
82
+ });
83
+
84
+ // Check if already loading to prevent duplicate work
85
+ const existingPromise = this.loadingPromises.get(componentKey);
86
+ if (existingPromise && !options.forceRefresh) {
87
+ this.log(`Component already loading: ${spec.name}, waiting...`);
88
+ return existingPromise;
89
+ }
90
+
91
+ // Create loading promise
92
+ const loadPromise = this.doLoadComponent(spec, options, componentKey, startTime);
93
+ this.loadingPromises.set(componentKey, loadPromise);
94
+
95
+ try {
96
+ const result = await loadPromise;
97
+ return result;
98
+ } finally {
99
+ this.loadingPromises.delete(componentKey);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Internal method that does the actual loading
105
+ */
106
+ private async doLoadComponent(
107
+ spec: ComponentSpec,
108
+ options: LoadOptions,
109
+ componentKey: string,
110
+ startTime: number
111
+ ): Promise<LoadResult> {
112
+ const errors: LoadResult['errors'] = [];
113
+
114
+ try {
115
+ // STEP 1: Check if already loaded in ComponentRegistry
116
+ const namespace = spec.namespace || options.defaultNamespace || 'Global';
117
+ const version = spec.version || options.defaultVersion || 'latest';
118
+
119
+ const existing = this.registry.get(spec.name, namespace, version);
120
+ if (existing && !options.forceRefresh && !options.forceRecompile) {
121
+ this.log(`Component found in registry: ${spec.name}`);
122
+
123
+ // Still need to notify registry for usage tracking
124
+ if (options.trackUsage !== false) {
125
+ await this.notifyRegistryUsageIfNeeded(spec, componentKey);
126
+ }
127
+
128
+ // Get cached spec if available
129
+ const cachedEntry = this.fetchCache.get(componentKey);
130
+
131
+ return {
132
+ success: true,
133
+ component: existing,
134
+ spec: cachedEntry?.spec || spec,
135
+ fromCache: true
136
+ };
137
+ }
138
+
139
+ // STEP 2: Fetch full spec if needed
140
+ let fullSpec = spec;
141
+ if (this.needsFetch(spec)) {
142
+ this.log(`Fetching component spec: ${spec.name}`);
143
+ try {
144
+ fullSpec = await this.fetchComponentSpec(spec, options.contextUser, {
145
+ resolutionMode: options.resolutionMode
146
+ });
147
+
148
+ // Cache the fetched spec
149
+ this.fetchCache.set(componentKey, {
150
+ spec: fullSpec,
151
+ fetchedAt: new Date(),
152
+ hash: await this.calculateHash(fullSpec),
153
+ usageNotified: false
154
+ });
155
+ } catch (error) {
156
+ errors.push({
157
+ message: `Failed to fetch component: ${error instanceof Error ? error.message : String(error)}`,
158
+ phase: 'fetch',
159
+ componentName: spec.name
160
+ });
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ // STEP 3: Notify registry of usage (exactly once per session)
166
+ if (options.trackUsage !== false) {
167
+ await this.notifyRegistryUsageIfNeeded(fullSpec, componentKey);
168
+ }
169
+
170
+ // STEP 4: Compile if needed
171
+ let compiledComponent = existing;
172
+ if (!compiledComponent || options.forceRecompile) {
173
+ this.log(`Compiling component: ${spec.name}`);
174
+ try {
175
+ compiledComponent = await this.compileComponent(fullSpec, options);
176
+ } catch (error) {
177
+ errors.push({
178
+ message: `Failed to compile component: ${error instanceof Error ? error.message : String(error)}`,
179
+ phase: 'compile',
180
+ componentName: spec.name
181
+ });
182
+ throw error;
183
+ }
184
+ }
185
+
186
+ // STEP 5: Register in ComponentRegistry
187
+ if (!existing || options.forceRefresh || options.forceRecompile) {
188
+ this.log(`Registering component: ${spec.name}`);
189
+ this.registry.register(
190
+ fullSpec.name,
191
+ compiledComponent,
192
+ namespace,
193
+ version
194
+ );
195
+ }
196
+
197
+ // STEP 6: Process dependencies recursively
198
+ const dependencies: Record<string, ComponentObject> = {};
199
+ if (fullSpec.dependencies && fullSpec.dependencies.length > 0) {
200
+ this.log(`Loading ${fullSpec.dependencies.length} dependencies for ${spec.name}`);
201
+
202
+ // Load dependencies in batches for efficiency
203
+ const depResults = await this.loadDependenciesBatched(
204
+ fullSpec.dependencies,
205
+ { ...options, isDependent: true }
206
+ );
207
+
208
+ for (const result of depResults) {
209
+ if (result.success && result.component) {
210
+ const depSpec = fullSpec.dependencies.find(d =>
211
+ d.name === (result.spec?.name || '')
212
+ );
213
+ if (depSpec) {
214
+ dependencies[depSpec.name] = result.component;
215
+ }
216
+ } else if (result.errors) {
217
+ errors.push(...result.errors);
218
+ }
219
+ }
220
+ }
221
+
222
+ const elapsed = Date.now() - startTime;
223
+ this.log(`Component loaded successfully: ${spec.name} (${elapsed}ms)`, {
224
+ fromCache: false,
225
+ dependencyCount: Object.keys(dependencies).length
226
+ });
227
+
228
+ return {
229
+ success: errors.length === 0,
230
+ component: compiledComponent,
231
+ spec: fullSpec,
232
+ fromCache: false,
233
+ dependencies,
234
+ errors: errors.length > 0 ? errors : undefined
235
+ };
236
+
237
+ } catch (error) {
238
+ const elapsed = Date.now() - startTime;
239
+ this.log(`Failed to load component: ${spec.name} (${elapsed}ms)`, error);
240
+
241
+ return {
242
+ success: false,
243
+ fromCache: false,
244
+ errors: errors.length > 0 ? errors : [{
245
+ message: error instanceof Error ? error.message : String(error),
246
+ phase: 'fetch',
247
+ componentName: spec.name
248
+ }]
249
+ };
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Load a complete hierarchy efficiently
255
+ */
256
+ async loadHierarchy(
257
+ rootSpec: ComponentSpec,
258
+ options: LoadOptions = {}
259
+ ): Promise<HierarchyResult> {
260
+ const startTime = Date.now();
261
+ const loaded: string[] = [];
262
+ const errors: HierarchyResult['errors'] = [];
263
+ const components: Record<string, ComponentObject> = {};
264
+ const stats = {
265
+ fromCache: 0,
266
+ fetched: 0,
267
+ compiled: 0,
268
+ totalTime: 0
269
+ };
270
+
271
+ this.log(`Loading component hierarchy: ${rootSpec.name}`, {
272
+ location: rootSpec.location,
273
+ registry: rootSpec.registry
274
+ });
275
+
276
+ try {
277
+ // Initialize component engine if needed (skip in browser context where it doesn't exist)
278
+ if (this.componentEngine && typeof this.componentEngine.Config === 'function') {
279
+ await this.componentEngine.Config(false, options.contextUser);
280
+ }
281
+
282
+ // Load the root component and all its dependencies
283
+ const result = await this.loadComponentRecursive(
284
+ rootSpec,
285
+ options,
286
+ loaded,
287
+ errors,
288
+ components,
289
+ stats,
290
+ new Set()
291
+ );
292
+
293
+ stats.totalTime = Date.now() - startTime;
294
+
295
+ this.log(`Hierarchy loaded: ${rootSpec.name}`, {
296
+ success: errors.length === 0,
297
+ loadedCount: loaded.length,
298
+ errors: errors.length,
299
+ ...stats
300
+ });
301
+
302
+ // Unwrap components before returning
303
+ // Components are ComponentObject wrappers, but consumers expect just the React components
304
+ const unwrappedComponents: Record<string, ComponentObject> = {};
305
+ for (const [name, componentObject] of Object.entries(components)) {
306
+ if (componentObject && typeof componentObject === 'object' && 'component' in componentObject) {
307
+ // Extract the actual React component function
308
+ unwrappedComponents[name] = (componentObject as any).component;
309
+ } else {
310
+ // Already a function or something else - use as-is
311
+ unwrappedComponents[name] = componentObject;
312
+ }
313
+ }
314
+
315
+ return {
316
+ success: errors.length === 0,
317
+ rootComponent: result.component,
318
+ resolvedSpec: result.spec,
319
+ loadedComponents: loaded,
320
+ errors,
321
+ components: unwrappedComponents,
322
+ stats
323
+ };
324
+
325
+ } catch (error) {
326
+ stats.totalTime = Date.now() - startTime;
327
+
328
+ this.log(`Failed to load hierarchy: ${rootSpec.name}`, error);
329
+
330
+ return {
331
+ success: false,
332
+ loadedComponents: loaded,
333
+ errors: [...errors, {
334
+ message: error instanceof Error ? error.message : String(error),
335
+ phase: 'fetch',
336
+ componentName: rootSpec.name
337
+ }],
338
+ stats
339
+ };
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Recursively load a component and its dependencies
345
+ */
346
+ private async loadComponentRecursive(
347
+ spec: ComponentSpec,
348
+ options: LoadOptions,
349
+ loaded: string[],
350
+ errors: HierarchyResult['errors'],
351
+ components: Record<string, ComponentObject>,
352
+ stats: HierarchyResult['stats'],
353
+ visited: Set<string>
354
+ ): Promise<LoadResult> {
355
+ const componentKey = this.getComponentKey(spec, options);
356
+
357
+ // Prevent circular dependencies
358
+ if (visited.has(componentKey)) {
359
+ this.log(`Circular dependency detected: ${spec.name}`);
360
+ return {
361
+ success: true,
362
+ component: components[spec.name],
363
+ spec,
364
+ fromCache: true
365
+ };
366
+ }
367
+ visited.add(componentKey);
368
+
369
+ // Load this component
370
+ const result = await this.loadComponent(spec, options);
371
+
372
+ if (result.success && result.component) {
373
+ loaded.push(spec.name);
374
+ components[spec.name] = result.component;
375
+
376
+ // Update stats
377
+ if (stats) {
378
+ if (result.fromCache) stats.fromCache++;
379
+ else {
380
+ stats.fetched++;
381
+ stats.compiled++;
382
+ }
383
+ }
384
+
385
+ // Load dependencies
386
+ if (result.spec?.dependencies) {
387
+ for (const dep of result.spec.dependencies) {
388
+ await this.loadComponentRecursive(
389
+ dep,
390
+ { ...options, isDependent: true },
391
+ loaded,
392
+ errors,
393
+ components,
394
+ stats,
395
+ visited
396
+ );
397
+ }
398
+ }
399
+ } else if (result.errors) {
400
+ errors.push(...result.errors);
401
+ }
402
+
403
+ return result;
404
+ }
405
+
406
+ /**
407
+ * Load dependencies in batches for efficiency
408
+ */
409
+ private async loadDependenciesBatched(
410
+ dependencies: ComponentSpec[],
411
+ options: LoadOptions
412
+ ): Promise<LoadResult[]> {
413
+ const batchSize = this.config.dependencyBatchSize || 5;
414
+ const results: LoadResult[] = [];
415
+
416
+ for (let i = 0; i < dependencies.length; i += batchSize) {
417
+ const batch = dependencies.slice(i, i + batchSize);
418
+ const batchPromises = batch.map(dep => this.loadComponent(dep, options));
419
+ const batchResults = await Promise.all(batchPromises);
420
+ results.push(...batchResults);
421
+ }
422
+
423
+ return results;
424
+ }
425
+
426
+ /**
427
+ * Check if a component needs to be fetched from a registry
428
+ */
429
+ private needsFetch(spec: ComponentSpec): boolean {
430
+ // Need to fetch if:
431
+ // 1. It's a registry component without code
432
+ // 2. It's missing required fields
433
+ return spec.location === 'registry' && !spec.code;
434
+ }
435
+
436
+ /**
437
+ * Fetch a component specification from a registry (local or external)
438
+ */
439
+ private async fetchComponentSpec(
440
+ spec: ComponentSpec,
441
+ contextUser?: UserInfo,
442
+ options?: { resolutionMode?: ResolutionMode }
443
+ ): Promise<ComponentSpec> {
444
+ // Check cache first
445
+ const cacheKey = this.getComponentKey(spec, {});
446
+ const cached = this.fetchCache.get(cacheKey);
447
+
448
+ if (cached && this.isCacheValid(cached)) {
449
+ this.log(`Using cached spec for: ${spec.name}`);
450
+ return cached.spec;
451
+ }
452
+
453
+ // Handle LOCAL registry components (registry is null/undefined)
454
+ if (!spec.registry) {
455
+ this.log(`Fetching from local registry: ${spec.name}`);
456
+
457
+ // Find component in local ComponentMetadataEngine
458
+ const localComponent = this.componentEngine.Components?.find(
459
+ (c: any) => c.Name === spec.name &&
460
+ (!spec.namespace || c.Namespace === spec.namespace)
461
+ );
462
+
463
+ if (!localComponent) {
464
+ throw new Error(`Local component not found: ${spec.name}`);
465
+ }
466
+
467
+ // Parse specification from local component
468
+ if (!localComponent.Specification) {
469
+ throw new Error(`Local component ${spec.name} has no specification`);
470
+ }
471
+
472
+ const fullSpec = JSON.parse(localComponent.Specification);
473
+
474
+ // Cache it
475
+ this.fetchCache.set(cacheKey, {
476
+ spec: fullSpec,
477
+ fetchedAt: new Date(),
478
+ usageNotified: false
479
+ });
480
+
481
+ return fullSpec;
482
+ }
483
+
484
+ // Handle EXTERNAL registry components (registry has a name)
485
+ // Initialize GraphQL client if needed
486
+ if (!this.graphQLClient) {
487
+ await this.initializeGraphQLClient();
488
+ }
489
+
490
+ if (!this.graphQLClient) {
491
+ throw new Error('GraphQL client not available for registry fetching');
492
+ }
493
+
494
+ // Fetch from external registry
495
+ this.log(`Fetching from external registry: ${spec.registry}/${spec.name}`);
496
+
497
+ const fullSpec = await this.graphQLClient.GetRegistryComponent({
498
+ registryName: spec.registry,
499
+ namespace: spec.namespace || 'Global',
500
+ name: spec.name,
501
+ version: spec.version || 'latest'
502
+ });
503
+
504
+ if (!fullSpec) {
505
+ throw new Error(`Component not found in registry: ${spec.registry}/${spec.name}`);
506
+ }
507
+
508
+ // Apply resolution mode if specified
509
+ const processedSpec = this.applyResolutionMode(fullSpec, spec, options?.resolutionMode);
510
+
511
+ // Cache it
512
+ this.fetchCache.set(cacheKey, {
513
+ spec: processedSpec,
514
+ fetchedAt: new Date(),
515
+ usageNotified: false
516
+ });
517
+
518
+ return processedSpec;
519
+ }
520
+
521
+ /**
522
+ * Apply resolution mode to a fetched spec (recursively including dependencies)
523
+ */
524
+ private applyResolutionMode(
525
+ fullSpec: ComponentSpec,
526
+ originalSpec: ComponentSpec,
527
+ resolutionMode?: ResolutionMode
528
+ ): ComponentSpec {
529
+ let processedSpec: ComponentSpec;
530
+
531
+ if (resolutionMode === 'embed') {
532
+ // Convert to embedded format for test harness
533
+ processedSpec = {
534
+ ...fullSpec,
535
+ location: 'embedded',
536
+ registry: undefined,
537
+ // namespace and name can stay for identification
538
+ };
539
+ } else {
540
+ // Default: preserve-metadata mode
541
+ // Keep original registry metadata but include fetched code
542
+ processedSpec = {
543
+ ...fullSpec,
544
+ location: originalSpec.location,
545
+ registry: originalSpec.registry,
546
+ namespace: originalSpec.namespace || fullSpec.namespace,
547
+ name: originalSpec.name || fullSpec.name
548
+ };
549
+ }
550
+
551
+ // Recursively apply resolution mode to dependencies
552
+ if (processedSpec.dependencies && processedSpec.dependencies.length > 0) {
553
+ processedSpec.dependencies = processedSpec.dependencies.map(dep => {
554
+ // For dependencies, use the dep itself as both full and original spec
555
+ // since they've already been fetched and processed
556
+ return this.applyResolutionMode(dep, dep, resolutionMode);
557
+ });
558
+ }
559
+
560
+ return processedSpec;
561
+ }
562
+
563
+ /**
564
+ * Compile a component specification
565
+ */
566
+ private async compileComponent(
567
+ spec: ComponentSpec,
568
+ options: LoadOptions
569
+ ): Promise<ComponentObject> {
570
+ // Get all available libraries - use passed libraries or fall back to ComponentMetadataEngine
571
+ const allLibraries = options.allLibraries || this.componentEngine.ComponentLibraries || [];
572
+
573
+ // Filter valid libraries
574
+ const validLibraries = spec.libraries?.filter(lib =>
575
+ lib && lib.name && lib.globalVariable &&
576
+ lib.name !== 'unknown' && lib.globalVariable !== 'undefined'
577
+ );
578
+
579
+ // Compile the component
580
+ const result = await this.compiler.compile({
581
+ componentName: spec.name,
582
+ componentCode: spec.code || '',
583
+ libraries: validLibraries,
584
+ dependencies: spec.dependencies,
585
+ allLibraries
586
+ });
587
+
588
+ if (!result.success || !result.component) {
589
+ throw new Error(result.error?.message || 'Compilation failed');
590
+ }
591
+
592
+ // Add loaded libraries to runtime context
593
+ if (result.loadedLibraries && result.loadedLibraries.size > 0) {
594
+ if (!this.runtimeContext.libraries) {
595
+ this.runtimeContext.libraries = {};
596
+ }
597
+ result.loadedLibraries.forEach((value, key) => {
598
+ this.runtimeContext.libraries![key] = value;
599
+ });
600
+ }
601
+
602
+ // Get the component object from the factory
603
+ const componentObject = result.component.factory(
604
+ this.runtimeContext,
605
+ undefined, // styles
606
+ {} // components - will be injected by parent
607
+ );
608
+
609
+ return componentObject;
610
+ }
611
+
612
+ /**
613
+ * Notify registry of component usage for licensing
614
+ * Only happens once per component per session
615
+ */
616
+ private async notifyRegistryUsageIfNeeded(
617
+ spec: ComponentSpec,
618
+ componentKey: string
619
+ ): Promise<void> {
620
+ if (!spec.registry || !this.config.enableUsageTracking) {
621
+ return; // Only for external registry components with tracking enabled
622
+ }
623
+
624
+ const notificationKey = `${spec.registry}:${componentKey}`;
625
+ if (this.registryNotifications.has(notificationKey)) {
626
+ this.log(`Usage already notified for: ${spec.name}`);
627
+ return; // Already notified this session
628
+ }
629
+
630
+ try {
631
+ // In the future, make lightweight usage notification call to registry
632
+ // For now, the fetch itself serves as the notification
633
+ this.log(`Notifying registry usage for: ${spec.name}`);
634
+ this.registryNotifications.add(notificationKey);
635
+
636
+ // Update cache entry
637
+ const cached = this.fetchCache.get(componentKey);
638
+ if (cached) {
639
+ cached.usageNotified = true;
640
+ }
641
+ } catch (error) {
642
+ // Log but don't fail - usage tracking shouldn't break component loading
643
+ console.warn(`Failed to notify registry usage for ${componentKey}:`, error);
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Initialize GraphQL client for registry operations
649
+ */
650
+ private async initializeGraphQLClient(): Promise<void> {
651
+ try {
652
+ const provider = Metadata?.Provider;
653
+ if (provider && (provider as any).ExecuteGQL) {
654
+ const { GraphQLComponentRegistryClient } = await import('@memberjunction/graphql-dataprovider');
655
+ this.graphQLClient = new GraphQLComponentRegistryClient(provider as any);
656
+ this.log('GraphQL client initialized');
657
+ }
658
+ } catch (error) {
659
+ LogError(`Failed to initialize GraphQL client: ${error instanceof Error ? error.message : String(error)}`);
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Check if a cache entry is still valid
665
+ */
666
+ private isCacheValid(entry: CacheEntry): boolean {
667
+ const age = Date.now() - entry.fetchedAt.getTime();
668
+ return age < (this.config.cacheTTL || 3600000);
669
+ }
670
+
671
+ /**
672
+ * Calculate a hash for a component spec (for cache validation)
673
+ */
674
+ private async calculateHash(spec: ComponentSpec): Promise<string> {
675
+ // Simple hash based on spec content
676
+ const content = JSON.stringify({
677
+ name: spec.name,
678
+ version: spec.version,
679
+ code: spec.code,
680
+ libraries: spec.libraries
681
+ });
682
+
683
+ // Simple hash function (in production, use crypto)
684
+ let hash = 0;
685
+ for (let i = 0; i < content.length; i++) {
686
+ const char = content.charCodeAt(i);
687
+ hash = ((hash << 5) - hash) + char;
688
+ hash = hash & hash; // Convert to 32bit integer
689
+ }
690
+ return hash.toString(16);
691
+ }
692
+
693
+ /**
694
+ * Generate a unique key for a component
695
+ */
696
+ private getComponentKey(spec: ComponentSpec, options: LoadOptions): string {
697
+ const registry = spec.registry || 'local';
698
+ const namespace = spec.namespace || options.defaultNamespace || 'Global';
699
+ const version = spec.version || options.defaultVersion || 'latest';
700
+ return `${registry}:${namespace}:${spec.name}:${version}`;
701
+ }
702
+
703
+ /**
704
+ * Clear all caches
705
+ */
706
+ clearCache(): void {
707
+ this.fetchCache.clear();
708
+ this.registryNotifications.clear();
709
+ this.loadingPromises.clear();
710
+ this.log('All caches cleared');
711
+ }
712
+
713
+ /**
714
+ * Get cache statistics
715
+ */
716
+ getCacheStats(): {
717
+ fetchCacheSize: number;
718
+ notificationsCount: number;
719
+ loadingCount: number;
720
+ } {
721
+ return {
722
+ fetchCacheSize: this.fetchCache.size,
723
+ notificationsCount: this.registryNotifications.size,
724
+ loadingCount: this.loadingPromises.size
725
+ };
726
+ }
727
+
728
+ /**
729
+ * Log a message if debug is enabled
730
+ */
731
+ private log(message: string, data?: any): void {
732
+ if (this.config.debug) {
733
+ console.log(`🎯 [ComponentManager] ${message}`, data || '');
734
+ }
735
+ }
736
+ }