@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 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,2 @@
1
+ import { Hub, HubConfig } from '../Models/Hub';
2
+ export declare const HubFactory: ({ effects, sources }?: HubConfig) => Hub;
@@ -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,3 @@
1
+ import { OperatorFunction } from 'rxjs';
2
+ import { Action } from './Action';
3
+ export type Effect<T, S> = OperatorFunction<Action<T>, Action<S>>;
@@ -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,8 @@
1
+ import { Observable } from 'rxjs';
2
+ export interface Reactable<T, S = ActionMap> {
3
+ state$: Observable<T>;
4
+ actions: S;
5
+ }
6
+ export interface ActionMap {
7
+ [key: string | number]: (payload?: unknown) => void;
8
+ }
@@ -0,0 +1,4 @@
1
+ export { Action, ScopedEffects } from './Action';
2
+ export { Effect } from './Effect';
3
+ export { Hub, Reducer, HubConfig, StoreConfig, Dispatcher } from './Hub';
4
+ export { Reactable, ActionMap } from './Reactable';
@@ -0,0 +1,3 @@
1
+ import { OperatorFunction } from 'rxjs';
2
+ import { Action } from '../Models/Action';
3
+ export declare const ofType: (type: string) => OperatorFunction<Action<unknown>, Action<unknown>>;
@@ -0,0 +1,2 @@
1
+ export declare const TEST_ACTION = "TEST_ACTION";
2
+ export declare const TEST_ACTION_SUCCESS = "TEST_ACTION_SUCCESS";
@@ -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
+ }>;
@@ -0,0 +1,2 @@
1
+ export * from './Models';
2
+ export * from './Helpers';
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
+ }