From 4040b98bd375a2bdb1b03cac478718d2b30ce22a Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 20 Sep 2019 16:05:10 +0200 Subject: [PATCH 01/10] Remove everything that is not qualifiers --- .../complexModelCollection.tests.ts | 69 -------- .../data-collection/complexModelCollection.ts | 60 ------- examples/globalActions.ts | 11 -- examples/ping-pong.ts | 123 -------------- examples/qualified-stream.ts | 8 - examples/react/functional-connect.tsx | 39 ----- examples/react/hoc-connect.tsx | 17 -- examples/routines/consoleRoutines.tests.ts | 11 -- examples/routines/consoleRoutines.ts | 54 ------ src/actionOperators.tests.ts | 60 ------- src/actionOperators.ts | 67 -------- src/recipes/collection.tests.ts | 75 --------- src/recipes/collection.ts | 87 ---------- src/recipes/reactConnect.tests.ts | 109 ------------ src/recipes/reactConnect.ts | 68 -------- src/reducer.tests.ts | 65 -------- src/reducer.ts | 86 ---------- src/routines/actionRoutine.tests.ts | 28 ---- src/routines/actionRoutine.ts | 37 ----- src/routines/hookRoutine.tests.ts | 28 ---- src/routines/hookRoutine.ts | 30 ---- src/stateStream.ts | 155 ------------------ src/testUtils.ts | 72 -------- src/utils/operators.tests.ts | 41 +---- src/utils/operators.ts | 65 +------- src/utils/utils.tests.ts | 72 -------- src/utils/utils.ts | 34 ---- 27 files changed, 5 insertions(+), 1566 deletions(-) delete mode 100644 examples/data-collection/complexModelCollection.tests.ts delete mode 100644 examples/data-collection/complexModelCollection.ts delete mode 100644 examples/globalActions.ts delete mode 100644 examples/ping-pong.ts delete mode 100644 examples/qualified-stream.ts delete mode 100644 examples/react/functional-connect.tsx delete mode 100644 examples/react/hoc-connect.tsx delete mode 100644 examples/routines/consoleRoutines.tests.ts delete mode 100644 examples/routines/consoleRoutines.ts delete mode 100644 src/actionOperators.tests.ts delete mode 100644 src/actionOperators.ts delete mode 100644 src/recipes/collection.tests.ts delete mode 100644 src/recipes/collection.ts delete mode 100644 src/recipes/reactConnect.tests.ts delete mode 100644 src/recipes/reactConnect.ts delete mode 100644 src/reducer.tests.ts delete mode 100644 src/reducer.ts delete mode 100644 src/routines/actionRoutine.tests.ts delete mode 100644 src/routines/actionRoutine.ts delete mode 100644 src/routines/hookRoutine.tests.ts delete mode 100644 src/routines/hookRoutine.ts delete mode 100644 src/stateStream.ts delete mode 100644 src/utils/utils.tests.ts delete mode 100644 src/utils/utils.ts diff --git a/examples/data-collection/complexModelCollection.tests.ts b/examples/data-collection/complexModelCollection.tests.ts deleted file mode 100644 index cb5373f3..00000000 --- a/examples/data-collection/complexModelCollection.tests.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { deepEqual } from 'assert'; -import { dispatchAction } from '../globalActions'; -import { latest } from 'stream-patterns/testUtils'; -import { - ComplexModel, - putComplexModel, - complexModelCollection$, - deleteChildren, -} from './complexModelCollection'; - -describe('examples', function() { - // The collection stream needs to be live for latest to work - this.beforeEach(function() { - this.subscription = complexModelCollection$.subscribe(); - }); - this.afterEach(function() { - this.subscription.unsubscribe(); - }); - - it('complexModelCollection', async function() { - const complexA: ComplexModel = { - id: 'a', - data: 'This is a root thingy', - }; - const complexB: ComplexModel = { - id: 'b', - data: 'This is also a root thingy', - }; - const complexA1: ComplexModel = { - id: 'a1', - parent: complexA.id, - data: 'This is a child', - }; - const complexA2: ComplexModel = { - id: 'a2', - parent: complexA.id, - data: 'This is also a child', - }; - const complexB1: ComplexModel = { - id: 'b1', - parent: complexB.id, - data: 'This is a child of B', - }; - - dispatchAction(putComplexModel(complexA)); - dispatchAction(putComplexModel(complexA2)); - dispatchAction(putComplexModel(complexA1)); - dispatchAction(putComplexModel(complexB)); - dispatchAction(putComplexModel(complexB1)); - - let complexModels = await latest(complexModelCollection$); - deepEqual(complexModels.contents, { - a: complexA, - b: complexB, - a1: complexA1, - a2: complexA2, - b1: complexB1, - }); - - dispatchAction(deleteChildren(complexA.id)); - - complexModels = await latest(complexModelCollection$); - deepEqual(complexModels.contents, { - a: complexA, - b: complexB, - b1: complexB1, - }); - }); -}); diff --git a/examples/data-collection/complexModelCollection.ts b/examples/data-collection/complexModelCollection.ts deleted file mode 100644 index 85bbfa24..00000000 --- a/examples/data-collection/complexModelCollection.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { OperatorFunction, pipe } from 'rxjs'; -import { map, flatMap, tap, switchMap } from 'rxjs/operators'; -import { action$, dispatchAction } from '../globalActions'; -import { - combineActionOperators, - registerActionOperators, -} from 'stream-patterns/actionOperators'; -import { - collection, - WithId, - Collection, -} from 'stream-patterns/recipes/collection'; -import { actionRoutine } from 'stream-patterns/routines/actionRoutine'; -import { extractPayload } from 'stream-patterns/utils/operators'; - -export type ComplexModel = WithId & { - parent?: ComplexModel['id']; - data: string; -}; - -export const [ - complexModelCollection$, - putComplexModel, - removeComplexModel, - replaceComplexModels, -] = collection('complexModels', action$); - -/** - * Stream operator that accepts a collection of ComplexModel, and emits a - * collection of the ComplexModels that are children of the given parent - * ComplexModel. - * - * @param parentId The id of the ComplexModel to pick the children of - */ -export const pickComplexModelChildren = ( - parentId: string -): OperatorFunction, ComplexModel[]> => - map(({ contents }) => - Object.values(contents).filter(({ parent }) => parent === parentId) - ); - -/** - * Delete all the children of a ComplexModel - */ -export const deleteChildren = actionRoutine( - 'Delete children', - pipe( - extractPayload(), - switchMap(parentId => - complexModelCollection$.pipe(pickComplexModelChildren(parentId)) - ), - flatMap(children => children.map(({ id }) => removeComplexModel(id))), - tap(dispatchAction) - ) -); - -export const routines = combineActionOperators(deleteChildren); - -// In a setup function -registerActionOperators(action$, routines); diff --git a/examples/globalActions.ts b/examples/globalActions.ts deleted file mode 100644 index 7a0f7281..00000000 --- a/examples/globalActions.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Subject } from 'rxjs'; -import { Action } from 'stream-patterns/types/Action'; -import { ActionDispatcher } from 'stream-patterns/types/helpers'; -import { tag } from 'rxjs-spy/operators'; - -const actionSubject$ = new Subject>(); - -export const action$ = actionSubject$.pipe(tag('action$')); - -export const dispatchAction: ActionDispatcher = action => - actionSubject$.next(action); diff --git a/examples/ping-pong.ts b/examples/ping-pong.ts deleted file mode 100644 index a7b57e7c..00000000 --- a/examples/ping-pong.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { pipe } from 'rxjs'; -import { tap, map, flatMap, combineLatest, filter } from 'rxjs/operators'; -import { combineActionOperators } from 'stream-patterns/actionOperators'; -import { reducer, combineReducers } from 'stream-patterns/reducer'; -import { actionRoutine } from 'stream-patterns/routines/actionRoutine'; -import { hookRoutine } from 'stream-patterns/routines/hookRoutine'; -import { reduceToStateStream } from 'stream-patterns/stateStream'; -import { ActionWithoutPayload } from 'stream-patterns/types/Action'; -import { extractPayload } from 'stream-patterns/utils/operators'; -import { action$, dispatchAction } from './globalActions'; - -/* - * A simple stream example: PING -- PONG - */ - -enum PingPongState { - PING, - PONG, -} - -type PingOrPong = { - pingOrPong: PingPongState; -}; - -//// Stream //// -// The action-reducers -const ping = reducer(() => PingPongState.PING); -const pong = reducer(() => PingPongState.PONG); - -const reducers = combineReducers(PingPongState.PONG, ping, pong); - -/** - * The pingPongState$ holds the last ping or pong type - */ -const pingPongState$ = action$.pipe( - reduceToStateStream('pingPongState$', reducers, PingPongState.PING) -); - -//// Pure routines //// -const logPingPong = actionRoutine( - 'Log ping or pong', - pipe( - extractPayload(), - tap(({ pingOrPong }) => { - if (pingOrPong === PingPongState.PING) { - console.log('PING'); - } else if (pingOrPong === PingPongState.PONG) { - console.log('PONG!'); - } - }) - ) -); - -// shim for window.alert -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const alert = (msg: string) => null; - -const alertInvalidPingPong = actionRoutine( - 'Alert the user of invalid ping pong state', - pipe( - extractPayload(), - tap(({ pingOrPong }) => alert(`Invalid ping or pong: ${pingOrPong}`)) - ) -); - -//// Multiplexing routines //// -// Utils -const filterPingPongActions = () => - filter( - ({ type }: ActionWithoutPayload) => type === ping.type || type === pong.type - ); -const actionToPingOrPong = (action: ActionWithoutPayload): PingOrPong => { - if (action.type === ping.type) { - return { pingOrPong: PingPongState.PING }; - } else if (action.type === pong.type) { - return { pingOrPong: PingPongState.PONG }; - } else { - throw new Error('Unknown action'); - } -}; - -const logWhenPingPong = hookRoutine( - pipe( - filterPingPongActions(), - map(action => logPingPong(actionToPingOrPong(action))), - tap(dispatchAction) - ), - ping, - pong -); - -const verifyWhenPingPong = hookRoutine( - pipe( - filterPingPongActions(), - map(action => verifyPingAndPongAlternating(actionToPingOrPong(action))), - tap(dispatchAction) - ), - ping, - pong -); - -//// Data routine //// -const verifyPingAndPongAlternating = actionRoutine( - 'verify next ping pong state is valid', - pipe( - extractPayload(), - combineLatest(pingPongState$), - flatMap(([{ pingOrPong: latest }, previous]) => { - if (latest === previous) { - return [alertInvalidPingPong({ pingOrPong: latest })]; - } - return []; - }) - ) -); - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const actionOperators = combineActionOperators( - logPingPong, - logWhenPingPong, - verifyWhenPingPong, - verifyPingAndPongAlternating -); diff --git a/examples/qualified-stream.ts b/examples/qualified-stream.ts deleted file mode 100644 index a3f03bac..00000000 --- a/examples/qualified-stream.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { combineReducers } from 'stream-patterns/reducer'; -import { createStateStreamFactory } from 'stream-patterns/stateStream'; - -export const qualified$Factory = createStateStreamFactory( - 'test stream', - combineReducers('no state'), - 'no state' -); diff --git a/examples/react/functional-connect.tsx b/examples/react/functional-connect.tsx deleted file mode 100644 index d0ff3d51..00000000 --- a/examples/react/functional-connect.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import React from 'react'; -import { qualified$Factory } from '../qualified-stream'; -import { - connectHookCreator, - ActionStreamProps, -} from 'stream-patterns/recipes/reactConnect'; - -const useViewModel = connectHookCreator(qualified$Factory); - -const ComplexComponent = ({ action$, dispatchAction }: ActionStreamProps) => { - const [viewModel, qualifiedAction$, qualifiedDispatchAction] = useViewModel( - action$, - dispatchAction - ); - - return ( -
- {viewModel.length === 0 ? ( -

No data!

- ) : ( - - )} -
- ); -}; - -const SimpleComponent = ({ action$, dispatchAction }: ActionStreamProps) => { - const [viewModel] = useViewModel(action$, dispatchAction); - - return ( -
-

State is: {viewModel}

-
- ); -}; diff --git a/examples/react/hoc-connect.tsx b/examples/react/hoc-connect.tsx deleted file mode 100644 index fdd345b4..00000000 --- a/examples/react/hoc-connect.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import React from 'react'; -import { action$, dispatchAction } from '../globalActions'; -import { qualified$Factory } from '../qualified-stream'; -import { connectHOC, ConnectProps } from 'stream-patterns/recipes/reactConnect'; - -const SimpleComponent = ({ viewModel }: ConnectProps) => ( -
-

State is {viewModel}

-
-); - -const SimpleComponentHOC = connectHOC(qualified$Factory, SimpleComponent); - -const a = ( - -); diff --git a/examples/routines/consoleRoutines.tests.ts b/examples/routines/consoleRoutines.tests.ts deleted file mode 100644 index a9f1c6bc..00000000 --- a/examples/routines/consoleRoutines.tests.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { deepEqual } from 'assert'; -import { dispatchAction } from '../globalActions'; -import { logToConsole, messages } from './consoleRoutines'; - -describe('examples', function() { - it('consoleRoutines', async function() { - dispatchAction(logToConsole('Hello World!')); - - deepEqual(messages, ['Hello World!', 'Message length: 12']); - }); -}); diff --git a/examples/routines/consoleRoutines.ts b/examples/routines/consoleRoutines.ts deleted file mode 100644 index 12ae1dd5..00000000 --- a/examples/routines/consoleRoutines.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { pipe } from 'rxjs'; -import { tap, map } from 'rxjs/operators'; -import { action$, dispatchAction } from '../globalActions'; -import { - combineActionOperators, - registerActionOperators, -} from 'stream-patterns/actionOperators'; -import { actionRoutine } from 'stream-patterns/routines/actionRoutine'; -import { hookRoutine } from 'stream-patterns/routines/hookRoutine'; -import { extractPayload } from 'stream-patterns/utils/operators'; - -export const messages: string[] = []; - -// Shim for console log, so we can verify what is logged -const consoleLog = (msg: string) => messages.push(msg); - -export const logToConsole = actionRoutine( - '[debug] log to console', - pipe( - extractPayload(), - tap(message => consoleLog(message)) - ) -); - -const logMessageLength = actionRoutine( - '[debug] log message length', - pipe( - extractPayload(), - tap(length => consoleLog('Message length: ' + length)) - ) -); - -const logMessageLengthHook = hookRoutine( - pipe( - extractPayload(), - map(message => logMessageLength(message.length)), - tap(dispatchAction) - ), - logToConsole -); - -const routines = combineActionOperators( - logToConsole, - logMessageLength, - logMessageLengthHook -); - -// In a setup function -registerActionOperators(action$, routines); - -// When the action logToConsole("Hello World!") is dispatched, the console would -// print: -// > Hello World! -// > Message length: 12 diff --git a/src/actionOperators.tests.ts b/src/actionOperators.tests.ts deleted file mode 100644 index 4fc4631f..00000000 --- a/src/actionOperators.tests.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { deepEqual, equal } from 'assert'; -import { of, pipe } from 'rxjs'; -import { mapTo, tap } from 'rxjs/operators'; -import { - combineActionOperators, - registerActionOperators, -} from 'stream-patterns/actionOperators'; -import { - actionWithoutPayload, - toHistoryPromise, -} from 'stream-patterns/testUtils'; -import { AnyAction } from 'stream-patterns/types/Action'; -import { ActionDispatcher } from 'stream-patterns/types/helpers'; - -describe('actionOperators', function() { - describe('combineActionOperators', function() { - it('Should work', async function() { - const one = actionWithoutPayload(Symbol('one')); - const two = actionWithoutPayload(Symbol('two')); - const three = actionWithoutPayload(Symbol('three')); - const alpha = actionWithoutPayload(Symbol('alpha')); - const bravo = actionWithoutPayload(Symbol('bravo')); - - const combined = combineActionOperators( - { - type: one.type, - operator: mapTo(alpha), - }, - { - types: [two.type, three.type], - operator: mapTo(bravo), - } - ); - - const res = await toHistoryPromise(of(one, two, three).pipe(combined)); - - deepEqual(res, [alpha, bravo, bravo]); - }); - }); - describe('registerActionOperators', function() { - it('Should work', async function() { - let lastAction: AnyAction | null = null; - const dispatchAction: ActionDispatcher = action => (lastAction = action); - - const one = actionWithoutPayload(Symbol('one')); - const two = actionWithoutPayload(Symbol('two')); - - const routine = pipe( - mapTo(two), - tap(dispatchAction) - ); - - const s = registerActionOperators(of(one), routine); - - equal(lastAction, two); - - s.unsubscribe(); - }); - }); -}); diff --git a/src/actionOperators.ts b/src/actionOperators.ts deleted file mode 100644 index 82005f2c..00000000 --- a/src/actionOperators.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { OperatorFunction, pipe } from 'rxjs'; -import { AnyAction, Action } from './types/Action'; -import { ActionCreator } from './types/ActionCreator'; -import { ActionStream } from 'stream-patterns/types/helpers'; -import { - fork, - _filterForActionOperator, -} from 'stream-patterns/utils/operators'; -import { subscribeAndGuard } from 'stream-patterns/utils/utils'; - -/** - * MultiActionOperator are streaming operators that should run for specific sets - * of actions. - * Their emitted values will always be discarded. - */ -export type MultiActionOperator = { - types: symbol[]; - operator: OperatorFunction; -}; - -/** - * ActionConsumers are streaming operators that should run for specific actions. - * Their emitted values will always be discarded. - */ -export type SingleActionOperator = { - type: symbol; - operator: OperatorFunction, unknown>; -}; - -export type AnySingleActionOperator = { - type: symbol; - operator: OperatorFunction; -}; - -/** - * ActionCreatorConsumers are ActionCreators that also function as - * ActionConsumers - */ -export type ActionCreatorOperator = ActionCreator & - SingleActionOperator; - -export type AnyActionCreatorOperator = ActionCreator & - AnySingleActionOperator; - -/** - * Combine action operator definitions to a single operator - * - * Nothing will be emitted from this operator. - * - * @param definitions The action operator definitions that should be combined - */ -export const combineActionOperators = ( - ...definitions: (SingleActionOperator | MultiActionOperator)[] -): OperatorFunction => - fork( - ...definitions.map(definition => - pipe( - _filterForActionOperator(definition), - definition.operator - ) - ) - ); - -export const registerActionOperators = ( - action$: ActionStream, - actionOperators: OperatorFunction -) => subscribeAndGuard(action$.pipe(actionOperators)); diff --git a/src/recipes/collection.tests.ts b/src/recipes/collection.tests.ts deleted file mode 100644 index 45d42907..00000000 --- a/src/recipes/collection.tests.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { deepEqual } from 'assert'; -import { Subject } from 'rxjs'; -import { toHistoryPromise } from 'stream-patterns/testUtils'; -import { AnyAction } from 'stream-patterns/types/Action'; -import { WithId, collection, Collection } from './collection'; - -interface SomeModel extends WithId { - data: symbol; -} - -describe('collection', function() { - it('Should create a working collection$', async function() { - const action$ = new Subject(); - const [collection$, putModel, removeModel, replaceModels] = collection< - SomeModel - >('SomeModel collection', action$); - - const collectionHistory = toHistoryPromise(collection$); - - const modelA: SomeModel = { id: 'a', data: Symbol('a') }; - const modelA2: SomeModel = { id: 'a', data: Symbol('a2') }; - const modelB: SomeModel = { id: 'b', data: Symbol('b') }; - - const expectedHistory: Collection[] = [ - // Seed is empty collection - {}, - // Put model A - { [modelA.id]: modelA }, - // Put model B - { [modelA.id]: modelA, [modelB.id]: modelB }, - // Replace model A with model A2 (Have same id) - { [modelA2.id]: modelA2, [modelB.id]: modelB }, - // Delete model B - { [modelA2.id]: modelA2 }, - // Replace all with model B - { [modelB.id]: modelB }, - ].map(contents => ({ contents })); - - action$.next(putModel(modelA)); - action$.next(putModel(modelB)); - action$.next(putModel(modelA2)); - action$.next(removeModel(modelB.id)); - action$.next(replaceModels({ [modelB.id]: modelB })); - - action$.complete(); - deepEqual(await collectionHistory, expectedHistory); - }); - it('Should handle multiple collections on the same action$', async function() { - const action$ = new Subject(); - const [collection1$, putModel1] = collection('1', action$); - const [collection2$, putModel2] = collection('2', action$); - - const collection1History = toHistoryPromise(collection1$); - const collection2History = toHistoryPromise(collection2$); - - const modelA: SomeModel = { id: 'a', data: Symbol('a') }; - const modelB: SomeModel = { id: 'b', data: Symbol('b') }; - - const expectedHistory1: Collection[] = [ - {}, - { [modelA.id]: modelA }, - ].map(contents => ({ contents })); - const expectedHistory2: Collection[] = [ - {}, - { [modelB.id]: modelB }, - ].map(contents => ({ contents })); - - action$.next(putModel1(modelA)); - action$.next(putModel2(modelB)); - - action$.complete(); - deepEqual(await collection1History, expectedHistory1); - deepEqual(await collection2History, expectedHistory2); - }); -}); diff --git a/src/recipes/collection.ts b/src/recipes/collection.ts deleted file mode 100644 index d5df9b83..00000000 --- a/src/recipes/collection.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Observable } from 'rxjs'; -import { createQualifiedActionCreator } from 'stream-patterns/qualifiers'; -import { combineReducers, reducer } from 'stream-patterns/reducer'; -import { createQualifiedStateStream } from 'stream-patterns/stateStream'; -import { ActionCreatorWithPayload } from 'stream-patterns/types/ActionCreator'; -import { ActionStream } from 'stream-patterns/types/helpers'; - -type IdType = string; - -export interface WithId { - id: IdType; -} - -export type CollectionContent = { - [id: string]: Model; -}; -export type Collection = { - contents: CollectionContent; -}; - -const EMPTY_COLLECTION: Collection = { contents: {} }; - -const _putModel = reducer(({ contents }, model) => ({ - contents: { - ...contents, - [model.id]: model, - }, -})); - -const _removeModel = reducer((state, removeId) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [removeId]: _, ...contents } = state.contents; - return { contents }; -}); - -const _replaceModels = reducer( - (_, contents) => ({ contents }) -); - -const reducers = combineReducers( - EMPTY_COLLECTION, - _putModel, - _removeModel, - _replaceModels -); - -/** - * Create a stream-based collection - * - * @template `Model` - The model type for the collection - * @param name The name of the collection - * @param action$ The action$ the collection should be subscribed to - * @returns A list with: - * - The collection stream - * - putModel action - * - removeModel action - * - replaceModels action - */ -export const collection = ( - name: string, - action$: ActionStream -): [ - Observable>, - ActionCreatorWithPayload, - ActionCreatorWithPayload, - ActionCreatorWithPayload> -] => { - const [_collection$, , qualifier] = createQualifiedStateStream( - 'collection - ' + name, - reducers, - EMPTY_COLLECTION, - action$ - ); - - const collection$ = _collection$ as Observable>; - const putModel = createQualifiedActionCreator( - qualifier, - _putModel - ) as ActionCreatorWithPayload; - const replaceModels = createQualifiedActionCreator( - qualifier, - _replaceModels - ) as ActionCreatorWithPayload>; - const removeModel = createQualifiedActionCreator(qualifier, _removeModel); - - return [collection$, putModel, removeModel, replaceModels]; -}; diff --git a/src/recipes/reactConnect.tests.ts b/src/recipes/reactConnect.tests.ts deleted file mode 100644 index 93ebdc4d..00000000 --- a/src/recipes/reactConnect.tests.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { equal, strictEqual } from 'assert'; -import { Subject } from 'rxjs'; -import { ActionDispatcher } from 'stream-patterns/types/helpers'; -import { reducer, combineReducers } from 'stream-patterns/reducer'; -import { createStateStreamFactory } from 'stream-patterns/stateStream'; -import { testStream, beforeEach } from 'stream-patterns/testUtils'; -import { AnyAction } from 'stream-patterns/types/Action'; -import { useStream, connectHookCreator } from './reactConnect'; - -describe('reactConnect', function() { - describe('useStream', function() { - const scaffolding = beforeEach(this, () => { - const initial = 'alfa'; - const emit = ['bravo', 'charlie']; - const [obs$, subscriptions] = testStream(...emit); - const hook = renderHook(() => useStream(obs$, initial)); - - return { - ...hook, - initial, - emit, - obs$, - subscriptions, - }; - }); - this.afterEach(function() { - scaffolding(this).unmount(); - }); - - it('should first return initial value', async function() { - const { result } = scaffolding(this); - - equal(result.current, 'alfa'); - }); - it('should return emitted values', async function() { - const { result, waitForNextUpdate } = scaffolding(this); - - await waitForNextUpdate(); - equal(result.current, 'bravo'); - }); - it('should unsubscribe on unmount', async function() { - const { subscriptions, unmount } = scaffolding(this); - - equal(subscriptions.value, 1, 'subscribed'); - unmount(); - - equal(subscriptions.value, 0, 'unsubscribed'); - }); - }); - describe('connectHookCreator', function() { - const scaffolding = beforeEach(this, () => { - const increment = reducer(a => a + 1); - const count$Factory = createStateStreamFactory( - 'count', - combineReducers(0, increment), - 0 - ); - - const useCount = connectHookCreator(count$Factory); - const action$ = new Subject(); - const dispatchAction: ActionDispatcher = a => action$.next(a); - - const hook = renderHook( - ({ action$, dispatchAction }) => useCount(action$, dispatchAction), - { - initialProps: { - action$, - dispatchAction, - }, - } - ); - const dispatchIncrement = () => hook.result.current[2](increment()); - - return { - ...hook, - dispatchIncrement, - }; - }); - this.afterEach(function() { - scaffolding(this).unmount(); - }); - - it('should receive values', async function() { - const { result, rerender, dispatchIncrement } = scaffolding(this); - - equal(result.current[0], 0); - - dispatchIncrement(); - // This should be await waitForNextUpdate(), but for some reason, that - // doesn't work... - rerender(); - - equal(result.current[0], 1); - }); - - it('should keep the same stream and dispatcher', async function() { - const { result, rerender } = scaffolding(this); - - const [, action$1, dispatchAction1] = result.current; - - rerender(); - - const [, action$2, dispatchAction2] = result.current; - strictEqual(action$1, action$2); - strictEqual(dispatchAction1, dispatchAction2); - }); - }); -}); diff --git a/src/recipes/reactConnect.ts b/src/recipes/reactConnect.ts deleted file mode 100644 index b4253e0f..00000000 --- a/src/recipes/reactConnect.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect, useState, createElement, ComponentType } from 'react'; -import { StateStreamFactory } from 'stream-patterns/stateStream'; -import { ActionStream, ActionDispatcher } from 'stream-patterns/types/helpers'; -import { Observable } from 'rxjs'; - -/** - * Utils for connecting a state stream factory to a React component. - */ - -export const useStream = (stream$: Observable, initial: T) => { - const [value, setValue] = useState(initial); - - useEffect(() => { - const subscription = stream$.subscribe(v => { - setValue(v); - }); - - return () => subscription.unsubscribe(); - }, [stream$]); - - return value; -}; - -export const connectHookCreator = ( - state$Factory: StateStreamFactory -) => ( - parentAction$: ActionStream, - parentDispatchAction: ActionDispatcher -): [StateShape, ActionStream, ActionDispatcher] => { - const [{ state$, action$, dispatchAction }] = useState(() => { - return state$Factory(parentAction$, parentDispatchAction); - }); - const viewModel = useStream(state$, state$Factory.seed); - - return [viewModel, action$, dispatchAction]; -}; - -export type ActionStreamProps = { - action$: ActionStream; - dispatchAction: ActionDispatcher; -}; - -type ViewModelProp = { - viewModel: StateShape; -}; - -export type ConnectProps = ActionStreamProps & - ViewModelProp; - -export const connectHOC = ( - state$Factory: StateStreamFactory, - WrappedComponent: ComponentType> -): ComponentType => { - const useViewModel = connectHookCreator(state$Factory); - - return ({ action$, dispatchAction }: ActionStreamProps) => { - const [viewModel, childAction$, childDispatchAction] = useViewModel( - action$, - dispatchAction - ); - - return createElement(WrappedComponent, { - viewModel, - action$: childAction$, - dispatchAction: childDispatchAction, - }); - }; -}; diff --git a/src/reducer.tests.ts b/src/reducer.tests.ts deleted file mode 100644 index 898c7a38..00000000 --- a/src/reducer.tests.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { of } from 'rxjs'; -import { deepEqual, equal } from 'assert'; -import { reducer, combineReducers } from 'stream-patterns/reducer'; -import { actionWithPayload, beforeEach } from 'stream-patterns/testUtils'; -import { VoidPayload } from 'stream-patterns/types/Action'; - -const throwErrorFn = () => { - throw new Error(); -}; - -describe('reducers', function() { - describe('reducer', function() { - const scaffolding = beforeEach(this, () => { - const reducerFn = (totalLength: number, payload: string) => - totalLength + payload.length; - - const addString = reducer(reducerFn); - const reducers = combineReducers(0, addString); - - return { - reducerFn, - addString, - reducers, - }; - }); - - it('Should store reducer function', function() { - const { reducerFn, addString } = scaffolding(this); - - equal(addString.reducer[1], reducerFn); - }); - it('Should create functioning action creator', function() { - const { addString } = scaffolding(this); - - const action = addString('Hello'); - - deepEqual(action, actionWithPayload(addString.type, 'Hello')); - }); - }); - - describe('combineReducers', function() { - it('Should reduce actions to state', async function() { - const incrementOne = reducer((accumulator: number) => accumulator + 1); - const incrementMany = reducer( - (accumulator: number, increment: number) => accumulator + increment - ); - - const res = await of(incrementOne(), incrementMany(2), incrementOne()) - .pipe(combineReducers(1, incrementOne, incrementMany)) - .toPromise(); - - equal(res, 5); - }); - it('Should ignore reducers that throw errors', async function() { - const throwError = reducer(throwErrorFn); - const setState = reducer(() => true); - - const res = await of(throwError(), setState()) - .pipe(combineReducers(false, throwError, setState)) - .toPromise(); - - equal(res, true); - }); - }); -}); diff --git a/src/reducer.ts b/src/reducer.ts deleted file mode 100644 index f09260f0..00000000 --- a/src/reducer.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { OperatorFunction, pipe } from 'rxjs'; -import { scan } from 'rxjs/operators'; -import { createActionCreator } from 'stream-patterns/actionCreator'; -import { - VoidPayload, - AnyAction, - UnknownAction, -} from 'stream-patterns/types/Action'; -import { ActionCreator } from 'stream-patterns/types/ActionCreator'; -import { ofType } from 'stream-patterns/utils/operators'; - -export type Reducer = ( - previousState: State, - payload: Payload -) => State; - -export type ReducerMap = Map>; - -type ReducerEntry = [symbol, Reducer]; - -export type ReducerDefinition = ActionCreator & { - reducer: ReducerEntry; -}; - -/** - * Create a reducer and its corresponding action - * - * This function doesn't alter the passed the reducer, but serves as a nice way - * of adding typings to it. - * - * It returns an action creator with the reducer stored on it. This action - * creator can be put directly into a `combineReducers` call. - * - * @param reducer The reducer function - * @template `State` - The state the reducer reduces to - * @template `Payload` - The payload of the action, fed to the reducer together - * with the state - * @returns An action creator for the payload, with the reducer stored on it - * - * @see combineReducers - */ -export const reducer = ( - reducer: Reducer -): ReducerDefinition => { - type PartialDefinition = Partial> & - ActionCreator; - - const definition: PartialDefinition = createActionCreator(''); - definition.reducer = [definition.type, reducer]; - - return definition as ReducerDefinition; -}; - -/** - * Combine the reducers into a stream operator - * - * The payload of each incoming action is applied to the matching reducers - * together with the previous state (or the seed if it's the first invocation), - * and the returned state is emitted. - * - * This operator does not change whether the stream is hot or cold. - * - * @param seed The initial input to the first reducer call - * @param reducers The reducer actions that should be combined - */ -export const combineReducers = ( - seed: State, - ...reducers: ReducerDefinition[] -): OperatorFunction => { - const reducerMap = new Map(reducers.map(({ reducer }) => reducer)); - return pipe( - ofType(...reducerMap.keys()), - scan((state: State, { type, payload }: UnknownAction) => { - const reducer = reducerMap.get(type); - if (reducer) { - try { - return reducer(state, payload); - } catch (_) { - return state; - } - } else { - return state; - } - }, seed) - ); -}; diff --git a/src/routines/actionRoutine.tests.ts b/src/routines/actionRoutine.tests.ts deleted file mode 100644 index b000df63..00000000 --- a/src/routines/actionRoutine.tests.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { deepEqual } from 'assert'; -import { of } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { actionWithPayload } from 'stream-patterns/testUtils'; -import { actionRoutine } from './actionRoutine'; - -describe('routines', function() { - describe('actionRoutine', function() { - it('Should create a actionRoutine definition', async function() { - const testRoutine = actionRoutine('test routine', tap(() => {})); - - await of(testRoutine()) - .pipe( - testRoutine.operator, - tap(() => { - throw new Error('Actions should not leak past routines'); - }) - ) - .toPromise(); - }); - it('Should serve as an action creator', function() { - const testRoutine = actionRoutine('test routine', tap(() => {})); - const action = testRoutine('Hello'); - - deepEqual(action, actionWithPayload(testRoutine.type, 'Hello')); - }); - }); -}); diff --git a/src/routines/actionRoutine.ts b/src/routines/actionRoutine.ts deleted file mode 100644 index 3f371994..00000000 --- a/src/routines/actionRoutine.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { pipe, OperatorFunction } from 'rxjs'; -import { ignoreElements } from 'rxjs/operators'; -import { createActionCreator } from 'stream-patterns/actionCreator'; -import { - ActionCreatorOperator, - AnyActionCreatorOperator, -} from 'stream-patterns/actionOperators'; -import { Action, VoidPayload } from 'stream-patterns/types/Action'; - -/** - * Define an action routine - a routine with a corresponding action definition - * - * Action routines should be used to: - * - Perform side effects from streams / with actions - * - Provide extra data to streams / reducers / other actions - * - * In contrast to the `hookRoutine`, action routines can only be invoked - * directly, with the action that is created by this function. - * - * Anything emitted from the operator will be discarded. - * - * @param operator The routine itself, a simple operator function that accepts - * payloads - * @template `Payload` - The type of payload to accept - */ -export const actionRoutine = ( - debugName: string, - operator: OperatorFunction, unknown> -): ActionCreatorOperator => { - const def: Partial = createActionCreator(debugName); - def.operator = pipe( - operator, - ignoreElements() - ); - - return def as ActionCreatorOperator; -}; diff --git a/src/routines/hookRoutine.tests.ts b/src/routines/hookRoutine.tests.ts deleted file mode 100644 index e55f2b66..00000000 --- a/src/routines/hookRoutine.tests.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { equal, deepEqual } from 'assert'; -import { of } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { createActionCreator } from 'stream-patterns/actionCreator'; -import { hookRoutine } from './hookRoutine'; - -describe('routines', function() { - const action = createActionCreator('action'); - - describe('hookRoutine', function() { - it('Should create an hookRoutine definition', async function() { - let invoked = false; - const epicDefinition = hookRoutine(tap(() => (invoked = true)), action); - - await of(action()) - .pipe( - epicDefinition.operator, - tap(() => { - throw new Error('Actions should not leak past hookRoutines'); - }) - ) - .toPromise(); - - equal(invoked, true); - deepEqual(epicDefinition.types, [action.type]); - }); - }); -}); diff --git a/src/routines/hookRoutine.ts b/src/routines/hookRoutine.ts deleted file mode 100644 index 4ca7d32b..00000000 --- a/src/routines/hookRoutine.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { OperatorFunction, pipe } from 'rxjs'; -import { ignoreElements } from 'rxjs/operators'; -import { MultiActionOperator } from 'stream-patterns/actionOperators'; -import { Action, VoidPayload } from 'stream-patterns/types/Action'; -import { ActionCreator } from 'stream-patterns/types/ActionCreator'; - -/** - * Define a routine hooked onto existing actions - * - * This type of routine should be used to add more functionality to existing - * actions, preferably actions intended to be used as events. - * - * You are adviced to not pull other streams into a hooked routine, but rather - * make the hooked routine invoke an action routine that pulls in the other - * streams. This gives better separation of concerns. - * - * @param operator The routine itself, a simple operator that accepts actions - * @param actions The actions to accept - * @template `Payload` - The type of payload on the action creators - */ -export const hookRoutine = ( - operator: OperatorFunction, unknown>, - ...actions: ActionCreator[] -): MultiActionOperator> => ({ - types: actions.map(({ type }) => type), - operator: pipe( - operator, - ignoreElements() - ), -}); diff --git a/src/stateStream.ts b/src/stateStream.ts deleted file mode 100644 index 165adf6d..00000000 --- a/src/stateStream.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { OperatorFunction, pipe, Observable } from 'rxjs'; -import { startWith, shareReplay } from 'rxjs/operators'; -import { tag } from 'rxjs-spy/operators'; -import { - getQualifier, - createChildDispatcher, - createChildActionStream, -} from 'stream-patterns/qualifiers'; -import { AnyAction } from 'stream-patterns/types/Action'; -import { ActionStream, ActionDispatcher } from 'stream-patterns/types/helpers'; - -/** - * A stream operator that accepts actions, and returns a hot, reference counted, - * state stream - * - * The payload of each incoming action is applied to the matching reducers - * together with the previous state (or the seed if it's the first invocation), - * and the returned state is emitted. The stream is started with the seed, and - * will replay the latest state to all subscribers. - * - * @param debugName A name for debugging purposes - * @param reducerOperator A streaming operator that reduces actions to a state - * @param seed The initial state to emit and feed to the reducers - */ -export const reduceToStateStream = ( - debugName: string, - reducerOperator: OperatorFunction, - seed: StateShape -): OperatorFunction => - pipe( - reducerOperator, - startWith(seed), - tag(debugName), - shareReplay({ - // All subscriptions to a state stream should receive the last state - bufferSize: 1, - // When all subscriptions are finished (for instance when a `connect`ed - // component is unmounted), The state should not be kept in memory - refCount: true, - }) - ); - -export const createQualifiedStateStream = ( - debugName: string, - reducerOperator: OperatorFunction, - seed: StateShape, - action$: ActionStream -): [Observable, ActionStream, symbol] => { - const qualifier = getQualifier(debugName); - - const filteredAction$ = createChildActionStream(action$, qualifier); - - const state$ = filteredAction$.pipe( - reduceToStateStream(`state$ - ${debugName}`, reducerOperator, seed) - ); - - return [state$, filteredAction$, qualifier]; -}; - -/** - * Create a qualified state stream - * - * This is intended for use with view model streams. - * - * This function takes everything that is needed to build a state stream, and - * the action stream and action dispatcher from the parent stream and returns an - * action stream, filtered on the qualifier, an action dispatcher, which adds - * the qualifier, and the state stream. - * - * This, in essence, allows you to have multiple instances of the same state - * stream connected to the same action stream. - * - * @param debugName A name for debugging purposes - * @param reducerOperator A streaming operator that reduces actions to a state - * @param seed The initial state to emit and feed to the reducers - * @param action$ The action$ to pipe the state stream from - * @param dispatchAction The action dispatcher to dispatch qualified actions to - * @returns `action$` - A derived stream from the `action$` argument that is - * filtered on the qualifier, and has the qualifier - * stripped away - * @return `dispatchAction` - An action dispatcher that dispatches to the - * `dispatchAction` argument, with an added qualifier - * @return `state$` - A state stream that is piped from the returned `action$`, - * and thus only reacts to actions dispatched with the - * returned `dispatchAction` function - */ -export const createQualifiedStateStreamAndDispatcher = ( - debugName: string, - reducerOperator: OperatorFunction, - seed: StateShape, - action$: ActionStream, - dispatchAction: ActionDispatcher -) => { - const [state$, filteredAction$, qualifier] = createQualifiedStateStream( - debugName, - reducerOperator, - seed, - action$ - ); - - const dispatchQualifiedAction = createChildDispatcher( - dispatchAction, - qualifier - ); - - return { - state$, - action$: filteredAction$, - dispatchAction: dispatchQualifiedAction, - }; -}; - -/** - * Create an instance of this stream. - * - * @see createQualifiedStateStreamAndDispatcher - */ -export interface StateStreamFactory { - seed: StateShape; - - (action$: ActionStream, dispatchAction: ActionDispatcher): { - state$: Observable; - action$: ActionStream; - dispatchAction: ActionDispatcher; - }; -} - -/** - * Create a state stream factory - * - * This is mainly a utility function that curries `createQualifiedStateStreamAndDispatcher`, - * but it also adds the `seed` argument to the returned factory. - * - * @param debugName A name for debugging purposes - * @param reducerOperator A streaming operator that reduces actions to a state - * @param seed The initial state to emit and feed to the reducers - * - * @see createQualifiedStateStreamAndDispatcher - */ -export const createStateStreamFactory = ( - debugName: string, - reducerOperator: OperatorFunction, - seed: StateShape -): StateStreamFactory => { - const factory = (action$: ActionStream, dispatchAction: ActionDispatcher) => - createQualifiedStateStreamAndDispatcher( - debugName, - reducerOperator, - seed, - action$, - dispatchAction - ); - factory.seed = seed; - return factory; -}; diff --git a/src/testUtils.ts b/src/testUtils.ts index 4ad1105f..375f0b5d 100644 --- a/src/testUtils.ts +++ b/src/testUtils.ts @@ -1,18 +1,7 @@ -import { - Subject, - Observable, - OperatorFunction, - of, - BehaviorSubject, - timer, -} from 'rxjs'; -import { tap, reduce, take, zip } from 'rxjs/operators'; import { ActionWithPayload, ActionWithoutPayload, - AnyAction, } from 'stream-patterns/types/Action'; -import { subscriptionCount } from 'rxjs-subscription-count'; export const actionWithoutPayload = ( type: symbol, @@ -30,64 +19,3 @@ export const actionWithPayload =

