/* IMPORT */ import { OBSERVABLE_FALSE, OBSERVABLE_TRUE } from '~/constants' import cleanup from '~/methods/cleanup' import effect from '~/methods/effect' import isObservableFrozen from '~/methods/is_observable_frozen' import memo from '~/methods/memo' import untrack from '~/methods/untrack' import warmup from '~/methods/warmup' import { readable } from '~/objects/callable' import Observable from '~/objects/observable' import { is } from '~/utils' /* HELPERS */ const isEqualForSelector = (a: unknown, b: unknown): boolean => { // Treat 0 and -0 as equal for selector purposes if ((a === 0 || is(a, -0)) && (b === 0 || is(b, -0))) return true return is(a, b) } import type { SelectorFunction, ObservableReadonly } from '~/types' import { callStack, Stack } from './debugger' /* HELPERS */ class DisposableMap extends Map { // This allows us to skip emptying the map in some cases, while still being able to signal that it was disposed disposed: boolean = false; } class SelectedObservable extends Observable { // This saves some memory compared to making a dedicated standalone object for metadata + a cleanup function count: number = 1; selecteds!: DisposableMap //TSC source?: any /* API */ call(): void { // Cleanup function if (this.selecteds.disposed) return this.count -= 1 if (this.count) return this.selecteds.delete(this.source) } } /* MAIN */ const selector = (source: () => T): SelectorFunction => { /* NORMALIZING SOURCE */ source = warmup(memo(source)) /* FROZEN SOURCE */ if (isObservableFrozen(source)) { const sourceValue = untrack(source) return (value: T): ObservableReadonly => { return (isEqualForSelector(value, sourceValue)) ? OBSERVABLE_TRUE : OBSERVABLE_FALSE } } /* SELECTEDS */ let selecteds = new DisposableMap() let selectedValue: T = untrack(source) const stack = callStack() effect((stack?: Stack) => { const valuePrev = selectedValue const valueNext = source() if (isEqualForSelector(valuePrev, valueNext)) return selectedValue = valueNext selecteds.get(valuePrev)?.set(false) selecteds.get(valueNext)?.set(true) }, { suspense: false, sync: true, stack }) /* CLEANUP ALL */ const cleanupAll = (): void => { selecteds.disposed = true } cleanup(cleanupAll) /* SELECTOR */ return (value: T): ObservableReadonly => { /* INIT */ let selected = selecteds.get(value) if (selected) { selected.count += 1 } else { const isSelected = isEqualForSelector(value, selectedValue) selected = new SelectedObservable(isSelected) selected.selecteds = selecteds selected.source = value selecteds.set(value, selected) } /* CLEANUP */ cleanup(selected) /* RETURN */ return readable(selected) } } /* EXPORT */ export default selector