@memberjunction/react-runtime 5.30.1 → 5.32.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 +33 -0
- package/dist/324.runtime.umd.js +111 -63
- package/dist/component-manager/component-manager.d.ts.map +1 -1
- package/dist/component-manager/component-manager.js +8 -6
- package/dist/component-manager/component-manager.js.map +1 -1
- package/dist/registry/component-registry-service.d.ts +4 -1
- package/dist/registry/component-registry-service.d.ts.map +1 -1
- package/dist/registry/component-registry-service.js +11 -4
- package/dist/registry/component-registry-service.js.map +1 -1
- package/dist/registry/component-registry.d.ts +7 -5
- package/dist/registry/component-registry.d.ts.map +1 -1
- package/dist/registry/component-registry.js +36 -22
- package/dist/registry/component-registry.js.map +1 -1
- package/dist/runtime.umd.js +3753 -2351
- package/package.json +6 -6
- package/src/component-manager/component-manager.ts +28 -12
- package/src/registry/component-registry-service.ts +18 -5
- package/src/registry/component-registry.ts +71 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/react-runtime",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.32.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.32.0",
|
|
33
|
+
"@memberjunction/global": "5.32.0",
|
|
34
|
+
"@memberjunction/interactive-component-types": "5.32.0",
|
|
35
|
+
"@memberjunction/core-entities": "5.32.0",
|
|
36
|
+
"@memberjunction/graphql-dataprovider": "5.32.0",
|
|
37
37
|
"@babel/standalone": "^7.29.1",
|
|
38
38
|
"rxjs": "^7.8.2"
|
|
39
39
|
},
|
|
@@ -115,8 +115,9 @@ export class ComponentManager {
|
|
|
115
115
|
// STEP 1: Check if already loaded in ComponentRegistry
|
|
116
116
|
const namespace = spec.namespace || options.defaultNamespace || 'Global';
|
|
117
117
|
const version = spec.version || options.defaultVersion || 'latest';
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const contentHash = this.calculateHash(spec);
|
|
119
|
+
|
|
120
|
+
const existing = this.registry.get(spec.name, namespace, version, contentHash);
|
|
120
121
|
if (existing && !options.forceRefresh && !options.forceRecompile) {
|
|
121
122
|
this.log(`Component found in registry: ${spec.name}`);
|
|
122
123
|
|
|
@@ -149,7 +150,7 @@ export class ComponentManager {
|
|
|
149
150
|
this.fetchCache.set(componentKey, {
|
|
150
151
|
spec: fullSpec,
|
|
151
152
|
fetchedAt: new Date(),
|
|
152
|
-
hash:
|
|
153
|
+
hash: this.calculateHash(fullSpec),
|
|
153
154
|
usageNotified: false
|
|
154
155
|
});
|
|
155
156
|
} catch (error) {
|
|
@@ -173,7 +174,7 @@ export class ComponentManager {
|
|
|
173
174
|
this.fetchCache.set(componentKey, {
|
|
174
175
|
spec: fullSpec,
|
|
175
176
|
fetchedAt: new Date(),
|
|
176
|
-
hash:
|
|
177
|
+
hash: this.calculateHash(fullSpec),
|
|
177
178
|
usageNotified: false
|
|
178
179
|
});
|
|
179
180
|
}
|
|
@@ -203,11 +204,16 @@ export class ComponentManager {
|
|
|
203
204
|
// STEP 5: Register in ComponentRegistry
|
|
204
205
|
if (!existing || options.forceRefresh || options.forceRecompile) {
|
|
205
206
|
this.log(`Registering component: ${spec.name}`);
|
|
207
|
+
// Register under the INPUT spec's hash so callers passing the same
|
|
208
|
+
// spec shape on subsequent loads hit the cache. If we keyed by the
|
|
209
|
+
// post-fetch fullSpec hash instead, registry-stub callers would
|
|
210
|
+
// never re-find the entry they just cached.
|
|
206
211
|
this.registry.register(
|
|
207
212
|
fullSpec.name,
|
|
208
213
|
compiledComponent,
|
|
209
214
|
namespace,
|
|
210
|
-
version
|
|
215
|
+
version,
|
|
216
|
+
contentHash
|
|
211
217
|
);
|
|
212
218
|
}
|
|
213
219
|
|
|
@@ -720,9 +726,14 @@ export class ComponentManager {
|
|
|
720
726
|
}
|
|
721
727
|
|
|
722
728
|
/**
|
|
723
|
-
* Calculate a
|
|
729
|
+
* Calculate a content fingerprint for a component spec. Used both as a
|
|
730
|
+
* cache-validation marker on `CacheEntry` and as part of the cache key so
|
|
731
|
+
* specs that share `(registry, namespace, name, version)` but carry
|
|
732
|
+
* different `code` (e.g. a registry-reference stub vs. an inline-code
|
|
733
|
+
* Studio export of the same artifact) do not collide on a single cache
|
|
734
|
+
* slot. Sync because callers use it to build keys in non-async paths.
|
|
724
735
|
*/
|
|
725
|
-
private
|
|
736
|
+
private calculateHash(spec: ComponentSpec): string {
|
|
726
737
|
// Simple hash based on spec content
|
|
727
738
|
const content = JSON.stringify({
|
|
728
739
|
name: spec.name,
|
|
@@ -730,25 +741,30 @@ export class ComponentManager {
|
|
|
730
741
|
code: spec.code,
|
|
731
742
|
libraries: spec.libraries
|
|
732
743
|
});
|
|
733
|
-
|
|
734
|
-
//
|
|
744
|
+
|
|
745
|
+
// FNV-style 32-bit integer hash
|
|
735
746
|
let hash = 0;
|
|
736
747
|
for (let i = 0; i < content.length; i++) {
|
|
737
748
|
const char = content.charCodeAt(i);
|
|
738
749
|
hash = ((hash << 5) - hash) + char;
|
|
739
|
-
hash = hash & hash;
|
|
750
|
+
hash = hash & hash;
|
|
740
751
|
}
|
|
741
752
|
return hash.toString(16);
|
|
742
753
|
}
|
|
743
754
|
|
|
744
755
|
/**
|
|
745
|
-
* Generate a unique key for a component
|
|
756
|
+
* Generate a unique key for a component. Includes a content fingerprint so
|
|
757
|
+
* specs that share `(registry, namespace, name, version)` but carry
|
|
758
|
+
* different `code` (e.g. a registry-reference stub vs. an inline-code
|
|
759
|
+
* Studio export of the same artifact) are cached as separate entries
|
|
760
|
+
* instead of clobbering each other on a single slot.
|
|
746
761
|
*/
|
|
747
762
|
private getComponentKey(spec: ComponentSpec, options: LoadOptions): string {
|
|
748
763
|
const registry = spec.registry || 'local';
|
|
749
764
|
const namespace = spec.namespace || options.defaultNamespace || 'Global';
|
|
750
765
|
const version = spec.version || options.defaultVersion || 'latest';
|
|
751
|
-
|
|
766
|
+
const hash = this.calculateHash(spec);
|
|
767
|
+
return `${registry}:${namespace}:${spec.name}:${version}#${hash}`;
|
|
752
768
|
}
|
|
753
769
|
|
|
754
770
|
/**
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
DependencyTree,
|
|
14
14
|
RegistryComponentMetadata
|
|
15
15
|
} from './registry-provider';
|
|
16
|
-
import { UserInfo, Metadata } from '@memberjunction/core';
|
|
16
|
+
import { UserInfo, Metadata, IMetadataProvider } from '@memberjunction/core';
|
|
17
17
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
18
18
|
import {
|
|
19
19
|
MJComponentEntity,
|
|
@@ -72,6 +72,19 @@ export class ComponentRegistryService {
|
|
|
72
72
|
private registryProviders = new Map<string, RegistryProvider>();
|
|
73
73
|
private debug: boolean = false;
|
|
74
74
|
private graphQLClient?: IComponentRegistryClient;
|
|
75
|
+
private _provider: IMetadataProvider | null = null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Optional metadata provider override. Callers should set
|
|
79
|
+
* `instance.Provider = providerToUse` before invoking registry methods
|
|
80
|
+
* in multi-provider contexts. Falls back to the global default provider when unset.
|
|
81
|
+
*/
|
|
82
|
+
public get Provider(): IMetadataProvider {
|
|
83
|
+
return this._provider ?? (new Metadata() as unknown as IMetadataProvider);
|
|
84
|
+
}
|
|
85
|
+
public set Provider(value: IMetadataProvider | null) {
|
|
86
|
+
this._provider = value;
|
|
87
|
+
}
|
|
75
88
|
|
|
76
89
|
private constructor(
|
|
77
90
|
compiler: ComponentCompiler,
|
|
@@ -153,7 +166,7 @@ export class ComponentRegistryService {
|
|
|
153
166
|
const client = new GraphQLComponentRegistryClient(provider as GraphQLDataProvider);
|
|
154
167
|
this.cachedProviderClient = client;
|
|
155
168
|
if (this.debug) {
|
|
156
|
-
console.log('📡 [ComponentRegistryService] Created GraphQL client from
|
|
169
|
+
console.log('📡 [ComponentRegistryService] Created GraphQL client from active metadata provider');
|
|
157
170
|
}
|
|
158
171
|
return client;
|
|
159
172
|
} catch (error) {
|
|
@@ -166,7 +179,7 @@ export class ComponentRegistryService {
|
|
|
166
179
|
} catch (error) {
|
|
167
180
|
// Provider might not be available in all environments
|
|
168
181
|
if (this.debug) {
|
|
169
|
-
console.log('⚠️ [ComponentRegistryService] Could not access
|
|
182
|
+
console.log('⚠️ [ComponentRegistryService] Could not access metadata provider:', error);
|
|
170
183
|
}
|
|
171
184
|
}
|
|
172
185
|
|
|
@@ -349,7 +362,7 @@ export class ComponentRegistryService {
|
|
|
349
362
|
// Get GraphQL client - use provided one or fallback to Metadata.Provider
|
|
350
363
|
const graphQLClient = await this.getGraphQLClient();
|
|
351
364
|
if (!graphQLClient) {
|
|
352
|
-
throw new Error('GraphQL client not available for external registry fetching. No client provided and
|
|
365
|
+
throw new Error('GraphQL client not available for external registry fetching. No client provided and the active metadata provider is not a GraphQLDataProvider.');
|
|
353
366
|
}
|
|
354
367
|
|
|
355
368
|
// Check if we have a cached version first
|
|
@@ -585,7 +598,7 @@ export class ComponentRegistryService {
|
|
|
585
598
|
contextUser?: UserInfo
|
|
586
599
|
): Promise<void> {
|
|
587
600
|
// Get the actual entity object to save
|
|
588
|
-
const md =
|
|
601
|
+
const md = this.Provider;
|
|
589
602
|
const componentEntity = await md.GetEntityObject<MJComponentEntity>('MJ: Components', contextUser);
|
|
590
603
|
|
|
591
604
|
// Load the existing component
|
|
@@ -53,6 +53,11 @@ export class ComponentRegistry {
|
|
|
53
53
|
* @param component - Compiled component object
|
|
54
54
|
* @param namespace - Component namespace (default: 'Global')
|
|
55
55
|
* @param version - Component version (default: 'v1')
|
|
56
|
+
* @param contentHash - Optional content fingerprint. When provided, the entry
|
|
57
|
+
* is keyed by `(name, namespace, version, contentHash)` so multiple specs
|
|
58
|
+
* that share `(name, namespace, version)` but carry different `code` (e.g.
|
|
59
|
+
* a registry-reference stub vs. an inline-code Studio export of the same
|
|
60
|
+
* artifact) coexist in the cache instead of clobbering each other.
|
|
56
61
|
* @param tags - Optional tags for categorization
|
|
57
62
|
* @returns The registered component's metadata
|
|
58
63
|
*/
|
|
@@ -61,10 +66,11 @@ export class ComponentRegistry {
|
|
|
61
66
|
component: ComponentObject,
|
|
62
67
|
namespace: string = 'Global',
|
|
63
68
|
version: string = 'v1',
|
|
69
|
+
contentHash?: string,
|
|
64
70
|
tags?: string[]
|
|
65
71
|
): ComponentMetadata {
|
|
66
|
-
const id = this.generateRegistryKey(name, namespace, version);
|
|
67
|
-
|
|
72
|
+
const id = this.generateRegistryKey(name, namespace, version, contentHash);
|
|
73
|
+
|
|
68
74
|
// Create metadata
|
|
69
75
|
const metadata: ComponentMetadata = {
|
|
70
76
|
id,
|
|
@@ -99,12 +105,14 @@ export class ComponentRegistry {
|
|
|
99
105
|
* @param name - Component name
|
|
100
106
|
* @param namespace - Component namespace
|
|
101
107
|
* @param version - Component version
|
|
108
|
+
* @param contentHash - Optional content fingerprint. When provided, looks up
|
|
109
|
+
* the exact `(name, namespace, version, contentHash)` entry. When omitted,
|
|
110
|
+
* falls back to the most recently registered entry matching the other keys
|
|
111
|
+
* (existing behavior).
|
|
102
112
|
* @returns The component object if found, undefined otherwise
|
|
103
113
|
*/
|
|
104
|
-
get(name: string, namespace: string = 'Global', version?: string): ComponentObject | undefined {
|
|
105
|
-
const id = version
|
|
106
|
-
? this.generateRegistryKey(name, namespace, version)
|
|
107
|
-
: this.findLatestVersion(name, namespace);
|
|
114
|
+
get(name: string, namespace: string = 'Global', version?: string, contentHash?: string): ComponentObject | undefined {
|
|
115
|
+
const id = this.resolveLookupKey(name, namespace, version, contentHash);
|
|
108
116
|
|
|
109
117
|
if (!id) return undefined;
|
|
110
118
|
|
|
@@ -124,13 +132,11 @@ export class ComponentRegistry {
|
|
|
124
132
|
* @param name - Component name
|
|
125
133
|
* @param namespace - Component namespace
|
|
126
134
|
* @param version - Component version
|
|
135
|
+
* @param contentHash - Optional content fingerprint (see {@link get})
|
|
127
136
|
* @returns true if the component exists
|
|
128
137
|
*/
|
|
129
|
-
has(name: string, namespace: string = 'Global', version?: string): boolean {
|
|
130
|
-
const id = version
|
|
131
|
-
? this.generateRegistryKey(name, namespace, version)
|
|
132
|
-
: this.findLatestVersion(name, namespace);
|
|
133
|
-
|
|
138
|
+
has(name: string, namespace: string = 'Global', version?: string, contentHash?: string): boolean {
|
|
139
|
+
const id = this.resolveLookupKey(name, namespace, version, contentHash);
|
|
134
140
|
return id ? this.registry.has(id) : false;
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -139,15 +145,12 @@ export class ComponentRegistry {
|
|
|
139
145
|
* @param name - Component name
|
|
140
146
|
* @param namespace - Component namespace
|
|
141
147
|
* @param version - Component version
|
|
148
|
+
* @param contentHash - Optional content fingerprint (see {@link get})
|
|
142
149
|
* @returns true if the component was removed
|
|
143
150
|
*/
|
|
144
|
-
unregister(name: string, namespace: string = 'Global', version?: string): boolean {
|
|
145
|
-
const id = version
|
|
146
|
-
? this.generateRegistryKey(name, namespace, version)
|
|
147
|
-
: this.findLatestVersion(name, namespace);
|
|
148
|
-
|
|
151
|
+
unregister(name: string, namespace: string = 'Global', version?: string, contentHash?: string): boolean {
|
|
152
|
+
const id = this.resolveLookupKey(name, namespace, version, contentHash);
|
|
149
153
|
if (!id) return false;
|
|
150
|
-
|
|
151
154
|
return this.registry.delete(id);
|
|
152
155
|
}
|
|
153
156
|
|
|
@@ -222,12 +225,10 @@ export class ComponentRegistry {
|
|
|
222
225
|
* @param name - Component name
|
|
223
226
|
* @param namespace - Component namespace
|
|
224
227
|
* @param version - Component version
|
|
228
|
+
* @param contentHash - Optional content fingerprint (see {@link get})
|
|
225
229
|
*/
|
|
226
|
-
release(name: string, namespace: string = 'Global', version?: string): void {
|
|
227
|
-
const id = version
|
|
228
|
-
? this.generateRegistryKey(name, namespace, version)
|
|
229
|
-
: this.findLatestVersion(name, namespace);
|
|
230
|
-
|
|
230
|
+
release(name: string, namespace: string = 'Global', version?: string, contentHash?: string): void {
|
|
231
|
+
const id = this.resolveLookupKey(name, namespace, version, contentHash);
|
|
231
232
|
if (!id) return;
|
|
232
233
|
|
|
233
234
|
const entry = this.registry.get(id);
|
|
@@ -352,17 +353,55 @@ export class ComponentRegistry {
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
/**
|
|
355
|
-
* Generates a unique registry key
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
|
|
359
|
-
|
|
356
|
+
* Generates a unique registry key. When `contentHash` is supplied, it is
|
|
357
|
+
* appended so two specs that share `(name, namespace, version)` but have
|
|
358
|
+
* different code body don't collide.
|
|
359
|
+
*/
|
|
360
|
+
private generateRegistryKey(name: string, namespace: string, version: string, contentHash?: string): string {
|
|
361
|
+
const base = this.config.enableNamespaces ? `${namespace}::${name}@${version}` : `${name}@${version}`;
|
|
362
|
+
return contentHash ? `${base}#${contentHash}` : base;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Resolves a lookup to an exact internal key. Centralizes the four
|
|
367
|
+
* lookup-shape variants used by `get`, `has`, `unregister`, and `release`:
|
|
368
|
+
*
|
|
369
|
+
* - hash + version → exact hash-suffixed key
|
|
370
|
+
* - version only → latest entry matching `(name, namespace, version)` across all hashes
|
|
371
|
+
* - hash only → no version → fall back to latest version (hash isn't enough alone)
|
|
372
|
+
* - neither → latest entry matching `(name, namespace)`
|
|
373
|
+
*/
|
|
374
|
+
private resolveLookupKey(name: string, namespace: string, version?: string, contentHash?: string): string | undefined {
|
|
375
|
+
if (version && contentHash) {
|
|
376
|
+
return this.generateRegistryKey(name, namespace, version, contentHash);
|
|
377
|
+
}
|
|
378
|
+
if (version) {
|
|
379
|
+
return this.findLatestForVersion(name, namespace, version);
|
|
380
|
+
}
|
|
381
|
+
return this.findLatestVersion(name, namespace);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Find the most recently registered entry whose metadata matches
|
|
386
|
+
* `(name, namespace, version)`. Different content hashes for the same
|
|
387
|
+
* version are scanned and the newest by `registeredAt` wins.
|
|
360
388
|
*/
|
|
361
|
-
private
|
|
362
|
-
|
|
363
|
-
|
|
389
|
+
private findLatestForVersion(name: string, namespace: string, version: string): string | undefined {
|
|
390
|
+
let latestKey: string | undefined;
|
|
391
|
+
let latestDate: Date | undefined;
|
|
392
|
+
|
|
393
|
+
for (const [key, entry] of this.registry) {
|
|
394
|
+
if (entry.metadata.name === name &&
|
|
395
|
+
entry.metadata.namespace === namespace &&
|
|
396
|
+
entry.metadata.version === version) {
|
|
397
|
+
if (!latestDate || entry.metadata.registeredAt > latestDate) {
|
|
398
|
+
latestDate = entry.metadata.registeredAt;
|
|
399
|
+
latestKey = key;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
364
402
|
}
|
|
365
|
-
|
|
403
|
+
|
|
404
|
+
return latestKey;
|
|
366
405
|
}
|
|
367
406
|
|
|
368
407
|
/**
|
|
@@ -376,7 +415,7 @@ export class ComponentRegistry {
|
|
|
376
415
|
let latestDate: Date | undefined;
|
|
377
416
|
|
|
378
417
|
for (const [key, entry] of this.registry) {
|
|
379
|
-
if (entry.metadata.name === name &&
|
|
418
|
+
if (entry.metadata.name === name &&
|
|
380
419
|
entry.metadata.namespace === namespace) {
|
|
381
420
|
if (!latestDate || entry.metadata.registeredAt > latestDate) {
|
|
382
421
|
latestDate = entry.metadata.registeredAt;
|