UNPKG

@typed/fp

Version:

Data Structures and Resources for fp-ts

278 lines 9.37 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 { 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'; /** * 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(initial, options = {}) { const { equals = deepEqualsEq.equals, key = Symbol() } = options; return { key, initial, equals, }; } /** * @since 0.11.0 * @category Combinator */ export const get = (kv) => E.asksE((e) => e.getKV(kv)); /** * @since 0.11.0 * @category Combinator */ export const has = (kv) => E.asksE((e) => e.hasKV(kv)); /** * @since 0.11.0 * @category Combinator */ export const set = (kv) => (value) => E.asksE((e) => e.setKV(kv, value)); /** * @since 0.11.0 * @category Combinator */ export const update = (kv) => (f) => pipe(kv, get, E.chainW(f), E.chainW(set(kv))); /** * @since 0.11.0 * @category Combinator */ export const remove = (kv) => E.asksE((e) => e.removeKV(kv)); /** * @since 0.12.0 * @category Combinator */ export const getAdapter = E.asks((e) => 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 = (event) => pipe(getSendEvent, E.apW(E.of(event))); /** * @since 0.12.0 * @category Combinator */ export const getKVEvents = (e) => snd(e.kvEvents); /** * @since 0.11.0 * @category Combinator */ export const listenTo = (kv) => pipe(getKVEvents, RS.filter((x) => x.key === kv.key)); /** * @since 0.11.0 * @category Combinator */ export const listenToValues = (kv) => pipe(kv, listenTo, RS.map((e) => (isRemoved(e) ? O.none : O.some(e.value))), RS.startWith(O.none)); /** * @since 0.11.0 * @category Combinator */ export const getParentEnv = E.asks((e) => 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 = (ref) => { const check = pipe(E.Do, E.bindW('hasRef', () => has(ref)), E.bindW('env', () => getEnv)); return pipe(check, E.chainW(E.chainRec(({ hasRef, env }) => { 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 = (kv) => (env) => pipe(kv, findKVProvider, E.chainW((refs) => pipe(env, E.useSome(refs)))); /** * @since 0.11.0 * @category Combinator */ export const withProviderStream = (kv) => (rs) => pipe(kv, findKVProvider, RS.fromEnv, RS.switchMapW((refs) => pipe(rs, RS.useSome(refs)))); /** * @since 0.12.0 * @category Combinator */ export const getEnv = E.asks(({ getKV, hasKV, setKV, removeKV, kvEvents, parentKVEnv }) => ({ getKV, hasKV, setKV, removeKV, kvEvents, parentKVEnv, })); /** * @since 0.11.0 * @category Refinement */ export const isCreated = (event) => event._tag === 'Created'; /** * @since 0.11.0 * @category Refinement */ export const isUpdated = (event) => event._tag === 'Updated'; /** * @since 0.11.0 * @category Refinement */ export const isRemoved = (event) => event._tag === 'Removed'; /** * @since 0.12.0 * @category Deconstructor */ export const matchW = (onCreated, onUpdated, onDeleted) => (event) => { 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 = matchW; /** * @since 0.12.0 * @category Environment Constructor */ export function env(options = {}) { 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]], }; } function createSendEvent(references, [push]) { return (event) => 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, sendEvent) { 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) { return { hasKV(kv) { return E.fromIO(() => references.has(kv.key)); }, }; } function makeSetKV(references, sendEvent) { const { getKV } = makeGetKV(references, sendEvent); return { setKV(kv, value) { return pipe(kv, getKV, E.map((previousValue) => [previousValue, !pipe(value, kv.equals(previousValue))]), 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, sendEvent) { 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 = (env) => 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 = (Eq) => { const refs = make(E.fromIO(() => new Map()), alwaysEqualsEq); const lookup = RM.lookup(Eq); const getOrCreate = (key, value) => pipe(refs, get, E.chainW((m) => pipe(m, lookup(key), O.matchW(() => pipe(value, E.tap((x) => 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) => { 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) => pipe(getOrCreate(key, createRefs(key)), E.useSome(parentEnv)))), E.bindW('deleteRefs', ({ parentEnv }) => E.of((key) => ({ 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 }))); }; //# sourceMappingURL=KV.js.map