Skip to content

Commit

Permalink
feat: add (optional) defaultValue configuration to firstValueFrom and…
Browse files Browse the repository at this point in the history
… lastValueFrom (ReactiveX#6204)

* feat: add defaultValue to first/lastValueFrom

* chore: update api_guardian

* refactor: use config objects for defaultValue

* chore: update api_guardian

* fix: test config arg using typeof
  • Loading branch information
cartant committed Apr 16, 2021
1 parent f23ad72 commit df51b04
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 16 deletions.
2 changes: 2 additions & 0 deletions api_guard/dist/types/index.d.ts
Expand Up @@ -124,6 +124,7 @@ export declare type FactoryOrValue<T> = T | (() => T);

export declare type Falsy = null | undefined | false | 0 | -0 | 0n | '';

export declare function firstValueFrom<T, D>(source: Observable<T>, config: FirstValueFromConfig<D>): Promise<T | D>;
export declare function firstValueFrom<T>(source: Observable<T>): Promise<T>;

export declare function forkJoin<T extends AnyCatcher>(arg: T): Observable<unknown>;
Expand Down Expand Up @@ -182,6 +183,7 @@ export declare function interval(period?: number, scheduler?: SchedulerLike): Ob

export declare function isObservable(obj: any): obj is Observable<unknown>;

export declare function lastValueFrom<T, D>(source: Observable<T>, config: LastValueFromConfig<D>): Promise<T | D>;
export declare function lastValueFrom<T>(source: Observable<T>): Promise<T>;

export declare function merge<A extends readonly unknown[]>(...sources: [...ObservableInputTuple<A>]): Observable<A[number]>;
Expand Down
18 changes: 15 additions & 3 deletions spec-dtslint/firstValueFrom-spec.ts
Expand Up @@ -2,7 +2,19 @@ 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
it('should infer the element type', () => {
const r = firstValueFrom(a$); // $ExpectType Promise<A>
})

it('should infer the element type from a default value', () => {
const r = firstValueFrom(a$, { defaultValue: null }); // $ExpectType Promise<A | null>
});

it('should require an argument', () => {
const r = firstValueFrom(); // $ExpectError
});

it('should require an observable argument', () => {
const r = firstValueFrom(Promise.resolve(42)); // $ExpectError
});
});
18 changes: 15 additions & 3 deletions spec-dtslint/lastValueFrom-spec.ts
Expand Up @@ -2,7 +2,19 @@ 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
it('should infer the element type', () => {
const r = lastValueFrom(a$); // $ExpectType Promise<A>
});

it('should infer the element type from a default value', () => {
const r = lastValueFrom(a$, { defaultValue: null }); // $ExpectType Promise<A | null>
});

it('should require an argument', () => {
const r = lastValueFrom(); // $ExpectError
});

it('should require an observable argument', () => {
const r = lastValueFrom(Promise.resolve(42)); // $ExpectError
});
});
17 changes: 17 additions & 0 deletions spec/firstValueFrom-spec.ts
Expand Up @@ -11,6 +11,23 @@ describe('firstValueFrom', () => {
expect(finalized).to.be.true;
});

it('should support a default value', async () => {
const source = EMPTY;
const result = await firstValueFrom(source, { defaultValue: 0 });
expect(result).to.equal(0);
});

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

it('should error for empty observables', async () => {
const source = EMPTY;
let error: any = null;
Expand Down
17 changes: 17 additions & 0 deletions spec/lastValueFrom-spec.ts
Expand Up @@ -14,6 +14,23 @@ describe('lastValueFrom', () => {
expect(finalized).to.be.true;
});

it('should support a default value', async () => {
const source = EMPTY;
const result = await lastValueFrom(source, { defaultValue: 0 });
expect(result).to.equal(0);
});

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

it('should error for empty observables', async () => {
const source = EMPTY;
let error: any = null;
Expand Down
24 changes: 19 additions & 5 deletions src/internal/firstValueFrom.ts
Expand Up @@ -2,13 +2,21 @@ import { Observable } from './Observable';
import { EmptyError } from './util/EmptyError';
import { SafeSubscriber } from './Subscriber';

export interface FirstValueFromConfig<T> {
defaultValue: T;
}

export function firstValueFrom<T, D>(source: Observable<T>, config: FirstValueFromConfig<D>): Promise<T | D>;
export function firstValueFrom<T>(source: Observable<T>): Promise<T>;

/**
* 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}.
* returned promise will reject with {@link EmptyError} or will resolve
* with the default value if a default was specified.
*
* If the observable stream emits an error, the returned promise will reject
* with that error.
Expand Down Expand Up @@ -41,17 +49,23 @@ import { SafeSubscriber } from './Subscriber';
* ```
*
* @param source the observable to convert to a promise
* @param config a configuration object to define the `defaultValue` to use if the source completes without emitting a value
*/
export function firstValueFrom<T>(source: Observable<T>) {
return new Promise<T>((resolve, reject) => {
export function firstValueFrom<T, D>(source: Observable<T>, config?: FirstValueFromConfig<D>) {
const hasConfig = typeof config === 'object';
return new Promise<T | D>((resolve, reject) => {
const subscriber = new SafeSubscriber<T>({
next: value => {
next: (value) => {
resolve(value);
subscriber.unsubscribe();
},
error: reject,
complete: () => {
reject(new EmptyError());
if (hasConfig) {
resolve(config!.defaultValue);
} else {
reject(new EmptyError());
}
},
});
source.subscribe(subscriber);
Expand Down
22 changes: 17 additions & 5 deletions src/internal/lastValueFrom.ts
@@ -1,13 +1,21 @@
import { Observable } from './Observable';
import { EmptyError } from './util/EmptyError';

export interface LastValueFromConfig<T> {
defaultValue: T;
}

export function lastValueFrom<T, D>(source: Observable<T>, config: LastValueFromConfig<D>): Promise<T | D>;
export function lastValueFrom<T>(source: Observable<T>): Promise<T>;

/**
* 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}.
* returned promise will reject with {@link EmptyError} or will resolve
* with the default value if a default was specified.
*
* If the observable stream emits an error, the returned promise will reject
* with that error.
Expand Down Expand Up @@ -40,24 +48,28 @@ import { EmptyError } from './util/EmptyError';
* ```
*
* @param source the observable to convert to a promise
* @param config a configuration object to define the `defaultValue` to use if the source completes without emitting a value
*/
export function lastValueFrom<T>(source: Observable<T>) {
return new Promise<T>((resolve, reject) => {
export function lastValueFrom<T, D>(source: Observable<T>, config?: LastValueFromConfig<D>) {
const hasConfig = typeof config === 'object';
return new Promise<T | D>((resolve, reject) => {
let _hasValue = false;
let _value: T;
source.subscribe({
next: value => {
next: (value) => {
_value = value;
_hasValue = true;
},
error: reject,
complete: () => {
if (_hasValue) {
resolve(_value);
} else if (hasConfig) {
resolve(config!.defaultValue);
} else {
reject(new EmptyError());
}
},
});
});
}
}

0 comments on commit df51b04

Please sign in to comment.