File size: 11,273 Bytes
bc20498 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
import { Observable } from '../Observable';
import { innerFrom } from '../observable/innerFrom';
import { Subject } from '../Subject';
import { ObservableInput, Observer, OperatorFunction, SubjectLike } from '../types';
import { operate } from '../util/lift';
import { createOperatorSubscriber, OperatorSubscriber } from './OperatorSubscriber';
export interface BasicGroupByOptions<K, T> {
element?: undefined;
duration?: (grouped: GroupedObservable<K, T>) => ObservableInput<any>;
connector?: () => SubjectLike<T>;
}
export interface GroupByOptionsWithElement<K, E, T> {
element: (value: T) => E;
duration?: (grouped: GroupedObservable<K, E>) => ObservableInput<any>;
connector?: () => SubjectLike<E>;
}
export function groupBy<T, K>(key: (value: T) => K, options: BasicGroupByOptions<K, T>): OperatorFunction<T, GroupedObservable<K, T>>;
export function groupBy<T, K, E>(
key: (value: T) => K,
options: GroupByOptionsWithElement<K, E, T>
): OperatorFunction<T, GroupedObservable<K, E>>;
export function groupBy<T, K extends T>(
key: (value: T) => value is K
): OperatorFunction<T, GroupedObservable<true, K> | GroupedObservable<false, Exclude<T, K>>>;
export function groupBy<T, K>(key: (value: T) => K): OperatorFunction<T, GroupedObservable<K, T>>;
/**
* @deprecated use the options parameter instead.
*/
export function groupBy<T, K>(
key: (value: T) => K,
element: void,
duration: (grouped: GroupedObservable<K, T>) => Observable<any>
): OperatorFunction<T, GroupedObservable<K, T>>;
/**
* @deprecated use the options parameter instead.
*/
export function groupBy<T, K, R>(
key: (value: T) => K,
element?: (value: T) => R,
duration?: (grouped: GroupedObservable<K, R>) => Observable<any>
): OperatorFunction<T, GroupedObservable<K, R>>;
/**
* Groups the items emitted by an Observable according to a specified criterion,
* and emits these grouped items as `GroupedObservables`, one
* {@link GroupedObservable} per group.
*
* 
*
* When the Observable emits an item, a key is computed for this item with the key function.
*
* If a {@link GroupedObservable} for this key exists, this {@link GroupedObservable} emits. Otherwise, a new
* {@link GroupedObservable} for this key is created and emits.
*
* A {@link GroupedObservable} represents values belonging to the same group represented by a common key. The common
* key is available as the `key` field of a {@link GroupedObservable} instance.
*
* The elements emitted by {@link GroupedObservable}s are by default the items emitted by the Observable, or elements
* returned by the element function.
*
* ## Examples
*
* Group objects by `id` and return as array
*
* ```ts
* import { of, groupBy, mergeMap, reduce } from 'rxjs';
*
* of(
* { id: 1, name: 'JavaScript' },
* { id: 2, name: 'Parcel' },
* { id: 2, name: 'webpack' },
* { id: 1, name: 'TypeScript' },
* { id: 3, name: 'TSLint' }
* ).pipe(
* groupBy(p => p.id),
* mergeMap(group$ => group$.pipe(reduce((acc, cur) => [...acc, cur], [])))
* )
* .subscribe(p => console.log(p));
*
* // displays:
* // [{ id: 1, name: 'JavaScript' }, { id: 1, name: 'TypeScript'}]
* // [{ id: 2, name: 'Parcel' }, { id: 2, name: 'webpack'}]
* // [{ id: 3, name: 'TSLint' }]
* ```
*
* Pivot data on the `id` field
*
* ```ts
* import { of, groupBy, mergeMap, reduce, map } from 'rxjs';
*
* of(
* { id: 1, name: 'JavaScript' },
* { id: 2, name: 'Parcel' },
* { id: 2, name: 'webpack' },
* { id: 1, name: 'TypeScript' },
* { id: 3, name: 'TSLint' }
* ).pipe(
* groupBy(p => p.id, { element: p => p.name }),
* mergeMap(group$ => group$.pipe(reduce((acc, cur) => [...acc, cur], [`${ group$.key }`]))),
* map(arr => ({ id: parseInt(arr[0], 10), values: arr.slice(1) }))
* )
* .subscribe(p => console.log(p));
*
* // displays:
* // { id: 1, values: [ 'JavaScript', 'TypeScript' ] }
* // { id: 2, values: [ 'Parcel', 'webpack' ] }
* // { id: 3, values: [ 'TSLint' ] }
* ```
*
* @param key A function that extracts the key
* for each item.
* @param element A function that extracts the
* return element for each item.
* @param duration
* A function that returns an Observable to determine how long each group should
* exist.
* @param connector Factory function to create an
* intermediate Subject through which grouped elements are emitted.
* @return A function that returns an Observable that emits GroupedObservables,
* each of which corresponds to a unique key value and each of which emits
* those items from the source Observable that share that key value.
*
* @deprecated Use the options parameter instead.
*/
export function groupBy<T, K, R>(
key: (value: T) => K,
element?: (value: T) => R,
duration?: (grouped: GroupedObservable<K, R>) => Observable<any>,
connector?: () => Subject<R>
): OperatorFunction<T, GroupedObservable<K, R>>;
// Impl
export function groupBy<T, K, R>(
keySelector: (value: T) => K,
elementOrOptions?: ((value: any) => any) | void | BasicGroupByOptions<K, T> | GroupByOptionsWithElement<K, R, T>,
duration?: (grouped: GroupedObservable<any, any>) => ObservableInput<any>,
connector?: () => SubjectLike<any>
): OperatorFunction<T, GroupedObservable<K, R>> {
return operate((source, subscriber) => {
let element: ((value: any) => any) | void;
if (!elementOrOptions || typeof elementOrOptions === 'function') {
element = elementOrOptions as ((value: any) => any);
} else {
({ duration, element, connector } = elementOrOptions);
}
// A lookup for the groups that we have so far.
const groups = new Map<K, SubjectLike<any>>();
// Used for notifying all groups and the subscriber in the same way.
const notify = (cb: (group: Observer<any>) => void) => {
groups.forEach(cb);
cb(subscriber);
};
// Used to handle errors from the source, AND errors that occur during the
// next call from the source.
const handleError = (err: any) => notify((consumer) => consumer.error(err));
// The number of actively subscribed groups
let activeGroups = 0;
// Whether or not teardown was attempted on this subscription.
let teardownAttempted = false;
// Capturing a reference to this, because we need a handle to it
// in `createGroupedObservable` below. This is what we use to
// subscribe to our source observable. This sometimes needs to be unsubscribed
// out-of-band with our `subscriber` which is the downstream subscriber, or destination,
// in cases where a user unsubscribes from the main resulting subscription, but
// still has groups from this subscription subscribed and would expect values from it
// Consider: `source.pipe(groupBy(fn), take(2))`.
const groupBySourceSubscriber = new OperatorSubscriber(
subscriber,
(value: T) => {
// Because we have to notify all groups of any errors that occur in here,
// we have to add our own try/catch to ensure that those errors are propagated.
// OperatorSubscriber will only send the error to the main subscriber.
try {
const key = keySelector(value);
let group = groups.get(key);
if (!group) {
// Create our group subject
groups.set(key, (group = connector ? connector() : new Subject<any>()));
// Emit the grouped observable. Note that we can't do a simple `asObservable()` here,
// because the grouped observable has special semantics around reference counting
// to ensure we don't sever our connection to the source prematurely.
const grouped = createGroupedObservable(key, group);
subscriber.next(grouped);
if (duration) {
const durationSubscriber = createOperatorSubscriber(
// Providing the group here ensures that it is disposed of -- via `unsubscribe` --
// when the duration subscription is torn down. That is important, because then
// if someone holds a handle to the grouped observable and tries to subscribe to it
// after the connection to the source has been severed, they will get an
// `ObjectUnsubscribedError` and know they can't possibly get any notifications.
group as any,
() => {
// Our duration notified! We can complete the group.
// The group will be removed from the map in the finalization phase.
group!.complete();
durationSubscriber?.unsubscribe();
},
// Completions are also sent to the group, but just the group.
undefined,
// Errors on the duration subscriber are sent to the group
// but only the group. They are not sent to the main subscription.
undefined,
// Finalization: Remove this group from our map.
() => groups.delete(key)
);
// Start our duration notifier.
groupBySourceSubscriber.add(innerFrom(duration(grouped)).subscribe(durationSubscriber));
}
}
// Send the value to our group.
group.next(element ? element(value) : value);
} catch (err) {
handleError(err);
}
},
// Source completes.
() => notify((consumer) => consumer.complete()),
// Error from the source.
handleError,
// Free up memory.
// When the source subscription is _finally_ torn down, release the subjects and keys
// in our groups Map, they may be quite large and we don't want to keep them around if we
// don't have to.
() => groups.clear(),
() => {
teardownAttempted = true;
// We only kill our subscription to the source if we have
// no active groups. As stated above, consider this scenario:
// source$.pipe(groupBy(fn), take(2)).
return activeGroups === 0;
}
);
// Subscribe to the source
source.subscribe(groupBySourceSubscriber);
/**
* Creates the actual grouped observable returned.
* @param key The key of the group
* @param groupSubject The subject that fuels the group
*/
function createGroupedObservable(key: K, groupSubject: SubjectLike<any>) {
const result: any = new Observable<T>((groupSubscriber) => {
activeGroups++;
const innerSub = groupSubject.subscribe(groupSubscriber);
return () => {
innerSub.unsubscribe();
// We can kill the subscription to our source if we now have no more
// active groups subscribed, and a finalization was already attempted on
// the source.
--activeGroups === 0 && teardownAttempted && groupBySourceSubscriber.unsubscribe();
};
});
result.key = key;
return result;
}
});
}
/**
* An observable of values that is the emitted by the result of a {@link groupBy} operator,
* contains a `key` property for the grouping.
*/
export interface GroupedObservable<K, T> extends Observable<T> {
/**
* The key value for the grouped notifications.
*/
readonly key: K;
}
|