Add push$ to Collection; make Container listen for retroactive blueprint changes

This commit is contained in:
Garrett Mills 2022-09-13 23:08:57 -05:00
parent a173393697
commit f1791b1d76
5 changed files with 94 additions and 9 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@extollo/lib", "name": "@extollo/lib",
"version": "0.14.2", "version": "0.14.3",
"description": "The framework library that lifts up your code.", "description": "The framework library that lifts up your code.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@ -14,7 +14,7 @@ import {
Collection, Collection,
globalRegistry, globalRegistry,
hasOwnProperty, hasOwnProperty,
logIfDebugging, logIfDebugging, Unsubscribe,
} from '../util' } from '../util'
import {ErrorWithContext, withErrorContext} from '../util/error/ErrorWithContext' import {ErrorWithContext, withErrorContext} from '../util/error/ErrorWithContext'
import {Factory} from './factory/Factory' import {Factory} from './factory/Factory'
@ -100,6 +100,8 @@ export class Container {
.then(value => listener.callback(value)) .then(value => listener.callback(value))
}) })
container.subscribeToBlueprintChanges(ContainerBlueprint.getContainerBlueprint())
return container return container
} }
@ -153,11 +155,33 @@ export class Container {
*/ */
protected waitingLifecycleCallbacks: Collection<WeakRef<AwareOfContainerLifecycle>> = new Collection() protected waitingLifecycleCallbacks: Collection<WeakRef<AwareOfContainerLifecycle>> = 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<Unsubscribe> = new Collection()
constructor() { constructor() {
this.registerSingletonInstance<Container>(Container, this) this.registerSingletonInstance<Container>(Container, this)
this.registerSingleton('injector', 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. * Purge all factories and instances of the given key from this container.
* @param key * @param key
@ -622,6 +646,8 @@ export class Container {
* Perform any cleanup necessary to destroy this container instance. * Perform any cleanup necessary to destroy this container instance.
*/ */
destroy(): void { destroy(): void {
this.blueprintSubscribers.mapCall('unsubscribe')
this.waitingLifecycleCallbacks this.waitingLifecycleCallbacks
.mapCall('deref') .mapCall('deref')
.whereDefined() .whereDefined()

View File

@ -3,6 +3,8 @@ import NamedFactory from './factory/NamedFactory'
import {AbstractFactory} from './factory/AbstractFactory' import {AbstractFactory} from './factory/AbstractFactory'
import {Factory} from './factory/Factory' import {Factory} from './factory/Factory'
import {ClosureFactory} from './factory/ClosureFactory' 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. */ /** Simple type alias for a callback to a container's onResolve method. */
export type ContainerResolutionCallback<T> = (() => unknown) | ((t: T) => unknown) export type ContainerResolutionCallback<T> = (() => unknown) | ((t: T) => unknown)
@ -25,11 +27,11 @@ export class ContainerBlueprint {
return this.instance return this.instance
} }
protected factories: (() => AbstractFactory<any>)[] = [] protected factories: Collection<(() => AbstractFactory<any>)> = collect()
protected constructableFactories: StaticClass<AbstractFactory<any>, any>[] = [] protected constructableFactories: Collection<StaticClass<AbstractFactory<any>, any>> = collect()
protected resolutionCallbacks: ({key: TypedDependencyKey<any>, callback: ContainerResolutionCallback<any>})[] = [] protected resolutionCallbacks: Collection<{key: TypedDependencyKey<any>, callback: ContainerResolutionCallback<any>}> = collect()
/** /**
* Register some factory class with the container. Should take no construction params. * 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. * Get an array of factory instances in the blueprint.
*/ */
resolve(): AbstractFactory<any>[] { resolve(): AbstractFactory<any>[] {
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<any>>): 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 * Get an array of static Factory classes that need to be instantiated by
* the container itself. * the container itself.
*/ */
resolveConstructable(): StaticClass<AbstractFactory<any>, any> { resolveConstructable(): StaticClass<AbstractFactory<any>, any>[] {
return [...this.constructableFactories] 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<StaticClass<AbstractFactory<any>, any>>): Unsubscribe {
return this.constructableFactories.push$(sub)
} }
/** /**
* Get an array of DependencyKey-callback pairs to register with new containers. * Get an array of DependencyKey-callback pairs to register with new containers.
*/ */
resolveResolutionCallbacks(): ({key: TypedDependencyKey<any>, callback: ContainerResolutionCallback<any>})[] { resolveResolutionCallbacks(): ({key: TypedDependencyKey<any>, callback: ContainerResolutionCallback<any>})[] {
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<any>, callback: ContainerResolutionCallback<any>}>): Unsubscribe {
return this.resolutionCallbacks.push$(sub)
} }
} }

View File

@ -166,6 +166,7 @@ export const Singleton = (name?: string): ClassDecorator => {
...(name ? { name } : {}), ...(name ? { name } : {}),
} }
logIfDebugging('extollo.di.singleton', 'Registering singleton target:', target, 'injectionType:', injectionType)
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target) Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)
Injectable()(target) Injectable()(target)

View File

@ -1,4 +1,5 @@
import {AsyncPipe, Pipeline} from '../support/Pipe' import {AsyncPipe, Pipeline} from '../support/Pipe'
import {Unsubscribe, Subscription} from '../support/BehaviorSubject'
type CollectionItem<T> = T type CollectionItem<T> = T
type MaybeCollectionItem<T> = CollectionItem<T> | undefined type MaybeCollectionItem<T> = CollectionItem<T> | undefined
@ -50,6 +51,8 @@ export {
class Collection<T> { class Collection<T> {
private storedItems: CollectionItem<T>[] = [] private storedItems: CollectionItem<T>[] = []
private pushSubscribers: Subscription<T>[] = []
/** /**
* Create a new collection from an array of items. * Create a new collection from an array of items.
* @param items * @param items
@ -966,6 +969,7 @@ class Collection<T> {
*/ */
prepend(item: CollectionItem<T>): Collection<T> { prepend(item: CollectionItem<T>): Collection<T> {
this.storedItems = [item, ...this.storedItems] this.storedItems = [item, ...this.storedItems]
this.callPushSubscribers(item)
return this return this
} }
@ -975,9 +979,33 @@ class Collection<T> {
*/ */
push(item: CollectionItem<T>): Collection<T> { push(item: CollectionItem<T>): Collection<T> {
this.storedItems.push(item) this.storedItems.push(item)
this.callPushSubscribers(item)
return this return this
} }
/**
* Subscribe to listen for items being added to the collection.
* @param sub
*/
push$(sub: Subscription<T>): 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. * Push the given items to the end of this collection.
* Unlike `merge()`, this mutates the current collection's items. * Unlike `merge()`, this mutates the current collection's items.
@ -987,6 +1015,7 @@ class Collection<T> {
const concats = items instanceof Collection ? items.all() : items const concats = items instanceof Collection ? items.all() : items
for ( const item of concats ) { for ( const item of concats ) {
this.storedItems.push(item) this.storedItems.push(item)
this.callPushSubscribers(item)
} }
return this return this
} }