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