##// END OF EJS Templates
Async operation cancellation proposal...
cin -
r9:c1c00bfb5487 propose cancellat...
parent child
Show More
@@ -0,0 +1,25
1 @startuml
2
3 participant Component as a
4 participant Other as b
5
6 [-> a : activate(ct)
7 activate a
8 <-- a : promise
9 a -> a : onActivating(ct)
10 activate a
11 a -> b : doAsyncWork(ct)
12 deactivate a
13 deactivate a
14 activate b
15
16 [-> b : ct.cancel
17 b --> a : reject(Cancelled)
18 deactivate b
19 activate a
20
21 a -> a : setFailState()
22
23 [<-- a : reject(Cancelled)
24
25 @enduml No newline at end of file
@@ -0,0 +1,20
1 import { ICancellation } from "./ICancellation";
2
3 export class EmptyCancellation implements ICancellation {
4 isSupported(): boolean {
5 return false;
6 }
7 throwIfRequested(): void {
8 }
9
10 isRequested(): boolean {
11 return false;
12 }
13
14 register(_cb: () => void): void {
15
16 }
17
18 static readonly default : EmptyCancellation = new EmptyCancellation();
19
20 } No newline at end of file
@@ -0,0 +1,6
1 export interface ICancellation {
2 throwIfRequested(): void;
3 isRequested(): boolean;
4 isSupported(): boolean;
5 register(cb: () => void): void;
6 } No newline at end of file
@@ -0,0 +1,92
1 import { IActivationController } from './IActivationController';
2 import { IActivatable } from './IActivatable';
3 import { AsyncComponent } from './AsyncComponent';
4 import { ICancellation } from '../ICancellation';
5 import { EmptyCancellation } from '../EmptyCancellation';
6
7 type Constructor<T = {}> = new (...args: any[]) => T;
8
9 function ActivatableMixin<TBase extends Constructor<AsyncComponent>>(Base: TBase) {
10 return class extends Base implements IActivatable {
11 _controller: IActivationController;
12
13 _active: boolean;
14
15 isActive() {
16 return this._active;
17 }
18
19 getActivationController() {
20 return this._controller;
21 }
22
23 setActivationController(controller: IActivationController) {
24 this._controller = controller;
25 }
26
27 async onActivating(ct: ICancellation) {
28 if (this._controller)
29 await this._controller.activating(this, ct);
30 }
31
32 async onActivated(ct: ICancellation) {
33 if (this._controller)
34 await this._controller.activated(this, ct);
35 }
36
37 async activate(ct: ICancellation = EmptyCancellation.default) {
38 if (this.isActive())
39 return;
40 ct = this.startOperation(ct);
41 try {
42 await this.onActivating(ct);
43 this._active = true;
44 try {
45 await this.onActivated(ct);
46 } catch {
47 // TODO log error
48 }
49 this.completeSuccess();
50 } catch (e) {
51 this.completeFail(e);
52 }
53 return this.getCompletion();
54 }
55
56 async onDeactivating(ct: ICancellation) {
57 if (this._controller)
58 await this._controller.deactivating(this, ct);
59 }
60
61 async onDeactivated(ct: ICancellation) {
62 if (this._controller)
63 await this._controller.deactivated(this, ct);
64 }
65
66 async deactivate(ct: ICancellation = EmptyCancellation.default) {
67 if (!this.isActive())
68 return;
69 ct = this.startOperation(ct);
70 try {
71 await this.onDeactivating(ct);
72 this._active = false;
73 try {
74 await this.onDeactivated(ct);
75 } catch {
76 // TODO log error
77 }
78 this.completeSuccess();
79 } catch (e) {
80 this.completeFail(e);
81 }
82 return this.getCompletion();
83 }
84
85 }
86 }
87
88 namespace ActivatableMixin {
89
90 }
91
92 export = ActivatableMixin; No newline at end of file
@@ -0,0 +1,47
1 import { ICancellation } from "../ICancellation";
2 import { EmptyCancellation } from "../EmptyCancellation";
3
4 export class AsyncComponent {
5 _completion: Promise<void>;
6
7 _deferred: {
8 resolve(): void
9 reject(reason: any): void
10 };
11
12 getCompletion() { return this._completion };
13
14 startOperation(ct: ICancellation = EmptyCancellation.default) {
15 if (this._deferred)
16 throw new Error("The async operation is already pending");
17
18 this._completion = new Promise<void>((resolve, reject) => {
19 this._deferred = {
20 resolve: resolve,
21 reject: reject
22 }
23 });
24 return ct;
25 }
26
27 completeSuccess() {
28 this._deferred.resolve();
29 this._deferred = null;
30 }
31
32 completeFail(reason: any) {
33 this._deferred.reject(reason);
34 this._deferred = null;
35 }
36
37 async runOperation(cb: (ct: ICancellation) => Promise<void>, ct: ICancellation = EmptyCancellation.default) {
38 //safe.argumentNotNull(cb, "cb")
39 ct = this.startOperation(ct);
40 try {
41 await cb(ct);
42 this.completeSuccess();
43 } catch(e) {
44 this.completeFail(e);
45 }
46 }
47 } No newline at end of file
@@ -0,0 +1,39
1 import { IActivationController } from "./IActivationController";
2 import { ICancellation } from "../ICancellation";
3
4 /**
5 * Интерфейс поддерживающий асинхронную активацию
6 */
7 export interface IActivatable {
8 /**
9 * @returns Boolean indicates the current state
10 */
11 isActive(): boolean;
12
13 /**
14 * Starts the component activation
15 * @param ct cancellation token for this operation
16 */
17 activate(ct?: ICancellation): Promise<void>;
18
19 /**
20 * Starts the component deactivation
21 * @param ct cancellation token for this operation
22 */
23 deactivate(ct?: ICancellation): Promise<void>;
24
25 /**
26 * Sets the activation controller for this component
27 * @param controller The activation controller
28 *
29 * Activation controller checks whether this component
30 * can be activated and manages the active state of the
31 * component
32 */
33 setActivationController(controller: IActivationController);
34
35 /**
36 * Gets the current activation controller for this component
37 */
38 getActivationController(): IActivationController;
39 } No newline at end of file
@@ -0,0 +1,19
1 import { IActivatable } from './IActivatable';
2 import { ICancellation } from '../ICancellation';
3 import { EmptyCancellation } from '../EmptyCancellation';
4
5 export interface IActivationController {
6 activating(component: IActivatable, ct?: ICancellation): Promise<void>;
7
8 activated(component: IActivatable, ct?: ICancellation): Promise<void>;
9
10 deactivating(component: IActivatable, ct?: ICancellation): Promise<void>;
11
12 deactivated(component: IActivatable, ct?: ICancellation): Promise<void>;
13
14 deactivate(ct?: ICancellation): Promise<void>;
15
16 activate(component: IActivatable, ct?: ICancellation): Promise<void>;
17
18 getActive(): IActivatable;
19 } No newline at end of file
@@ -0,0 +1,20
1 import * as TraceSource from './TraceSource'
2
3 class TraceEventArgs {
4 source : TraceSource
5
6 message : string
7
8 level : number
9
10 constructor(source: TraceSource, message: string) {
11 this.source = source;
12 this.message = message;
13 }
14 }
15
16 namespace TraceEventArgs {
17
18 }
19
20 export = TraceEventArgs No newline at end of file
@@ -0,0 +1,116
1 import * as TraceEventArgs from './TraceEventArgs'
2 import * as format from '../text/format'
3
4 interface Handler {
5 (arg: TraceEventArgs): void;
6 }
7
8 interface Destroyable {
9 destroy();
10 }
11
12 class HandlerDescriptor implements Destroyable {
13 private _target: TraceSource
14
15 readonly handler: Handler;
16
17 constructor(target: TraceSource, handler: Handler) {
18 this._target = target;
19 this.handler = handler;
20 }
21
22 destroy() {
23 this._target.remove(this);
24 }
25 }
26
27
28 class TraceSource {
29 readonly id: any
30
31 private _handlers: Array<HandlerDescriptor>
32
33 level: number
34
35 constructor(id: any) {
36 this.id = id || new Object();
37 this._handlers = new Array<HandlerDescriptor>();
38 }
39
40 on(handler: Handler): Destroyable {
41 if (!handler)
42 throw new Error("A handler must be specified");
43
44 let d = new HandlerDescriptor(this, handler)
45
46 this._handlers.push(d);
47
48 return d;
49 }
50
51 remove(cookie: any): void {
52 let i = this._handlers.indexOf(cookie);
53 if (i >= 0)
54 this._handlers.splice(i, 1);
55 }
56
57 protected emit(level: number, msg: string, ...args: any[]) {
58 if (level <= this.level) {
59 let event = new TraceEventArgs(this, format(msg, args));
60
61 this._handlers.forEach(d => {
62 try {
63 d.handler.call(null, event);
64 } catch {
65 // suppress error in log handlers
66 }
67 });
68 }
69 }
70
71 isDebugEnabled() {
72 return this.level >= TraceSource.DebugLevel;
73 }
74
75 debug(msg: string, ...args: any[]): void {
76 this.emit(TraceSource.DebugLevel, msg, args);
77 }
78
79 isLogEnabled() {
80 return this.level >= TraceSource.LogLevel;
81 }
82
83 log(msg: string, ...args: any[]): void {
84 this.emit(TraceSource.LogLevel, msg, args);
85 }
86
87 isWarnEnabled() {
88 return this.level >= TraceSource.WarnLevel;
89 }
90
91 warn(msg: string, ...args: any[]): void {
92 this.emit(TraceSource.WarnLevel, msg, args);
93 }
94
95 isErrorEnabled() {
96 return this.level >= TraceSource.ErrorLevel;
97 }
98
99 error(msg: string, ...args: any[]): void {
100 this.emit(TraceSource.ErrorLevel, msg, args);
101 }
102 }
103
104 namespace TraceSource {
105 export const DebugLevel = 400;
106
107 export const LogLevel = 300;
108
109 export const WarnLevel = 200;
110
111 export const ErrorLevel = 100;
112
113 export const SilentLevel = 0;
114 }
115
116 export = TraceSource; No newline at end of file
@@ -0,0 +1,7
1 declare function format(format: string, ...args: any[]): string;
2
3 declare namespace format {
4
5 }
6
7 export = format; No newline at end of file
@@ -0,0 +1,110
1 import * as tape from 'tape';
2 import * as ActivatableMixin from '@implab/core/components/ActivatableMixin';
3 import { AsyncComponent } from '@implab/core/components/AsyncComponent';
4 import { IActivationController } from '@implab/core/components/IActivationController';
5 import { IActivatable } from '@implab/core/components/IActivatable';
6 import { ICancellation } from '@implab/core/ICancellation';
7 import { EmptyCancellation } from '@implab/core/EmptyCancellation';
8
9 class SimpleActivatable extends ActivatableMixin(AsyncComponent) {
10
11 }
12
13 class MockActivationController implements IActivationController {
14
15 _active: IActivatable = null;
16
17
18 getActive() : IActivatable {
19 return this._active;
20 }
21
22 async deactivate() {
23 if (this._active)
24 await this._active.deactivate();
25 this._active = null;
26 }
27
28 async activate(component: IActivatable) {
29 if (!component || component.isActive())
30 return;
31 component.setActivationController(this);
32
33 await component.activate();
34 }
35
36 async activating(component: IActivatable, ct: ICancellation = EmptyCancellation.default) {
37 if (component != this._active)
38 await this.deactivate();
39 }
40
41 async activated(component: IActivatable, ct: ICancellation = EmptyCancellation.default) {
42 this._active = component;
43 }
44
45 async deactivating(component: IActivatable, ct: ICancellation = EmptyCancellation.default) {
46
47 }
48
49 async deactivated(component: IActivatable, ct: ICancellation = EmptyCancellation.default) {
50 if (this._active == component)
51 this._active = null;
52 }
53 }
54
55 tape('simple activation',async function(t){
56
57 let a = new SimpleActivatable();
58 t.false(a.isActive());
59
60 await a.activate();
61 t.true(a.isActive());
62
63 await a.deactivate();
64 t.false(a.isActive());
65
66 t.end();
67 });
68
69 tape('controller activation', async function(t) {
70
71 let a = new SimpleActivatable();
72 let c = new MockActivationController();
73
74 t.false(a.isActive(), "the component is not active by default");
75 t.assert(c.getActive() == null, "the activation controller doesn't have an active component by default");
76 t.assert(a.getActivationController() == null, "the component doesn't have an activation controller by default");
77
78 t.comment("Active the component through the controller");
79 await c.activate(a);
80 t.true(a.isActive(), "The component should successfully activate");
81 t.assert(c.getActive() == a, "The controller should point to the activated component");
82 t.assert(a.getActivationController() == c, "The component should point to the controller");
83
84 t.comment("Deactive the component throug the controller");
85 await c.deactivate();
86
87 t.false(a.isActive(), "The component should successfully deactivate");
88 t.assert(c.getActive() == null, "The controller shouldn't point to any component");
89 t.assert(a.getActivationController() == c, "The componet should point to it's controller");
90
91 t.end();
92 });
93
94 tape('handle error in onActivating', async function(t) {
95 let a = new SimpleActivatable();
96
97 a.onActivating = async function() {
98 throw "Should fail";
99 };
100
101 try {
102 await a.activate();
103 t.fail("activation should fail");
104 } catch {
105 }
106
107 t.false(a.isActive(), "the component should remain inactive");
108
109 t.end();
110 }); No newline at end of file
@@ -7,6 +7,7 def testDir = "$buildDir/test"
7 task clean {
7 task clean {
8 doLast {
8 doLast {
9 delete buildDir
9 delete buildDir
10 delete 'node_modules/@implab'
10 }
11 }
11 }
12 }
12
13
@@ -110,7 +110,7 define([
110 if (typeof (config) === "string") {
110 if (typeof (config) === "string") {
111 p = new Deferred();
111 p = new Deferred();
112 if (!contextRequire) {
112 if (!contextRequire) {
113 var shim = [config, new Uuid()].join(config.indexOf("/") != -1 ? "-" : "/");
113 var shim = [config, Uuid()].join(config.indexOf("/") != -1 ? "-" : "/");
114 define(shim, ["require", config], function (ctx, data) {
114 define(shim, ["require", config], function (ctx, data) {
115 p.resolve([data, {
115 p.resolve([data, {
116 contextRequire: ctx
116 contextRequire: ctx
@@ -175,11 +175,13 define([],
175 }
175 }
176 }
176 }
177
177
178 try {
178 return function() {
179 return wrapresult(fn.apply(thisArg, arguments));
179 try {
180 } catch (e) {
180 return wrapresult(fn.apply(thisArg, arguments));
181 return wrapresult(null, e);
181 } catch (e) {
182 }
182 return wrapresult(null, e);
183 }
184 };
183 },
185 },
184
186
185 create: function () {
187 create: function () {
@@ -6,6 +6,8
6 // Copyright (c) 2010-2012 Robert Kieffer
6 // Copyright (c) 2010-2012 Robert Kieffer
7 // MIT License - http://opensource.org/licenses/mit-license.php
7 // MIT License - http://opensource.org/licenses/mit-license.php
8
8
9 declare var window: any;
10
9 let _window : any = 'undefined' !== typeof window ? window : null;
11 let _window : any = 'undefined' !== typeof window ? window : null;
10
12
11 // Unique ID creation requires a high quality random # generator. We
13 // Unique ID creation requires a high quality random # generator. We
@@ -1,1 +1,1
1 define(["./dummy", "./example"]); No newline at end of file
1 define(["./dummy", "./example", "./ActivatableTests"]); No newline at end of file
@@ -4,7 +4,10
4 "module": "amd",
4 "module": "amd",
5 "sourceMap": true,
5 "sourceMap": true,
6 "outDir" : "build/dist",
6 "outDir" : "build/dist",
7 "declaration": true
7 "declaration": true,
8 "lib": [
9 "ES2015"
10 ]
8 },
11 },
9 "include" : [
12 "include" : [
10 "src/ts/**/*.ts"
13 "src/ts/**/*.ts"
@@ -4,7 +4,10
4 "module": "amd",
4 "module": "amd",
5 "sourceMap": true,
5 "sourceMap": true,
6 "outDir" : "build/test",
6 "outDir" : "build/test",
7 "moduleResolution": "node"
7 "moduleResolution": "node",
8 "lib": [
9 "ES2015"
10 ]
8 },
11 },
9 "include" : [
12 "include" : [
10 "test/ts/**/*.ts"
13 "test/ts/**/*.ts"
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now