diff --git a/src/SUSPENSE.ts b/src/SUSPENSE.ts index 70fbda4..f9ad1af 100644 --- a/src/SUSPENSE.ts +++ b/src/SUSPENSE.ts @@ -1 +1,6 @@ +/** + * This is a special symbol that can be emitted from our observables to let the + * react hook know that there is a value on its way, and that we want to + * leverage React Suspense API while we are waiting for that value. + */ export const SUSPENSE = Symbol("SUSPENSE") diff --git a/src/connectFactoryObservable.ts b/src/connectFactoryObservable.ts index fbcd188..2d3000b 100644 --- a/src/connectFactoryObservable.ts +++ b/src/connectFactoryObservable.ts @@ -6,19 +6,38 @@ import { BehaviorObservable } from "./BehaviorObservable" import { SUSPENSE } from "./SUSPENSE" import { useObservable } from "./useObservable" +/** + * Accepts: A factory function that returns an Observable. + * + * Returns [1, 2] + * 1. A React Hook function with the same parameters as the factory function. + * This hook will yield the latest update from the observable returned from + * the factory function. + * 2. A shared replayable version of the observable generated by the factory + * function that can be used for composing other streams that depend on it. + * This observable is disposed when its refCount goes down to zero. + * + * @param getObservable Factory of observables. The arguments of this function + * will be the ones used in the hook. + * @param options ConnectorOptions: + * - unsubscribeGraceTime (= 200): Amount of time in ms that the shared + * observable should wait before unsubscribing from the source observable + * when there are no new subscribers. + * - compare (= Object.is): Equality function. + */ export function connectFactoryObservable< A extends (number | string | boolean | null)[], O >( getObservable: (...args: A) => Observable, - _options?: ConnectorOptions, + options?: ConnectorOptions, ): [ (...args: A) => Exclude, (...args: A) => Observable, ] { - const options = { + const _options = { ...defaultConnectorOptions, - ..._options, + ...options, } const cache = new Map< @@ -36,13 +55,13 @@ export function connectFactoryObservable< return cachedVal } - const sharedObservable$ = distinctShareReplay(options.compare, () => { + const sharedObservable$ = distinctShareReplay(_options.compare, () => { cache.delete(key) })(concat(getObservable(...input), NEVER)) const reactObservable$ = reactEnhancer( sharedObservable$, - options.unsubscribeGraceTime, + _options.unsubscribeGraceTime, ) const result: [BehaviorObservable, BehaviorObservable] = [ sharedObservable$, diff --git a/src/connectObservable.ts b/src/connectObservable.ts index 777b5af..4da520c 100644 --- a/src/connectObservable.ts +++ b/src/connectObservable.ts @@ -4,21 +4,36 @@ import reactEnhancer from "./operators/react-enhancer" import { useObservable } from "./useObservable" import { ConnectorOptions, defaultConnectorOptions } from "./options" +/** + * Returns a hook that provides the latest update of the accepted observable, + * and the underlying enhanced observable, which shares the subscription to all + * of its subscribers, and always emits the latest value when subscribing to it. + * + * The shared subscription is closed as soon as there are no subscribers to that + * observable. + * + * @param observable Source observable to be used by the hook. + * @param options ConnectorOptions: + * - unsubscribeGraceTime (= 200): Amount of time in ms that the shared + * observable should wait before unsubscribing from the source observable + * when there are no new subscribers. + * - compare (= Object.is): Equality function. + */ export function connectObservable( observable: Observable, - _options?: ConnectorOptions, + options?: ConnectorOptions, ) { - const options = { + const _options = { ...defaultConnectorOptions, - ..._options, + ...options, } - const sharedObservable$ = distinctShareReplay(options.compare)( + const sharedObservable$ = distinctShareReplay(_options.compare)( concat(observable, NEVER), ) const reactObservable$ = reactEnhancer( sharedObservable$, - options.unsubscribeGraceTime, + _options.unsubscribeGraceTime, ) const useStaticObservable = () => useObservable(reactObservable$) diff --git a/src/createInput.ts b/src/createInput.ts index 415edc3..3839bb9 100644 --- a/src/createInput.ts +++ b/src/createInput.ts @@ -2,7 +2,19 @@ import { Subject, Observable, ReplaySubject } from "rxjs" import { distinctShareReplay } from "./operators/distinct-share-replay" interface CreateInput { + /** + * Creates a pool of void observables identified by strings, and returns: + * - A function that returns the observable by key + * - A function that makes the observable emit by key + */ (): [(key: string) => Observable, (key: string) => void] + /** + * Creates a pool of observables identified by strings, and returns: + * - A function that returns the observable by key + * - A function that updates the observable value by key + * + * @param defaultValue Default value. + */ (defaultValue?: T): [ (key: string) => Observable, (key: string, update: T | ((prev: T) => T)) => void, @@ -39,4 +51,9 @@ const createInput_ = (defaultValue: T = empty) => { return [getSource, onChange] as const } +/** + * Creates a pool of observables identified by strings, and returns: + * - A function that returns the observable by key + * - A function that updates the observable value by key + */ export const createInput = createInput_ as CreateInput diff --git a/src/index.tsx b/src/index.tsx index ea07a3b..3c56070 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,3 @@ -import { Observable } from "rxjs" import { distinctShareReplay as internalDistinctShareReplay } from "./operators/distinct-share-replay" // support for React Suspense @@ -10,9 +9,27 @@ export { switchMapSuspended } from "./operators/switchMapSuspended" // core export { connectObservable } from "./connectObservable" export { connectFactoryObservable } from "./connectFactoryObservable" -export const distinctShareReplay = internalDistinctShareReplay as ( - compareFn?: (a: T, b: T) => boolean, -) => (source$: Observable) => Observable + +/** + * A RxJS pipeable operator which performs a custom shareReplay that can be + * useful when working with these bindings. It's roughly the equivalent of: + * + * ```ts + * source$.pipe( + * distinctUntilChanged(compare), + * multicast(() => new ReplaySubject(1)), + * refCount(), + * ) + * ``` + * + * @param compareFn Equality function. + * + * @remarks The enhanced observables returned from connectObservable and + * connectFactoryObservable have been enhanced with this operator. + */ +export function distinctShareReplay(compareFn?: (a: T, b: T) => boolean) { + return internalDistinctShareReplay(compareFn) +} // utils export { createInput } from "./createInput" diff --git a/src/operators/distinct-share-replay.ts b/src/operators/distinct-share-replay.ts index e931915..b80330d 100644 --- a/src/operators/distinct-share-replay.ts +++ b/src/operators/distinct-share-replay.ts @@ -5,6 +5,7 @@ import { BehaviorObservable } from "../BehaviorObservable" function defaultTeardown() {} export const EMPTY_VALUE: any = {} + export const distinctShareReplay = ( compareFn: (a: T, b: T) => boolean = Object.is, teardown = defaultTeardown, diff --git a/src/operators/suspend.ts b/src/operators/suspend.ts index ddb7faf..9841af0 100644 --- a/src/operators/suspend.ts +++ b/src/operators/suspend.ts @@ -2,5 +2,10 @@ import { ObservableInput, from } from "rxjs" import { startWith } from "rxjs/operators" import { SUSPENSE } from "../SUSPENSE" +/** + * A RxJS creation operator that prepends a SUSPENSE on the source observable. + * + * @param source$ Source observable + */ export const suspend = (source$: ObservableInput) => from(source$).pipe(startWith(SUSPENSE)) diff --git a/src/operators/suspended.ts b/src/operators/suspended.ts index fd41499..80cecc6 100644 --- a/src/operators/suspended.ts +++ b/src/operators/suspended.ts @@ -1,3 +1,6 @@ import { suspend } from "./suspend" +/** + * A RxJS pipeable operator that prepends a SUSPENSE on the source observable. + */ export const suspended = () => suspend diff --git a/src/operators/switchMapSuspended.ts b/src/operators/switchMapSuspended.ts index f8147c6..fdaab6d 100644 --- a/src/operators/switchMapSuspended.ts +++ b/src/operators/switchMapSuspended.ts @@ -2,6 +2,12 @@ import { ObservableInput, Observable } from "rxjs" import { switchMap } from "rxjs/operators" import { suspend } from "./suspend" +/** + * Same behaviour as rxjs' `switchMap`, but prepending every new event with + * SUSPENSE. + * + * @param fn Projection function + */ export const switchMapSuspended = ( fn: (input: Input) => ObservableInput, ) => (src$: Observable) => src$.pipe(switchMap(x => suspend(fn(x))))