##// END OF EJS Templates
Added CancelledError, fixed lint warnings
cin -
r172:3969a8fb8049 release v1.4.6 default
parent child
Show More
@@ -0,0 +1,12
1 import { Cancellation } from "./Cancellation";
2 import { ICancellation } from "./interfaces";
3
4 export class CancelledError extends Error {
5 readonly cancellationToken: ICancellation;
6
7 constructor(message = "The operation is cancelled", ct = Cancellation.none) {
8 super(message);
9 this.cancellationToken = ct;
10 this.name = "CancelledError";
11 }
12 }
@@ -1,106 +1,107
1 1 import { CancellationAggregate } from "./CancellationAggregate";
2 import { CancelledError } from "./CancelledError";
2 3 import { ICancellation, IDestroyable } from "./interfaces";
3 4 import { argumentNotNull, destroyed } from "./safe";
4 5
5 6 export class Cancellation implements ICancellation {
6 7 private _reason: any;
7 8 private _cbs: Array<(e: any) => void> | undefined;
8 9
9 10 constructor(action: (cancel: (e?: any) => void) => void) {
10 11 argumentNotNull(action, "action");
11 12
12 13 action(this._cancel.bind(this));
13 14 }
14 15
15 16 isSupported(): boolean {
16 17 return true;
17 18 }
18 19 throwIfRequested(): void {
19 20 if (this._reason)
20 21 throw this._reason;
21 22 }
22 23
23 24 isRequested(): boolean {
24 25 return !!this._reason;
25 26 }
26 27
27 28 register(cb: (e: any) => void): IDestroyable {
28 29 argumentNotNull(cb, "cb");
29 30
30 31 if (this._reason) {
31 32 cb(this._reason);
32 33 return destroyed;
33 34 } else {
34 35 if (!this._cbs)
35 36 this._cbs = [cb];
36 37 else
37 38 this._cbs.push(cb);
38 39
39 40 const me = this;
40 41 return {
41 42 destroy() {
42 43 me._unregister(cb);
43 44 }
44 45 };
45 46 }
46 47 }
47 48
48 49 private _unregister(cb: any) {
49 50 if (this._cbs) {
50 51 const i = this._cbs.indexOf(cb);
51 52 if (i >= 0)
52 53 this._cbs.splice(i, 1);
53 54 }
54 55 }
55 56
56 57 private _cancel(reason: any) {
57 58 if (this._reason)
58 59 return;
59 60
60 this._reason = (reason = reason || new Error("Operation cancelled"));
61 this._reason = (reason = reason || new CancelledError(undefined, this));
61 62
62 63 if (this._cbs) {
63 64 this._cbs.forEach(cb => cb(reason));
64 65 this._cbs = undefined;
65 66 }
66 67 }
67 68
68 69 static readonly none: ICancellation = {
69 70 isSupported(): boolean {
70 71 return false;
71 72 },
72 73
73 74 throwIfRequested(): void {
74 75 },
75 76
76 77 isRequested(): boolean {
77 78 return false;
78 79 },
79 80
80 81 register(_cb: (e: any) => void): IDestroyable {
81 82 return destroyed;
82 83 }
83 84 };
84 85
85 86 /**
86 87 * Combines multiple cancellation tokens to the single aggregated token.
87 *
88 *
88 89 * Aggregated token will be considered as signalled when some tokens are
89 90 * signalled. The cancellation callback can be registered with the `register`
90 91 * method, it will be fired once with the first signalled token, all other
91 92 * tokens will be ignored.
92 *
93 *
93 94 * The tokens which don't support cancellation are filtered out, if there are
94 95 * no tokens left in the list the method returns `Cancellation.none`.
95 *
96 *
96 97 * @param args The list of cancellation tokens to combine
97 * @returns
98 * @returns Aggregated cancellation token
98 99 */
99 100 static combine(...args: ICancellation[]) {
100 101 const tokens = args.filter(ct => ct.isSupported());
101 102 return tokens.length > 1 ?
102 103 new CancellationAggregate(tokens) :
103 tokens.length == 1 ? tokens[0] :
104 tokens.length === 1 ? tokens[0] :
104 105 this.none;
105 106 }
106 107 }
@@ -1,41 +1,41
1 1 import { ICancellation, IDestroyable } from "./interfaces";
2 2
3 3 export class CancellationAggregate implements ICancellation {
4 4 private readonly _tokens: ICancellation[];
5 5
6 6 constructor(tokens: ICancellation[]) {
7 7 this._tokens = tokens || [];
8 8 }
9 9
10 10 throwIfRequested() {
11 11 this._tokens.forEach(ct => ct.throwIfRequested());
12 12 }
13 13
14 14 isRequested() {
15 15 return this._tokens.some(ct => ct.isRequested());
16 16 }
17 17 isSupported() {
18 18 return !!this._tokens.length;
19 19 }
20 20 register(cb: (e: any) => void): IDestroyable {
21 21 let fired = false;
22 22
23 23 const once = (e: any) => {
24 24 if (!fired) {
25 25 fired = true;
26 26 destroy();
27 27 cb(e);
28 28 }
29 }
29 };
30 30
31 31 const destroy = () => subscriptions
32 .splice(0,subscriptions.length) // empty array
32 .splice(0, subscriptions.length) // empty array
33 33 .forEach(subscription => subscription.destroy()); // cleanup
34 34
35 const subscriptions = this._tokens.map(ct => ct.register(once))
36
35 const subscriptions = this._tokens.map(ct => ct.register(once));
36
37 37 return {
38 38 destroy
39 39 };
40 40 }
41 41 }
@@ -1,35 +1,38
1 1 import { Cancellation } from "../Cancellation";
2 2 import { IAsyncComponent, ICancellation, ICancellable } from "../interfaces";
3 3
4 4 const noop = () => void (0);
5 5
6 6 export class AsyncComponent implements IAsyncComponent, ICancellable {
7 7 _cancel: ((e: any) => void) = noop;
8 8
9 9 _completion: Promise<void> = Promise.resolve();
10 10
11 11 getCompletion() { return this._completion; }
12 12
13 13 runOperation(op: (ct: ICancellation) => any, ct: ICancellation = Cancellation.none) {
14 14 // create inner cancellation bound to the passed cancellation token
15 15 const inner = new Cancellation(cancel => {
16 16 this._cancel = cancel;
17 17 });
18 18
19 19 const guard = async () => {
20 20 try {
21 return op(Cancellation.combine(ct, inner));
21 const combined = Cancellation.combine(ct, inner);
22 const result = await op(combined);
23 combined.throwIfRequested();
24 return result;
22 25 } finally {
23 26 // after the operation is complete we need to cleanup the
24 27 // resources
25 28 this._cancel = noop;
26 29 }
27 30 };
28 31
29 32 return this._completion = guard();
30 33 }
31 34
32 35 cancel(reason: any) {
33 36 this._cancel(reason);
34 37 }
35 38 }
@@ -1,216 +1,216
1 1 import { IDestroyable, MapOf } from "../interfaces";
2 2 import { argumentNotNull, isDestroyable, argumentNotEmptyString, isRemovable } from "../safe";
3 3 import { ILifetime, ServiceContainer } from "./interfaces";
4 4 import { ActivationContext } from "./ActivationContext";
5 5
6 6 function safeCall(item: () => void) {
7 7 try {
8 8 item();
9 9 } catch {
10 10 // silence!
11 11 }
12 12 }
13 13
14 14 const emptyLifetime: ILifetime = Object.freeze({
15 15 has() {
16 16 return false;
17 17 },
18 18
19 19 initialize() {
20 20
21 21 },
22 22
23 23 get() {
24 24 throw new Error("The specified item isn't registered with this lifetime manager");
25 25 },
26 26
27 27 store() {
28 28 // does nothing
29 29 },
30 30
31 31 toString() {
32 32 return `[object EmptyLifetime]`;
33 33 }
34 34
35 35 });
36 36
37 37 const unknownLifetime: ILifetime = Object.freeze({
38 38 has() {
39 39 return false;
40 40 },
41 41 initialize() {
42 42 throw new Error("Can't call initialize on the unknown lifetime object");
43 43 },
44 44 get() {
45 45 throw new Error("The lifetime object isn't initialized");
46 46 },
47 47 store() {
48 48 throw new Error("Can't store a value in the unknown lifetime object");
49 49 },
50 50 toString() {
51 51 return `[object UnknownLifetime]`;
52 52 }
53 53 });
54 54
55 55 let nextId = 0;
56 56
57 57 const singletons: any = {};
58 58
59 59 export class LifetimeManager implements IDestroyable {
60 60 private _cleanup: (() => void)[] = [];
61 61 private _cache: MapOf<any> = {};
62 62 private _destroyed = false;
63 63
64 64 private _pending: MapOf<boolean> = {};
65 65
66 66 create(): ILifetime {
67 67 const self = this;
68 68 const id = ++nextId;
69 69 return {
70 70 has() {
71 71 return (id in self._cache);
72 72 },
73 73
74 74 get() {
75 75 const t = self._cache[id];
76 76 if (t === undefined)
77 77 throw new Error(`The item with with the key ${id} isn't found`);
78 78 return t;
79 79 },
80 80
81 81 initialize() {
82 82 if (self._pending[id])
83 83 throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`);
84 84 self._pending[id] = true;
85 85 },
86 86
87 87 store(item: any, cleanup?: (item: any) => void) {
88 88 argumentNotNull(id, "id");
89 89 argumentNotNull(item, "item");
90 90
91 91 if (this.has())
92 92 throw new Error(`The item with with the key ${id} already registered with this lifetime manager`);
93 93 delete self._pending[id];
94 94
95 95 self._cache[id] = item;
96 96
97 97 if (self._destroyed)
98 98 throw new Error("Lifetime manager is destroyed");
99 99 if (cleanup) {
100 100 self._cleanup.push(() => cleanup(item));
101 101 } else if (isDestroyable(item)) {
102 102 self._cleanup.push(() => item.destroy());
103 103 }
104 104 }
105 105 };
106 106 }
107 107
108 108 destroy() {
109 109 if (!this._destroyed) {
110 110 this._destroyed = true;
111 111 this._cleanup.forEach(safeCall);
112 112 this._cleanup.length = 0;
113 113 }
114 114 }
115 115
116 116 static empty(): ILifetime {
117 117 return emptyLifetime;
118 118 }
119 119
120 120 static hierarchyLifetime() {
121 121 let _lifetime = unknownLifetime;
122 122 return {
123 123 initialize(context: ActivationContext<any>) {
124 124 if (_lifetime !== unknownLifetime)
125 125 throw new Error("Cyclic reference activation detected");
126 126
127 127 _lifetime = context.getContainer().getLifetimeManager().create();
128 128 },
129 129 get() {
130 130 return _lifetime.get();
131 131 },
132 132 has() {
133 133 return _lifetime.has();
134 134 },
135 135 store(item: any, cleanup?: (item: any) => void) {
136 136 return _lifetime.store(item, cleanup);
137 137 },
138 138 toString() {
139 139 return `[object HierarchyLifetime, has=${this.has()}]`;
140 140 }
141 141 };
142 142 }
143 143
144 144 static contextLifetime() {
145 145 let _lifetime = unknownLifetime;
146 146 return {
147 147 initialize(context: ActivationContext<any>) {
148 148 if (_lifetime !== unknownLifetime)
149 149 throw new Error("Cyclic reference detected");
150 150 _lifetime = context.createLifetime();
151 151 },
152 152 get() {
153 153 return _lifetime.get();
154 154 },
155 155 has() {
156 156 return _lifetime.has();
157 157 },
158 158 store(item: any) {
159 159 _lifetime.store(item);
160 160 },
161 161 toString() {
162 162 return `[object ContextLifetime, has=${this.has()}]`;
163 163 }
164 164 };
165 165 }
166 166
167 167 static singletonLifetime(typeId: string) {
168 168 argumentNotEmptyString(typeId, "typeId");
169 169 let pending = false;
170 170 return {
171 171 has() {
172 172 return typeId in singletons;
173 173 },
174 174 get() {
175 175 if (!this.has())
176 176 throw new Error(`The instance ${typeId} doesn't exists`);
177 177 return singletons[typeId];
178 178 },
179 179 initialize() {
180 180 if (pending)
181 181 throw new Error("Cyclic reference detected");
182 182 pending = true;
183 183 },
184 184 store(item: any) {
185 185 singletons[typeId] = item;
186 186 pending = false;
187 187 },
188 188 toString() {
189 189 return `[object SingletonLifetime, has=${this.has()}, typeId=${typeId}]`;
190 190 }
191 191 };
192 192 }
193 193
194 194 static containerLifetime(container: ServiceContainer<any>) {
195 195 let _lifetime = unknownLifetime;
196 196 return {
197 197 initialize(context: ActivationContext<any>) {
198 198 if (_lifetime !== unknownLifetime)
199 199 throw new Error("Cyclic reference detected");
200 200 _lifetime = container.getLifetimeManager().create();
201 201 },
202 202 get() {
203 203 return _lifetime.get();
204 204 },
205 205 has() {
206 206 return _lifetime.has();
207 207 },
208 208 store(item: any) {
209 209 _lifetime.store(item);
210 210 },
211 211 toString() {
212 return `[object ContainerLifetime, has=${_lifetime.has()}]`
212 return `[object ContainerLifetime, has=${_lifetime.has()}]`;
213 213 }
214 214 };
215 215 }
216 216 }
@@ -1,126 +1,164
1 1 export interface Constructor<T = {}> {
2 2 new(...args: any[]): T;
3 3 prototype: T;
4 4 }
5 5
6 6 export type PromiseOrValue<T> = T | PromiseLike<T>;
7 7
8 8 export type Factory<T = {}> = (...args: any[]) => T;
9 9
10 10 export type Predicate<T = any> = (x: T) => boolean;
11 11
12 12 export type MatchingMemberKeys<T, U> = { [K in keyof T]: T[K] extends U ? K : never}[keyof T];
13 13
14 14 export type NotMatchingMemberKeys<T, U> = { [K in keyof T]: T[K] extends U ? never : K}[keyof T];
15 15
16 16 export type ExtractMembers<T, U> = Pick<T, MatchingMemberKeys<T, U>>;
17 17
18 18 export type ExcludeMembers<T, U> = Pick<T, NotMatchingMemberKeys<T, U>>;
19 19
20 20 export interface MapOf<T> {
21 21 [key: string]: T;
22 22 }
23 23
24 24 export interface IDestroyable {
25 25 destroy(): void;
26 26 }
27 27
28 28 export interface IRemovable {
29 29 remove(): void;
30 30 }
31 31
32 /**
33 * Interface for the cancellation token. Cancellation token is
34 * a marker indicating that the cancellation was requested, it
35 * is up to the operation to decide whether to interrupt or
36 * to complete its execution.
37 *
38 * This interface defines several methods of interaction with
39 * the cancellation i.e. either to poll its status or to react
40 * through the callback.
41 */
32 42 export interface ICancellation {
43 /**
44 * Throws an exception if the cancellation has been requested,
45 * otherwise does nothing.
46 */
33 47 throwIfRequested(): void;
48
49 /**
50 * Checks whether the cancellation is requested.
51 * @returns true is the cancellation has been requested,
52 * otherwise returns false.
53 */
34 54 isRequested(): boolean;
55
56 /**
57 * Checks the ability of the token to be signaled.
58 *
59 * @returns true if the token is able to request
60 * the cancellation, false otherwise.
61 */
35 62 isSupported(): boolean;
63
64 /**
65 * Registers the callback to be called when the cancellation
66 * is requested.
67 *
68 * @param cb The callback which receives the reason of the
69 * cancellation.
70 * @returns The subscription, after the operation is completed
71 * it should unregister the callback to free resources by
72 * calling the `destroy()` method of the subscription.
73 */
36 74 register(cb: (e: any) => void): IDestroyable;
37 75 }
38 76
39 77 /**
40 78 * Π˜Π½Ρ‚Π΅Ρ€Ρ„Π΅ΠΉΡ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‰ΠΈΠΉ Π°ΡΠΈΠ½Ρ…Ρ€ΠΎΠ½Π½ΡƒΡŽ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΡŽ
41 79 */
42 80 export interface IActivatable {
43 81 /**
44 82 * @returns Boolean indicates the current state
45 83 */
46 84 isActive(): boolean;
47 85
48 86 /**
49 87 * Starts the component activation
50 88 * @param ct cancellation token for this operation
51 89 */
52 90 activate(ct?: ICancellation): Promise<void>;
53 91
54 92 /**
55 93 * Starts the component deactivation
56 94 * @param ct cancellation token for this operation
57 95 */
58 96 deactivate(ct?: ICancellation): Promise<void>;
59 97
60 98 /**
61 99 * Sets the activation controller for this component
62 100 * @param controller The activation controller
63 101 *
64 102 * Activation controller checks whether this component
65 103 * can be activated and manages the active state of the
66 104 * component
67 105 */
68 106 setActivationController(controller: IActivationController): void;
69 107
70 108 /** Indicates whether this component has an activation controller */
71 109 hasActivationController(): boolean;
72 110
73 111 /**
74 112 * Gets the current activation controller for this component
75 113 */
76 114 getActivationController(): IActivationController;
77 115 }
78 116
79 117 export interface IActivationController {
80 118 activating(component: IActivatable, ct?: ICancellation): Promise<void>;
81 119
82 120 activated(component: IActivatable, ct?: ICancellation): Promise<void>;
83 121
84 122 deactivating(component: IActivatable, ct?: ICancellation): Promise<void>;
85 123
86 124 deactivated(component: IActivatable, ct?: ICancellation): Promise<void>;
87 125
88 126 deactivate(ct?: ICancellation): Promise<void>;
89 127
90 128 activate(component: IActivatable, ct?: ICancellation): Promise<void>;
91 129
92 130 hasActive(): boolean;
93 131
94 132 getActive(): IActivatable;
95 133 }
96 134
97 135 export interface IAsyncComponent {
98 136 getCompletion(): Promise<void>;
99 137 }
100 138
101 139 export interface ICancellable {
102 140 cancel(reason?: any): void;
103 141 }
104 142
105 143 export interface IObservable<T> {
106 144 on(next: (x: T) => void, error?: (e: any) => void, complete?: () => void): IDestroyable;
107 145 next(ct?: ICancellation): Promise<T>;
108 146 }
109 147
110 148 export interface IObserver<T> {
111 149 next(event: T): void;
112 150
113 151 error(e: any): void;
114 152
115 153 complete(): void;
116 154 }
117 155
118 156 export interface TextWriter {
119 157 write(obj: any): void;
120 158 write(format: string, ...args: any[]): void;
121 159
122 160 writeLine(obj?: any): void;
123 161 writeLine(format: string, ...args: any[]): void;
124 162
125 163 writeValue(value: any, spec?: string): void;
126 164 }
@@ -1,80 +1,80
1 1 import { test } from "./TestTraits";
2 2 import { fluent } from "../di/traits";
3 3 import { Bar } from "../mock/Bar";
4 4 import { Container } from "../di/Container";
5 5 import { Foo } from "../mock/Foo";
6 6 import { Box } from "../mock/Box";
7 7 import { delay } from "../safe";
8 8 import { FooServices, Services } from "../mock/services";
9 9 import { ContainerConfiguration } from "../di/fluent/interfaces";
10 10
11 11 test("Simple fluent config", async t => {
12 12 const config = fluent<{ host: string; bar: Bar; foo: Foo }>()
13 13 .register({
14 14 host: it => it.value("example.com"),
15 15 bar: it => it.factory(resolve => new Bar({ host: resolve("host") }, "s-bar")),
16 16 foo: it => import("../mock/Foo").then(m => it.lifetime("container").factory(() => new m.Foo()))
17 17 });
18 18
19 19 const c1 = new Container<{}>();
20 20 const container = await config.apply(c1);
21 21
22 22 t.equal(container.resolve("host"), "example.com", "The value should be resolved");
23 23 t.assert(container.resolve("bar"), "The service should de activated");
24 24 t.equal(container.resolve("foo"), container.resolve("foo"), "The service should be activated once");
25 25 });
26 26
27 27 test("Nested async configuration", async t => {
28 28 const container = await new Container<{
29 29 foo: Foo;
30 30 box: Box<Foo>
31 31 }>().fluent({
32 32 foo: it => delay(0).then(() => it.factory(() => new Foo())),
33 33 box: it => it.lifetime("context").factory($dependency => new Box($dependency("foo")))
34 34 });
35 35
36 36 t.assert(container.resolve("box").getValue(), "The dependency should be set");
37 t.equals(container.resolve("box").getValue(), container.resolve("box").getValue(), "The service should be activated once")
37 t.equals(container.resolve("box").getValue(), container.resolve("box").getValue(), "The service should be activated once");
38 38 });
39 39
40 40 test("Bad fluent config", async t => {
41 41 try {
42 42 await new Container<{
43 43 foo: Foo;
44 44 box: Box<Foo>
45 45 }>().fluent({
46 46 foo: it => delay(0).then(() => it.factory(() => new Foo())),
47 47 box: it => it.lifetime("context")
48 48 .override("foo", () => { throw new Error("bad override"); })
49 49 .factory($dependency => new Box($dependency("foo")))
50 50 });
51 51 t.fail("Should throw");
52 52 } catch (e) {
53 53 t.pass("The configuration should fail");
54 54 t.equal(e.message, "bad override", "the error should pass");
55 55 }
56 56 });
57 57
58 58 test("Load fluent config", async t => {
59 59 const container = new Container<Services>();
60 60
61 61 await container.configure("../mock/config", { contextRequire: require });
62 62
63 63 t.assert(container.resolve("host"), "Should resolve simple value");
64 64 });
65 65
66 66 test("Container applyConfig", async t => {
67 67 const container = await new Container<{}>().applyConfig(import("../mock/config"));
68 68
69 69 t.assert(container.resolve("host"), "Should resolve simple value");
70 70 });
71 71
72 72 test("Child container config", async t => {
73 73 const container = await new Container<{}>().applyConfig(import("../mock/config"));
74
74
75 75 const fooServices: ContainerConfiguration<FooServices> = (await import("../mock/config2")).default;
76 76
77 77 const child = await fooServices.apply(container.createChildContainer());
78 78
79 79 t.assert(child.resolve("foo"), "foo should be resolved");
80 80 });
General Comments 0
You need to be logged in to leave comments. Login now