Turning RxJS Observable into an asynchronous iterable - javascript

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

Related

How to implement a simpler Promise in JavaScript?

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);
});

How to create a race in setters?

I have setters:
addSettersAndGetters(i) {
// Vue component
const self = this;
let promise_h, promise_w;
Object.defineProperties(this.preorder_positions[i].shapes[0].shape, {
'height_out': {
set: function (val) {
this._height_out = +val;
self.preorder_positions[i].height = +val;
promise_h = new Promise (() => self.validateShapes(i));
},
get: function () {
return this._height_out;
}
},
'width_out': {
set: function (val) {
this._width_out = +val;
self.preorder_positions[i].width = +val;
promise_w = new Promise (() => self.validateShapes(i));
},
get: function () {
return this._width_out;
}
},
});
return Promise.race([promise_h, promise_w]);
},
But when I call the method, the error is:
Uncaught (in promise) TypeError: Promise resolver undefined is not a function
How can I implement a race, as this will reduce the number of database queries?
UPD: Function call - this.addSettersAndGetters(i)
validateShapes(pi) {
// validateShapes запускается в updated без pi
if (pi !== undefined ) {
const self = this;
let promise = new Promise(function () {
self.$refs.form.validate();
});
promise.then(self.getPositionPrice(pi));
this.first_validation = true;
}
},

Unexpected undefined returned as subscription from Observable, where object expected

I cannot find the issue with the code below, why I am getting undefined for subscription, when an object has been returned that defines the unsubscribe method. Ignore map() as I just call static fromTimeout directly here.
class Observable {
constructor(subscribe) {
this._subscribe = subscribe
}
// Expose public api method for observers to use...
subscribe(observer) {
this._subscribe(observer)
}
static fromTimeout(time) {
return new Observable(function(observer) {
let handler = function() {
observer.next("next value")
observer.complete()
}
const timeout = setTimeout(handler, time)
return {
unsubscribe: function() {
clearTimeout(timeout)
}
}
})
}
map(projection) {
const self = this
return new Observable(function(observer) {
const subscription = self.subscribe({
next: function(value) {
observer.next(projection(value))
},
complete: function() {
observer.complete()
}
})
return subscription
})
}
}
const obs1 = Observable.fromTimeout(500)
const subscription = obs1
// .map(v => v.toUpperCase())
.subscribe({
next: function(value) {
console.log("next: ", value)
},
complete: function() {
console.log("complete called")
}
})
setTimeout(function() {
console.log(subscription) // WHY undefined!?
subscription.unsubscribe()
}, 1000)
The return statement is missing in:
subscribe(observer) {
return this._subscribe(observer)
}
class Observable {
constructor(subscribe) {
this._subscribe = subscribe
}
// Expose public api method for observers to use...
subscribe(observer) {
return this._subscribe(observer)
}
static fromTimeout(time) {
return new Observable(function(observer) {
let handler = function() {
observer.next("next value")
observer.complete()
}
const timeout = setTimeout(handler, time)
return {
unsubscribe: function() {
clearTimeout(timeout)
}
}
})
}
map(projection) {
const self = this
return new Observable(function(observer) {
const subscription = self.subscribe({
next: function(value) {
observer.next(projection(value))
},
complete: function() {
observer.complete()
}
})
return subscription
})
}
}
const obs1 = Observable.fromTimeout(500)
const subscription = obs1
// .map(v => v.toUpperCase())
.subscribe({
next: function(value) {
console.log("next: ", value)
},
complete: function() {
console.log("complete called")
}
})
setTimeout(function() {
console.log(subscription) // WHY undefined!?
subscription.unsubscribe()
}, 1000)

Pure JavaScript $.Deferred

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)

Fluent async api with ES6 proxy javascript

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);
}
})();

Categories

Resources