effector
Version:
Business logic with ease
1,524 lines (1,424 loc) • 116 kB
TypeScript
/**
* This tuple type is intended for use as a generic constraint to infer concrete
* tuple type of ANY length.
*
* @see https://github.com/krzkaczor/ts-essentials/blob/a4c2485bc3f37843267820ec552aa662251767bc/lib/types.ts#L169
*/
type Tuple<T = unknown> = [T?, ...T[]]
type RoTuple<T = unknown> = readonly [T?, ...T[]]
/**
* Non inferential type parameter usage. NoInfer in source and in return of fn helps with
detecting loose objects against target type.
*
* @see https://github.com/microsoft/TypeScript/issues/14829#issuecomment-504042546
*/
type NoInfer<T> = [T][T extends any ? 0 : never]
/**
* Generic Json type
*/
type Json =
| null
| undefined
| boolean
| string
| number
| Json[]
| {[k: string]: Json}
// Type for extention purpose. Represents combinable sample source.
export type Combinable = {[key: string]: Store<any>} | Tuple<Store<any>>
// Helper type, which unwraps combinable sample source value.
export type GetCombinedValue<T> = Show<{
[K in keyof T]: T[K] extends Store<infer U> ? U : never
}>
export type StoreValue<T> = T extends Store<infer S> ? S : never
export type EventPayload<T> = T extends Event<infer P> ? P : never
export type UnitValue<T> = T extends Unit<infer V> ? V : never
export type EffectParams<FX extends Effect<any, any, any>> = FX extends Effect<
infer P,
any,
any
>
? P
: never
export type EffectResult<FX extends Effect<any, any, any>> = FX extends Effect<
any,
infer D,
any
>
? D
: never
export type EffectError<FX extends Effect<any, any, any>> = FX extends Effect<
any,
any,
infer E
>
? E
: never
// Taken from the source code of typescript 4.5. Remove when we separate types for different versions
/**
* Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`.
*/
type Awaited<T> = T extends null | undefined
? T // special case for `null | undefined` when not in `--strictNullChecks` mode
: T extends object // `await` only unwraps object types with a callable then. Non-object types are not unwrapped.
? T extends {then(onfulfilled: infer F): any} // thenable, extracts the first argument to `then()`
? F extends (value: infer V) => any // if the argument to `then` is callable, extracts the argument
? Awaited<V> // recursively unwrap the value
: never // the argument to `then` was not callable.
: T // argument was not an object
: T // non-thenable
type OptionalParams<Args extends any[]> =
Args['length'] extends 0 // does handler accept 0 arguments?
? void // works since TS v3.3.3
: 0 | 1 extends Args['length'] // is the first argument optional?
/**
* Applying `infer` to a variadic arguments here we'll get `Args` of
* shape `[T]` or `[T?]`, where T(?) is a type of handler `params`.
* In case T is optional we get `T | undefined` back from `Args[0]`.
* We lose information about argument's optionality, but we can make it
* optional again by appending `void` type, so the result type will be
* `T | undefined | void`.
*
* The disadvantage of this method is that we can't restore optonality
* in case of `params?: any` because in a union `any` type absorbs any
* other type (`any | undefined | void` becomes just `any`). And we
* have similar situation also with the `unknown` type.
*/
? Args[0] | void
: Args[0]
type EffectByHandler<FN extends Function, Fail> = FN extends (...args: infer Args) => infer Done
? Effect<OptionalParams<Args>, Awaited<Done>, Fail>
: never
export const version: string
export type kind = 'store' | 'event' | 'effect' | 'domain' | 'scope'
export type Observer<A> = {
readonly next?: (value: A) => void
//error(err: Error): void
//complete(): void
}
export type Observable<T> = {
subscribe: (observer: Observer<T>) => Subscription
}
export type Subscription = {
(): void
unsubscribe(): void
}
export interface Unit<T> {
readonly kind: kind
readonly __: T
}
export interface UnitTargetable<T> extends Unit<T> {
readonly targetable: true
}
export type CompositeName = {
shortName: string
fullName: string
path: Array<string>
}
/**
* This is a workaround for https://github.com/microsoft/TypeScript/issues/35162
*
* The problem was that we couldn't use guard as sample's clock parameter because
* sample's clock-related generic inferred as `unknown` in cases when guard returned
* `Event<T>`. This happens because `Event` has a callable signature. With `Unit<T>`
* as the return type we won't see any problems.
*/
type EventAsReturnType<Payload> = any extends Payload ? Event<Payload> : never
type EventCallableAsReturnType<Payload> = any extends Payload ? EventCallable<Payload> : never
/**
* Event you can subscribe to.
* It represents a user action, a step in the application process, a command to execute, or an intention to make modifications, among other things.
*/
export interface Event<Payload> extends Unit<Payload> {
kind: "event"
map<T>(fn: (payload: Payload) => T): EventAsReturnType<T>
filter<T extends Payload>(config: {
fn(payload: Payload): payload is T
}): EventAsReturnType<T>
filter(config: {fn(payload: Payload): boolean}): EventAsReturnType<Payload>
filterMap<T>(fn: (payload: Payload) => T | undefined): EventAsReturnType<T>
watch(watcher: (payload: Payload) => any): Subscription
subscribe(observer: Observer<Payload>): Subscription
/**
* @deprecated use .compositeName.fullName instead
*/
getType(): string
compositeName: CompositeName
sid: string | null
shortName: string
}
/**
* The function you can call to trigger an event.
*/
export interface EventCallable<Payload> extends Event<Payload>, UnitTargetable<Payload> {
kind: "event"
(payload: Payload): Payload
(this: IfUnknown<Payload, void, Payload extends void ? void : `Error: Expected 1 argument, but got 0`>, payload?: Payload): void
prepend<Before = void>(fn: (_: Before) => Payload): EventCallable<Before>
}
/**
* Container for (possibly async) side effects
*/
export interface Effect<Params, Done, Fail = Error> extends UnitTargetable<Params> {
kind: "effect"
(params: Params): Promise<Done>
readonly done: Event<{params: Params; result: Done}>
readonly doneData: Event<Done>
readonly fail: Event<{params: Params; error: Fail}>
readonly failData: Event<Fail>
readonly finally: Event<
| {
status: 'done'
params: Params
result: Done
}
| {
status: 'fail'
params: Params
error: Fail
}
>
readonly use: {
(handler: (params: Params) => Promise<Done> | Done): Effect<
Params,
Done,
Fail
>
getCurrent(): (params: Params) => Promise<Done>
}
pending: Store<boolean>
inFlight: Store<number>
watch(watcher: (payload: Params) => any): Subscription
filter<T extends Params>(config: {
fn(payload: Params): payload is T
}): EventAsReturnType<T>
filter(config: {fn(payload: Params): boolean}): EventAsReturnType<Params>
filterMap<T>(fn: (payload: Params) => T | undefined): EventAsReturnType<T>
map<T>(fn: (params: Params) => T): EventAsReturnType<T>
prepend<Before>(fn: (_: Before) => Params): EventCallable<Before>
subscribe(observer: Observer<Params>): Subscription
getType(): string
compositeName: CompositeName
sid: string | null
shortName: string
}
type InferValueFromTupleOfUnits<T extends Tuple<Unit<any>>> =
T[number] extends Unit<infer R> ? R : never
type InferValueFromTupleOfUnitTargetables<T extends Tuple<UnitTargetable<any>>> =
T[number] extends UnitTargetable<infer R>? R : never
export interface Store<State> extends Unit<State> {
kind: "store"
map<T>(fn: (state: State) => T, config?: {skipVoid?: boolean}): Store<T>
updates: Event<State>
getState(): State
subscribe(listener: Observer<State> | ((state: State) => any)): Subscription
watch<E>(watcher: (state: State, payload: undefined) => any): Subscription
watch<E>(
trigger: Unit<E>,
watcher: (state: State, payload: E) => any,
): Subscription
/**
* @deprecated use js pipe instead
*/
thru<U>(fn: (store: Store<State>) => U): U
defaultState: State
compositeName: CompositeName
shortName: string
sid: string | null
}
/**
* Hacky way to force TS perform checks against unsafe widening
*/
interface StoreValueType<X> {
_: X
(type: X): void
}
export interface StoreWritable<State> extends Store<State>, UnitTargetable<State> {
kind: "store"
readonly ____: StoreValueType<State>
on<E>(
trigger: Unit<E>,
reducer: (state: State, payload: E) => State | void,
): this
on<E>(
triggers: Unit<E>[],
reducer: (state: State, payload: E) => State | void,
): this
on<E extends Tuple<Unit<any>>>(
triggers: E,
reducer: (state: State, payload: InferValueFromTupleOfUnits<E>) => State | void,
): this
off(trigger: Unit<any>): this
reset(...triggers: Array<Unit<any>>): this
reset(triggers: Array<Unit<any>>): this
reinit: EventCallable<void>
}
interface InternalStore<State> extends StoreWritable<State> {
setState(state: State): void
}
export const is: {
unit(obj: unknown): obj is Unit<any> | UnitTargetable<any>
store<O, T>(
obj: O | Unit<T> | UnitTargetable<T>,
): obj is typeof obj extends Unit<T>
? Store<T> | StoreWritable<T>
: Store<any> | StoreWritable<any>
event<O, T>(
obj: O | Unit<T> | UnitTargetable<T>
): obj is typeof obj extends Unit<T>
? Event<T> | EventCallable<T>
: Event<any> | EventCallable<any>
effect<O, T, P, F>(
obj: O | Effect<T, P, F>
): obj is Effect<T, P, F>
domain(obj: unknown): obj is Domain
scope(obj: unknown): obj is Scope
attached<E extends Effect<any, any, any>>(obj: unknown): obj is E
targetable<T>(obj: Unit<T>): obj is UnitTargetable<T>
}
/**
* A way to group and process events, stores and effects. Useful for logging and assigning a reset trigger to many effects.
* Domain is notified via onCreateEvent, onCreateStore, onCreateEffect, onCreateDomain methods when events, stores, effects, or nested domains are created
*/
export class Domain implements Unit<any> {
readonly kind: kind
readonly __: any
onCreateEvent(hook: (newEvent: EventCallable<unknown>) => any): Subscription
onCreateEffect(
hook: (newEffect: Effect<unknown, unknown, unknown>) => any,
): Subscription
onCreateStore(
hook: (newStore: InternalStore<unknown>) => any,
): Subscription
onCreateDomain(hook: (newDomain: Domain) => any): Subscription
event<Payload = void>(name?: string): EventCallable<Payload>
event<Payload = void>(config: {name?: string; sid?: string}): EventCallable<Payload>
createEvent<Payload = void>(name?: string): EventCallable<Payload>
createEvent<Payload = void>(config: {
name?: string
sid?: string
}): EventCallable<Payload>
effect<FN extends Function>(handler: FN): EffectByHandler<FN, Error>
effect<Params, Done, Fail = Error>(
handler: (params: Params) => Done | Promise<Done>,
): Effect<Params, Done, Fail>
effect<FN extends Function, Fail>(handler: FN): EffectByHandler<FN, Fail>
effect<Params, Done, Fail = Error>(
name?: string,
config?: {
handler?: (params: Params) => Promise<Done> | Done
sid?: string
},
): Effect<Params, Done, Fail>
effect<Params, Done, Fail = Error>(config: {
handler?: (params: Params) => Promise<Done> | Done
sid?: string
name?: string
}): Effect<Params, Done, Fail>
createEffect<FN extends Function>(handler: FN): EffectByHandler<FN, Error>
createEffect<Params, Done, Fail = Error>(
handler: (params: Params) => Done | Promise<Done>,
): Effect<Params, Done, Fail>
createEffect<FN extends Function, Fail>(handler: FN): EffectByHandler<FN, Fail>
createEffect<FN extends Function>(config: {
name?: string
handler: FN
sid?: string
}): EffectByHandler<FN, Error>
createEffect<Params, Done, Fail = Error>(
name?: string,
config?: {
handler?: (params: Params) => Promise<Done> | Done
sid?: string
},
): Effect<Params, Done, Fail>
createEffect<Params, Done, Fail = Error>(config: {
handler?: (params: Params) => Promise<Done> | Done
sid?: string
name?: string
}): Effect<Params, Done, Fail>
domain(name?: string): Domain
createDomain(name?: string): Domain
store<State, SerializedState extends Json = Json>(
defaultState: State,
config?: {
name?: string
sid?: string
updateFilter?: (update: State, current: State) => boolean
serialize?:
| 'ignore'
| {
write: (state: State) => SerializedState
read: (json: SerializedState) => State
}
},
): StoreWritable<State>
createStore<State, SerializedState extends Json = Json>(
defaultState: State,
config?: {
name?: string
sid?: string
updateFilter?: (update: State, current: State) => boolean
serialize?:
| 'ignore'
| {
write: (state: State) => SerializedState
read: (json: SerializedState) => State
}
skipVoid?: boolean
},
): StoreWritable<State>
sid: string | null
compositeName: CompositeName
shortName: string
getType(): string
history: {
domains: Set<Domain>
stores: Set<StoreWritable<any>>
effects: Set<Effect<any, any, any>>
events: Set<EventCallable<any>>
}
}
export type ID = string
export type StateRefOp =
| {type: 'map'; from?: StateRef; fn?: (value: any) => any}
| {type: 'field'; from: StateRef; field: string}
export type StateRef = {
id: ID
current: any
type?: 'list' | 'shape'
before?: StateRefOp[]
noInit?: boolean
sid?: string
}
export type Stack = {
value: any
a: any
b: any
parent?: Stack
node: Node
page?: any
scope?: Scope
meta?: Record<string, any>
}
type BarrierPriorityTag = 'barrier' | 'sampler' | 'effect'
type FromValue = {
from: 'value'
store: any
}
type FromStore = {
from: 'store'
store: StateRef
}
type FromRegister = {
from: 'a' | 'b' | 'stack'
}
type ToRegister = {
to: 'a' | 'b' | 'stack'
}
type ToStore = {
to: 'store'
target: StateRef
}
type MoveCmd<Data> = {
id: ID
type: 'mov'
data: Data
order?: {
priority: BarrierPriorityTag
barrierID?: number
}
}
export type Cmd =
| Compute
| Mov
type MovValReg = MoveCmd<FromValue & ToRegister>
type MovValStore = MoveCmd<FromValue & ToStore>
type MovStoreReg = MoveCmd<FromStore & ToRegister>
type MovStoreStore = MoveCmd<FromStore & ToStore>
type MovRegReg = MoveCmd<FromRegister & ToRegister>
type MovRegStore = MoveCmd<FromRegister & ToStore>
export type Mov =
| MovValReg
| MovValStore
| MovStoreReg
| MovStoreStore
| MovRegReg
| MovRegStore
export type Compute = {
id: ID
type: 'compute'
data: {
fn?: (data: any, scope: {[key: string]: any}, reg: Stack) => any
safe: boolean
filter: boolean
pure: boolean
}
order?: {
priority: BarrierPriorityTag
barrierID?: number
}
}
export type Node = {
id: ID
next: Array<Node>
seq: Array<Cmd>
scope: {[field: string]: any}
meta: {[field: string]: any}
family: {
type: 'regular' | 'crosslink' | 'domain'
links: Node[]
owners: Node[]
}
}
export const step: {
compute(data: {
fn?: (data: any, scope: {[key: string]: any}, stack: Stack) => any
batch?: boolean
priority?: BarrierPriorityTag | false
safe?: boolean
filter?: boolean
pure?: boolean
}): Compute
filter(data: {
fn: (data: any, scope: {[field: string]: any}, stack: Stack) => boolean
pure?: boolean
}): Compute
run(data: {fn: (data: any, scope: {[field: string]: any}, stack: Stack) => any}): Compute
mov(data: {
from?: 'value' | 'store' | 'stack' | 'a' | 'b'
to?: 'stack' | 'a' | 'b' | 'store'
store?: StateRef
target?: StateRef
batch?: boolean
priority?: BarrierPriorityTag | false
}): Mov
}
/* `forward` types */
type ForwardTarget = UnitTargetable<unknown> | ReadonlyArray<UnitTargetable<unknown>>
type CleanSingleTarget<
Target extends UnitTargetable<unknown>,
Clock,
> = Target extends UnitTargetable<infer T>
? T extends void
? UnitTargetable<unknown>
: T extends Clock
? UnitTargetable<T>
// Needed to force typecheck
: UnitTargetable<Clock>
: never
type CleanTarget<
Target extends ForwardTarget,
From,
> = Target extends UnitTargetable<any>
? CleanSingleTarget<Target, From>
: {
[K in keyof Target]: Target[K] extends UnitTargetable<unknown>
? CleanSingleTarget<Target[K], From>
: never
}
/**
* Method to create connection between units in a declarative way. Sends updates from one set of units to another
* @deprecated use `sample({clock, target})` instead
*/
export function forward<From, T extends ForwardTarget>(opts: {
/**
* By default TS picks "best common type" `T` between `from` and `to` arguments.
* This lets us forward from `string | number` to `string` for instance, and
* this is wrong.
*
* Fortunately we have a way to disable such behavior. By adding `& {}` to some
* generic type we tell TS "do not try to infer this generic type from
* corresponding argument type".
*
* Generic `T` won't be inferred from `from` any more. Forwarding from "less
* strict" to "more strict" will produce an error as expected.
*
* @see https://www.typescriptlang.org/docs/handbook/type-inference.html#best-common-type
*/
from: Unit<From & {}>
to: CleanTarget<T, From>
}): Subscription
/**
* Method to create connection between units in a declarative way. Sends updates from one set of units to another
* @deprecated use `sample({clock, target})` instead
*/
export function forward(opts: {
from: Unit<any>
to: ReadonlyArray<UnitTargetable<void>>
}): Subscription
/**
* Method to create connection between units in a declarative way. Sends updates from one set of units to another
* @deprecated use `sample({clock, target})` instead
*/
export function forward(opts: {
from: ReadonlyArray<Unit<any>>
to: ReadonlyArray<UnitTargetable<void>>
}): Subscription
/**
* Method to create connection between units in a declarative way. Sends updates from one set of units to another
* @deprecated use `sample({clock, target})` instead
*/
export function forward(opts: {
from: ReadonlyArray<Unit<any>>
to: UnitTargetable<void>
}): Subscription
/**
* Method to create connection between units in a declarative way. Sends updates from one set of units to another
* @deprecated use `sample({clock, target})` instead
*/
export function forward<To, From extends To>(opts: {
from: ReadonlyArray<Unit<From>>
to: UnitTargetable<To> | ReadonlyArray<UnitTargetable<To>>
}): Subscription
// Allow `* -> void` forwarding (e.g. `string -> void`).
/**
* Method to create connection between units in a declarative way. Sends updates from one set of units to another
* @deprecated use `sample({clock, target})` instead
*/
export function forward(opts: {from: Unit<any>; to: UnitTargetable<void>}): Subscription
// Do not remove the signature below to avoid breaking change!
/**
* Method to create connection between units in a declarative way. Sends updates from one set of units to another
* @deprecated use `sample({clock, target})` instead
*/
export function forward<To, From extends To>(opts: {
from: Unit<From>
to: UnitTargetable<To> | ReadonlyArray<UnitTargetable<To>>
}): Subscription
/**
* Merges array of units (events, effects or stores), returns a new event, which fires upon trigger of any of given units
* @param units array of units to be merged
*/
export function merge<T>(units: ReadonlyArray<Unit<T>>): EventAsReturnType<T>
/**
* Merges array of units (events, effects or stores), returns a new event, which fires upon trigger of any of given units
* @param units array of units to be merged
*/
export function merge<T extends ReadonlyArray<Unit<any>>>(
units: T,
): T[number] extends Unit<infer R> ? Event<R> : never
/**
* Method for destroying units and graph nodes. Low level tool, usually absent in common applications
* @param unit unit to be erased
* @param opts optional configuration object
*/
export function clearNode(unit: Unit<any> | Node, opts?: {deep?: boolean}): void
/**
* Method to create a new graph node. Low level tool, usually absent in common applications
*/
export function createNode(opts?: {
node?: Array<Cmd | false | void | null>
parent?: Array<Unit<any> | Node>
child?: Array<Unit<any> | Node>
scope?: {[field: string]: any}
meta?: {[field: string]: any}
family?: {
type?: 'regular' | 'crosslink' | 'domain'
owners?: Unit<any> | Node | Array<Unit<any> | Node>
links?: Unit<any> | Node | Array<Unit<any> | Node>
}
regional?: boolean
}): Node
/**
* Allows to directly start computation from given unit or graph node. Low level tool, usually absent in common applications
* @param unit unit or graph node to launch
* @param payload data to pass to computation
*/
export function launch<T>(unit: Unit<T> | Node, payload: T): void
/**
* Allows to directly start computation from given unit or graph node. Low level tool, usually absent in common applications
* @param config configuration object
*/
export function launch<T>(config: {
target: Unit<T> | Node
params: T
defer?: boolean
page?: any
scope?: Scope
meta?: Record<string, any>
}): void
/**
* Allows to directly start computation from given unit or graph node. Low level tool, usually absent in common applications
* @param config configuration object
*/
export function launch(config: {
target: Array<Unit<any> | Node>
params: any[]
defer?: boolean
page?: any
scope?: Scope
meta?: Record<string, any>
}): void
/**
* Method to create an event subscribed to given observable
* @param observable object with `subscribe` method, e.g. rxjs stream or redux store
*/
export function fromObservable<T>(observable: unknown): Event<T>
/**
* Creates an event
*/
export function createEvent<E = void>(eventName?: string): EventCallable<E>
/**
* Creates an event
*/
export function createEvent<E = void>(config: {
name?: string
sid?: string
domain?: Domain
}): EventCallable<E>
/**
* Creates an effect
* @param handler function to handle effect calls
*/
export function createEffect<FN extends Function>(handler: FN): EffectByHandler<FN, Error>
/**
* Creates an effect
* @param handler function to handle effect calls
*/
export function createEffect<Params, Done, Fail = Error>(
handler: (params: Params) => Done | Promise<Done>,
): Effect<Params, Done, Fail>
/**
* Creates an effect
* @param handler function to handle effect calls
*/
export function createEffect<FN extends Function, Fail>(handler: FN): EffectByHandler<FN, Fail>
/**
* Creates an effect
*/
export function createEffect<FN extends Function>(name: string, config: {
handler: FN
sid?: string
domain?: Domain
}): EffectByHandler<FN, Error>
/**
* Creates an effect
*/
export function createEffect<Params, Done, Fail = Error>(
effectName?: string,
config?: {
handler?: (params: Params) => Promise<Done> | Done
sid?: string
domain?: Domain
},
): Effect<Params, Done, Fail>
/**
* Creates an effect
*/
export function createEffect<FN extends Function>(config: {
name?: string
handler: FN
sid?: string
domain?: Domain
}): EffectByHandler<FN, Error>
/**
* Creates an effect
*/
export function createEffect<Params, Done, Fail = Error>(config: {
name?: string
handler?: (params: Params) => Promise<Done> | Done
sid?: string
domain?: Domain
}): Effect<Params, Done, Fail>
/**
* Creates a store
* @param defaultState default state
* @param config optional configuration object
*/
export function createStore<State, SerializedState extends Json = Json>(
defaultState: State,
config?: {
skipVoid?: boolean;
name?: string;
sid?: string
updateFilter?: (update: State, current: State) => boolean
serialize?:
| 'ignore'
| {
write: (state: State) => SerializedState
read: (json: SerializedState) => State
}
domain?: Domain;
},
): StoreWritable<State>
export function setStoreName<State>(store: Store<State>, name: string): void
type UnionToIntersection<Union> = (
Union extends any ? (k: Union) => void : never
) extends (k: infer intersection) => void
? intersection
: never;
type GetUnionLast<Union> = UnionToIntersection<
Union extends any ? () => Union : never
> extends () => infer Last
? Last
: never;
/**
* Chooses one of the cases by given conditions. It "splits" source unit into several events, which fires when payload matches their conditions.
* Works like pattern matching for payload values and external stores
* @param source unit which will trigger computation in split
* @param match object with matching functions which allows to trigger one of created events
*/
export function split<
S,
Match extends {[name: string]: (payload: S) => boolean}
>(
source: Unit<S>,
match: Match,
): Show<{
[K in keyof Match]: Match[K] extends (p: any) => p is infer R
? Event<R>
: Event<S>
} & {__: Event<S>}>
type MatchConstraint<Source> =
Unit<any>
| ((p: UnitValue<Source>) => void)
| Record<string, ((p: UnitValue<Source>) => boolean) | Store<boolean>>;
type CaseRecord<Keys extends PropertyKey = string> = Partial<Record<Keys | '__', UnitTargetable<any> | RoTuple<UnitTargetable<any>>>>;
/**
* Chooses one of cases by given conditions. It "splits" source unit into several targets, which fires when payload matches their conditions.
* Works like pattern matching for payload values and external units
*/
export function split<
Clock extends Unit<any> | RoTuple<Unit<any>>,
Source extends Unit<any>,
Match extends MatchConstraint<Source>,
Cases extends CaseRecord<InferMatchKeys<Match>>,
>(
config: SplitConfig<Clock, Source, Match, Cases>
): void;
type SplitConfig<
Clock,
Source,
Match extends MatchConstraint<Source>,
Cases extends CaseRecord<InferMatchKeys<Match>>
> = Exclude<keyof Cases, '__'> extends InferMatchKeys<Match>
? TypeOfMatch<Match> extends 'record'
? SplitImpl<Clock, Source, Match, Cases>
: TypeOfMatch<Match> extends 'unit'
? SplitImpl<Clock, Source, Match, Cases>
: TypeOfMatch<Match> extends 'fn'
? SplitImpl<Clock, Source, Match, Cases>
: {clock?: Clock; source: Source; match: Match; cases: Cases}
: {
clock?: Clock;
source: Source;
match: RebuildMatch<Source, Match, Cases>;
cases: { [K in keyof Cases as K extends InferMatchKeys<Match> | '__' ? K : never]: Cases[K] };
};
type InferMatchKeys<Match> = Match extends Unit<infer Keys>
? Keys extends PropertyKey ? Keys : never
: Match extends (source: any) => infer Keys
? Keys extends PropertyKey ? Keys : never
: Match extends Record<string, ((source: any) => boolean) | Store<boolean>>
? keyof Match
: never;
type TypeOfMatch<Match> =
Match extends Unit<any>
? 'unit'
: Match extends (s: any) => void
? 'fn'
: Match extends Record<string, ((p: any) => void) | Store<boolean>>
? 'record'
: never;
type RebuildMatch<
Source,
Match,
Cases,
Keys extends PropertyKey = Exclude<keyof Cases, '__'>
> =
Match extends Unit<any>
? Unit<Keys>
: Match extends (p: UnitValue<Source>) => void
? (p: UnitValue<Source>) => Keys
: Match extends Record<string, ((p: UnitValue<Source>) => boolean) | Store<boolean>>
? { [K in Keys]: K extends keyof Match ? Match[K] : (p: UnitValue<Source>) => boolean | Store<boolean> }
: never;
type SplitImpl<
Clock,
Source,
Match,
Cases
> = MatchCasesIsAssignable<Source, Match, Cases> extends infer AssignableDict
? AssignableDict extends Record<string, 'yes'>
? {
clock?: Clock;
source: Source;
match: Match;
cases: Cases
}
: MatchHasInference<Source, Match> extends 'yes'
? {
clock?: Clock;
source: Source;
match: RebuildMatchInference<Source, Match, AssignableDict>;
cases: Show<RebuildCases<Source, Match, Cases>>;
}
: {
clock?: Clock;
source: RebuildSource<Source, Cases>;
match: Match;
cases: Show<RebuildCases<Source, Match, Cases>>;
}
: never;
type MatchValueReader<Match, K, Source> =
K extends keyof Match
? Match[K] extends (src: any) => src is infer R
? UnitValue<Source> extends R
? UnitValue<Source>
: R
: UnitValue<Source>
: UnitValue<Source>;
type MatchHasInference<Source, Match> =
Match extends Record<string, ((p: UnitValue<Source>) => boolean) | Store<boolean>>
? 'yes' extends {
[K in keyof Match]: UnitValue<Source> extends MatchValueReader<Match, K, Source> ? 'no' : 'yes'
}[keyof Match] ? 'yes' : 'no'
: 'no';
type MatchCasesIsAssignable<
Source,
Match,
Cases
> =
{
[K in keyof Cases]: IfCaseAssignableToValue<Cases[K], MatchValueReader<Match, K, Source>>
}
type IfValidCaseValue<CaseValue, MatchValue, Y, N> =
WhichType<CaseValue> extends 'void' | 'unknown'
? Y
: IfAssignable<MatchValue, CaseValue, Y, N>;
type IfCaseAssignableToValue<Case, Value> =
Case extends UnitTargetable<any>
? IfValidCaseValue<UnitValue<Case>, Value, 'yes', ['no', UnitValue<Case>]>
: Case extends RoTuple<UnitTargetable<any>>
? IsCaseAssignableToValueLoop<Case, Value>
: never;
type IsCaseAssignableToValueLoop<Cases extends RoTuple<Unit<any>>, Value> =
Cases extends readonly [infer Case, ...infer Rest]
? Rest extends readonly any[]
? IfValidCaseValue<UnitValue<Case>, Value, 'yes', 'no'> extends 'yes'
? IsCaseAssignableToValueLoop<Rest, Value>
: ['no', UnitValue<Case>]
: never
: 'yes';
type RebuildMatchInference<
Source,
Match,
AssignableDict
> = Match extends Record<string, ((p: UnitValue<Source>) => boolean) | Store<boolean>>
? {
[K in keyof Match]: K extends keyof AssignableDict
? AssignableDict[K] extends ['no', infer Value]
? Value extends UnitValue<Source>
? (source: UnitValue<Source>) => source is Value
: never
: Match[K]
: Match[K]
}
: never;
type RebuildCases<Source, Match, Cases> = {
[K in keyof Cases]: K extends InferMatchKeys<Match> | '__'
? RebuildCase<MatchValueReader<Match, K, Source>, Cases[K]>
: Cases[K];
}
type RebuildCase<MatchValue, Case> = Case extends UnitTargetable<infer CaseValue>
? IfValidCaseValue<CaseValue, MatchValue, Case, UnitTargetable<MatchValue>>
: Case extends RoTuple<UnitTargetable<any>>
? RebuildCaseLoop<Case, MatchValue>
: never;
type RebuildCaseLoop<Cases extends readonly any[], Value, Result extends readonly any[] = []> =
Cases extends readonly [infer Case, ...infer Rest]
? Rest extends readonly any[]
? RebuildCaseLoop<
Rest,
Value,
[
...Result,
IfValidCaseValue<UnitValue<Case>, Value, Case, UnitTargetable<Value>>
]
>
: Result
: Result;
type RebuildSource<Source, Cases> =
{ [K in keyof Cases]: GetFirstUnassignableCase<UnitValue<Source>, Cases[K]> } extends infer Values
? Values extends Record<string, never>
? Source
: { [K in keyof Values as [Values[K]] extends [never] ? never : K]: Values[K] } extends infer InvalidValues
? Unit<GetUnionLast<InvalidValues[keyof InvalidValues]>>
: never
: never;
type GetFirstUnassignableCase<SourceValue, Case> =
Case extends UnitTargetable<infer CaseValue>
? IfValidCaseValue<CaseValue, SourceValue, SourceValue, CaseValue>
: Case extends RoTuple<UnitTargetable<any>>
? GetFirstUnassignableLoop<Case, SourceValue>
: never;
type GetFirstUnassignableLoop<Cases extends RoTuple<Unit<any>>, Value> =
Cases extends readonly [infer Case, ...infer Rest]
? Rest extends readonly any[]
? IfValidCaseValue<UnitValue<Case>, Value, 'yes', 'no'> extends 'yes'
? GetFirstUnassignableLoop<Rest, Value>
: UnitValue<Case>
: never
: never;
/**
* Shorthand for creating events attached to store by providing object with reducers for them
* @param store target store
* @param api object with reducers
*/
export function createApi<
S,
Api extends {[name: string]: ((store: S, e: any) => (S | void))}
>(
store: StoreWritable<S>,
api: Api,
): {
[K in keyof Api]: ((store: S, e: void) => (S | void)) extends Api[K]
? EventCallable<void>
: Api[K] extends ((store: S) => (S | void))
? EventCallable<void>
: Api[K] extends ((store: S, e: infer E) => (S | void))
? EventCallable<E extends void ? Exclude<E, undefined> | void : E>
: any
}
/**
* Creates a Store out of successful results of Effect.
* It works like a shortcut for `createStore(defaultState).on(effect.done, (_, {result}) => result)`
* @param effect source effect
* @param defaultState initial state of new store
*/
export function restore<Done>(
effect: Effect<any, Done, any>,
defaultState: Done,
): StoreWritable<Done>
/**
* Creates a Store out of successful results of Effect.
* It works like a shortcut for `createStore(defaultState).on(effect.done, (_, {result}) => result)`
* @param effect source effect
* @param defaultState initial state of new store
*/
export function restore<Done>(
effect: Effect<any, Done, any>,
defaultState: null,
): StoreWritable<Done | null>
/**
* Creates a Store from Event.
* It works like a shortcut for `createStore(defaultState).on(event, (_, payload) => payload)`
* @param event source event
* @param defaultState initial state of new store
*/
export function restore<E>(event: Event<E>, defaultState: E): StoreWritable<E>
/**
* Creates a Store from Event.
* It works like a shortcut for `createStore(defaultState).on(event, (_, payload) => payload)`
* @param event source event
* @param defaultState initial state of new store
*/
export function restore<E>(event: Event<E>, defaultState: null): StoreWritable<E | null>
export function restore<T extends Event<any>>(event: T): never
export function restore<T extends Effect<any, any, any>>(effect: T): never
export function restore<State extends {[key: string]: Store<any> | any}>(
state: State,
): {
[K in keyof State]: State[K] extends Store<infer S>
? StoreWritable<S>
: StoreWritable<State[K]>
}
/**
* Creates a domain
*/
export function createDomain(domainName?: string, config?: { domain?: Domain }): Domain
export function createDomain(config?: { name?: string; domain?: Domain }): Domain
type WhichTypeKind =
| 'never'
| 'any'
| 'unknown'
| 'void'
| 'undefined'
| 'value'
type NotType<T extends WhichTypeKind> = Exclude<WhichTypeKind, T>
type WhichType<T> = [T] extends [never]
? 'never'
: [unknown] extends [T]
? [0] extends [1 & T]
? 'any'
: 'unknown'
: [T] extends [void]
? [void] extends [T]
? 'void'
: 'undefined'
: 'value'
type BuiltInObject =
| Error
| Date
| RegExp
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| ReadonlyMap<unknown, unknown>
| ReadonlySet<unknown>
| WeakMap<object, unknown>
| WeakSet<object>
| ArrayBuffer
| DataView
| Function
| Promise<unknown>
| Generator
type UnitObject = Store<any> | Event<any> | Effect<any, any, any> | Unit<any>
/**
* Force typescript to print real type instead of geneic types
*
* It's better to see {a: string; b: number}
* instead of GetCombinedValue<{a: Store<string>; b: Store<number>}>
* */
type Show<A extends any> =
A extends BuiltInObject
? A
: A extends UnitObject
? A
: {
[K in keyof A]: A[K]
} // & {}
/* sample types */
type TupleObject<T extends Array<any>> = {
[I in Exclude<keyof T, keyof any[]>]: T[I]
}
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
type IfUnknown<T, Y, N> = 0 extends (1 & T) ? N : unknown extends T ? Y : N;
type IfAssignable<T, U, Y, N> =
(<G>() => IfAny<T & U, 0, G extends T ? 1 : 2>) extends
(<G>() => IfAny<T & U, 0, G extends U ? 1 : 2>)
? Y
: (T extends U ? never : 1) extends never
? Y
: T extends Array<any>
? number extends T['length']
? N
: U extends Array<any>
? number extends U['length']
? N
: TupleObject<T> extends TupleObject<U> ? Y : N
: N
: N
type Source<A> = Unit<A> | Combinable
type Clock<B> = Unit<B> | Tuple<Unit<any>>
type Target = UnitTargetable<any> | Tuple<any>
type GetTupleWithoutAny<T> = T extends Array<infer U>
? U extends Unit<infer Value>
? IfAny<Value, never, Value>
: never
: never
type GetMergedValue<T> = GetTupleWithoutAny<T> extends never ? any : GetTupleWithoutAny<T>
type GetSource<S> = S extends Unit<infer Value> ? Value : GetCombinedValue<S>
type GetClock<C> = C extends Unit<infer Value> ? Value : GetMergedValue<C>
/** Replaces incompatible unit type with string error message.
* There is no error message if target type is void.
*/
type ReplaceUnit<Target, Result, Value> = IfAssignable<Result, Value,
Target,
Value extends void
? Target
: 'incompatible unit in target'
>
// [...T] is used to show sample result as a tuple (not array)
type TargetTuple<Target extends Array<unknown>, Result> = [...{
[Index in keyof Target]: Target[Index] extends UnitTargetable<infer Value>
? ReplaceUnit<Target[Index], Result, Value>
: 'non-unit item in target'
}]
type MultiTarget<Target, Result> = Target extends UnitTargetable<infer Value>
? ReplaceUnit<Target, Result, Value>
: Target extends Tuple<unknown>
? TargetTuple<Target, Result>
: 'non-unit item in target'
type SampleImpl<
Target,
Source,
Clock,
FLBool,
FilterFun,
FN,
FNInf,
FNInfSource extends (
Source extends Unit<any> | SourceRecord
? TypeOfSource<Source>
: never
),
FNInfClock extends (
Clock extends Units
? TypeOfClock<Clock>
: never
),
FNAltArg,
FLUnit,
SomeFN,
> =
// no target
unknown extends Target
// no target, no source
? unknown extends Source
? unknown extends Clock
? [message: {error: 'either target, clock or source should exists'}]
// no target, no source, has clock
: Clock extends Units
? SampleFilterDef<
ModeSelector<
'clock | | filter | fn | ',
'clock | | filter | | ',
'clock | | | fn | ',
'clock | | | | ',
SomeFN
>,
Source, Clock, FLUnit, FLBool, FilterFun, FN, FNInf, FNInfSource, FNInfClock, FNAltArg, SomeFN
>
: [message: {error: 'clock should be unit or array of units'; got: Clock}]
// no target, has source
: Source extends Unit<any> | SourceRecord
// no target, has source, no clock
? unknown extends Clock
? SampleFilterDef<
ModeSelector<
' | source | filter | fn | ',
' | source | filter | | ',
' | source | | fn | ',
' | source | | | ',
SomeFN
>,
Source, Clock, FLUnit, FLBool, FilterFun, FN, FNInf, FNInfSource, FNInfClock, FNAltArg, SomeFN
>
// no target, has source, has clock
: Clock extends Units
? SampleFilterDef<
ModeSelector<
'clock | source | filter | fn | ',
'clock | source | filter | | ',
'clock | source | | fn | ',
'clock | source | | | ',
SomeFN
>,
Source, Clock, FLUnit, FLBool, FilterFun, FN, FNInf, FNInfSource, FNInfClock, FNAltArg, SomeFN
>
: [message: {error: 'clock should be unit or array of units'; got: Clock}]
: [message: {error: 'source should be unit or object with stores'; got: Source}]
// has target
: Target extends UnitsTarget | ReadonlyArray<UnitTargetable<any>>
// has target, no source
? unknown extends Source
? unknown extends Clock
? [message: {error: 'either target, clock or source should exists'}]
// has target, no source, has clock
: Clock extends Units
? SampleFilterTargetDef<
ModeSelector<
'clock | | filter | fn | target',
'clock | | filter | | target',
'clock | | | fn | target',
'clock | | | | target',
SomeFN
>,
Target, Source, Clock, FLUnit, FLBool, FilterFun, FN, FNInf, FNInfSource, FNInfClock, FNAltArg, SomeFN
>
: [message: {error: 'clock should be unit or array of units'; got: Clock}]
// has target, has source
: Source extends Unit<any> | SourceRecord
// has target, has source, no clock
? unknown extends Clock
? SampleFilterTargetDef<
ModeSelector<
' | source | filter | fn | target',
' | source | filter | | target',
' | source | | fn | target',
' | source | | | target',
SomeFN
>,
Target, Source, Clock, FLUnit, FLBool, FilterFun, FN, FNInf, FNInfSource, FNInfClock, FNAltArg, SomeFN
>
// has target, has source, has clock
: Clock extends Units
? SampleFilterTargetDef<
ModeSelector<
'clock | source | filter | fn | target',
'clock | source | filter | | target',
'clock | source | | fn | target',
'clock | source | | | target',
SomeFN
>,
Target, Source, Clock, FLUnit, FLBool, FilterFun, FN, FNInf, FNInfSource, FNInfClock, FNAltArg, SomeFN
>
: [message: {error: 'clock should be unit or array of units'; got: Clock}]
: [message: {error: 'source should be unit or object with stores'; got: Source}]
: Target extends InvalidUnitsTarget ? [message: {error: 'derived units are not allowed in target'; got: Target}] : [message: {error: 'target should be unit or array of units'; got: Target}]
type ModeSelector<
FilterAndFN,
FilterOnly,
FNOnly,
None,
SomeFN,
> = unknown extends SomeFN
? FilterAndFN
: SomeFN extends {fn: any; filter: any}
? FilterAndFN
: SomeFN extends {filter: any}
? FilterOnly
: SomeFN extends {fn: any}
? FNOnly
: None
type SampleRet<
Target,
Source,
Clock,
FLUnit,
FLBool,
FilterFun,
FN,
FNAltArg,
FNInf,
FNInfSource extends (
Source extends Unit<any> | SourceRecord
? TypeOfSource<Source>
: never
),
FNInfClock extends (
Clock extends Units
? TypeOfClock<Clock>
: never
),
SomeFN,
ForceTargetInference
> = unknown extends Target
? unknown extends Clock
? unknown extends Source
? never
: Source extends Unit<any> | SourceRecord
// has filter, has fn
? unknown extends SomeFN
? FLUnit extends Unit<any>
? FN extends (src: TypeOfSource<Source>) => any
? EventAsReturnType<ReturnType<FN>>
: never
: FLBool extends BooleanConstructor
? FNAltArg extends (arg: NonFalsy<TypeOfSource<Source>>) => any
? EventAsReturnType<ReturnType<FNAltArg>>
: never
: FilterFun extends (src: TypeOfSource<Source>) => src is FNInfSource
? FNInf extends (src: FNInfSource) => any
? EventAsReturnType<ReturnType<FNInf>>
: never
: FN extends (src: TypeOfSource<Source>) => any
? EventAsReturnType<ReturnType<FN>>
: never
// has filter, has fn
: SomeFN extends {filter: any; fn: any}
? FLUnit extends Unit<any>
? FN extends (src: TypeOfSource<Source>) => any
? EventAsReturnType<ReturnType<FN>>
: never
: FLBool extends BooleanConstructor
? FNAltArg extends (arg: NonFalsy<TypeOfSource<Source>>) => any
? EventAsReturnType<ReturnType<FNAltArg>>
: never
: FilterFun extends (src: TypeOfSource<Source>) => src is FNInfSource
? FNInf extends (src: FNInfSource) => any
? EventAsReturnType<ReturnType<FNInf>>
: never
: FN extends (src: TypeOfSource<Source>) => any
? EventAsReturnType<ReturnType<FN>>
: never
// no filter, has fn
: SomeFN extends {fn: any}
? FN extends (src: TypeOfSource<Source>) => any
? Source extends Store<any> | SourceRecord
? Store<ReturnType<FN>>
: EventAsReturnType<ReturnType<FN>>
: never
// has filter, no fn
: SomeFN extends {filter: any}
? FLUnit extends Unit<any>
? EventAsReturnType<TypeOfSource<Source>>
: FLBool extends BooleanConstructor
? EventAsReturnType<NonFalsy<TypeOfSource<Source>>>
: FilterFun extends (src: TypeOfSource<Source>) => src is FNInfSource
? EventAsReturnType<FNInfSource>
: EventAsReturnType<TypeOfSource<Source>>
// no filter, no fn
: Source extends Store<any> | SourceRecord
? Store<TypeOfSource<Source>>
: EventAsReturnType<TypeOfSource<Source>>
: never
: unknown extends Source
? Clock extends Units
// has filter, has fn
? unknown extends SomeFN
? FLUnit extends Unit<any>
? FN extends (clk: TypeOfClock<Clock>) => any
? EventAsReturnType<ReturnType<FN>>
: never
: FLBool extends BooleanConstructor
? FNAltArg extends (arg: NonFalsy<TypeOfClock<Clock>>) => any
? EventAsReturnType<ReturnType<FNAltArg>>
: never
: FilterFun extends (clk: TypeOfClock<Clock>) => clk is FNInfClock
? FNInf extends (clk: FNInfClock) => any
? EventAsReturnType<ReturnType<FNInf>>
: never
: FN extends (clk: TypeOfClock<Clock>) => any
? EventAsReturnType<ReturnType<FN>>
: never
// has filter, has fn
: SomeFN extends {filter: any; fn: any}
? FLUnit extends Unit<any>
? FN extends (clk: TypeOfClock<Clock>) => any
? EventAsReturnType<ReturnType<FN>>
: never
: FLBool extends BooleanConstructor
? FNAltArg extends (arg: NonFalsy<TypeOfClock<Clock>>) => any
? EventAsReturnType<ReturnType<FNAltArg>>
: never
: FilterFun extends (clk: TypeOfClock<Clock>) => clk is FNInfClock
? FNInf extends (src: FNInfClock) => any
? EventAsReturnType<ReturnType<FNInf>>
: never
: FN extends (clk: TypeOfClock<Clock>) => any
? EventAsReturnType<ReturnType<FN>>
: never
// no filter, has fn
: SomeFN extends {fn: any}
? FN extends (clk: TypeOfClock<Clock>) => any
? Clock extends Store<any>
? Store<ReturnType<FN>>
: EventAsReturnType<ReturnType<FN>>
: never
// has filter, no fn
: SomeFN extends {filter: any}
? FLUnit extends Unit<any>
? EventAsReturnType<TypeOfClock<Clock>>
: FLBool extends BooleanConstructor
? EventAsReturnType<NonFalsy<TypeOfClock<Clock>>>
: FilterFun extends (clk: TypeOfClock<Clock>) => clk is FNInfClock
? EventAsReturnType<FNInfClock>
: EventAsReturnType<TypeOfClock<Clock>>
// no filter, no fn
: Clock extends Store<any>
? Store<TypeOfClock<Clock>>
: EventAsReturnType<TypeOfClock<Clock>>
: never
: Clock extends Units
? Source extends Unit<any> | SourceRecord
// has filter, has fn
? unknown extends SomeFN
? FLUnit extends Unit<any>
? FN extends (src: TypeOfSource<Source>, clk: TypeOfClock<Clock>) => any
? EventAsReturnType<ReturnType<FN>>
: never
: FLBool extends BooleanConstructor
? FNAltArg extends (arg: NonFalsy<TypeOfSource<Source>>, clk: TypeOfClock<Clock>) => any
? EventAsReturnType<ReturnType<FNAltArg>>
: never
: FilterFun extends (src: TypeOfSource<Source>, clk: TypeOfClock<Clock>) => src is FNInfSource
? FNInf extends (src: FNInfSource, clk: TypeOfClock<Clock>) =