UNPKG

@feature-hub/core

Version:

Create scalable web applications using micro frontends.

233 lines 10.8 kB
"use strict"; 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