( ...actionWithoutPayload(type, qualifiers), payload, }); - -/** - * NB: Rememeber to attach routines after calling this, so the actions arrive in - * the order you expect - */ -export const checkActions = async ( - action$: Subject, - checkers: ((action: AnyAction) => void)[] -) => { - let i = 0; - await action$ - .pipe( - tap(action => { - const checker = checkers[i]; - - checker(action); - - if (++i === checkers.length) { - action$.complete(); - } - }) - ) - .toPromise(); -}; - -export const beforeEach = ( - suite: Mocha.Suite, - setup: () => T -): ((context: Mocha.Context) => T) => { - suite.beforeEach(function() { - this.scaffolding = setup(); - }); - - return context => context.scaffolding as T; -}; - -const collectHistory = (): OperatorFunction => - reduce((acc, value) => [...acc, value], [] as T[]); - -/** - * Returnes a promise that resolves to an array of the history of the observable - * when the observable is completed. - */ -export const toHistoryPromise = (obs: Observable): Promise => - obs.pipe(collectHistory()).toPromise(); - -export const latest = (obs: Observable): Promise => - obs.pipe(take(1)).toPromise(); - -export const testStream = ( - ...values: T[] -): [Observable, BehaviorSubject] => { - const subscriptionCounter = new BehaviorSubject(0); - const stream = of(...values).pipe( - // Makes sure each value is emitted with one node-tick in between - zip(timer(0, 0), v => v), - subscriptionCount(subscriptionCounter) - ); - - return [stream, subscriptionCounter]; -}; diff --git a/src/utils/operators.tests.ts b/src/utils/operators.tests.ts index dc5c8482..f718d289 100644 --- a/src/utils/operators.tests.ts +++ b/src/utils/operators.tests.ts @@ -1,6 +1,6 @@ import { equal, deepEqual } from 'assert'; -import { of, OperatorFunction, Subject, pipe } from 'rxjs'; -import { tap, reduce, filter } from 'rxjs/operators'; +import { of, OperatorFunction } from 'rxjs'; +import { tap, reduce } from 'rxjs/operators'; import { actionWithPayload, actionWithoutPayload, @@ -9,7 +9,7 @@ import { ActionWithPayload, ActionWithoutPayload, } from 'stream-patterns/types/Action'; -import { extractPayload, ofType, fork } from './operators'; +import { extractPayload, ofType } from './operators'; const pipeActionWithPayload = ( payload: P, @@ -72,39 +72,4 @@ describe('operators', function() { deepEqual(collectedTypes, [targetType1, targetType2]); }); }); - - describe('fork', function() { - it('Should Run each pipe in parallel', async function() { - let aCount = 0; - let bCount = 0; - let beforeCount = 0; - - const subj = new Subject<{ type: 'A' | 'B' }>(); - const promise = subj - .pipe( - tap(() => (beforeCount += 1)), - fork( - pipe( - filter(({ type }) => type === 'A'), - tap(() => (aCount += 1)) - ), - pipe( - filter(({ type }) => type === 'B'), - tap(() => (bCount += 1)) - ) - ) - ) - .toPromise(); - - subj.next({ type: 'A' }); - subj.next({ type: 'B' }); - subj.complete(); - - await promise; - - equal(beforeCount, 2, 'Parent pipe should be hot'); - equal(aCount, 1, 'Pipe A should run in isolation'); - equal(bCount, 1, 'Pipe B should run in isolation'); - }); - }); }); diff --git a/src/utils/operators.ts b/src/utils/operators.ts index 6c4742b8..4f7d1339 100644 --- a/src/utils/operators.ts +++ b/src/utils/operators.ts @@ -1,9 +1,5 @@ -import { OperatorFunction, MonoTypeOperatorFunction, merge, pipe } from 'rxjs'; -import { map, filter, share } from 'rxjs/operators'; -import { - SingleActionOperator, - MultiActionOperator, -} from 'stream-patterns/actionOperators'; +import { OperatorFunction, MonoTypeOperatorFunction } from 'rxjs'; +import { map, filter } from 'rxjs/operators'; import { ActionWithPayload, AnyAction } from 'stream-patterns/types/Action'; //// Routines //// @@ -31,63 +27,6 @@ export const ofType = ( ): MonoTypeOperatorFunction => filter(({ type }) => targetTypes.indexOf(type) !== -1); -/** - * Runs operators in parallel and merges their results - * - * For each operator, the returned observable is subscribed to a pipe from the - * source observable with the operator. This makes it a bit like the `flatMap` - * operator and the merge function, but on an operator level instead of value - * or observable level. - * - * NB: Each operator will create a "copy" of the stream, so any operators - * before the `coldFork` operator, will be executed for each operator passed - * to `coldFork`. Because of this, you might want to use the `fork` - * operator, which includes a `share` operator to make the upstream hot. - * - * @param operators Operators to run in parallell and merge the results of - */ -const coldFork = ( - ...operators: OperatorFunction[] -): OperatorFunction => source => - merge(...operators.map(operator => source.pipe(operator))); - -/** - * Runs operators in parallel and merges their results - * - * For each operator, the returned observable is subscribed to a pipe from the - * source observable with the operator. This makes it a bit like the `flatMap` - * operator and the merge function, but on an operator level instead of value - * or observable level. - * - * This operator includes the `share` operator on the parent stream, to prevent - * operators that are attached before this one from running multiple times. - * - * @param operators Operators to run in parallell and merge the results of - */ -export const fork = ( - ...operators: OperatorFunction[] -): OperatorFunction => - pipe( - share(), - coldFork(...operators) - ); - -/** - * Creates a stream operator that filters actions appropriate for the given - * action operator, whether for a single or multiple actions. - * - * @param definition The action operator - */ -export const _filterForActionOperator = ( - definition: SingleActionOperator | MultiActionOperator -) => { - if ((definition as SingleActionOperator).type) { - return ofType((definition as SingleActionOperator).type); - } else { - return ofType(...(definition as MultiActionOperator).types); - } -}; - /** * Stream operator to extract the payload from an action * diff --git a/src/utils/utils.tests.ts b/src/utils/utils.tests.ts deleted file mode 100644 index 5a7ff533..00000000 --- a/src/utils/utils.tests.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { equal } from 'assert'; -import { Subject } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { ReducerDefinition, Reducer } from 'stream-patterns/reducer'; -import { sameReducerFn, subscribeAndGuard } from 'stream-patterns/utils/utils'; - -describe('utils', function() { - describe('subscribeAndGuard', function() { - this.beforeAll(function() { - this.originalConsoleError = console.error; - console.error = () => (this.hasLoggedErr = true); - }); - this.beforeEach(function() { - this.hasLoggedErr = false; - }); - this.afterAll(function() { - console.error = this.originalConsoleError; - }); - - it('Should subscribe to the stream', function() { - const subject = new Subject(); - - let count = 0; - const stream$ = subject.pipe(tap(() => count++)); - - const subscription = subscribeAndGuard(stream$); - - subject.next('one'); - subject.next('two'); - - equal(count, 2); - subscription.unsubscribe(); - }); - - it('Should stay subscribed after error', function() { - const subject = new Subject(); - - let count = 0; - const stream$ = subject.pipe( - tap(arg => { - if (arg === 'two') { - throw new Error('Two'); - } - count++; - }) - ); - - const subscription = subscribeAndGuard(stream$); - - subject.next('one'); - subject.next('two'); - subject.next('three'); - - equal(this.hasLoggedErr, true); - equal(count, 2); - subscription.unsubscribe(); - }); - }); - - describe('sameReducerFn', function() { - it('Should extract the reducer from a reducer definition', function() { - const reducer: Reducer = (acc, payload) => - acc + payload.length; - - const reducerDefinition: ReducerDefinition = { - reducer: [Symbol(), reducer], - } as any; - - equal(sameReducerFn(reducerDefinition), reducer); - }); - }); -}); diff --git a/src/utils/utils.ts b/src/utils/utils.ts deleted file mode 100644 index f7ac57c2..00000000 --- a/src/utils/utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Observable } from 'rxjs'; -import { catchError } from 'rxjs/operators'; -import { Reducer, ReducerDefinition } from 'stream-patterns/reducer'; - -/** - * Silences errors and subscribes the stream - * - * Errors are logged to console, and the stream will continue. - * - * @param stream$ The stream to subscribe and silence errors from - */ -export const subscribeAndGuard = (stream$: Observable) => - stream$ - .pipe( - catchError((error, stream) => { - console.error('UNHANDLED ERROR IN STREAM', error); - return stream; - }) - ) - .subscribe(); - -/** - * Extract the reducer from a reducer definition, for using the same reducer - * in multiple definitions - * - * ``` - * export const aliasReducerAction = reducer(sameReducerFn(originalReducerAction)); - * ``` - * - * @param ReducerDefinition The reducer definition to extract the reducer from - */ -export const sameReducerFn = ( - reducerDefinition: ReducerDefinition -): Reducer => reducerDefinition.reducer[1]; From 95078bc2fd019beb8aba6b602ca147dfbdcc2983 Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 20 Sep 2019 16:33:04 +0200 Subject: [PATCH 02/10] Only allow one qualifier per action --- src/createActionCreator.ts | 4 +- src/index.ts | 1 - src/internal/testUtils.ts | 8 ++-- src/qualifiers.tests.ts | 33 ++------------ src/qualifiers.ts | 92 +++++++++++--------------------------- src/types/Action.ts | 2 +- 6 files changed, 35 insertions(+), 105 deletions(-) diff --git a/src/createActionCreator.ts b/src/createActionCreator.ts index 7cfb04c6..95bbe06b 100644 --- a/src/createActionCreator.ts +++ b/src/createActionCreator.ts @@ -25,9 +25,7 @@ export function createActionCreator(type: string): UnknownActionCreator { const action = (payload?: any) => ({ type, payload, - meta: { - qualifiers: [], - }, + meta: {}, }); action.type = type; diff --git a/src/index.ts b/src/index.ts index 5415c98c..5b0ab9bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,5 +20,4 @@ export { createActionCreator } from './createActionCreator'; export { createQualifiedActionCreator, createChildDispatcher, - createChildActionStream, } from './qualifiers'; diff --git a/src/internal/testUtils.ts b/src/internal/testUtils.ts index 25bd63d5..76833981 100644 --- a/src/internal/testUtils.ts +++ b/src/internal/testUtils.ts @@ -2,17 +2,17 @@ import { ActionWithPayload, ActionWithoutPayload } from 'types/Action'; export const actionWithoutPayload = ( type: string, - qualifiers: symbol[] = [] + qualifier?: symbol ): ActionWithoutPayload => ({ - meta: { qualifiers }, + meta: { qualifier }, type, }); export const actionWithPayload =

( type: string, payload: P, - qualifiers: symbol[] = [] + qualifier?: symbol ): ActionWithPayload

=> ({ - ...actionWithoutPayload(type, qualifiers), + ...actionWithoutPayload(type, qualifier), payload, }); diff --git a/src/qualifiers.tests.ts b/src/qualifiers.tests.ts index c3c7c917..77144495 100644 --- a/src/qualifiers.tests.ts +++ b/src/qualifiers.tests.ts @@ -1,10 +1,8 @@ -import { of } from 'rxjs'; import { deepEqual } from 'assert'; import { createQualifiedActionCreator, ActionDispatcher, createChildDispatcher, - createChildActionStream, } from 'rxbeach'; import { actionWithPayload, @@ -19,7 +17,7 @@ describe('qualifiers', function() { const parentQualifier = Symbol('parent qualifier'); const childQualifier = Symbol('child qualifier'); const actionCreator = (payload: number) => - actionWithPayload(type, payload, [parentQualifier]); + actionWithPayload(type, payload, parentQualifier); actionCreator.type = type; const qualifiedActionCreator = createQualifiedActionCreator( @@ -29,10 +27,7 @@ describe('qualifiers', function() { const action = qualifiedActionCreator(12); - deepEqual( - action, - actionWithPayload(type, 12, [childQualifier, parentQualifier]) - ); + deepEqual(action, actionWithPayload(type, 12, childQualifier)); }); }); @@ -49,33 +44,13 @@ describe('qualifiers', function() { qualifier ); - const action = actionWithoutPayload('action', [parentQualifier]); + const action = actionWithoutPayload('action', parentQualifier); childDispatcher(action); deepEqual(dispatchedAction, { payload: undefined, - ...actionWithoutPayload(action.type, [qualifier, parentQualifier]), - }); - }); - }); - - describe('createChildActionStream', function() { - it('Should filter and strip qualifiers', async function() { - const qualifier = Symbol('qualifier'); - const type = 'type'; - - const res = await createChildActionStream( - of( - actionWithoutPayload('wrong action'), - actionWithoutPayload(type, [qualifier]) - ), - qualifier - ).toPromise(); - - deepEqual(res, { - type: type, - meta: { qualifiers: [] }, + ...actionWithoutPayload(action.type, qualifier), }); }); }); diff --git a/src/qualifiers.ts b/src/qualifiers.ts index b6c808d0..5a68d801 100644 --- a/src/qualifiers.ts +++ b/src/qualifiers.ts @@ -1,43 +1,28 @@ import { MonoTypeOperatorFunction } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; -import { tag } from 'rxjs-spy/operators'; -import { Action, ActionCreator, ActionDispatcher, ActionStream } from 'rxbeach'; +import { filter } from 'rxjs/operators'; +import { Action, ActionCreator, ActionDispatcher } from 'rxbeach'; import { UnknownAction } from 'rxbeach/internal'; /** - * Alias to create a new symbol + * Create a new qualifier + * + * @param description A description of this qualifier, does not need to be + * unique per instance. Usually the name of what will create + * instances of qualifiers */ -export const getQualifier = (name: string) => Symbol(name); +export const createQualifier = (description: string) => Symbol(description); /** - * Stream operator that filters out actions without the correct top qualifier + * Stream operator that filters for actions with the correct qualifier * * @param qualifier The qualifier to filter for */ -const filterQualifier = ( - qualifier: symbol +export const filterQualifier = ( + targetQualifier: symbol ): MonoTypeOperatorFunction> => - filter( - ({ - meta: { - qualifiers: [actionQualifier], - }, - }) => actionQualifier === qualifier - ); - -/** - * Stream operator that strips out the top qualifier of all actions - * - * Does no checks to ensure qualifiers exists before stripping them. Will error - * out if no qualifers are set. - */ -const stripQualifier = (): MonoTypeOperatorFunction> => - map(({ meta: { qualifiers: [, ...qualifiers], ...meta }, ...action }) => ({ - ...action, - meta: { ...meta, qualifiers }, - })); + filter(({ meta: { qualifier } }) => qualifier === targetQualifier); -export const _appendQualifierToAction = ( +const _appendQualifierToAction = ( qualifier: symbol, action: UnknownAction ) => ({ @@ -45,26 +30,23 @@ export const _appendQualifierToAction = ( payload: action.payload, meta: { ...action.meta, - qualifiers: [qualifier, ...action.meta.qualifiers], + qualifier, }, }); /** - * Decorate an action creator so that the created actions have qualifiers + * Decorate an action creator so the created actions have the given qualifier * - * The given qualifier is added as the top qualifier to each action created by - * the action creator. - * - * Existing qualifiers on the actions will be shifted down. + * The given qualifier will replace existing qualifiers on the action objects. * * In contrast to `createChildDispatcher`, the function returned by this * function creates an action object instead of dispatching it. * * @see createChildDispatcher - * @param qualifier The qualifier to add to the created actions + * @param qualifier The qualifier to set for the created actions * @param actionCreator The action creator to decorate * @returns An action creator that creates actions using the passed action - * creator, and adds the qualifier + * creator, and sets the given qualifier */ export const createQualifiedActionCreator = ( qualifier: symbol, @@ -80,21 +62,19 @@ export const createQualifiedActionCreator = ( /** * Create a dispatcher that dispatches qualified actions to the parent dispatcher * - * The given qualifier is added as the top qualifier to each action dispatched - * with this function, before the action is dispatched with the action - * dispatcher from the arguments. - * - * Existing qualifiers on the actions will be shifted down. + * The given qualifier is set as the qualifier for each action dispatched with + * this function, before the action is dispatched with the action dispatcher + * from the arguments. * * In contrast to `createQualifiedActionCreator`, the function returned by this * function dispatches the action, instead of creating it. * * @see createQualifiedActionCreator - * @param parentDispatcher The dispatcher the returned action dispatcher will + * @param parentDispatcher The dispatcher the returned action dispatcher will * dispatch to - * @param qualifier The qualifier that is added as the top qualifier to each - * action before they are passed on to the parent dispatcher - * @returns An action dispatches that adds the qualifier before passing the + * @param qualifier The qualifier that will be set for each action before they + * are passed on to the parent dispatcher + * @returns An action dispatcher that sets the qualifier before passing the * actions to the parent dispatcher */ export const createChildDispatcher = ( @@ -102,25 +82,3 @@ export const createChildDispatcher = ( qualifier: symbol ): ActionDispatcher => action => parentDispatcher(_appendQualifierToAction(qualifier, action)); - -/** - * Create an action stream that only emits actions with the correct top qualifier - * - * The returned stream will only return actions that had the given qualifier as - * top qualifier on the given action stream. The emited actions on the returned - * stream will have the qualifier removed. - * - * @param action$ The action stream with qualified actions - * @param qualifier The qualifier to filter for - * @returns An action stream filtered on the given qualifier, but with the - * qualifier stripped away - */ -export const createChildActionStream = ( - action$: ActionStream, - qualifier: symbol -): ActionStream => - action$.pipe( - filterQualifier(qualifier), - stripQualifier(), - tag('action$ - ' + qualifier.description) - ); diff --git a/src/types/Action.ts b/src/types/Action.ts index 01c2aabd..16e5d253 100644 --- a/src/types/Action.ts +++ b/src/types/Action.ts @@ -1,7 +1,7 @@ import { VoidPayload } from 'rxbeach/internal'; type Meta = { - qualifiers: symbol[]; + qualifier?: symbol; }; export type ActionWithoutPayload = { From 61d63d1880eb0edf95e82116095c5c21a856f3b3 Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 20 Sep 2019 16:41:09 +0200 Subject: [PATCH 03/10] Work around ts-mocha test resolution --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3a4cb6c..697c1663 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "license": "MIT", "scripts": { - "test": "ts-mocha --paths -p tsconfig.json src/**/*.tests.ts examples/**/*.tests.ts", + "test": "ts-mocha --paths -p tsconfig.json src/*.tests.ts src/**/*.tests.ts examples/**/*.tests.ts", "build": "tsc -p tsconfig.json", "lint": "eslint --ext .ts src examples" }, From e444957e134442db30d47dd431bb6d6b4e22cb89 Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 20 Sep 2019 16:42:57 +0200 Subject: [PATCH 04/10] Move filterQualifier to operators --- src/operators/index.ts | 2 +- src/operators/operators.ts | 12 +++++++++++- src/qualifiers.ts | 14 +------------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/operators/index.ts b/src/operators/index.ts index d4814f32..6031f2c5 100644 --- a/src/operators/index.ts +++ b/src/operators/index.ts @@ -1 +1 @@ -export { ofType, extractPayload } from './operators'; +export { ofType, extractPayload, filterQualifier } from './operators'; diff --git a/src/operators/operators.ts b/src/operators/operators.ts index c88b1409..6fe0344a 100644 --- a/src/operators/operators.ts +++ b/src/operators/operators.ts @@ -1,6 +1,6 @@ import { OperatorFunction, MonoTypeOperatorFunction } from 'rxjs'; import { map, filter } from 'rxjs/operators'; -import { ActionWithPayload } from 'rxbeach'; +import { ActionWithPayload, Action } from 'rxbeach'; import { AnyAction } from 'rxbeach/internal'; //// Routines //// @@ -39,3 +39,13 @@ export const extractPayload = (): OperatorFunction< ActionWithPayload, Payload > => map(action => action.payload); + +/** + * Stream operator that filters for actions with the correct qualifier + * + * @param qualifier The qualifier to filter for + */ +export const filterQualifier = ( + targetQualifier: symbol +): MonoTypeOperatorFunction> => + filter(({ meta: { qualifier } }) => qualifier === targetQualifier); diff --git a/src/qualifiers.ts b/src/qualifiers.ts index 5a68d801..f4da5180 100644 --- a/src/qualifiers.ts +++ b/src/qualifiers.ts @@ -1,6 +1,4 @@ -import { MonoTypeOperatorFunction } from 'rxjs'; -import { filter } from 'rxjs/operators'; -import { Action, ActionCreator, ActionDispatcher } from 'rxbeach'; +import { ActionCreator, ActionDispatcher } from 'rxbeach'; import { UnknownAction } from 'rxbeach/internal'; /** @@ -12,16 +10,6 @@ import { UnknownAction } from 'rxbeach/internal'; */ export const createQualifier = (description: string) => Symbol(description); -/** - * Stream operator that filters for actions with the correct qualifier - * - * @param qualifier The qualifier to filter for - */ -export const filterQualifier = ( - targetQualifier: symbol -): MonoTypeOperatorFunction> => - filter(({ meta: { qualifier } }) => qualifier === targetQualifier); - const _appendQualifierToAction = ( qualifier: symbol, action: UnknownAction From e0fe8f4b85bec961527589ad9f9ab547785b86ed Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 20 Sep 2019 16:52:45 +0200 Subject: [PATCH 05/10] Rename qualifiers to namespaces --- src/index.ts | 5 +-- src/internal/testUtils.ts | 8 ++-- src/namespace.tests.ts | 55 ++++++++++++++++++++++++++++ src/{qualifiers.ts => namespace.ts} | 51 ++++++++++++-------------- src/operators/index.ts | 2 +- src/operators/operators.ts | 10 ++--- src/qualifiers.tests.ts | 57 ----------------------------- src/types/Action.ts | 2 +- 8 files changed, 91 insertions(+), 99 deletions(-) create mode 100644 src/namespace.tests.ts rename src/{qualifiers.ts => namespace.ts} (51%) delete mode 100644 src/qualifiers.tests.ts diff --git a/src/index.ts b/src/index.ts index 5b0ab9bc..6df693b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,4 @@ export { export { createActionCreator } from './createActionCreator'; -export { - createQualifiedActionCreator, - createChildDispatcher, -} from './qualifiers'; +export { namespaceActionCreator, namespaceActionDispatcher } from './namespace'; diff --git a/src/internal/testUtils.ts b/src/internal/testUtils.ts index 76833981..708d1dba 100644 --- a/src/internal/testUtils.ts +++ b/src/internal/testUtils.ts @@ -2,17 +2,17 @@ import { ActionWithPayload, ActionWithoutPayload } from 'types/Action'; export const actionWithoutPayload = ( type: string, - qualifier?: symbol + namespace?: symbol ): ActionWithoutPayload => ({ - meta: { qualifier }, + meta: { namespace }, type, }); export const actionWithPayload =

( type: string, payload: P, - qualifier?: symbol + namespace?: symbol ): ActionWithPayload

=> ({ - ...actionWithoutPayload(type, qualifier), + ...actionWithoutPayload(type, namespace), payload, }); diff --git a/src/namespace.tests.ts b/src/namespace.tests.ts new file mode 100644 index 00000000..68b8cfd5 --- /dev/null +++ b/src/namespace.tests.ts @@ -0,0 +1,55 @@ +import { deepEqual } from 'assert'; +import { + namespaceActionCreator, + ActionDispatcher, + namespaceActionDispatcher, +} from 'rxbeach'; +import { + actionWithPayload, + AnyAction, + actionWithoutPayload, +} from 'rxbeach/internal'; + +describe('namespace', function() { + describe('namespaceActionCreator', function() { + it('Should create actions with namespace', function() { + const type = 'action type'; + const namespace = Symbol('new namespace'); + const actionCreator = (payload: number) => + actionWithPayload(type, payload, Symbol('old namespace')); + actionCreator.type = type; + + const namespacedActionCreator = namespaceActionCreator( + namespace, + actionCreator + ); + + const action = namespacedActionCreator(12); + + deepEqual(action, actionWithPayload(type, 12, namespace)); + }); + }); + + describe('namespaceActionDispatcher', function() { + it('Should invoke the parent dispatcher with namespaced actions', function() { + let dispatchedAction: AnyAction | undefined; + const parentDispatcher: ActionDispatcher = action => + (dispatchedAction = action); + + const namespace = Symbol('new namespace'); + const childDispatcher = namespaceActionDispatcher( + parentDispatcher, + namespace + ); + + const action = actionWithoutPayload('action', Symbol('old namespace')); + + childDispatcher(action); + + deepEqual(dispatchedAction, { + payload: undefined, + ...actionWithoutPayload(action.type, namespace), + }); + }); + }); +}); diff --git a/src/qualifiers.ts b/src/namespace.ts similarity index 51% rename from src/qualifiers.ts rename to src/namespace.ts index f4da5180..ed41bb7f 100644 --- a/src/qualifiers.ts +++ b/src/namespace.ts @@ -2,71 +2,68 @@ import { ActionCreator, ActionDispatcher } from 'rxbeach'; import { UnknownAction } from 'rxbeach/internal'; /** - * Create a new qualifier + * Create a new namespace * - * @param description A description of this qualifier, does not need to be + * @param description A description of this namespace, does not need to be * unique per instance. Usually the name of what will create - * instances of qualifiers + * instances of namespaces */ -export const createQualifier = (description: string) => Symbol(description); +export const createNamespace = (description: string) => Symbol(description); -const _appendQualifierToAction = ( - qualifier: symbol, - action: UnknownAction -) => ({ +const _namespaceAction = (namespace: symbol, action: UnknownAction) => ({ type: action.type, payload: action.payload, meta: { ...action.meta, - qualifier, + namespace, }, }); /** - * Decorate an action creator so the created actions have the given qualifier + * Decorate an action creator so the created actions have the given namespace * - * The given qualifier will replace existing qualifiers on the action objects. + * The given namespace will replace existing namespaces on the action objects. * - * In contrast to `createChildDispatcher`, the function returned by this + * In contrast to `namespaceActionDispatcher`, the function returned by this * function creates an action object instead of dispatching it. * - * @see createChildDispatcher - * @param qualifier The qualifier to set for the created actions + * @see namespaceActionDispatcher + * @param namespace The namespace to set for the created actions * @param actionCreator The action creator to decorate * @returns An action creator that creates actions using the passed action - * creator, and sets the given qualifier + * creator, and sets the given namespace */ -export const createQualifiedActionCreator = ( - qualifier: symbol, +export const namespaceActionCreator = ( + namespace: symbol, actionCreator: ActionCreator ): ActionCreator => { const creator = (payload?: any) => - _appendQualifierToAction(qualifier, actionCreator(payload)); + _namespaceAction(namespace, actionCreator(payload)); creator.type = actionCreator.type; return creator as ActionCreator; }; /** - * Create a dispatcher that dispatches qualified actions to the parent dispatcher + * Decorate an action dispatcher so it dispatches namespaced actions * - * The given qualifier is set as the qualifier for each action dispatched with + * The given namespace is set as the namespace for each action dispatched with * this function, before the action is dispatched with the action dispatcher * from the arguments. * - * In contrast to `createQualifiedActionCreator`, the function returned by this + * In contrast to `namespaceActionCreator`, the function returned by this * function dispatches the action, instead of creating it. * - * @see createQualifiedActionCreator + * @see namespaceActionCreator * @param parentDispatcher The dispatcher the returned action dispatcher will * dispatch to - * @param qualifier The qualifier that will be set for each action before they + * @param namespace The namespace that will be set for each action before they * are passed on to the parent dispatcher - * @returns An action dispatcher that sets the qualifier before passing the + * @returns An action dispatcher that sets the namespace before passing the * actions to the parent dispatcher */ -export const createChildDispatcher = ( +export const namespaceActionDispatcher = ( parentDispatcher: ActionDispatcher, - qualifier: symbol + namespace: symbol ): ActionDispatcher => action => - parentDispatcher(_appendQualifierToAction(qualifier, action)); + parentDispatcher(_namespaceAction(namespace, action)); diff --git a/src/operators/index.ts b/src/operators/index.ts index 6031f2c5..48b2ba82 100644 --- a/src/operators/index.ts +++ b/src/operators/index.ts @@ -1 +1 @@ -export { ofType, extractPayload, filterQualifier } from './operators'; +export { ofType, extractPayload, filterNamespace } from './operators'; diff --git a/src/operators/operators.ts b/src/operators/operators.ts index 6fe0344a..c8c578d0 100644 --- a/src/operators/operators.ts +++ b/src/operators/operators.ts @@ -41,11 +41,11 @@ export const extractPayload = (): OperatorFunction< > => map(action => action.payload); /** - * Stream operator that filters for actions with the correct qualifier + * Stream operator that filters for actions with the correct namespace * - * @param qualifier The qualifier to filter for + * @param namespace The namespace to filter for */ -export const filterQualifier = ( - targetQualifier: symbol +export const filterNamespace = ( + targetNamespace: symbol ): MonoTypeOperatorFunction> => - filter(({ meta: { qualifier } }) => qualifier === targetQualifier); + filter(({ meta: { namespace } }) => namespace === targetNamespace); diff --git a/src/qualifiers.tests.ts b/src/qualifiers.tests.ts deleted file mode 100644 index 77144495..00000000 --- a/src/qualifiers.tests.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { deepEqual } from 'assert'; -import { - createQualifiedActionCreator, - ActionDispatcher, - createChildDispatcher, -} from 'rxbeach'; -import { - actionWithPayload, - AnyAction, - actionWithoutPayload, -} from 'rxbeach/internal'; - -describe('qualifiers', function() { - describe('createQualifiedActionCreator', function() { - it('Should create actions with qualifier', function() { - const type = 'action type'; - const parentQualifier = Symbol('parent qualifier'); - const childQualifier = Symbol('child qualifier'); - const actionCreator = (payload: number) => - actionWithPayload(type, payload, parentQualifier); - actionCreator.type = type; - - const qualifiedActionCreator = createQualifiedActionCreator( - childQualifier, - actionCreator - ); - - const action = qualifiedActionCreator(12); - - deepEqual(action, actionWithPayload(type, 12, childQualifier)); - }); - }); - - describe('createChildDispatcher', function() { - it('Should invoke the parent dispatcher with qualified actions', function() { - let dispatchedAction: AnyAction | undefined; - const parentDispatcher: ActionDispatcher = action => - (dispatchedAction = action); - - const parentQualifier = Symbol('parent'); - const qualifier = Symbol('child'); - const childDispatcher = createChildDispatcher( - parentDispatcher, - qualifier - ); - - const action = actionWithoutPayload('action', parentQualifier); - - childDispatcher(action); - - deepEqual(dispatchedAction, { - payload: undefined, - ...actionWithoutPayload(action.type, qualifier), - }); - }); - }); -}); diff --git a/src/types/Action.ts b/src/types/Action.ts index 16e5d253..e98190ab 100644 --- a/src/types/Action.ts +++ b/src/types/Action.ts @@ -1,7 +1,7 @@ import { VoidPayload } from 'rxbeach/internal'; type Meta = { - qualifier?: symbol; + namespace?: symbol; }; export type ActionWithoutPayload = { From b3614b655bd54094a848fb41f054d2c9f6c9cf0b Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 20 Sep 2019 17:13:05 +0200 Subject: [PATCH 06/10] Add namespace example --- examples/index.tests.ts | 7 ++++ examples/namespace.tests.ts | 61 +++++++++++++++++++++++++++++++++ examples/simpleActions.tests.ts | 4 +-- src/namespace.tests.ts | 4 +-- src/namespace.ts | 4 +-- 5 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 examples/index.tests.ts create mode 100644 examples/namespace.tests.ts diff --git a/examples/index.tests.ts b/examples/index.tests.ts new file mode 100644 index 00000000..3002e03b --- /dev/null +++ b/examples/index.tests.ts @@ -0,0 +1,7 @@ +import simpleActionExamples from './simpleActions.tests'; +import namespaceExamples from './namespace.tests'; + +describe('examples', function() { + simpleActionExamples(); + namespaceExamples(); +}); diff --git a/examples/namespace.tests.ts b/examples/namespace.tests.ts new file mode 100644 index 00000000..1062b7c4 --- /dev/null +++ b/examples/namespace.tests.ts @@ -0,0 +1,61 @@ +import { equal, deepEqual } from 'assert'; +import { of, Subject } from 'rxjs'; +import { reduce } from 'rxjs/operators'; +import { + createActionCreator, + namespaceActionCreator, + namespaceActionDispatcher, + ActionWithPayload, +} from 'rxbeach'; +import { filterNamespace } from 'rxbeach/operators'; +import { AnyAction, actionWithPayload } from 'rxbeach/internal'; + +const sumOp = reduce( + (a, b) => a + ((b as ActionWithPayload).payload || 0), + 0 +); + +export default function registerExamples() { + describe('namespaces', function() { + const testAction = createActionCreator('[test] primitive action'); + const namespaceA = Symbol('A'); + const namespaceB = Symbol('B'); + + it('can namespace the action creators', async function() { + const testActionA = namespaceActionCreator(namespaceA, testAction); + const testActionB = namespaceActionCreator(namespaceB, testAction); + + const action$ = of(testActionA(1), testActionB(2)); + + const a = await action$.pipe(filterNamespace(namespaceA)).toPromise(); + const b = await action$.pipe(filterNamespace(namespaceB)).toPromise(); + const sum = await action$.pipe(sumOp).toPromise(); + + deepEqual(a, actionWithPayload(testAction.type, 1, namespaceA)); + deepEqual(b, actionWithPayload(testAction.type, 2, namespaceB)); + equal(sum, 3); + }); + + it('can namespace dispatchAction', async function() { + const action$ = new Subject(); + const dispatchAction = action$.next.bind(action$); + + const dispatchA = namespaceActionDispatcher(namespaceA, dispatchAction); + const dispatchB = namespaceActionDispatcher(namespaceB, dispatchAction); + + const a_p = action$.pipe(filterNamespace(namespaceA)).toPromise(); + const b_p = action$.pipe(filterNamespace(namespaceB)).toPromise(); + const sum_p = action$.pipe(sumOp).toPromise(); + + dispatchA(testAction(1)); + dispatchB(testAction(2)); + action$.complete(); + + const [a, b, sum] = await Promise.all([a_p, b_p, sum_p]); + + deepEqual(a, actionWithPayload(testAction.type, 1, namespaceA)); + deepEqual(b, actionWithPayload(testAction.type, 2, namespaceB)); + equal(sum, 3); + }); + }); +} diff --git a/examples/simpleActions.tests.ts b/examples/simpleActions.tests.ts index 22816595..0a47939c 100644 --- a/examples/simpleActions.tests.ts +++ b/examples/simpleActions.tests.ts @@ -4,7 +4,7 @@ import { map } from 'rxjs/operators'; import { createActionCreator, ExtractPayload } from 'rxbeach'; import { ofType, extractPayload } from 'rxbeach/operators'; -describe('example', function() { +export default function registerExamples() { describe('simple actions', function() { const voidAction = createActionCreator('[test] void action'); const primitiveAction = createActionCreator( @@ -47,4 +47,4 @@ describe('example', function() { const payload_assignable_to_extracted: Extracted = (null as any) as Payload; const extracted_assignable_to_payload: Payload = (null as any) as Extracted; }); -}); +} diff --git a/src/namespace.tests.ts b/src/namespace.tests.ts index 68b8cfd5..14159fc6 100644 --- a/src/namespace.tests.ts +++ b/src/namespace.tests.ts @@ -38,8 +38,8 @@ describe('namespace', function() { const namespace = Symbol('new namespace'); const childDispatcher = namespaceActionDispatcher( - parentDispatcher, - namespace + namespace, + parentDispatcher ); const action = actionWithoutPayload('action', Symbol('old namespace')); diff --git a/src/namespace.ts b/src/namespace.ts index ed41bb7f..5dd76b9f 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -63,7 +63,7 @@ export const namespaceActionCreator = ( * actions to the parent dispatcher */ export const namespaceActionDispatcher = ( - parentDispatcher: ActionDispatcher, - namespace: symbol + namespace: symbol, + parentDispatcher: ActionDispatcher ): ActionDispatcher => action => parentDispatcher(_namespaceAction(namespace, action)); From dd0cb261cb064dace661a9cab47c7e4c75c50596 Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 20 Sep 2019 17:16:13 +0200 Subject: [PATCH 07/10] Change registration of examples --- examples/{simpleActions.tests.ts => actions.tests.ts} | 4 ++-- examples/index.tests.ts | 4 ++-- examples/namespace.tests.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename examples/{simpleActions.tests.ts => actions.tests.ts} (94%) diff --git a/examples/simpleActions.tests.ts b/examples/actions.tests.ts similarity index 94% rename from examples/simpleActions.tests.ts rename to examples/actions.tests.ts index 0a47939c..3d219da9 100644 --- a/examples/simpleActions.tests.ts +++ b/examples/actions.tests.ts @@ -4,8 +4,8 @@ import { map } from 'rxjs/operators'; import { createActionCreator, ExtractPayload } from 'rxbeach'; import { ofType, extractPayload } from 'rxbeach/operators'; -export default function registerExamples() { - describe('simple actions', function() { +export default function actionExamples() { + describe('actions', function() { const voidAction = createActionCreator('[test] void action'); const primitiveAction = createActionCreator( '[test] primitive action' diff --git a/examples/index.tests.ts b/examples/index.tests.ts index 3002e03b..ff843b57 100644 --- a/examples/index.tests.ts +++ b/examples/index.tests.ts @@ -1,7 +1,7 @@ -import simpleActionExamples from './simpleActions.tests'; +import actionExamples from './actions.tests'; import namespaceExamples from './namespace.tests'; describe('examples', function() { - simpleActionExamples(); + actionExamples(); namespaceExamples(); }); diff --git a/examples/namespace.tests.ts b/examples/namespace.tests.ts index 1062b7c4..e6c4ba8d 100644 --- a/examples/namespace.tests.ts +++ b/examples/namespace.tests.ts @@ -15,7 +15,7 @@ const sumOp = reduce( 0 ); -export default function registerExamples() { +export default function namespaceExamples() { describe('namespaces', function() { const testAction = createActionCreator('[test] primitive action'); const namespaceA = Symbol('A'); From a02c78b4bfae94ac7430fcffd15e311ee65c467b Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Wed, 25 Sep 2019 11:25:48 +0200 Subject: [PATCH 08/10] Use strings for namespaces --- examples/namespace.tests.ts | 4 ++-- src/internal/testUtils.ts | 4 ++-- src/namespace.tests.ts | 8 ++++---- src/namespace.ts | 15 +++------------ src/operators/operators.ts | 2 +- src/types/Action.ts | 2 +- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/examples/namespace.tests.ts b/examples/namespace.tests.ts index 4a60180b..7cef9bf0 100644 --- a/examples/namespace.tests.ts +++ b/examples/namespace.tests.ts @@ -18,8 +18,8 @@ const sumOp = reduce( export default function namespaceExamples() { describe('namespaces', function() { const testAction = actionCreator('[test] primitive action'); - const namespaceA = Symbol('A'); - const namespaceB = Symbol('B'); + const namespaceA = 'A'; + const namespaceB = 'B'; it('can namespace the action creators', async function() { const testActionA = namespaceActionCreator(namespaceA, testAction); diff --git a/src/internal/testUtils.ts b/src/internal/testUtils.ts index 708d1dba..50d9c17a 100644 --- a/src/internal/testUtils.ts +++ b/src/internal/testUtils.ts @@ -2,7 +2,7 @@ import { ActionWithPayload, ActionWithoutPayload } from 'types/Action'; export const actionWithoutPayload = ( type: string, - namespace?: symbol + namespace?: string ): ActionWithoutPayload => ({ meta: { namespace }, type, @@ -11,7 +11,7 @@ export const actionWithoutPayload = ( export const actionWithPayload =

( type: string, payload: P, - namespace?: symbol + namespace?: string ): ActionWithPayload

=> ({ ...actionWithoutPayload(type, namespace), payload, diff --git a/src/namespace.tests.ts b/src/namespace.tests.ts index 14159fc6..aec6dff3 100644 --- a/src/namespace.tests.ts +++ b/src/namespace.tests.ts @@ -14,9 +14,9 @@ describe('namespace', function() { describe('namespaceActionCreator', function() { it('Should create actions with namespace', function() { const type = 'action type'; - const namespace = Symbol('new namespace'); + const namespace = 'new namespace'; const actionCreator = (payload: number) => - actionWithPayload(type, payload, Symbol('old namespace')); + actionWithPayload(type, payload, 'old namespace'); actionCreator.type = type; const namespacedActionCreator = namespaceActionCreator( @@ -36,13 +36,13 @@ describe('namespace', function() { const parentDispatcher: ActionDispatcher = action => (dispatchedAction = action); - const namespace = Symbol('new namespace'); + const namespace = 'new namespace'; const childDispatcher = namespaceActionDispatcher( namespace, parentDispatcher ); - const action = actionWithoutPayload('action', Symbol('old namespace')); + const action = actionWithoutPayload('action', 'old namespace'); childDispatcher(action); diff --git a/src/namespace.ts b/src/namespace.ts index 5dd76b9f..5b19c298 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -1,16 +1,7 @@ import { ActionCreator, ActionDispatcher } from 'rxbeach'; import { UnknownAction } from 'rxbeach/internal'; -/** - * Create a new namespace - * - * @param description A description of this namespace, does not need to be - * unique per instance. Usually the name of what will create - * instances of namespaces - */ -export const createNamespace = (description: string) => Symbol(description); - -const _namespaceAction = (namespace: symbol, action: UnknownAction) => ({ +const _namespaceAction = (namespace: string, action: UnknownAction) => ({ type: action.type, payload: action.payload, meta: { @@ -34,7 +25,7 @@ const _namespaceAction = (namespace: symbol, action: UnknownAction) => ({ * creator, and sets the given namespace */ export const namespaceActionCreator = ( - namespace: symbol, + namespace: string, actionCreator: ActionCreator ): ActionCreator => { const creator = (payload?: any) => @@ -63,7 +54,7 @@ export const namespaceActionCreator = ( * actions to the parent dispatcher */ export const namespaceActionDispatcher = ( - namespace: symbol, + namespace: string, parentDispatcher: ActionDispatcher ): ActionDispatcher => action => parentDispatcher(_namespaceAction(namespace, action)); diff --git a/src/operators/operators.ts b/src/operators/operators.ts index c8c578d0..7035e4d7 100644 --- a/src/operators/operators.ts +++ b/src/operators/operators.ts @@ -46,6 +46,6 @@ export const extractPayload = (): OperatorFunction< * @param namespace The namespace to filter for */ export const filterNamespace = ( - targetNamespace: symbol + targetNamespace: string ): MonoTypeOperatorFunction> => filter(({ meta: { namespace } }) => namespace === targetNamespace); diff --git a/src/types/Action.ts b/src/types/Action.ts index e98190ab..82c04f30 100644 --- a/src/types/Action.ts +++ b/src/types/Action.ts @@ -1,7 +1,7 @@ import { VoidPayload } from 'rxbeach/internal'; type Meta = { - namespace?: symbol; + namespace?: string; }; export type ActionWithoutPayload = { From bfc96ae579f49c152e1b4c7900f3e9dab8558367 Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Wed, 25 Sep 2019 11:30:10 +0200 Subject: [PATCH 09/10] filterNamespace -> withNamespace + tests --- examples/namespace.tests.ts | 10 +++++----- src/operators/index.ts | 2 +- src/operators/operators.tests.ts | 29 +++++++++++++++++++++++------ src/operators/operators.ts | 2 +- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/examples/namespace.tests.ts b/examples/namespace.tests.ts index 7cef9bf0..8991ffe3 100644 --- a/examples/namespace.tests.ts +++ b/examples/namespace.tests.ts @@ -7,7 +7,7 @@ import { namespaceActionDispatcher, ActionWithPayload, } from 'rxbeach'; -import { filterNamespace } from 'rxbeach/operators'; +import { withNamespace } from 'rxbeach/operators'; import { AnyAction, actionWithPayload } from 'rxbeach/internal'; const sumOp = reduce( @@ -27,8 +27,8 @@ export default function namespaceExamples() { const action$ = of(testActionA(1), testActionB(2)); - const a = await action$.pipe(filterNamespace(namespaceA)).toPromise(); - const b = await action$.pipe(filterNamespace(namespaceB)).toPromise(); + const a = await action$.pipe(withNamespace(namespaceA)).toPromise(); + const b = await action$.pipe(withNamespace(namespaceB)).toPromise(); const sum = await action$.pipe(sumOp).toPromise(); deepEqual(a, actionWithPayload(testAction.type, 1, namespaceA)); @@ -43,8 +43,8 @@ export default function namespaceExamples() { const dispatchA = namespaceActionDispatcher(namespaceA, dispatchAction); const dispatchB = namespaceActionDispatcher(namespaceB, dispatchAction); - const a_p = action$.pipe(filterNamespace(namespaceA)).toPromise(); - const b_p = action$.pipe(filterNamespace(namespaceB)).toPromise(); + const a_p = action$.pipe(withNamespace(namespaceA)).toPromise(); + const b_p = action$.pipe(withNamespace(namespaceB)).toPromise(); const sum_p = action$.pipe(sumOp).toPromise(); dispatchA(testAction(1)); diff --git a/src/operators/index.ts b/src/operators/index.ts index 48b2ba82..e401e0af 100644 --- a/src/operators/index.ts +++ b/src/operators/index.ts @@ -1 +1 @@ -export { ofType, extractPayload, filterNamespace } from './operators'; +export { ofType, extractPayload, withNamespace } from './operators'; diff --git a/src/operators/operators.tests.ts b/src/operators/operators.tests.ts index eaf2f394..1b361f02 100644 --- a/src/operators/operators.tests.ts +++ b/src/operators/operators.tests.ts @@ -1,9 +1,10 @@ import { equal, deepEqual } from 'assert'; import { of, OperatorFunction } from 'rxjs'; -import { tap, reduce } from 'rxjs/operators'; +import { reduce } from 'rxjs/operators'; import { ActionWithPayload, ActionWithoutPayload } from 'rxbeach'; import { extractPayload, ofType } from 'rxbeach/operators'; import { actionWithPayload, actionWithoutPayload } from 'rxbeach/internal'; +import { withNamespace } from './operators'; const pipeActionWithPayload = ( payload: P, @@ -35,16 +36,15 @@ describe('operators', function() { const targetType = 'Correct type'; const otherType = 'Wrong type'; - await of( + const res = await of( actionWithoutPayload(targetType), actionWithoutPayload(otherType), actionWithoutPayload(targetType) ) - .pipe( - ofType(targetType), - tap(action => equal(action.type, targetType)) - ) + .pipe(ofType(targetType)) .toPromise(); + + equal(res.type, targetType); }); it('Should filter multiple action types', async function() { @@ -66,4 +66,21 @@ describe('operators', function() { deepEqual(collectedTypes, [targetType1, targetType2]); }); }); + + describe('withNamespace', function() { + it('Should filter actions by namespace', async function() { + const actionType = 'actionType'; + const namespace = 'namespace'; + + const res = await of( + actionWithoutPayload(actionType), + actionWithoutPayload(actionType, namespace), + actionWithoutPayload(actionType) + ) + .pipe(withNamespace(namespace)) + .toPromise(); + + deepEqual(res, actionWithoutPayload(actionType, namespace)); + }); + }); }); diff --git a/src/operators/operators.ts b/src/operators/operators.ts index 7035e4d7..ef4cd9e0 100644 --- a/src/operators/operators.ts +++ b/src/operators/operators.ts @@ -45,7 +45,7 @@ export const extractPayload = (): OperatorFunction< * * @param namespace The namespace to filter for */ -export const filterNamespace = ( +export const withNamespace = ( targetNamespace: string ): MonoTypeOperatorFunction> => filter(({ meta: { namespace } }) => namespace === targetNamespace); From 30c7ca875733a9a16c1e202d3ec6a29ae9693f88 Mon Sep 17 00:00:00 2001 From: Tobias Laundal Date: Fri, 27 Sep 2019 10:13:40 +0200 Subject: [PATCH 10/10] Consolidate mockAction, clean up namespace tests. Should have been two commits --- examples/namespace.tests.ts | 107 ++++++++++++++++++++++--------- src/internal/index.ts | 2 +- src/internal/testUtils.ts | 27 ++++---- src/namespace.tests.ts | 54 ++++++++-------- src/operators/operators.tests.ts | 24 +++---- 5 files changed, 126 insertions(+), 88 deletions(-) diff --git a/examples/namespace.tests.ts b/examples/namespace.tests.ts index 8991ffe3..ccfa5905 100644 --- a/examples/namespace.tests.ts +++ b/examples/namespace.tests.ts @@ -5,15 +5,9 @@ import { actionCreator, namespaceActionCreator, namespaceActionDispatcher, - ActionWithPayload, } from 'rxbeach'; import { withNamespace } from 'rxbeach/operators'; -import { AnyAction, actionWithPayload } from 'rxbeach/internal'; - -const sumOp = reduce( - (a, b) => a + ((b as ActionWithPayload).payload || 0), - 0 -); +import { AnyAction, mockAction } from 'rxbeach/internal'; export default function namespaceExamples() { describe('namespaces', function() { @@ -21,41 +15,94 @@ export default function namespaceExamples() { const namespaceA = 'A'; const namespaceB = 'B'; - it('can namespace the action creators', async function() { + describe('namespacing action creators', function() { const testActionA = namespaceActionCreator(namespaceA, testAction); const testActionB = namespaceActionCreator(namespaceB, testAction); + const actionObjectA = testActionA(1); + const actionObjectB = testActionB(2); + + let lastActionNamespaceA: AnyAction | undefined; + let lastActionNamespaceB: AnyAction | undefined; + let sumAllNamespaces: number | undefined; + this.afterEach(async function() { + lastActionNamespaceA = undefined; + lastActionNamespaceB = undefined; + sumAllNamespaces = undefined; + }); - const action$ = of(testActionA(1), testActionB(2)); + this.beforeEach(async function() { + const action$ = of(actionObjectA, actionObjectB); + const actionA$ = action$.pipe(withNamespace(namespaceA)); + const actionB$ = action$.pipe(withNamespace(namespaceB)); + const sum$ = action$.pipe(reduce((a, b) => a + (b.payload || 0), 0)); - const a = await action$.pipe(withNamespace(namespaceA)).toPromise(); - const b = await action$.pipe(withNamespace(namespaceB)).toPromise(); - const sum = await action$.pipe(sumOp).toPromise(); + lastActionNamespaceA = await actionA$.toPromise(); + lastActionNamespaceB = await actionB$.toPromise(); + sumAllNamespaces = await sum$.toPromise(); + }); - deepEqual(a, actionWithPayload(testAction.type, 1, namespaceA)); - deepEqual(b, actionWithPayload(testAction.type, 2, namespaceB)); - equal(sum, 3); + it('can filter namespace A', async function() { + equal(lastActionNamespaceA, actionObjectA); + }); + it('can filter namespace B', async function() { + equal(lastActionNamespaceB, actionObjectB); + }); + it('dispatches to main action$', async function() { + equal(sumAllNamespaces, 3); + }); }); - it('can namespace dispatchAction', async function() { - const action$ = new Subject(); - const dispatchAction = action$.next.bind(action$); + describe('namespacing action dispatchers', function() { + let lastActionNamespaceA: AnyAction | undefined; + let lastActionNamespaceB: AnyAction | undefined; + let sumAllNamespaces: number | undefined; + this.afterEach(function() { + lastActionNamespaceA = undefined; + lastActionNamespaceB = undefined; + sumAllNamespaces = undefined; + }); + + this.beforeEach(async function() { + const action$ = new Subject(); + const dispatchAction = action$.next.bind(action$); + + const dispatchA = namespaceActionDispatcher(namespaceA, dispatchAction); + const dispatchB = namespaceActionDispatcher(namespaceB, dispatchAction); + + const a_p = action$.pipe(withNamespace(namespaceA)).toPromise(); + const b_p = action$.pipe(withNamespace(namespaceB)).toPromise(); + const sum_p = action$ + .pipe(reduce((a: any, b: any) => a + (b.payload || 0), 0)) + .toPromise(); + + dispatchA(testAction(1)); + dispatchB(testAction(2)); + action$.complete(); - const dispatchA = namespaceActionDispatcher(namespaceA, dispatchAction); - const dispatchB = namespaceActionDispatcher(namespaceB, dispatchAction); + const [a, b, sum] = await Promise.all([a_p, b_p, sum_p]); - const a_p = action$.pipe(withNamespace(namespaceA)).toPromise(); - const b_p = action$.pipe(withNamespace(namespaceB)).toPromise(); - const sum_p = action$.pipe(sumOp).toPromise(); + lastActionNamespaceA = a; + lastActionNamespaceB = b; + sumAllNamespaces = sum; + }); - dispatchA(testAction(1)); - dispatchB(testAction(2)); - action$.complete(); + it('applies namespace A', function() { + deepEqual( + lastActionNamespaceA, + mockAction(testAction.type, namespaceA, 1) + ); + }); - const [a, b, sum] = await Promise.all([a_p, b_p, sum_p]); + it('applies namespace B', function() { + deepEqual( + lastActionNamespaceB, + mockAction(testAction.type, namespaceB, 2) + ); + }); - deepEqual(a, actionWithPayload(testAction.type, 1, namespaceA)); - deepEqual(b, actionWithPayload(testAction.type, 2, namespaceB)); - equal(sum, 3); + it('dispatches to root action$', function() { + equal(sumAllNamespaces, 3); + }); }); }); } diff --git a/src/internal/index.ts b/src/internal/index.ts index 20abdc8c..8a506433 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -1,4 +1,4 @@ -export { actionWithoutPayload, actionWithPayload } from './testUtils'; +export { mockAction } from './testUtils'; export { VoidPayload, AnyAction, diff --git a/src/internal/testUtils.ts b/src/internal/testUtils.ts index 50d9c17a..3fff6802 100644 --- a/src/internal/testUtils.ts +++ b/src/internal/testUtils.ts @@ -1,18 +1,13 @@ -import { ActionWithPayload, ActionWithoutPayload } from 'types/Action'; +import { Action } from 'rxbeach'; +import { VoidPayload } from 'rxbeach/internal'; -export const actionWithoutPayload = ( +export const mockAction =

( type: string, - namespace?: string -): ActionWithoutPayload => ({ - meta: { namespace }, - type, -}); - -export const actionWithPayload =

( - type: string, - payload: P, - namespace?: string -): ActionWithPayload

=> ({ - ...actionWithoutPayload(type, namespace), - payload, -}); + namespace?: string, + payload?: P +): Action

=> + ({ + meta: { namespace }, + type, + payload, + } as Action

); diff --git a/src/namespace.tests.ts b/src/namespace.tests.ts index aec6dff3..8e91090e 100644 --- a/src/namespace.tests.ts +++ b/src/namespace.tests.ts @@ -4,51 +4,47 @@ import { ActionDispatcher, namespaceActionDispatcher, } from 'rxbeach'; -import { - actionWithPayload, - AnyAction, - actionWithoutPayload, -} from 'rxbeach/internal'; +import { mockAction, AnyAction } from 'rxbeach/internal'; describe('namespace', function() { describe('namespaceActionCreator', function() { - it('Should create actions with namespace', function() { - const type = 'action type'; - const namespace = 'new namespace'; - const actionCreator = (payload: number) => - actionWithPayload(type, payload, 'old namespace'); - actionCreator.type = type; + const type = 'action type'; + const namespace = 'new namespace'; + const actionCreator = (payload: number) => + mockAction(type, 'old namespace', payload); + actionCreator.type = type; - const namespacedActionCreator = namespaceActionCreator( - namespace, - actionCreator - ); + const namespacedActionCreator = namespaceActionCreator( + namespace, + actionCreator + ); - const action = namespacedActionCreator(12); + const actionObject = namespacedActionCreator(12); - deepEqual(action, actionWithPayload(type, 12, namespace)); + it('Should create actions with namespace', function() { + deepEqual(actionObject, mockAction(type, namespace, 12)); }); }); describe('namespaceActionDispatcher', function() { - it('Should invoke the parent dispatcher with namespaced actions', function() { - let dispatchedAction: AnyAction | undefined; - const parentDispatcher: ActionDispatcher = action => - (dispatchedAction = action); + let dispatchedAction: AnyAction | undefined; + const parentDispatcher: ActionDispatcher = action => + (dispatchedAction = action); - const namespace = 'new namespace'; - const childDispatcher = namespaceActionDispatcher( - namespace, - parentDispatcher - ); + const namespace = 'new namespace'; + const childDispatcher = namespaceActionDispatcher( + namespace, + parentDispatcher + ); - const action = actionWithoutPayload('action', 'old namespace'); + const actionObject = mockAction('action', 'old namespace'); - childDispatcher(action); + childDispatcher(actionObject); + it('Should invoke the parent dispatcher with namespaced actions', function() { deepEqual(dispatchedAction, { payload: undefined, - ...actionWithoutPayload(action.type, namespace), + ...mockAction(actionObject.type, namespace), }); }); }); diff --git a/src/operators/operators.tests.ts b/src/operators/operators.tests.ts index 1b361f02..4f5fac8b 100644 --- a/src/operators/operators.tests.ts +++ b/src/operators/operators.tests.ts @@ -3,14 +3,14 @@ import { of, OperatorFunction } from 'rxjs'; import { reduce } from 'rxjs/operators'; import { ActionWithPayload, ActionWithoutPayload } from 'rxbeach'; import { extractPayload, ofType } from 'rxbeach/operators'; -import { actionWithPayload, actionWithoutPayload } from 'rxbeach/internal'; +import { mockAction } from 'rxbeach/internal'; import { withNamespace } from './operators'; const pipeActionWithPayload = ( payload: P, pipe: OperatorFunction, R> ): Promise => - of(actionWithPayload('', payload)) + of(mockAction('', '', payload) as ActionWithPayload

) .pipe(pipe) .toPromise(); @@ -37,9 +37,9 @@ describe('operators', function() { const otherType = 'Wrong type'; const res = await of( - actionWithoutPayload(targetType), - actionWithoutPayload(otherType), - actionWithoutPayload(targetType) + mockAction(targetType), + mockAction(otherType), + mockAction(targetType) ) .pipe(ofType(targetType)) .toPromise(); @@ -53,9 +53,9 @@ describe('operators', function() { const otherType = 'Wrong type'; const collectedTypes = await of( - actionWithoutPayload(targetType1), - actionWithoutPayload(otherType), - actionWithoutPayload(targetType2) + mockAction(targetType1), + mockAction(otherType), + mockAction(targetType2) ) .pipe( ofType(targetType1, targetType2), @@ -73,14 +73,14 @@ describe('operators', function() { const namespace = 'namespace'; const res = await of( - actionWithoutPayload(actionType), - actionWithoutPayload(actionType, namespace), - actionWithoutPayload(actionType) + mockAction(actionType), + mockAction(actionType, namespace), + mockAction(actionType) ) .pipe(withNamespace(namespace)) .toPromise(); - deepEqual(res, actionWithoutPayload(actionType, namespace)); + deepEqual(res, mockAction(actionType, namespace)); }); }); });