@memberjunction/react-runtime 2.91.0 โ†’ 2.93.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 (60) hide show
  1. package/.turbo/turbo-build.log +14 -16
  2. package/CHANGELOG.md +31 -0
  3. package/dist/compiler/babel-config.js.map +1 -1
  4. package/dist/compiler/component-compiler.d.ts.map +1 -1
  5. package/dist/compiler/component-compiler.js +13 -8
  6. package/dist/compiler/component-compiler.js.map +1 -1
  7. package/dist/compiler/index.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/registry/component-registry-service.d.ts +36 -0
  13. package/dist/registry/component-registry-service.d.ts.map +1 -0
  14. package/dist/registry/component-registry-service.js +303 -0
  15. package/dist/registry/component-registry-service.js.map +1 -0
  16. package/dist/registry/component-registry.js.map +1 -1
  17. package/dist/registry/component-resolver.d.ts +11 -2
  18. package/dist/registry/component-resolver.d.ts.map +1 -1
  19. package/dist/registry/component-resolver.js +63 -11
  20. package/dist/registry/component-resolver.js.map +1 -1
  21. package/dist/registry/index.d.ts +2 -0
  22. package/dist/registry/index.d.ts.map +1 -1
  23. package/dist/registry/index.js +3 -1
  24. package/dist/registry/index.js.map +1 -1
  25. package/dist/registry/registry-provider.d.ts +54 -0
  26. package/dist/registry/registry-provider.d.ts.map +1 -0
  27. package/dist/registry/registry-provider.js +3 -0
  28. package/dist/registry/registry-provider.js.map +1 -0
  29. package/dist/runtime/component-hierarchy.d.ts.map +1 -1
  30. package/dist/runtime/component-hierarchy.js +2 -1
  31. package/dist/runtime/component-hierarchy.js.map +1 -1
  32. package/dist/runtime/error-boundary.js.map +1 -1
  33. package/dist/runtime/index.js.map +1 -1
  34. package/dist/runtime/prop-builder.js.map +1 -1
  35. package/dist/runtime/react-root-manager.js.map +1 -1
  36. package/dist/runtime.umd.js +1 -1
  37. package/dist/types/index.d.ts +4 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js.map +1 -1
  40. package/dist/types/library-config.js.map +1 -1
  41. package/dist/utilities/cache-manager.js.map +1 -1
  42. package/dist/utilities/component-error-analyzer.js.map +1 -1
  43. package/dist/utilities/component-styles.js.map +1 -1
  44. package/dist/utilities/core-libraries.js.map +1 -1
  45. package/dist/utilities/index.js.map +1 -1
  46. package/dist/utilities/library-loader.js.map +1 -1
  47. package/dist/utilities/library-registry.js.map +1 -1
  48. package/dist/utilities/resource-manager.js.map +1 -1
  49. package/dist/utilities/standard-libraries.js.map +1 -1
  50. package/package.json +5 -6
  51. package/src/compiler/component-compiler.ts +18 -8
  52. package/src/index.ts +4 -2
  53. package/src/registry/component-registry-service.ts +560 -0
  54. package/src/registry/component-resolver.ts +104 -15
  55. package/src/registry/index.ts +9 -0
  56. package/src/registry/registry-provider.ts +119 -0
  57. package/src/runtime/component-hierarchy.ts +2 -1
  58. package/src/types/index.ts +3 -0
  59. package/tsconfig.json +2 -1
  60. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,560 @@
