import type { AnyValueReactor, Signal, ValueOfValueReactor, ValueReactor, } from "../reactor-core/index.ts" import { effect, signal } from "../reactor-core/index.ts" import { castValueInitializer } from "./utility.ts" export interface TapOptions { target: R tapper?: ((value: ValueOfValueReactor) => void) | undefined } /** * @description Taps into the value changes of a ValueReactor without modifying its value. */ export const tap = (options: TapOptions): R => { const { target, tapper = (value: ValueOfValueReactor): void => console.log(value) } = options effect(() => { const value = target.get() as ValueOfValueReactor tapper(value) }) return target } export interface WithHistoryOptions { target: ValueReactor } /** * @description Creates a signal that maintains a history of values from a ValueReactor. * The history is stored in an array, with each new value appended to the end. * * Note: This implementation does not limit the size of the history array. Use * with caution to avoid excessive memory usage. */ export const withHistory = (options: WithHistoryOptions): Signal => { const { target } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() }, }) const history: V[] = [] const eTarget = effect(() => { history.push(target.get()) result.set([...history]) }) return result } export interface BufferByCountOptions { target: ValueReactor count: number } /** * @description Buffers values from a ValueReactor and emits them as an array * when the number of buffered values reaches the specified count. */ export const bufferByCount = (options: BufferByCountOptions): Signal => { const { target, count } = options if (count <= 0) { throw new Error("bufferByCount operator requires count to be greater than 0") } const result = signal(() => [], { onDispose: () => { eTarget.dispose() }, }) const bufferedValues: V[] = [] const eTarget = effect(() => { bufferedValues.push(target.get()) if (bufferedValues.length >= count) { result.set([...bufferedValues]) bufferedValues.length = 0 } }) return result } export interface BufferByTimeOptions { target: ValueReactor time: number } /** * @description Buffers values from a ValueReactor and emits them as an array * at specified time intervals. */ export const bufferByTime = (options: BufferByTimeOptions): Signal => { const { target, time } = options if (time <= 0) { throw new Error("bufferByTime operator requires timeInMilliseconds to be greater than 0") } const result = signal(() => [], { onDispose: () => { eTarget.dispose() clearInterval(intervalId) }, }) const bufferedValues: V[] = [] const eTarget = effect(() => { bufferedValues.push(target.get()) }) const intervalId = setInterval(() => { result.set([...bufferedValues]) bufferedValues.length = 0 }, time) return result } export interface BufferByTriggerOptions { target: ValueReactor trigger: ValueReactor } /** * @description Buffers values from a ValueReactor and emits them as an array when the trigger ValueReactor * changes. */ export const bufferByTrigger = ( options: BufferByTriggerOptions, ): Signal => { const { target, trigger } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() eTrigger.dispose() }, }) const bufferedValues: VTarget[] = [] const eTarget = effect(() => { bufferedValues.push(target.get()) }) const eTrigger = effect(() => { trigger.get() result.set([...bufferedValues]) bufferedValues.length = 0 }) return result } export interface BufferByToggleOptions { target: ValueReactor open: ValueReactor close: ValueReactor } /** * @description Buffers values from a ValueReactor and emits them as an array * when the open ValueReactor emits a value, and stops buffering * when the close ValueReactor emits a value. */ export const bufferByToggle = ( options: BufferByToggleOptions, ): Signal => { const { target, open, close } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() eOpen.dispose() eClose.dispose() }, }) const bufferedValues: VTarget[] = [] let buffering = false const eOpen = effect(() => { open.get() buffering = true }) const eTarget = effect(() => { const value = target.get() if (buffering === true) { bufferedValues.push(value) } }) const eClose = effect(() => { close.get() if (buffering === true) { result.set([...bufferedValues]) bufferedValues.length = 0 buffering = false } }) return result } export interface BufferByDynamicOptions { target: ValueReactor dynamic: (value: VTarget) => ValueReactor } /** * @description Buffers values from a ValueReactor and emits them as an array * when the ValueReactor returned by the `dynamic` function emits a value. * The `dynamic` function is called each time the buffer is emitted * to get a new ValueReactor for the next buffer. */ export const bufferByDynamic = ( options: BufferByDynamicOptions, ): Signal => { const { target, dynamic } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() eDynamic.dispose() }, }) const bufferedValues: VTarget[] = [] const eTarget = effect(() => { const value = target.get() bufferedValues.push(value) }) let trigger: ValueReactor | undefined = undefined const eDynamic = effect(() => { trigger?.dispose() const targetValue = target.getWithoutTrack() trigger = dynamic(targetValue) trigger.get() result.set([...bufferedValues]) bufferedValues.length = 0 }) return result } export interface GroupToArrayByOptions { target: ValueReactor keyGetter: (value: V) => K } /** * @description Groups values from a ValueReactor into an array of arrays based on a key * extracted by the keyGetter function. */ export const groupToArrayBy = (options: GroupToArrayByOptions): Signal => { const { target, keyGetter } = options const result = signal(() => [], { onDispose: () => { eTarget.dispose() }, }) const groups = new Map() const eTarget = effect(() => { const value = target.get() const key = keyGetter(value) if (groups.has(key) === false) { groups.set(key, []) result.set([...groups.values()]) } const groupedArray = groups.get(key)! groupedArray.push(value) }) return result } export interface GroupToObjectByOptions { target: ValueReactor keyGetter: (value: V) => K } /** * @description Groups values from a ValueReactor into an object where each key maps to an array * of values corresponding to that key. */ export const groupToObjectBy = ( options: GroupToObjectByOptions, ): Signal> => { const { target, keyGetter } = options const result = signal>( () => { return {} as Record }, { onDispose: () => { eTarget.dispose() }, }, ) const groups = new Map() const eTarget = effect(() => { const value = target.get() const key = keyGetter(value) if (groups.has(key) === false) { groups.set(key, []) result.set(Object.fromEntries(groups.entries()) as Record) } const groupedArray = groups.get(key)! groupedArray.push(value) }) return result } /** * @description Groups values from a ValueReactor into an array of Signals based on a key * extracted by the keyGetter function. */ export const groupToSignalArrayBy = ( target: ValueReactor, keyGetter: (value: V) => K, ): Signal>> => { const result = signal>>(() => [], { onDispose: () => { eTarget.dispose() }, }) const groups = new Map>() const eTarget = effect(() => { const value = target.get() const key = keyGetter(value) if (groups.has(key) === false) { const newGroup = signal(() => []) groups.set(key, newGroup) result.set([...groups.values()]) } const groupedSignal = groups.get(key)! const currentArray = groupedSignal.get() groupedSignal.set([...currentArray, value]) }) return result } export interface GroupToSignalObjectByOptions { target: ValueReactor keyGetter: (value: V) => K } /** * @description Groups values from a ValueReactor into a Signal of an object where each key maps to a Signal * of an array of values corresponding to that key. */ export const groupToSignalObjectBy = ( options: GroupToSignalObjectByOptions, ): Signal>> => { const { target, keyGetter } = options const result = signal>>( () => { return {} as Record> }, { onDispose: () => { eTarget.dispose() }, }, ) const groups = new Map>() const eTarget = effect(() => { const value = target.get() const key = keyGetter(value) if (groups.has(key) === false) { const newGroup = signal(() => []) groups.set(key, newGroup) result.set(Object.fromEntries(groups.entries()) as Record>) } const groupedSignal = groups.get(key)! const currentArray = groupedSignal.get() groupedSignal.set([...currentArray, value]) }) return result } export interface MapOptions { target: ValueReactor mapper: (value: VTarget) => VMapped } /** * @description Maps values from a ValueReactor using the provided mapper function and * emits the mapped values as a new Signal. */ export const map = (options: MapOptions): Signal => { const { target, mapper } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() }, }) const eTarget = effect(() => { const value = target.get() const newValue = mapper(value) result.set(newValue) }) return result } export interface ContextualMapInitializingMapperContext { isInitializingRun: true targetValue: VTarget } export interface ContextualMapUpdatingMapperContext { isInitializingRun: false targetValue: VTarget previousMappedValue: VMapped } export type ContextualMapMapperContextOf = | ContextualMapInitializingMapperContext | ContextualMapUpdatingMapperContext export interface ContextualMapOptions { target: ValueReactor mapper: (context: ContextualMapMapperContextOf) => VMapped } /** * @description Maps values from a ValueReactor using the provided contextual mapper function and * emits the mapped values as a new Signal. */ export const contextualMap = ( options: ContextualMapOptions, ): Signal => { const { target, mapper } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() }, }) const eTarget = effect((context) => { const targetValue = target.get() const { isInitializingRun } = context if (isInitializingRun === true) { const newValue = mapper({ isInitializingRun, targetValue, }) result.set(newValue) } else { const previousMappedValue = result.getWithoutTrack() const newValue = mapper({ isInitializingRun, targetValue, previousMappedValue, }) result.set(newValue) } }) return result } export interface MergeMapOptions { target: ValueReactor mapper: (value: VTarget) => ValueReactor } /** * @description Maps each value from a ValueReactor to a ValueReactor using the provided `mapper`, * subscribes to every mapped reactor, and merges their emitted values into a single `Signal`. */ export const mergeMap = ( options: MergeMapOptions, ): Signal => { const { target, mapper } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() }, }) const eTarget = effect(() => { const value = target.get() const currentMappedReactor = mapper(value) effect(() => { const mappedValue = currentMappedReactor.get() result.set(mappedValue) }) }) return result } export interface SwitchMapOptions { target: ValueReactor mapper: (value: VTarget) => ValueReactor } /** * @description Maps each value from a ValueReactor to a ValueReactor using the provided `mapper`, * subscribes only to the latest mapped reactor, and emits its values as a `Signal`. */ export const switchMap = ( options: SwitchMapOptions, ): Signal => { const { target, mapper } = options const result = signal(castValueInitializer(), { onDispose: () => { eTarget.dispose() }, }) let lastMappedReactor: ValueReactor | undefined = undefined const eTarget = effect(() => { const value = target.get() lastMappedReactor?.dispose() lastMappedReactor = mapper(value) effect(() => { const mappedValue = lastMappedReactor!.get() result.set(mappedValue) }) }) return result } export interface ScanOptions { target: ValueReactor accumulator: (acc: VAccumulated, value: VTarget) => VAccumulated seed: VAccumulated } /** * @description Accumulates values from a ValueReactor using the provided accumulator function * and emits the accumulated value as a Signal. */ export const scan = ( options: ScanOptions, ): Signal => { const { target, accumulator, seed } = options const result = signal(() => seed, { onDispose: () => { eTarget.dispose() }, }) const eTarget = effect(() => { const value = target.get() const newValue = accumulator(result.getWithoutTrack(), value) result.set(newValue) }) return result } export interface PairwiseOptions { target: ValueReactor } /** * @description Emits the previous and current values from a ValueReactor as a tuple. * The first emitted tuple will have `undefined` as the previous value. */ export const pairwise = (options: PairwiseOptions): Signal<[V | undefined, V]> => { const { target } = options const result = signal<[V | undefined, V]>(() => [undefined, target.get()], { onDispose: () => { eTarget.dispose() }, }) let previousValue: V | undefined = undefined const eTarget = effect(() => { const currentValue = target.get() result.set([previousValue, currentValue]) previousValue = currentValue }) return result }