Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Strawman Proposal for toPromise replacement #5295

Merged
merged 1 commit into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions spec-dtslint/firstValueFrom-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { firstValueFrom } from 'rxjs';
import { a$ } from 'helpers';

describe('firstValueFrom', () => {
const r0 = firstValueFrom(a$); // $ExpectType Promise<A>
const r1 = firstValueFrom(); // $ExpectError
const r2 = firstValueFrom(Promise.resolve(42)); // $ExpectError
});
8 changes: 8 additions & 0 deletions spec-dtslint/lastValueFrom-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { lastValueFrom } from 'rxjs';
import { a$ } from 'helpers';

describe('lastValueFrom', () => {
const r0 = lastValueFrom(a$); // $ExpectType Promise<A>
const r1 = lastValueFrom(); // $ExpectError
const r2 = lastValueFrom(Promise.resolve(42)); // $ExpectError
});
44 changes: 44 additions & 0 deletions spec/firstValueFrom-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { interval, firstValueFrom, EMPTY, EmptyError, throwError, of } from 'rxjs';
import { expect } from 'chai';
import { finalize } from 'rxjs/operators';

describe('firstValueFrom', () => {
it('should emit the first value as a promise', async () => {
let finalized = false;
const source = interval(10).pipe(finalize(() => (finalized = true)));
const result = await firstValueFrom(source);
expect(result).to.equal(0);
expect(finalized).to.be.true;
});

it('should error for empty observables', async () => {
const source = EMPTY;
let error: any = null;
try {
await firstValueFrom(source);
} catch (err) {
error = err;
}
expect(error).to.be.an.instanceOf(EmptyError);
});

it('should error for errored observables', async () => {
const source = throwError(new Error('blorp!'));
let error: any = null;
try {
await firstValueFrom(source);
} catch (err) {
error = err;
}
expect(error).to.be.an.instanceOf(Error);
expect(error.message).to.equal('blorp!');
});

it('should work with a synchronous observable', async () => {
let finalized = false;
const source = of('apples', 'bananas').pipe(finalize(() => (finalized = true)));
const result = await firstValueFrom(source);
expect(result).to.equal('apples');
expect(finalized).to.be.true;
});
});
13 changes: 13 additions & 0 deletions spec/helpers/test-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ import { iterator } from 'rxjs/internal/symbol/iterator';
import * as sinon from 'sinon';
import { expect } from 'chai';

if (process && process.on) {
/**
* With async/await functions in Node, mocha seems to allow
* tests to pass, even they shouldn't there's something about how
* it handles the rejected promise where it does not notice
* that the test failed.
*/
process.on('unhandledRejection', err => {
console.error(err);
process.exit(1);
});
}

export function lowerCaseO<T>(...args: Array<any>): Observable<T> {
const o: any = {
subscribe(observer: any) {
Expand Down
47 changes: 47 additions & 0 deletions spec/lastValueFrom-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { interval, lastValueFrom, EMPTY, EmptyError, throwError, of } from 'rxjs';
import { expect } from 'chai';
import { finalize, take } from 'rxjs/operators';

describe('lastValueFrom', () => {
it('should emit the last value as a promise', async () => {
let finalized = false;
const source = interval(2).pipe(
take(10),
finalize(() => (finalized = true))
);
const result = await lastValueFrom(source);
expect(result).to.equal(9);
expect(finalized).to.be.true;
});

it('should error for empty observables', async () => {
const source = EMPTY;
let error: any = null;
try {
await lastValueFrom(source);
} catch (err) {
error = err;
}
expect(error).to.be.an.instanceOf(EmptyError);
});

it('should error for errored observables', async () => {
const source = throwError(new Error('blorp!'));
let error: any = null;
try {
await lastValueFrom(source);
} catch (err) {
error = err;
}
expect(error).to.be.an.instanceOf(Error);
expect(error.message).to.equal('blorp!');
});

it('should work with a synchronous observable', async () => {
let finalized = false;
const source = of('apples', 'bananas').pipe(finalize(() => (finalized = true)));
const result = await lastValueFrom(source);
expect(result).to.equal('bananas');
expect(finalized).to.be.true;
});
});
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export { noop } from './internal/util/noop';
export { identity } from './internal/util/identity';
export { isObservable } from './internal/util/isObservable';

/* Promise Conversion */
export { lastValueFrom } from './internal/lastValueFrom';
export { firstValueFrom } from './internal/firstValueFrom';

/* Error types */
export { ArgumentOutOfRangeError } from './internal/util/ArgumentOutOfRangeError';
export { EmptyError } from './internal/util/EmptyError';
Expand Down
54 changes: 54 additions & 0 deletions src/internal/firstValueFrom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Observable } from './Observable';
import { EmptyError } from './util/EmptyError';
import { Subscription } from './Subscription';

/**
* Converts an observable to a promise by subscribing to the observable,
* and returning a promise that will resolve as soon as the first value
* arrives from the observable. The subscription will then be closed.
*
* If the observable stream completes before any values were emitted, the
* returned promise will reject with {@link EmptyError}.
*
* If the observable stream emits an error, the returned promise will reject
* with that error.
*
* ### Example
*
* Wait for the first value from a stream and emit it from a promise in
* an async function.
*
* ```ts
* import { interval, firstValueFrom } from 'rxjs';
*
* async function execute() {
* const source$ = interval(2000);
* const firstNumber = await firstValueFrom(source);
* console.log(`The first number is ${firstNumber}`);
* }
*
* execute();
*
* // Expected output:
* // "The first number is 0"
* ```
*
* @param source the observable to convert to a promise
*/
export function firstValueFrom<T>(source$: Observable<T>) {
return new Promise<T>((resolve, reject) => {
const subs = new Subscription();
subs.add(
source$.subscribe({
next: value => {
resolve(value);
subs.unsubscribe();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that this will handle the degenerate case of an infinite synchronous source. To handle that, I think this would have to unsubscribe via the next callback's this instead. Thoughts?

},
error: reject,
complete: () => {
reject(new EmptyError());
},
})
);
});
}
57 changes: 57 additions & 0 deletions src/internal/lastValueFrom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Observable } from './Observable';
import { EmptyError } from './util/EmptyError';

/**
* Converts an observable to a promise by subscribing to the observable,
* waiting for it to complete, and resolving the returned promise with the
* last value from the observed stream.
*
* If the observable stream completes before any values were emitted, the
* returned promise will reject with {@link EmptyError}.
*
* If the observable stream emits an error, the returned promise will reject
* with that error.
*
* ### Example
*
* Wait for the last value from a stream and emit it from a promise in
* an async function.
*
* ```ts
* import { interval, lastValueFrom } from 'rxjs';
* import { take } from 'rxjs/operators';
*
* async function execute() {
* const source$ = interval(2000).pipe(take(10));
* const finalNumber = await lastValueFrom(source);
* console.log(`The final number is ${finalNumber}`);
* }
*
* execute();
*
* // Expected output:
* // "The final number is 9"
* ```
*
* @param source the observable to convert to a promise
*/
export function lastValueFrom<T>(source: Observable<T>) {
return new Promise<T>((resolve, reject) => {
let _hasValue = false;
let _value: T;
source.subscribe({
next: value => {
_value = value;
_hasValue = true;
},
error: reject,
complete: () => {
if (_hasValue) {
resolve(_value);
} else {
reject(new EmptyError());
}
},
});
});
}