I want to construct a function that creates and returns an Observable. Depending on the parameters this function receives it should either create the Observable and return it directly or (in a special case) call another asynchronous function before returning the Observable.
Abstract example:
async function doSomethingAsync(){
return 'success';
}
function returnObservable(specialCase: boolean): Observable<any>
const observable = new Observable(
observer =>
observer.next(1)
}
);
if(specialCase) {
doSomethingAsync().then(
then => {
// this does not work, of course, but that's what I would like to be able to do
return observable;
}
)
} else {
return observable;
}
}
Now my problem is that appearently I cannot call an asynchronous function and then return the Observable. I could make this whole returnObservable function asynchronous and just await the doSomethingAsync but then the returnObservable function would return a Promise returning an Observable - and that's not what I want. The consumer of this function should receive the Observable directly.
I hope I could make my problem clear. Is there any way to solve this problem?
Is there any way to solve this problem?
Because you're returning an Observable, there may be.
It's impossible to have returnObservable block synchronously until an async function returns. But, you can have it return an Observable that (optionally) won't emit any events until the async function completes. You could even have it not even call the async function until/unless something subscribes to it.
Something vaguely like:
function returnObservable(specialCase: boolean): Observable<any>
const promise = specialCase ? doSomethingAsync() : Promise.resolve();
const observable = new Observable(
observer => {
promise
.then(() => {
observer.next(1);
})
.catch(error => {
// deal with the error
});
}
);
return observable;
}
(You might even be able to use fromPromise. I'm not strong enough on Rx.js to be sure.)
Transform the promise into an observable:
async function doSomethingAsync(){
return 'success';
}
function returnObservable(specialCase: boolean): Observable<any>
const observable = new Observable(
observer =>
observer.next(1)
}
);
if(specialCase) {
return Observable.from(doSomethingAsync().then(
then => {
// this does not work, of course, but that's what I would like to be able to do
return observable;
}
));
}
return observable;
}
Related
I'm given a code with 3 parts that must not be changed:
1. a call to getData function, and then prints the output.
getData().then(console.log);
2. The function signeture:
async getData(): Promise<string[]>
3. function getDataFromUrl
function getDataFromUrl(url: string, callback: any) {
fetch(URL)
.then((content: any) => content.json())
.then((data) => callback(data));
}
This is my implementation for getData function:
async getData(): Promise<string[]> {
return await new Promise<any>(function(resolve, reject) {
resolve(getDataFromUrl(myUrl, ((data: string[]): string[]=> {
return data
})))
});
}
The problem is that the code after fetch, runs after
getData().then(console.log);
so console.log prints: undefined
What do I need to change in the getData function?
Thanks
Instead of resolving the getDataFromUrl function, you should resolve the value that the callback function exposes.
getData(): Promise<string[]> {
return new Promise<string[]>(function(resolve, reject) {
getDataFromUrl(myUrl, (data: string[]): string[] => {
resolve(data)
})
});
}
It's unfortunate you can't change anything about the other functions, because returning the promise created by fetch in your getDataFromUrl function, would make the code way better, as Quentin demonstrates.
getDataFromUrl doesn't have a return value, so it returns undefined.
So you pass undefined to resolve (which resolves your other promise) and then, later the promise created by getDataFromUrl (or rather fetch) resolves.
Don't create bonus promises
Don't mix callbacks and promises
Just return the promise from fetch.
function getDataFromUrl(url: string, callback: any) {
return fetch(URL)
.then((content: any) => content.json());
}
And use that promise instead of creating a new one in getData (which doesn't need to use await because that will just resolve the promise which will immediately get wrapped in a new promise by async.
getData(): Promise<string[]> {
return getDataFromUrl(myUrl);
}
function getDataFromUrl(url, f) {
// call fetch (returns promise) but we can use then as well.
// note our callback: f where we pass the data to the callback.
fetch(url).then((content) => content.json()).then(data => f(data));
}
// edit: note we don't need this be async anymore...
async function getData() {
// create a promise...
return new Promise((resolve, reject) => {
// we call with callback arrow and resolve data here
getDataFromUrl('https://randomuser.me/api/', (data) => resolve(data));
})
}
// we print the data once the getData is done fetching
// you can also use reject to handle anything that goes wrong in above.
getData().then(data => console.log(data));
Resources used:
Fetch API
Async, Await, and Promise
Callback
I want to cancel a promise in my React application using the AbortController and unfortunately the abort event is not recognized so that I cannot react to it.
My setup looks like this:
WrapperComponent.tsx: Here I'm creating the AbortController and pass the signal to my method calculateSomeStuff that returns a Promise. The controller I'm passing to my Table component as a prop.
export const WrapperComponent = () => {
const controller = new AbortController();
const signal = abortController.signal;
// This function gets called in my useEffect
// I'm passing signal to the method calculateSomeStuff
const doSomeStuff = (file: any): void => {
calculateSomeStuff(signal, file)
.then((hash) => {
// do some stuff
})
.catch((error) => {
// throw error
});
};
return (<Table controller={controller} />)
}
The calculateSomeStuff method looks like this:
export const calculateSomeStuff = async (signal, file): Promise<any> => {
if (signal.aborted) {
console.log('signal.aborted', signal.aborted);
return Promise.reject(new DOMException('Aborted', 'AbortError'));
}
for (let i = 0; i <= 10; i++) {
// do some stuff
}
const secret = 'ojefbgwovwevwrf';
return new Promise((resolve, reject) => {
console.log('Promise Started');
resolve(secret);
signal.addEventListener('abort', () => {
console.log('Aborted');
reject(new DOMException('Aborted', 'AbortError'));
});
});
};
Within my Table component I call the abort() method like this:
export const Table = ({controller}) => {
const handleAbort = ( fileName: string) => {
controller.abort();
};
return (
<Button
onClick={() => handleAbort()}
/>
);
}
What am I doing wrong here? My console.logs are not visible and the signal is never set to true after calling the handleAbort handler.
Based off your code, there are a few corrections to make:
Don't return new Promise() inside an async function
You use new Promise if you're taking something event-based but naturally asynchronous, and wrap it into a Promise. Examples:
setTimeout
Web Worker messages
FileReader events
But in an async function, your return value will already be converted to a promise. Rejections will automatically be converted to exceptions you can catch with try/catch. Example:
async function MyAsyncFunction(): Promise<number> {
try {
const value1 = await functionThatReturnsPromise(); // unwraps promise
const value2 = await anotherPromiseReturner(); // unwraps promise
if (problem)
throw new Error('I throw, caller gets a promise that is eventually rejected')
return value1 + value2; // I return a value, caller gets a promise that is eventually resolved
} catch(e) {
// rejected promise and other errors caught here
console.error(e);
throw e; // rethrow to caller
}
}
The caller will get a promise right away, but it won't be resolved until the code hits the return statement or a throw.
What if you have work that needs to be wrapped with a Promise constructor, and you want to do it from an async function? Put the Promise constructor in a separate, non-async function. Then await the non-async function from the async function.
function wrapSomeApi() {
return new Promise(...);
}
async function myAsyncFunction() {
await wrapSomeApi();
}
When using new Promise(...), the promise must be returned before the work is done
Your code should roughly follow this pattern:
function MyAsyncWrapper() {
return new Promise((resolve, reject) => {
const workDoer = new WorkDoer();
workDoer.on('done', result => resolve(result));
workDoer.on('error', error => reject(error));
// exits right away while work completes in background
})
}
You almost never want to use Promise.resolve(value) or Promise.reject(error). Those are only for cases where you have an interface that needs a promise but you already have the value.
AbortController is for fetch only
The folks that run TC39 have been trying to figure out cancellation for a while, but right now there's no official cancellation API.
AbortController is accepted by fetch for cancelling HTTP requests, and that is useful. But it's not meant for cancelling regular old work.
Luckily, you can do it yourself. Everything with async/await is a co-routine, there's no pre-emptive multitasking where you can abort a thread or force a rejection. Instead, you can create a simple token object and pass it to your long running async function:
const token = { cancelled: false };
await doLongRunningTask(params, token);
To do the cancellation, just change the value of cancelled.
someElement.on('click', () => token.cancelled = true);
Long running work usually involves some kind of loop. Just check the token in the loop, and exit the loop if it's cancelled
async function doLongRunningTask(params: string, token: { cancelled: boolean }) {
for (const task of workToDo()) {
if (token.cancelled)
throw new Error('task got cancelled');
await task.doStep();
}
}
Since you're using react, you need token to be the same reference between renders. So, you can use the useRef hook for this:
function useCancelToken() {
const token = useRef({ cancelled: false });
const cancel = () => token.current.cancelled = true;
return [token.current, cancel];
}
const [token, cancel] = useCancelToken();
// ...
return <>
<button onClick={ () => doLongRunningTask(token) }>Start work</button>
<button onClick={ () => cancel() }>Cancel</button>
</>;
hash-wasm is only semi-async
You mentioned you were using hash-wasm. This library looks async, as all its APIs return promises. But in reality, it's only await-ing on the WASM loader. That gets cached after the first run, and after that all the calculations are synchronous.
Async code that doesn't actually await doesn't have any benefits. It will not pause to unblock the thread.
So how can you let your code breath if you've got CPU intensive code like what hash-wasm uses? You can do your work in increments, and schedule those increments with setTimeout:
for (const step of stepsToDo) {
if (token.cancelled)
throw new Error('task got cancelled');
// schedule the step to run ASAP, but let other events process first
await new Promise(resolve => setTimeout(resolve, 0));
const chunk = await loadChunk();
updateHash(chunk);
}
(Note that I'm using a Promise constructor here, but awaiting immediately instead of returning it)
The technique above will run slower than just doing the task. But by yielding the thread, stuff like React updates can execute without an awkward hang.
If you really need performance, check out Web Workers, which let you do CPU-heavy work off-thread so it doesn't block the main thread. Libraries like workerize can help you convert async functions to run in a worker.
That's everything I have for now, I'm sorry for writing a novel
I can suggest my library (use-async-effect2) for managing the cancellation of asynchronous tasks/promises.
Here is a simple demo with nested async function cancellation:
import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
// just for testing
const factorialAsync = CPromise.promisify(function* (n) {
console.log(`factorialAsync::${n}`);
yield CPromise.delay(500);
return n != 1 ? n * (yield factorialAsync(n - 1)) : 1;
});
function TestComponent({ url, timeout }) {
const [text, setText] = useState("");
const myTask = useAsyncCallback(
function* (n) {
for (let i = 0; i <= 5; i++) {
setText(`Working...${i}`);
yield CPromise.delay(500);
}
setText(`Calculating Factorial of ${n}`);
const factorial = yield factorialAsync(n);
setText(`Done! Factorial=${factorial}`);
},
{ cancelPrevious: true }
);
return (
<div>
<div>{text}</div>
<button onClick={() => myTask(15)}>
Run task
</button>
<button onClick={myTask.cancel}>
Cancel task
</button>
</div>
);
}
i want to know how to 'wait' without using async/await to finish observable and then return that value
// import {Observable, of} from 'rxjs';
getFirstQueue() {
if (this.insideVar) {
return of(this.insideVar);
}
this.http.get(this.createUrl(`some_resource`)).subscribe((someVar: any) => {
this.insideVar = someVar;
});
return this.insideVar;
}
You can't return from a subscription. You have to return an Observable. A function higher up the stack will then subscribe to this observable.
You can use tap to perform the same side-effect you are currently doing in the subscribe body before returning.
getFirstQueue(): Observable<any> {
if (this.activeQueue) {
return of(this.activeQueue);
}
return this.http.get(this.createUrl(`some_resource`)).pipe(
tap((someVar: any) => {
this.insideVar = someVar;
})
);
}
It is not possible to go from asynchronous callback to synchronous context. For that you need to use async/await, but if you take a look at the decompiled code the async/await is just a syntactic sugar on top of Promises, so it only looks like it came from async to sync
Is there any chance to return from helpMe function value from getDataFromApi() ? So far every time i call this function i get "null" value.
async function helpMe() {
let promise = null;
let sub = someService.someObservable.subscribe(async () => {
promise = await getDataFromApi()
})
subscriptions.push(sub)
return promise;
}
The first goal is i need to store subscription in global sub array. The second goal is when i get response with status 400 - I dont want to open modal. Only when i get 200 and everything is allright i want modal to be opened.
function async load() {
const promise = await helpMe();
openModal();
}
Passing an async function to subscribe is pointless - it throws away the returned promise, it doesn't wait for anything. You would need to use
new Promise((resolve, reject) => {
someService.someObservable.subscribe(resolve, reject);
})
or just call the builtin toPromise method:
async function helpMe() {
await someService.someObservable.toPromise();
return getDataFromApi();
}
Instead of using async/await feature, you could just go with plain rxjs. The switchmap operator might help you here:
public helpMe()
{
return this.someService.someObservable.pipe(
switchMap(result =>
{
return someDataFromApi();
}),
tap(resultFromApi => {
// ... do something with `resultFromApi` from `someDataFromApi`.
}),
).toPromise();
}
I can't understand why adding .take(1) at the end of my observable, triggers the result, and if I don't it keeps pending:
function generateToken(identifier){
return new Observable<string>((observer) => {
jwt.sign(identifier, 'devsecret', (err, token) => {
if (err) {
observer.error(err);
} else if (token) {
observer.next(token);
}
});
}).pipe( take(1));
}
Does anyone know why? Care to share the reason and whether this is a proper implementation? Mind that I'm not subscribing to this function anywhere else, but I keep piping the result.
here is where I call the method and return a response with a authorization header
public login(identifier): Observable<any> {
return generateToken(identifier).pipe(
catchError((err: Error) => of(err)),
map(token => {
return {'Authorization': token}
}));
}
and last but not least this function is converted in a promise and the response is returned as an http request
function async userLogin(identifier) {
return await login(identifier).toPromise();
}
Thanks for your time and patience
This explains your issue:
return await login(identifier).toPromise();
Promise resolves on Observable completion or rejects if it errors, so it works with take(1) because it takes the first Observable value and completes it.
You can also get the output if you complete it. And it looks a bit more appropriate:
} else if (token) {
observer.next(token);
observer.complete(); <---
}
take(1) makes sure the subscriber.complete() method is called right after the first item is emitted. BTW, this can be done directly by calling observer.complete() after the observer.next().
toPromise() will only resolve once the stream is completed, not on every emission.
Try
.pipe( () => take(1) );
Or
.pipe( take );
Same behavior happens in promises. The callback wants a function not a statement.