If I have a method that sometimes returns a string and sometimes returns a promise that results in a string how can I chain that in my observable?
Example
function doSomething() {
if (Math.random() < 0.5) {
return 'test';
}
return new Promise(resolve => resolve('test'));
}
const example = of(undefined).pipe(
mergeMap(() => doSomething())
);
const subscribe = example.subscribe(val => console.log(val));
I would like to always log "test".
One way of solving the problem would be to make the function passed to mergeMap an async function and to await the return value of doSomething, like this:
const example = of(undefined).pipe(
mergeMap(async () => await doSomething())
);
You can do this because mergeMap expects the function to return an ObservableInput and a promise is a valid ObservableInput.
Doing so takes advantage of that fact await will accept either a promise or an ordinary value - to which the promise returned by the async function will resolve.
function doSomething() {
if (Math.random() < 0.5) {
return 'test';
}
return new Promise(resolve => resolve('test'));
}
const example = of(undefined)
.pipe(
map(() => doSomething()),
mergeMap(value => value instanceof Promise ? from(value) : from([value]))
);
const subscribe = example.subscribe(val => console.log(val));
Not that you asked, but your code is an anti-pattern. Your doSomething method should always return the same value type, in your case, either a string or a promise.
Related
If you're using JS, the documentation works well. But in case of angular I would prefer to handle observables instead of promises. The problem is that this kind of promise has a handler. I tried many approaches listed below but nothing seems to work.
from(listen("click", v => v))
let x = async() => listen("click", v => v)
Does anyone know how to convert this kind of event to an Observable?
The response is always this:
function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
}
You would have to create an Observable yourself with new Observable.
const obs$ = new Observable((subscriber) => {
const unlisten = listen("click", v => subscriber.next(v))
return async () => {
(await unlisten)()
}
})
Inside the callback, we listen to the events and pass each event to subscriber.next(v).
We also want to call unlisten when the Observable is unsubscribed to clean up the event listener. We can do that by returning the unlisten. The function returned by the callback will be called when the Observable is unsubscribed.
Thanks to #Tobias S., I was able to create those 2 functions and reuse them in all my services.
import {from, map, Observable, ObservableInput, ObservedValueOf} from "rxjs";
import {emit, listen, Event} from "#tauri-apps/api/event";
export function tauriListen(listenerName: string): Observable<any> {
return new Observable<any>((subscriber) => {
// return from(listen(listenerName, v => subscriber.next(v))).subscribe()
const unlisten = listen(listenerName, v => subscriber.next(v))
return async () => {
(await unlisten)()
}
}).pipe(
map((response: Event<any>) => response.payload)
);
}
export function tauriEmit(emitterName: string, payload: any) {
return from(emit(emitterName, payload));
}
I'm trying to achieve next:
private beginTransaction(): Observable() {
..
}
private test(): void {
this.beginTransaction().subscribe((): void => {
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
this.commitTransaction();
});
}
beginTransaction can be called concurrently, but should delay the observable until first or only one beginTransaction finished.
In order words: Only one transaction can be in progress at any time.
What have I tried:
private transactionInProgress: boolean = false;
private canBeginTransaction: Subject<void> = new Subject<void>();
private bla3(): void {
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 1');
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 2');
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 3');
this.commitTransaction();
});
}
private commitTransaction(): void {
this.transactionInProgress = false;
this.canBeginTransaction.next();
}
private beginTransaction(): Observable<void> {
if(this.transactionInProgress) {
return of(undefined)
.pipe(
skipUntil(this.canBeginTransaction),
tap((): void => {
console.log('begin transaction');
})
);
}
this.transactionInProgress = true;
return of(undefined);
}
What you've asked about is pretty vague and general. Without a doubt, a more constrained scenario could probably look a whole lot simpler.
Regardless, here I create a pipeline that only lets transaction(): Observable be subscribed to once at a time.
Here's how that might look:
/****
* Represents what each transaction does. Isn't concerned about
* order/timing/'transactionInProgress' or anything like that.
*
* Here is a fake transaction that just takes 3-5 seconds to emit
* the string: `Hello ${name}`
****/
function transaction(args): Observable<string> {
const name = args?.message;
const duration = 3000 + (Math.random() * 2000);
return of("Hello").pipe(
tap(_ => console.log("starting transaction")),
switchMap(v => timer(duration).pipe(
map(_ => `${v} ${name}`)
)),
tap(_ => console.log("Ending transation"))
);
}
// Track transactions
let currentTransactionId = 0;
// Start transactions
const transactionSubj = new Subject<any>();
// Perform transaction: concatMap ensures we only start a new one if
// there isn't a current transaction underway
const transaction$ = transactionSubj.pipe(
concatMap(({id, args}) => transaction(args).pipe(
map(payload => ({id, payload}))
)),
shareReplay(1)
);
/****
* Begin a new transaction, we give it an ID since transactions are
* "hot" and we don't want to return the wrong (earlier) transactions,
* just the current one started with this call.
****/
function beginTransaction(args): Observable<any> {
return defer(() => {
const currentId = currentTransactionId++;
transactionSubj.next({id: currentId, args});
return transaction$.pipe(
first(({id}) => id === currentId),
map(({payload}) => payload)
);
})
}
// Queue up 3 transactions, each one will wait for the previous
// one to complete before it will begin.
beginTransaction({message: "Dave"}).subscribe(console.log);
beginTransaction({message: "Tom"}).subscribe(console.log);
beginTransaction({message: "Tim"}).subscribe(console.log);
Asynchronous Transactions
The current setup requires transactions to be asynchronous, or you risk losing the first one. The workaround for that is not simple, so I've built an operator that subscribes, then calls a function as soon as possible after that.
Here it is:
function initialize<T>(fn: () => void): MonoTypeOperatorFunction<T> {
return s => new Observable(observer => {
const bindOn = name => observer[name].bind(observer);
const sub = s.subscribe({
next: bindOn("next"),
error: bindOn("error"),
complete: bindOn("complete")
});
fn();
return {
unsubscribe: () => sub.unsubscribe
};
});
}
and here it is in use:
function beginTransaction(args): Observable<any> {
return defer(() => {
const currentId = currentTransactionId++;
return transaction$.pipe(
initialize(() => transactionSubj.next({id: currentId, args})),
first(({id}) => id === currentId),
map(({payload}) => payload)
);
})
}
Aside: Why Use defer?
Consider re-writting beginTransaction:
function beginTransaction(args): Observable<any> {
const currentId = currentTransactionId++;
return transaction$.pipe(
initialize(() => transactionSubj.next({id: currentId, args})),
first(({id}) => id === currentId),
map(({payload}) => payload)
);
}
In this case, the ID is set at the moment you invoke beginTransaction.
// The ID is set here, but it won't be used until subscribed
const preppedTransaction = beginTransaction({message: "Dave"});
// 10 seconds later, that ID gets used.
setTimeout(
() => preppedTransaction.subscribe(console.log),
10000
);
If transactionSubj.next is called without the initialize operator, then this problem gets even worse as transactionSubj.next would also get called 10 seconds before the observable is subscribed to (You're sure to miss the output)
The problems continue:
What if you want to subscribe to the same observable twice?
const preppedTransaction = beginTransaction({message: "Dave"});
preppedTransaction.subscribe(
value => console.log("First Subscribe: ", value)
);
preppedTransaction.subscribe(
value => console.log("Second Subscribe: ", value)
);
I would expect the output to be:
First Subscribe: Hello Dave
Second Subscribe: Hello Dave
Instead, you get
First Subscribe: Hello Dave
First Subscribe: Hello Dave
Second Subscribe: Hello Dave
Second Subscribe: Hello Dave
Because you don't get a new ID on subscribing, the two subscriptions share one ID. defer fixes this problem by not assigning an id until subscription. This becomes seriously important when managing errors in streams (letting you re-try an observable after it errors).
I am not sure I have understood the problem right, but it looks to me as concatMap is the operator you are looking for.
An example could be the following
const transactionTriggers$ = from([
't1', 't2', 't3'
])
function processTransation(trigger: string) {
console.log(`Start processing transation triggered by ${trigger}`)
// do whatever needs to be done and then return an Observable
console.log(`Transation triggered by ${trigger} processing ......`)
return of(`Transation triggered by ${trigger} processed`)
}
transactionTriggers$.pipe(
concatMap(trigger => processTransation(trigger)),
tap(console.log)
).subscribe()
You basically start from a stream of events, where each event is supposed to trigger the processing of the transaction.
Then you use processTransaction function to do whatever you have to do to process a transaction. processTransactio needs to return an Observable which emits the result of the processing when the transaction has been processed and then completes.
Then in the pipe you can use tap to do further stuff with the result of the processing, if required.
You can try the code in this stackblitz.
I'm looking for a way to buffer values of an observable until some other observable has emitted, but then emit all the previous values. Something like skipUntil, if skipUntil also emitted skipped values as soon as the second observable emitted.
--a--b----c-----d---e--- (source)
-----------1------------- (other1)
------------abc-d---e----(desired output)
You can use bufferWhen:
import { fromEvent, interval } from 'rxjs';
import { bufferWhen } from 'rxjs/operators';
const clicks = fromEvent(document, 'click');
const buffered = clicks.pipe(bufferWhen(() =>
interval(1000 + Math.random() * 4000)
));
buffered.subscribe(x => console.log(x));
Here's the custom operator I came up with. Not sure if it can be done in a prettier way.
export function bufferUntil(stopBufferSignal: Observable<any>) {
return <T>(source: Observable<T>): Observable<T> => {
return source.pipe(buffer(stopBufferSignal),
take(1),
flatMap(ar => {
const sourceWithNSkipped$: Observable<T> = source.pipe(skip(ar.length));
const bufferedItems$: Observable<T> = from(ar);
return bufferedItems$.pipe(concat(sourceWithNSkipped$))
}));
}
}
I am learning to use RXJS. In this scenario, I am chaining a few async requests using rxjs. At the last mergeMap, I'd like to have access to the first mergeMap's params. I have explored the option using Global or withLatest, but neither options seem to be the right fit here.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => {
return readCSVFile(gauge.id);
}),
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id))),
catchError(error => of(`Bad Promise: ${error}`))
);
readCSVFile is an async request which returns an observable to read CSV from a remote server.
readStringToArray is another async request which returns an observable to convert string to Arrays
transposeArray just does the transpose
uploadToDB is async DB request, which needs gague.id from the first mergeMap.
How do I get that? It would be great to take some advice on why the way I am doing it is bad.
For now, I am just passing the ID layer by layer, but it doesn't feel to be correct.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id)),
mergeMap(({ data, gaugeId }: any) => readStringToArray(data, gaugeId)),
map(({ data, gaugeId }) => transposeArray(data, gaugeId)),
mergeMap(({ data, gaugeId }) => uploadToDB(data, gaugeId)),
catchError(error => of(`Bad Promise: ${error}`))
);
Why don't you do simply this?
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id)))
)),
catchError(error => of(`Bad Promise: ${error}`))
);
You can also wrap the inner observable in a function:
uploadCSVFilesFromGaugeID(gaugeID): Observable<void> {
return readCSVFile(gaugeID).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gaugeID))
);
}
In order to do this at the end:
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => uploadCSVFileFromGaugeID(gauge.id)),
catchError(error => of(`Bad Promise: ${error}`))
);
MergeMap requires all observable inputs; else, previous values may be returned.
It is a difficult job to concatenate and display the merging response. But here is a straightforward example I made so you can have a better idea. How do we easily perform sophisticated merging.
async playWithBbservable() {
const observable1 = new Observable((subscriber) => {
subscriber.next(this.test1());
});
const observable2 = new Observable((subscriber) => {
subscriber.next(this.test2());
});
const observable3 = new Observable((subscriber) => {
setTimeout(() => {
subscriber.next(this.test3());
subscriber.complete();
}, 1000);
});
console.log('just before subscribe');
let result = observable1.pipe(
mergeMap((val: any) => {
return observable2.pipe(
mergeMap((val2: any) => {
return observable3.pipe(
map((val3: any) => {
console.log(`${val} ${val2} ${val3}`);
})
);
})
);
})
);
result.subscribe({
next(x) {
console.log('got value ' + x);
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
console.log('just after subscribe');
}
test1() {
return 'ABC';
}
test2() {
return 'PQR';
}
test3() {
return 'ZYX';
}
I'm trying to chain promises, but the second one doesn't call the resolve function. What do I do wrong?
function getCustomers(){
let promise = new Promise((resolve, reject) => {
console.log("Getting customers");
// Emulate an async server call here
setTimeout(() => {
var success = true;
if (success) {
resolve( "John Smith"); // got the customer
} else {
reject("Can't get customers");
}
}, 1000);
}
);
return promise;
}
function getOrders(customer) {
let promise = new Promise((resolve, reject) => {
console.log("Getting orders");
// Emulate an async server call here
setTimeout(() => {
var success = true;
if (success) {
resolve("Order 123"); // got the order
} else {
reject("Can't get orders");
}
}, 1000);
}
);
return promise;
}
getCustomers()
.then((cust) => getOrders(cust))
.catch((err) => console.log(err));
console.log("Chained getCustomers and getOrders. Waiting for results");
The code prints "Getting orders" from the second function, but doesn't print "Order 123":
Getting customers
Chained getCustomers and getOrders. Waiting for results
Getting orders
Update. I wanted to insert the print on the console between chained methods that return promises. I guess something like this is not possible:
getCustomers()
.then((cust) => console.log(cust)) //Can't print between chained promises?
.then((cust) => getOrders(cust))
.then((order) => console.log(order))
.catch((err) => console.error(err));
You want to chain a success handler (for your resolve result "Order 123"), not an error handler. So use then instead of catch :-)
getCustomers()
.then(getOrders)
.then((orders) => console.log(orders))
.catch((err) => console.error(err));
None of the promises was rejected, so the console.log(err) in your code was never called.
I wanted to insert the print on the console between chained methods that return promises. I guess something like this is not possible:
getCustomers()
.then((cust) => console.log(cust)) //Can't print between chained promises?
.then((cust) => getOrders(cust))
Yes it is possible, but you are intercepting a chain here. So the second then callback actually is not called with cust, but with the result of the first then callback - and console.log returns undefined, with which getOrders will get some problems.
You'd either do
var customers = getCustomers();
customers.then(console.log);
customers.then(getOrders).then((orders) => …)
or simpler just
getCustomers()
.then((cust) => { console.log(cust); return cust; })
.then(getOrders)
.then((orders) => …)
Here is a code example for Sequential execution for node.js using ES6 ECMAScript. Maybe somebody finds it useful.
http://es6-features.org/#PromiseUsage
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
var soapClient = easysoap.createClient(params);
//Sequential execution for node.js using ES6 ECMAScript
console.log('getAllFunctions:');
soapClient.getAllFunctions()
.then((functionArray) => {
return new Promise((resolve, reject) => {
console.log(functionArray);
console.log('getMethodParamsByName:');
resolve();
});
})
.then(() => {
return soapClient.getMethodParamsByName('test1'); //will return promise
})
.then((methodParams) => {
console.log(methodParams.request); //Console log can be outside Promise like here too
console.log(methodParams.response);
console.log('call');
return soapClient.call({ //Return promise
method: 'test1',
params: {
myArg1: 'aa',
myArg2: 'bb'
}
});
})
.then((callResponse) => {
console.log(callResponse); // response data as json
console.log('end');
})
.catch((err) => {
throw new Error(err);
});