diff --git a/package.json b/package.json index 74ccb7a..666c7da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@extollo/lib", - "version": "0.9.15", + "version": "0.9.16", "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 53dd05e..dcc450f 100644 --- a/src/di/Container.ts +++ b/src/di/Container.ts @@ -8,7 +8,7 @@ import { TypedDependencyKey, } from './types' import {AbstractFactory} from './factory/AbstractFactory' -import {collect, Collection, ErrorWithContext, globalRegistry, logIfDebugging} from '../util' +import {Awaitable, collect, Collection, ErrorWithContext, globalRegistry, hasOwnProperty, logIfDebugging} from '../util' import {Factory} from './factory/Factory' import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError' import {ClosureFactory} from './factory/ClosureFactory' @@ -21,6 +21,32 @@ export type MaybeFactory = AbstractFactory | undefined export type MaybeDependency = any | undefined export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any } +/** + * Singletons that implement this interface receive callbacks for + * structural container events. + */ +export interface AwareOfContainerLifecycle { + awareOfContainerLifecycle: true + + /** Called when this key is realized by its parent container. */ + onContainerRealize?(): Awaitable + + /** Called before the parent container of this instance is destroyed. */ + onContainerDestroy?(): Awaitable + + /** Called before an instance of a key is released from the container. */ + onContainerRelease?(): Awaitable +} + +export function isAwareOfContainerLifecycle(what: unknown): what is AwareOfContainerLifecycle { + return Boolean( + typeof what === 'object' + && what !== null + && hasOwnProperty(what, 'awareOfContainerLifecycle') + && what.awareOfContainerLifecycle, + ) +} + /** * A container of resolve-able dependencies that are created via inversion-of-control. */ @@ -113,6 +139,12 @@ export class Container { */ protected waitingResolveCallbacks: Collection<{ key: DependencyKey, callback: (t: unknown) => unknown }> = new Collection<{key: DependencyKey; callback:(t: unknown) => unknown}>(); + /** + * Collection of created objects that should have lifecycle events called on them, if they still exist. + * @protected + */ + protected waitingLifecycleCallbacks: Collection> = new Collection() + constructor() { this.registerSingletonInstance(Container, this) this.registerSingleton('injector', this) @@ -133,7 +165,14 @@ export class Container { * @param key */ release(key: DependencyKey): this { - this.instances = this.instances.filter(x => x.key !== key) + this.instances = this.instances.filter(x => { + if ( x.key === key && isAwareOfContainerLifecycle(x.value) ) { + x.value.onContainerRelease?.() + } + + return x.key !== key + }) + return this } @@ -365,6 +404,10 @@ export class Container { value: newInstance, }) + if ( isAwareOfContainerLifecycle(newInstance) ) { + newInstance.onContainerRealize?.() + } + this.waitingResolveCallbacks = this.waitingResolveCallbacks.filter(waiter => { if ( waiter.key === key ) { waiter.callback(newInstance) @@ -421,6 +464,10 @@ export class Container { } }) + if ( isAwareOfContainerLifecycle(inst) ) { + this.waitingLifecycleCallbacks.push(new WeakRef(inst)) + } + return inst } @@ -521,7 +568,10 @@ export class Container { */ makeNew(key: TypedDependencyKey, ...parameters: any[]): T { if ( isInstantiable(key) ) { - return this.produceFactory(new Factory(key), parameters) + const result = this.produceFactory(new Factory(key), parameters) + if ( isAwareOfContainerLifecycle(result) ) { + result.onContainerRealize?.() + } } throw new TypeError(`Invalid or unknown make target: ${key}`) @@ -541,6 +591,21 @@ export class Container { return factory.getDependencyKeys().pluck('key') } + /** + * Perform any cleanup necessary to destroy this container instance. + */ + destroy(): void { + this.waitingLifecycleCallbacks + .mapCall('deref') + .whereDefined() + .each(inst => { + if ( isAwareOfContainerLifecycle(inst) ) { + inst.onContainerRelease?.() + inst.onContainerDestroy?.() + } + }) + } + /** * Given a different container, copy the factories and instances from this container over to it. * @param container diff --git a/src/service/HTTPServer.ts b/src/service/HTTPServer.ts index 3e7e12a..f84dbbc 100644 --- a/src/service/HTTPServer.ts +++ b/src/service/HTTPServer.ts @@ -131,6 +131,9 @@ export class HTTPServer extends Unit { if ( extolloReq.response.canSend() ) { await extolloReq.response.send() } + }).finally(() => { + this.logging.verbose('Destroying request container...') + extolloReq.destroy() }) } } diff --git a/src/support/bus/Bus.ts b/src/support/bus/Bus.ts index da43c5c..a97c108 100644 --- a/src/support/bus/Bus.ts +++ b/src/support/bus/Bus.ts @@ -1,4 +1,4 @@ -import {Inject, Singleton, StaticInstantiable} from '../../di' +import {AwareOfContainerLifecycle, Inject, Singleton, StaticInstantiable} from '../../di' import { BusConnectorConfig, BusSubscriber, @@ -25,7 +25,9 @@ export interface BusInternalSubscription { * Propagating event bus implementation. */ @Singleton() -export class Bus extends Unit implements EventBus { +export class Bus extends Unit implements EventBus, AwareOfContainerLifecycle { + awareOfContainerLifecycle: true = true + @Inject() protected readonly logging!: Logging @@ -228,4 +230,8 @@ export class Bus extends Unit implements EventBus< this.isUp = false } + + onContainerDestroy(): Awaitable { + this.down() + } } diff --git a/src/support/bus/LocalBus.ts b/src/support/bus/LocalBus.ts index 6d598af..87d4338 100644 --- a/src/support/bus/LocalBus.ts +++ b/src/support/bus/LocalBus.ts @@ -1,4 +1,4 @@ -import {Inject, Injectable, StaticInstantiable} from '../../di' +import {AwareOfContainerLifecycle, Inject, Injectable, StaticInstantiable} from '../../di' import {BusSubscriber, Event, EventBus, EventHandler, EventHandlerReturn, EventHandlerSubscription} from './types' import {Awaitable, Collection, ifDebugging, Pipeline, uuid4} from '../../util' import {Logging} from '../../service/Logging' @@ -10,7 +10,9 @@ import {CanonicalItemClass} from '../CanonicalReceiver' * Non-connectable event bus implementation. Can forward events to the main Bus instance. */ @Injectable() -export class LocalBus extends CanonicalItemClass implements EventBus { +export class LocalBus extends CanonicalItemClass implements EventBus, AwareOfContainerLifecycle { + awareOfContainerLifecycle: true = true + @Inject() protected readonly logging!: Logging @@ -135,4 +137,8 @@ export class LocalBus extends CanonicalItemClass i this.isUp = false } + + onContainerRelease(): Awaitable { + this.down() + } } diff --git a/src/support/bus/RedisBus.ts b/src/support/bus/RedisBus.ts index d5653a1..754147a 100644 --- a/src/support/bus/RedisBus.ts +++ b/src/support/bus/RedisBus.ts @@ -1,5 +1,5 @@ import {BusSubscriber, Event, EventBus, EventHandler, EventHandlerReturn, EventHandlerSubscription} from './types' -import {Container, Inject, Injectable, StaticInstantiable} from '../../di' +import {AwareOfContainerLifecycle, Container, Inject, Injectable, StaticInstantiable} from '../../di' import {Awaitable, Collection, Pipeline, uuid4} from '../../util' import {Redis} from '../redis/Redis' import {Serialization} from './serial/Serialization' @@ -11,7 +11,9 @@ import {getEventName} from './getEventName' * Event bus implementation that does pub/sub over a Redis connection. */ @Injectable() -export class RedisBus implements EventBus { +export class RedisBus implements EventBus, AwareOfContainerLifecycle { + awareOfContainerLifecycle: true = true + @Inject() protected readonly redis!: Redis @@ -125,8 +127,11 @@ export class RedisBus implements EventBus { } down(): Awaitable { - // The Redis service will clean up the connections when the framework exits, - // so we don't need to do anything here. - return undefined + this.subscriberConnection?.disconnect() + this.publisherConnection?.disconnect() + } + + onContainerRelease(): Awaitable { + this.down() } } diff --git a/src/tsconfig.json b/src/tsconfig.json index 7f677c5..9373828 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -3,9 +3,10 @@ "experimentalDecorators": true, "module": "commonjs", "target": "es6", - "sourceMap": true + "sourceMap": true, + "lib": ["ESNext"] }, "exclude": [ "node_modules" ] -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 637b913..fb4dc66 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "./lib", "strict": true, "experimentalDecorators": true, - "emitDecoratorMetadata": true + "emitDecoratorMetadata": true, + "lib": ["ESNext"] }, "include": ["src"], "exclude": ["node_modules"]