1
+ /**
2
+ * @fileoverview Service for managing registry-based component loading with caching
3
+ * @module @memberjunction/react-runtime/registry
4
+ */
5
+
6
+ import { ComponentSpec } from '@memberjunction/interactive-component-types';
7
+ import { ComponentCompiler } from '../compiler';
8
+ import { RuntimeContext } from '../types';
9
+ import {
10
+ RegistryProvider,
11
+ RegistryComponentResponse,
12
+ ComponentDependencyInfo,
13
+ DependencyTree,
14
+ RegistryComponentMetadata
15
+ } from './registry-provider';
16
+ import { UserInfo, Metadata } from '@memberjunction/core';
17
+ import {
18
+ ComponentEntity,
19
+ ComponentRegistryEntity,
20
+ ComponentDependencyEntity,
21
+ ComponentLibraryLinkEntity,
22
+ ComponentMetadataEngine
23
+ } from '@memberjunction/core-entities';
24
+
25
+ /**
26
+ * Cached compiled component with metadata
27
+ */
28
+ interface CachedCompiledComponent {
29
+ component: Function;
30
+ metadata: RegistryComponentResponse['metadata'];
31
+ compiledAt: Date;
32
+ lastUsed: Date;
33
+ useCount: number;
34
+ }
35
+
36
+ /**
37
+ * Service for managing component loading from registries with compilation caching
38
+ */
39
+ export class ComponentRegistryService {
40
+ private static instance: ComponentRegistryService | null = null;
41
+
42
+ // Caches
43
+ private compiledComponentCache = new Map<string, CachedCompiledComponent>();
44
+ private componentReferences = new Map<string, Set<string>>();
45
+
46
+ // Dependencies
47
+ private compiler: ComponentCompiler;
48
+ private runtimeContext: RuntimeContext;
49
+ private componentEngine = ComponentMetadataEngine.Instance;
50
+ private registryProviders = new Map<string, RegistryProvider>();
51
+
52
+ private constructor(
53
+ compiler: ComponentCompiler,
54
+ runtimeContext: RuntimeContext
55
+ ) {
56
+ this.compiler = compiler;
57
+ this.runtimeContext = runtimeContext;
58
+ }
59
+
60
+ /**
61
+ * Get or create the singleton instance
62
+ */
63
+ static getInstance(
64
+ compiler: ComponentCompiler,
65
+ context: RuntimeContext
66
+ ): ComponentRegistryService {
67
+ if (!ComponentRegistryService.instance) {
68
+ ComponentRegistryService.instance = new ComponentRegistryService(compiler, context);
69
+ }
70
+ return ComponentRegistryService.instance;
71
+ }
72
+
73
+ /**
74
+ * Initialize the service with metadata
75
+ */
76
+ async initialize(contextUser?: UserInfo): Promise<void> {
77
+ // Initialize metadata engine
78
+ await this.componentEngine.Config(false, contextUser);
79
+ }
80
+
81
+ /**
82
+ * Get a compiled component, using cache if available
83
+ */
84
+ async getCompiledComponent(
85
+ componentId: string,
86
+ referenceId?: string,
87
+ contextUser?: UserInfo
88
+ ): Promise<Function> {
89
+ await this.initialize(contextUser);
90
+
91
+ // Find component in metadata
92
+ const component = this.componentEngine.Components.find((c: ComponentEntity) => c.ID === componentId);
93
+ if (!component) {
94
+ throw new Error(`Component not found: ${componentId}`);
95
+ }
96
+
97
+ const key = this.getComponentKey(component.Name, component.Namespace, component.Version, component.SourceRegistryID);
98
+
99
+ // Check if already compiled and cached
100
+ if (this.compiledComponentCache.has(key)) {
101
+ const cached = this.compiledComponentCache.get(key)!;
102
+ cached.lastUsed = new Date();
103
+ cached.useCount++;
104
+
105
+ // Track reference if provided
106
+ if (referenceId) {
107
+ this.addComponentReference(key, referenceId);
108
+ }
109
+
110
+ console.log(`โœ… Reusing compiled component from cache: ${key} (use count: ${cached.useCount})`);
111
+ return cached.component;
112
+ }
113
+
114
+ // Not in cache, need to load and compile
115
+ console.log(`๐Ÿ”„ Loading and compiling component: ${key}`);
116
+
117
+ // Get the component specification
118
+ const spec = await this.getComponentSpec(componentId, contextUser);
119
+
120
+ // Compile the component
121
+ // Load all libraries from metadata engine
122
+ const allLibraries = this.componentEngine.ComponentLibraries || [];
123
+
124
+ const compilationResult = await this.compiler.compile({
125
+ componentName: component.Name,
126
+ componentCode: spec.code,
127
+ libraries: spec.libraries,
128
+ allLibraries
129
+ });
130
+
131
+ if (!compilationResult.success) {
132
+ throw new Error(`Failed to compile component ${component.Name}: ${compilationResult.error}`);
133
+ }
134
+
135
+ // Cache the compiled component
136
+ const metadata: RegistryComponentMetadata = {
137
+ name: component.Name,
138
+ namespace: component.Namespace || '',
139
+ version: component.Version,
140
+ description: component.Description || '',
141
+ title: component.Title || undefined,
142
+ type: component.Type || undefined,
143
+ status: component.Status || undefined,
144
+ properties: spec.properties,
145
+ events: spec.events,
146
+ libraries: spec.libraries,
147
+ dependencies: spec.dependencies,
148
+ sourceRegistryID: component.SourceRegistryID,
149
+ isLocal: !component.SourceRegistryID
150
+ };
151
+
152
+ if (!compilationResult.component) {
153
+ throw new Error(`Component compilation succeeded but no component returned`);
154
+ }
155
+ const compiledComponent = compilationResult.component.component(this.runtimeContext);
156
+ this.compiledComponentCache.set(key, {
157
+ component: compiledComponent,
158
+ metadata,
159
+ compiledAt: new Date(),
160
+ lastUsed: new Date(),
161
+ useCount: 1
162
+ });
163
+
164
+ // Track reference
165
+ if (referenceId) {
166
+ this.addComponentReference(key, referenceId);
167
+ }
168
+
169
+ return compiledComponent;
170
+ }
171
+
172
+ /**
173
+ * Get component specification from database or external registry
174
+ */
175
+ async getComponentSpec(
176
+ componentId: string,
177
+ contextUser?: UserInfo
178
+ ): Promise<ComponentSpec> {
179
+ await this.initialize(contextUser);
180
+
181
+ const component = this.componentEngine.Components.find((c: ComponentEntity) => c.ID === componentId);
182
+ if (!component) {
183
+ throw new Error(`Component not found: ${componentId}`);
184
+ }
185
+
186
+ if (!component.SourceRegistryID) {
187
+ // LOCAL: Use specification from database
188
+ if (!component.Specification) {
189
+ throw new Error(`Local component ${component.Name} has no specification`);
190
+ }
191
+ return JSON.parse(component.Specification);
192
+ }
193
+
194
+ // EXTERNAL: Check if we have a cached version
195
+ if (component.Specification && component.LastSyncedAt) {
196
+ // For now, always use cached version (no expiration)
197
+ console.log(`Using cached external component: ${component.Name} (synced: ${component.LastSyncedAt})`);
198
+ return JSON.parse(component.Specification);
199
+ }
200
+
201
+ // Need to fetch from external registry
202
+ const registry = this.componentEngine.ComponentRegistries?.find(
203
+ r => r.ID === component.SourceRegistryID
204
+ );
205
+
206
+ if (!registry) {
207
+ throw new Error(`Registry not found: ${component.SourceRegistryID}`);
208
+ }
209
+
210
+ if (!registry) {
211
+ throw new Error(`Registry not found: ${component.SourceRegistryID}`);
212
+ }
213
+
214
+ const spec = await this.fetchFromExternalRegistry(
215
+ registry.URI || '',
216
+ component.Name,
217
+ component.Namespace || '',
218
+ component.Version,
219
+ this.getRegistryApiKey(registry.ID) // API keys stored in env vars or secure config
220
+ );
221
+
222
+ // Store in local database for future use
223
+ await this.cacheExternalComponent(componentId, spec, contextUser);
224
+
225
+ return spec;
226
+ }
227
+
228
+ /**
229
+ * Fetch component from external registry via HTTP
230
+ */
231
+ private async fetchFromExternalRegistry(
232
+ uri: string,
233
+ name: string,
234
+ namespace: string,
235
+ version: string,
236
+ apiKey?: string
237
+ ): Promise<ComponentSpec> {
238
+ const url = `${uri}/components/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/${version}`;
239
+
240
+ const headers: HeadersInit = {
241
+ 'Accept': 'application/json'
242
+ };
243
+
244
+ if (apiKey) {
245
+ headers['Authorization'] = `Bearer ${apiKey}`;
246
+ }
247
+
248
+ console.log(`Fetching from external registry: ${url}`);
249
+
250
+ const response = await fetch(url, { headers });
251
+
252
+ if (!response.ok) {
253
+ throw new Error(`Registry fetch failed: ${response.status} ${response.statusText}`);
254
+ }
255
+
256
+ const spec = await response.json() as ComponentSpec;
257
+ return spec;
258
+ }
259
+
260
+ /**
261
+ * Cache an external component in the local database
262
+ */
263
+ private async cacheExternalComponent(
264
+ componentId: string,
265
+ spec: ComponentSpec,
266
+ contextUser?: UserInfo
267
+ ): Promise<void> {
268
+ // Get the actual entity object to save
269
+ const md = new Metadata();
270
+ const componentEntity = await md.GetEntityObject<ComponentEntity>('MJ: Components', contextUser);
271
+
272
+ // Load the existing component
273
+ if (!await componentEntity.Load(componentId)) {
274
+ throw new Error(`Failed to load component entity: ${componentId}`);
275
+ }
276
+
277
+ // Update with fetched specification and all fields from spec
278
+ componentEntity.Specification = JSON.stringify(spec);
279
+ componentEntity.LastSyncedAt = new Date();
280
+
281
+ // Set ReplicatedAt only on first fetch
282
+ if (!componentEntity.ReplicatedAt) {
283
+ componentEntity.ReplicatedAt = new Date();
284
+ }
285
+
286
+ // Update all fields from the spec with strong typing
287
+ if (spec.name) {
288
+ componentEntity.Name = spec.name;
289
+ }
290
+
291
+ if (spec.namespace) {
292
+ componentEntity.Namespace = spec.namespace;
293
+ }
294
+
295
+ if (spec.version) {
296
+ componentEntity.Version = spec.version;
297
+ }
298
+
299
+ if (spec.title) {
300
+ componentEntity.Title = spec.title;
301
+ }
302
+
303
+ if (spec.description) {
304
+ componentEntity.Description = spec.description;
305
+ }
306
+
307
+ if (spec.type) {
308
+ // Map spec type to entity type (entity has specific enum values)
309
+ const typeMap: Record<string, ComponentEntity['Type']> = {
310
+ 'report': 'Report',
311
+ 'dashboard': 'Dashboard',
312
+ 'form': 'Form',
313
+ 'table': 'Table',
314
+ 'chart': 'Chart',
315
+ 'navigation': 'Navigation',
316
+ 'search': 'Search',
317
+ 'widget': 'Widget',
318
+ 'utility': 'Utility',
319
+ 'other': 'Other'
320
+ };
321
+
322
+ const mappedType = typeMap[spec.type.toLowerCase()];
323
+ if (mappedType) {
324
+ componentEntity.Type = mappedType;
325
+ }
326
+ }
327
+
328
+ if (spec.functionalRequirements) {
329
+ componentEntity.FunctionalRequirements = spec.functionalRequirements;
330
+ }
331
+
332
+ if (spec.technicalDesign) {
333
+ componentEntity.TechnicalDesign = spec.technicalDesign;
334
+ }
335
+
336
+ // Save back to database
337
+ const result = await componentEntity.Save();
338
+ if (!result) {
339
+ throw new Error(`Failed to save cached component: ${componentEntity.Name}\n${componentEntity.LatestResult.Message || componentEntity.LatestResult.Error || componentEntity.LatestResult.Errors?.join(',')}`);
340
+ }
341
+
342
+ console.log(`Cached external component: ${componentEntity.Name} at ${componentEntity.LastSyncedAt}`);
343
+
344
+ // Refresh metadata cache after saving
345
+ await this.componentEngine.Config(true, contextUser);
346
+ }
347
+
348
+ /**
349
+ * Load component dependencies from database
350
+ */
351
+ async loadDependencies(
352
+ componentId: string,
353
+ contextUser?: UserInfo
354
+ ): Promise<ComponentDependencyInfo[]> {
355
+ await this.initialize(contextUser);
356
+
357
+ // Get dependencies from metadata cache
358
+ const dependencies = this.componentEngine.ComponentDependencies?.filter(
359
+ d => d.ComponentID === componentId
360
+ ) || [];
361
+
362
+ const result: ComponentDependencyInfo[] = [];
363
+
364
+ for (const dep of dependencies) {
365
+ // Find the dependency component
366
+ const depComponent = this.componentEngine.Components.find(
367
+ (c: ComponentEntity) => c.ID === dep.DependencyComponentID
368
+ );
369
+
370
+ if (depComponent) {
371
+ result.push({
372
+ name: depComponent.Name,
373
+ namespace: depComponent.Namespace || '',
374
+ version: depComponent.Version, // Version comes from the linked Component record
375
+ isRequired: true, // All dependencies are required in MemberJunction
376
+ location: depComponent.SourceRegistryID ? 'registry' : 'embedded',
377
+ sourceRegistryID: depComponent.SourceRegistryID
378
+ });
379
+ }
380
+ }
381
+
382
+ return result;
383
+ }
384
+
385
+ /**
386
+ * Resolve full dependency tree for a component
387
+ */
388
+ async resolveDependencyTree(
389
+ componentId: string,
390
+ contextUser?: UserInfo,
391
+ visited = new Set<string>()
392
+ ): Promise<DependencyTree> {
393
+ if (visited.has(componentId)) {
394
+ return {
395
+ componentId,
396
+ circular: true
397
+ };
398
+ }
399
+ visited.add(componentId);
400
+
401
+ await this.initialize(contextUser);
402
+
403
+ const component = this.componentEngine.Components.find((c: ComponentEntity) => c.ID === componentId);
404
+ if (!component) {
405
+ return { componentId, dependencies: [] };
406
+ }
407
+
408
+ // Get direct dependencies
409
+ const directDeps = await this.loadDependencies(componentId, contextUser);
410
+
411
+ // Recursively resolve each dependency
412
+ const dependencies: DependencyTree[] = [];
413
+ for (const dep of directDeps) {
414
+ // Find the dependency component
415
+ const depComponent = this.componentEngine.Components.find(
416
+ c => c.Name.trim().toLowerCase() === dep.name.trim().toLowerCase() &&
417
+ c.Namespace?.trim().toLowerCase() === dep.namespace?.trim().toLowerCase()
418
+ );
419
+
420
+ if (depComponent) {
421
+ const subTree = await this.resolveDependencyTree(
422
+ depComponent.ID,
423
+ contextUser,
424
+ visited
425
+ );
426
+ dependencies.push(subTree);
427
+ }
428
+ }
429
+
430
+ return {
431
+ componentId,
432
+ name: component.Name,
433
+ namespace: component.Namespace || undefined,
434
+ version: component.Version,
435
+ dependencies,
436
+ totalCount: dependencies.reduce((sum, d) => sum + (d.totalCount || 1), 1)
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Get components to load in dependency order
442
+ */
443
+ async getComponentsToLoad(
444
+ rootComponentId: string,
445
+ contextUser?: UserInfo
446
+ ): Promise<string[]> {
447
+ const tree = await this.resolveDependencyTree(rootComponentId, contextUser);
448
+
449
+ // Flatten tree in dependency order (depth-first)
450
+ const ordered: string[] = [];
451
+ const processNode = (node: DependencyTree) => {
452
+ if (node.dependencies) {
453
+ node.dependencies.forEach(processNode);
454
+ }
455
+ if (!ordered.includes(node.componentId)) {
456
+ ordered.push(node.componentId);
457
+ }
458
+ };
459
+ processNode(tree);
460
+
461
+ return ordered;
462
+ }
463
+
464
+ /**
465
+ * Add a reference to a component
466
+ */
467
+ private addComponentReference(componentKey: string, referenceId: string): void {
468
+ if (!this.componentReferences.has(componentKey)) {
469
+ this.componentReferences.set(componentKey, new Set());
470
+ }
471
+ this.componentReferences.get(componentKey)!.add(referenceId);
472
+ }
473
+
474
+ /**
475
+ * Remove a reference to a component
476
+ */
477
+ removeComponentReference(componentKey: string, referenceId: string): void {
478
+ const refs = this.componentReferences.get(componentKey);
479
+ if (refs) {
480
+ refs.delete(referenceId);
481
+
482
+ // If no more references and cache cleanup is enabled
483
+ if (refs.size === 0) {
484
+ this.considerCacheEviction(componentKey);
485
+ }
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Consider evicting a component from cache
491
+ */
492
+ private considerCacheEviction(componentKey: string): void {
493
+ const cached = this.compiledComponentCache.get(componentKey);
494
+ if (cached) {
495
+ const timeSinceLastUse = Date.now() - cached.lastUsed.getTime();
496
+ const evictionThreshold = 5 * 60 * 1000; // 5 minutes
497
+
498
+ if (timeSinceLastUse > evictionThreshold) {
499
+ console.log(`๐Ÿ—‘๏ธ Evicting unused component from cache: ${componentKey}`);
500
+ this.compiledComponentCache.delete(componentKey);
501
+ }
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Get API key for a registry from secure configuration
507
+ * @param registryId - Registry ID
508
+ * @returns API key or undefined
509
+ */
510
+ private getRegistryApiKey(registryId: string): string | undefined {
511
+ // API keys should be stored in environment variables or secure configuration
512
+ // Format: REGISTRY_API_KEY_{registryId} or similar
513
+ // This is a placeholder - actual implementation would depend on the security infrastructure
514
+ const envKey = `REGISTRY_API_KEY_${registryId.replace(/-/g, '_').toUpperCase()}`;
515
+ return process.env[envKey];
516
+ }
517
+
518
+ /**
519
+ * Get cache statistics
520
+ */
521
+ getCacheStats(): {
522
+ compiledComponents: number;
523
+ totalUseCount: number;
524
+ memoryEstimate: string;
525
+ } {
526
+ let totalUseCount = 0;
527
+ this.compiledComponentCache.forEach(cached => {
528
+ totalUseCount += cached.useCount;
529
+ });
530
+
531
+ return {
532
+ compiledComponents: this.compiledComponentCache.size,
533
+ totalUseCount,
534
+ memoryEstimate: `~${(this.compiledComponentCache.size * 50)}KB` // Rough estimate
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Clear all caches
540
+ */
541
+ clearCache(): void {
542
+ console.log('๐Ÿงน Clearing all component caches');
543
+ this.compiledComponentCache.clear();
544
+ this.componentReferences.clear();
545
+ }
546
+
547
+ /**
548
+ * Generate a cache key for a component
549
+ */
550
+ private getComponentKey(
551
+ name: string,
552
+ namespace: string | null | undefined,
553
+ version: string,
554
+ sourceRegistryId: string | null | undefined
555
+ ): string {
556
+ const registryPart = sourceRegistryId || 'local';
557
+ const namespacePart = namespace || 'global';
558
+ return `${registryPart}/${namespacePart}/${name}@${version}`;
559
+ }
560
+ }