如何将现有的回调API转换为Promise?


721

我想使用Promise,但我有以下格式的回调API:

1. DOM加载或其他一次事件:

window.onload; // set to callback
...
window.onload = function() {

};

2.普通回调:

function request(onChangeHandler) {
    ...
}
request(function() {
    // change happened
    ...
});

3.节点样式回调(“ nodeback”):

function getStuff(dat, callback) {
    ...
}
getStuff("dataParam", function(err, data) {
    ...
})

4.带有节点样式回调的整个库:

API;
API.one(function(err, data) {
    API.two(function(err, data2) {
        API.three(function(err, data3) {
            ...
        });
    });
});

如何在promise中使用API​​,如何“承诺”它?


我发布了我自己的答案,但是对于在特定库中或在更多情况下如何执行此操作的更多答案,也非常欢迎进行编辑。
本杰明·格林鲍姆2014年

@Bergi这是一个有趣的想法,我尝试使用两种常见方法(Promise构造函数和deferred对象)做出一个通用答案。我试图给出两种选择。我同意RTFMing解决了这个问题,但是我们经常在这里和在bug跟踪器中都遇到这个问题,所以我认为存在一个“规范问题”-我认为RTFMing解决了JS标签中大约50%的问题:D您有一个有趣的见解可以为答案做出贡献或进行编辑,我们将不胜感激。
本杰明·格林巴姆2014年

创建一个new Promise额外的开销吗?我想将所有同步Noje.js函数包装在Promise中,以便从Node应用程序中删除所有同步代码,但这是最佳实践吗?换句话说,一个接受静态参数(例如字符串)并返回计算结果的函数,我应该将其包装在promise中吗?...我读到某个地方,您在Nodejs中不应有任何同步代码。
罗尼·罗斯顿

1
@RonRoyston不,用promise包装同步调用不是一个好主意-只执行可能执行I / O的异步调用
Benjamin Gruenbaum

Answers:


743

承诺有状态,它们从待定状态开始,可以解决:

  • 完成意味着计算成功完成。
  • 拒绝表示计算失败。

