From df51b04d7ec68a72b3a4b0d69c3bb29264c72611 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 16 Apr 2021 11:26:21 +1000 Subject: [PATCH] feat: add (optional) defaultValue configuration to firstValueFrom and lastValueFrom (#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 --- api_guard/dist/types/index.d.ts | 2 ++ spec-dtslint/firstValueFrom-spec.ts | 18 +++++++++++++++--- spec-dtslint/lastValueFrom-spec.ts | 18 +++++++++++++++--- spec/firstValueFrom-spec.ts | 17 +++++++++++++++++ spec/lastValueFrom-spec.ts | 17 +++++++++++++++++ src/internal/firstValueFrom.ts | 24 +++++++++++++++++++----- src/internal/lastValueFrom.ts | 22 +++++++++++++++++----- 7 files changed, 102 insertions(+), 16 deletions(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index 0cc2052c7e..41baf154ca 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -124,6 +124,7 @@ export declare type FactoryOrValue = T | (() => T); export declare type Falsy = null | undefined | false | 0 | -0 | 0n | ''; +export declare function firstValueFrom(source: Observable, config: FirstValueFromConfig): Promise; export declare function firstValueFrom(source: Observable): Promise; export declare function forkJoin(arg: T): Observable; @@ -182,6 +183,7 @@ export declare function interval(period?: number, scheduler?: SchedulerLike): Ob export declare function isObservable(obj: any): obj is Observable; +export declare function lastValueFrom(source: Observable, config: LastValueFromConfig): Promise; export declare function lastValueFrom(source: Observable): Promise; export declare function merge(...sources: [...ObservableInputTuple]): Observable; diff --git a/spec-dtslint/firstValueFrom-spec.ts b/spec-dtslint/firstValueFrom-spec.ts index 7711a2d90a..f709e15594 100644 --- a/spec-dtslint/firstValueFrom-spec.ts +++ b/spec-dtslint/firstValueFrom-spec.ts @@ -2,7 +2,19 @@ import { firstValueFrom } from 'rxjs'; import { a$ } from 'helpers'; describe('firstValueFrom', () => { - const r0 = firstValueFrom(a$); // $ExpectType Promise - const r1 = firstValueFrom(); // $ExpectError - const r2 = firstValueFrom(Promise.resolve(42)); // $ExpectError + it('should infer the element type', () => { + const r = firstValueFrom(a$); // $ExpectType Promise + }) + + it('should infer the element type from a default value', () => { + const r = firstValueFrom(a$, { defaultValue: null }); // $ExpectType Promise + }); + + it('should require an argument', () => { + const r = firstValueFrom(); // $ExpectError + }); + + it('should require an observable argument', () => { + const r = firstValueFrom(Promise.resolve(42)); // $ExpectError + }); }); diff --git a/spec-dtslint/lastValueFrom-spec.ts b/spec-dtslint/lastValueFrom-spec.ts index 2228b8cfcc..5c8450a857 100644 --- a/spec-dtslint/lastValueFrom-spec.ts +++ b/spec-dtslint/lastValueFrom-spec.ts @@ -2,7 +2,19 @@ import { lastValueFrom } from 'rxjs'; import { a$ } from 'helpers'; describe('lastValueFrom', () => { - const r0 = lastValueFrom(a$); // $ExpectType Promise - const r1 = lastValueFrom(); // $ExpectError - const r2 = lastValueFrom(Promise.resolve(42)); // $ExpectError + it('should infer the element type', () => { + const r = lastValueFrom(a$); // $ExpectType Promise + }); + + it('should infer the element type from a default value', () => { + const r = lastValueFrom(a$, { defaultValue: null }); // $ExpectType Promise + }); + + it('should require an argument', () => { + const r = lastValueFrom(); // $ExpectError + }); + + it('should require an observable argument', () => { + const r = lastValueFrom(Promise.resolve(42)); // $ExpectError + }); }); diff --git a/spec/firstValueFrom-spec.ts b/spec/firstValueFrom-spec.ts index 8fc9e3a31c..09e66258b9 100644 --- a/spec/firstValueFrom-spec.ts +++ b/spec/firstValueFrom-spec.ts @@ -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; diff --git a/spec/lastValueFrom-spec.ts b/spec/lastValueFrom-spec.ts index a4540b8dab..27cdc4c4cb 100644 --- a/spec/lastValueFrom-spec.ts +++ b/spec/lastValueFrom-spec.ts @@ -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; diff --git a/src/internal/firstValueFrom.ts b/src/internal/firstValueFrom.ts index d1b9d5315f..e30ad1d341 100644 --- a/src/internal/firstValueFrom.ts +++ b/src/internal/firstValueFrom.ts @@ -2,13 +2,21 @@ import { Observable } from './Observable'; import { EmptyError } from './util/EmptyError'; import { SafeSubscriber } from './Subscriber'; +export interface FirstValueFromConfig { + defaultValue: T; +} + +export function firstValueFrom(source: Observable, config: FirstValueFromConfig): Promise; +export function firstValueFrom(source: Observable): Promise; + /** * 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. @@ -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(source: Observable) { - return new Promise((resolve, reject) => { +export function firstValueFrom(source: Observable, config?: FirstValueFromConfig) { + const hasConfig = typeof config === 'object'; + return new Promise((resolve, reject) => { const subscriber = new SafeSubscriber({ - 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); diff --git a/src/internal/lastValueFrom.ts b/src/internal/lastValueFrom.ts index 2d48025917..e180879f60 100644 --- a/src/internal/lastValueFrom.ts +++ b/src/internal/lastValueFrom.ts @@ -1,13 +1,21 @@ import { Observable } from './Observable'; import { EmptyError } from './util/EmptyError'; +export interface LastValueFromConfig { + defaultValue: T; +} + +export function lastValueFrom(source: Observable, config: LastValueFromConfig): Promise; +export function lastValueFrom(source: Observable): Promise; + /** * 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. @@ -40,13 +48,15 @@ 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(source: Observable) { - return new Promise((resolve, reject) => { +export function lastValueFrom(source: Observable, config?: LastValueFromConfig) { + const hasConfig = typeof config === 'object'; + return new Promise((resolve, reject) => { let _hasValue = false; let _value: T; source.subscribe({ - next: value => { + next: (value) => { _value = value; _hasValue = true; }, @@ -54,10 +64,12 @@ export function lastValueFrom(source: Observable) { complete: () => { if (_hasValue) { resolve(_value); + } else if (hasConfig) { + resolve(config!.defaultValue); } else { reject(new EmptyError()); } }, }); }); -} \ No newline at end of file +}