@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.
- package/.turbo/turbo-build.log +15 -20
- package/CHANGELOG.md +26 -0
- package/README.md +171 -1
- package/dist/compiler/component-compiler.d.ts.map +1 -1
- package/dist/compiler/component-compiler.js +59 -8
- package/dist/compiler/component-compiler.js.map +1 -1
- package/dist/component-manager/component-manager.d.ts +39 -0
- package/dist/component-manager/component-manager.d.ts.map +1 -0
- package/dist/component-manager/component-manager.js +474 -0
- package/dist/component-manager/component-manager.js.map +1 -0
- package/dist/component-manager/index.d.ts +3 -0
- package/dist/component-manager/index.d.ts.map +1 -0
- package/dist/component-manager/index.js +6 -0
- package/dist/component-manager/index.js.map +1 -0
- package/dist/component-manager/types.d.ts +62 -0
- package/dist/component-manager/types.d.ts.map +1 -0
- package/dist/component-manager/types.js +3 -0
- package/dist/component-manager/types.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/registry/component-registry-service.d.ts +16 -1
- package/dist/registry/component-registry-service.d.ts.map +1 -1
- package/dist/registry/component-registry-service.js +212 -10
- package/dist/registry/component-registry-service.js.map +1 -1
- package/dist/registry/component-registry.d.ts +1 -1
- 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.map +1 -1
- package/dist/registry/component-resolver.js +122 -52
- package/dist/registry/component-resolver.js.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/runtime/component-hierarchy.d.ts +4 -0
- package/dist/runtime/component-hierarchy.d.ts.map +1 -1
- package/dist/runtime/component-hierarchy.js +127 -12
- package/dist/runtime/component-hierarchy.js.map +1 -1
- package/dist/runtime.umd.js +535 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utilities/component-unwrapper.d.ts +7 -0
- package/dist/utilities/component-unwrapper.d.ts.map +1 -0
- package/dist/utilities/component-unwrapper.js +369 -0
- package/dist/utilities/component-unwrapper.js.map +1 -0
- 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-loader.d.ts +3 -0
- package/dist/utilities/library-loader.d.ts.map +1 -1
- package/dist/utilities/library-loader.js +101 -17
- package/dist/utilities/library-loader.js.map +1 -1
- package/examples/component-registry-integration.ts +191 -0
- package/package.json +6 -5
- package/src/compiler/component-compiler.ts +101 -23
- package/src/component-manager/component-manager.ts +736 -0
- package/src/component-manager/index.ts +13 -0
- package/src/component-manager/types.ts +204 -0
- package/src/index.ts +37 -1
- package/src/registry/component-registry-service.ts +315 -18
- package/src/registry/component-registry.ts +1 -1
- package/src/registry/component-resolver.ts +159 -67
- package/src/registry/index.ts +1 -1
- package/src/runtime/component-hierarchy.ts +124 -13
- package/src/types/index.ts +2 -0
- package/src/utilities/component-unwrapper.ts +481 -0
- package/src/utilities/index.ts +2 -1
- 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
|
+
}
|