承诺返回函数绝不应该抛出,而应该返回拒绝。从promise返回函数抛出将迫使您同时使用a } catch { a .catch。使用承诺的API的人们不会期望诺言。如果您不确定JS中异步API的工作方式-请首先查看此答案

1. DOM加载或其他一次事件:

因此,创建承诺通常意味着指定何时结算-意味着何时进入承诺或拒绝阶段以指示数据可用(并且可以通过访问.then)。

使用支持Promise本地构造函数(如本机ES6 Promise)的现代Promise实现:

function load() {
    return new Promise(function(resolve, reject) {
        window.onload = resolve;
    });
}

然后,您将使用产生的promise,如下所示:

load().then(function() {
    // Do things after onload
});

使用支持延迟的库(让我们在此示例中使用$ q,但稍后我们还将使用jQuery):

function load() {
    var d = $q.defer();
    window.onload = function() { d.resolve(); };
    return d.promise;
}

或者使用jQuery之类的jQuery,将一次事件挂起:

function done() {
    var d = $.Deferred();
    $("#myObject").once("click",function() {
        d.resolve();
    });
    return d.promise();
}

2.普通回调:

这些API相当常见,因为……在JS中回调很常见。让我们看看具有onSuccessand 的常见情况onFail

function getUserData(userId, onLoad, onFail) { 

使用支持Promise本地构造函数(如本机ES6 Promise)的现代Promise实现:

function getUserDataAsync(userId) {
    return new Promise(function(resolve, reject) {
        getUserData(userId, resolve, reject);
    });
}

使用支持延迟的库(在此示例中,我们使用jQuery,但上面我们也使用$ q):

function getUserDataAsync(userId) {
    var d = $.Deferred();
    getUserData(userId, function(res){ d.resolve(res); }, function(err){ d.reject(err); });
    return d.promise();
}

jQuery还提供了一种$.Deferred(fn)表单,其优点是允许我们编写一个非常new Promise(fn)类似于该表单的表达式,如下所示:

function getUserDataAsync(userId) {
    return $.Deferred(function(dfrd) {
        getUserData(userId, dfrd.resolve, dfrd.reject);
    }).promise();
}

注意:这里我们利用了jQuery的deferred resolvereject方法是“可分离的” 的事实。即。它们绑定到jQuery.Deferred()的实例。并非所有库都提供此功能。

3.节点样式回调(“ nodeback”):

节点样式回调(nodebacks)具有特定的格式,其中回调始终是最后一个参数,而其第一个参数是错误。让我们首先手动分配一个:

getStuff("dataParam", function(err, data) { 

至:

function getStuffAsync(param) {
    return new Promise(function(resolve, reject) {
        getStuff(param, function(err, data) {
            if (err !== null) reject(err);
            else resolve(data);
        });
    });
}

使用deferred,您可以执行以下操作(在本示例中,请使用Q,尽管Q现在支持您应该使用的新语法):

function getStuffAsync(param) {
    var d = Q.defer();
    getStuff(param, function(err, data) {
        if (err !== null) d.reject(err);
        else d.resolve(data);
    });
    return d.promise;   
}

通常,您不应该过多地手动分配内容,大多数基于Node设计的Promise库以及Node 8+中的本机Promise具有内置的用于使NodeBbacks富集的方法。例如

var getStuffAsync = Promise.promisify(getStuff); // Bluebird
var getStuffAsync = Q.denodeify(getStuff); // Q
var getStuffAsync = util.promisify(getStuff); // Native promises, node only

4.带有节点样式回调的整个库:

这里没有黄金法则,您一一承诺。但是,某些promise实现允许您批量执行此操作,例如在Bluebird中,将nodeback API转换为promise API很简单:

Promise.promisifyAll(API);

或在Node中具有本机承诺

const { promisify } = require('util');
const promiseAPI = Object.entries(API).map(([key, v]) => ({key, fn: promisify(v)}))
                         .reduce((o, p) => Object.assign(o, {[p.key]: p.fn}), {});

笔记:

  • 当然,当您在.then处理程序中时,您不需要散布事物。从.then处理程序返回一个承诺将解决或拒绝该承诺的值。从.then处理程序中扔出也是一种好习惯,并且会拒绝诺言-这就是著名的诺言抛出安全性。
  • 在实际onload情况下,您应该使用addEventListener而不是onX

本杰明,我接受了您的编辑邀请,并向案例2添加了另一个jQuery示例。在它出现之前,需要进行同行评审。希望你喜欢。
Roamer-1888

@ Roamer-1888,它被拒绝了,因为我没有及时看到并接受它。值得的是,尽管有用,但我认为添加的内容不太相关。
Benjamin Gruenbaum 2014年

2
本杰明,无论是否resolve()reject()被写入被重用,恕我冒昧,我的建议编辑是相关的,因为它提供了形式的jQuery的例子$.Deferred(fn),这是中缺乏。如果仅包括一个jQuery示例,那么我建议它应采用这种形式而不是其他形式var d = $.Deferred();。应鼓励人们使用经常被忽略的$.Deferred(fn)形式,此外,在这样的答案中,它使jQuery与使用揭示构造器模式的库
Roamer-1888

呵呵,公平地说,我不知道jQuery允许您这样做$.Deferred(fn),如果您在接下来的15分钟之内(而不是在现有示例中)对其进行编辑,我相信我可以尝试按时批准它:)
Benjamin Gruenbaum

7
这是一个很好的答案。您可能还想提及util.promisifyNode.js,它将从RC 8.0.0开始添加到其核心中以对其进行更新。它的工作方式与Bluebird并没有太大区别Promise.promisify,但优点是不需要额外的依赖关系,以防您只需要原生Promise。我为想要阅读有关该主题的任何人写了一篇关于util.promisify的博客文章。
布鲁诺

55

今天,我可以用PromiseNode.js作为一个普通的JavaScript方法。

一个简单的基本示例Promise(使用KISS方式):

普通 Javascript异步API代码:

function divisionAPI (number, divider, successCallback, errorCallback) {

    if (divider == 0) {
        return errorCallback( new Error("Division by zero") )
    }

    successCallback( number / divider )

}

Promise Javascript异步API代码:

function divisionAPI (number, divider) {

    return new Promise(function (fulfilled, rejected) {

        if (divider == 0) {
            return rejected( new Error("Division by zero") )
        }

        fulfilled( number / divider )

     })

}

(我建议访问这个美丽的资源

Promise可以一起使用async\awaitES7,以使程序流程等待一个fullfiled像下面这样的结果:

function getName () {

    return new Promise(function (fulfilled, rejected) {

        var name = "John Doe";

        // wait 3000 milliseconds before calling fulfilled() method
        setTimeout ( 
            function() {
                fulfilled( name )
            }, 
            3000
        )

    })

}


async function foo () {

    var name = await getName(); // awaits for a fulfilled result!

    console.log(name); // the console writes "John Doe" after 3000 milliseconds

}


foo() // calling the foo() method to run the code

使用.then()方法使用相同代码的另一种用法

function getName () {

    return new Promise(function (fulfilled, rejected) {

        var name = "John Doe";

        // wait 3000 milliseconds before calling fulfilled() method
        setTimeout ( 
            function() {
                fulfilled( name )
            }, 
            3000
        )

    })

}


// the console writes "John Doe" after 3000 milliseconds
getName().then(function(name){ console.log(name) })

Promise也可以在基于Node.js的任何平台上使用,例如react-native

奖励混合方法
(假设回调方法具有两个参数,分别是错误和结果)

function divisionAPI (number, divider, callback) {

    return new Promise(function (fulfilled, rejected) {

        if (divider == 0) {
            let error = new Error("Division by zero")
            callback && callback( error )
            return rejected( error )
        }

        let result = number / divider
        callback && callback( null, result )
        fulfilled( result )

     })

}

上面的方法可以响应老式回调和Promise用法的结果。

希望这可以帮助。


3
这些似乎没有显示如何转换为诺言。
Dmitri Zaitsev

33

在Node.JS中将函数转换为Promise之前

var request = require('request'); //http wrapped module

function requestWrapper(url, callback) {
    request.get(url, function (err, response) {
      if (err) {
        callback(err);
      }else{
        callback(null, response);             
      }      
    })
}


requestWrapper(url, function (err, response) {
    console.log(err, response)
})

转换后

var request = require('request');

function requestWrapper(url) {
  return new Promise(function (resolve, reject) { //returning promise
    request.get(url, function (err, response) {
      if (err) {
        reject(err); //promise reject
      }else{
        resolve(response); //promise resolve
      }
    })
  })
}


requestWrapper('http://localhost:8080/promise_request/1').then(function(response){
    console.log(response) //resolve callback(success)
}).catch(function(error){
    console.log(error) //reject callback(failure)
})

如果您需要处理多个请求

var allRequests = [];
allRequests.push(requestWrapper('http://localhost:8080/promise_request/1')) 
allRequests.push(requestWrapper('http://localhost:8080/promise_request/2'))
allRequests.push(requestWrapper('http://localhost:8080/promise_request/5'))    

Promise.all(allRequests).then(function (results) {
  console.log(results);//result will be array which contains each promise response
}).catch(function (err) {
  console.log(err)
});

23

我认为window.onload@Benjamin 的建议不会一直有效,因为它无法检测到加载后是否被调用。我被很多次咬伤了。这是应该始终有效的版本:

function promiseDOMready() {
    return new Promise(function(resolve) {
        if (document.readyState === "complete") return resolve();
        document.addEventListener("DOMContentLoaded", resolve);
    });
}
promiseDOMready().then(initOnLoad);

1
“已经完成”的分支不应该使用setTimeout(resolve, 0)(或setImmediate,如果可用)以确保异步调用它?
Alnitak

5
@Alnitak resolve同步调用是可以的。框架保证 Promise的then处理程序被异步调用,无论是否resolve被同步调用。
杰夫·鲍曼

15

Node.js 8.0.0包含一个新util.promisify()API,该API允许将标准Node.js回调样式API封装在返回Promise的函数中。的使用示例util.promisify()如下所示。

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

readFile('/some/file')
  .then((data) => { /** ... **/ })
  .catch((err) => { /** ... **/ });

请参阅改进的对承诺的支持


2
已经有两个答案对此进行了描述,为什么还要发布第三个答案呢?
本杰明·格林鲍姆

1
仅仅因为该版本的节点现在已经发布,并且我已经报告了“官方”功能描述和链接。
Gian Marco Gherardi

14

在Node.js 8.0.0的候选版本中,有一个新的实用程序util.promisify(我已经写过util.promisify),该实用程序封装了使任何函数散布的能力。

它与其他答案中建议的方法没有太大不同,但是具有作为核心方法的优点,并且不需要其他依赖项。

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

然后,您有一个readFile返回native 的方法Promise

readFile('./notes.txt')
  .then(txt => console.log(txt))
  .catch(...);

1
嘿,我(OP)实际上提出了util.promisify两次建议(早在2014年,当撰写此问题时,就在几个月前-我将其作为Node的核心成员予以推动,并且是Node中的当前版本)。由于尚未公开提供-我尚未将其添加到此答案中。我们将非常感谢使用反馈,并希望了解一些陷阱,以便为发行版提供更好的文档:)
Benjamin Gruenbaum

1
此外,您可能想util.promisify在博客文章中讨论用于混杂的自定义标志:)
Benjamin Gruenbaum

@BenjaminGruenbaum您的意思是使用util.promisify.custom符号可以覆盖util.promisify的结果吗?老实说,这是故意遗漏的原因,因为我尚无法找到有用的用例。也许您可以给我一些建议?
布鲁诺

1
当然,请考虑类似API fs.exists或不遵循Node约定的API-蓝鸟Promise.promisify 会把它们弄错,但会把它们弄util.promisify对。
本杰明·格伦鲍姆

7

您可以将JavaScript本机Promise与Node JS一起使用。

我的Cloud 9代码链接:https : //ide.c9.io/adx2803/native-promises-in-node

/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
        request.get(url, function (error, response, body) {
            if (!error && response.statusCode == 200) {
                resolve(body);
            }
            else {
                reject(error);
            }
        })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
    //get the post with post id 100
    promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
        var obj = JSON.parse(result);
        return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
    })
    .catch(function (e) {
        console.log(e);
    })
    .then(function (result) {
        res.end(result);
    })
})

