From f1791b1d762834fc25b0b26a368f41180094b881 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 13 Sep 2022 23:08:57 -0500 Subject: [PATCH] Add push$ to Collection; make Container listen for retroactive blueprint changes --- package.json | 2 +- src/di/Container.ts | 28 +++++++++++++++++++- src/di/ContainerBlueprint.ts | 43 ++++++++++++++++++++++++++----- src/di/decorator/injection.ts | 1 + src/util/collection/Collection.ts | 29 +++++++++++++++++++++ 5 files changed, 94 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 817c8be..c0c00e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@extollo/lib", - "version": "0.14.2", + "version": "0.14.3", "description": "The framework library that lifts up your code.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/di/Container.ts b/src/di/Container.ts index 92278d6..9a9b8ee 100644 --- a/src/di/Container.ts +++ b/src/di/Container.ts @@ -14,7 +14,7 @@ import { Collection, globalRegistry, hasOwnProperty, - logIfDebugging, + logIfDebugging, Unsubscribe, } from '../util' import {ErrorWithContext, withErrorContext} from '../util/error/ErrorWithContext' import {Factory} from './factory/Factory' @@ -100,6 +100,8 @@ export class Container { .then(value => listener.callback(value)) }) + container.subscribeToBlueprintChanges(ContainerBlueprint.getContainerBlueprint()) + return container } @@ -153,11 +155,33 @@ export class Container { */ protected waitingLifecycleCallbacks: Collection> = new Collection() + /** + * Collection of subscriptions to ContainerBlueprint events. + * We keep this around so we can remove the subscriptions when the container is destroyed. + * @protected + */ + protected blueprintSubscribers: Collection = new Collection() + constructor() { this.registerSingletonInstance(Container, this) this.registerSingleton('injector', this) } + /** Make the container listen to changes in the given blueprint. */ + private subscribeToBlueprintChanges(blueprint: ContainerBlueprint): void { + this.blueprintSubscribers.push( + blueprint.resolve$(factory => this.registerFactory(factory())), + ) + + this.blueprintSubscribers.push( + blueprint.resolveConstructable$(factoryClass => this.registerFactory(this.make(factoryClass))), + ) + + this.blueprintSubscribers.push( + blueprint.resolveResolutionCallbacks$(listener => this.onResolve(listener.key).then(value => listener.callback(value))), + ) + } + /** * Purge all factories and instances of the given key from this container. * @param key @@ -622,6 +646,8 @@ export class Container { * Perform any cleanup necessary to destroy this container instance. */ destroy(): void { + this.blueprintSubscribers.mapCall('unsubscribe') + this.waitingLifecycleCallbacks .mapCall('deref') .whereDefined() diff --git a/src/di/ContainerBlueprint.ts b/src/di/ContainerBlueprint.ts index 1bebb74..a6b6659 100644 --- a/src/di/ContainerBlueprint.ts +++ b/src/di/ContainerBlueprint.ts @@ -3,6 +3,8 @@ import NamedFactory from './factory/NamedFactory' import {AbstractFactory} from './factory/AbstractFactory' import {Factory} from './factory/Factory' import {ClosureFactory} from './factory/ClosureFactory' +import {Collection, collect} from '../util/collection/Collection' +import {Subscription, Unsubscribe} from '../util/support/BehaviorSubject' /** Simple type alias for a callback to a container's onResolve method. */ export type ContainerResolutionCallback = (() => unknown) | ((t: T) => unknown) @@ -25,11 +27,11 @@ export class ContainerBlueprint { return this.instance } - protected factories: (() => AbstractFactory)[] = [] + protected factories: Collection<(() => AbstractFactory)> = collect() - protected constructableFactories: StaticClass, any>[] = [] + protected constructableFactories: Collection, any>> = collect() - protected resolutionCallbacks: ({key: TypedDependencyKey, callback: ContainerResolutionCallback})[] = [] + protected resolutionCallbacks: Collection<{key: TypedDependencyKey, callback: ContainerResolutionCallback}> = collect() /** * Register some factory class with the container. Should take no construction params. @@ -74,7 +76,16 @@ export class ContainerBlueprint { * Get an array of factory instances in the blueprint. */ resolve(): AbstractFactory[] { - return this.factories.map(x => x()) + return this.factories.map(x => x()).all() + } + + /** + * Subscribe to new factories being registered. + * Used by `Container` implementations to listen for factories being registered after the container is realized. + * @param sub + */ + resolve$(sub: Subscription<() => AbstractFactory>): Unsubscribe { + return this.factories.push$(sub) } /** @@ -94,14 +105,32 @@ export class ContainerBlueprint { * Get an array of static Factory classes that need to be instantiated by * the container itself. */ - resolveConstructable(): StaticClass, any> { - return [...this.constructableFactories] + resolveConstructable(): StaticClass, any>[] { + return this.constructableFactories.all() + } + + /** + * Subscribe to new constructable factories being registered. + * Used by `Container` implementations to listen for factories registered after the container is realized. + * @param sub + */ + resolveConstructable$(sub: Subscription, any>>): Unsubscribe { + return this.constructableFactories.push$(sub) } /** * Get an array of DependencyKey-callback pairs to register with new containers. */ resolveResolutionCallbacks(): ({key: TypedDependencyKey, callback: ContainerResolutionCallback})[] { - return [...this.resolutionCallbacks] + return this.resolutionCallbacks.all() + } + + /** + * Subscribe to new resolution callbacks being registered. + * Used by `Container` implementations to listen for callbacks registered after the container is realized. + * @param sub + */ + resolveResolutionCallbacks$(sub: Subscription<{key: TypedDependencyKey, callback: ContainerResolutionCallback}>): Unsubscribe { + return this.resolutionCallbacks.push$(sub) } } diff --git a/src/di/decorator/injection.ts b/src/di/decorator/injection.ts index ccebbbb..8682018 100644 --- a/src/di/decorator/injection.ts +++ b/src/di/decorator/injection.ts @@ -166,6 +166,7 @@ export const Singleton = (name?: string): ClassDecorator => { ...(name ? { name } : {}), } + logIfDebugging('extollo.di.singleton', 'Registering singleton target:', target, 'injectionType:', injectionType) Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target) Injectable()(target) diff --git a/src/util/collection/Collection.ts b/src/util/collection/Collection.ts index 6cf13a3..19a9de9 100644 --- a/src/util/collection/Collection.ts +++ b/src/util/collection/Collection.ts @@ -1,4 +1,5 @@ import {AsyncPipe, Pipeline} from '../support/Pipe' +import {Unsubscribe, Subscription} from '../support/BehaviorSubject' type CollectionItem = T type MaybeCollectionItem = CollectionItem | undefined @@ -50,6 +51,8 @@ export { class Collection { private storedItems: CollectionItem[] = [] + private pushSubscribers: Subscription[] = [] + /** * Create a new collection from an array of items. * @param items @@ -966,6 +969,7 @@ class Collection { */ prepend(item: CollectionItem): Collection { this.storedItems = [item, ...this.storedItems] + this.callPushSubscribers(item) return this } @@ -975,9 +979,33 @@ class Collection { */ push(item: CollectionItem): Collection { this.storedItems.push(item) + this.callPushSubscribers(item) return this } + /** + * Subscribe to listen for items being added to the collection. + * @param sub + */ + push$(sub: Subscription): Unsubscribe { + this.pushSubscribers.push(sub) + return { + unsubscribe: () => this.pushSubscribers = this.pushSubscribers.filter(x => x !== sub), + } + } + + /** Helper to notify subscribers that an item has been pushed to the collection. */ + private callPushSubscribers(item: T): void { + this.pushSubscribers + .forEach(sub => { + if ( typeof sub === 'object' ) { + sub?.next?.(item) + } else { + sub(item) + } + }) + } + /** * Push the given items to the end of this collection. * Unlike `merge()`, this mutates the current collection's items. @@ -987,6 +1015,7 @@ class Collection { const concats = items instanceof Collection ? items.all() : items for ( const item of concats ) { this.storedItems.push(item) + this.callPushSubscribers(item) } return this }