import { MonoTypeOperatorFunction } from '../types'; import { identity } from '../util/identity'; import { operate } from '../util/lift'; import { createOperatorSubscriber } from './OperatorSubscriber'; export function distinctUntilChanged(comparator?: (previous: T, current: T) => boolean): MonoTypeOperatorFunction; export function distinctUntilChanged( comparator: (previous: K, current: K) => boolean, keySelector: (value: T) => K ): MonoTypeOperatorFunction; /** * Returns a result {@link Observable} that emits all values pushed by the source observable if they * are distinct in comparison to the last value the result observable emitted. * * When provided without parameters or with the first parameter (`{@link distinctUntilChanged#comparator comparator}`), * it behaves like this: * * 1. It will always emit the first value from the source. * 2. For all subsequent values pushed by the source, they will be compared to the previously emitted values * using the provided `comparator` or an `===` equality check. * 3. If the value pushed by the source is determined to be unequal by this check, that value is emitted and * becomes the new "previously emitted value" internally. * * When the second parameter (`{@link distinctUntilChanged#keySelector keySelector}`) is provided, the behavior * changes: * * 1. It will always emit the first value from the source. * 2. The `keySelector` will be run against all values, including the first value. * 3. For all values after the first, the selected key will be compared against the key selected from * the previously emitted value using the `comparator`. * 4. If the keys are determined to be unequal by this check, the value (not the key), is emitted * and the selected key from that value is saved for future comparisons against other keys. * * ## Examples * * A very basic example with no `{@link distinctUntilChanged#comparator comparator}`. Note that `1` is emitted more than once, * because it's distinct in comparison to the _previously emitted_ value, * not in comparison to _all other emitted values_. * * ```ts * import { of, distinctUntilChanged } from 'rxjs'; * * of(1, 1, 1, 2, 2, 2, 1, 1, 3, 3) * .pipe(distinctUntilChanged()) * .subscribe(console.log); * // Logs: 1, 2, 1, 3 * ``` * * With a `{@link distinctUntilChanged#comparator comparator}`, you can do custom comparisons. Let's say * you only want to emit a value when all of its components have * changed: * * ```ts * import { of, distinctUntilChanged } from 'rxjs'; * * const totallyDifferentBuilds$ = of( * { engineVersion: '1.1.0', transmissionVersion: '1.2.0' }, * { engineVersion: '1.1.0', transmissionVersion: '1.4.0' }, * { engineVersion: '1.3.0', transmissionVersion: '1.4.0' }, * { engineVersion: '1.3.0', transmissionVersion: '1.5.0' }, * { engineVersion: '2.0.0', transmissionVersion: '1.5.0' } * ).pipe( * distinctUntilChanged((prev, curr) => { * return ( * prev.engineVersion === curr.engineVersion || * prev.transmissionVersion === curr.transmissionVersion * ); * }) * ); * * totallyDifferentBuilds$.subscribe(console.log); * * // Logs: * // { engineVersion: '1.1.0', transmissionVersion: '1.2.0' } * // { engineVersion: '1.3.0', transmissionVersion: '1.4.0' } * // { engineVersion: '2.0.0', transmissionVersion: '1.5.0' } * ``` * * You can also provide a custom `{@link distinctUntilChanged#comparator comparator}` to check that emitted * changes are only in one direction. Let's say you only want to get * the next record temperature: * * ```ts * import { of, distinctUntilChanged } from 'rxjs'; * * const temps$ = of(30, 31, 20, 34, 33, 29, 35, 20); * * const recordHighs$ = temps$.pipe( * distinctUntilChanged((prevHigh, temp) => { * // If the current temp is less than * // or the same as the previous record, * // the record hasn't changed. * return temp <= prevHigh; * }) * ); * * recordHighs$.subscribe(console.log); * // Logs: 30, 31, 34, 35 * ``` * * Selecting update events only when the `updatedBy` field shows * the account changed hands. * * ```ts * import { of, distinctUntilChanged } from 'rxjs'; * * // A stream of updates to a given account * const accountUpdates$ = of( * { updatedBy: 'blesh', data: [] }, * { updatedBy: 'blesh', data: [] }, * { updatedBy: 'ncjamieson', data: [] }, * { updatedBy: 'ncjamieson', data: [] }, * { updatedBy: 'blesh', data: [] } * ); * * // We only want the events where it changed hands * const changedHands$ = accountUpdates$.pipe( * distinctUntilChanged(undefined, update => update.updatedBy) * ); * * changedHands$.subscribe(console.log); * // Logs: * // { updatedBy: 'blesh', data: Array[0] } * // { updatedBy: 'ncjamieson', data: Array[0] } * // { updatedBy: 'blesh', data: Array[0] } * ``` * * @see {@link distinct} * @see {@link distinctUntilKeyChanged} * * @param comparator A function used to compare the previous and current keys for * equality. Defaults to a `===` check. * @param keySelector Used to select a key value to be passed to the `comparator`. * * @return A function that returns an Observable that emits items from the * source Observable with distinct values. */ export function distinctUntilChanged( comparator?: (previous: K, current: K) => boolean, keySelector: (value: T) => K = identity as (value: T) => K ): MonoTypeOperatorFunction { // We've been allowing `null` do be passed as the `compare`, so we can't do // a default value for the parameter, because that will only work // for `undefined`. comparator = comparator ?? defaultCompare; return operate((source, subscriber) => { // The previous key, used to compare against keys selected // from new arrivals to determine "distinctiveness". let previousKey: K; // Whether or not this is the first value we've gotten. let first = true; source.subscribe( createOperatorSubscriber(subscriber, (value) => { // We always call the key selector. const currentKey = keySelector(value); // If it's the first value, we always emit it. // Otherwise, we compare this key to the previous key, and // if the comparer returns false, we emit. if (first || !comparator!(previousKey, currentKey)) { // Update our state *before* we emit the value // as emission can be the source of re-entrant code // in functional libraries like this. We only really // need to do this if it's the first value, or if the // key we're tracking in previous needs to change. first = false; previousKey = currentKey; // Emit the value! subscriber.next(value); } }) ); }); } function defaultCompare(a: any, b: any) { return a === b; }