var server = app.listen(8081, function () {
    var host = server.address().address
    var port = server.address().port

    console.log("Example app listening at http://%s:%s", host, port)
})

//run webservice on browser : http://localhost:8081/listAlbums

7

使用普通的老式JavaScript,这是实现api回调的一种解决方案。

function get(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('get', url);
        xhr.addEventListener('readystatechange', function () {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    console.log('successful ... should call callback ... ');
                    callback(null, JSON.parse(xhr.responseText));
                } else {
                    console.log('error ... callback with error data ... ');
                    callback(xhr, null);
                }
            }
        });
        xhr.send();
    }

/**
     * @function promisify: convert api based callbacks to promises
     * @description takes in a factory function and promisifies it
     * @params {function} input function to promisify
     * @params {array} an array of inputs to the function to be promisified
     * @return {function} promisified function
     * */
    function promisify(fn) {
        return function () {
            var args = Array.prototype.slice.call(arguments);
            return new Promise(function(resolve, reject) {
                fn.apply(null, args.concat(function (err, result) {
                    if (err) reject(err);
                    else resolve(result);
                }));
            });
        }
    }

var get_promisified = promisify(get);
var promise = get_promisified('some_url');
promise.then(function (data) {
        // corresponds to the resolve function
        console.log('successful operation: ', data);
}, function (error) {
        console.log(error);
});

