import { distinctUntilChanged, filter, map, share } from 'rxjs'; import { Signals, SignalsFactory } from './signals-factory'; import { Store } from './store'; import { DerivedId, EventId, getDerivedId, getEventId, getStateId } from './store-utils'; export const TRIGGER_NO_VALUE = '$INTERNAL_TRIGGER_NO_VALUE$'; export type TriggerNoValueType = typeof TRIGGER_NO_VALUE; type TriggerState = { currentInput: T | TriggerNoValueType; triggeredInput: { value: T | TriggerNoValueType; }; changedAfterTrigger: boolean; }; const isNotTriggerNoValueType = ( v: T | TriggerNoValueType, ): v is Exclude => v !== TRIGGER_NO_VALUE; /** * Type specifying the input {@link TriggerSignals} (the corresponding signal-sources are NOT added to the store * by the TriggerSignals-setup, but by whoever uses the signals, e.g. by extendSetup or fmap or just using dispatch). * * @template T - specifies the value type for the triggered signal */ export type TriggerInputSignals = { /** Behavior representing the input signal */ triggerInput: DerivedId; /** Event that triggers the current input as output */ trigger: EventId; }; /** * Type specifying the output {@link TriggerSignals} (signals produced by TriggerSignals). * * @template T - specifies the value type for the triggered signal */ export type TriggerOutputSignals = { /** * Behavior representing the triggered output signal. * Triggering the same input multiple times will NOT lead to multiple outputs (all behaviors are distinctUntilChanged). * You can instead use the triggered event, if you need to take action on each trigger event. */ triggeredOutput: DerivedId; /** * Event with the triggered output. * In contrast to triggeredOutput, triggering the same input multiple times will lead to the same number of triggered events. */ triggered: EventId; }; /** * This type specifies input and output signals of a triggerable behavior. * * @template T - specifies the value type for the triggered signal */ export type TriggerSignals = Signals, TriggerOutputSignals>; /** * This type specifies the type of the argument to {@link TriggerSignalsBuild}, hence the configuration of {@link TriggerSignals}. */ export type TriggerConfiguration = { /** Optional string to be used as argument to calls of getBehaviorId and getEventId */ nameExtension?: string; }; /** * Type specifying the {@link SignalsBuild} function for {@link TriggerSignals}, hence a function taking an {@link TriggerConfiguration} and producing TriggerSignals. * * @template T - specifies the value type for the triggered signal * @param {TriggerConfiguration} config - the configuration for the TriggerSignals */ export type TriggerSignalsBuild = (config: TriggerConfiguration) => TriggerSignals; const getInputSignalIds = (nameExtension?: string): TriggerInputSignals => ({ triggerInput: getDerivedId(`${nameExtension ?? ''}_triggerInput`), trigger: getEventId(`${nameExtension ?? ''}_trigger`), }); const getOutputSignalIds = (nameExtension?: string): TriggerOutputSignals => ({ triggeredOutput: getDerivedId(`${nameExtension ?? ''}_triggeredOutput`), triggered: getEventId(`${nameExtension ?? ''}_triggered`), }); const getTriggerBuilder: TriggerSignalsBuild = ( config: TriggerConfiguration, ): TriggerSignals => { const inIds = getInputSignalIds(config.nameExtension); const outIds = getOutputSignalIds(config.nameExtension); const setup = (store: Store) => { const internalState = getStateId>(); const newInput = getEventId(); store.connect(inIds.triggerInput, newInput); store.addState(internalState, { currentInput: TRIGGER_NO_VALUE, triggeredInput: { value: TRIGGER_NO_VALUE, }, changedAfterTrigger: false, }); store.addReducer(internalState, newInput, (state, currentInput) => ({ currentInput, triggeredInput: { value: state.triggeredInput.value, }, changedAfterTrigger: true, })); store.addReducer(internalState, inIds.trigger, state => ({ currentInput: state.currentInput, triggeredInput: { value: state.currentInput, }, changedAfterTrigger: false, })); const sharedObs = store.getBehavior(internalState).pipe( filter(s => !s.changedAfterTrigger && s.currentInput === s.triggeredInput.value), map(s => s.triggeredInput.value), filter(isNotTriggerNoValueType), share(), ); store.addDerivedState(outIds.triggeredOutput, sharedObs.pipe(distinctUntilChanged())); store.addEventSource(outIds.triggered, sharedObs); }; return { setup, input: inIds, output: outIds, effects: {}, }; }; /** * This type specifies a {@link SignalsFactory} wrapping {@link TriggerSignals}. * * @template T - specifies the value type for the triggered signal */ export type TriggerSignalsFactory = SignalsFactory< TriggerInputSignals, TriggerOutputSignals, TriggerConfiguration >; /** * This function creates a {@link TriggerSignalsFactory}. * * @template T - specifies the value type for the triggered signal * @returns {TriggerSignalsFactory} */ export const getTriggerSignalsFactory = (): TriggerSignalsFactory => new SignalsFactory, TriggerOutputSignals, TriggerConfiguration>( getTriggerBuilder, );