So... I have some methods. Each method returns a promise.
myAsyncMethods: {
myNavigate () {
// Imagine this is returning a webdriverio promise
return new Promise(function(resolve){
setTimeout(resolve, 1000);
})
},
myClick () {
// Imagine this is returning a webdriverio promise
return new Promise(function(resolve){
setTimeout(resolve, 2000);
})
}
}
I'm trying to make end to end tests, so the prom chain must be linear (first click, next navigate, etc)
For now, I can do this...
makeItFluent(myAsyncMethods)
.myNavigate()
.myClick()
.then(() => myAsyncMethods.otherMethod())
.then(() => /*do other stuff*/ )
...with ES6 proxy feature:
function makeItFluent (actions) {
let prom = Promise.resolve();
const builder = new Proxy(actions, {
get (target, propKey) {
const origMethod = target[propKey];
return function continueBuilding (...args) {
// keep chaining promises
prom = prom.then(() => (typeof origMethod === 'function') && origMethod(...args));
// return an augmented promise with proxied object
return Object.assign(prom, builder);
};
}
});
return builder;
};
But, the thing I cannot do is the following:
makeItFluent(myAsyncMethods)
.myNavigate()
.myClick()
.then(() => myAsyncMethods.otherMethod())
.then(() => /*do other stuff*/ )
.myNavigate()
Because then is not a proxied method, and thus it does not return myAsyncMethods. I tried to proxy then but with no results.
Any idea?
thanks devs ;)
I would return wrapped Promises from yourAsyncMethods which allows mixing of sync and async methods with Proxy and Reflect and executing them in the correct order :
/* WRAP PROMISE */
let handlers;
const wrap = function (target) {
if (typeof target === 'object' && target && typeof target.then === 'function') {
// The target needs to be stored internally as a function, so that it can use
// the `apply` and `construct` handlers.
var targetFunc = function () { return target; };
targetFunc._promise_chain_cache = Object.create(null);
return new Proxy(targetFunc, handlers);
}
return target;
};
// original was written in TS > 2.5, you might need a polyfill :
if (typeof Reflect === 'undefined') {
require('harmony-reflect');
}
handlers = {
get: function (target, property) {
if (property === 'inspect') {
return function () { return '[chainable Promise]'; };
}
if (property === '_raw') {
return target();
}
if (typeof property === 'symbol') {
return target()[property];
}
// If the Promise itself has the property ('then', 'catch', etc.), return the
// property itself, bound to the target.
// However, wrap the result of calling this function.
// This allows wrappedPromise.then(something) to also be wrapped.
if (property in target()) {
const isFn = typeof target()[property] === 'function';
if (property !== 'constructor' && !property.startsWith('_') && isFn) {
return function () {
return wrap(target()[property].apply(target(), arguments));
};
}
return target()[property];
}
// If the property has a value in the cache, use that value.
if (Object.prototype.hasOwnProperty.call(target._promise_chain_cache, property)) {
return target._promise_chain_cache[property];
}
// If the Promise library allows synchronous inspection (bluebird, etc.),
// ensure that properties of resolved
// Promises are also resolved immediately.
const isValueFn = typeof target().value === 'function';
if (target().isFulfilled && target().isFulfilled() && isValueFn) {
return wrap(target().constructor.resolve(target().value()[property]));
}
// Otherwise, return a promise for that property.
// Store it in the cache so that subsequent references to that property
// will return the same promise.
target._promise_chain_cache[property] = wrap(target().then(function (result) {
if (result && (typeof result === 'object' || typeof result === 'function')) {
return wrap(result[property]);
}
const _p = `"${property}" of "${result}".`;
throw new TypeError(`Promise chain rejection: Cannot read property ${_p}`);
}));
return target._promise_chain_cache[property];
},
apply: function (target, thisArg, args) {
// If the wrapped Promise is called, return a Promise that calls the result
return wrap(target().constructor.all([target(), thisArg]).then(function (results) {
if (typeof results[0] === 'function') {
return wrap(Reflect.apply(results[0], results[1], args));
}
throw new TypeError(`Promise chain rejection: Attempted to call ${results[0]}` +
' which is not a function.');
}));
},
construct: function (target, args) {
return wrap(target().then(function (result) {
return wrap(Reflect.construct(result, args));
}));
}
};
// Make sure all other references to the proxied object refer to the promise itself,
// not the function wrapping it
Object.getOwnPropertyNames(Reflect).forEach(function (handler) {
handlers[handler] = handlers[handler] || function (target, arg1, arg2, arg3) {
return Reflect[handler](target(), arg1, arg2, arg3);
};
});
You would use it with your methods like
myAsyncMethods: {
myNavigate () {
// Imagine this is returning a webdriverio promise
var myPromise = new Promise(function(resolve){
setTimeout(resolve, 1000);
});
return wrap(myPromise)
},
// ...
Please note two things :
You might need a polyfill for Reflect : https://www.npmjs.com/package/harmony-reflect
We need to check proxy get handlers for built-in Symbols, e.g. : https://github.com/nodejs/node/issues/10731 (but also some browsers)
You can now mix it like
FOO.myNavigate().mySyncPropertyOrGetter.myClick().mySyncMethod().myNavigate() ...
https://michaelzanggl.com/articles/end-of-chain/
A promise is nothing more than a "thenable" (an object with a then() method), which conforms to the specs. And await is simply a wrapper around promises to provide cleaner, concise syntax.
class NiceClass {
promises = [];
doOne = () => {
this.promises.push(new Promise((resolve, reject) => {
this.one = 1;
resolve();
}));
return this;
}
doTwo = () => {
this.promises.push(new Promise((resolve, reject) => {
this.two = 2;
resolve();
}));
return this;
}
async then(resolve, reject) {
let results = await Promise.all(this.promises);
resolve(results);
}
build = () => {
return Promise.all(this.promises)
}
}
Them you can call it in both ways.
(async () => {
try {
let nice = new NiceClass();
let result = await nice
.doOne()
.doTwo();
console.log(nice);
let nice2 = new NiceClass();
let result2 = await nice2
.doOne()
.doTwo()
.build();
console.log(nice2, result2);
} catch(error) {
console.log('Promise error', error);
}
})();
Related
I'm learning JavaScript, and I decided that an excelent chalenge would be to implement a custom Promise class in JavaScript. I managed to implement the method then, and it works just fine, but I'm having difficulties with the error handling and the method catch. Here is my code for the Promise class (in a module called Promise.mjs):
export default class _Promise {
constructor(executor) {
if (executor && executor instanceof Function) {
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch (error) {
this.reject(error);
}
}
}
resolve() {
if (this.callback && this.callback instanceof Function) {
return this.callback(...arguments);
}
}
reject(error) {
if (this.errorCallback && this.errorCallback instanceof Function) {
return this.errorCallback(error);
} else {
throw `Unhandled Promise Rejection\n\tError: ${error}`;
}
}
then(callback) {
this.callback = callback;
return this;
}
catch(errorCallback) {
this.errorCallback = errorCallback;
return this;
}
}
When I import and use this class in the following code, all the then() clauses run as according, and I get the desired result in the console:
import _Promise from "./Promise.mjs";
function sum(...args) {
let total = 0;
return new _Promise(function (resolve, reject) {
setTimeout(function () {
for (const arg of args) {
if (typeof arg !== 'number') {
reject(`Invalid argument: ${arg}`);
}
total += arg;
}
resolve(total);
}, 500);
});
}
console.time('codeExecution');
sum(1, 3, 5).then(function (a) {
console.log(a);
return sum(2, 4).then(function (b) {
console.log(b);
return sum(a, b).then(function (result) {
console.log(result);
console.timeEnd('codeExecution');
});
});
}).catch(function (error) {
console.log(error);
});
But, when I add an invalid argument to the sum() function, i.e. not a number, the reject() method runs, but it don't stop the then() chain, as should be, and we also get an exception. This can be seen from the following code:
import _Promise from "./Promise.mjs";
function sum(...args) {
let total = 0;
return new _Promise(function (resolve, reject) {
setTimeout(function () {
for (const arg of args) {
if (typeof arg !== 'number') {
reject(`Invalid argument: ${arg}`);
}
total += arg;
}
resolve(total);
}, 500);
});
}
console.time('codeExecution');
sum(1, 3, '5').then(function (a) {
console.log(a);
return sum(2, 4).then(function (b) {
console.log(b);
return sum(a, b).then(function (result) {
console.log(result);
console.timeEnd('codeExecution');
});
});
}).catch(function (error) {
console.log(error);
});
Also, if I catch an error in nested then() methods, the outer catch() doesn't notice this and I get an exception again. The goal is to implement a lightweight functional version of Promises, but not necessarily with all its functionality. Could you help me?
The problem in your code is that your sum function calls both the reject and the resolve functions. There's no handling in the sum function that will cause it not to call the resolve function at the end, and there is nothing in your _Promise that blocks this behavior.
You have 2 options to fix this.
Option 1 would be if you want your _Promise to act like a real Promise you will need to manage a state and once a promise got to a final state stop calling the callback or errorCallback.
Option 2 would be to prevent from calling both reject and resolve in the function calling the _Promise, in this case, the sum function.
With the comments that you guys provide me, I was able to improve the code and correct the errors mentioned, as shown below. Now, I would like you to give me suggestions on how to proceed and improve the code. Thanks. (The code can also be found on github).
const PENDING = 0;
const FULFILLED = 1;
const REJECTED = 2;
function _Promise(executor) {
let state = PENDING;
let callOnFulfilled = [];
let callOnRejected = undefined;;
function resolve(...args) {
if (!state) {
state = FULFILLED;
}
resolveCallbacks(...args);
};
function reject(error) {
state = REJECTED;
if (callOnRejected && (callOnRejected instanceof Function)) {
callOnRejected(error);
callOnRejected = undefined;
callOnFulfilled = [];
} else {
throw `Unhandled Promise Rejection\n\tError: ${error}`;
}
};
function resolveCallbacks(...value) {
if (state !== REJECTED) {
let callback = undefined;
do {
callback = callOnFulfilled.shift();
if (callback && (callback instanceof Function)) {
const result = callback(...value);
if (result instanceof _Promise) {
result.then(resolveCallbacks, reject);
return;
} else {
value = [result];
}
}
} while (callback);
}
};
if (executor && (executor instanceof Function)) {
executor(resolve, reject);
}
this.then = function (onFulfilled, onRejected) {
if (onFulfilled) {
callOnFulfilled.push(onFulfilled);
if (state === FULFILLED) {
resolveCallbacks();
}
}
if (onRejected && !callOnRejected) {
callOnRejected = onRejected;
}
return this;
};
this.catch = function (onRejected) {
return this.then(undefined, onRejected);
};
}
function sum(...args) {
let total = 0;
return new _Promise(function (resolve, reject) {
setTimeout(function () {
for (const arg of args) {
if (typeof arg !== 'number') {
reject(`Invalid argument: ${arg}`);
}
total += arg;
}
resolve(total);
}, 500);
});
}
console.time('codeExecution');
sum(1, 3, 5).then(function (a) {
console.log(a);
return sum(2, 4).then(function (b) {
console.log(b);
return sum(a, b).then(function (result) {
console.log(result);
return 25;
});
}).then(function (value) {
console.log(value);
console.timeEnd('codeExecution');
});
}).catch(function (error) {
console.log(error);
});
Sometimes I'd like to use the RxJS operators to manipulate an endless asynchronous iterable without buffering the values. It is easy to turn an iterable into an Observable. Are there downsides in the following approach to turn an Observable into an asynchronous iterable?
const iterable = async function* (observable) {
let buffer = [],
resolve,
reject;
const subscription = observable.subscribe({
next: value => {
if (resolve) {
resolve(value);
resolve = reject = undefined;
} else {
buffer.push(Promise.resolve(value));
}
},
error: e => {
if (reject) {
reject(e);
resolve = reject = undefined;
}
},
complete: () => {},
});
while (!subscription.isStopped || buffer.length) {
yield buffer.shift() ||
new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
}
subscription.unsubscribe();
};
Codepen demo
Here is an alternative implementation of the iterable as an Observer (without using a generator).
class IterableObserver {
constructor(observable) {
this.buffer = [];
this.resolve = undefined;
this.reject = undefined;
this.isStopped = false;
observable && observable.subscribe(this);
}
[Symbol.asyncIterator]() {
const t = this;
return {
next: async () => {
if (!t.isStopped || t.buffer.length) {
if (t.buffer.length) {
return {
value: t.buffer.shift(),
};
} else {
return new Promise((_resolve, _reject) => {
t.resolve = _resolve;
t.reject = _reject;
});
}
} else {
return { done: true };
}
},
};
}
next(value) {
if (this.resolve) {
this.resolve({ value });
this.resolve = this.reject = undefined;
} else {
this.buffer.push(value);
}
}
error(e) {
this.isStopped = true;
if (this.reject) {
this.reject(e);
this.resolve = this.reject = undefined;
}
}
complete() {
this.isStopped = true;
}
}
The benefit from this was questioned. Let's say you have an API which provides you with an asynchronous iterable of text file lines through the function makeTextFileLineIterator and you would need to provide an asynchronous iterable of the first N lines in uppercase for your client. How would you do that? With the RxJS operators and the iterable conversion it would be easy:
const lineIterator = makeTextFileLineIterator('https://cdn.skypack.dev/rxjs#^7.1.0/operators?min');
const upperCaseLines = new IterableObserver(from(lineIterator).pipe(
take(6),
map(line=>line.toLocaleUpperCase())),
);
Codepen demo
I have a function that can return result in both callback and promise:
function foo(args, cb) {
// do stuff
const promise = getSomePromise();
if (cb) {
promise.then((result) => {
cb(null, result);
}, (err) => {
cb(err);
});
} else {
return promise;
}
}
I want to alter the result of promise before returning it. How to do this in a way that introduces the least amount of spaghetti code?
Add a function to alter and return modified result
function alterResult(result){
const alteredResult = /// do something to result
return alteredResult;
}
Then add it in a then()to:
const promise = getSomePromise().then(alterResult);
You can write a function which returns something and pass it to the callback when initiating it like this
function foo(cb) {
const promise = getSomePromise(); // return some promise
promise.then((result) => { // result is here just "Hello"
cb(doSomething(result)); // return the altered value here to the callback
})
}
function getSomePromise() {
return new Promise((resolve, _) => resolve("Hello")) // make a new promise
}
function doSomething(res) { // define a function that alters your result
return res + ", World"
}
foo((val) => console.log(val)) // log it
How would I write the following without jQuery?
var dfd = $.Deferred()
dfd.done(done)
dfd.resolve()
function done() {
console.log('done')
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Use native promises:
Promise
.resolve()
.then(done);
function done() {
console.log('done')
}
Update
Without the chain:
let op = Promise.resolve();
op.then(done);
function done() {
console.log('done')
}
function Deferred (){
let res,rej,p = new Promise((a,b)=>(res = a, rej = b));
p.resolve = res;
p.reject = rej;
return p;
}
You just need to expose resolve and reject to make it work.
The problem with using native promises is that the resolve and reject handler is provided in the callback, so if you try and call them before they are actually assigned.
In my opinion it's more robust to just implement a deferred yourself for example:
function deferred() {
let thens = []
let catches = []
let status
let resolvedValue
let rejectedError
return {
resolve: value => {
status = 'resolved'
resolvedValue = value
thens.forEach(t => t(value))
thens = [] // Avoid memleaks.
},
reject: error => {
status = 'rejected'
rejectedError = error
catches.forEach(c => c(error))
catches = [] // Avoid memleaks.
},
then: cb => {
if (status === 'resolved') {
cb(resolvedValue)
} else {
thens.unshift(cb)
}
},
catch: cb => {
if (status === 'rejected') {
cb(rejectedError)
} else {
catches.unshift(cb)
}
},
}
}
const d = deferred()
setTimeout(() => {
d.resolve('good')
}, 1000)
// Will be called after 1s
d.then(value => console.log('#1 resolved!', value))
setTimeout(() => {
// Will be called after 3s and executed right away as it's already resolved
d.then(value => console.log('#2 resolved!', value))
}, 3000)
I'm trying to create chaining with the javascript methods similar to what we have with jquery. Please let me know how to implement chaining with javascript.
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
this.currentUser = user;
return this.currentUser.toUpperCase();
},
fnCreateUserId: function() {
return this.currentUser + Math.random();
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId();
As I already explained, since you are returning a string from fnFormatUserName you cannot use it for chaining.
To enable chaining, you need to return the object which invoked method. So, you cannot use getter methods for chaining.
In your example, the way to handle it is to have getter methods and methods with updates the object which can be used for chaining like
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
this.currentUser = user.toUpperCase();
return this;
},
fnCreateUserId: function() {
this.userId = this.currentUser + Math.random();
return this;
},
getUserId: function() {
return this.userId;
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId().getUserId();
document.body.innerHTML = output;
Another version could be
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
if (arguments.length == 0) {
return this.currentUser;
} else {
this.currentUser = user.toUpperCase();
return this;
}
},
fnCreateUserId: function() {
this.userId = this.currentUser + Math.random();
return this;
},
getUserId: function() {
return this.userId;
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId().getUserId();
r1.innerHTML = output;
r2.innerHTML = controller.fnFormatUserName();
<div id="r1"></div>
<div id="r2"></div>
You can use proxies to decorate methods so that they will return the object itself ("this") instead of the actual method return value. Below is an implementation of a chainer function that will do just that for any object. The code also declares a special symbol "target" which can be used to access the original object (and unaltered method return values), discarding the chaining proxy.
const target = Symbol('Symbol for the target of the chainer proxy');
const targetSymbol = target;
const chainer = (target) =>
new Proxy(target, {
get: (_, prop, receiver) =>
prop === targetSymbol
? target
: typeof target[prop] === 'function'
? new Proxy(target[prop], {
apply: (f, _, args) => {
f.apply(target, args);
return receiver;
},
})
: target[prop],
});
const controller = {
currentUser: '',
fnFormatUserName: function(user) {
return this.currentUser = user.toUpperCase();
},
fnCreateUserId: function() {
return this.currentUser + Math.random();
}
}
const output = chainer(controller).fnFormatUserName('Manju')[target].fnCreateUserId();
console.log(output);
Another option would be that the decorated methods would always return an intermediate object with two properties: the this context ("this") and a reference to the original object with undecorated methods ("target"). See below.
const chainer = (target) =>
new Proxy(target, {
get: (_, prop, receiver) =>
typeof target[prop] === 'function'
? new Proxy(target[prop], {
apply: (f, _, args) => {
f.apply(target, args);
return {
this: receiver,
target,
}
},
})
: target[prop],
});
const counter = {
value: 0,
increment: function() {
return ++this.value;
}
}
const value = chainer(counter)
.increment().this
.increment().target
.increment();
console.log(value);
I suppose this might be seen as "cheating", but you could very easily achieve a similar result by extending the String.prototype like:
String.prototype.upper=function(){return this.toUpperCase()};
String.prototype.makeId=function(){return this+Math.random()};
// test
const str="abc";
console.log(str.upper().makeId(), str);
It will, of course, change the behaviour of all strings in the current session as they will now have the additional methods .upper() and .makeId() associated with them.