6

kriskowal的Q库包含应答回调函数。这样的方法:

obj.prototype.dosomething(params, cb) {
  ...blah blah...
  cb(error, results);
}

可以用Q.ninvoke转换

Q.ninvoke(obj,"dosomething",params).
then(function(results) {
});

1
规范答案已经提到Q.denodeify。我们需要强调图书馆帮手吗?
Bergi 2015年

3
我发现这个有用的关于Q中promisifying导致谷歌在这里
埃德赛克斯

4

当您有一些需要回调的函数并且希望它们返回一个Promise时,您可以使用此函数进行转换。

function callbackToPromise(func){

    return function(){

        // change this to use what ever promise lib you are using
        // In this case i'm using angular $q that I exposed on a util module

        var defered = util.$q.defer();

        var cb = (val) => {
            defered.resolve(val);
        }

        var args = Array.prototype.slice.call(arguments);
        args.push(cb);    
        func.apply(this, args);

        return defered.promise;
    }
}

4

在内置了promise和async的节点v7.6 +下:

// promisify.js
let promisify = fn => (...args) =>
    new Promise((resolve, reject) =>
        fn(...args, (err, result) => {
            if (err) return reject(err);
            return resolve(result);
        })
    );

module.exports = promisify;

如何使用:

let readdir = require('fs').readdir;
let promisify = require('./promisify');
let readdirP = promisify(readdir);

