@feature-hub/core
Version:
Create scalable web applications using micro frontends.
233 lines • 10.8 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FeatureServiceRegistry = void 0;
const coerce_1 = __importDefault(require("semver/functions/coerce"));
const satisfies_1 = __importDefault(require("semver/functions/satisfies"));
const valid_1 = __importDefault(require("semver/functions/valid"));
const Messages = __importStar(require("./internal/feature-service-registry-messages"));
const toposort_dependencies_1 = require("./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.
*/
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 (0, toposort_dependencies_1.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 && (0, coerce_1.default)(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) => (0, satisfies_1.default)(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 (!(0, valid_1.default)(version)) {
throw new Error(Messages.featureServiceVersionInvalid(providerId, registrantId, version));
}
}
}
}
exports.FeatureServiceRegistry = FeatureServiceRegistry;
//# sourceMappingURL=feature-service-registry.js.map
;