Skip to content

Commit

Permalink
Rewrite using Effect abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
performanceArtist committed Mar 29, 2021
1 parent c6f89e6 commit 11c679d
Show file tree
Hide file tree
Showing 25 changed files with 433 additions and 250 deletions.
2 changes: 1 addition & 1 deletion examples/basic/todo.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as rx from 'rxjs';

// api has its own types that may differ from the view's types
// changes in the view should not affect the api, but changes in the api affect the view
// conversions(mappings to view types) should be handled by epic or container
// conversions(mappings to view types) should be handled by container
export type Todo = {
id: number;
text: string;
Expand Down
24 changes: 12 additions & 12 deletions examples/basic/todo.medium.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
import { medium, ray } from '../../src';
import { medium, effect } from '../../src';
import { pipe } from 'fp-ts/lib/pipeable';
import * as rxo from 'rxjs/operators';
import { array, option } from 'fp-ts';
import { makeTodoSource, TodoSource } from './todo.source';
import { TodoApi, makeTodoApi } from './todo.api';

type Deps = {
export type TodoDeps = {
todoApi: TodoApi;
todoSource: TodoSource;
};

export const todoMedium = medium.map(
// Creates a minimal medium value(similar to Applicative's of).
// Keys('todoApi', 'todoSource') are a memoization measure - this ensures
// that regardless of the object passed to Medium, it will only recreate itself when
// that regardless of an object passed to Medium, it will only recreate itself when
// the values at the aforementioned keys have changed.
medium.id<Deps>()('todoApi', 'todoSource'),
medium.id<TodoDeps>()('todoApi', 'todoSource'),
(deps, on) => {
const { todoApi, todoSource } = deps;

const setTodos$ = pipe(
const setTodos = pipe(
// this function is used to filter actions triggered by sources
// it returns an observable with an action payload
on(todoSource.create('getTodos')),
rxo.switchMap(todoApi.getTodos),
// `ray` is a generic action creator used to add information to side effects.
// `infer` derives the following action automatically upon mapping: Ray<'setTodos$', RequestResult<Todo[]>>,
ray.infer(todos =>
// create an effect representation
effect.tag('setTodos', todos =>
todoSource.state.modify(state => ({ ...state, todos })),
),
);

const updateTodo$ = pipe(
const updateTodo = pipe(
on(todoSource.create('toggleDone')),
rxo.withLatestFrom(todoSource.state.value$),
rxo.map(([id, state]) =>
Expand All @@ -40,16 +40,16 @@ export const todoMedium = medium.map(
option.chain(array.findFirst(todo => todo.id === id)),
),
),
ray.infer(todo => {
effect.tag('updateTodo', todo => {
if (option.isSome(todo)) {
todoApi.updateTodo(todo.value);
}
}),
);

return {
setTodos$,
updateTodo$,
setTodos,
updateTodo,
};
},
);
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/todo.source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const initialState: TodoState = {
};

// There are no reasons I know of not to make sources lazy, as it gives you more flexibility and
// control over creation. This way you can also create multiple components with the same state and events.
// control over creation. This way you can also create multiple components with the same state and event shape.
export const makeTodoSource = () =>
source.create(
'todo',
Expand Down
9 changes: 4 additions & 5 deletions examples/basic/todo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ describe('todo', () => {
const { todoSource } = deps;

todoSource.dispatch('getTodos')();

expect(history.take()).toStrictEqual([
output('setTodos$')(requestResult.success([])),
output('setTodos')(requestResult.success([])),
]);
},
),
Expand All @@ -35,7 +34,7 @@ describe('todo', () => {
withTodo(
() => {
const todoSource = makeTodoSource();
todoSource.state.modify(state => ({
todoSource.state.modify((state) => ({
...state,
todos: requestResult.success([{ id: 1, text: '', done: false }]),
}));
Expand All @@ -53,12 +52,12 @@ describe('todo', () => {

todoSource.dispatch('toggleDone')(0);
expect(history.take()).toStrictEqual([
output('updateTodo$')(option.none),
output('updateTodo')(option.none),
]);

todoSource.dispatch('toggleDone')(1);
expect(history.take()).toStrictEqual([
output('updateTodo$')(option.some({ id: 1, text: '', done: true })),
output('updateTodo')(option.some({ id: 1, text: '', done: true })),
]);
},
),
Expand Down
54 changes: 0 additions & 54 deletions examples/map/errorReport.ts

This file was deleted.

4 changes: 2 additions & 2 deletions examples/chain/tour.spec.ts → examples/tour/tour.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('Env', () => {
expect(history.take()).toStrictEqual([]);

isFirstLogin.next(true);
expect(history.take()).toStrictEqual([output('setIsOpen$')(true)]);
expect(history.take()).toStrictEqual([output('setIsOpen')(true)]);
},
),
);
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('Symbol env', () => {
expect(history.take()).toStrictEqual([]);

symbol.next('USD/CAD');
expect(history.take()).toStrictEqual([output('setIsOpen$')(true)]);
expect(history.take()).toStrictEqual([output('setIsOpen')(true)]);
},
),
);
Expand Down
47 changes: 27 additions & 20 deletions examples/chain/tour.ts → examples/tour/tour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ import { identity } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import * as rx from 'rxjs';
import * as rxo from 'rxjs/operators';
import { carrier, medium, ray } from '../../src';
import { carrier, medium, effect } from '../../src';

// Consider this case: we want to start an app tour(and run its effects) only in case this is the user's first login.
// Since the effects of medium are simply observables, we can use chain-like operators(e.g. switchMap) to
// create an observable that contains the effects of tourMedium inside of the other "env" medium, that controls its lifecycle.

type TourMediumDeps = {
tour: {
setIsOpen: (isOpen: boolean) => void;
};
};

const tourMedium = medium.map(medium.id<TourMediumDeps>()('tour'), deps => {
const tourMedium = medium.map(medium.id<TourMediumDeps>()('tour'), (deps) => {
const { tour } = deps;

const setIsOpen$ = pipe(rx.of(true), ray.infer(tour.setIsOpen));
const setIsOpen = pipe(rx.of(true), effect.tag('setIsOpen', tour.setIsOpen));

return { setIsOpen$ };
return { setIsOpen };
});

type UserInfo = {
Expand All @@ -36,20 +34,21 @@ export const tourEnvMedium = medium.map(
(deps, _, [tourMedium]) => {
const { userInfo } = deps;

// Carrier is an underlying value of the medium.
// Medium type is basically Selector<E, Carrier<E, A>>.
// Carrier "carries" over the dependencies +
// a function that takes a stream of actions and returns an object with streams of effects.
// carrier.mergeOutput merges the object streams to create one stream of effects,
// which can be "chained", using operators like switchMap.
const tourMedium$ = pipe(
userInfo.isFirstLogin$,
rxo.filter(identity),
rxo.switchMap(() => pipe(tourMedium, carrier.mergeOutput)),
// "transform" allows to modify the effect's input stream,
// as long as it produces the same payload type
const setIsOpen = pipe(
tourMedium.setIsOpen,
effect.transform((input$) =>
pipe(
userInfo.isFirstLogin$,
rxo.filter(identity),
rxo.switchMap(() => input$),
),
),
);

return {
tourMedium$,
setIsOpen,
};
},
);
Expand Down Expand Up @@ -78,24 +77,32 @@ export const tourSymbolEnvMedium = pipe(
medium.id<EnvMediumSymbolDeps>()('userInfo', 'symbolProvider'),
),
selector.map(([tourMedium, envMedium]) =>
carrier.map(envMedium, deps => {
carrier.map(envMedium, (deps) => {
const { userInfo, symbolProvider } = deps;

const tourMedium$ = pipe(
const tourEffects$ = pipe(
rx.combineLatest([userInfo.isFirstLogin$, symbolProvider.symbol$]),
rxo.filter(([isFirstLogin]) => isFirstLogin),
rxo.switchMap(([_, symbol]) =>
pipe(
tourMedium.run({
symbol,
}),
// uses sources to produce an effect map
carrier.merge,
),
),
);

const tourStart = pipe(
tourEffects$,
rxo.filter(e => e.type === 'setIsOpen'),
rxo.map((e) => e.payload),
effect.tag('setIsOpen', () => {}),
);

return {
tourMedium$,
tourStart,
};
}),
),
Expand Down
61 changes: 61 additions & 0 deletions examples/withReports/withReports.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { RequestResult, requestResult } from '@performance-artist/fp-ts-adt';
import { option } from 'fp-ts';
import * as rx from 'rxjs';
import { test } from '../../src';
import { ReportDeps, withReports as reportsMedium } from './withReports';
import { makeTodoSource, Todo } from '../basic/todo.source';
import { TodoDeps } from '../basic/todo.medium';
import { unorderedEqualStrict } from '../../src/medium/testing';

const withReports = test.withMedium(reportsMedium);

const mockTodos: RequestResult<Todo[]> = requestResult.success([
{ id: 1, text: '', done: false },
]);

const makeDeps = (): ReportDeps & TodoDeps => ({
todoSource: makeTodoSource(),
todoApi: {
getTodos: () => rx.of(mockTodos),
updateTodo: () => {},
},
logger: () => {},
});

describe('reports', () => {
it(
'Triggers report on any update',
withReports(makeDeps, (deps, history, output) => {
const { todoSource } = deps;

todoSource.dispatch('getTodos')();
expect(history.take()).toStrictEqual([
output('setTodos')(mockTodos),
output('updateReport')(mockTodos),
]);

todoSource.dispatch('toggleDone')(1);
expect(history.take()).toStrictEqual([
output('updateTodo')(option.some({ id: 1, text: '', done: true })),
output('updateReport')(option.some({ id: 1, text: '', done: true })),
]);
}),
);

it(
'Triggers logger on error(no todo for the provided id)',
withReports(makeDeps, (deps, history, output) => {
const { todoSource } = deps;

todoSource.dispatch('toggleDone')(10);
const [first, ...rest] = history.take();
expect(first).toStrictEqual(output('updateTodo')(option.none));
expect(
unorderedEqualStrict(rest, [
output('updateReport')(option.none),
output('errorReport')(10),
]),
).toBe(true);
}),
);
});
Loading

0 comments on commit 11c679d

Please sign in to comment.