Skip to content

Commit 675c3c4

Browse files
miklXapphire13MeltingMosaic
authored
[feat] Add support for cleaning up disposable instances, part deux (#183)
* [feat] Add support for cleaning up disposable instances * [feat] Add support for asynchronous disposables Co-authored-by: Steven Hobson-Campbell <[email protected]> Co-authored-by: Lee C <[email protected]>
1 parent 837692c commit 675c3c4

8 files changed

+218
-10
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ constructor injection.
3333
- [Circular dependencies](#circular-dependencies)
3434
- [The `delay` helper function](#the-delay-helper-function)
3535
- [Interfaces and circular dependencies](#interfaces-and-circular-dependencies)
36+
- [Disposable instances](#disposable-instances)
3637
- [Full examples](#full-examples)
3738
- [Example without interfaces](#example-without-interfaces)
3839
- [Example with interfaces](#example-with-interfaces)
@@ -681,6 +682,20 @@ export class Bar implements IBar {
681682
}
682683
```
683684
685+
# Disposable instances
686+
All instances created by the container that implement the [`Disposable`](./src/types/disposable.ts)
687+
interface will automatically be disposed of when the container is disposed.
688+
689+
```typescript
690+
container.dispose();
691+
```
692+
693+
or to await all asynchronous disposals:
694+
695+
```typescript
696+
await container.dispose();
697+
```
698+
684699
# Full examples
685700
686701
## Example without interfaces

src/__tests__/disposable.test.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Disposable, {isDisposable} from "../types/disposable";
2+
3+
describe("Disposable", () => {
4+
describe("isDisposable", () => {
5+
it("returns false for non-disposable object", () => {
6+
const nonDisposable = {};
7+
8+
expect(isDisposable(nonDisposable)).toBeFalsy();
9+
});
10+
11+
it("returns false when dispose method takes too many args", () => {
12+
const specialDisposable = {
13+
dispose(_: any) {}
14+
};
15+
16+
expect(isDisposable(specialDisposable)).toBeFalsy();
17+
});
18+
19+
it("returns true for disposable object", () => {
20+
const disposable: Disposable = {
21+
dispose() {}
22+
};
23+
24+
expect(isDisposable(disposable)).toBeTruthy();
25+
});
26+
});
27+
});

src/__tests__/global-container.test.ts

+85
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {instance as globalContainer} from "../dependency-container";
1111
import injectAll from "../decorators/inject-all";
1212
import Lifecycle from "../types/lifecycle";
1313
import {ValueProvider} from "../providers";
14+
import Disposable from "../types/disposable";
1415

1516
interface IBar {
1617
value: string;
@@ -820,3 +821,87 @@ test("predicateAwareClassFactory returns new instances each call with caching of
820821

821822
expect(factory(globalContainer)).not.toBe(factory(globalContainer));
822823
});
824+
825+
describe("dispose", () => {
826+
class Foo implements Disposable {
827+
disposed = false;
828+
dispose(): void {
829+
this.disposed = true;
830+
}
831+
}
832+
class Bar implements Disposable {
833+
disposed = false;
834+
dispose(): void {
835+
this.disposed = true;
836+
}
837+
}
838+
class Baz implements Disposable {
839+
disposed = false;
840+
async dispose(): Promise<void> {
841+
return new Promise(resolve => {
842+
process.nextTick(() => {
843+
this.disposed = true;
844+
resolve();
845+
});
846+
});
847+
}
848+
}
849+
850+
it("renders the container useless", () => {
851+
const container = globalContainer.createChildContainer();
852+
container.dispose();
853+
854+
expect(() => container.register("Bar", {useClass: Bar})).toThrow(
855+
/disposed/
856+
);
857+
expect(() => container.reset()).toThrow(/disposed/);
858+
expect(() => container.resolve("indisposed")).toThrow(/disposed/);
859+
});
860+
861+
it("disposes all child disposables", () => {
862+
const container = globalContainer.createChildContainer();
863+
864+
const foo = container.resolve(Foo);
865+
const bar = container.resolve(Bar);
866+
867+
container.dispose();
868+
869+
expect(foo.disposed).toBeTruthy();
870+
expect(bar.disposed).toBeTruthy();
871+
});
872+
873+
it("disposes asynchronous disposables", async () => {
874+
const container = globalContainer.createChildContainer();
875+
876+
const foo = container.resolve(Foo);
877+
const baz = container.resolve(Baz);
878+
879+
await container.dispose();
880+
881+
expect(foo.disposed).toBeTruthy();
882+
expect(baz.disposed).toBeTruthy();
883+
});
884+
885+
it("disposes all instances of the same type", () => {
886+
const container = globalContainer.createChildContainer();
887+
888+
const foo1 = container.resolve(Foo);
889+
const foo2 = container.resolve(Foo);
890+
891+
container.dispose();
892+
893+
expect(foo1.disposed).toBeTruthy();
894+
expect(foo2.disposed).toBeTruthy();
895+
});
896+
897+
it("doesn't dispose of instances created external to the container", () => {
898+
const foo = new Foo();
899+
const container = globalContainer.createChildContainer();
900+
901+
container.registerInstance(Foo, foo);
902+
container.resolve(Foo);
903+
container.dispose();
904+
905+
expect(foo.disposed).toBeFalsy();
906+
});
907+
});

src/dependency-container.ts

+65-9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import Lifecycle from "./types/lifecycle";
2828
import ResolutionContext from "./resolution-context";
2929
import {formatErrorCtor} from "./error-helpers";
3030
import {DelayedConstructor} from "./lazy-helpers";
31+
import Disposable, {isDisposable} from "./types/disposable";
3132
import InterceptorOptions from "./types/interceptor-options";
3233
import Interceptors from "./interceptors";
3334

@@ -45,6 +46,8 @@ export const typeInfo = new Map<constructor<any>, ParamInfo[]>();
4546
class InternalDependencyContainer implements DependencyContainer {
4647
private _registry = new Registry();
4748
private interceptors = new Interceptors();
49+
private disposed = false;
50+
private disposables = new Set<Disposable>();
4851

4952
public constructor(private parent?: InternalDependencyContainer) {}
5053

@@ -81,6 +84,8 @@ class InternalDependencyContainer implements DependencyContainer {
8184
providerOrConstructor: Provider<T> | constructor<T>,
8285
options: RegistrationOptions = {lifecycle: Lifecycle.Transient}
8386
): InternalDependencyContainer {
87+
this.ensureNotDisposed();
88+
8489
let provider: Provider<T>;
8590

8691
if (!isProvider(providerOrConstructor)) {
@@ -139,6 +144,8 @@ class InternalDependencyContainer implements DependencyContainer {
139144
from: InjectionToken<T>,
140145
to: InjectionToken<T>
141146
): InternalDependencyContainer {
147+
this.ensureNotDisposed();
148+
142149
if (isNormalToken(to)) {
143150
return this.register(from, {
144151
useToken: to
@@ -154,6 +161,8 @@ class InternalDependencyContainer implements DependencyContainer {
154161
token: InjectionToken<T>,
155162
instance: T
156163
): InternalDependencyContainer {
164+
this.ensureNotDisposed();
165+
157166
return this.register(token, {
158167
useValue: instance
159168
});
@@ -171,6 +180,8 @@ class InternalDependencyContainer implements DependencyContainer {
171180
from: InjectionToken<T>,
172181
to?: InjectionToken<T>
173182
): InternalDependencyContainer {
183+
this.ensureNotDisposed();
184+
174185
if (isNormalToken(from)) {
175186
if (isNormalToken(to)) {
176187
return this.register(
@@ -213,6 +224,8 @@ class InternalDependencyContainer implements DependencyContainer {
213224
token: InjectionToken<T>,
214225
context: ResolutionContext = new ResolutionContext()
215226
): T {
227+
this.ensureNotDisposed();
228+
216229
const registration = this.getRegistration(token);
217230

218231
if (!registration && isNormalToken(token)) {
@@ -282,6 +295,8 @@ class InternalDependencyContainer implements DependencyContainer {
282295
registration: Registration,
283296
context: ResolutionContext
284297
): T {
298+
this.ensureNotDisposed();
299+
285300
// If we have already resolved this scoped dependency, return it
286301
if (
287302
registration.options.lifecycle === Lifecycle.ResolutionScoped &&
@@ -334,6 +349,8 @@ class InternalDependencyContainer implements DependencyContainer {
334349
token: InjectionToken<T>,
335350
context: ResolutionContext = new ResolutionContext()
336351
): T[] {
352+
this.ensureNotDisposed();
353+
337354
const registrations = this.getAllRegistrations(token);
338355

339356
if (!registrations && isNormalToken(token)) {
@@ -360,6 +377,8 @@ class InternalDependencyContainer implements DependencyContainer {
360377
}
361378

362379
public isRegistered<T>(token: InjectionToken<T>, recursive = false): boolean {
380+
this.ensureNotDisposed();
381+
363382
return (
364383
this._registry.has(token) ||
365384
(recursive &&
@@ -369,12 +388,15 @@ class InternalDependencyContainer implements DependencyContainer {
369388
}
370389

371390
public reset(): void {
391+
this.ensureNotDisposed();
372392
this._registry.clear();
373393
this.interceptors.preResolution.clear();
374394
this.interceptors.postResolution.clear();
375395
}
376396

377397
public clearInstances(): void {
398+
this.ensureNotDisposed();
399+
378400
for (const [token, registrations] of this._registry.entries()) {
379401
this._registry.setAll(
380402
token,
@@ -391,6 +413,8 @@ class InternalDependencyContainer implements DependencyContainer {
391413
}
392414

393415
public createChildContainer(): DependencyContainer {
416+
this.ensureNotDisposed();
417+
394418
const childContainer = new InternalDependencyContainer(this);
395419

396420
for (const [token, registrations] of this._registry.entries()) {
@@ -443,6 +467,21 @@ class InternalDependencyContainer implements DependencyContainer {
443467
});
444468
}
445469

470+
public async dispose(): Promise<void> {
471+
this.disposed = true;
472+
473+
const promises: Promise<unknown>[] = [];
474+
this.disposables.forEach(disposable => {
475+
const maybePromise = disposable.dispose();
476+
477+
if (maybePromise) {
478+
promises.push(maybePromise);
479+
}
480+
});
481+
482+
await Promise.all(promises);
483+
}
484+
446485
private getRegistration<T>(token: InjectionToken<T>): Registration | null {
447486
if (this.isRegistered(token)) {
448487
return this._registry.get(token)!;
@@ -478,18 +517,27 @@ class InternalDependencyContainer implements DependencyContainer {
478517
this.resolve(target, context)
479518
);
480519
}
481-
const paramInfo = typeInfo.get(ctor);
482-
if (!paramInfo || paramInfo.length === 0) {
483-
if (ctor.length === 0) {
484-
return new ctor();
485-
} else {
486-
throw new Error(`TypeInfo not known for "${ctor.name}"`);
520+
521+
const instance: T = (() => {
522+
const paramInfo = typeInfo.get(ctor);
523+
if (!paramInfo || paramInfo.length === 0) {
524+
if (ctor.length === 0) {
525+
return new ctor();
526+
} else {
527+
throw new Error(`TypeInfo not known for "${ctor.name}"`);
528+
}
487529
}
488-
}
489530

490-
const params = paramInfo.map(this.resolveParams(context, ctor));
531+
const params = paramInfo.map(this.resolveParams(context, ctor));
532+
533+
return new ctor(...params);
534+
})();
491535

492-
return new ctor(...params);
536+
if (isDisposable(instance)) {
537+
this.disposables.add(instance);
538+
}
539+
540+
return instance;
493541
}
494542

495543
private resolveParams<T>(context: ResolutionContext, ctor: constructor<T>) {
@@ -523,6 +571,14 @@ class InternalDependencyContainer implements DependencyContainer {
523571
}
524572
};
525573
}
574+
575+
private ensureNotDisposed(): void {
576+
if (this.disposed) {
577+
throw new Error(
578+
"This container has been disposed, you cannot interact with a disposed container"
579+
);
580+
}
581+
}
526582
}
527583

528584
export const instance: DependencyContainer = new InternalDependencyContainer();

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ if (typeof Reflect === "undefined" || !Reflect.getMetadata) {
66

77
export {
88
DependencyContainer,
9+
Disposable,
910
Lifecycle,
1011
RegistrationOptions,
1112
Frequency

src/types/dependency-container.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ValueProvider from "../providers/value-provider";
55
import ClassProvider from "../providers/class-provider";
66
import constructor from "./constructor";
77
import RegistrationOptions from "./registration-options";
8+
import Disposable from "./disposable";
89
import InterceptorOptions from "./interceptor-options";
910

1011
export type ResolutionType = "Single" | "All";
@@ -30,7 +31,7 @@ export interface PostResolutionInterceptorCallback<T = any> {
3031
): void;
3132
}
3233

33-
export default interface DependencyContainer {
34+
export default interface DependencyContainer extends Disposable {
3435
register<T>(
3536
token: InjectionToken<T>,
3637
provider: ValueProvider<T>
@@ -120,4 +121,10 @@ export default interface DependencyContainer {
120121
callback: PostResolutionInterceptorCallback<T>,
121122
options?: InterceptorOptions
122123
): void;
124+
125+
/**
126+
* Calls `.dispose()` on all disposable instances created by the container.
127+
* After calling this, the container may no longer be used.
128+
*/
129+
dispose(): Promise<void> | void;
123130
}

src/types/disposable.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default interface Disposable {
2+
dispose(): Promise<void> | void;
3+
}
4+
5+
export function isDisposable(value: any): value is Disposable {
6+
if (typeof value.dispose !== "function") return false;
7+
8+
const disposeFun: Function = value.dispose;
9+
10+
// `.dispose()` takes in no arguments
11+
if (disposeFun.length > 0) {
12+
return false;
13+
}
14+
15+
return true;
16+
}

0 commit comments

Comments
 (0)