##// END OF EJS Templates
sync
cin -
r15:3985e8405319 tip default
parent child
Show More
@@ -0,0 +1,61
1 import { ILifetimeSlot } from "../typings/interfaces";
2 import { isDestroyable } from "./traits";
3
4 const _destroy = (item: unknown) => () => isDestroyable(item) && item.destroy();
5
6 enum State {
7 New,
8
9 Initialized,
10
11 Destroyed
12 }
13
14 export class LifetimeSlot<T> implements ILifetimeSlot<T> {
15 private _value: NonNullable<T> | undefined = undefined;
16
17 private _state = State.New;
18
19 cleanup: () => void;
20
21 readonly remove: () => void;
22
23 constructor(remove: () => void) {
24 this.remove = this._finalizer(remove);
25 this.cleanup = this._finalizer(() => { });
26 }
27
28 has(): boolean {
29 this._assertNotDisposed();
30 return this._value !== undefined;
31 }
32 get(): NonNullable<T> {
33 this._assertNotDisposed();
34 if (this._value === undefined)
35 throw new Error("The slot doesn't have a value");
36 return this._value;
37 }
38
39 initialize(): boolean {
40 return this._state === State.New ?
41 (this._state = State.Initialized, true) :
42 false;
43 }
44
45 store(item: NonNullable<T>, cleanup: ((item: NonNullable<T>) => void) | undefined = _destroy): void {
46 this._assertNotDisposed();
47 this._value = item;
48 if (cleanup)
49 this.cleanup = this._finalizer(() => cleanup(item));
50 }
51
52 private _finalizer(cb: () => void) {
53 return () => this._state !== State.Destroyed ? (this._state = State.Destroyed, cb()) : void (0);
54 }
55
56 private _assertNotDisposed() {
57 if (this._state === State.Destroyed)
58 throw new Error("The slot is disposed");
59 }
60
61 } No newline at end of file
@@ -30,9 +30,8 typescript {
30 30 declaration = true
31 31 experimentalDecorators = true
32 32 strict = true
33 module = "commonjs"
34 target = "es5"
35 lib = ["es2015", "dom", "scripthost"]
33 module = "nodenext"
34 target = "ESNext"
36 35 }
37 36 tscCmd = "$projectDir/node_modules/.bin/tsc"
38 37 tsLintCmd = "$projectDir/node_modules/.bin/tslint"
@@ -1,15 +1,8
1 import { ActivationError } from "./ActivationError";
2 import { Descriptor, ILifetime, IActivationContext, DescriptorMap, ILifetimeManager, ILifetimeSlot } from "./interfaces";
3 import { argumentNotNull, prototype } from "./traits";
4
5 export interface ActivationContextInfo {
6 name: string;
7
8 service: string;
9
10 }
11
12 let nextId = 1;
1 import { ActivationError, ActivationItem } from "./ActivationError";
2 import { IActivationContext, DescriptorMap, ILifetimeManager, ILifetimeSlot, ServiceLocator, IContainerBuilder } from "../typings/interfaces";
3 import { argumentNotNull, each, prototype } from "./traits";
4 import { LifetimeManager } from "./LifetimeManager";
5 import { ContainerBuilder } from "./ContainerBuilder";
13 6
14 7 /** This object is created once per `Container.resolve` method call and used to
15 8 * cache dependencies and to track created instances. The activation context
@@ -21,17 +14,14 let nextId = 1;
21 14 * this activation context.
22 15 */
23 16 export class ActivationContext<S> implements IActivationContext<S> {
24 private readonly _cache: Record<string, unknown>;
25 17
26 private readonly _services: DescriptorMap<S>;
27
28 private readonly _name: string;
18 private readonly _container: ServiceLocator<S>;
29 19
30 private readonly _service: Descriptor<S, unknown>;
20 private readonly _contextScope: ILifetimeManager;
31 21
32 private readonly _lifetimeManagers: ILifetimeManager[];
22 private _services: DescriptorMap<S>;
33 23
34 private readonly _parent: ActivationContext<S> | undefined;
24 private readonly _scope: ILifetimeManager[];
35 25
36 26 /** Creates a new activation context with the specified parameters.
37 27 * @param containerLifetimeManager the container which starts the activation process
@@ -41,31 +31,31 export class ActivationContext<S> implem
41 31 * @param service the service to activate, this parameter is used for the
42 32 * debug purpose.
43 33 */
44 constructor(lifetimeManagers: ILifetimeManager[], services: DescriptorMap<S>, name: string, service: Descriptor<S, unknown>, cache = {}) {
45 this._name = name;
46 this._service = service;
47 this._cache = cache;
34 constructor(container: ServiceLocator<S>, scope: ILifetimeManager[], services: DescriptorMap<S>, contextScope: ILifetimeManager = new LifetimeManager()) {
35 this._container = container;
36 this._contextScope = contextScope;
48 37 this._services = services;
49 this._lifetimeManagers = lifetimeManagers;
50 }
51
52 /** the name of the current resolving dependency */
53 getName() {
54 return this._name;
38 this._scope = scope;
55 39 }
56 40
57 resolve<K extends keyof S>(name: K): NonNullable<S[K]>;
58 resolve<K extends keyof S, T>(name: K, def: T): NonNullable<S[K]> | T;
59 resolve<K extends keyof S, T>(name: K, def?: T): NonNullable<S[K]> | T | undefined {
60 const d = this._services[name];
41 resolve<K extends keyof S>(name: K, stack: ActivationItem[]): NonNullable<S[K]>;
42 resolve<K extends keyof S, T>(name: K, stack: ActivationItem[], def: T): NonNullable<S[K]> | T;
43 resolve<K extends keyof S, T>(name: K, stack: ActivationItem[], def?: T): NonNullable<S[K]> | T | undefined {
44 const service = this._services[name];
61 45
62 if (d !== undefined) {
63 return this.activate(d, name.toString());
46 if (service !== undefined) {
47 return service.activate(
48 this,
49 stack.concat({
50 name: name.toString(),
51 descriptor: service.toString()
52 })
53 );
64 54 } else {
65 if (arguments.length > 1)
55 if (arguments.length > 2)
66 56 return def;
67 57 else
68 throw new Error(`Service ${String(name)} not found`);
58 throw new Error("Service not found");
69 59 }
70 60 }
71 61
@@ -75,7 +65,7 export class ActivationContext<S> implem
75 65 * @name{string} the name of the service
76 66 * @service{string} the service descriptor to register
77 67 */
78 register<K extends keyof S>(name: K, service: DescriptorMap<S>[K]) {
68 private _register<K extends keyof S>(name: K, service: DescriptorMap<S>[K]) {
79 69 argumentNotNull(name, "name");
80 70
81 71 const d = this._services[name];
@@ -85,69 +75,37 export class ActivationContext<S> implem
85 75 this._services[name] = service;
86 76 }
87 77
88 ownerSlot<T>(slotId: string | number): ILifetimeSlot<T> {
89 return this._lifetimeManagers[this._service.level].slot(slotId);
78 scopeSlot<T>(level: number, slotId: string | number): ILifetimeSlot<T> {
79 if (level < 0 || level >= this._scope.length)
80 throw new Error("The scope level is out of range");
81 return this._scope[level].slot(slotId);
90 82 }
91 83
92 containerSlot<T>(slotId: string | number): ILifetimeSlot<T> {
93 return this._lifetimeManagers[this._lifetimeManagers.length - 1].slot(slotId);
84 hierarchySlot<T>(slotId: string | number): ILifetimeSlot<T> {
85 return this._scope[this._scope.length - 1].slot(slotId);
86 }
87
88 selfContainer(): ServiceLocator<S> {
89 return this._container;
90 }
91
92 createChildContainer(): IContainerBuilder<S, keyof S> {
93 return new ContainerBuilder(this._services, this._scope);
94 94 }
95 95
96 96 contextSlot<T>(slotId: string | number): ILifetimeSlot<T> {
97
98 }
99
100 createLifetime<T>(): ILifetime<T> {
101 const id = nextId++;
102 return {
103 initialize() { },
104 has: () => id in this._cache,
105 get: () => {
106 const v = this._cache[id] as T;
107 if (v === undefined || v === null)
108 throw new Error("The value isn't present in the activation context");
109 return v;
110 },
111 store: item => {
112 this._cache[id] = item;
113 }
114 };
97 return this._contextScope.slot(slotId);
115 98 }
116 99
117 activate<T>(d: Descriptor<S, T>, name: string) {
118 // TODO: add logging
119 // if (trace.isLogEnabled())
120 // trace.log("enter {0} {1}", name, d);
121
122 const ctx = new ActivationContext(
123 this._containerLifetimeManager,
124 d.hasOverrides ? prototype(this._services) : this._services,
125 name,
126 d,
127 this._cache
128 );
129
130 const v = d.activate(ctx);
131
132 // if (trace.isLogEnabled())
133 // trace.log(`leave ${name}`);
134
135 return v;
100 withOverrides<X>(overrides: DescriptorMap<S>, action: () => X) {
101 const services = this._services;
102 this._services = prototype(this._services);
103 try {
104 each(overrides, (v, k) => this._register(k, v));
105 return action();
106 } finally {
107 this._services = services;
136 108 }
137
138 getStack(): ActivationContextInfo[] {
139 const stack = [{
140 name: this._name,
141 service: this._service.toString()
142 }];
143
144 return this._parent ?
145 stack.concat(this._parent.getStack()) :
146 stack;
147 }
148
149 fail(innerException: unknown): never {
150 throw new ActivationError(this._name, this.getStack(), innerException);
151 109 }
152 110
153 111 }
@@ -1,6 +1,6
1 1 export interface ActivationItem {
2 2 name: string;
3 service: string;
3 descriptor: string;
4 4 }
5 5
6 6 /**
@@ -17,11 +17,6 export class ActivationError {
17 17 readonly activationStack: ActivationItem[];
18 18
19 19 /**
20 * The name of the failed service
21 */
22 readonly service: string;
23
24 /**
25 20 * The exception which occurred during activation of the service
26 21 */
27 22 readonly innerException: unknown;
@@ -31,30 +26,32 export class ActivationError {
31 26 */
32 27 readonly message: string;
33 28
34 constructor(service: string, activationStack: ActivationItem[], innerException: unknown) {
35 this.message = "Failed to activate the service";
29 constructor(message: string, activationStack: ActivationItem[], innerException?: unknown) {
30 this.message = message;
36 31 this.activationStack = activationStack;
37 this.service = service;
38 32 this.innerException = innerException;
39 33 }
40 34
41 35 toString() {
42 36 const parts = [this.message];
43 if (this.service)
44 parts.push(`when activating: ${String(this.service)}`);
37
38 if (this.activationStack && this.activationStack.length) {
39 const [{ name, descriptor }, ...before] = this.activationStack;
40
41 parts.push(`when activating: ${name}, ${descriptor}`);
42
43 if (before) {
44 parts.push("at");
45 parts.push.apply(
46 null,
47 before.map(({ name: name, descriptor: service }) => ` ${name} ${service}`)
48 );
49 }
50 }
45 51
46 52 if (this.innerException)
47 53 parts.push(`caused by: ${String(this.innerException)}`);
48 54
49 if (this.activationStack) {
50 parts.push("at");
51 parts.push.apply(null,
52 this.activationStack
53 .map(({ name, service }) => ` ${name} ${service}`)
54 );
55
56 }
57
58 55 return parts.join("\n");
59 56 }
60 57 }
@@ -1,23 +1,34
1 1 import { ActivationContext } from "./ActivationContext";
2 import { ActivationError } from "./ActivationError";
3 2 import { ContainerBuilder } from "./ContainerBuilder";
4 import { LifetimeManager } from "./LifetimeManager";
5 import { DescriptorMap, IContainerBuilder, IDestroyable, ILifetimeManager, ServiceLocator } from "./interfaces";
3 import { LifetimeManager, emptySlot } from "./LifetimeManager";
4 import { DescriptorMap, IContainerBuilder, IDestroyable, ILifetimeManager, ILifetimeSlot, ServiceLocator } from "../typings/interfaces";
5
6 let nextId = 1;
6 7
7 8 export class Container<S> implements ServiceLocator<S>, IDestroyable {
8 9 private readonly _services: DescriptorMap<S>;
9 10
10 private readonly _lifetimeManagers: ILifetimeManager[];
11 private readonly _scope: ILifetimeManager[];
12
13 private readonly _containerId = `container-${nextId++}`;
14
15 private readonly _slot: ILifetimeSlot<this>;
11 16
12 17 private _disposed: boolean;
13 18
14 private readonly _onDestroyed: () => void;
15
16 constructor(services: DescriptorMap<S>, lifetimeManagers: ILifetimeManager[], destroyed: () => void) {
19 constructor(services: DescriptorMap<S>, parentScope: ILifetimeManager[]) {
17 20 this._services = services;
18 21 this._disposed = false;
19 this._lifetimeManagers = lifetimeManagers.concat(new LifetimeManager());
20 this._onDestroyed = destroyed;
22 this._scope = parentScope.concat(new LifetimeManager());
23
24 // If this container is created inside the parent container scope,
25 // allocated lifetime slot
26 this._slot = parentScope.length ?
27 parentScope[parentScope.length - 1].slot(this._containerId) :
28 emptySlot<this>();
29
30 // store the container reference in the lifetime slot
31 this._slot.store(this);
21 32
22 33 }
23 34
@@ -29,9 +40,7 export class Container<S> implements Ser
29 40 createChildBuilder(): IContainerBuilder<S, keyof S> {
30 41 this._assertNotDestroyed();
31 42
32 const lifetime = this._lifetimeManager.create<IDestroyable>();
33
34 return new ContainerBuilder(this._services, lifetime);
43 return new ContainerBuilder(this._services, this._scope);
35 44 }
36 45
37 46 resolve<K extends keyof S>(name: K): NonNullable<S[K]>;
@@ -39,26 +48,24 export class Container<S> implements Ser
39 48 resolve<K extends keyof S, T>(name: K, def?: T) {
40 49 this._assertNotDestroyed();
41 50
42 const d = this._services[name];
43 if (d === undefined) {
44 if (arguments.length > 1)
45 return def;
46 else
47 throw new Error(`Service '${String(name)}' isn't found`);
48 } else {
49 const context = new ActivationContext(this._lifetimeManagers, this._services, String(name), d);
50 try {
51 return d.activate(context);
52 } catch (error) {
53 throw new ActivationError(name.toString(), context.getStack(), error);
51 const context = new ActivationContext(this, this._scope, this._services);
52
53 return arguments.length === 1 ?
54 context.resolve(name, []) :
55 context.resolve(name, [], def);
54 56 }
55 }
56 }
57
57 58 destroy() {
58 59 if (this._disposed)
59 60 return;
60 61 this._disposed = true;
61 this._lifetimeManager.destroy();
62 (0,this._onDestroyed)();
62
63 // destroy own scope
64 this._scope[this._scope.length - 1].destroy();
65
66 // release lifetime slot if the container is destroyed before the parent
67 // container. If this container is destroyed during the parent container
68 // cleanup procedure this call will have no effect.
69 this._slot.remove();
63 70 }
64 71 }
@@ -1,9 +1,6
1 1 import { Container } from "./Container";
2 2 import { DescriptorBuilder } from "./DescriptorBuilder";
3 import { containerSelfDescriptor } from "./DescriptorImpl";
4 import { Descriptor, IContainerBuilder, IDescriptorBuilder, DescriptorMap, ServiceLocator, ILifetime, IDestroyable, ContainerServices, ContainerServicesConstraint } from "./interfaces";
5 import { emptyLifetime, LifetimeManager } from "./LifetimeManager";
6 import { isDestroyable, prototype } from "./traits";
3 import { Descriptor, IContainerBuilder, IDescriptorBuilder, DescriptorMap, ServiceLocator, ILifetimeManager } from "../typings/interfaces";
7 4
8 5 /**
9 6 * Container builder used to prepare service descriptors and create a IoC container
@@ -13,20 +10,20 export class ContainerBuilder<S, U exten
13 10
14 11 private _pending = 1;
15 12
16 private readonly _services: DescriptorMap<ContainerServices<S>>;
13 private readonly _services: DescriptorMap<S>;
17 14
18 private readonly _lifetimeManager = new LifetimeManager();
15 private readonly _scope: ILifetimeManager[];
19 16
20 private readonly _lifetime: ILifetime<IDestroyable>;
17 private readonly _level: number;
21 18
22 constructor(parentServices: DescriptorMap<S> | null = null, lifetime?: ILifetime<IDestroyable>) {
19 constructor(parentServices: DescriptorMap<S> | null, scope: ILifetimeManager[] = []) {
23 20 this._services = { ...parentServices }; // create a copy
24 this._lifetime = lifetime ?? emptyLifetime();
21 this._level = scope.length;
22 this._scope = scope;
25 23 }
26 createServiceBuilder<K extends U>(name: K):
27 IDescriptorBuilder<S, S[K], Record<never, never>, U> {
24 createServiceBuilder<K extends U>(name: K): IDescriptorBuilder<S, S[K], Record<never, never>, U> {
28 25
29 return new DescriptorBuilder(this._lifetimeManager, this._register(name), this._fail);
26 return new DescriptorBuilder(this._level, String(name), this._register(name), this._fail);
30 27
31 28 }
32 29
@@ -35,13 +32,7 export class ContainerBuilder<S, U exten
35 32 if (!this._complete())
36 33 throw new Error("The configuration didn't complete.");
37 34
38 const {remove, store} = this._lifetime(null);
39
40 const container = new Container(this._services, this._lifetimeManager, remove);
41
42 store(container);
43
44 return container;
35 return new Container(this._services, this._scope);
45 36 }
46 37
47 38 private readonly _register = <K extends U>(name: K) =>
@@ -1,7 +1,7
1 import { BuildDescriptorFn, IDescriptorBuilder, DepsMap, Resolve, DescriptorMap } from "./interfaces";
2 import { Descriptor, ILifetime, ActivationType } from "./interfaces";
1 import { BuildDescriptorFn, IDescriptorBuilder, DepsMap, Resolve, DescriptorMap } from "../typings/interfaces";
2 import { Descriptor, ILifetime, ActivationType } from "../typings/interfaces";
3 3 import { DescriptorImpl } from "./DescriptorImpl";
4 import { containerLifetime, contextLifetime, emptyLifetime, hierarchyLifetime, LifetimeManager, singletonLifetime } from "./LifetimeManager";
4 import { contextLifetime, emptyLifetime, hierarchyLifetime, scopeLifetime, singletonLifetime } from "./LifetimeManager";
5 5 import { each, isPromise, isString, key, oid } from "./traits";
6 6
7 7 /**
@@ -15,6 +15,10 export class DescriptorBuilder<S, T, R,
15 15
16 16 private readonly _refs: DepsMap<R>;
17 17
18 private readonly _level: number;
19
20 private readonly _instanceId: string | number;
21
18 22 private _lifetime: ILifetime<T> = emptyLifetime<T>();
19 23
20 24 private _overrides: DescriptorMap<S>;
@@ -36,11 +40,13 export class DescriptorBuilder<S, T, R,
36 40 * @param cb The callback to receive the built service descriptor
37 41 * @param eb The callback to receive the error due
38 42 */
39 constructor(cb: (d: Descriptor<S, T>) => void, eb: (err: unknown) => void) {
43 constructor(level: number, instanceId: string | number, cb: (d: Descriptor<S, T>) => void, eb: (err: unknown) => void) {
40 44 this._cb = cb;
41 45 this._eb = eb;
42 46 this._overrides = {};
43 47 this._refs = {};
48 this._level = level;
49 this._instanceId = instanceId;
44 50 }
45 51
46 52 /** Declares dependencies to be consumed in the factory method */
@@ -85,6 +91,8 export class DescriptorBuilder<S, T, R,
85 91 if (builder) {
86 92 this._defer();
87 93 const d = new DescriptorBuilder<S, NonNullable<S[K]>, object, U>(
94 this._level,
95 String(nameOrServices),
88 96 result => {
89 97 this._overrides[nameOrServices] = result;
90 98 this._complete();
@@ -145,11 +153,11 export class DescriptorBuilder<S, T, R,
145 153 _resolveLifetime<T>(activation: ActivationType, typeId?: string | object): ILifetime<T> {
146 154 switch (activation) {
147 155 case "container":
148 return containerLifetime();
156 return scopeLifetime(this._level, this._instanceId);
149 157 case "hierarchy":
150 return hierarchyLifetime();
158 return hierarchyLifetime(this._instanceId);
151 159 case "context":
152 return contextLifetime();
160 return contextLifetime(this._instanceId);
153 161 case "singleton": {
154 162 if (!typeId)
155 163 throw Error("The singleton activation requires a typeId");
@@ -1,9 +1,10
1 import { Descriptor, ILifetime, DepsMap, IActivationContext, DescriptorMap } from "./interfaces";
1 import { Descriptor, ILifetime, IActivationContext, DescriptorMap, DescriptorDepsMap } from "../typings/interfaces";
2 import { ActivationError, ActivationItem } from "./ActivationError";
2 3 import { each, key } from "./traits";
3 4
4 5 export interface DescriptorImplArgs<S, T> {
5 6
6 readonly lifetime: ILifetime<NonNullable<T>>;
7 readonly lifetime: ILifetime<T>;
7 8
8 9 readonly factory: (refs: Record<key, unknown>) => NonNullable<T>;
9 10
@@ -11,90 +12,73 export interface DescriptorImplArgs<S, T
11 12
12 13 readonly overrides?: DescriptorMap<S>;
13 14
14 readonly dependencies?: DepsMap<S>;
15 readonly dependencies?: DescriptorDepsMap<S>;
15 16 }
16 17
17 export const containerSelfDescriptor = <S>() => Object.freeze({
18 level: 0,
19 activate(context: IActivationContext<S>) {
20 return context.createChildContainer();
21 }
22 });
23
24
25 18 export class DescriptorImpl<S, T> implements Descriptor<S, T> {
26 19
27 private readonly _overrides?: DescriptorMap<S>;
20 private readonly _overrides: DescriptorMap<S> | undefined;
28 21
29 private readonly _lifetime: ILifetime<NonNullable<T>>;
22 private readonly _lifetime: ILifetime<T>;
30 23
31 24 private readonly _factory: (refs: Record<key, unknown>) => NonNullable<T>;
32 25
33 private readonly _cleanup?: (item: NonNullable<T>) => void;
26 private readonly _cleanup: (item: NonNullable<T>) => void;
34 27
35 private readonly _deps?: DepsMap<S>;
36
37 readonly hasOverrides: boolean;
28 private readonly _dependencies: DescriptorDepsMap<S> | undefined;
38 29
39 30 constructor({ lifetime, factory, cleanup, overrides, dependencies }: DescriptorImplArgs<S, T>) {
40 31 this._lifetime = lifetime;
41 32 this._factory = factory;
42 if (cleanup)
43 this._cleanup = cleanup;
44 if (overrides)
33 this._cleanup = cleanup ? cleanup : () => { };
45 34 this._overrides = overrides;
46 if (dependencies)
47 this._deps = dependencies;
48
49 this.hasOverrides = !!overrides;
35 this._dependencies = dependencies;
50 36 }
51 37
52 activate(context: IActivationContext<S>): NonNullable<T> {
38 activate(context: IActivationContext<S>, stack: ActivationItem[]): NonNullable<T> {
53 39
54 const { has, get, initialize, store } = this._lifetime(context);
40 const slot = this._lifetime(context);
41 if (slot.has())
42 return slot.get();
55 43
56 if (has())
57 return get();
44 if (!slot.initialize())
45 throw new Error("Cyclic reference detected");
58 46
59 initialize();
47 const instance = this._overrides ?
48 context.withOverrides(this._overrides, () => this._activate(context, stack)) :
49 this._activate(context, stack);
50
51 slot.store(instance, this._cleanup);
60 52
61 if (this._overrides)
62 each(this._overrides, (v, k) => context.register(k, v));
53 return instance;
54 }
63 55
64 const resolve = <K extends keyof S>({ name, lazy, ...opts }: { name: K; lazy?: boolean; default?: S[K] | null; }) => {
56 private _activate(context: IActivationContext<S>, stack: ActivationItem[]) {
57 const refs: Record<key, unknown> = {};
58 if (this._dependencies) {
59 const resolve = <K extends keyof S>({ name, lazy, ...opts }: { name: K; lazy?: boolean; default?: S[K]; }) => {
65 60 if (lazy) {
66 61 return "default" in opts ?
67 () => context.resolve(name, opts.default) :
68 () => context.resolve(name);
62 () => context.resolve(name, stack, opts.default) :
63 () => context.resolve(name, stack);
69 64 } else {
70 65 return "default" in opts ?
71 context.resolve(name, opts.default) :
72 context.resolve(name);
66 context.resolve(name, stack, opts.default) :
67 context.resolve(name, stack);
73 68 }
74 69 };
75 70
76 const deps = this._deps;
77
78 const refs = deps ?
79 Object.keys(deps)
80 .map(k => {
81 const ref = deps[k];
82 return typeof ref !== "object" ?
83 { [k]: resolve({ name: ref }) } :
84 { [k]: resolve(ref) };
85 })
86 .reduce((a, p) => ({ ...a, ...p }), {}) :
87 {};
71 // can throw activation exception
72 each(this._dependencies, (v, k) => {
73 refs[k] = resolve(v);
74 });
75 }
88 76
89 77 try {
90 78 // call the factory method
91 const instance = (0, this._factory)(refs);
92
93 // store the instance
94 store(instance, this._cleanup);
95 return instance;
96 } catch (err) {
97 context.fail(err);
79 return (0, this._factory)(refs);
80 } catch (e) {
81 throw new ActivationError("Error creating instance", stack, e);
98 82 }
99 83 }
100 84
@@ -1,4 +1,4
1 import { ConfigurationMap, ConfigurationMapConstraint, ContainerServices, ContainerServicesConstraint, ExtractRequiredKeys, IContainerBuilder } from "./interfaces";
1 import { ConfigurationMap, ConfigurationMapConstraint, ContainerServices, ContainerServicesConstraint, ExtractRequiredKeys, IContainerBuilder } from "../typings/interfaces";
2 2 import { argumentNotNull, each, isKey } from "./traits";
3 3
4 4 type ContainerExtensionConstraint<X, S> = ContainerServicesConstraint<Pick<S, keyof X & keyof S>>;
@@ -1,13 +1,6
1 import { IDestroyable, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces";
2 import { argumentNotNull, isDestroyable } from "./traits";
3
4 const safeCall = (item: () => void) => {
5 try {
6 item();
7 } catch {
8 // silence!
9 }
10 };
1 import { ILifetime, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "../typings/interfaces";
2 import { LifetimeSlot } from "./LifetimeSlot";
3 import { argumentNotNull } from "./traits";
11 4
12 5 const noop = () => { };
13 6
@@ -18,7 +11,7 const fail = (message: string) => (): ne
18 11 const _emptySlot = Object.freeze({
19 12 has: () => false,
20 13
21 initialize: noop,
14 initialize: () => false,
22 15
23 16 get: fail("The specified item isn't registered with a lifetime manager"),
24 17
@@ -29,105 +22,48 const _emptySlot = Object.freeze({
29 22 cleanup: noop,
30 23 });
31 24
32 const _destroy = (item: unknown) => () => isDestroyable(item) && item.destroy() ;
33
34 const _makeCleanup = <T>(value: T, cleanup?: (item: T) => void) =>
35 cleanup ? () => cleanup(value) : _destroy(value);
36
37 const newSlot = <T>(put: (item: ILifetimeSlot<T>) => void, remove: () => void): ILifetimeSlot<T> => ({
38 has: () => false,
39
40 initialize: () => put(pendingSlot(put, remove)),
41
42 get: fail("The slot doesn't hold a value"),
43
44 store: (value, cleanup) => put(valueSlot(value, cleanup, remove)),
45
46 remove: noop,
47
48 cleanup: noop,
49 });
50
51 const pendingSlot = <T>(put: (item: ILifetimeSlot<T>) => void, remove: () => void): ILifetimeSlot<T> => ({
52 has: () => false,
53
54 get: fail("The value in this slot doesn't exist"),
55
56 initialize: fail("Cyclic reference detected"),
57
58 store: (value, cleanup) => put(valueSlot(value, cleanup, remove)),
59
60 remove,
61
62 cleanup: noop
63 });
64
65 const valueSlot = <T>(value: T, cleanup: ((item: T) => void) | undefined, remove: () => void) => ({
66 has: () => true,
67
68 get: () => value,
69
70 initialize: fail("The slot already has a value"),
71
72 store: fail("The slot already has a value"),
73
74 cleanup: _makeCleanup(value, cleanup),
75
76 remove: remove
77 });
78
79 25 export class LifetimeManager implements ILifetimeManager {
80 26 private _destroyed = false;
81 27
82 28 private readonly _slots: Record<string, ILifetimeSlot<unknown>> = {};
83 29
84 slot<T>(cookie: string | number): ILifetimeSlot<T> {
85 if (cookie in this._slots)
86 return this._slots[cookie] as ILifetimeSlot<T>;
87
88 return newSlot<T>(this._put(cookie), this._remove(cookie));
89 }
90
91 private readonly _put = (id: string | number) => <T>(slot: ILifetimeSlot<T>) => {
92 this._assertNotDestroyed();
93 this._slots[id] = slot as ILifetimeSlot<unknown>;
94 };
95
96 private readonly _remove = (id: string | number) => () => {
97 this._assertNotDestroyed();
98 delete this._slots[id];
99 };
100
101 private _assertNotDestroyed() {
30 slot<T>(slotId: string | number): ILifetimeSlot<T> {
102 31 if (this._destroyed)
103 32 throw new Error("The lifetime manager is destroyed");
104 33
34 if (slotId in this._slots)
35 return this._slots[slotId] as ILifetimeSlot<T>;
36
37 return this._slots[slotId] = new LifetimeSlot<T>(() => delete this._slots[slotId]);
105 38 }
106 39
40
107 41 destroy() {
108 42 if (!this._destroyed) {
109 43 this._destroyed = true;
110 Object.values(this._slots).forEach(({ cleanup }) => safeCall(cleanup));
44 Object.values(this._slots).forEach(slot => {
45 try {
46 slot.cleanup();
47 } catch {
48 // ignore
49 }
50 });
111 51 }
112 52 }
113 53
114 54 }
115 55
116 export const emptyLifetime = <T>() => () => _emptySlot as ILifetimeSlot<T>;
56 export const emptySlot = <T>() => _emptySlot as ILifetimeSlot<T>;
117 57
118 let nextId = 1;
58 export const emptyLifetime = <T>() => emptySlot as ILifetime<T>;
119 59
120 export const containerLifetime = <T>() => {
121 const slotId = nextId ++;
122 return (context: ILifetimeContext) =>
123 context.ownerSlot<T>(slotId);
124 };
60 export const scopeLifetime = <T>(level: number, slotId: string | number) =>
61 (context: ILifetimeContext) =>
62 context.scopeSlot<T>(level, slotId);
125 63
126 export const hierarchyLifetime = <T>() => {
127 const slotId = nextId++;
128 return (context: ILifetimeContext) =>
129 context.containerSlot<T>(slotId);
130 };
64 export const hierarchyLifetime = <T>(slotId: string | number) =>
65 (context: ILifetimeContext) =>
66 context.hierarchySlot<T>(slotId);
131 67
132 68 /**
133 69 * Creates a lifetime instance bound to the current activation context. This
@@ -138,12 +74,9 export const hierarchyLifetime = <T>() =
138 74 *
139 75 * @returns The instance of the lifetime.
140 76 */
141 export const contextLifetime = <T>() => {
142 const slotId = nextId++;
143
144 return (context: ILifetimeContext) =>
77 export const contextLifetime = <T>(slotId: string | number) =>
78 (context: ILifetimeContext) =>
145 79 context.contextSlot<T>(slotId);
146 };
147 80
148 81 const singletons = new LifetimeManager();
149 82
@@ -162,13 +95,3 export const singletonLifetime = <T>(typ
162 95
163 96 return () => singletons.slot<T>(typeId);
164 97 };
165
166 /** Creates a lifetime bound to the specified container. Using this lifetime
167 * will create a single service instance per the specified container.
168 *
169 * @param container The container which will manage the lifetime for the service
170 */
171 export const containerLifetime = <T>(manager: ILifetimeManager) => {
172 const slotId = nextId++;
173 return () => manager.slot<T>(slotId);
174 };
@@ -1,5 +1,5
1 1 import { FluentConfiguration } from "./FluentConfiguration";
2 import { ContainerServices, ContainerServicesConstraint, IDestroyable } from "./interfaces";
2 import { ContainerServicesConstraint, IDestroyable } from "../typings/interfaces";
3 3
4 4 export function fluent<S extends ContainerServicesConstraint<S>>() {
5 5 return new FluentConfiguration<S>();
@@ -1,5 +1,5
1 import { ContainerBuilder } from "./ContainerBuilder";
2 import { key } from "./traits";
1 import { ActivationItem } from "../ts/ActivationError";
2 import { key } from "../ts/traits";
3 3
4 4 export interface IDestroyable {
5 5 destroy(): void;
@@ -32,6 +32,10 export type DepsMap<S> = {
32 32 [k in key]: Refs<S> | keyof S;
33 33 };
34 34
35 export type DescriptorDepsMap<S> = {
36 [k in key]: Refs<S>;
37 };
38
35 39 export type Refs<S> = {
36 40 [k in keyof S]: Ref<k, S[k]>;
37 41 }[keyof S];
@@ -47,7 +51,7 export type Ref<K extends key, D> = {
47 51 * When specified the dependency becomes optional, the default value can be
48 52 * `null` or `undefined`
49 53 */
50 default?: D | null
54 default?: D;
51 55 };
52 56
53 57 export type Lazy<T, L extends boolean> = L extends true ? () => T : T;
@@ -55,14 +59,14 export type Lazy<T, L extends boolean> =
55 59 /** Возвращает тип свойства `default` в типе {@link T} */
56 60 export type InferDefault<T> = T extends { default: infer D } ? D : never;
57 61
58 export type InferLazy<R> = R extends { lazy: infer L } ?
62 export type IsLazy<R> = R extends { lazy: infer L } ?
59 63 L extends true ? true : false :
60 64 false;
61 65 export type Resolve<S, R> =
62 66 R extends keyof S ? NonNullable<S[R]> :
63 67 R extends Ref<infer K, unknown> ?
64 68 K extends keyof S ?
65 Lazy<NonNullable<S[K]> | InferDefault<R>, InferLazy<R>> :
69 Lazy<NonNullable<S[K]> | InferDefault<R>, IsLazy<R>> :
66 70 never :
67 71 never;
68 72
@@ -161,34 +165,38 export interface Descriptor<S, T> {
161 165 /** This flags indicates that this registration can be replaced or overridden. */
162 166 readonly configurable?: boolean;
163 167
164 /** If specified signals the activation context that a new service scope
165 * should be created to isolate service overrides.
166 */
167 readonly hasOverrides?: boolean;
168
169 activate(context: IActivationContext<S>): NonNullable<T>;
168 activate(context: IActivationContext<S>, stack: ActivationItem[]): NonNullable<T>;
170 169 }
171 170
172 171 /** The context used to initialize lifetime instance {@linkcode ILifetime} */
173 172 export interface ILifetimeContext {
174 173
175 ownerSlot<T>(slotId: string | number): ILifetimeSlot<T>;
176
177 174 contextSlot<T>(slotId: string | number): ILifetimeSlot<T>;
178 175
179 containerSlot<T>(slotId: string | number): ILifetimeSlot<T>;
176 /**
177 *
178 * @param slotId
179 */
180 scopeSlot<T>(level: number, slotId: string | number): ILifetimeSlot<T>;
180 181
182 hierarchySlot<T>(slotId: string | number): ILifetimeSlot<T>;
181 183 }
182 184
183 export interface IActivationContext<S> extends ILifetimeContext, ServiceLocator<S> {
185 export interface IActivationContext<S> extends ILifetimeContext {
184 186
185 register<K extends keyof S>(name: K, service: DescriptorMap<S>[K]): void;
186
187 fail(error: unknown): never;
187 /**
188 * @param overrides
189 * @param action
190 * @throws
191 */
192 withOverrides<X>(overrides: DescriptorMap<S>, action: () => X): X;
188 193
189 194 selfContainer(): ServiceLocator<S>;
190 195
191 createChildContainer(): IContainerBuilder<S, Exclude<keyof S, ContainerKeys>>;
196 createChildContainer(): IContainerBuilder<S, keyof S>;
197
198 resolve<K extends keyof S>(name: K, stack: ActivationItem[]): NonNullable<S[K]>;
199 resolve<K extends keyof S, T>(name: K, stack: ActivationItem[], def: T): NonNullable<S[K]> | T;
192 200 }
193 201
194 202 /**
@@ -202,7 +210,7 export type DescriptorMap<S> = {
202 210 [k in keyof S]?: Descriptor<S, S[k]>;
203 211 };
204 212
205 type ContainerKeys = keyof ContainerProvided<object>;
213 export type ContainerKeys = keyof ContainerProvided<object>;
206 214
207 215 export type ContainerProvided<S, U extends keyof S = keyof S> = {
208 216 container: ServiceLocator<ContainerProvided<S>>;
@@ -251,17 +259,28 export type ActivationType = "singleton"
251 259 export type ILifetime<T> = (context: ILifetimeContext) => ILifetimeSlot<T>;
252 260
253 261 export interface ILifetimeSlot<T> {
254 readonly has: () => boolean;
262 has(): boolean;
263
264 get(): NonNullable<T>;
255 265
256 readonly get: () => T;
257
258 readonly initialize: () => void;
266 /**
267 * Initializes the slot, if the slot is already initialized returns false,
268 * otherwise returns true. This method is used to detect cyclic references.
269 */
270 initialize(): boolean;
259 271
260 readonly store: (item: T, cleanup?: (item: T) => void) => void;
272 store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void): void;
261 273
262 readonly remove: () => void;
274 /**
275 * Removes the slot from the lifetime manager. The inner value will not be
276 * disposed. If the slot is cleaned up calling this method has no effect.
277 */
278 remove(): void;
263 279
264 readonly cleanup: () => void;
280 /**
281 * Disposed inner value of this slot.
282 */
283 cleanup(): void;
265 284 }
266 285
267 286 export interface ILifetimeManager extends IDestroyable {
@@ -6,7 +6,6
6 6 "listFiles": true,
7 7 "strict": true,
8 8 "types": [],
9 "target": "ES2018",
10 "lib": ["ES2018", "DOM", "ScriptHost"]
9 "target": "ESNext"
11 10 }
12 11 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now