async function myAsyncFn(path) {
    let entries = await readdirP(path);
    return entries;
}

3

在Node.js 8中,您可以使用以下npm模块即时实现对象方法:

https://www.npmjs.com/package/doasync

它使用util.promisifyProxies,以便您的对象保持不变。记忆也可以通过使用WeakMaps完成。这里有些例子:

与对象:

const fs = require('fs');
const doAsync = require('doasync');

doAsync(fs).readFile('package.json', 'utf8')
  .then(result => {
    console.dir(JSON.parse(result), {colors: true});
  });

具有功能:

doAsync(request)('http://www.google.com')
  .then(({body}) => {
    console.log(body);
    // ...
  });

您甚至可以使用native callapply绑定一些上下文:

doAsync(myFunc).apply(context, params)
  .then(result => { /*...*/ });

2

您可以在ES6中使用本地Promise,例如处理setTimeout:

enqueue(data) {

    const queue = this;
    // returns the Promise
    return new Promise(function (resolve, reject) {
        setTimeout(()=> {
                queue.source.push(data);
                resolve(queue); //call native resolve when finish
            }
            , 10); // resolve() will be called in 10 ms
    });

}

在这个例子中,诺言没有理由失败,所以reject()永远不会被称为。


2

回调风格函数总是这样(在node.js中几乎所有的功能是这种风格):

//fs.readdir(path[, options], callback)
fs.readdir('mypath',(err,files)=>console.log(files))

此样式具有相同的功能:

  1. 回调函数由最后一个参数传递。

  2. 回调函数始终将错误对象作为其第一个参数。

因此,您可以编写一个函数来转换具有以下样式的函数:

const R =require('ramda')

/**
 * A convenient function for handle error in callback function.
 * Accept two function res(resolve) and rej(reject) ,
 * return a wrap function that accept a list arguments,
 * the first argument as error, if error is null,
 * the res function will call,else the rej function.
 * @param {function} res the function which will call when no error throw
 * @param {function} rej the function which will call when  error occur
 * @return {function} return a function that accept a list arguments,
 * the first argument as error, if error is null, the res function
 * will call,else the rej function
 **/
const checkErr = (res, rej) => (err, ...data) => R.ifElse(
    R.propEq('err', null),
    R.compose(
        res,
        R.prop('data')
    ),
    R.compose(
        rej,
        R.prop('err')
    )
)({err, data})

/**
 * wrap the callback style function to Promise style function,
 * the callback style function must restrict by convention:
 * 1. the function must put the callback function where the last of arguments,
 * such as (arg1,arg2,arg3,arg...,callback)
 * 2. the callback function must call as callback(err,arg1,arg2,arg...)
 * @param {function} fun the callback style function to transform
 * @return {function} return the new function that will return a Promise,
 * while the origin function throw a error, the Promise will be Promise.reject(error),
 * while the origin function work fine, the Promise will be Promise.resolve(args: array),
 * the args is which callback function accept
 * */
 const toPromise = (fun) => (...args) => new Promise(
    (res, rej) => R.apply(
        fun,
        R.append(
            checkErr(res, rej),
            args
        )
    )
)

为了更加简洁,上面的示例使用了ramda.js。Ramda.js是用于函数式编程的出色库。在上面的代码中,我们使用了apply(例如javascript function.prototype.apply)和append(例如javascript function.prototype.push)。因此,我们现在可以将回调样式函数转换为Promise样式函数:

