Add request container lifecycle handling

orm-types
Garrett Mills 2 years ago
parent 514a578260
commit 78cb26fcb2

@ -1,6 +1,6 @@
{ {
"name": "@extollo/lib", "name": "@extollo/lib",
"version": "0.9.15", "version": "0.9.16",
"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",

@ -8,7 +8,7 @@ import {
TypedDependencyKey, TypedDependencyKey,
} from './types' } from './types'
import {AbstractFactory} from './factory/AbstractFactory' 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 {Factory} from './factory/Factory'
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError' import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
import {ClosureFactory} from './factory/ClosureFactory' import {ClosureFactory} from './factory/ClosureFactory'
@ -21,6 +21,32 @@ export type MaybeFactory<T> = AbstractFactory<T> | undefined
export type MaybeDependency = any | undefined export type MaybeDependency = any | undefined
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any } 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<unknown>
/** Called before the parent container of this instance is destroyed. */
onContainerDestroy?(): Awaitable<unknown>
/** Called before an instance of a key is released from the container. */
onContainerRelease?(): Awaitable<unknown>
}
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. * 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}>(); 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<WeakRef<AwareOfContainerLifecycle>> = new Collection()
constructor() { constructor() {
this.registerSingletonInstance<Container>(Container, this) this.registerSingletonInstance<Container>(Container, this)
this.registerSingleton('injector', this) this.registerSingleton('injector', this)
@ -133,7 +165,14 @@ export class Container {
* @param key * @param key
*/ */
release(key: DependencyKey): this { 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 return this
} }
@ -365,6 +404,10 @@ export class Container {
value: newInstance, value: newInstance,
}) })
if ( isAwareOfContainerLifecycle(newInstance) ) {
newInstance.onContainerRealize?.()
}
this.waitingResolveCallbacks = this.waitingResolveCallbacks.filter(waiter => { this.waitingResolveCallbacks = this.waitingResolveCallbacks.filter(waiter => {
if ( waiter.key === key ) { if ( waiter.key === key ) {
waiter.callback(newInstance) waiter.callback(newInstance)
@ -421,6 +464,10 @@ export class Container {
} }
}) })
if ( isAwareOfContainerLifecycle(inst) ) {
this.waitingLifecycleCallbacks.push(new WeakRef<AwareOfContainerLifecycle>(inst))
}
return inst return inst
} }
@ -521,7 +568,10 @@ export class Container {
*/ */
makeNew<T>(key: TypedDependencyKey<T>, ...parameters: any[]): T { makeNew<T>(key: TypedDependencyKey<T>, ...parameters: any[]): T {
if ( isInstantiable(key) ) { 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}`) throw new TypeError(`Invalid or unknown make target: ${key}`)
@ -541,6 +591,21 @@ export class Container {
return factory.getDependencyKeys().pluck('key') 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. * Given a different container, copy the factories and instances from this container over to it.
* @param container * @param container

@ -131,6 +131,9 @@ export class HTTPServer extends Unit {
if ( extolloReq.response.canSend() ) { if ( extolloReq.response.canSend() ) {
await extolloReq.response.send() await extolloReq.response.send()
} }
}).finally(() => {
this.logging.verbose('Destroying request container...')
extolloReq.destroy()
}) })
} }
} }

@ -1,4 +1,4 @@
import {Inject, Singleton, StaticInstantiable} from '../../di' import {AwareOfContainerLifecycle, Inject, Singleton, StaticInstantiable} from '../../di'
import { import {
BusConnectorConfig, BusConnectorConfig,
BusSubscriber, BusSubscriber,
@ -25,7 +25,9 @@ export interface BusInternalSubscription {
* Propagating event bus implementation. * Propagating event bus implementation.
*/ */
@Singleton() @Singleton()
export class Bus<TEvent extends Event = Event> extends Unit implements EventBus<TEvent> { export class Bus<TEvent extends Event = Event> extends Unit implements EventBus<TEvent>, AwareOfContainerLifecycle {
awareOfContainerLifecycle: true = true
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
@ -228,4 +230,8 @@ export class Bus<TEvent extends Event = Event> extends Unit implements EventBus<
this.isUp = false this.isUp = false
} }
onContainerDestroy(): Awaitable<void> {
this.down()
}
} }

@ -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 {BusSubscriber, Event, EventBus, EventHandler, EventHandlerReturn, EventHandlerSubscription} from './types'
import {Awaitable, Collection, ifDebugging, Pipeline, uuid4} from '../../util' import {Awaitable, Collection, ifDebugging, Pipeline, uuid4} from '../../util'
import {Logging} from '../../service/Logging' 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. * Non-connectable event bus implementation. Can forward events to the main Bus instance.
*/ */
@Injectable() @Injectable()
export class LocalBus<TEvent extends Event = Event> extends CanonicalItemClass implements EventBus<TEvent> { export class LocalBus<TEvent extends Event = Event> extends CanonicalItemClass implements EventBus<TEvent>, AwareOfContainerLifecycle {
awareOfContainerLifecycle: true = true
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
@ -135,4 +137,8 @@ export class LocalBus<TEvent extends Event = Event> extends CanonicalItemClass i
this.isUp = false this.isUp = false
} }
onContainerRelease(): Awaitable<void> {
this.down()
}
} }

@ -1,5 +1,5 @@
import {BusSubscriber, Event, EventBus, EventHandler, EventHandlerReturn, EventHandlerSubscription} from './types' 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 {Awaitable, Collection, Pipeline, uuid4} from '../../util'
import {Redis} from '../redis/Redis' import {Redis} from '../redis/Redis'
import {Serialization} from './serial/Serialization' import {Serialization} from './serial/Serialization'
@ -11,7 +11,9 @@ import {getEventName} from './getEventName'
* Event bus implementation that does pub/sub over a Redis connection. * Event bus implementation that does pub/sub over a Redis connection.
*/ */
@Injectable() @Injectable()
export class RedisBus implements EventBus { export class RedisBus implements EventBus, AwareOfContainerLifecycle {
awareOfContainerLifecycle: true = true
@Inject() @Inject()
protected readonly redis!: Redis protected readonly redis!: Redis
@ -125,8 +127,11 @@ export class RedisBus implements EventBus {
} }
down(): Awaitable<void> { down(): Awaitable<void> {
// The Redis service will clean up the connections when the framework exits, this.subscriberConnection?.disconnect()
// so we don't need to do anything here. this.publisherConnection?.disconnect()
return undefined }
onContainerRelease(): Awaitable<void> {
this.down()
} }
} }

@ -3,9 +3,10 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"module": "commonjs", "module": "commonjs",
"target": "es6", "target": "es6",
"sourceMap": true "sourceMap": true,
"lib": ["ESNext"]
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"
] ]
} }

@ -6,7 +6,8 @@
"outDir": "./lib", "outDir": "./lib",
"strict": true, "strict": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true "emitDecoratorMetadata": true,
"lib": ["ESNext"]
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

Loading…
Cancel
Save