Skip to content

Commit

Permalink
feat(lastValue): Adds lastValue method to Observable
Browse files Browse the repository at this point in the history
This is meant to be a more well-behaved, a better-named, replacement for `toPromise`.

Resolves #5075
  • Loading branch information
benlesh committed Oct 15, 2019
1 parent 865b7d3 commit ea79fd2
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 18 deletions.
18 changes: 16 additions & 2 deletions spec-dtslint/Observable-spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable, of, OperatorFunction } from 'rxjs';
import { Observable, of, OperatorFunction, EMPTY, NEVER } from 'rxjs';
import { mapTo } from 'rxjs/operators';

function a<I extends string, O extends string>(input: I, output: O): OperatorFunction<I, O>;
Expand All @@ -11,7 +11,7 @@ function a<I, O extends string>(output: O): OperatorFunction<I, O>;
* That is, `a('0', '1')` returns `OperatorFunction<'0', '1'>`.
* That means that the `a` function can be used to create consecutive
* arguments that are either compatible or incompatible.
*
*
* ```javascript
* a('0', '1'), a('1', '2') // OK
* a('0', '1'), a('#', '2') // Error '1' is not compatible with '#'
Expand Down Expand Up @@ -127,3 +127,17 @@ describe('pipe', () => {
const o = of('foo').pipe(customOperator()); // $ExpectType Observable<string>
});
});

describe('toPromise', () => {
const a = EMPTY.toPromise(); // $ExpectType Promise<void>
const b = NEVER.toPromise(); // $ExpectType Promise<void>
const c = of(1, 2, 3).toPromise() // $ExpectType Promise<number | void>
const d = of(1, 'two', false).toPromise() // $ExpectType Promise<string | number | boolean | void>
});

describe('lastValue', () => {
const a = EMPTY.lastValue(); // $ExpectType Promise<never>
const b = NEVER.lastValue(); // $ExpectType Promise<never>
const c = of(1, 2, 3).lastValue() // $ExpectType Promise<number>
const d = of(1, 'two', false).lastValue() // $ExpectType Promise<string | number | boolean>
});
30 changes: 30 additions & 0 deletions spec/operators/lastValue-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { of, EMPTY, EmptyError, throwError } from 'rxjs';
import { expect } from 'chai';

describe('Observable.prototype.lastValue', () => {
it('should resolve with the last value', async () => {
const value = await of(1, 2, 3).lastValue();
expect(value).to.equal(3);
});

it('should reject for empty observables with EmptyError', async () => {
let error: any;
try {
await EMPTY.lastValue();
} catch (err) {
error = err;
};
expect(error).to.be.instanceOf(EmptyError);
});

it('should reject on errored observables', async () => {
let error: any;
try {
await throwError(new Error('so bad, so so bad')).lastValue();
} catch (err) {
error = err;
};
expect(error).to.be.an.instanceOf(Error);
expect(error.message).to.equal('so bad, so so bad');
});
});
12 changes: 6 additions & 6 deletions spec/operators/toPromise-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import { of, EMPTY, throwError, config } from 'rxjs';

/** @test {toPromise} */
describe('Observable.toPromise', () => {
it('should convert an Observable to a promise of its last value', (done: MochaDone) => {
of(1, 2, 3).toPromise(Promise).then((x: number) => {
it('should convert an Observable to a promise of its last value', done => {
of(1, 2, 3).toPromise(Promise).then(x => {
expect(x).to.equal(3);
done();
});
});

it('should convert an empty Observable to a promise of undefined', (done: MochaDone) => {
it('should convert an empty Observable to a promise of undefined', done => {
EMPTY.toPromise(Promise).then((x) => {
expect(x).to.be.undefined;
done();
});
});

it('should handle errors properly', (done: MochaDone) => {
it('should handle errors properly', done => {
throwError('bad').toPromise(Promise).then(() => {
done(new Error('should not be called'));
}, (err: any) => {
Expand All @@ -26,14 +26,14 @@ describe('Observable.toPromise', () => {
});
});

it('should allow for global config via config.Promise', (done: MochaDone) => {
it('should allow for global config via config.Promise', done => {
let wasCalled = false;
config.Promise = function MyPromise(callback: Function) {
wasCalled = true;
return new Promise(callback as any);
} as any;

of(42).toPromise().then((x: number) => {
of(42).toPromise().then(x => {
expect(wasCalled).to.be.true;
expect(x).to.equal(42);
done();
Expand Down
46 changes: 36 additions & 10 deletions src/internal/Observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { throwError } from './observable/throwError';
import { observable as Symbol_observable } from './symbol/observable';
import { pipeFromArray } from './util/pipe';
import { config } from './config';
import { EmptyError } from './util/EmptyError';

/**
* A representation of any set of values over any amount of time. This is the most basic building block
Expand Down Expand Up @@ -347,28 +348,53 @@ export class Observable<T> implements Subscribable<T> {
}

/* tslint:disable:max-line-length */
toPromise<T>(this: Observable<T>): Promise<T>;
toPromise<T>(this: Observable<T>, PromiseCtor: typeof Promise): Promise<T>;
toPromise<T>(this: Observable<T>, PromiseCtor: PromiseConstructorLike): Promise<T>;
toPromise<T>(this: Observable<T>): Promise<T | void>;
toPromise<T>(this: Observable<T>, PromiseCtor: typeof Promise): Promise<T | void>;
toPromise<T>(this: Observable<T>, PromiseCtor: PromiseConstructorLike): Promise<T | void>;
/* tslint:enable:max-line-length */

/**
* Subscribe to this Observable and get a Promise resolving on
* `complete` with the last emission.
* Subscribe to the observable and resolve with the last emitted value
* when the observable completes. If the observable does not emit at
* least one value, the returned promise will resolve with `undefined`.
*
* @method toPromise
* @param [promiseCtor] a constructor function used to instantiate
* the Promise
* @return A Promise that resolves with the last value emit, or
* rejects on an error.
* @deprecated remove in v8. Use `lastValue` instead.
*/
toPromise(promiseCtor?: PromiseConstructorLike): Promise<T> {
toPromise(promiseCtor?: PromiseConstructorLike): Promise<T | void> {
promiseCtor = getPromiseCtor(promiseCtor);

return new promiseCtor((resolve, reject) => {
let value: any;
this.subscribe((x: T) => value = x, (err: any) => reject(err), () => resolve(value));
}) as Promise<T>;
}) as Promise<T | void>;
}

/**
* Subscribe to the observable and resolve with the last emitted value
* when the observable completes. If the observable does not emit at
* least one value, the returned promise will reject with an {@link EmptyError}.
*/
lastValue(): Promise<T> {
return new Promise<T>((resolve, reject) => {
let hasValue = false;
let lastValue: T;
this.subscribe({
next: value => {
hasValue = true;
lastValue = value;
},
error: reject,
complete: () => {
if (hasValue) {
resolve(lastValue);
} else {
reject(new EmptyError());
}
}
})
});
}
}

Expand Down

0 comments on commit ea79fd2

Please sign in to comment.