@memberjunction/react-runtime 5.32.0 → 5.34.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 -14
- package/CHANGELOG.md +38 -0
- package/dist/324.runtime.umd.js +150 -104
- package/dist/component-manager/component-manager.d.ts.map +1 -1
- package/dist/component-manager/component-manager.js +32 -13
- package/dist/component-manager/component-manager.js.map +1 -1
- package/dist/registry/component-registry-service.d.ts.map +1 -1
- package/dist/registry/component-registry-service.js +5 -6
- package/dist/registry/component-registry-service.js.map +1 -1
- package/dist/registry/component-resolver.d.ts.map +1 -1
- package/dist/registry/component-resolver.js +3 -21
- package/dist/registry/component-resolver.js.map +1 -1
- package/dist/runtime.umd.js +6786 -2767
- package/package.json +6 -6
- package/src/__tests__/component-manager.test.ts +340 -0
- package/src/component-manager/component-manager.ts +62 -43
- package/src/registry/component-registry-service.ts +14 -20
- package/src/registry/component-resolver.ts +11 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/react-runtime",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.34.0",
|
|
4
4
|
"description": "Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
},
|
|
30
30
|
"homepage": "https://github.com/MemberJunction/MJ#readme",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@memberjunction/core": "5.
|
|
33
|
-
"@memberjunction/global": "5.
|
|
34
|
-
"@memberjunction/interactive-component-types": "5.
|
|
35
|
-
"@memberjunction/core-entities": "5.
|
|
36
|
-
"@memberjunction/graphql-dataprovider": "5.
|
|
32
|
+
"@memberjunction/core": "5.34.0",
|
|
33
|
+
"@memberjunction/global": "5.34.0",
|
|
34
|
+
"@memberjunction/interactive-component-types": "5.34.0",
|
|
35
|
+
"@memberjunction/core-entities": "5.34.0",
|
|
36
|
+
"@memberjunction/graphql-dataprovider": "5.34.0",
|
|
37
37
|
"@babel/standalone": "^7.29.1",
|
|
38
38
|
"rxjs": "^7.8.2"
|
|
39
39
|
},
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Stub globals before imports
|
|
4
|
+
vi.stubGlobal('window', {
|
|
5
|
+
setInterval: vi.fn().mockReturnValue(1),
|
|
6
|
+
clearInterval: vi.fn(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Mock ComponentMetadataEngine before importing ComponentManager
|
|
10
|
+
vi.mock('@memberjunction/core-entities', () => ({
|
|
11
|
+
ComponentMetadataEngine: {
|
|
12
|
+
Instance: {
|
|
13
|
+
Config: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
Components: [],
|
|
15
|
+
ComponentLibraries: [],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
MJComponentLibraryEntity: class {},
|
|
19
|
+
MJComponentEntityExtended: class {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('@memberjunction/core', () => ({
|
|
23
|
+
UserInfo: class {},
|
|
24
|
+
Metadata: class {},
|
|
25
|
+
LogError: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { ComponentManager } from '../component-manager/component-manager';
|
|
29
|
+
import { ComponentRegistry } from '../registry/component-registry';
|
|
30
|
+
import { ComponentSpec } from '@memberjunction/interactive-component-types';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a minimal mock ComponentCompiler.
|
|
34
|
+
* The compile() result returns a factory that produces a tagged component object
|
|
35
|
+
* so tests can verify which code was actually compiled.
|
|
36
|
+
*/
|
|
37
|
+
function createMockCompiler() {
|
|
38
|
+
return {
|
|
39
|
+
compile: vi.fn().mockImplementation(async (opts: { componentName: string; componentCode: string }) => ({
|
|
40
|
+
success: true,
|
|
41
|
+
component: {
|
|
42
|
+
factory: () => ({ __name: opts.componentName, __code: opts.componentCode }),
|
|
43
|
+
},
|
|
44
|
+
loadedLibraries: new Map(),
|
|
45
|
+
})),
|
|
46
|
+
setBabelInstance: vi.fn(),
|
|
47
|
+
} as any;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a minimal mock RuntimeContext.
|
|
52
|
+
*/
|
|
53
|
+
function createMockRuntimeContext() {
|
|
54
|
+
return {
|
|
55
|
+
React: {},
|
|
56
|
+
globals: {},
|
|
57
|
+
libraries: {},
|
|
58
|
+
getLibrary: vi.fn(),
|
|
59
|
+
} as any;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Helper to build a ComponentSpec with sensible defaults.
|
|
64
|
+
*/
|
|
65
|
+
function makeSpec(overrides: Partial<ComponentSpec> & { name: string }): ComponentSpec {
|
|
66
|
+
return {
|
|
67
|
+
location: 'embedded',
|
|
68
|
+
version: '1.0.0',
|
|
69
|
+
namespace: 'Test',
|
|
70
|
+
code: `function ${overrides.name}() { return null; }`,
|
|
71
|
+
...overrides,
|
|
72
|
+
} as ComponentSpec;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('ComponentManager', () => {
|
|
76
|
+
let compiler: ReturnType<typeof createMockCompiler>;
|
|
77
|
+
let registry: ComponentRegistry;
|
|
78
|
+
let manager: ComponentManager;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
compiler = createMockCompiler();
|
|
82
|
+
registry = new ComponentRegistry({ cleanupInterval: 0 });
|
|
83
|
+
manager = new ComponentManager(compiler, registry, createMockRuntimeContext(), {
|
|
84
|
+
debug: false,
|
|
85
|
+
enableUsageTracking: false,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// --------------------------------------------------------------------------
|
|
90
|
+
// Basic loading
|
|
91
|
+
// --------------------------------------------------------------------------
|
|
92
|
+
describe('loadComponent', () => {
|
|
93
|
+
it('should compile and return a component on first load', async () => {
|
|
94
|
+
const spec = makeSpec({ name: 'Button' });
|
|
95
|
+
const result = await manager.loadComponent(spec);
|
|
96
|
+
|
|
97
|
+
expect(result.success).toBe(true);
|
|
98
|
+
expect(result.fromCache).toBe(false);
|
|
99
|
+
expect(compiler.compile).toHaveBeenCalledTimes(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return from cache on second load with same spec', async () => {
|
|
103
|
+
const spec = makeSpec({ name: 'Button' });
|
|
104
|
+
|
|
105
|
+
await manager.loadComponent(spec);
|
|
106
|
+
compiler.compile.mockClear();
|
|
107
|
+
|
|
108
|
+
const result2 = await manager.loadComponent(spec);
|
|
109
|
+
expect(result2.success).toBe(true);
|
|
110
|
+
expect(result2.fromCache).toBe(true);
|
|
111
|
+
expect(compiler.compile).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should recompile when code changes (different hash)', async () => {
|
|
115
|
+
const specV1 = makeSpec({ name: 'Button', code: 'function Button() { return 1; }' });
|
|
116
|
+
const specV2 = makeSpec({ name: 'Button', code: 'function Button() { return 2; }' });
|
|
117
|
+
|
|
118
|
+
await manager.loadComponent(specV1);
|
|
119
|
+
compiler.compile.mockClear();
|
|
120
|
+
|
|
121
|
+
const result = await manager.loadComponent(specV2);
|
|
122
|
+
expect(result.success).toBe(true);
|
|
123
|
+
expect(result.fromCache).toBe(false);
|
|
124
|
+
expect(compiler.compile).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should recompile when forceRecompile is set', async () => {
|
|
128
|
+
const spec = makeSpec({ name: 'Button' });
|
|
129
|
+
|
|
130
|
+
await manager.loadComponent(spec);
|
|
131
|
+
compiler.compile.mockClear();
|
|
132
|
+
|
|
133
|
+
const result = await manager.loadComponent(spec, { forceRecompile: true });
|
|
134
|
+
expect(result.success).toBe(true);
|
|
135
|
+
expect(result.fromCache).toBe(false);
|
|
136
|
+
expect(compiler.compile).toHaveBeenCalledTimes(1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// --------------------------------------------------------------------------
|
|
141
|
+
// loadHierarchy – dependency resolution
|
|
142
|
+
// --------------------------------------------------------------------------
|
|
143
|
+
describe('loadHierarchy', () => {
|
|
144
|
+
it('should load root and its dependencies', async () => {
|
|
145
|
+
const childSpec = makeSpec({ name: 'ChildBtn', code: 'function ChildBtn() { return "child"; }' });
|
|
146
|
+
const rootSpec = makeSpec({
|
|
147
|
+
name: 'ParentForm',
|
|
148
|
+
code: 'function ParentForm() { return "parent"; }',
|
|
149
|
+
dependencies: [childSpec],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = await manager.loadHierarchy(rootSpec);
|
|
153
|
+
|
|
154
|
+
expect(result.success).toBe(true);
|
|
155
|
+
expect(result.loadedComponents).toContain('ParentForm');
|
|
156
|
+
expect(result.loadedComponents).toContain('ChildBtn');
|
|
157
|
+
expect(compiler.compile).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should use updated dependency code even when root is cached', async () => {
|
|
161
|
+
const childV1 = makeSpec({ name: 'Search', code: 'function Search() { return "v1"; }' });
|
|
162
|
+
const rootSpec = makeSpec({
|
|
163
|
+
name: 'Dashboard',
|
|
164
|
+
code: 'function Dashboard() { return "root"; }',
|
|
165
|
+
dependencies: [childV1],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// First load – populates cache
|
|
169
|
+
await manager.loadHierarchy(rootSpec);
|
|
170
|
+
expect(compiler.compile).toHaveBeenCalledTimes(2);
|
|
171
|
+
compiler.compile.mockClear();
|
|
172
|
+
|
|
173
|
+
// Second load – same root code, but updated Search dependency code
|
|
174
|
+
const childV2 = makeSpec({ name: 'Search', code: 'function Search() { return "v2"; }' });
|
|
175
|
+
const rootSpecUpdated = makeSpec({
|
|
176
|
+
name: 'Dashboard',
|
|
177
|
+
code: 'function Dashboard() { return "root"; }', // same root code
|
|
178
|
+
dependencies: [childV2],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await manager.loadHierarchy(rootSpecUpdated);
|
|
182
|
+
|
|
183
|
+
expect(result.success).toBe(true);
|
|
184
|
+
// Root should be cached, Search should recompile with new code
|
|
185
|
+
expect(result.stats?.fromCache).toBe(1); // root only
|
|
186
|
+
expect(compiler.compile).toHaveBeenCalledTimes(1); // Search recompiled
|
|
187
|
+
// Verify the recompiled dependency used the v2 code
|
|
188
|
+
const compileCall = compiler.compile.mock.calls[0][0];
|
|
189
|
+
expect(compileCall.componentCode).toContain('return "v2"');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should cache dependencies individually by content hash', async () => {
|
|
193
|
+
const child = makeSpec({ name: 'Grid', code: 'function Grid() { return "grid"; }' });
|
|
194
|
+
const root1 = makeSpec({
|
|
195
|
+
name: 'Page1',
|
|
196
|
+
code: 'function Page1() { return "p1"; }',
|
|
197
|
+
dependencies: [child],
|
|
198
|
+
});
|
|
199
|
+
const root2 = makeSpec({
|
|
200
|
+
name: 'Page2',
|
|
201
|
+
code: 'function Page2() { return "p2"; }',
|
|
202
|
+
dependencies: [{ ...child }], // same Grid dependency
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await manager.loadHierarchy(root1);
|
|
206
|
+
expect(compiler.compile).toHaveBeenCalledTimes(2); // Page1 + Grid
|
|
207
|
+
compiler.compile.mockClear();
|
|
208
|
+
|
|
209
|
+
// Loading Page2 with the exact same Grid dep should reuse Grid from cache
|
|
210
|
+
const result = await manager.loadHierarchy(root2);
|
|
211
|
+
expect(result.success).toBe(true);
|
|
212
|
+
expect(compiler.compile).toHaveBeenCalledTimes(1); // Only Page2 compiled, Grid cached
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should handle deeply nested dependency updates', async () => {
|
|
216
|
+
const leafV1 = makeSpec({ name: 'Leaf', code: 'function Leaf() { return "leaf-v1"; }' });
|
|
217
|
+
const mid = makeSpec({
|
|
218
|
+
name: 'Middle',
|
|
219
|
+
code: 'function Middle() { return "mid"; }',
|
|
220
|
+
dependencies: [leafV1],
|
|
221
|
+
});
|
|
222
|
+
const root = makeSpec({
|
|
223
|
+
name: 'Root',
|
|
224
|
+
code: 'function Root() { return "root"; }',
|
|
225
|
+
dependencies: [mid],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// First load
|
|
229
|
+
await manager.loadHierarchy(root);
|
|
230
|
+
expect(compiler.compile).toHaveBeenCalledTimes(3);
|
|
231
|
+
compiler.compile.mockClear();
|
|
232
|
+
|
|
233
|
+
// Update only the leaf
|
|
234
|
+
const leafV2 = makeSpec({ name: 'Leaf', code: 'function Leaf() { return "leaf-v2"; }' });
|
|
235
|
+
const midUpdated = makeSpec({
|
|
236
|
+
name: 'Middle',
|
|
237
|
+
code: 'function Middle() { return "mid"; }',
|
|
238
|
+
dependencies: [leafV2],
|
|
239
|
+
});
|
|
240
|
+
const rootUpdated = makeSpec({
|
|
241
|
+
name: 'Root',
|
|
242
|
+
code: 'function Root() { return "root"; }',
|
|
243
|
+
dependencies: [midUpdated],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const result = await manager.loadHierarchy(rootUpdated);
|
|
247
|
+
expect(result.success).toBe(true);
|
|
248
|
+
// Root cached, Middle cached, only Leaf recompiled
|
|
249
|
+
expect(compiler.compile).toHaveBeenCalledTimes(1);
|
|
250
|
+
const compileCall = compiler.compile.mock.calls[0][0];
|
|
251
|
+
expect(compileCall.componentCode).toContain('leaf-v2');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should handle circular dependencies without infinite loop', async () => {
|
|
255
|
+
const specA = makeSpec({ name: 'CompA', code: 'function CompA() {}' });
|
|
256
|
+
const specB = makeSpec({
|
|
257
|
+
name: 'CompB',
|
|
258
|
+
code: 'function CompB() {}',
|
|
259
|
+
dependencies: [specA],
|
|
260
|
+
});
|
|
261
|
+
specA.dependencies = [specB];
|
|
262
|
+
|
|
263
|
+
const root = makeSpec({
|
|
264
|
+
name: 'Root',
|
|
265
|
+
code: 'function Root() {}',
|
|
266
|
+
dependencies: [specA],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const result = await manager.loadHierarchy(root);
|
|
270
|
+
expect(result.success).toBe(true);
|
|
271
|
+
expect(result.loadedComponents).toContain('Root');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should fallback to cached spec dependencies when input has none', async () => {
|
|
275
|
+
const child = makeSpec({ name: 'Chart', code: 'function Chart() {}' });
|
|
276
|
+
const fullRootSpec = makeSpec({
|
|
277
|
+
name: 'Dashboard',
|
|
278
|
+
code: 'function Dashboard() {}',
|
|
279
|
+
dependencies: [child],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// First load with full spec (has dependencies)
|
|
283
|
+
await manager.loadHierarchy(fullRootSpec);
|
|
284
|
+
expect(compiler.compile).toHaveBeenCalledTimes(2);
|
|
285
|
+
compiler.compile.mockClear();
|
|
286
|
+
|
|
287
|
+
// Second load with no dependencies field - simulates registry reference
|
|
288
|
+
const minimalSpec = makeSpec({
|
|
289
|
+
name: 'Dashboard',
|
|
290
|
+
code: 'function Dashboard() {}',
|
|
291
|
+
});
|
|
292
|
+
delete (minimalSpec as any).dependencies;
|
|
293
|
+
|
|
294
|
+
const result = await manager.loadHierarchy(minimalSpec);
|
|
295
|
+
expect(result.success).toBe(true);
|
|
296
|
+
expect(result.loadedComponents).toContain('Dashboard');
|
|
297
|
+
// Chart should still be loaded via cached spec's dependencies
|
|
298
|
+
expect(result.loadedComponents).toContain('Chart');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should prefer input dependencies over stale cached dependencies', async () => {
|
|
302
|
+
const depA = makeSpec({ name: 'DepA', code: 'function DepA() { return "A"; }' });
|
|
303
|
+
const root = makeSpec({
|
|
304
|
+
name: 'App',
|
|
305
|
+
code: 'function App() { return "app"; }',
|
|
306
|
+
dependencies: [depA],
|
|
307
|
+
});
|
|
308
|
+
await manager.loadHierarchy(root);
|
|
309
|
+
compiler.compile.mockClear();
|
|
310
|
+
|
|
311
|
+
// Same root code but dependency list changed (depB instead of depA)
|
|
312
|
+
const depB = makeSpec({ name: 'DepB', code: 'function DepB() { return "B"; }' });
|
|
313
|
+
const rootUpdated = makeSpec({
|
|
314
|
+
name: 'App',
|
|
315
|
+
code: 'function App() { return "app"; }',
|
|
316
|
+
dependencies: [depB],
|
|
317
|
+
});
|
|
318
|
+
const result = await manager.loadHierarchy(rootUpdated);
|
|
319
|
+
|
|
320
|
+
expect(result.success).toBe(true);
|
|
321
|
+
expect(result.loadedComponents).toContain('DepB');
|
|
322
|
+
expect(compiler.compile).toHaveBeenCalledTimes(1); // only DepB
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// --------------------------------------------------------------------------
|
|
327
|
+
// Cache management
|
|
328
|
+
// --------------------------------------------------------------------------
|
|
329
|
+
describe('clearCache', () => {
|
|
330
|
+
it('should clear the fetch cache', async () => {
|
|
331
|
+
const spec = makeSpec({ name: 'Widget' });
|
|
332
|
+
await manager.loadComponent(spec);
|
|
333
|
+
|
|
334
|
+
manager.clearCache();
|
|
335
|
+
|
|
336
|
+
const stats = manager.getCacheStats();
|
|
337
|
+
expect(stats.fetchCacheSize).toBe(0);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { ComponentSpec, ComponentLibraryDependency } from '@memberjunction/interactive-component-types';
|
|
7
7
|
import { UserInfo, Metadata, LogError } from '@memberjunction/core';
|
|
8
|
-
import { ComponentMetadataEngine, MJComponentLibraryEntity
|
|
8
|
+
import { ComponentMetadataEngine, MJComponentLibraryEntity } from '@memberjunction/core-entities';
|
|
9
9
|
|
|
10
10
|
import { ComponentCompiler } from '../compiler';
|
|
11
11
|
import { ComponentRegistry } from '../registry';
|
|
@@ -84,6 +84,25 @@ export class ComponentManager {
|
|
|
84
84
|
// Check if already loading to prevent duplicate work
|
|
85
85
|
const existingPromise = this.loadingPromises.get(componentKey);
|
|
86
86
|
if (existingPromise && !options.forceRefresh) {
|
|
87
|
+
// Before returning the pending promise, check if the compiled component
|
|
88
|
+
// is already in the registry. This prevents deadlock in circular dependency
|
|
89
|
+
// chains: A's Step 6 loads B, B's Step 6 loads A again — but A was already
|
|
90
|
+
// compiled and registered (Step 5 runs before Step 6). Returning the pending
|
|
91
|
+
// promise would deadlock because it's waiting on its own call chain.
|
|
92
|
+
const namespace = spec.namespace || options.defaultNamespace || 'Global';
|
|
93
|
+
const version = spec.version || options.defaultVersion || 'latest';
|
|
94
|
+
const contentHash = this.calculateHash(spec);
|
|
95
|
+
const registered = this.registry.get(spec.name, namespace, version, contentHash);
|
|
96
|
+
if (registered) {
|
|
97
|
+
this.log(`Component already registered (circular dep resolution): ${spec.name}`);
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
component: registered,
|
|
101
|
+
spec,
|
|
102
|
+
fromCache: true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
this.log(`Component already loading: ${spec.name}, waiting...`);
|
|
88
107
|
return existingPromise;
|
|
89
108
|
}
|
|
@@ -406,8 +425,13 @@ export class ComponentManager {
|
|
|
406
425
|
}
|
|
407
426
|
|
|
408
427
|
// Load dependencies
|
|
409
|
-
|
|
410
|
-
|
|
428
|
+
// Prefer the input spec's dependencies over the cached result's dependencies.
|
|
429
|
+
// When a parent component is served from cache, result.spec contains stale
|
|
430
|
+
// dependency code. The input spec has the latest dependency code from the caller.
|
|
431
|
+
// Each dependency still gets its own individual cache check via loadComponent.
|
|
432
|
+
const dependencies = spec.dependencies || result.spec?.dependencies;
|
|
433
|
+
if (dependencies) {
|
|
434
|
+
for (const dep of dependencies) {
|
|
411
435
|
// Normalize dependency spec for local registry lookup
|
|
412
436
|
const depSpec = { ...dep };
|
|
413
437
|
// OPTIMIZATION: If the dependency already has code (from registry population),
|
|
@@ -491,87 +515,82 @@ export class ComponentManager {
|
|
|
491
515
|
// Check cache first
|
|
492
516
|
const cacheKey = this.getComponentKey(spec, {});
|
|
493
517
|
const cached = this.fetchCache.get(cacheKey);
|
|
494
|
-
|
|
518
|
+
|
|
495
519
|
if (cached && this.isCacheValid(cached)) {
|
|
496
520
|
this.log(`Using cached spec for: ${spec.name}`);
|
|
497
521
|
return cached.spec;
|
|
498
522
|
}
|
|
499
|
-
|
|
523
|
+
|
|
500
524
|
// Handle LOCAL registry components (registry is null/undefined)
|
|
501
525
|
if (!spec.registry) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// Find component in local ComponentMetadataEngine
|
|
505
|
-
const localComponent = this.componentEngine.Components?.find(
|
|
506
|
-
(c: MJComponentEntityExtended) => {
|
|
507
|
-
// Match by name (case-insensitive for better compatibility)
|
|
508
|
-
const nameMatch = c.Name?.toLowerCase() === spec.name?.toLowerCase();
|
|
509
|
-
|
|
510
|
-
// Match by namespace if provided (handle different formats)
|
|
511
|
-
const namespaceMatch = !spec.namespace || c.Namespace?.toLowerCase() === spec.namespace?.toLowerCase();
|
|
526
|
+
const localComponent = await this.componentEngine.FindComponent(spec.name, spec.namespace);
|
|
512
527
|
|
|
513
|
-
if (nameMatch && !namespaceMatch) {
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return nameMatch && namespaceMatch;
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
|
|
520
528
|
if (!localComponent) {
|
|
521
529
|
throw new Error(`Local component not found: ${spec.name}`);
|
|
522
530
|
}
|
|
523
|
-
|
|
524
|
-
// Parse specification from local component
|
|
531
|
+
|
|
525
532
|
if (!localComponent.Specification) {
|
|
526
533
|
throw new Error(`Local component ${spec.name} has no specification`);
|
|
527
534
|
}
|
|
528
|
-
|
|
535
|
+
|
|
529
536
|
const fullSpec = JSON.parse(localComponent.Specification);
|
|
530
|
-
|
|
531
|
-
// Cache it
|
|
537
|
+
|
|
532
538
|
this.fetchCache.set(cacheKey, {
|
|
533
539
|
spec: fullSpec,
|
|
534
540
|
fetchedAt: new Date(),
|
|
535
541
|
usageNotified: false
|
|
536
542
|
});
|
|
537
|
-
|
|
543
|
+
|
|
538
544
|
return fullSpec;
|
|
539
545
|
}
|
|
540
|
-
|
|
546
|
+
|
|
541
547
|
// Handle EXTERNAL registry components (registry has a name)
|
|
542
|
-
// Initialize GraphQL client if needed
|
|
543
548
|
if (!this.graphQLClient) {
|
|
544
549
|
await this.initializeGraphQLClient();
|
|
545
550
|
}
|
|
546
|
-
|
|
551
|
+
|
|
547
552
|
if (!this.graphQLClient) {
|
|
548
553
|
throw new Error('GraphQL client not available for registry fetching');
|
|
549
554
|
}
|
|
550
|
-
|
|
551
|
-
// Fetch from external registry
|
|
555
|
+
|
|
556
|
+
// Fetch from external registry, passing the cached hash (if any) so the
|
|
557
|
+
// server can return 304 Not Modified when the spec hasn't changed
|
|
552
558
|
this.log(`Fetching from external registry: ${spec.registry}/${spec.name}`);
|
|
553
|
-
|
|
554
|
-
|
|
559
|
+
const cachedHash = cached?.hash;
|
|
560
|
+
|
|
561
|
+
const response = await this.graphQLClient.GetRegistryComponentWithHash({
|
|
555
562
|
registryName: spec.registry,
|
|
556
563
|
namespace: spec.namespace || 'Global',
|
|
557
564
|
name: spec.name,
|
|
558
|
-
version: spec.version || 'latest'
|
|
565
|
+
version: spec.version || 'latest',
|
|
566
|
+
hash: cachedHash
|
|
559
567
|
});
|
|
560
|
-
|
|
561
|
-
|
|
568
|
+
|
|
569
|
+
// If not modified (304), reuse the cached spec and refresh the TTL
|
|
570
|
+
if (response.notModified && cached) {
|
|
571
|
+
this.log(`Registry returned 304 for ${spec.name}, reusing cached spec`);
|
|
572
|
+
this.fetchCache.set(cacheKey, {
|
|
573
|
+
...cached,
|
|
574
|
+
fetchedAt: new Date()
|
|
575
|
+
});
|
|
576
|
+
return cached.spec;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!response.specification) {
|
|
562
580
|
throw new Error(`Component not found in registry: ${spec.registry}/${spec.name}`);
|
|
563
581
|
}
|
|
564
|
-
|
|
565
|
-
|
|
582
|
+
|
|
583
|
+
const fullSpec = response.specification as ComponentSpec;
|
|
566
584
|
const processedSpec = this.applyResolutionMode(fullSpec, spec, options?.resolutionMode);
|
|
567
|
-
|
|
568
|
-
// Cache it
|
|
585
|
+
|
|
586
|
+
// Cache it with the registry hash for future 304 checks
|
|
569
587
|
this.fetchCache.set(cacheKey, {
|
|
570
588
|
spec: processedSpec,
|
|
571
589
|
fetchedAt: new Date(),
|
|
590
|
+
hash: response.hash,
|
|
572
591
|
usageNotified: false
|
|
573
592
|
});
|
|
574
|
-
|
|
593
|
+
|
|
575
594
|
return processedSpec;
|
|
576
595
|
}
|
|
577
596
|
|
|
@@ -232,8 +232,8 @@ export class ComponentRegistryService {
|
|
|
232
232
|
): Promise<ComponentObject> {
|
|
233
233
|
await this.initialize(contextUser);
|
|
234
234
|
|
|
235
|
-
// Find component in metadata
|
|
236
|
-
const component = this.componentEngine.
|
|
235
|
+
// Find component in metadata via targeted query
|
|
236
|
+
const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
|
|
237
237
|
if (!component) {
|
|
238
238
|
throw new Error(`Component not found: ${componentId}`);
|
|
239
239
|
}
|
|
@@ -487,7 +487,7 @@ export class ComponentRegistryService {
|
|
|
487
487
|
): Promise<ComponentSpec> {
|
|
488
488
|
await this.initialize(contextUser);
|
|
489
489
|
|
|
490
|
-
const component = this.componentEngine.
|
|
490
|
+
const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
|
|
491
491
|
if (!component) {
|
|
492
492
|
throw new Error(`Component not found: ${componentId}`);
|
|
493
493
|
}
|
|
@@ -697,16 +697,14 @@ export class ComponentRegistryService {
|
|
|
697
697
|
|
|
698
698
|
for (const dep of dependencies) {
|
|
699
699
|
// Find the dependency component
|
|
700
|
-
const depComponent = this.componentEngine.
|
|
701
|
-
|
|
702
|
-
);
|
|
703
|
-
|
|
700
|
+
const depComponent = await this.componentEngine.FindComponentByID(dep.DependencyComponentID, contextUser);
|
|
701
|
+
|
|
704
702
|
if (depComponent) {
|
|
705
703
|
result.push({
|
|
706
704
|
name: depComponent.Name,
|
|
707
705
|
namespace: depComponent.Namespace || '',
|
|
708
|
-
version: depComponent.Version,
|
|
709
|
-
isRequired: true,
|
|
706
|
+
version: depComponent.Version,
|
|
707
|
+
isRequired: true,
|
|
710
708
|
location: depComponent.SourceRegistryID ? 'registry' : 'embedded',
|
|
711
709
|
sourceRegistryID: depComponent.SourceRegistryID
|
|
712
710
|
});
|
|
@@ -734,27 +732,23 @@ export class ComponentRegistryService {
|
|
|
734
732
|
|
|
735
733
|
await this.initialize(contextUser);
|
|
736
734
|
|
|
737
|
-
const component = this.componentEngine.
|
|
735
|
+
const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
|
|
738
736
|
if (!component) {
|
|
739
737
|
return { componentId, dependencies: [] };
|
|
740
738
|
}
|
|
741
|
-
|
|
739
|
+
|
|
742
740
|
// Get direct dependencies
|
|
743
741
|
const directDeps = await this.loadDependencies(componentId, contextUser);
|
|
744
|
-
|
|
742
|
+
|
|
745
743
|
// Recursively resolve each dependency
|
|
746
744
|
const dependencies: DependencyTree[] = [];
|
|
747
745
|
for (const dep of directDeps) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
c => c.Name.trim().toLowerCase() === dep.name.trim().toLowerCase() &&
|
|
751
|
-
c.Namespace?.trim().toLowerCase() === dep.namespace?.trim().toLowerCase()
|
|
752
|
-
);
|
|
753
|
-
|
|
746
|
+
const depComponent = await this.componentEngine.FindComponent(dep.name, dep.namespace, undefined, contextUser);
|
|
747
|
+
|
|
754
748
|
if (depComponent) {
|
|
755
749
|
const subTree = await this.resolveDependencyTree(
|
|
756
|
-
depComponent.ID,
|
|
757
|
-
contextUser,
|
|
750
|
+
depComponent.ID,
|
|
751
|
+
contextUser,
|
|
758
752
|
visited
|
|
759
753
|
);
|
|
760
754
|
dependencies.push(subTree);
|