@feature-hub/core
Version:
Create scalable web applications using micro frontends.
203 lines • 9.31 kB
JavaScript
import semverCoerce from 'semver/functions/coerce';
import semverSatisfies from 'semver/functions/satisfies';
import semverValid from 'semver/functions/valid';
import * as Messages from './internal/feature-service-registry-messages';
import { toposortDependencies, } from './internal/toposort-dependencies';
function mergeFeatureServiceDependencies({ dependencies, optionalDependencies, }) {
return Object.assign(Object.assign({}, (dependencies && dependencies.featureServices)), (optionalDependencies && optionalDependencies.featureServices));
}
function createDependencyGraph(definitions) {
const dependencyGraph = new Map();
for (const definition of definitions) {
dependencyGraph.set(definition.id, mergeFeatureServiceDependencies(definition));
}
return dependencyGraph;
}
function isOptionalFeatureServiceDependency({ optionalDependencies }, providerId) {
return Boolean(optionalDependencies &&
optionalDependencies.featureServices &&
optionalDependencies.featureServices.hasOwnProperty(providerId));
}
function createProviderDefinitionsById(definitions) {
const providerDefinitionsById = new Map();
for (const definition of definitions) {
providerDefinitionsById.set(definition.id, definition);
}
return providerDefinitionsById;
}
/**
* The FeatureServiceRegistry provides Feature Services to dependent consumers.
* The integrator should instantiate a singleton instance of the registry.
*/
export class FeatureServiceRegistry {
constructor(options = {}) {
this.options = options;
this.sharedFeatureServices = new Map();
this.consumerIds = new Set();
this.logger = options.logger || console;
}
/**
* Register a set of Feature Services to make them available for binding to
* dependent consumers.
*
* @throws Throws an error if the dependencies of one of the provider
* definitions can't be fulfilled.
* @throws Throws an error if one of the registered Feature Services contains
* an invalid version according to semver notation.
*
* @param providerDefinitions Feature Services that should be registered. A
* Feature Service and its dependencies must either be registered together, or
* the dependencies must have already been registered. It is not possible to
* provide dependencies later. Sorting the provided definitions is not
* necessary, since the registry takes care of registering the given
* definitions in the correct order.
* @param registrantId The ID of the entity that registers the provider
* definitions.
*/
registerFeatureServices(providerDefinitions, registrantId) {
const providerDefinitionsById = createProviderDefinitionsById(providerDefinitions);
const dependencyGraph = createDependencyGraph(providerDefinitions);
for (const providerId of toposortDependencies(dependencyGraph)) {
this.registerFeatureService(providerDefinitionsById, providerId, registrantId);
}
}
/**
* Bind all dependencies to a consumer.
*
* @throws Throws an error if non-optional dependencies can't be fulfilled.
* @throws Throws an error if called with the same consumer ID more than once.
*
* @param consumerDefinition The definition of the consumer to which
* dependencies should be bound.
* @param consumerId The ID of the consumer to which dependencies should be
* bound.
* @param consumerName The name of the consumer to which dependencies should be
* bound.
*/
bindFeatureServices(consumerDefinition, consumerId, consumerName) {
if (this.consumerIds.has(consumerId)) {
throw new Error(Messages.featureServicesAlreadyBound(consumerId));
}
const bindings = new Map();
const featureServices = Object.create(null);
const allDependencies = mergeFeatureServiceDependencies(consumerDefinition);
for (const providerId of Object.keys(allDependencies)) {
const optional = isOptionalFeatureServiceDependency(consumerDefinition, providerId);
const versionRange = allDependencies[providerId];
const binding = this.bindFeatureService(providerId, consumerId, consumerName, versionRange, { optional });
if (!binding) {
continue;
}
this.logger.info(Messages.featureServiceSuccessfullyBound(providerId, consumerId));
bindings.set(providerId, binding);
featureServices[providerId] = binding.featureService;
}
this.consumerIds.add(consumerId);
let unbound = false;
const unbind = () => {
if (unbound) {
throw new Error(Messages.featureServicesAlreadyUnbound(consumerId));
}
unbound = true;
this.consumerIds.delete(consumerId);
for (const [providerId, binding] of bindings.entries()) {
try {
if (binding.unbind) {
binding.unbind();
}
this.logger.info(Messages.featureServiceSuccessfullyUnbound(providerId, consumerId));
}
catch (error) {
this.logger.error(Messages.featureServiceCouldNotBeUnbound(providerId, consumerId), error);
}
}
};
return { featureServices, unbind };
}
/**
* Returns info about consumers and registered feature services.
*/
getInfo() {
return {
consumerIds: Array.from(this.consumerIds),
featureServices: [...this.sharedFeatureServices.entries()].map(([id, sharedFeatureService]) => ({
id,
versions: Object.keys(sharedFeatureService),
})),
};
}
registerFeatureService(providerDefinitionsById, providerId, registrantId) {
const providerDefinition = providerDefinitionsById.get(providerId);
if (!providerDefinition) {
return;
}
if (this.sharedFeatureServices.has(providerId)) {
this.logger.warn(Messages.featureServiceAlreadyRegistered(providerId, registrantId));
return;
}
this.validateExternals(providerDefinition, providerId);
const { featureServices } = this.bindFeatureServices(providerDefinition, providerId);
const sharedFeatureService = providerDefinition.create({ featureServices });
if (sharedFeatureService) {
this.validateFeatureServiceVersions(sharedFeatureService, providerId, registrantId);
this.sharedFeatureServices.set(providerId, sharedFeatureService);
this.logger.info(Messages.featureServiceSuccessfullyRegistered(providerId, registrantId));
}
else {
this.logger.info(Messages.featureServiceReturnedUndefined(providerId, registrantId));
}
}
bindFeatureService(providerId, consumerId, consumerName, versionRange, { optional }) {
const coercedVersion = versionRange && semverCoerce(versionRange);
if (!coercedVersion) {
const message = Messages.featureServiceDependencyVersionInvalid(optional, providerId, consumerId);
if (optional) {
this.logger.info(message);
return;
}
throw new Error(message);
}
const caretRange = `^${coercedVersion.version}`;
const sharedFeatureService = this.sharedFeatureServices.get(providerId);
if (!sharedFeatureService) {
const message = Messages.featureServiceNotRegistered(optional, providerId, consumerId);
if (optional) {
this.logger.info(message);
return;
}
throw new Error(message);
}
const supportedVersions = Object.keys(sharedFeatureService);
const version = supportedVersions.find((supportedVersion) => semverSatisfies(supportedVersion, caretRange));
const bindFeatureService = version
? sharedFeatureService[version]
: undefined;
if (!bindFeatureService) {
const message = Messages.featureServiceUnsupported(optional, providerId, consumerId, caretRange, supportedVersions);
if (optional) {
this.logger.info(message);
return;
}
throw new Error(message);
}
return bindFeatureService(consumerId, consumerName);
}
validateExternals(consumerDefinition, consumerId) {
const { externalsValidator } = this.options;
if (!externalsValidator) {
return;
}
const { dependencies } = consumerDefinition;
if (dependencies && dependencies.externals) {
externalsValidator.validate(dependencies.externals, consumerId);
}
}
validateFeatureServiceVersions(sharedFeatureService, providerId, registrantId) {
for (const version of Object.keys(sharedFeatureService)) {
if (!semverValid(version)) {
throw new Error(Messages.featureServiceVersionInvalid(providerId, registrantId, version));
}
}
}
}
//# sourceMappingURL=feature-service-registry.js.map