import xs, {Stream, MemoryStream} from 'xstream'; import dropRepeats from 'xstream/extra/dropRepeats'; import {DevToolEnabledSource} from '@cycle/run'; import {adapt} from '@cycle/run/lib/adapt'; import {Getter, Setter, Lens, Scope, Reducer} from './types'; function updateArrayEntry(array: Array, scope: number | string, newVal: any): Array { if (newVal === array[scope]) { return array; } const index = parseInt(scope as string); if (typeof newVal === 'undefined') { return array.filter((val, i) => i !== index); } return array.map((val, i) => i === index ? newVal : val); } function makeGetter(scope: Scope): Getter { if (typeof scope === 'string' || typeof scope === 'number') { return function lensGet(state) { if (typeof state === 'undefined') { return void 0; } else { return state[scope]; } }; } else { return scope.get; } } function makeSetter(scope: Scope): Setter { if (typeof scope === 'string' || typeof scope === 'number') { return function lensSet(state: T, childState: R): T { if (Array.isArray(state)) { return updateArrayEntry(state, scope, childState) as any; } else if (typeof state === 'undefined') { return {[scope]: childState} as any as T; } else { return {...(state as any), [scope]: childState}; } }; } else { return scope.set; } } export function isolateSource( source: StateSource, scope: Scope): StateSource { return source.select(scope); } export function isolateSink( innerReducer$: Stream>, scope: Scope): Stream> { const get = makeGetter(scope); const set = makeSetter(scope); return innerReducer$ .map(innerReducer => function outerReducer(outer: T | undefined) { const prevInner = get(outer); const nextInner = innerReducer(prevInner); if (prevInner === nextInner) { return outer; } else { return set(outer, nextInner); } }); } /** * Represents a piece of application state dynamically changing over time. */ export class StateSource { public state$: MemoryStream; private _state$: MemoryStream; private _name: string; constructor(stream: Stream, name: string) { this._state$ = stream .filter(s => typeof s !== 'undefined') .compose(dropRepeats()) .remember(); this._name = name; this.state$ = adapt(this._state$); (this._state$ as MemoryStream & DevToolEnabledSource)._isCycleSource = name; } /** * Selects a part (or scope) of the state object and returns a new StateSource * dynamically representing that selected part of the state. * * @param {string|number|lens} scope as a string, this argument represents the * property you want to select from the state object. As a number, this * represents the array index you want to select from the state array. As a * lens object (an object with get() and set()), this argument represents any * custom way of selecting something from the state object. */ public select(scope: Scope): StateSource { const get = makeGetter(scope); return new StateSource(this._state$.map(get), this._name); } public isolateSource = isolateSource; public isolateSink = isolateSink; }