Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 56 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ const appSaga = {
return model;
}
},
*updater(model, action) {
async *updater(model, action) {
switch (action.type) {
case 'GREET_WITH_DELAY':
yield wait(1e3); // wait 1 second
await wait(1e3); // wait 1 second
// dispatch GREET action to store after delay
yield { type: 'GREET', message: model.message };
break;
Expand All @@ -41,36 +41,37 @@ const store = createStore(appReducer, applyMiddleware(modelSaga.middleware));
```


### Observing continual side-effects
### Processing continual side-effects

```js
const { ObservableSubscribed } = require('aperol');
function repeat(interval) {
return new Observable((observer) => {
const handler = setInterval(() => observer.next());
return () => clearInterval(handler);
});
const { AsyncIteratorStarted, runItContinual } = require('aperol');
async function* repeat(interval) {
while (true) {
await wait(interval);
yield;
}
}
const appSaga = {
reducer(model, action) {
switch (action.type) {
case 'GREET':
return { ...model, count: model.count + 1 };
case ObservableSubscribed:
case AsyncIteratorStarted:
if (action.sourceAction.type === 'GREET_REPEATABLE') {
return { ...model, greetingSubscription: action.subscription };
return { ...model, greetingAsyncIterator: action.asyncIterator };
}
default:
return model;
}
},
*updater(model, action) {
async *updater(model, action) {
switch (action.type) {
case 'GREET_REPEATABLE':
yield repeat(1e3) // repeat every 1 second
.map(function () {
// dispatch GREET action to store repeatable
yield { type: 'GREET' };
yield runItContinual(async function* () {
for await (let _ of repeat(1e3)) { // repeat every 1 second
// dispatch GREET action to store repeatable
yield { type: 'GREET' };
}
});
break;
case 'GREET':
Expand All @@ -80,8 +81,8 @@ const appSaga = {
}
break;
case 'STOP_GREETING':
// When no more needed subscribing side-effect greeting
model.greetingSubscription.unsubscribe();
// When no more needed processing side-effect greeting
await model.greetingAsyncIterator.return();
break;
}
},
Expand All @@ -103,16 +104,44 @@ const modelSaga = createModelSaga(appSaga);
```


### Destroy for backend
When you plan to use aperol in node.js on backend it should be destroyed model saga when you no more needs it for user.

### Model-less saga
*Analogy to stateless components in React*
```js
// It will unsubscribe all Observable subscriptions
modelSaga.destroy();
async function* appSaga(action) {
switch (action.type) {
case 'GREET_WITH_DELAY':
await wait(1e3);
yield { type: 'GREET' };
break;
}
}
```

### Server side rendering

```jsx
const { createModelSaga } = require('aperol');
const appReducer = require('./appReducer');
const appSaga = require('./appSaga');
handleGetRequest(async (request) => {
const modelSaga = createModelSaga(appSaga, {
// continual effects are not wanted to wait for
// do not run any continual generator & return to all continual async iterators
skipContinual: true,
});
const store = createStore(appReducer, applyMiddleware(modelSaga.middleware));
// Handle request by yourself & wait for updaters are done
await store.dispatch({ type: 'HANDLE_REQUEST', request }).__promise;
const html = renderToString(<Application state={store.getState()}/>);
return html;
});
```

## Notes
*library automatically polyfill Observable if not available in global Symbol context with `zen-observable`*
### Polyfill
For running library in old browsers & non harmony flagged Node.js is necessary to polyfill `Symbol.asyncIterator`. You can achieve it by simple implementation included in library.
```js
require('aperol').polyfillAsyncIterator();
```


## Motivation
Expand Down Expand Up @@ -140,7 +169,7 @@ npm install aperol@next --save
```

## Conclusion
This library was involved because there was no standardized pure-functional way how handle asynchronous side effects in redux based application.
This library was involved because there was no standardized pure-functional way how process asynchronous side effects in redux based application.
Also missing standard way how to handle continual side-effects.
Aperol was inspired in some existing libraries like a [prism](https://github.com/salsita/prism) or [redux-saga](https://github.com/redux-saga/redux-saga).
Aperol was inspired in some existing libraries like a [prism](https://github.com/salsita/prism), [redux-saga](https://github.com/redux-saga/redux-saga) or [RxJS](https://github.com/Reactive-Extensions/RxJS).
Aperol uses the last syntactic sugar from ES2016/TypeScript like a `async/await`, `iterator`, `asyncIterator` etc. For using is strictly recommended using transpilers like a TypeScript or Babel.
1 change: 1 addition & 0 deletions mocha.opts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
--require ts-node/register
--require ./src/polyfill/asyncIterator.ts
--ui bdd
--watch-extensions ts
tests/**/*.spec.ts
19 changes: 1 addition & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aperol",
"version": "1.0.3",
"version": "2.0.0-next.2",
"description": "JS library for asynchronous processing of side effects in action based application.",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -50,7 +50,6 @@
"@types/should": "^8.3.0",
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"json-loader": "^0.5.4",
"mocha": "^3.4.2",
Expand All @@ -60,7 +59,6 @@
"ts-node": "^3.2.0",
"tslint": "^5.5.0",
"typescript": "^2.4.1",
"webpack": "^1.15.0",
"zen-observable": "^0.5.2"
"webpack": "^1.15.0"
}
}
12 changes: 12 additions & 0 deletions src/AsyncIteratorStarted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

import { Action } from 'redux';

export type AsyncIteratorStartedType = '@@aperol/AsyncIteratorStarted';
export const AsyncIteratorStarted: AsyncIteratorStartedType = '@@aperol/AsyncIteratorStarted';
export interface AsyncIteratorStarted<TSourceAction extends Action> {
type: AsyncIteratorStartedType;
asyncIterator: AsyncIterableIterator<Action>;
promise: Promise<void>;
sourceAction: TSourceAction;
}
export default AsyncIteratorStarted;
11 changes: 11 additions & 0 deletions src/Continual/ContinualAsyncIteratorStarted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

import { Action } from 'redux';

export const ContinualAsyncIteratorStarted = '@@aperol/Continual/ContinualAsyncIteratorStarted';
export interface ContinualAsyncIteratorStarted<TSourceAction extends Action> {
type: typeof ContinualAsyncIteratorStarted;
asyncIterator: AsyncIterableIterator<Action>;
promise: Promise<void>;
sourceAction: TSourceAction;
}
export default ContinualAsyncIteratorStarted;
11 changes: 11 additions & 0 deletions src/Continual/RunAsyncIteratorGeneratorContinual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

import { Action } from 'redux';
import { SourceAction } from '../internalActions';

export const RunAsyncIteratorGeneratorContinual = '@@aperol/Continual/RunAsyncIteratorGeneratorContinual';
export interface RunAsyncIteratorGeneratorContinual extends SourceAction<Action> {
type: typeof RunAsyncIteratorGeneratorContinual;
uid: string;
asyncIteratorGenerator: () => AsyncIterableIterator<Action>;
}
export default RunAsyncIteratorGeneratorContinual;
12 changes: 12 additions & 0 deletions src/Continual/continualActionCreators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

import { Action } from 'redux';
import { generateUid } from '../Helper/hash';
import RunAsyncIteratorGeneratorContinual from './RunAsyncIteratorGeneratorContinual';

export function runItContinual(asyncIteratorGenerator: () => AsyncIterableIterator<Action>) {
return {
type: RunAsyncIteratorGeneratorContinual,
uid: generateUid(),
asyncIteratorGenerator,
} as RunAsyncIteratorGeneratorContinual;
}
36 changes: 36 additions & 0 deletions src/Continual/continualSaga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

import { Action } from 'redux';
import {
PromiseAction,
sourceActionProperty,
} from '../internalActions';
import ContinualAsyncIteratorStarted from './ContinualAsyncIteratorStarted';
import RunAsyncIteratorGeneratorContinual from './RunAsyncIteratorGeneratorContinual';

export interface Model {}

const initialModel = {};

export function reducer(model: Model = initialModel, _action: Action) {
return model;
}

export async function *updater(
_model: Model,
action: RunAsyncIteratorGeneratorContinual & PromiseAction
) {
switch (action.type) {
case RunAsyncIteratorGeneratorContinual:
const sourceAction = action[sourceActionProperty];
const asyncIterator = action.asyncIteratorGenerator();
yield {
type: ContinualAsyncIteratorStarted,
asyncIterator,
sourceAction,
promise: action.__promise,
} as ContinualAsyncIteratorStarted<Action>;
yield* asyncIterator;
break;
default:
}
}
4 changes: 4 additions & 0 deletions src/Helper/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export function generateUid() {
return Math.random().toString().substr(2);
}
9 changes: 9 additions & 0 deletions src/Helper/property.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

export function extendWithInternalProperty(object: object, property: string, value: any) {
Object.defineProperty(object, property, {
enumerable: false,
configurable: false,
writable: false,
value,
});
}
7 changes: 0 additions & 7 deletions src/IPromiseAction.ts

This file was deleted.

3 changes: 1 addition & 2 deletions src/ISaga.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

import { Action } from 'redux';
import IUpdaterYield from './IUpdaterYield';

interface ISaga<TModel> {
reducer(model: TModel, action: Action): TModel;
updater(model: TModel, action: Action): Iterator<IUpdaterYield>;
updater(model: TModel, action: Action): AsyncIterableIterator<Action>;
}
export default ISaga;
16 changes: 0 additions & 16 deletions src/IUpdaterYield.ts

This file was deleted.

12 changes: 0 additions & 12 deletions src/ObservableSubscribed.ts

This file was deleted.

Loading