UNPKG

botbuilder-dialogs-adaptive-runtime-core

Version:

Bot Framework Adaptive Dialogs runtime core components

300 lines (257 loc) 11 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import assert from 'assert'; import { DepGraph } from 'dependency-graph'; import { ok } from 'assert'; import { stringify } from './util'; /** * Factory describes a generic factory function signature. The type is generic over a few parameters: * * @template Type type the factory produces * @template Initial true if the `initialValue` passed to the factory must be defined */ export type Factory<Type, Initial extends boolean> = ( initialValue: Initial extends true ? Type : Type | undefined ) => Type; /** * DependencyFactory is a function signature that produces an instance that depends on a set of * other services. The type is generic over a few parameters: * * @template Type type the factory produces * @template Dependencies the services this factory function depends on * @template Initial true if the `initialValue` passed to the factory must be defined */ export type DependencyFactory<Type, Dependencies, Initial extends boolean> = ( dependencies: Dependencies, initialValue: Initial extends true ? Type : Type | undefined ) => Type; /** * ServiceCollection is an interface that describes a set of methods to register services. This, in a lighter way, * mimics the .NET dependency injection service collection functionality, except for instances rather than types. */ export class ServiceCollection { // We store the full set of dependencies as a workaround to the fact that `DepGraph` throws an error if you // attempt to register a dependency to a node that does not yet exist. private readonly dependencies = new Map<string, string[]>(); /** * `DepGraph` is a dependency graph data structure. In our case, the services we support are encoded as a * dependency graph where nodes are named with a key and store a list of factory methods. */ private readonly graph = new DepGraph<Array<DependencyFactory<unknown, Record<string, unknown>, true>>>(); /** * Cache constructed instances for reuse */ private cache: Record<string, unknown> = {}; /** * Construct a Providers instance * * @template S services interface * @param defaultServices default set of services */ constructor(defaultServices: Record<string, unknown> = {}) { Object.entries(defaultServices).forEach(([key, instance]) => { this.addInstance(key, instance); }); } /** * Register an instance by key. This will overwrite existing instances. * * @param key key of the instance being provided * @param instance instance to provide * @returns this for chaining */ addInstance<InstanceType>(key: string, instance: InstanceType): this { this.graph.addNode(key, [() => instance]); return this; } /** * Register a factory for a key. * * @param key key that factory will provide * @param factory function that creates an instance to provide * @returns this for chaining */ addFactory<InstanceType>(key: string, factory: Factory<InstanceType, false>): this; /** * Register a factory for a key with a set of dependencies. * * @param key key that factory will provide * @param dependencies set of things this instance depends on. Will be provided to factory function via `services`. * @param factory function that creates an instance to provide * @returns this for chaining */ addFactory<InstanceType, Dependencies>( key: string, dependencies: string[], factory: DependencyFactory<InstanceType, Dependencies, false> ): this; /** * @internal */ addFactory<InstanceType, Dependencies>( key: string, depsOrFactory: string[] | Factory<InstanceType, false>, maybeFactory?: DependencyFactory<InstanceType, Dependencies, false> ): this { const dependencies = Array.isArray(depsOrFactory) ? depsOrFactory : undefined; let factory: DependencyFactory<InstanceType, Dependencies, false> | undefined = maybeFactory; if (!factory && typeof depsOrFactory === 'function') { factory = (_services, value) => depsOrFactory(value); } // Asserts factory is not undefined ok(factory, 'illegal invocation with undefined factory'); if (dependencies) { this.dependencies.set(key, dependencies); } // If the graph already has this key, fetch its data and remove it (to be replaced) let factories: unknown[] = []; if (this.graph.hasNode(key)) { factories = this.graph.getNodeData(key); this.graph.removeNode(key); } // Note: we have done the type checking above, so disabling no-explicit-any is okay. // eslint-disable-next-line @typescript-eslint/no-explicit-any this.graph.addNode(key, factories.concat(factory) as any); return this; } /** * Register a factory (that expects the initial value that is not undefined) for a key. * * @param key key of the instance being provided * @param instance instance to provide * @returns this for chaining */ composeFactory<InstanceType>(key: string, factory: Factory<InstanceType, true>): this; /** * Register a factory (that expects an initial value that is not undefined) for a key * with a set of dependencies. * * @param key key that factory will provide * @param dependencies set of things this instance depends on. Will be provided to factory function via `services`. * @param factory function that creates an instance to provide * @returns this for chaining */ composeFactory<InstanceType, Dependencies>( key: string, dependencies: string[], factory: DependencyFactory<InstanceType, Dependencies, true> ): this; /** * @internal */ composeFactory<InstanceType, Dependencies>( key: string, depsOrFactory: string[] | Factory<InstanceType, true>, maybeFactory?: DependencyFactory<InstanceType, Dependencies, true> ): this { if (maybeFactory) { return this.addFactory<InstanceType, Dependencies>( key, Array.isArray(depsOrFactory) ? depsOrFactory : [], (dependencies, value) => { ok(value, `unable to create ${key}, initial value undefined`); return maybeFactory(dependencies, value); } ); } else { ok(typeof depsOrFactory === 'function', 'illegal invocation with undefined factory'); return this.addFactory<InstanceType>(key, (value) => { ok(value, `unable to create ${key}, initial value undefined`); return depsOrFactory(value); }); } } // Register dependencies and then build nodes. Note: `nodes` is a function because ordering may // depend on results of dependency registration private buildNodes<ReturnType = Record<string, unknown>>( generateNodes: () => string[], reuseServices: Record<string, unknown> = {} ): ReturnType { // Consume all dependencies and then reset so updating registrations without re-registering // dependencies works this.dependencies.forEach((dependencies, node) => dependencies.forEach((dependency) => this.graph.addDependency(node, stringify(dependency))) ); // Generate nodes after registering dependencies so ordering is correct const nodes = generateNodes(); const services = nodes.reduce((services, service) => { // Extra precaution if (!this.graph.hasNode(service)) { return services; } // Helper to generate return value const assignValue = (value: unknown) => ({ ...services, [service]: value, }); // Optionally reuse existing service const reusedService = reuseServices[service]; if (reusedService !== undefined) { return assignValue(reusedService); } // Each node stores a list of factory methods. const factories = this.graph.getNodeData(service); // Produce the instance by reducing those factories, passing the instance along for composition. const instance = factories.reduce((value, factory) => factory(services, value), <unknown>services[service]); return assignValue(instance); }, <Record<string, unknown>>{}); // Cache results for subsequent invocations that may desire pre-constructed instances Object.assign(this.cache, services); return services as ReturnType; } /** * Build a single service. * * @param key service to build * @param deep reconstruct all dependencies * @returns the service instance, or undefined */ makeInstance<InstanceType = unknown>(key: string, deep = false): InstanceType | undefined { // If this is not a deep reconstruction, reuse any services that `key` depends on let initialServices: Record<string, unknown> | undefined; if (!deep) { const { [key]: _, ...cached } = this.cache; initialServices = cached; } const services = this.buildNodes<Record<string, InstanceType | undefined>>( () => this.graph.dependenciesOf(key).concat(key), initialServices ); return services[key]; } /** * Build a single service and assert that it is not undefined. * * @param key service to build * @param deep reconstruct all dependencies * @returns the service instance */ mustMakeInstance<InstanceType = unknown>(key: string, deep = false): InstanceType { const instance = this.makeInstance<InstanceType>(key, deep); assert.ok(instance, `\`${key}\` instance undefined!`); return instance; } /** * Build the full set of services. * * @returns all resolved services */ makeInstances<InstancesType>(): InstancesType { return this.buildNodes<InstancesType>(() => this.graph.overallOrder()); } /** * Build the full set of services, asserting that the specified keys are not undefined. * * @param keys instances that must be not undefined * @returns all resolve services */ mustMakeInstances<InstancesType extends Record<string, unknown> = Record<string, unknown>>( ...keys: string[] ): InstancesType { const instances = this.makeInstances<InstancesType>(); keys.forEach((key) => { assert.ok(instances[key], `\`${key}\` instance undefined!`); }); return instances; } }