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",
"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",

View File

@ -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<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() {
this.registerSingletonInstance<Container>(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()

View File

@ -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<T> = (() => unknown) | ((t: T) => unknown)
@ -25,11 +27,11 @@ export class ContainerBlueprint {
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.
@ -74,7 +76,16 @@ export class ContainerBlueprint {
* Get an array of factory instances in the blueprint.
*/
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
* the container itself.
*/
resolveConstructable(): StaticClass<AbstractFactory<any>, any> {
return [...this.constructableFactories]
resolveConstructable(): StaticClass<AbstractFactory<any>, 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<StaticClass<AbstractFactory<any>, any>>): Unsubscribe {
return this.constructableFactories.push$(sub)
}
/**
* Get an array of DependencyKey-callback pairs to register with new containers.
*/
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 } : {}),
}
logIfDebugging('extollo.di.singleton', 'Registering singleton target:', target, 'injectionType:', injectionType)
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)
Injectable()(target)

View File

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