import mitt, { Emitter } from 'mitt'; import { createReadonlyStateWireGuard, ReadonlyStateWire, } from '../state-wire/readonly-state-wire'; import { createStateWireGuard, StateWire } from '../state-wire/state-wire'; import { memoize } from '../utils/memoize'; import { Defined } from '../utils/type-utils'; import { createId } from '../utils/wire-id'; type ReconnectFunction = (options?: O) => void; type ConnectFunction = () => ReconnectFunction; export type GetWireValue = (wire: ReadonlyStateWire) => V; export type SetWireValue = (wire: StateWire, value: Defined) => void; export interface ReadOnlySelectorOptions { get: (options: { get: GetWireValue }) => V; } export interface WritableSelectorOptions { get: (options: { get: GetWireValue }) => V; set: (options: { get: GetWireValue; set: SetWireValue }, value: V) => void; } export interface SelectorOptions { get: (options: { get: GetWireValue }) => V; set?: (options: { get: GetWireValue; set: SetWireValue }, value: V) => void; } type UnsubscribeMap = Map, null | (() => void)>; type ValueMap = Map, unknown>; const resubscribe = ( oldActiveWires: UnsubscribeMap, newActiveWires: UnsubscribeMap, update?: () => void, ) => { oldActiveWires.forEach((unsubscribe, wire) => { if (unsubscribe && !newActiveWires.has(wire)) { unsubscribe(); oldActiveWires.set(wire, null); } }); newActiveWires.forEach((unsubscribe, wire) => { const newUnsubscribe = oldActiveWires.get(wire) ?? wire.subscribe(() => { update && update(); }); newActiveWires.set(wire, newUnsubscribe); }); }; const valuesChanged = (values: ValueMap) => { let changed = false; values.forEach((value, wire) => { if (value !== wire.getValue()) { changed = true; } }); return changed; }; export function createStateSelector( options: WritableSelectorOptions, ): [StateWire, ConnectFunction>]; export function createStateSelector( options: ReadOnlySelectorOptions, ): [ReadonlyStateWire, ConnectFunction>]; export function createStateSelector( options: SelectorOptions, ): | [ReadonlyStateWire, ConnectFunction>] | [StateWire, ConnectFunction>] { const key: string = 'value'; const activeWires: { current: UnsubscribeMap } = { current: new Map() }; const activeValues: { current: ValueMap } = { current: new Map() }; let { get: getOption, set: setOption } = options; const get = (wire: ReadonlyStateWire) => { activeWires.current.set(wire, null); const value = wire.getValue(); activeValues.current.set(wire, value); return value; }; const setGet = (wire: ReadonlyStateWire) => { return wire.getValue(); }; const set = (wire: StateWire, value: Defined) => { wire.setValue(value); }; let stateValue = getOption({ get }); const emitter: Emitter = mitt(); const update = () => { const oldActiveWires = activeWires.current; const newActiveWires = (activeWires.current = new Map()); activeValues.current = new Map(); const oldValue = stateValue; const newValue = getOption({ get }); resubscribe(oldActiveWires, newActiveWires, update); if (oldValue !== newValue) { stateValue = newValue; emitter.emit(key, newValue); } }; const disconnect = () => { resubscribe(activeWires.current, new Map()); }; const connect = () => { if (valuesChanged(activeValues.current)) { update(); } else { resubscribe(new Map(), activeWires.current, update); } return (options?: SelectorOptions) => { if (options) { getOption = options.get; setOption = options.set; update(); } else { disconnect(); } }; }; const getValue = () => { return stateValue; }; const setValue = (value: Defined) => { setOption && setOption({ get: setGet, set }, value); }; const subscribe = (callback: (value: Defined) => void) => { emitter.on(key, callback); return () => { emitter.off(key, callback); }; }; const id = createId('s-'); const _getId = () => id; // it should memoize for each selector instance, do not move to top level const memoizedGetLinkIds = memoize((wires: UnsubscribeMap) => { return [ ...Array.from(wires.keys()).map((wire) => wire._getLinkIds()), _getId(), ]; }); const _getLinkIds = () => { return memoizedGetLinkIds(activeWires.current); }; const selectorContext: ReadonlyStateWire | StateWire = setOption ? { getValue, subscribe, setValue, ...createStateWireGuard(), _getId, _getLinkIds, } : { getValue, subscribe, ...createReadonlyStateWireGuard(), _getId, _getLinkIds, }; return [selectorContext, connect]; }