UNPKG

@typed/fp

Version:

Data Structures and Resources for fp-ts

625 lines (560 loc) 14.2 kB
/** * `KV` is an abstraction for managing state-based applications using [Env](./Env.ts.md). It exposes an extensible * get/set/delete API for managing keys to values. Every `KV` is connected to an `Env` that will * provide the default value lazily when first asked for or after being deleted previously. * * The provided implementation will also send events containing all of the creations/updates/deletes * occurring in real-time. * @since 0.11.0 */ import * as B from 'fp-ts/boolean' import { Eq, EqStrict } from 'fp-ts/Eq' import { pipe } from 'fp-ts/function' import * as RM from 'fp-ts/ReadonlyMap' import { not } from 'fp-ts/Refinement' import { fst, snd } from 'fp-ts/Tuple2' import * as A from './Adapter' import * as D from './Disposable' import * as E from './Env' import * as EE from './EnvEither' import { alwaysEqualsEq, deepEqualsEq } from './Eq' import * as O from './Option' import * as RS from './ReaderStream' import * as R from './Resume' /** * @since 0.11.0 * @category Model */ export interface KV<K, E, A> extends Eq<A> { readonly key: K readonly initial: E.Env<E, A> } /** * @since 0.11.0 * @category Model */ export interface Of<K, A> extends KV<K, unknown, A> {} /** * @since 0.11.0 * @category Type-level */ export type KeyOf<A> = [A] extends [KV<infer R, any, any>] ? R : never /** * @since 0.11.0 * @category Type-level */ export type EnvOf<A> = [A] extends [KV<any, infer R, any>] ? R : never /** * @since 0.11.0 * @category Type-level */ export type ValueOf<A> = [A] extends [KV<any, any, infer R>] ? R : never /** * @since 0.11.0 * @category Options */ export type Options<K, A> = { readonly key?: K } & Partial<Eq<A>> /** * Note that by default an incrementing index is utilized to generate a key if one is not * provided. In other words, by default, this is not referentially transparent for * your own convenience * * @since 0.11.0 * @category Constructor */ export function make<E, A, K = symbol>( initial: E.Env<E, A>, options: Options<K, A> = {}, ): KV<K, E, A> { const { equals = deepEqualsEq.equals, key = Symbol() as unknown as K } = options return { key, initial, equals, } } /** * @since 0.11.0 * @category Combinator */ export const get = <K, E, A>(kv: KV<K, E, A>) => E.asksE((e: Get) => e.getKV(kv)) /** * @since 0.12.0 * @category Environment */ export interface Get { readonly getKV: <K, E, A>(kv: KV<K, E, A>) => E.Env<E, A> } /** * @since 0.11.0 * @category Combinator */ export const has = <K, E, A>(kv: KV<K, E, A>) => E.asksE((e: Has) => e.hasKV(kv)) /** * @since 0.12.0 * @category Environment */ export interface Has { readonly hasKV: <K, E, A>(kv: KV<K, E, A>) => E.Of<boolean> } /** * @since 0.11.0 * @category Combinator */ export const set = <K, E, A>(kv: KV<K, E, A>) => (value: A) => E.asksE((e: Set) => e.setKV(kv, value)) /** * @since 0.12.0 * @category Environment */ export interface Set { readonly setKV: <K, E, A>(kv: KV<K, E, A>, value: A) => E.Env<E, A> } /** * @since 0.11.0 * @category Combinator */ export const update = <K, E1, A>(kv: KV<K, E1, A>) => <E2>(f: (value: A) => E.Env<E2, A>) => pipe(kv, get, E.chainW(f), E.chainW(set(kv))) /** * @since 0.11.0 * @category Combinator */ export const remove = <K, E, A>(kv: KV<K, E, A>) => E.asksE((e: Remove) => e.removeKV(kv)) /** * @since 0.12.0 * @category Environment */ export interface Remove { readonly removeKV: <K, E, A>(kv: KV<K, E, A>) => E.Env<E, O.Option<A>> } /** * @since 0.12.0 * @category Environment */ export interface Events { readonly kvEvents: Adapter } /** * @since 0.12.0 * @category Combinator */ export const getAdapter = E.asks((e: Events) => e.kvEvents) /** * @since 0.11.0 * @category Combinator */ export const getSendEvent = pipe(getAdapter, E.map(fst)) /** * @since 0.11.0 * @category Combinator */ export const sendEvent = <K, A>(event: Event<K, A>) => pipe(getSendEvent, E.apW(E.of(event))) /** * @since 0.12.0 * @category Combinator */ export const getKVEvents: RS.ReaderStream<Events, Event<any, any>> = (e: Events) => snd(e.kvEvents) /** * @since 0.11.0 * @category Combinator */ export const listenTo = <K, E, A>(kv: KV<K, E, A>): RS.ReaderStream<Events, Event<K, A>> => pipe( getKVEvents, RS.filter((x) => x.key === kv.key), ) /** * @since 0.11.0 * @category Combinator */ export const listenToValues = <K, E, A>( kv: KV<K, E, A>, ): RS.ReaderStream<E & Events, O.Option<A>> => pipe( kv, listenTo, RS.map((e) => (isRemoved(e) ? O.none : O.some(e.value))), RS.startWith(O.none), ) /** * @since 0.12.0 * @category Environment */ export interface ParentEnv { readonly parentKVEnv: O.Option<Env> } /** * @since 0.11.0 * @category Combinator */ export const getParentEnv = E.asks((e: ParentEnv) => e.parentKVEnv) /** * Traverse up the tree of KVEnv and parent KVEnv to find the closest KVEnv that * has reference for a given KV. This is useful for providing a React-like Context * API atop of KV. * @since 0.11.0 * @category Combinator */ export const findKVProvider = <K, E, A>(ref: KV<K, E, A>): E.Env<Env, Env> => { const check = pipe( E.Do, E.bindW('hasRef', () => has(ref)), E.bindW('env', () => getEnv), ) return pipe( check, E.chainW( E.chainRec(({ hasRef, env }: E.ValueOf<typeof check>) => { if (hasRef || O.isNone(env.parentKVEnv)) { return pipe(env, EE.of) } return pipe(check, E.useSome(env.parentKVEnv.value), EE.fromEnvL) }), ), ) } /** * @since 0.11.0 * @category Combinator */ export const withProvider = <K, E, A>(kv: KV<K, E, A>) => <E2, B>(env: E.Env<E2, B>) => pipe( kv, findKVProvider, E.chainW((refs) => pipe(env, E.useSome(refs))), ) /** * @since 0.11.0 * @category Combinator */ export const withProviderStream = <K, E, A>(kv: KV<K, E, A>) => <E2, B>(rs: RS.ReaderStream<E2, B>) => pipe( kv, findKVProvider, RS.fromEnv, RS.switchMapW((refs) => pipe(rs, RS.useSome(refs))), ) /** * @since 0.12.0 * @category Environment */ export interface Env extends Get, Has, Set, Remove, Events, ParentEnv {} /** * @since 0.12.0 * @category Combinator */ export const getEnv = E.asks( ({ getKV, hasKV, setKV, removeKV, kvEvents, parentKVEnv }: Env): Env => ({ getKV, hasKV, setKV, removeKV, kvEvents, parentKVEnv, }), ) /** * @since 0.11.0 * @category Model */ export type Adapter = A.Adapter<Event<any, any>> /** * @since 0.12.0 * @category Model */ export type Event<K, A> = Created<K, A> | Updated<K, A> | Removed<K> /** * @since 0.12.0 * @category Model */ export interface Created<K, A> { readonly _tag: 'Created' readonly key: K readonly value: A readonly fromAncestor: boolean } /** * @since 0.11.0 * @category Refinement */ export const isCreated = <K, A>(event: Event<K, A>): event is Created<K, A> => event._tag === 'Created' /** * @since 0.12.0 * @category Model */ export interface Updated<K, A> { readonly _tag: 'Updated' readonly key: K readonly previousValue: A readonly value: A readonly fromAncestor: boolean } /** * @since 0.11.0 * @category Refinement */ export const isUpdated = <K, A>(event: Event<K, A>): event is Updated<K, A> => event._tag === 'Updated' /** * @since 0.12.0 * @category Model */ export interface Removed<K> { readonly _tag: 'Removed' readonly key: K readonly fromAncestor: boolean } /** * @since 0.11.0 * @category Refinement */ export const isRemoved = <K, A>(event: Event<K, A>): event is Removed<K> => event._tag === 'Removed' /** * @since 0.12.0 * @category Deconstructor */ export const matchW = <A, K, B, C, D>( onCreated: (value: A, key: K) => B, onUpdated: (previousValue: A, value: A, key: K) => C, onDeleted: (key: K) => D, ) => (event: Event<K, A>): B | C | D => { if (event._tag === 'Updated') { return onUpdated(event.previousValue, event.value, event.key) } if (event._tag === 'Created') { return onCreated(event.value, event.key) } return onDeleted(event.key) } /** * @since 0.12.0 * @category Deconstructor */ export const match: <A, K, B>( onCreated: (value: A, key: K) => B, onUpdated: (previousValue: A, value: A, key: K) => B, onDeleted: (key: K) => B, ) => (event: Event<K, A>) => B = matchW /** * @since 0.12.0 * @category Environment Constructor */ export function env(options: EnvOptions = {}): Env { const { initial = [], kvEvents = A.create() } = options const references = new Map(initial) const sendEvent = createSendEvent(references, kvEvents) return { ...makeGetKV(references, sendEvent), ...makeHasKV(references), ...makeSetKV(references, sendEvent), ...makeDeleteKV(references, sendEvent), parentKVEnv: O.fromNullable(options.parentEnv), kvEvents: [sendEvent, kvEvents[1]], } } /** * @since 0.11.0 * @category Options */ export type EnvOptions = { readonly initial?: Iterable<readonly [any, any]> readonly kvEvents?: Adapter readonly parentEnv?: Env } function createSendEvent<K>(references: Map<any, any>, [push]: Adapter) { return (event: Event<K, any>) => pipe( event.fromAncestor, B.matchW( // Only update our local references when event.fromAncestor is false // as this indicates the event originates from within our current environment. () => { if (event._tag === 'Created' || event._tag === 'Updated') { references.set(event.key, event.value) } else { references.delete(event.key) } push(event) }, // When event.fromAncestor is true, the event originated from another environment. // We only replicate the event such that a descendant KVEnv can be re-sampled when it subscribes to // a Ref from an Ancestor's environment. () => push(event), ), ) } function makeGetKV(references: Map<any, any>, sendEvent: (event: Event<any, any>) => void): Get { return { getKV(kv) { if (references.has(kv.key)) { return E.of(references.get(kv.key)!) } return pipe( kv.initial, E.chainFirstIOK( (value) => () => sendEvent({ _tag: 'Created', key: kv.key, value, fromAncestor: false }), ), ) }, } } function makeHasKV(references: Map<any, any>): Has { return { hasKV(kv) { return E.fromIO(() => references.has(kv.key)) }, } } function makeSetKV(references: Map<any, any>, sendEvent: (event: Event<any, any>) => void): Set { const { getKV } = makeGetKV(references, sendEvent) return { setKV(kv, value) { return pipe( kv, getKV, E.map((previousValue) => [previousValue, !pipe(value, kv.equals(previousValue))] as const), E.chainFirstIOK( ([previousValue, changed]) => () => // Only send event when things changed changed && sendEvent({ _tag: 'Updated', key: kv.key, previousValue, value, fromAncestor: false, }), ), E.map(([previousValue, changed]) => (changed ? value : previousValue)), ) }, } } function makeDeleteKV( references: Map<any, any>, sendEvent: (event: Event<any, any>) => void, ): Remove { return { removeKV(kv) { return pipe( E.fromIO(() => (references.has(kv.key) ? O.some(references.get(kv.key)) : O.none)), E.chainFirstIOK( () => () => sendEvent({ _tag: 'Removed', key: kv.key, fromAncestor: false }), ), ) }, } } /** * Sample an Env with the latest references when updates have occured. * @since 0.11.0 * @category Combinator */ export const sample = <E, A>(env: E.Env<E, A>): RS.ReaderStream<E & Env, A> => pipe( getKVEvents, RS.filter(not(isCreated)), RS.startWith(null), RS.exhaustMapLatestEnv(() => env), ) /** * A shared KV for keeping track of a context's disposable resources. * @since 0.11.0 * @category KV */ export const Disposable = make(E.fromIO(D.settable), { ...EqStrict, key: Symbol.for('@typed/fp/KV.Disposable'), }) /** * @since 0.11.0 * @category Use */ export const useKeyedEnvs = <A>(Eq: Eq<A>) => { const refs = make( E.fromIO(() => new Map<A, Env>()), alwaysEqualsEq, ) const lookup = RM.lookup(Eq) const getOrCreate = <E>(key: A, value: E.Env<E, Env>) => pipe( refs, get, E.chainW((m) => pipe( m, lookup(key), O.matchW( () => pipe( value, E.tap((x: Env) => m.set(key, x)), ), E.of, ), ), ), ) const dispose = pipe( Disposable, get, E.tap((d) => d.dispose()), E.chainFirstW(() => remove(Disposable)), ) return pipe( E.Do, E.apSW('parentEnv', getEnv), E.bindW('createRefs', ({ parentEnv }) => E.of((key: A) => { const r = env({ parentEnv }) return pipe( refs, get, E.map((m) => m.set(key, r)), E.constant(r), E.useSome(parentEnv), ) }), ), E.bindW('findRefs', ({ createRefs, parentEnv }) => E.of((key: A) => pipe(getOrCreate(key, createRefs(key)), E.useSome(parentEnv))), ), E.bindW('deleteRefs', ({ parentEnv }) => E.of( (key: A): D.Disposable => ({ dispose: () => pipe( parentEnv, get(refs), R.map((refs) => refs.get(key)), R.chainFirst(() => pipe( refs, get, E.tap((m) => m.delete(key)), )(parentEnv), ), R.chain((refs) => (refs ? dispose(refs) : R.of(null))), R.exec, ), }), ), ), E.map(({ findRefs, deleteRefs }) => ({ findRefs, deleteRefs } as const)), ) }