Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespaces #17

Merged
merged 12 commits into from
Sep 27, 2019
6 changes: 3 additions & 3 deletions examples/simpleActions.tests.ts → examples/actions.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { map } from 'rxjs/operators';
import { createActionCreator, ExtractPayload } from 'rxbeach';
import { ofType, extractPayload } from 'rxbeach/operators';

describe('example', function() {
describe('simple actions', function() {
export default function actionExamples() {
describe('actions', function() {
const voidAction = createActionCreator('[test] void action');
const primitiveAction = createActionCreator<number>(
'[test] primitive action'
Expand Down Expand Up @@ -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;
});
});
}
7 changes: 7 additions & 0 deletions examples/index.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import actionExamples from './actions.tests';
import namespaceExamples from './namespace.tests';

describe('examples', function() {
actionExamples();
namespaceExamples();
});
61 changes: 61 additions & 0 deletions examples/namespace.tests.ts
Original file line number Diff line number Diff line change
@@ -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<number>).payload || 0),
0
);

export default function namespaceExamples() {
describe('namespaces', function() {
const testAction = createActionCreator<number>('[test] primitive action');
const namespaceA = Symbol('A');
tlaundal marked this conversation as resolved.
Show resolved Hide resolved
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));
tlaundal marked this conversation as resolved.
Show resolved Hide resolved
deepEqual(b, actionWithPayload(testAction.type, 2, namespaceB));
equal(sum, 3);
});

it('can namespace dispatchAction', async function() {
const action$ = new Subject<AnyAction>();
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);
});
});
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 1 addition & 3 deletions src/createActionCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ export function createActionCreator(type: string): UnknownActionCreator {
const action = (payload?: any) => ({
type,
payload,
meta: {
qualifiers: [],
},
meta: {},
});
action.type = type;

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export {
} from './types/helpers';

export { createActionCreator } from './createActionCreator';

export { namespaceActionCreator, namespaceActionDispatcher } from './namespace';
8 changes: 4 additions & 4 deletions src/internal/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { ActionWithPayload, ActionWithoutPayload } from 'types/Action';

export const actionWithoutPayload = (
type: string,
qualifiers: string[] = []
namespace?: symbol
tlaundal marked this conversation as resolved.
Show resolved Hide resolved
): ActionWithoutPayload => ({
meta: { qualifiers },
meta: { namespace },
tlaundal marked this conversation as resolved.
Show resolved Hide resolved
type,
});

export const actionWithPayload = <P>(
type: string,
payload: P,
qualifiers: string[] = []
namespace?: symbol
): ActionWithPayload<P> => ({
...actionWithoutPayload(type, qualifiers),
...actionWithoutPayload(type, namespace),
payload,
});
55 changes: 55 additions & 0 deletions src/namespace.tests.ts
Original file line number Diff line number Diff line change
@@ -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;
tlaundal marked this conversation as resolved.
Show resolved Hide resolved

const namespacedActionCreator = namespaceActionCreator(
namespace,
actionCreator
);

const action = namespacedActionCreator(12);

deepEqual(action, actionWithPayload(type, 12, namespace));
tlaundal marked this conversation as resolved.
Show resolved Hide resolved
});
});

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(
namespace,
parentDispatcher
);

const action = actionWithoutPayload('action', Symbol('old namespace'));

childDispatcher(action);

deepEqual(dispatchedAction, {
payload: undefined,
...actionWithoutPayload(action.type, namespace),
});
});
});
});
69 changes: 69 additions & 0 deletions src/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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);
tlaundal marked this conversation as resolved.
Show resolved Hide resolved

const _namespaceAction = (namespace: symbol, action: UnknownAction) => ({
type: action.type,
payload: action.payload,
tlaundal marked this conversation as resolved.
Show resolved Hide resolved
meta: {
...action.meta,
namespace,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if meta contains a namespace key? Maybe namespace should be top level? Unless there is a reason for it to live under meta.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tooling controls meta. Our action creators doesn't give the call sites access to meta.
FSA defines type, payload, meta and error as the only allowed top level properties.

In short, I think this is a non-issue, and we have a test verifying that if it should happen, the new namespace takes precedence. This is also documented in namespaceActionCreator.

},
});

/**
* Decorate an action creator so the created actions have the given namespace
*
* The given namespace will replace existing namespaces on the action objects.
*
* In contrast to `namespaceActionDispatcher`, the function returned by this
* function creates an action object instead of dispatching it.
*
* @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 namespace
*/
export const namespaceActionCreator = <Payload>(
namespace: symbol,
actionCreator: ActionCreator<Payload>
): ActionCreator<Payload> => {
const creator = (payload?: any) =>
_namespaceAction(namespace, actionCreator(payload));
creator.type = actionCreator.type;
tlaundal marked this conversation as resolved.
Show resolved Hide resolved

return creator as ActionCreator<Payload>;
};

/**
* Decorate an action dispatcher so it dispatches namespaced actions
*
* 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 `namespaceActionCreator`, the function returned by this
* function dispatches the action, instead of creating it.
*
* @see namespaceActionCreator
* @param parentDispatcher The dispatcher the returned action dispatcher will
* dispatch to
* @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 namespace before passing the
* actions to the parent dispatcher
*/
export const namespaceActionDispatcher = (
namespace: symbol,
parentDispatcher: ActionDispatcher
): ActionDispatcher => action =>
parentDispatcher(_namespaceAction(namespace, action));
2 changes: 1 addition & 1 deletion src/operators/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { ofType, extractPayload } from './operators';
export { ofType, extractPayload, filterNamespace } from './operators';
12 changes: 11 additions & 1 deletion src/operators/operators.ts
Original file line number Diff line number Diff line change
@@ -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 ////
Expand Down Expand Up @@ -39,3 +39,13 @@ export const extractPayload = <Payload>(): OperatorFunction<
ActionWithPayload<Payload>,
Payload
> => map(action => action.payload);

/**
* Stream operator that filters for actions with the correct namespace
*
* @param namespace The namespace to filter for
*/
export const filterNamespace = (
tlaundal marked this conversation as resolved.
Show resolved Hide resolved
targetNamespace: symbol
): MonoTypeOperatorFunction<Action<any>> =>
filter(({ meta: { namespace } }) => namespace === targetNamespace);
2 changes: 1 addition & 1 deletion src/types/Action.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { VoidPayload } from 'rxbeach/internal';

type Meta = {
qualifiers: string[];
namespace?: symbol;
};

export type ActionWithoutPayload = {
Expand Down