/* eslint-disable @typescript-eslint/no-use-before-define */ import { Observable, take } from 'rxjs'; import { Store } from './store'; import { BehaviorId, DerivedId, EffectId, SignalId, ToBehaviorIdValueType, ToSignalIdValueType, getDerivedId, } from './store-utils'; import { Configuration, KeysOfValueType, Merged, MergedConfiguration, WithValueType, merge, } from './type-utils'; /** * This type defines an object that maps identifier names to {@link SignalId}s or nested {@link NameToSignalId}s. */ export type NameToSignalId = { [key: string]: SignalId | NameToSignalId }; /** * This type defines an object that maps identifier names to {@link EffectId}s or nested {@link NameToEffectId}s. */ export type NameToEffectId = { [key: string]: EffectId | NameToEffectId }; /** * This type defines an object that holds input and output {@link SignalId}s of a {@link Signals} type. * * @template IN - {@link NameToSignalId} defining input signals-ids * @template OUT - {@link NameToSignalId} defining output signals-ids */ export type SignalIds = { readonly input: IN; readonly output: OUT; }; /** * This type defines an object that holds {@link EffectId}s of a {@link Signals} type. * * @template EFF - {@link NameToEffectId} defining effect-ids */ export type EffectIds = { readonly effects: EFF; }; /** * This type defines an object with a function 'setup' that takes a {@link Store} instance * as argument and executes the required setup (wireing) to produce certain output signals. */ export type SetupWithStore = { readonly setup: (store: Store) => void; }; /** * This type defines an immutable object that encapsulates {@link SignalIds}, {@link SetupWithStore} and {@link EffectIds}. * The setup method creates all the necessary wireing to configure the {@link Store} instance for the `SignalIds`. * The `Signals` type represents the reactive counterpart to a class-instance (the IN/OUT-{@link NameToSignalId} * being the counterpart to a class-interface). * `SetupWithStore` must add sources to the store for all output-ids, but it must NOT add sources for * the input-ids and it must also NOT add effects for the effect-ids. * * @template IN - concrete {@link NameToSignalId} defining input signal-ids. `SetupWithStore` does NOT add corresponding signals to the store (hence this must be done by the user of this `Signals` object, e.g. via `connect`) * @template OUT - concrete {@link NameToSignalId} defining output signal-ids. In contrast to the input signal-ids, the `SetupWithStore` method takes care of setting up corresponding signals in the store. * @template EFF - concrete {@link NameToEffectId} defining effect-ids. `SetupWithStore` does NOT add corresponding effects to the store (hence this must be done by the user of this `Signals` object) */ export type Signals< IN extends NameToSignalId, OUT extends NameToSignalId, EFF extends NameToEffectId = {}, > = SetupWithStore & SignalIds & EffectIds; /** * This type defines a function taking some config-object as argument and returning a {@link Signals} object, hence the type being used as constructor argument for {@link SignalsFactory}. * It creates input- and output-ids, as well as effect-ids. * It is the reactive counterpart of a class/class-constructor. * However, the setup method of the returned `Signals` only adds signals to the store * for the output-ids. For the input-ids, corresponding signals must be setup somewhere else (e.g. via `store.connect`). * If you have a scenario, where setup needs a {@link SignalId} that is NOT created by {@link SignalsBuild}, then you can pass it via `CONFIG`. * The same holds true for effects (no effects for the generated effect-ids will be added to the store). * (see {@link SignalsFactory.useExistingEffect} on how to delegate from a generated effect-id to an existing effect) * * @template IN - concrete {@link NameToSignalId} defining input signal-ids of the resulting `Signals` object * @template OUT - concrete {@link NameToSignalId} defining output signal-ids of the resulting `Signals` object * @template CONFIG - concrete {@link Configuration} for the given `SignalsBuild` * @template EFF - concrete {@link NameToEffectId} defining effect-ids of the resulting `Signals` object */ export type SignalsBuild< IN extends NameToSignalId, OUT extends NameToSignalId, CONFIG extends Configuration, EFF extends NameToEffectId, > = (config: CONFIG) => Signals; /** * This type specifies a function mapping from ```SignalsBuild``` to ```SignalsFactory```, * hence the argument to the monadic-bind method of a {@link SignalsFactory}. * * @template IN1 - concrete {@link NameToSignalId} defining input-ids produced by the given `SignalsBuild` * @template OUT1 - concrete {@link NameToSignalId} defining output-ids produced by the given `SignalsBuild` * @template CONFIG1 - concrete {@link Configuration} for the given `SignalsBuild` * @template EFF1 - concrete {@link NameToEffectId} defining effect-ids produced by the given `SignalsBuild` * @template IN2 - concrete {@link NameToSignalId} defining input signal-ids of the resulting `SignalsFactory` * @template OUT2 - concrete {@link NameToSignalId} defining output signal-ids of the resulting `SignalsFactory` * @template CONFIG2 - concrete {@link Configuration} for the resulting `SignalsFactory` * @template EFF2 - concrete {@link NameToEffectId} defining effect-ids of the resulting `SignalsFactory` */ export type BindMapper< IN1 extends NameToSignalId, OUT1 extends NameToSignalId, CONFIG1 extends Configuration, EFF1 extends NameToEffectId, IN2 extends NameToSignalId, OUT2 extends NameToSignalId, CONFIG2 extends Configuration, EFF2 extends NameToEffectId, > = ( signalsBuild: SignalsBuild, ) => SignalsFactory; /** * This type specifies a function mapping from ```SignalsBuild``` to ```SignalsBuild```, * hence the argument to the functor-map-method of a {@link SignalsFactory}. * * @template IN1 - concrete {@link NameToSignalId} defining input signal-ids produced by the given `SignalsBuild` * @template OUT1 - concrete {@link NameToSignalId} defining output signal-ids produced by the given `SignalsBuild` * @template CONFIG1 - concrete {@link Configuration} for the given `SignalsBuild` * @template EFF1 - concrete {@link NameToEffectId} produced by the given `SignalsBuild` * @template IN2 - concrete {@link NameToSignalId} defining input signals-ids produced by the resulting `SignalsBuild` * @template OUT2 - concrete {@link NameToSignalId} defining output signal-ids produced by the resulting `SignalsBuild` * @template CONFIG2 - concrete {@link Configuration} for the resulting `SignalsBuild` * @template EFF2 - concrete {@link NameToEffectId} produced by the resulting `SignalsBuild` */ export type BuildMapper< IN1 extends NameToSignalId, OUT1 extends NameToSignalId, CONFIG1 extends Configuration, EFF1 extends NameToEffectId, IN2 extends NameToSignalId, OUT2 extends NameToSignalId, CONFIG2 extends Configuration, EFF2 extends NameToEffectId, > = ( signalsBuild: SignalsBuild, ) => SignalsBuild; /** * This type specifies the result of the {@link SignalsFactory.compose} method. * * @template IN1 - concrete {@link NameToSignalId} defining input signal-ids of `SignalsFactory1` * @template OUT1 - concrete {@link NameToSignalId} defining output signal-ids of `SignalsFactory1` * @template CONFIG1 - concrete {@link Configuration} of `SignalsFactory1` * @template EFF1 - concrete {@link NameToEffectId} of `SignalsFactory1` * @template IN2 - concrete {@link NameToSignalId} defining input signal-ids of `SignalsFactory2` * @template OUT2 - concrete {@link NameToSignalId} defining output signal-ids of `SignalsFactory2` * @template CONFIG2 - concrete {@link Configuration} of `SignalsFactory2` * @template EFF1 - concrete {@link NameToEffectId} of `SignalsFactory2` */ export type ComposedFactory< IN1 extends NameToSignalId, OUT1 extends NameToSignalId, CONFIG1 extends Configuration, EFF1 extends NameToEffectId, IN2 extends NameToSignalId, OUT2 extends NameToSignalId, CONFIG2 extends Configuration, EFF2 extends NameToEffectId, > = SignalsFactory< Merged, Merged, MergedConfiguration, Merged >; export type SignalsFactoryArgs< IN extends NameToSignalId, OUT extends NameToSignalId, CONFIG extends Configuration, EFF extends NameToEffectId, > = { store: Store; input: IN; output: OUT; config: CONFIG; effects: EFF; }; /** * This type specifies the argument to the {@link SignalsFactory.extendSetup} method. * * @template IN - concrete {@link NameToSignalId} defining input signal-ids of a `SignalsFactory` * @template OUT - concrete {@link NameToSignalId} defining output signal-ids of a `SignalsFactory` * @template CONFIG - concrete {@link Configuration} of a `SignalsFactory` * @template EFF - concrete {@link NameToEffectId} of a `SignalsFactory` */ export type ExtendSetup< IN extends NameToSignalId, OUT extends NameToSignalId, CONFIG extends Configuration, EFF extends NameToEffectId, > = (args: SignalsFactoryArgs) => void; /** * Function mapping from CONFIG1 to CONFIG2 */ export type MapConfig = ( config: CONFIG2, ) => CONFIG1; /** * Function mapping from one concrete {@link NameToSignalId} to another {@link NameToSignalId} */ export type MapSignalIds = ( ids: T1, config: CONFIG, ) => T2; /** * Function mapping from one concrete {@link NameToEffectId} to another {@link NameToEffectId} */ export type MapEffectIds = ( ids: T1, config: CONFIG, ) => T2; /** * `AddSignalId` is the result type of adding a key in a {@link NameToSignalId} (also see the difference to {@link AddOrReplaceId}). * * ```ts * AddSignalId<{ a: EventId }, 'a', EventId -> { a: EventId } * AddSignalId<{ a: EventId }, 'b', EventId -> { a: EventId; b: EventId } * ``` * * @template T - concrete `NameToSignalIds` * @template N - string key * @template ID - concrete `SignalId` */ export type AddSignalId< T extends NameToSignalId, N extends keyof T, ID extends SignalId, > = T & { [K in N]: ID; }; /** * `AddOrReplaceId` is the result type of adding or replacing a key in a {@link NameToSignalId}. * * ```ts * AddOrReplaceId<{ a: EventId }, 'a', EventId -> { a: EventId } * AddOrReplaceId<{ a: EventId }, 'b', EventId -> { a: EventId; b: EventId } * ``` * * @typedef {object} AddOrReplaceId * @template T - concrete `NameToSignalIds` * @template N - string key * @template ID - concrete `SignalId` */ export type AddOrReplaceId< T extends NameToSignalId, N extends keyof T, ID extends SignalId, > = Omit & { [K in N]: ID; }; /** * `AddEffectId` is the result type of adding a key in a {@link NameToEffectId}. * * ```ts * AddEffectId<{ a: EffectId }, N, ID> -> { a: EffectId; [N]: ID } * ``` * * @typedef {object} AddEffectId * @template T - concrete `NameToEffectIds` * @template N - string key * @template ID - concrete `EffectId` */ export type AddEffectId< T extends NameToEffectId, N extends keyof T, ID extends EffectId, > = T & { [K in N]: ID; }; /** * `RenameId` is the result type of renaming a key in a {@link NameToSignalId}. * * ```ts * RenameId<{ [N1]: T[N1] }> -> { [N2]: T[N1] } * ``` * * @typedef {object} RenameId * @template T - concrete `NameToSignalIds` * @template N1 - string old key * @template N2 - string new key */ export type RenameId = Omit< T, N1 | N2 > & { [K in N2]: T[N1]; }; /** * `RenameEffectId` is the result type of renaming a key in a {@link NameToEffectId}. * * ```ts * RenameEffectId<{ [N1]: T[N1] }, N1, N2> -> { [N2]: T[N1] } * ``` * * @typedef {object} RenameEffectId * @template T - concrete `NameToEffectIds` * @template N1 - string old key * @template N2 - string new key */ export type RenameEffectId = Omit< T, N1 | N2 > & { [K in N2]: T[N1]; }; /** * `AssertNonExistingKey` is a helper type that returns `K`, if `K` does not extend `keyof T`. * If `K extends keyof T` (K is a key of the object T), then `never | Message` will be the result type. * The latter would result in a TS-error, if a function expects an argument as `AssertNonExistingKey`, saying "argument of type `` is not assignable to parameter of type ``" * * @typedef {object} AssertNonExistingKey - evaluates to K only if K is not in keyof T, else gives compile-time error including Message * @template K - string * @template T - object with string keys * @template Message - message to be used in assertion */ export type AssertNonExistingKey< K extends string, T extends { [key: string]: any }, Message extends string, > = (K & (K extends keyof T ? never : K)) | Message; /** * A `SignalsFactory` wraps a {@link SignalsBuild} function, allowing for simple {@link Signals} composition * and manipulation. * Via the `compose` method, two `SignalsFactories` can be easily composed to a new one. * Several convenience methods simplify re-mapping of `IN`, `OUT`, `CONFIG` and `EFF`. * Monadic `bind` and `fmap` methods can be used for more complex cases (though 90% of use-cases should be covered by * `compose`, `extendSetup`, `mapInput`, `mapOutput` and `mapConfig`). * `SignalsFactory` instances are immutable (persistent data structures), hence all methods return a new `SignalsFactory` instance. * (The whole purpose of this class is to provide immutable `SignalsBuild` composition) * * @class SignalsFactory */ export class SignalsFactory< IN extends NameToSignalId, OUT extends NameToSignalId, CONFIG extends Configuration = {}, EFF extends NameToEffectId = {}, > { /** * The constructor takes a pure function implementing SignalsBuild. * * @param {SignalsBuild} build - a pure function mapping from CONFIG to Signals * @constructor */ constructor(readonly build: SignalsBuild) {} /** * The `compose` method takes a second `SignalsFactory2` and returns a new `SignalsFactory` * that represents the composition of this `SignalsFactory` and the argument `SignalsFactory2`. * You can re-map the input, output, configuration and effects of the resulting composed `SignalsFactory` * by using `mapInput`, `mapOutput`, `mapConfig` and `mapEffects` correspondingly (as well as the many other convenience methods). * * @param {SignalsFactory} factory2 - a `SignalsFactory` * @returns {SignalsFactory} - the `SignalsFactory` resulting from the composition of this `SignalsFactory` and `SignalsFactory2` */ compose< IN2 extends NameToSignalId, OUT2 extends NameToSignalId, CONFIG2 extends Configuration, EFF2 extends NameToEffectId, >( factory2: SignalsFactory, ): ComposedFactory { const build: SignalsBuild< Merged, Merged, MergedConfiguration, Merged > = (config: MergedConfiguration) => { const s1 = this.build(config?.c1 ?? config); const s2 = factory2.build(config?.c2 ?? config); return { setup: (store: Store) => { s1.setup(store); s2.setup(store); }, input: merge(s1.input, s2.input), output: merge(s1.output, s2.output), effects: merge(s1.effects, s2.effects), }; }; return new SignalsFactory< Merged, Merged, MergedConfiguration, Merged >(build); } /** * This method implements monadic-bind for the `SignalsFactory`. * It takes as argument a pure function that implements `SignalsMapToFactory`, hence * a function taking `SignalsBuild` as argument and returning a `SignalsFactory`. * The result of `bind` is a new `SignalsFactory` that uses the `SignalsBuild::build` * of the factory returned from the mapper (hence the result of bind is not the same instance as returned by the mapper). * * @param {BindMapper} mapper - a pure function mapping from `SignalsBuild` to `SignalsFactory` * @returns {SignalsFactory} - a new `SignalsFactory` */ bind< IN2 extends NameToSignalId, OUT2 extends NameToSignalId, CONFIG2 extends Configuration, EFF2 extends NameToEffectId, >( mapper: BindMapper, ): SignalsFactory { const newBuild = (config: CONFIG2) => mapper(this.build).build(config); return new SignalsFactory(newBuild); } /** * This method implements the functor-fmap for the `SignalsFactory`. * It takes as argument a pure function that implements `BuildMapper`, hence * a function taking `SignalsBuild` as argument and returning a `SignalsBuild`. * The result of `fmap` is a new `SignalsFactory` that uses the `SignalsBuild` returned * from the mapper. * * @param {BuildMapper} mapper - a pure function mapping from `SignalsBuild` to `SignalsBuild` * @returns {SignalsFactory} - a new `SignalsFactory` */ fmap< IN2 extends NameToSignalId, OUT2 extends NameToSignalId, CONFIG2 extends Configuration, EFF2 extends NameToEffectId, >( mapper: BuildMapper, ): SignalsFactory { const newBuild = (config: CONFIG2) => mapper(this.build)(config); return new SignalsFactory(newBuild); } /** * The `extendSetup` method takes as argument a function that implements `ExtendSetup`. * It returns a new `SignalsFactory` of the same type as this `SignalsFactory`, but with a wrapped `SignalsBuild` that * extends the code executed in the `Signals` setup method by the provided code. * * @param {ExtendSetup} extend - a function extending the setup method of the `Signals` produced by the `SignalsBuild` of the resulting `SignalsFactory` * @returns {SignalsFactory} - a new `SignalsFactory` with extended store setup */ extendSetup(extend: ExtendSetup): SignalsFactory { return this.fmap(sb => config => { const s = sb(config); return { ...s, setup: (store: Store) => { s.setup(store); extend({ store, input: s.input, output: s.output, config, effects: s.effects }); }, }; }); } /** * The `connect` method offers an easy way to connect an output signal to an input signal. * It takes the name of an output-id (a key of `OUT`) as first argument and the name of an input-id (a key of `IN`) as second argument. * If keepInputId is set to true, it returns a new `SignalsFactory` of the same type as this `SignalsFactory`, * but with an extended setup logic, that connects the `Signal` that corresponds to the named output-id to a * new `Signal` corresponding to the named input-id. * If keepInputId is set to false, it also extends the setup logic to connect the corresponding signal-ids, but in addition * the specified input-id will be excluded from the `IN`-type of the resulting factory. * (also see `store.connect` documentation, as this is used under the hood) * Typescript will enforce that both names (keys) map to compatible signal ids, hence if `KIN` corresponds to `SignalId`, * then `KOUT` must correspond to `SignalId` (though one might be an `EventId` and the other a `BehaviorId`). * Even though you only use strings as arguments, this method is still type-safe and will not compile if you try to specify names of non-existing or incompatible ids. * * @param {KIN} inputName - a key of IN, where `IN[toName]` must be of type `Signal` * @param {KOUT} outputName - a key of OUT, where `OUT[outputName]` must be of type `Signal` * @param {boolean} keepInputId - if set to false, the input-id corresponding to toName will be removed from the resulting factories' `IN` * @returns {SignalsFactory} - a new `SignalsFactory` with the concrete type depending on the keepInputId argument */ connect< KIN extends keyof IN, KOUT extends keyof WithValueType>>, B extends boolean, >( outputName: KOUT, inputName: KIN, keepInputId: B, ): B extends true ? SignalsFactory : SignalsFactory, OUT, CONFIG, EFF> { const fnew: SignalsFactory = this.extendSetup( ({ store, input, output }) => { const fromId: SignalId = output[outputName] as SignalId; const toId: SignalId = input[inputName] as SignalId; store.connect(fromId, toId); }, ); const result = (keepInputId ? fnew : fnew.removeInputId(inputName)) as B extends true ? SignalsFactory : SignalsFactory, OUT, CONFIG, EFF>; return result; } /** * The `connectId` method is a more general version of the `connect` method. In contrast to `connect`, it does not take the name * of an `OUT` signal-id, but instead directly a signal-id (thus, some arbitrary signal-id can be used). * Everything else works like `connect`, so please see the corresponding documentation there. * * @param {KIN} inputName - a key of IN, where `IN[toName]` must be of type `Signal` * @param {ID} fromId - a `Signal` * @param {boolean} keepInputId - if set to false, the input-id corresponding to toName will be removed from the resulting factories' `IN` * @returns {SignalsFactory} - a new `SignalsFactory` with the concrete type depending on the keepInputId argument */ connectId>, B extends boolean>( fromId: ID, inputName: K, keepInputId: B, ): B extends true ? SignalsFactory : SignalsFactory, OUT, CONFIG, EFF> { const fnew: SignalsFactory = this.extendSetup(({ store, input }) => { const toId: SignalId = input[inputName] as SignalId; store.connect(fromId, toId); }); const result = (keepInputId ? fnew : fnew.removeInputId(inputName)) as B extends true ? SignalsFactory : SignalsFactory, OUT, CONFIG, EFF>; return result; } /** * The `connectObservable` method is even more general compared to the `connect` and `connectId` methods. In contrast to the previous two, * its first argument is a function that takes store, output and config as arguments and returns an observable. * For the other arguments, please see the documentation of the connect method. * * @param {KIN} inputName - a key of IN, where `IN[toName]` must be of type `Signal` * @param {function} sourceGetter - a function returning an `Observable` * @param {boolean} keepInputId - if set to false, the input-id corresponding to toName will be removed from the resulting factories' `IN` * @returns {SignalsFactory} - a new `SignalsFactory` with the concrete type depending on the keepInputId argument */ connectObservable< K extends keyof IN, S extends ToSignalIdValueType, O extends Observable, B extends boolean, >( sourceGetter: (args: SignalsFactoryArgs) => O, inputName: K, keepInputId: B, ): B extends true ? SignalsFactory : SignalsFactory, OUT, CONFIG, EFF> { const fnew: SignalsFactory = this.extendSetup(args => { const toId: SignalId = args.input[inputName] as SignalId; args.store.connectObservable(sourceGetter(args), toId); }); const result = (keepInputId ? fnew : fnew.removeInputId(inputName)) as B extends true ? SignalsFactory : SignalsFactory, OUT, CONFIG, EFF>; return result; } /** * The `mapConfig` method takes as argument a pure function that implements `MapConfig`. * It returns a new `SignalsFactory`. * * @param {MapConfig} mapper - a pure function mapping `CONFIG2` to `CONFIG` * @returns {SignalsFactory} - a new `SignalsFactory` with different `Configuration` type */ mapConfig( mapper: MapConfig, ): SignalsFactory { const build = (config: CONFIG2) => this.build(mapper(config)); return new SignalsFactory(build); } /** * The `mapInput` method takes as argument a pure function that implements `MapSignalIds`. * It returns a new `SignalsFactory`. * * @param {MapSignalIds} mapper - a pure function mapping from `IN` to `IN2` * @returns {SignalsFactory} - a new `SignalsFactory` with different input signals */ mapInput( mapper: MapSignalIds, ): SignalsFactory { return this.fmap(sb => config => { const s = sb(config); return { ...s, input: mapper(s.input, config), }; }); } /** * `addInputId` can be used as a short alternative to `mapInput`, if you want * to add just a single `SignalId` to the input signal ids. * * @template K - a concrete string to be used as key for the new `SignalId` * @template ID - a concrete `SignalId` type * @param {K} name - the name of the new `SignalId` * @param {function} idGetter - a function returning the new `SignalId` * @returns {SignalsFactory} - a new `SignalsFactory` with modified input signals */ addInputId>( name: AssertNonExistingKey, idGetter: (config: CONFIG) => ID, ): SignalsFactory, OUT, CONFIG, EFF> { return this.mapInput>((input, config) => ({ ...input, [name]: idGetter(config), })); } /** * `renameInputId` can be used as a short alternative to `mapInput`, if you want * to change the name of a single `SignalId` in the input signal ids. * * @template K1 - a concrete key of `IN` * @template K2 - the new name for `IN[K1]` * @param {K1} oldName - the old key * @param {K2} newName - the new key * @returns {SignalsFactory} - a new `SignalsFactory` with modified input signals */ renameInputId( oldName: K1, newName: K2, ): SignalsFactory, OUT, CONFIG, EFF> { return this.mapInput>(input => { const { [oldName]: mapId, ...rest } = input; const result = { ...rest, [newName]: mapId, }; return result as RenameId; }); } /** * `removeInputId` can be used as a short alternative to `mapInput`, if you want * to remove just a single `SignalId` from the input signal ids. * * @template K - a concrete `K` of the input signals of this factory * @param {K} name - the name of the `SignalId` to be removed * @returns {SignalsFactory} - a new `SignalsFactory` with modified input signals */ removeInputId(name: K): SignalsFactory, OUT, CONFIG, EFF> { return this.mapInput>(input => { const result = { ...input }; delete result[name]; return result; }); } /** * The `mapOutput` method takes as argument a pure function that implements `MapSignalIds`. * It returns a new `SignalsFactory`. * * @param {MapSignalIds} mapper - a pure function mapping from `OUT` to `OUT2` * @returns {SignalsFactory} - a new `SignalsFactory` with different output signals */ mapOutput( mapper: MapSignalIds, ): SignalsFactory { return this.fmap(sb => config => { const s = sb(config); return { ...s, output: mapper(s.output, config), }; }); } /** * `addOutputId` can be used as a short alternative to `mapOutput`, if you want * to add just a single `SignalId` to the output signal ids. * * @template K - a concrete string to be used as key for the new `SignalId` * @template ID - a concrete `SignalId` type * @param {K} name - the name of the new `SignalId` * @param {function} idGetter - a function returning the new `SignalId` * @returns {SignalsFactory} - a new `SignalsFactory` with modified output signals */ addOutputId>( name: AssertNonExistingKey, idGetter: (config: CONFIG) => ID, ): SignalsFactory, CONFIG, EFF> { return this.mapOutput>((output, config) => ({ ...output, [name]: idGetter(config), })); } /** * `renameOutputId` can be used as a short alternative to `mapOutput`, if you want * to change the name of a single `SignalId` in the output signal ids. * * @template K1 - a concrete key of `OUT` * @template K2 - the new name for `OUT[K1]` * @param {K1} oldName - the old key * @param {K2} newName - the new key * @returns {SignalsFactory} - a new `SignalsFactory` with modified output signals */ renameOutputId( oldName: K1, newName: K2, ): SignalsFactory, CONFIG, EFF> { return this.mapOutput>(output => { const { [oldName]: mapId, ...rest } = output; const result = { ...rest, [newName]: mapId, }; return result as RenameId; }); } /** * `removeOutputId` can be used as a short alternative to `mapOutput`, if you want * to remove just a single `SignalId` from the output signal ids. * * @template K - a concrete `K` of the output signals of this factory * @param {K} name - the name of the `SignalId` to be removed * @returns {SignalsFactory} - a new `SignalsFactory` with modified output signals */ removeOutputId(name: K): SignalsFactory, CONFIG, EFF> { return this.mapOutput>(output => { const result = { ...output }; delete result[name]; return result; }); } /** * `mapOutputBehavior` can be used to transfrom an output-behavior of type `TOLD` to a new behavior of type `TNEW`, * keeping the output-id-name. Thus, this is a short form for adding a new behavior based on an existing * output-behavior, then removing the ID of the existing one from output and finally, putting the ID of the * new behavior into output under the removed name of the existing behavior. * Hence, this transforms from `SignalsFactory` to `SignalsFactory` * with `OUT2` having the same keys as `OUT`, but `OUT2[KOUT]` having type `TNEW` and `OUT[KOUT]` having type `TOLD` * * @template KOUT - a concrete output key mapping to a `BehaviorId` * @template TNEW - value-type of the `OUT2[KOUT]` BehaviorId after mapping * @param {KOUT} outputName - the name of a `BehaviorId` from `OUT` * @param {function} mapper - a function returning `Observable` * @returns {SignalsFactory} - a new `SignalsFactory` with a modified output signal */ mapOutputBehavior>>( outputName: KOUT, mapper: ( old: Observable>, args: SignalsFactoryArgs, ) => Observable, ): SignalsFactory>, CONFIG, EFF> { return this.fmap>, CONFIG, EFF>(sb => config => { const s = sb(config); const newId = getDerivedId(); const oldId = s.output[outputName] as BehaviorId>; return { ...s, setup: store => { store.addDerivedState( newId, mapper(store.getBehavior(oldId), { store, input: s.input, output: s.output, config, effects: s.effects, }), ); s.setup(store); }, output: { ...s.output, [outputName]: newId, }, }; }); } /** * The `mapEffects` method takes as argument a pure function that implements `MapEffectIds`. * It returns a new `SignalsFactory`. * * @param {MapSignalIds} mapper - a pure function mapping from `EFF` to `EFF2` * @returns {SignalsFactory} - a new `SignalsFactory` with different effect-ids */ mapEffects( mapper: MapEffectIds, ): SignalsFactory { return this.fmap(sb => config => { const s = sb(config); return { ...s, effects: mapper(s.effects, config), }; }); } /** * `addEffectId` can be used as a short alternative to `mapEffects`, if you want * to add just a single `EffectId` to the effect-ids. * * @template K - a concrete string to be used as key for the new `EffectId` * @template ID - a concrete `EffectId` type * @param {K} name - the name of the new `EffectId` * @param {function} idGetter - a function returning the new `EffectId` * @returns {SignalsFactory} - a new `SignalsFactory` with modified effect-ids */ addEffectId>( name: AssertNonExistingKey, idGetter: (config: CONFIG) => ID, ): SignalsFactory> { return this.mapEffects>((effects, config) => ({ ...effects, [name]: idGetter(config), })); } /** * `removeEffectId` can be used as a short alternative to `mapEffects`, if you want * to remove just a single `EffectId` from the effect-ids. * * @template K - a concrete key of the effect-ids of this factory * @param {K} name - the name of in `NameToEffectIds` to be removed * @returns {SignalsFactory} - a new `SignalsFactory` with modified effect-ids */ removeEffectId(name: K): SignalsFactory> { return this.mapEffects>(effects => { const result = { ...effects }; delete result[name]; return result; }); } /** * `useExistingEffect` can be used to get a factory that uses an existing effect * as the effect referenced by a given effect-id generated by this factory. * * @template K - a concrete key of the effect-ids of this factory * @param {K} name - a name from this factory's effects, mapping to a concrete `EffectId` * @param {function} idGetter - a function, getting the config as argument and returning the `EffectId` of an existing effect that should be used for the `EffectId` specified by `K`. * @param {B} keepEffectId - specifies whether the resulting `SignalsFactory` should still produce the `EffectId` referenced by `K`. * @returns {SignalsFactory} - a new `SignalsFactory` with the concrete type depending on the `keepInputId` argument */ useExistingEffect< InputType, ResultType, K extends KeysOfValueType>, B extends boolean, >( name: K, idGetter: (config: CONFIG) => EffectId, keepEffectId: B, ): B extends true ? SignalsFactory : SignalsFactory> { const result = this.extendSetup(({ store, config, effects }) => { store .getEffect(idGetter(config)) .pipe(take(1)) .subscribe(effect => { store.addEffect(effects[name] as EffectId, effect); }); }); return (keepEffectId ? result : result.removeEffectId(name)) as B extends true ? SignalsFactory : SignalsFactory>; } /** * `renameEffectId` can be used as a short alternative to `mapEffects`, if you want * to change the name of a single `EffectId` in the effect-ids. * * @template K1 - a concrete key of `EFF` * @template K2 - the new name for `EFF[K1]` * @param {K1} oldName - the old key * @param {K2} newName - the new key * @returns {SignalsFactory} - a new `SignalsFactory` with modified effect-ids */ renameEffectId( oldName: K1, newName: K2, ): SignalsFactory> { return this.mapEffects>(effects => { const { [oldName]: mapId, ...rest } = effects; const result = { ...rest, [newName]: mapId, }; return result as RenameEffectId; }); } }