From f5b2474f9a2cf659cdebf19ba49055e5bd2f1c90 Mon Sep 17 00:00:00 2001 From: Derek Burgman Date: Thu, 12 May 2022 17:56:15 -0500 Subject: [PATCH] feat: added IterationQueryChangeWatcher - added IterationQueryChangeWatcher to track changes parallel to a FirestoreItemPageIterationInstance --- .../loader/collection.loader.instance.ts | 10 +- .../store.collection.change.directive.ts | 66 +++++++++++++ .../src/lib/model/store/store.collection.ts | 5 +- .../src/lib/client/firestore/driver.query.ts | 10 +- .../src/lib/common/firestore/driver/query.ts | 5 +- .../lib/common/firestore/query/accumulator.ts | 1 - .../src/lib/common/firestore/query/index.ts | 1 + .../lib/common/firestore/query/iterator.ts | 52 +++++++--- .../lib/common/firestore/query/query.util.ts | 2 +- .../src/lib/common/firestore/query/watcher.ts | 99 +++++++++++++++++++ .../src/lib/common/firestore/types.ts | 4 + .../rxjs/src/lib/iterator/iterator.page.ts | 13 ++- packages/util/src/lib/grouping.ts | 6 +- 13 files changed, 241 insertions(+), 33 deletions(-) create mode 100644 packages/dbx-firebase/src/lib/model/store/store.collection.change.directive.ts create mode 100644 packages/firebase/src/lib/common/firestore/query/watcher.ts diff --git a/packages/dbx-firebase/src/lib/model/loader/collection.loader.instance.ts b/packages/dbx-firebase/src/lib/model/loader/collection.loader.instance.ts index e857b5b01..6f37a4a5a 100644 --- a/packages/dbx-firebase/src/lib/model/loader/collection.loader.instance.ts +++ b/packages/dbx-firebase/src/lib/model/loader/collection.loader.instance.ts @@ -1,6 +1,6 @@ -import { PageListLoadingState, cleanupDestroyable, filterMaybe, useFirst, SubscriptionObject, accumulatorFlattenPageListLoadingState, tapLog } from '@dereekb/rxjs'; -import { BehaviorSubject, combineLatest, map, shareReplay, distinctUntilChanged, Subject, throttleTime, switchMap, Observable, tap, startWith, NEVER, of, filter } from 'rxjs'; -import { FirebaseQueryItemAccumulator, firebaseQueryItemAccumulator, FirestoreCollection, FirestoreDocument, FirestoreItemPageIterationInstance, FirestoreItemPageIteratorFilter, FirestoreQueryConstraint } from '@dereekb/firebase'; +import { PageListLoadingState, cleanupDestroyable, filterMaybe, useFirst, SubscriptionObject, accumulatorFlattenPageListLoadingState } from '@dereekb/rxjs'; +import { BehaviorSubject, combineLatest, map, shareReplay, distinctUntilChanged, Subject, throttleTime, switchMap, Observable, tap, startWith, NEVER } from 'rxjs'; +import { FirebaseQueryItemAccumulator, firebaseQueryItemAccumulator, FirestoreCollection, FirestoreDocument, FirestoreItemPageIterationInstance, FirestoreItemPageIteratorFilter, FirestoreQueryConstraint, IterationQueryChangeWatcher, iterationQueryChangeWatcher } from '@dereekb/firebase'; import { ArrayOrValue, Destroyable, Initialized, Maybe } from '@dereekb/util'; import { DbxFirebaseCollectionLoader } from './collection.loader'; @@ -59,6 +59,10 @@ export class DbxFirebaseCollectionLoaderInstance> = this.firestoreIteration$.pipe( + map(instance => iterationQueryChangeWatcher({ instance })) + ); + readonly accumulator$: Observable> = this.firestoreIteration$.pipe( map(x => firebaseQueryItemAccumulator(x)), cleanupDestroyable(), diff --git a/packages/dbx-firebase/src/lib/model/store/store.collection.change.directive.ts b/packages/dbx-firebase/src/lib/model/store/store.collection.change.directive.ts new file mode 100644 index 000000000..01e878b8b --- /dev/null +++ b/packages/dbx-firebase/src/lib/model/store/store.collection.change.directive.ts @@ -0,0 +1,66 @@ +import { Directive, forwardRef, Input, Provider, Type } from '@angular/core'; +import { FirestoreDocument, FirestoreQueryConstraint } from "@dereekb/firebase"; +import { Maybe, ArrayOrValue } from '@dereekb/util'; +import { DbxFirebaseCollectionStore } from "./store.collection"; + +/** + * Abstract directive that contains a DbxFirebaseCollectionStore and provides an interface for communicating with other directives. + */ +@Directive() +export abstract class DbxFirebaseCollectionStoreDirective = FirestoreDocument, S extends DbxFirebaseCollectionStore = DbxFirebaseCollectionStore> { + + constructor(readonly store: S) { } + + readonly pageLoadingState$ = this.store.pageLoadingState$; + + // MARK: Inputs + @Input() + set maxPages(maxPages: Maybe) { + this.store.setMaxPages(maxPages); + } + + @Input() + set itemsPerPage(itemsPerPage: Maybe) { + this.store.setItemsPerPage(itemsPerPage); + } + + @Input() + set constraints(constraints: Maybe>) { + this.store.setConstraints(constraints); + } + + next() { + this.store.next(); + } + + restart() { + this.store.restart(); + } + + setConstraints(constraints: Maybe>) { + this.store.setConstraints(constraints); + } + +} + +/** + * Configures providers for a DbxFirebaseCollectionStoreDirective. + * + * Can optionally also provide the actual store type to include in the providers array so it is instantiated by Angular. + * + * @param sourceType + */ +export function provideDbxFirebaseCollectionStoreDirective>(sourceType: Type): Provider[]; +export function provideDbxFirebaseCollectionStoreDirective, C extends DbxFirebaseCollectionStoreDirective = DbxFirebaseCollectionStoreDirective>(sourceType: Type, storeType: Type): Provider[]; +export function provideDbxFirebaseCollectionStoreDirective, C extends DbxFirebaseCollectionStoreDirective = DbxFirebaseCollectionStoreDirective>(sourceType: Type, storeType?: Type): Provider[] { + const providers: Provider[] = [{ + provide: DbxFirebaseCollectionStoreDirective, + useExisting: forwardRef(() => sourceType) + }]; + + if (storeType) { + providers.push(storeType); + } + + return providers; +} diff --git a/packages/dbx-firebase/src/lib/model/store/store.collection.ts b/packages/dbx-firebase/src/lib/model/store/store.collection.ts index 13931f454..03352f04b 100644 --- a/packages/dbx-firebase/src/lib/model/store/store.collection.ts +++ b/packages/dbx-firebase/src/lib/model/store/store.collection.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable, shareReplay, distinctUntilChanged, Subscription, exhaustMap, first, map, switchMap, tap } from 'rxjs'; -import { FirebaseQueryItemAccumulator, FirestoreCollection, FirestoreDocument, FirestoreItemPageIterationInstance, FirestoreQueryConstraint } from '@dereekb/firebase'; +import { FirebaseQueryItemAccumulator, FirestoreCollection, FirestoreDocument, FirestoreItemPageIterationInstance, FirestoreQueryConstraint, IterationQueryChangeWatcher } from '@dereekb/firebase'; import { ObservableOrValue, cleanupDestroyable, PageListLoadingState, filterMaybe } from '@dereekb/rxjs'; import { ArrayOrValue, Maybe } from '@dereekb/util'; import { LockSetComponentStore } from '@dereekb/dbx-core'; @@ -15,7 +15,7 @@ export interface DbxFirebaseCollectionStore = setConstraints(observableOrValue: ObservableOrValue>>>): Subscription; next(observableOrValue: ObservableOrValue): void; restart(observableOrValue: ObservableOrValue): void; - + readonly setFirestoreCollection: (() => void) | ((observableOrValue: ObservableOrValue>>) => Subscription); } @@ -102,6 +102,7 @@ export class AbstractDbxFirebaseCollectionStore> = this.loader$.pipe(switchMap(x => x.firestoreIteration$)); + readonly queryChangeWatcher$: Observable> = this.loader$.pipe(switchMap(x => x.queryChangeWatcher$)); readonly accumulator$: Observable> = this.loader$.pipe(switchMap(x => x.accumulator$)); readonly pageLoadingState$: Observable> = this.loader$.pipe(switchMap(x => x.pageLoadingState$)); diff --git a/packages/firebase/src/lib/client/firestore/driver.query.ts b/packages/firebase/src/lib/client/firestore/driver.query.ts index d2eed26ad..d14a33bc7 100644 --- a/packages/firebase/src/lib/client/firestore/driver.query.ts +++ b/packages/firebase/src/lib/client/firestore/driver.query.ts @@ -1,10 +1,10 @@ import { Observable } from 'rxjs'; -import { ArrayOrValue } from '@dereekb/util'; -import { DocumentSnapshot, getDocs, limit, query, QueryConstraint, startAt, Query as FirebaseFirestoreQuery, where, startAfter, orderBy, limitToLast, endBefore, endAt, onSnapshot, Transaction as FirebaseFirestoreTransaction } from "firebase/firestore"; +import { ArrayOrValue, Maybe } from '@dereekb/util'; +import { DocumentSnapshot, getDocs, limit, query, QueryConstraint, startAt, Query as FirebaseFirestoreQuery, where, startAfter, orderBy, limitToLast, endBefore, endAt, onSnapshot } from "firebase/firestore"; import { FIRESTORE_LIMIT_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AFTER_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_WHERE_QUERY_CONSTRAINT_TYPE, FIRESTORE_LIMIT_TO_LAST_QUERY_CONSTRAINT_TYPE, FIRESTORE_ORDER_BY_QUERY_CONSTRAINT_TYPE, FullFirestoreQueryConstraintHandlersMapping, FIRESTORE_OFFSET_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_BEFORE_QUERY_CONSTRAINT_TYPE } from './../../common/firestore/query/constraint'; import { makeFirestoreQueryConstraintFunctionsDriver } from '../../common/firestore/driver/query.handler'; import { FirestoreQueryConstraintFunctionsDriver, FirestoreQueryDriver } from "../../common/firestore/driver/query"; -import { Query, QuerySnapshot, Transaction } from "../../common/firestore/types"; +import { Query, QuerySnapshot, SnapshotListenOptions, Transaction } from "../../common/firestore/types"; import { streamFromOnSnapshot } from '../../common/firestore/query/query.util'; export interface FirebaseFirestoreQueryBuilder { @@ -50,8 +50,8 @@ export function firebaseFirestoreQueryDriver(): FirestoreQueryDriver { return getDocs(query as FirebaseFirestoreQuery); }, - streamDocs(query: Query): Observable> { - return streamFromOnSnapshot((obs) => onSnapshot((query as FirebaseFirestoreQuery), obs)); + streamDocs(query: Query, options?: Maybe): Observable> { + return streamFromOnSnapshot((obs) => (options) ? onSnapshot((query as FirebaseFirestoreQuery), options, obs) : onSnapshot((query as FirebaseFirestoreQuery), obs)); } }; } diff --git a/packages/firebase/src/lib/common/firestore/driver/query.ts b/packages/firebase/src/lib/common/firestore/driver/query.ts index a3158b71d..135796a7c 100644 --- a/packages/firebase/src/lib/common/firestore/driver/query.ts +++ b/packages/firebase/src/lib/common/firestore/driver/query.ts @@ -1,7 +1,8 @@ -import { Transaction } from './../types'; +import { SnapshotListenOptions, Transaction } from './../types'; import { Observable } from 'rxjs'; import { Query, QuerySnapshot } from "../types"; import { FirestoreQueryConstraint } from "../query/constraint"; +import { Maybe } from '@dereekb/util'; export type FirestoreQueryDriverQueryFunction = (query: Query, ...queryConstraints: FirestoreQueryConstraint[]) => Query; @@ -23,7 +24,7 @@ export interface FirestoreQueryDriver extends FirestoreQueryConstraintFunctionsD * @param transaction */ getDocs(query: Query, transaction?: Transaction): Promise>; - streamDocs(query: Query): Observable>; + streamDocs(query: Query, options?: Maybe): Observable>; } /** diff --git a/packages/firebase/src/lib/common/firestore/query/accumulator.ts b/packages/firebase/src/lib/common/firestore/query/accumulator.ts index 6190955c7..81b303174 100644 --- a/packages/firebase/src/lib/common/firestore/query/accumulator.ts +++ b/packages/firebase/src/lib/common/firestore/query/accumulator.ts @@ -30,7 +30,6 @@ export function firebaseQuerySnapshotAccumulator(iteration: FirestoreItemP export function firebaseQueryItemAccumulator(iteration: FirestoreItemPageIterationInstance): FirebaseQueryItemAccumulator; export function firebaseQueryItemAccumulator(iteration: FirestoreItemPageIterationInstance, mapItem?: MapFunction, U>): MappedFirebaseQuerySnapshotAccumulator; export function firebaseQueryItemAccumulator(iteration: FirestoreItemPageIterationInstance, mapItem?: MapFunction, U>): MappedFirebaseQuerySnapshotAccumulator { - mapItem = mapItem ?? (((x: DocumentDataWithId) => x) as unknown as MapFunction, U>); const mapFn: ItemAccumulatorMapFunction> = (x: QueryDocumentSnapshotArray) => { diff --git a/packages/firebase/src/lib/common/firestore/query/index.ts b/packages/firebase/src/lib/common/firestore/query/index.ts index 10a26a958..46662c61d 100644 --- a/packages/firebase/src/lib/common/firestore/query/index.ts +++ b/packages/firebase/src/lib/common/firestore/query/index.ts @@ -3,3 +3,4 @@ export * from './constraint'; export * from './iterator'; export * from './query'; export * from './query.util'; +export * from './watcher'; diff --git a/packages/firebase/src/lib/common/firestore/query/iterator.ts b/packages/firebase/src/lib/common/firestore/query/iterator.ts index 012b510ef..bae403701 100644 --- a/packages/firebase/src/lib/common/firestore/query/iterator.ts +++ b/packages/firebase/src/lib/common/firestore/query/iterator.ts @@ -1,7 +1,7 @@ import { PageLoadingState, ItemPageIterator, ItemPageIterationInstance, ItemPageIterationConfig, ItemPageIteratorDelegate, ItemPageIteratorRequest, ItemPageIteratorResult, MappedPageItemIterationInstance, ItemPageLimit } from '@dereekb/rxjs'; -import { QueryDocumentSnapshotArray, QuerySnapshot } from "../types"; +import { QueryDocumentSnapshotArray, QuerySnapshot, SnapshotListenOptions } from "../types"; import { asArray, Maybe, lastValue, mergeIntoArray, ArrayOrValue } from '@dereekb/util'; -import { from, Observable, of, exhaustMap } from "rxjs"; +import { from, Observable, of, exhaustMap, map } from "rxjs"; import { CollectionReferenceRef } from '../reference'; import { FirestoreQueryDriverRef } from '../driver/query'; import { FIRESTORE_LIMIT_QUERY_CONSTRAINT_TYPE, FirestoreQueryConstraint, limit, startAfter } from './constraint'; @@ -24,17 +24,33 @@ export interface FirestoreItemPageIterationBaseConfig extends CollectionRefer export interface FirestoreItemPageIterationConfig extends FirestoreItemPageIterationBaseConfig, ItemPageIterationConfig { } export interface FirestoreItemPageQueryResult { + /** + * Time the result was read at. + */ + readonly time: Date; /** * The relevant docs for this page result. This value will omit the cursor. */ - docs: QueryDocumentSnapshotArray; + readonly docs: QueryDocumentSnapshotArray; /** * The raw snapshot returned from the query. */ - snapshot: QuerySnapshot; + readonly snapshot: QuerySnapshot; + /** + * Reloads these results as a snapshot. + */ + reload(): Promise>; + /** + * Streams these results. + */ + stream(options?: FirestoreItemPageQueryResultStreamOptions): Observable>; +} + +export interface FirestoreItemPageQueryResultStreamOptions { + options?: Maybe } -export type FirestoreItemPageIteratorDelegate = ItemPageIteratorDelegate, FirestoreItemPageIteratorFilter, FirestoreItemPageIterationConfig>; +export type FirestoreItemPageIteratorDelegate = ItemPageIteratorDelegate, FirestoreItemPageIteratorFilter, FirestoreItemPageIterationConfig> export type InternalFirestoreItemPageIterationInstance = ItemPageIterationInstance, FirestoreItemPageIteratorFilter, FirestoreItemPageIterationConfig>; export function filterDisallowedFirestoreItemPageIteratorInputContraints(constraints: FirestoreQueryConstraint[]): FirestoreQueryConstraint[] { @@ -72,22 +88,28 @@ export function makeFirestoreItemPageIteratorDelegate(): FirestoreItemPageIte } // Add Limit - const limitCount = filter?.limit ?? itemsPerPage + ((startAfterFilter) ? 1 : 0); - constraints.push(limit(limitCount)); // Add 1 for cursor, since results will start at our cursor. + const limitCount = filterLimit ?? itemsPerPage + ((startAfterFilter) ? 1 : 0); // todo: may not be needed. + const limitConstraint = limit(limitCount); + const constraintsWithLimit = [...constraints, limitConstraint]; - const batchQuery = driver.query(collection, ...constraints); + // make query + const batchQuery = driver.query(collection, ...constraintsWithLimit); const resultPromise: Promise>> = driver.getDocs(batchQuery).then((snapshot) => { - let docs = snapshot.docs; - - // Remove the cursor document from the results. - if (cursorDocument && docs[0].id === cursorDocument.id) { - docs = docs.slice(1); - } + const time = new Date(); + const docs = snapshot.docs; const result: ItemPageIteratorResult> = { value: { + time, docs, - snapshot + snapshot, + reload() { + return driver.getDocs(batchQuery); + }, + stream(options?: FirestoreItemPageQueryResultStreamOptions) { + // todo: consider allowing limit to be changed here to stream a subset. This will be useful for detecting collection changes. + return driver.streamDocs(batchQuery, options?.options); + } }, end: snapshot.empty }; diff --git a/packages/firebase/src/lib/common/firestore/query/query.util.ts b/packages/firebase/src/lib/common/firestore/query/query.util.ts index f41dd9a3c..35827a20b 100644 --- a/packages/firebase/src/lib/common/firestore/query/query.util.ts +++ b/packages/firebase/src/lib/common/firestore/query/query.util.ts @@ -1,4 +1,4 @@ -import { DocumentReference, QueryDocumentSnapshot, QuerySnapshot } from './../types'; +import { DocumentReference, QuerySnapshot } from './../types'; import { Observable } from "rxjs"; // MARK: OnSnapshot diff --git a/packages/firebase/src/lib/common/firestore/query/watcher.ts b/packages/firebase/src/lib/common/firestore/query/watcher.ts new file mode 100644 index 000000000..198db94f0 --- /dev/null +++ b/packages/firebase/src/lib/common/firestore/query/watcher.ts @@ -0,0 +1,99 @@ +import { groupValues } from '@dereekb/util'; +import { timeHasExpired } from '@dereekb/date'; +import { filter, map, Observable, skip, switchMap } from 'rxjs'; +import { DocumentChange, QuerySnapshot } from '../types'; +import { FirestoreItemPageIterationInstance } from "./iterator"; + +export const DEFAULT_QUERY_CHANGE_WATCHER_DELAY = 1000 * 8; + +export interface IterationQueryChangeWatcherConfig { + readonly instance: FirestoreItemPageIterationInstance; + readonly delay?: number; +} + +export interface IterationQueryChangeWatcher { + + /** + * Streams all subsequent query changes. + */ + readonly stream$: Observable>; + + /** + * Event stream + */ + readonly event$: Observable>; + + /** + * Change + */ + readonly change$: Observable; + +} + +export interface IterationQueryChangeWatcherEvent extends IterationQueryChangeWatcherChangeGroup { + readonly changes: DocumentChange[]; + readonly type: IterationQueryChangeWatcherChangeType; +} + +export interface IterationQueryChangeWatcherChangeGroup { + readonly added: DocumentChange[]; + readonly removed: DocumentChange[]; + readonly modified: DocumentChange[]; +} + +export type IterationQueryChangeWatcherChangeType = 'addedAndRemoved' | 'added' | 'removed' | 'modified' | 'none'; + +export function iterationQueryChangeWatcher(config: IterationQueryChangeWatcherConfig): IterationQueryChangeWatcher { + const { instance, delay: timeUntilActive = DEFAULT_QUERY_CHANGE_WATCHER_DELAY } = config; + const stream$ = instance.snapshotIteration.firstSuccessfulPageResults$.pipe(switchMap((first) => { + const { time, stream } = first.value!.value!; + + // todo: capture the change type. + + return stream().pipe( + skip(1), // skip the first value. + filter(() => timeHasExpired(time, timeUntilActive)) + ); + })); + + const event$ = stream$.pipe(map(event => { + const changes = event.docChanges(); + + const results: IterationQueryChangeWatcherChangeGroup = groupValues(changes, (x) => x.type); + (results as any).changes = changes; + (results as any).added = results.added ?? []; + (results as any).removed = results.removed ?? []; + (results as any).modified = results.modified ?? []; + (results as any).type = iterationQueryChangeWatcherChangeTypeForGroup(results); + + return results as IterationQueryChangeWatcherEvent; + })); + + const change$ = event$.pipe(map(x => x.type)); + + return { + stream$, + change$, + event$ + }; +} + +export function iterationQueryChangeWatcherChangeTypeForGroup(group: IterationQueryChangeWatcherChangeGroup): IterationQueryChangeWatcherChangeType { + const hasAdded = group.added.length > 0; + const hasRemoved = group.removed.length > 0; + let type: IterationQueryChangeWatcherChangeType; + + if (hasAdded && hasRemoved) { + type = 'addedAndRemoved'; + } else if (hasAdded) { + type = 'added'; + } else if (hasRemoved) { + type = 'removed'; + } else if (group.modified.length > 0) { + type = 'modified'; + } else { + type = 'none'; + } + + return type; +} diff --git a/packages/firebase/src/lib/common/firestore/types.ts b/packages/firebase/src/lib/common/firestore/types.ts index 31abf4359..3b5834662 100644 --- a/packages/firebase/src/lib/common/firestore/types.ts +++ b/packages/firebase/src/lib/common/firestore/types.ts @@ -100,6 +100,10 @@ export interface SnapshotOptions { readonly serverTimestamps?: 'estimate' | 'previous' | 'none'; } +export interface SnapshotListenOptions { + readonly includeMetadataChanges?: boolean; +} + // MARK: Converter /** * Mirrors the types/methods of FirestoreDataConverter. diff --git a/packages/rxjs/src/lib/iterator/iterator.page.ts b/packages/rxjs/src/lib/iterator/iterator.page.ts index 4f4fff2c5..d9f5376e0 100644 --- a/packages/rxjs/src/lib/iterator/iterator.page.ts +++ b/packages/rxjs/src/lib/iterator/iterator.page.ts @@ -125,6 +125,7 @@ export interface ItemPageIterationInstanceState { n: number; current: Maybe>>; latestFinished: Maybe>>; + firstSuccessful: Maybe>>; lastSuccessful: Maybe>>; } @@ -198,6 +199,7 @@ export class ItemPageIterationInstance>> = this._lastFinishedPageResultState$.pipe(map(x => x?.value)); private readonly _lastFinishedPageResultItem$: Observable> = this._lastFinishedPageResult$.pipe(map(x => x?.value)); + /** + * The first page results that has finished loading without an error. + */ + readonly firstSuccessfulPageResults$: Observable>> = this.state$.pipe( + map(x => x.firstSuccessful), + filterMaybe(), + shareReplay(1) + ); + /** * The latest page results that has finished loading without an error. */ diff --git a/packages/util/src/lib/grouping.ts b/packages/util/src/lib/grouping.ts index 1828d6f3e..b8636519a 100644 --- a/packages/util/src/lib/grouping.ts +++ b/packages/util/src/lib/grouping.ts @@ -12,8 +12,8 @@ export interface GroupingResult { [key: string]: T[]; } -export type KeyedGroupingResult = { - [k in K]: T[]; +export type KeyedGroupingResult = { + [K in keyof O]: T[]; } export interface PairsGroupingResult { @@ -190,7 +190,7 @@ export function separateValues(values: T[], checkInclusion: (x: T) => boolean * @param values * @param groupKeyFn */ -export function groupValues(values: T[], groupKeyFn: ReadKeyFunction): KeyedGroupingResult; +export function groupValues(values: T[], groupKeyFn: ReadKeyFunction): KeyedGroupingResult; export function groupValues(values: T[], groupKeyFn: ReadKeyFunction): GroupingResult; export function groupValues(values: T[], groupKeyFn: ReadKeyFunction): GroupingResult { const map = makeValuesGroupMap(values, groupKeyFn);