@reactables/core 0.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -0
- package/dist/Factories/HubFactory.d.ts +2 -0
- package/dist/Factories/HubFactory.test.d.ts +1 -0
- package/dist/Helpers/RxBuilder.d.ts +8 -0
- package/dist/Helpers/addEffects.d.ts +5 -0
- package/dist/Helpers/createSlice.d.ts +24 -0
- package/dist/Helpers/createSlice.test.d.ts +1 -0
- package/dist/Helpers/index.d.ts +1 -0
- package/dist/Models/Action.d.ts +11 -0
- package/dist/Models/Effect.d.ts +3 -0
- package/dist/Models/Hub.d.ts +21 -0
- package/dist/Models/Reactable.d.ts +8 -0
- package/dist/Models/index.d.ts +4 -0
- package/dist/Operators/ofType.d.ts +3 -0
- package/dist/Testing/Actions.d.ts +2 -0
- package/dist/Testing/Effects.d.ts +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +200 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Reactables Core
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Reactive state management with RxJS.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Installation](#installation)
|
|
10
|
+
1. [Core concepts](#core-concepts)
|
|
11
|
+
1. [Reactables](#reactable-concept)
|
|
12
|
+
1. [Hub and Store](#hub-stores)
|
|
13
|
+
1. [Effects](#effects)
|
|
14
|
+
1. [Scoped Effects](#scoped-effects)
|
|
15
|
+
1. [Flow & Containment](#flow-containment)
|
|
16
|
+
1. [Examples](#examples)
|
|
17
|
+
1. [Basic Counter](#basic-counter-example)
|
|
18
|
+
1. [Scoped Effects - Updating Todos](#scoped-effects-example)
|
|
19
|
+
1. [Connecting Multiple Reactables - Event Prices](#connecting-hub-example)
|
|
20
|
+
1. [API](#api)
|
|
21
|
+
1. [Reactable](#reactable)
|
|
22
|
+
1. [RxBuilder](#rx-builder)
|
|
23
|
+
1. [RxConfig](#rx-config)
|
|
24
|
+
1. [Other Interfaces](#interfaces)
|
|
25
|
+
1. [Effect](#api-effect)
|
|
26
|
+
1. [ScopedEffects](#api-scoped-effects)
|
|
27
|
+
1. [Action](#api-action)
|
|
28
|
+
1. [Reducer](#api-reducer)
|
|
29
|
+
1. [Testing](#testing)
|
|
30
|
+
1. [Reactables](#testing-reactables)
|
|
31
|
+
|
|
32
|
+
## Installation <a name="installation"></a>
|
|
33
|
+
|
|
34
|
+
`npm i @reactables/core`
|
|
35
|
+
|
|
36
|
+
## Core concepts <a name="core-concepts"></a>
|
|
37
|
+
|
|
38
|
+
**Prerequisite**: Basic understanding of [Redux](https://redux.js.org/introduction/core-concepts) and [RxJS](https://rxjs.dev/) is helpful.
|
|
39
|
+
|
|
40
|
+
In this documentation the term *stream* will refer to an RxJS observable stream.
|
|
41
|
+
|
|
42
|
+
### Reactables <a name="reactable-concept"></a>
|
|
43
|
+
|
|
44
|
+
[Reactables](#reactable) (prefixed with Rx) are objects that encapulate all the logic required for state management. They expose a `state$` observable and `actions` methods. Applications can subscribe to `state$` to receive state changes and call action methods to trigger them.
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { RxCounter } from '@reactables/examples';
|
|
48
|
+
|
|
49
|
+
const counterReactable = RxCounter();
|
|
50
|
+
|
|
51
|
+
const { state$, actions: { increment, reset } } = counterReactable;
|
|
52
|
+
|
|
53
|
+
state$.subscribe(({ count }) => {
|
|
54
|
+
// Update the count when state changes.
|
|
55
|
+
document.getElementById('count').innerHTML = count;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Bind click handlers
|
|
59
|
+
document.getElementById('increment').addEventListener('click', increment);
|
|
60
|
+
document.getElementById('reset').addEventListener('click', reset);
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
For a full example, see [Basic Counter Example](#basic-counter-example).
|
|
64
|
+
|
|
65
|
+
### Hub and Store <a name="hub-stores"></a>
|
|
66
|
+
|
|
67
|
+
Internally, [Reactables](#reactable-concept) are composed of a hub and store.
|
|
68
|
+
|
|
69
|
+
The hub is responsible for dispatching actions to the store. It is also responsible for handling side effects.
|
|
70
|
+
|
|
71
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/SlideOneHubStore.jpg" width="600" />
|
|
72
|
+
|
|
73
|
+
### Effects<a name="effects"></a>
|
|
74
|
+
|
|
75
|
+
When initializing a [Reactable](#reactable-concept) we can declare effects. The hub will listen for various actions and perform side effects as needed. The store will receive actions resulting from these effects.
|
|
76
|
+
|
|
77
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/SlideTwoEffect.jpg" width="600" />
|
|
78
|
+
|
|
79
|
+
### Scoped Effects <a name="scoped-effects"></a>
|
|
80
|
+
|
|
81
|
+
Scoped Effects are dynamically created streams scoped to a particular action & key combination when an action is dispatch.
|
|
82
|
+
|
|
83
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/SlideThreeScopedEffects.jpg" width="600" />
|
|
84
|
+
|
|
85
|
+
### Flow & Containment <a name="flow-containment"></a>
|
|
86
|
+
Actions and logic flow through the App in one direction and are **contained** in their respective streams. This makes state updates more predictable and traceable during debugging.
|
|
87
|
+
|
|
88
|
+
Avoid [tapping](https://rxjs.dev/api/operators/tap) your streams. This prevents **logic leaking** from the stream(s).
|
|
89
|
+
|
|
90
|
+
- i.e do not [tap](https://rxjs.dev/api/operators/tap) stream A to trigger an action on stream B. Instead consider declaring stream A as a [source](#hub-sources) for stream B so the dependency is explicit.
|
|
91
|
+
|
|
92
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/SlideSevenEightUnidirectionalFlow.jpg" />
|
|
93
|
+
|
|
94
|
+
## Examples <a name="examples"></a>
|
|
95
|
+
|
|
96
|
+
### Basic Counter <a name="basic-counter-example"></a>
|
|
97
|
+
|
|
98
|
+
Basic counter example. Button clicks dispatch actions to increment or reset the counter.
|
|
99
|
+
|
|
100
|
+
Basic Counter | Design Diagram | Try it out on StackBlitz.<br /> Choose your framework
|
|
101
|
+
:-------------------------:|:-------------------------:|:-------------------------:
|
|
102
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/BasicCounterExamplePic.jpg" width="200" /> | <img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/Slide11BasicCounterExample.jpg" width="400" /> | <a href="https://stackblitz.com/edit/github-qtpo1k?file=src%2Findex.js"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/VanillaJS.jpg" width="50" /></a><br /><a href="https://stackblitz.com/edit/github-hfk1t1?file=src%2FCounter.tsx"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/React.png" width="60" /></a><br /><a href="https://stackblitz.com/edit/github-98unub?file=src%2Fapp%2Fcounter%2Fcounter.component.ts"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/Angular.png" width="60" /></a>
|
|
103
|
+
|
|
104
|
+
### Scoped Effects - Updating Todos <a name="scoped-effects-example"></a>
|
|
105
|
+
|
|
106
|
+
Updating statuses of todo items shows scoped effects in action. An 'update todo' stream is created for each todo during update. Pending async calls in their respective stream are cancelled if a new request comes in with RxJS [switchMap](https://www.learnrxjs.io/learn-rxjs/operators/transformation/switchmap) operator.
|
|
107
|
+
|
|
108
|
+
Todo Status Updates | Design Diagram | Try it out on StackBlitz.<br /> Choose your framework
|
|
109
|
+
:-------------------------:|:-------------------------:|:-------------------------:
|
|
110
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/ScopedEffectsTodosPic.jpg" width="200" /> | <img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/Slide12ScopedEffectsExampleTodos.jpg" width="400" /> | <a href="https://stackblitz.com/edit/github-6pgtev?file=src%2Findex.js"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/VanillaJS.jpg" width="50" /></a><br /><a href="https://stackblitz.com/edit/github-1r6pki?file=src%2FTodoUpdates.tsx"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/React.png" width="60" /><br /><a href="https://stackblitz.com/edit/github-zfmupm?file=src%2Fapp%2Fapp.component.ts,src%2Fapp%2Ftodos%2Ftodos.component.ts"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/Angular.png" width="60" /></a>
|
|
111
|
+
|
|
112
|
+
### Connecting Multiple Reactables - Event Prices <a name="connecting-hub-example"></a>
|
|
113
|
+
|
|
114
|
+
This examples shows two set reactables. The first is responsible for updating state of the user controls. The second fetches prices based on input from the first set.
|
|
115
|
+
|
|
116
|
+
Event Prices | Design Diagram | Try it out on StackBlitz.<br /> Choose your framework
|
|
117
|
+
:-------------------------:|:-------------------------:|:-------------------------:
|
|
118
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/ConnectingHubsEventPricesPic.jpg" width="200" /> | <img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/Slide13ConnectingHubsExampleEventPrices.jpg" width="400" /> | <a href="https://stackblitz.com/edit/github-pgpwly?file=src%2findex.js"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/VanillaJS.jpg" width="50" /></a><br /><a href="https://stackblitz.com/edit/github-eypqvc?file=src%2FEventTickets.tsx"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/React.png" width="60" /></a><br /><a href="https://stackblitz.com/edit/github-66mbtu?file=src%2Fapp%2Fevent-tickets%2Fevent-tickets.component.ts"><img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/Angular.png" width="60" /></a>
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## API <a name="api"></a>
|
|
122
|
+
|
|
123
|
+
### Reactable <a name="reactable"></a>
|
|
124
|
+
|
|
125
|
+
Reactables provide the API for applications and UI components to receive and trigger state updates.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
export interface Reactable<T, S = ActionMap> {
|
|
129
|
+
state$: Observable<T>;
|
|
130
|
+
actions?: S;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ActionMap {
|
|
134
|
+
[key: string | number]: (payload?: unknown) => void;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
| Property | Description |
|
|
138
|
+
| -------- | ----------- |
|
|
139
|
+
| state$ | Observable that emit state changes |
|
|
140
|
+
| actions (optional) | Dictionary of methods that dispatches actions to update state |
|
|
141
|
+
|
|
142
|
+
### RxBuilder <a name="rx-builder"></a>
|
|
143
|
+
|
|
144
|
+
RxBuilder factory help build [Reactables](#reactable). Accepts a [RxConfig](#rx-confg) configuration object
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
type RxBuilder = <T, S extends Cases<T>>(config: RxConfig<T, S>) => Reactable<T, unknown>
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### RxConfig <a name="rx-config"></a>
|
|
152
|
+
|
|
153
|
+
Configuration object for creating Reactables.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
interface RxConfig <T, S extends Cases<T>>{
|
|
157
|
+
initialState: T;
|
|
158
|
+
reducers: S;
|
|
159
|
+
storeValue?: boolean;
|
|
160
|
+
debug?: boolean;
|
|
161
|
+
effects?: Effect<unknown, unknown>[];
|
|
162
|
+
sources?: Observable<Action<unknown>>[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface Cases<T> {
|
|
166
|
+
[key: string]: SingleActionReducer<T, unknown>
|
|
167
|
+
| {
|
|
168
|
+
reducer: SingleActionReducer<T, unknown>
|
|
169
|
+
effects?: (payload?: unknown) => ScopedEffects<unknown>
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type SingleActionReducer<T, S> = (state: T, action: Action<S>) => T;
|
|
174
|
+
```
|
|
175
|
+
| Property | Description |
|
|
176
|
+
| -------- | ----------- |
|
|
177
|
+
| initialState | Initial state of the Reactable |
|
|
178
|
+
| reducers | Dictionary of cases for the Reactable to handle. Each case can be a reducer function or a configuration object. RxBuilder will use this to generate Actions, Reducers, and add [ScopedEffects](#api-scoped-effects). |
|
|
179
|
+
| debug (optional) | to turn on debugging to console.log all messages received by the store and state changes |
|
|
180
|
+
| storeValue (optional) | Option to store value if Reactable is used to persist application state. Subsequent subscriptions will receive the latest stored value. Default to false |
|
|
181
|
+
| effects (optional) | Array of [Effects](#api-effects) to be registered to the Reactable |
|
|
182
|
+
| sources (optional) <a name="hub-sources"></a> | Additional [Action](#api-actions) Observables the Reactable is listening to |
|
|
183
|
+
|
|
184
|
+
Debug Example:
|
|
185
|
+
|
|
186
|
+
<img src="https://raw.githubusercontent.com/reactables/reactables/main/documentation/SlideSixDebug.jpg" width="500" />
|
|
187
|
+
|
|
188
|
+
### Other Interfaces <a name="interfaces"></a>
|
|
189
|
+
|
|
190
|
+
#### Effect <a name="api-effect"></a>
|
|
191
|
+
|
|
192
|
+
Effects are expressed as [RxJS Operator Functions](https://rxjs.dev/api/index/interface/OperatorFunction). They pipe the [dispatcher$](#hub-dispatcher) stream and run side effects on incoming [Actions](#api-action).
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
type Effect<T, S> = OperatorFunction<Action<T>, Action<S>>;
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### ScopedEffects <a name="api-scoped-effects"></a>
|
|
199
|
+
|
|
200
|
+
Scoped Effects are declared in [Actions](#api-action). They are dynamically created stream(s) scoped to an Action `type` & `key` combination.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
interface ScopedEffects<T> {
|
|
204
|
+
key?: string;
|
|
205
|
+
effects: Effect<T, unknown>[];
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
| Property | Description |
|
|
209
|
+
| -------- | ----------- |
|
|
210
|
+
| key (optional) | key to be combined with the Action `type` to generate a unique signature for the effect stream(s). Example: An id for the entity the action is being performed on. |
|
|
211
|
+
| effects | Array of [Effects](#api-effects) scoped to the Action `type` & `key` |
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
|
|
217
|
+
const UPDATE_TODO = 'UPDATE_TODO';
|
|
218
|
+
const UPDATE_TODO_SUCCESS = 'UPDATE_TODO_SUCCESS';
|
|
219
|
+
const updateTodo = ({ id, message }, todoService: TodoService) => ({
|
|
220
|
+
type: UPDATE_TODO,
|
|
221
|
+
payload: { id, message },
|
|
222
|
+
scopedEffects: {
|
|
223
|
+
key: id,
|
|
224
|
+
effects: [
|
|
225
|
+
(updateTodoActions$: Observable<Action<string>>) =>
|
|
226
|
+
updateTodoActions$.pipe(
|
|
227
|
+
mergeMap(({ payload: { id, message } }) => todoService.updateTodo(id, message))
|
|
228
|
+
map(({ data }) => ({
|
|
229
|
+
type: UPDATE_TODO_SUCCESS,
|
|
230
|
+
payload: data
|
|
231
|
+
}))
|
|
232
|
+
)
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Action <a name="api-action"></a>
|
|
239
|
+
```typescript
|
|
240
|
+
interface Action<T = undefined> {
|
|
241
|
+
type: string;
|
|
242
|
+
payload?: T;
|
|
243
|
+
scopedEffects?: ScopedEffects<T>;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
| Property | Description |
|
|
247
|
+
| -------- | ----------- |
|
|
248
|
+
| type | type of Action being dispatched |
|
|
249
|
+
| payload (optional) | payload associated with Action |
|
|
250
|
+
| scopedEffects (optional) | [See ScopedEffects](#api-scoped-effects) |
|
|
251
|
+
|
|
252
|
+
#### Reducer <a name="api-reducer"></a>
|
|
253
|
+
|
|
254
|
+
From [Redux Docs](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers)
|
|
255
|
+
> Reducers are functions that take the current state and an action as arguments, and return a new state result
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
type Reducer<T> = (state?: T, action?: Action<unknown>) => T;
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Testing <a name="testing"></a>
|
|
262
|
+
|
|
263
|
+
We can use RxJS's built in [Marble Testing](https://rxjs.dev/guide/testing/marble-testing) for testing [Reactables](#reactable).
|
|
264
|
+
|
|
265
|
+
### Reactables <a name="testing-reactables"></a>
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// https://github.com/reactables/reactables/blob/main/packages/examples/src/Counter/Counter.test.ts
|
|
269
|
+
import { Counter } from './Counter';
|
|
270
|
+
import { Subscription } from 'rxjs';
|
|
271
|
+
import { TestScheduler } from 'rxjs/testing';
|
|
272
|
+
|
|
273
|
+
describe('Counter', () => {
|
|
274
|
+
let testScheduler: TestScheduler;
|
|
275
|
+
let subscription: Subscription;
|
|
276
|
+
|
|
277
|
+
beforeEach(() => {
|
|
278
|
+
testScheduler = new TestScheduler((actual, expected) => {
|
|
279
|
+
expect(actual).toEqual(expected);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
afterEach(() => {
|
|
283
|
+
subscription?.unsubscribe();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should increment and reset', () => {
|
|
287
|
+
testScheduler.run(({ expectObservable, cold }) => {
|
|
288
|
+
// Create Counter Reactable
|
|
289
|
+
const {
|
|
290
|
+
state$,
|
|
291
|
+
actions: { increment, reset },
|
|
292
|
+
} = Counter();
|
|
293
|
+
|
|
294
|
+
// Call actions
|
|
295
|
+
subscription = cold('--b-c', {
|
|
296
|
+
b: increment,
|
|
297
|
+
c: reset,
|
|
298
|
+
}).subscribe((action) => action());
|
|
299
|
+
|
|
300
|
+
// Assertions
|
|
301
|
+
expectObservable(state$).toBe('a-b-c', {
|
|
302
|
+
a: { count: 0 },
|
|
303
|
+
b: { count: 1 },
|
|
304
|
+
c: { count: 0 },
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SliceConfig, Cases } from './createSlice';
|
|
2
|
+
import { HubConfig } from '../Models/Hub';
|
|
3
|
+
import { Reactable } from '../Models/Reactable';
|
|
4
|
+
export interface RxConfig<T, S extends Cases<T>> extends SliceConfig<T, S>, HubConfig {
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
storeValue?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const RxBuilder: <T, S extends Cases<T>>({ effects, sources, debug, storeValue, ...sliceConfig }: RxConfig<T, S>) => Reactable<T, { [K in keyof S]: (payload?: unknown) => void; }>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Action, ActionCreator, ScopedEffects } from '../Models/Action';
|
|
2
|
+
import { Effect } from '../Models/Effect';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
export declare const addEffects: <T>(actionCreator: ActionCreator<T>, scopedEffects: (payload: T) => ScopedEffects<T>) => ActionCreator<T>;
|
|
5
|
+
export declare const createEffect: <T, S>(action: string, operator: (actions$: Observable<Action<T>>) => Observable<Action<S>>) => Effect<T, S>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Action, ActionCreator, ScopedEffects } from '../Models/Action';
|
|
2
|
+
import { Reducer } from '../Models/Hub';
|
|
3
|
+
export type SingleActionReducer<T, S> = (state: T, action: Action<S>) => T;
|
|
4
|
+
export interface Slice<T> {
|
|
5
|
+
actions: {
|
|
6
|
+
[key: string]: ActionCreator<unknown>;
|
|
7
|
+
};
|
|
8
|
+
reducer: Reducer<T>;
|
|
9
|
+
}
|
|
10
|
+
export interface Cases<T> {
|
|
11
|
+
[key: string]: SingleActionReducer<T, unknown> | {
|
|
12
|
+
reducer: SingleActionReducer<T, unknown>;
|
|
13
|
+
effects?: (payload?: unknown) => ScopedEffects<unknown>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface SliceConfig<T, S extends Cases<T>> {
|
|
17
|
+
initialState: T;
|
|
18
|
+
reducers: S;
|
|
19
|
+
name?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const createSlice: <T, S extends Cases<T>>(config: SliceConfig<T, S>) => {
|
|
22
|
+
reducer: Reducer<T>;
|
|
23
|
+
actions: { [K in keyof S]: ActionCreator<unknown>; };
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RxBuilder } from './RxBuilder';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect } from './Effect';
|
|
2
|
+
export interface ScopedEffects<T> {
|
|
3
|
+
key?: string | number;
|
|
4
|
+
effects: Effect<T, unknown>[];
|
|
5
|
+
}
|
|
6
|
+
export interface Action<T = undefined> {
|
|
7
|
+
type: string;
|
|
8
|
+
scopedEffects?: ScopedEffects<T>;
|
|
9
|
+
payload?: T;
|
|
10
|
+
}
|
|
11
|
+
export type ActionCreator<T> = (payload?: T) => Action<T>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Action } from './Action';
|
|
2
|
+
import { Effect } from './Effect';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
export type Reducer<T> = (state?: T, action?: Action<unknown>) => T;
|
|
5
|
+
export interface StoreConfig<T> {
|
|
6
|
+
reducer: Reducer<T>;
|
|
7
|
+
initialState?: T;
|
|
8
|
+
name?: string;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
storeValue?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type Dispatcher = <T>(...actions: Action<T>[]) => void;
|
|
13
|
+
export interface HubConfig {
|
|
14
|
+
effects?: Effect<unknown, unknown>[];
|
|
15
|
+
sources?: Observable<Action<unknown>>[];
|
|
16
|
+
}
|
|
17
|
+
export interface Hub {
|
|
18
|
+
messages$: Observable<Action<unknown>>;
|
|
19
|
+
store: <T>(config: StoreConfig<T>) => Observable<T>;
|
|
20
|
+
dispatch: Dispatcher;
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { Action } from '../Models/Action';
|
|
3
|
+
export declare const switchMapTestEffect: (action$: Observable<Action<string>>) => Observable<{
|
|
4
|
+
type: string;
|
|
5
|
+
payload: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const debounceTestEffect: (action$: Observable<Action<string>>) => Observable<{
|
|
8
|
+
type: string;
|
|
9
|
+
payload: string;
|
|
10
|
+
}>;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var rxjs = require('rxjs');
|
|
6
|
+
var operators = require('rxjs/operators');
|
|
7
|
+
|
|
8
|
+
/******************************************************************************
|
|
9
|
+
Copyright (c) Microsoft Corporation.
|
|
10
|
+
|
|
11
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
12
|
+
purpose with or without fee is hereby granted.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
15
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
16
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
17
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
18
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
19
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
20
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
21
|
+
***************************************************************************** */
|
|
22
|
+
|
|
23
|
+
var __assign = function() {
|
|
24
|
+
__assign = Object.assign || function __assign(t) {
|
|
25
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
26
|
+
s = arguments[i];
|
|
27
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
|
|
28
|
+
}
|
|
29
|
+
return t;
|
|
30
|
+
};
|
|
31
|
+
return __assign.apply(this, arguments);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function __rest(s, e) {
|
|
35
|
+
var t = {};
|
|
36
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
37
|
+
t[p] = s[p];
|
|
38
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
39
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
40
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
41
|
+
t[p[i]] = s[p[i]];
|
|
42
|
+
}
|
|
43
|
+
return t;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function __spreadArray(to, from, pack) {
|
|
47
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
48
|
+
if (ar || !(i in from)) {
|
|
49
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
50
|
+
ar[i] = from[i];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
57
|
+
var e = new Error(message);
|
|
58
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
var addEffects = function (actionCreator, scopedEffects) {
|
|
62
|
+
return function (payload) { return (__assign(__assign({}, actionCreator(payload)), { scopedEffects: scopedEffects(payload) })); };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
var createSlice = function (config) {
|
|
66
|
+
var name = config.name, initialState = config.initialState, reducers = config.reducers;
|
|
67
|
+
var reducer = Object.entries(reducers).reduce(function (acc, _a) {
|
|
68
|
+
var key = _a[0], _case = _a[1];
|
|
69
|
+
var _reducer = typeof _case === 'function' ? _case : _case.reducer;
|
|
70
|
+
var newFunc = function (state, action) {
|
|
71
|
+
if (action && action.type === "".concat(name ? "".concat(name, "/") : '').concat(key)) {
|
|
72
|
+
return _reducer(state, action);
|
|
73
|
+
}
|
|
74
|
+
return acc(state, action);
|
|
75
|
+
};
|
|
76
|
+
return newFunc;
|
|
77
|
+
}, function (state) {
|
|
78
|
+
if (state === void 0) { state = initialState; }
|
|
79
|
+
return state;
|
|
80
|
+
});
|
|
81
|
+
var actions = Object.entries(reducers).reduce(function (acc, _a) {
|
|
82
|
+
var key = _a[0], _case = _a[1];
|
|
83
|
+
acc[key] = function (payload) { return ({
|
|
84
|
+
type: "".concat(name ? "".concat(name, "/") : '').concat(key),
|
|
85
|
+
payload: payload
|
|
86
|
+
}); };
|
|
87
|
+
if (typeof _case !== 'function' && _case.effects) {
|
|
88
|
+
acc[key] = addEffects(acc[key], _case.effects);
|
|
89
|
+
}
|
|
90
|
+
return acc;
|
|
91
|
+
}, {});
|
|
92
|
+
return {
|
|
93
|
+
reducer: reducer,
|
|
94
|
+
actions: actions
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
var getScopedEffectSignature = function (actionType, key) {
|
|
99
|
+
return "type: ".concat(actionType, ", scoped: true").concat(key ? ",key:".concat(key) : '');
|
|
100
|
+
};
|
|
101
|
+
var HubFactory = function (_a) {
|
|
102
|
+
var _b = _a === void 0 ? {} : _a, effects = _b.effects, _c = _b.sources, sources = _c === void 0 ? [] : _c;
|
|
103
|
+
var dispatcher$ = new rxjs.ReplaySubject(1);
|
|
104
|
+
var inputStream$ = rxjs.merge.apply(void 0, __spreadArray([dispatcher$], sources.map(function (source) { return source.pipe(operators.shareReplay(1)); }), false));
|
|
105
|
+
var genericEffects = (effects === null || effects === void 0 ? void 0 : effects.reduce(function (result, effect) {
|
|
106
|
+
return result.concat(inputStream$.pipe(effect));
|
|
107
|
+
}, [])) || [];
|
|
108
|
+
// Should we keep this in the stream with a scan operator instead?
|
|
109
|
+
var scopedEffectsDict = {};
|
|
110
|
+
var mergedScopedEffects = inputStream$.pipe(operators.filter(function (_a) {
|
|
111
|
+
var type = _a.type, scopedEffects = _a.scopedEffects;
|
|
112
|
+
var hasEffects = Boolean(scopedEffects && scopedEffects.effects.length);
|
|
113
|
+
return (hasEffects &&
|
|
114
|
+
scopedEffectsDict[getScopedEffectSignature(type, scopedEffects.key)] === undefined);
|
|
115
|
+
}), operators.tap(function (_a) {
|
|
116
|
+
var type = _a.type, _b = _a.scopedEffects, key = _b.key, effects = _b.effects;
|
|
117
|
+
scopedEffectsDict[getScopedEffectSignature(type, key)] = effects;
|
|
118
|
+
}), operators.map(function (_a) {
|
|
119
|
+
var type = _a.type, _b = _a.scopedEffects, key = _b.key, effects = _b.effects;
|
|
120
|
+
var signature = getScopedEffectSignature(type, key);
|
|
121
|
+
var pipedEffects = effects.reduce(function (acc, effect) {
|
|
122
|
+
return acc.concat(inputStream$.pipe(operators.filter(function (initialAction) {
|
|
123
|
+
var _a;
|
|
124
|
+
return getScopedEffectSignature(initialAction.type, (_a = initialAction.scopedEffects) === null || _a === void 0 ? void 0 : _a.key) ===
|
|
125
|
+
signature;
|
|
126
|
+
}), effect));
|
|
127
|
+
}, []);
|
|
128
|
+
return rxjs.merge.apply(void 0, pipedEffects);
|
|
129
|
+
}), operators.mergeAll());
|
|
130
|
+
var messages$ = rxjs.merge.apply(void 0, __spreadArray([inputStream$, mergedScopedEffects], genericEffects, false)).pipe(operators.share());
|
|
131
|
+
var store = function (_a) {
|
|
132
|
+
var reducer = _a.reducer, name = _a.name, debug = _a.debug, initialState = _a.initialState, _b = _a.storeValue, storeValue = _b === void 0 ? false : _b;
|
|
133
|
+
var debugName = "[Stream Name] ".concat(name || 'undefined');
|
|
134
|
+
var seedState = initialState !== undefined ? initialState : reducer();
|
|
135
|
+
var state$ = messages$.pipe(operators.tap(function (action) {
|
|
136
|
+
debug && console.log(debugName, '[Message Received]', action);
|
|
137
|
+
}), operators.scan(reducer, seedState), operators.startWith(null, seedState), operators.pairwise(), operators.tap(function (_a) {
|
|
138
|
+
var prevState = _a[0], newState = _a[1];
|
|
139
|
+
if (debug) {
|
|
140
|
+
var hasDiff = prevState !== newState;
|
|
141
|
+
if (hasDiff) {
|
|
142
|
+
console.log(debugName, '[State changed] Prev State:', prevState, 'New State:', newState);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(debugName, '[State unchanged] State:', newState);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}), operators.map(function (pair) { return pair[1]; }));
|
|
149
|
+
if (storeValue) {
|
|
150
|
+
var replaySubject_1 = new rxjs.ReplaySubject(1);
|
|
151
|
+
state$.subscribe(function (state) { return replaySubject_1.next(state); });
|
|
152
|
+
return replaySubject_1;
|
|
153
|
+
}
|
|
154
|
+
return state$;
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
messages$: messages$,
|
|
158
|
+
store: store,
|
|
159
|
+
dispatch: function () {
|
|
160
|
+
var actions = [];
|
|
161
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
162
|
+
actions[_i] = arguments[_i];
|
|
163
|
+
}
|
|
164
|
+
actions.forEach(function (action) {
|
|
165
|
+
dispatcher$.next(action);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
var RxBuilder = function (_a) {
|
|
172
|
+
var effects = _a.effects, _b = _a.sources, sources = _b === void 0 ? [] : _b, _c = _a.debug, debug = _c === void 0 ? false : _c, _d = _a.storeValue, storeValue = _d === void 0 ? false : _d, sliceConfig = __rest(_a, ["effects", "sources", "debug", "storeValue"]);
|
|
173
|
+
var _e = createSlice(sliceConfig), reducer = _e.reducer, actions = _e.actions;
|
|
174
|
+
// Check sources and see if need to add effects
|
|
175
|
+
sources = sources.map(function (action$) {
|
|
176
|
+
return action$.pipe(rxjs.map(function (action) {
|
|
177
|
+
var _case = sliceConfig.reducers[action.type];
|
|
178
|
+
if (typeof _case !== 'function' && _case.effects) {
|
|
179
|
+
return __assign(__assign({}, action), { scopedEffects: _case.effects(action.payload) });
|
|
180
|
+
}
|
|
181
|
+
return action;
|
|
182
|
+
}));
|
|
183
|
+
});
|
|
184
|
+
var hub = HubFactory({ effects: effects, sources: sources });
|
|
185
|
+
var actionsResult = Object.fromEntries(Object.entries(actions).map(function (_a) {
|
|
186
|
+
var key = _a[0], actionCreator = _a[1];
|
|
187
|
+
return [
|
|
188
|
+
key,
|
|
189
|
+
function (payload) {
|
|
190
|
+
hub.dispatch(actionCreator(payload));
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
}));
|
|
194
|
+
return {
|
|
195
|
+
state$: hub.store({ reducer: reducer, debug: debug, storeValue: storeValue }),
|
|
196
|
+
actions: actionsResult
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
exports.RxBuilder = RxBuilder;
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reactables/core",
|
|
3
|
+
"description": "State management with rxjs",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"build": "rimraf dist && rollup --config",
|
|
11
|
+
"lint": "eslint --max-warnings 0 \"src/**/*.ts*\" && prettier --check src/",
|
|
12
|
+
"fix": "eslint --fix \"src/**/*.ts*\" && prettier --write \"src/**/*.(ts*|scss)\""
|
|
13
|
+
},
|
|
14
|
+
"author": "David Lai",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"rxjs": "^6.0.0 || ^7.0.0"
|
|
18
|
+
},
|
|
19
|
+
"version": "0.4.0-alpha.0"
|
|
20
|
+
}
|