import {
Observable,
combineLatest,
debounceTime,
filter,
map,
of,
switchMap,
take,
throwError,
} from 'rxjs';
import {
EffectError,
EffectResult,
ToEffectError,
isEffectError,
isNotEffectError,
toEffectError,
} from './effect-result';
import { Signals, SignalsFactory } from './signals-factory';
import { Effect, SafeEffectResult, Store, UnhandledEffectError } from './store';
import {
DerivedId,
EffectId,
EventId,
NO_VALUE,
NoValueType,
getDerivedId,
getEffectId,
getEventId,
getStateId,
isNoValueType,
isNotNoValueType,
} from './store-utils';
/**
* Value-type for the combined derived behavior produced by {@link EffectSignals}.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
*/
export type CombinedEffectResult = {
/**
* The current input (which might differ from the resultInput),
* or `NO_VALUE`, if no input was received yet
*/
currentInput: Input | NoValueType;
/**
* The current effect-result,
* or `NO_VALUE` if no result was received yet,
* or the effect produced an error
*/
result: SafeEffectResult | NoValueType;
/**
* The input that produced the current effect-result,
* or `NO_VALUE`, if initial result or no result received yet */
resultInput: Input | NoValueType;
/**
* Indicates whether the effect is currently running.
* In case of a factory without trigger, this will be true whenever one or multiple
* of the following conditions are met:
* `currentInput !== resultInput`,
* or an invalidation event has been sent,
* or the effect has sent a result, but has not yet completed.
* In case of a factory with result-trigger, in addition to the previous
* criteria, a trigegr event must have been received.
*/
resultPending: boolean;
};
/**
* Type representing a {@link CombinedEffectResult} in it's error state (non-pending)
*
* @template Input - specifies the input type for the effect
* @template Error - specifies the error type of the effect
*/
export type CombinedEffectResultInErrorState = {
currentInput: Input;
result: ToEffectError | EffectError;
resultInput: Input;
resultPending: false;
};
/**
* Typeguard to check if a {@link CombinedEffectResult} is a {@link CombinedEffectResultInErrorState}
*/
export const isCombinedEffectResultInErrorState = (
cer: CombinedEffectResult,
): cer is CombinedEffectResultInErrorState => isEffectError(cer.result);
/**
* Type representing a {@link CombinedEffectResult} in it's success state (pending or non-pending)
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
*/
export type CombinedEffectResultInSuccessState = {
currentInput: Input | NoValueType;
result: Result;
resultInput: Input | NoValueType;
resultPending: boolean;
};
/**
* Typeguard to check if a {@link CombinedEffectResult} is a {@link CombinedEffectResultInSuccessState}
*/
export const isCombinedEffectResultInSuccessState = (
cer: CombinedEffectResult,
): cer is CombinedEffectResultInSuccessState => isNotEffectError(cer.result);
/**
* Type representing a {@link CombinedEffectResult} in it's success state (non-pending, hece completed effect)
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
*/
export type CombinedEffectResultInCompletedSuccessState = {
currentInput: Input | NoValueType;
result: Result;
resultInput: Input | NoValueType;
resultPending: false;
};
/**
* Typeguard to check if a {@link CombinedEffectResult} is a {@link CombinedEffectResultInCompletedSuccessState}
*/
export const isCombinedEffectResultInCompletedSuccessState = (
cer: CombinedEffectResult,
): cer is CombinedEffectResultInCompletedSuccessState =>
isNotEffectError(cer.result) && !cer.resultPending;
/**
* Value-type for result events produced by {@link EffectSignals}.
* In case the effect completes after one result, two reult events will
* be dispatched, one with completed false and one with completed true.
* This is to handle cases where the effect might send multiple results
* before completing. Thus, if an effect never completes, all result events
* will have completed false.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
*/
export type EffectResultEvent = {
/** the effect result */
result: SafeEffectResult;
/** the effect input that lead to the result */
resultInput: Input;
/** the input of the previous completed result, or `NO_VALUE` */
// previousInput: Input | NoValueType;
/** the previous completed result, or `NO_VALUE` */
// previousResult: SafeEffectResult | NoValueType;
/** has the effect for the given resultInput completed */
completed: boolean;
};
/**
* Value-type for completed result events produced by {@link EffectSignals}.
* In contrast to {@link EffectResultEvent}, events with this type are only dispatched,
* if the effect has completed, hence it will never be fired for effects that never complete.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
*/
export type EffectCompletedResultEvent = Omit<
EffectResultEvent,
'completed'
> & { completed: true };
/**
* Typeguard to check if a {@link EffectResultEvent} is a {@link EffectCompletedResultEvent}
*/
export const isCompletedResultEvent = (
value: EffectCompletedResultEvent | EffectResultEvent,
): value is EffectCompletedResultEvent => value.completed;
export type EffectCompletedResultEventInSuccessState = {
result: Result;
resultInput: Input;
completed: true;
};
export type EffectCompletedResultEventInErrorState = {
result: ToEffectError | EffectError;
resultInput: Input;
completed: true;
};
export const isCompletedResultEventInSuccessState = (
value: EffectCompletedResultEvent,
): value is EffectCompletedResultEventInSuccessState =>
isNotEffectError(value.result);
export const isCompletedResultEventInErrorState = (
value: EffectCompletedResultEvent,
): value is EffectCompletedResultEventInErrorState => isEffectError(value.result);
/**
* Type specifying the input {@link EffectSignals} (the corresponding signal-sources are NOT added to the store
* by the EffectSignals-setup, but by whoever uses the signals, e.g. by extendSetup or fmap or just using dispatch).
* The {@link EffectSignalsFactory} gives you the guarantee, that invalidate-events are NOT missed, even while
* the combined-behavior is not subscribed.
*
* @template Input - specifies the input type for the effect
*/
export type EffectInputSignals = {
/** Behavior being consumed by EffectSignals as input (see {@link EffectConfiguration} on how to configure your factory to subscribe the corresponding behavior eagerly) */
input: DerivedId;
/** Event that can be dispatched to trigger re-evaluation of the current input under the given effect */
invalidate: EventId;
/**
* Event that can be dispatched to trigger the given effect.
* This event has only meaning, if `withTrigger` is configured (see `EffectConfiguration`),
* else dispatching it is a no-op. */
trigger: EventId;
};
/**
* Type specifying the output {@link EffectSignals} (signals produced by `EffectSignals`).
* The {@link EffectSignalsFactory} takes care that subscribing the result-events keeps
* the effect itself lazy (hence only subscribing the combined behavior will subscribe the effect itself)!
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
*/
export type EffectOutputSignals = {
/** Produced combined effect result behavior */
combined: DerivedId>;
/** Produced success events */
results: EventId>;
/** Produced success events */
completedResults: EventId>;
};
/**
* Type specifying the effect-type of {@link EffectSignals} (the corresponding effects are NOT added to the store
* by the EffectSignals-setup, but by whoever uses the signals, e.g. by `useExistingEffect` or via extended configuration plus `extendSetup`).
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
*/
export type EffectFactoryEffects = {
id: EffectId;
};
/**
* This type specifies generic effect signals. `EffectSignals` generically handle side-effects (hence, are an abstraction over side-effects).
* They fulfill the following requirements:
* ```markdown
* 1.) The produced CombinedEffectResult behavior must be lazy, hence, as long as it is not subscribed,
* no effect will be triggered (so subscribing just results, or completedResults will not trigger the effect).
* 2.) Unhandled effect errors are caught and will lead to an EffectError.
* 3.) In addition to the combined-behavior, also event-streams for EffectResultEvent and EffectCompletedResultEvent are provided. This is important
* in cases where e.g. an effect success should be used to trigger something else (e.g. close a popup), but you cannot use the result
* behavior, because it would mean to always subscribe the result. In contrast, subscription of the an rsult-event-stream will NOT
* subscribe the effect.
* ```
*
* See the documentation for {@link EffectConfiguration} for further configuration of `EffectSignals`.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
*/
export type EffectSignals = Signals<
EffectInputSignals,
EffectOutputSignals,
EffectFactoryEffects
>;
/**
* This type specifies the type of the argument to {@link EffectSignalsBuild}, hence the configuration of {@link EffectSignals}.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
*/
export type EffectConfiguration = {
/** Function used to determine whether a new input equals the previous one. Defaults to strict equals (`a === b`) */
effectInputEquals?: (a: Input, b: Input) => boolean;
/** Defaults to false. If true, the effect will only be performed in case a trigger event is received (else, whenever the input changes) */
withTrigger?: boolean;
/** If defined, this function will be used to determine an initial result for the result behavior */
initialResultGetter?: () => Result;
/** If defined and `>0`, then it will be used as milliseconds to debounce new input to the effect (please DON'T debounce the input signal yourself, because that would debounce before trigger and/or input equals!) */
effectDebounceTime?: number;
/** Function to wrap the effect defined by effectId with a custom `Effect` */
wrappedEffectGetter?: (effect: Effect) => Effect;
/** Specifies whether the input behavior should be subscribed eagerly (defaults to false) */
eagerInputSubscription?: boolean;
/** Optional string to be used as argument to calls of `getBehaviorId` and `getEventId` */
nameExtension?: string;
};
/**
* Type specifying the {@link SignalsBuild} function for {@link EffectSignals}, hence a function taking a {@link EffectConfiguration} and producing {@link EffectSignals}.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
* @template Error - specifies the error type of the effect
* @param {EffectConfiguration} config - the configuration for the `EffectSignals`
*/
export type EffectSignalsBuild = (
config: EffectConfiguration,
) => EffectSignals;
const getInputSignalIds = (nameExtension?: string): EffectInputSignals => ({
input: getDerivedId(`${nameExtension ?? ''}_input`),
invalidate: getEventId(`${nameExtension ?? ''}_invalidate`),
trigger: getEventId(`${nameExtension ?? ''}_trigger`),
});
const getOutputSignalIds = (
nameExtension?: string,
): EffectOutputSignals => ({
combined: getDerivedId>(
`${nameExtension ?? ''}_combined`,
),
results: getEventId>(`${nameExtension ?? ''}_results`),
completedResults: getEventId>(
`${nameExtension ?? ''}_completedResults`,
),
});
const NO_VALUE_TRIGGERED_INPUT = '$RXS_INTERNAL_NV_TI$';
type NoValueTriggeredInput = typeof NO_VALUE_TRIGGERED_INPUT;
type InternalEffectResult