@johngw/stream
Version:
Reactive programming tools using the WHATWG Streams API.
132 lines (127 loc) • 3.79 kB
text/typescript
import { ForkableRecallStream } from '@johngw/stream/sinks/ForkableRecallStream'
import {
stateReducer,
StateReducerInput,
StateReducerOutput,
StateReducers,
} from '@johngw/stream/transformers/stateReducer'
import { Subject, SubjectOptions } from '@johngw/stream/subjects/Subject'
import { ControllableStream } from '@johngw/stream/sources/ControllableStream'
/**
* The constructor options for a {@link StatefulSubject}.
*
* @group Subjects
*/
interface StatefulSubjectOptions<Actions extends Record<string, unknown>, State>
extends Omit<
SubjectOptions<
StateReducerInput<Actions>,
StateReducerOutput<Actions, State>
>,
'forkable' | 'transform'
> {
pipeThroughOptions?: StreamPipeOptions
}
/**
* Another form of the {@link Subject:class} that reduces a piece
* of state with actions and queues changes to the stream.
*
* It's important to note that if the result of an action does not change
* the state then nothing is queued to the stream.
*
* @group Subjects
* @see {@link stateReducer:function}
* @example
* ```
* // The State will be the type of data you want to manipulate.
* interface State {
* authors: string[]
* }
*
* // The Actions are a record of "action name" to a parameter.
* // If the action doesn't require a parameter, use `void`.
* type Actions = {
* 'add author': string
* 'action that doesnt need a parameter': void
* }
*
* // To get the best out of this API, pass the Actions type and State
* // during construction.
* const subject = new StatefulSubject<Actions, State>(
* {
* __INIT__: () => ({ authors: [] }),
*
* 'add author': (state, author) => state.authors.includes(author) ? state : {
* ...state,
* authors: [...state.authors, author],
* },
*
* 'action that doesnt need a parameter': (state) => state
* }
* )
*
* subject.fork().pipeTo(write(console.info))
* // { action: '__INIT__', state: { authors: [] } }
*
* subject.dispatch('add author', 'Jane Austin')
* // { action: 'add author', param: 'Jane Austin', state: { authors: ['Jane Austin'] } }
*
* subject.dispatch('add author', 'Jane Austin')
* // **Nothing will be queued as the state did not change.**
* ```
*/
export class StatefulSubject<
Actions extends Record<string, unknown>,
State
> extends Subject<
StateReducerInput<Actions>,
StateReducerOutput<Actions, State>
> {
constructor(
reducers: StateReducers<Actions, State>,
options: StatefulSubjectOptions<Actions, State> = {}
) {
super({
...options,
forkable: new ForkableRecallStream(),
transform: stateReducer(reducers),
})
}
/**
* Returns a new {@link StatefulControllableStream}. Once all controllers
* have been closed, then the source is also closed.
*/
override control() {
const subjectController = super.control()
return Object.assign(subjectController, {
/**
* Parameterised version of enqueue for simpliclity.
*
* @example
* ```
* subject.dispatch('action', 'param')
* // Instead of
* subject.enqueue({ action: 'action', param: 'param' })
* ```
*/
dispatch<Action extends keyof Actions>(
...args: Actions[Action] extends void
? [action: Action]
: [action: Action, param: Actions[Action]]
) {
subjectController.enqueue({
action: args[0],
param: args[1],
} as StateReducerInput<Actions>)
},
}) as StatefulSubjectController<Actions>
}
}
export interface StatefulSubjectController<
Actions extends Record<string, unknown>
> extends ControllableStream<StateReducerInput<Actions>> {
dispatch<Action extends keyof Actions>(
action: Action,
param?: Actions[Action]
): void
}