const {readdir} = require('fs')
const readdirP = toPromise(readdir)
readdir(Path)
    .then(
        (files) => console.log(files),
        (err) => console.log(err)
    )

toPromisecheckErr功能是通过自己的疯抢库,它是一个功能的编程库叉通过ramda.js(由我创建)。

希望这个答案对您有用。


2

你可以做这样的事情

// @flow

const toPromise = (f: (any) => void) => {
  return new Promise<any>((resolve, reject) => {
    try {
      f((result) => {
        resolve(result)
      })
    } catch (e) {
      reject(e)
    }
  })
}

export default toPromise

然后用

async loadData() {
  const friends = await toPromise(FriendsManager.loadFriends)

  console.log(friends)
}

2
嘿,我不确定这会增加现有答案(也许弄清楚吗?)。另外,无需在promise构造函数内进行try / catch(它会自动为您完成此操作)。还不清楚它的功能是什么(成功时
仅用


1

我的callback函数的承诺版本是P函数:

var P = function() {
  var self = this;
  var method = arguments[0];
  var params = Array.prototype.slice.call(arguments, 1);
  return new Promise((resolve, reject) => {
    if (method && typeof(method) == 'function') {
      params.push(function(err, state) {
        if (!err) return resolve(state)
        else return reject(err);
      });
      method.apply(self, params);
    } else return reject(new Error('not a function'));
  });
}
var callback = function(par, callback) {
  var rnd = Math.floor(Math.random() * 2) + 1;
  return rnd > 1 ? callback(null, par) : callback(new Error("trap"));
}

callback("callback", (err, state) => err ? console.error(err) : console.log(state))
callback("callback", (err, state) => err ? console.error(err) : console.log(state))
callback("callback", (err, state) => err ? console.error(err) : console.log(state))
callback("callback", (err, state) => err ? console.error(err) : console.log(state))

P(callback, "promise").then(v => console.log(v)).catch(e => console.error(e))
P(callback, "promise").then(v => console.log(v)).catch(e => console.error(e))
P(callback, "promise").then(v => console.log(v)).catch(e => console.error(e))
P(callback, "promise").then(v => console.log(v)).catch(e => console.error(e))

P函数要求回调签名必须为callback(error,result)


1
与本地承诺或以上答案相比,这有什么优势?
本杰明·格伦鲍姆

您对原生承诺的意思是什么?
loretoparisi


啊,当然是:)。公正和举例说明基本思想。实际上,您可以看到甚至本机代码也要求功能签名必须像某些定义那样,(err, value) => ...或者您必须定义自定义代码(请参阅自定义功能)。谢谢你,好赶上。
loretoparisi

1
@loretoparisi仅供参考,var P = function (fn, ...args) { return new Promise((resolve, reject) => fn.call(this, ...args, (error, result) => error ? reject(error) : resolve(result))); };可以做与您相同的事情,它要简单得多。
Patrick Roberts

1

以下是如何将函数(回调API)转换为Promise的实现。

function promisify(functionToExec) {
  return function() {
    var array = Object.values(arguments);
    return new Promise((resolve, reject) => {
      array.push(resolve)
      try {
         functionToExec.apply(null, array);
      } catch (error) {
         reject(error)
      }
    })
  }
}

// USE SCENARIO

function apiFunction (path, callback) { // Not a promise
  // Logic
}

var promisedFunction = promisify(apiFunction);

promisedFunction('path').then(()=>{
  // Receive the result here (callback)
})

// Or use it with await like this
let result = await promisedFunction('path');

-2

大概晚了5年,但我想在此发布我的promesify版本,该版本采用回调API的功能并将其转化为承诺

const promesify = fn => {
  return (...params) => ({
    then: cbThen => ({
      catch: cbCatch => {
        fn(...params, cbThen, cbCatch);
      }
    })
  });
};

在这里看看这个非常简单的版本:https : //gist.github.com/jdtorregrosas/aeee96dd07558a5d18db1ff02f31e21a


1
这不是一个承诺,它不会链接,处理在回调中引发的错误或在此之后接受第二个参数……
Benjamin Gruenbaum
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.