rx-tiny-flux 1.0.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/LICENSE +21 -0
- package/Readme.md +297 -0
- package/dist/rx-tiny-flux.esm.js +2175 -0
- package/examples/counter.js +132 -0
- package/jsconfig.json +8 -0
- package/package.json +40 -0
- package/rollup.config.js +43 -0
- package/src/actions.js +16 -0
- package/src/effects.js +33 -0
- package/src/index.js +8 -0
- package/src/reducers.js +67 -0
- package/src/rxjs.js +20 -0
- package/src/selectors.js +39 -0
- package/src/store.js +125 -0
- package/src/utils.js +25 -0
- package/src/zeppos.js +62 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Store,
|
|
3
|
+
createAction,
|
|
4
|
+
createReducer,
|
|
5
|
+
createEffect,
|
|
6
|
+
on,
|
|
7
|
+
anyAction,
|
|
8
|
+
createFeatureSelector,
|
|
9
|
+
createSelector,
|
|
10
|
+
ofType,
|
|
11
|
+
concatMap,
|
|
12
|
+
from,
|
|
13
|
+
map,
|
|
14
|
+
} from '../dist/rx-tiny-flux.esm.js'; // Pointing to the built file for a realistic test
|
|
15
|
+
|
|
16
|
+
// 1. ESTADO INICIAL
|
|
17
|
+
// The Store can start with an empty state.
|
|
18
|
+
const initialState = {};
|
|
19
|
+
|
|
20
|
+
// 2. AÇÕES
|
|
21
|
+
// We create "messages" that describe events in the application.
|
|
22
|
+
const increment = createAction('[Counter] Increment');
|
|
23
|
+
const decrement = createAction('[Counter] Decrement');
|
|
24
|
+
const incrementAsync = createAction('[Counter] Increment Async');
|
|
25
|
+
const incrementSuccess = createAction('[Counter] Increment Success');
|
|
26
|
+
|
|
27
|
+
// 3. REDUTORES
|
|
28
|
+
// Pure functions that calculate the new state based on the previous state and an action.
|
|
29
|
+
|
|
30
|
+
// This reducer manages the 'counter' slice of the state.
|
|
31
|
+
const counterReducer = createReducer(
|
|
32
|
+
'counter',
|
|
33
|
+
{ value: 0, lastUpdate: null }, // 1. Initial state for this slice.
|
|
34
|
+
on(increment, incrementSuccess, (state) => ({
|
|
35
|
+
...state,
|
|
36
|
+
value: state.value + 1,
|
|
37
|
+
lastUpdate: new Date().toISOString(),
|
|
38
|
+
})),
|
|
39
|
+
on(decrement, (state) => ({
|
|
40
|
+
...state,
|
|
41
|
+
value: state.value - 1,
|
|
42
|
+
lastUpdate: new Date().toISOString(),
|
|
43
|
+
}))
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// This reducer manages the 'log' slice. It reacts to ANY action.
|
|
47
|
+
const logReducer = createReducer(
|
|
48
|
+
'log',
|
|
49
|
+
[], // Initial state for this slice
|
|
50
|
+
// Using the `anyAction` token to create a handler that catches all actions.
|
|
51
|
+
on(anyAction, (state, action) => [...state, `Action: ${action.type} at ${new Date().toISOString()}`])
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// 4. EFEITOS
|
|
55
|
+
// Handle side effects, such as asynchronous calls.
|
|
56
|
+
|
|
57
|
+
// Let's create a function that simulates an API call returning a Promise.
|
|
58
|
+
const fakeApiCall = () => {
|
|
59
|
+
console.log('-> [Effect] Starting fake API call...');
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
console.log('<- [Effect] Fake API call finished.');
|
|
63
|
+
resolve({ success: true }); // The API returns a success object
|
|
64
|
+
}, 1000);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const incrementAsyncEffect = createEffect((actions$) =>
|
|
69
|
+
actions$.pipe(
|
|
70
|
+
ofType(incrementAsync), // Listens only for the 'incrementAsync' action
|
|
71
|
+
// Use concatMap to handle the async operation. It waits for the inner Observable to complete.
|
|
72
|
+
concatMap(() => from(fakeApiCall()).pipe( // `from` converts the Promise into an Observable
|
|
73
|
+
map(() => incrementSuccess()) // On success, map the result to a new action
|
|
74
|
+
))
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// 5. SELETORES
|
|
79
|
+
// Functions to extract specific pieces of state.
|
|
80
|
+
|
|
81
|
+
// 5.1. We use `createFeatureSelector` to get a top-level slice of the state.
|
|
82
|
+
const selectCounterSlice = createFeatureSelector('counter');
|
|
83
|
+
|
|
84
|
+
// 5.2. We use `createSelector` to compose and extract more granular data from the slice.
|
|
85
|
+
const selectCounterValue = createSelector(
|
|
86
|
+
selectCounterSlice,
|
|
87
|
+
(counter) => counter?.value // Added 'optional chaining' for safety
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const selectLastUpdate = createSelector(
|
|
91
|
+
selectCounterSlice,
|
|
92
|
+
(counter) => counter?.lastUpdate
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// 6. STORE SETUP
|
|
96
|
+
const store = new Store(initialState);
|
|
97
|
+
store.registerReducers(counterReducer, logReducer);
|
|
98
|
+
store.registerEffects(incrementAsyncEffect);
|
|
99
|
+
|
|
100
|
+
// 7. USING THE STORE
|
|
101
|
+
// We subscribe to the selector to observe changes in the counter's value.
|
|
102
|
+
// It's crucial to capture the subscription object so we can unsubscribe later.
|
|
103
|
+
const counterSubscription = store.select(selectCounterValue).subscribe((value) => {
|
|
104
|
+
console.log(`Counter value is now: ${value}`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Let's also subscribe to the log to see all actions
|
|
108
|
+
const logSubscription = store.select((state) => state.log).subscribe((log) => {
|
|
109
|
+
console.log('--- Action Log ---');
|
|
110
|
+
console.log(log.join('\n'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 8. DISPATCHING ACTIONS
|
|
114
|
+
console.log('Dispatching actions...');
|
|
115
|
+
store.dispatch(increment());
|
|
116
|
+
store.dispatch(increment());
|
|
117
|
+
store.dispatch(incrementAsync()); // Will trigger the effect and update the counter after 1s.
|
|
118
|
+
|
|
119
|
+
// 9. CLEANUP (Unsubscribing)
|
|
120
|
+
// In a real application (like a component in a UI framework or a long-running service),
|
|
121
|
+
// you must unsubscribe from subscriptions to prevent memory leaks when they are no longer needed.
|
|
122
|
+
// In this simple script, the process would exit anyway, but we demonstrate the practice here
|
|
123
|
+
// by unsubscribing after a few seconds.
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
console.log('\n--- Cleaning up subscriptions ---');
|
|
126
|
+
counterSubscription.unsubscribe();
|
|
127
|
+
logSubscription.unsubscribe();
|
|
128
|
+
console.log('Subscriptions cleaned up. Further state changes will not be logged to the console.');
|
|
129
|
+
|
|
130
|
+
// This action will still be processed by the store, but our subscriptions won't react to it.
|
|
131
|
+
store.dispatch(increment());
|
|
132
|
+
}, 2000);
|
package/jsconfig.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rx-tiny-flux",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight, minimalist state management library for pure JavaScript projects, inspired by NgRx and Redux, and built with RxJS.",
|
|
5
|
+
"author": "Bernardo Baumblatt <baumblatt@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/baumblatt/rx-tiny-flux.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"rxjs",
|
|
13
|
+
"state-management",
|
|
14
|
+
"redux",
|
|
15
|
+
"ngrx",
|
|
16
|
+
"flux",
|
|
17
|
+
"zeppos",
|
|
18
|
+
"tiny",
|
|
19
|
+
"lightweight"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "dist/rx-tiny-flux.esm.js",
|
|
23
|
+
"zeppos": true,
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./dist/rx-tiny-flux.esm.js",
|
|
26
|
+
"./zeppos": "./dist/zeppos.esm.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"start": "node examples/counter.js",
|
|
30
|
+
"build": "rollup -c"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
34
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
|
35
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
36
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
37
|
+
"rollup": "^4.9.6",
|
|
38
|
+
"rxjs": "^7.8.2"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
2
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
3
|
+
import terser from '@rollup/plugin-terser';
|
|
4
|
+
import json from '@rollup/plugin-json';
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
// Main library bundle
|
|
8
|
+
{
|
|
9
|
+
input: 'src/index.js',
|
|
10
|
+
output: [
|
|
11
|
+
{
|
|
12
|
+
file: 'dist/rx-tiny-flux.esm.js',
|
|
13
|
+
format: 'es',
|
|
14
|
+
sourcemap: false,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
file: 'dist/rx-tiny-flux.esm.min.js',
|
|
18
|
+
format: 'es',
|
|
19
|
+
sourcemap: false,
|
|
20
|
+
plugins: [terser()],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
plugins: [json(), nodeResolve(), commonjs()],
|
|
24
|
+
},
|
|
25
|
+
// Third entry point for ZeppOS plugin
|
|
26
|
+
{
|
|
27
|
+
input: 'src/zeppos.js',
|
|
28
|
+
output: [
|
|
29
|
+
{
|
|
30
|
+
file: 'dist/zeppos.esm.js',
|
|
31
|
+
format: 'es',
|
|
32
|
+
sourcemap: false,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
file: 'dist/zeppos.esm.min.js',
|
|
36
|
+
format: 'es',
|
|
37
|
+
sourcemap: false,
|
|
38
|
+
plugins: [terser()],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
plugins: [json(), nodeResolve(), commonjs()],
|
|
42
|
+
},
|
|
43
|
+
];
|
package/src/actions.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} Action
|
|
3
|
+
* @property {string} type - The action type, a unique string describing it.
|
|
4
|
+
* @property {any} [payload] - The data associated with the action.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Factory function to create an action.
|
|
9
|
+
* @param {string} type - The action type.
|
|
10
|
+
* @returns {function(any=): Action} A function that creates the action with an optional payload.
|
|
11
|
+
*/
|
|
12
|
+
export function createAction(type) {
|
|
13
|
+
const actionCreator = (payload) => ({ type, payload });
|
|
14
|
+
actionCreator.type = type; // Attaches the type directly to the function for easy access
|
|
15
|
+
return actionCreator;
|
|
16
|
+
}
|
package/src/effects.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { filter } from 'rxjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./actions').Action} Action
|
|
5
|
+
* @typedef {import('rxjs').Observable<Action>} ActionStream
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom RxJS operator to filter actions by type.
|
|
10
|
+
* @param {...(function(any=): Action)} actionCreators - The action creators whose types will be used for filtering.
|
|
11
|
+
* @returns {import('rxjs').OperatorFunction<Action, Action>}
|
|
12
|
+
*/
|
|
13
|
+
export function ofType(...actionCreators) {
|
|
14
|
+
// Extracts the 'type' string from each action creator passed as an argument.
|
|
15
|
+
const allowedTypes = actionCreators.map(creator => creator.type);
|
|
16
|
+
|
|
17
|
+
return filter(action => allowedTypes.includes(action.type));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Factory function to create an effect.
|
|
22
|
+
* An effect is a function that receives the stream of all actions
|
|
23
|
+
* and returns a new stream of actions to be dispatched.
|
|
24
|
+
*
|
|
25
|
+
* @param {function(ActionStream): ActionStream} effectFn
|
|
26
|
+
* @returns {function(ActionStream): ActionStream} The effect function.
|
|
27
|
+
*/
|
|
28
|
+
export function createEffect(effectFn) {
|
|
29
|
+
if (typeof effectFn !== 'function') {
|
|
30
|
+
throw new Error('Effect must be a function.');
|
|
31
|
+
}
|
|
32
|
+
return effectFn;
|
|
33
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Store } from './store.js';
|
|
2
|
+
export { createAction } from './actions.js';
|
|
3
|
+
export { createReducer, on, anyAction } from './reducers.js';
|
|
4
|
+
export { createEffect, ofType } from './effects.js';
|
|
5
|
+
export { createSelector, createFeatureSelector } from './selectors.js';
|
|
6
|
+
|
|
7
|
+
// Re-export RxJS operators from the main entry point
|
|
8
|
+
export * from './rxjs.js';
|
package/src/reducers.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./actions').Action} Action
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A token to be used with `on` to catch any action.
|
|
7
|
+
*/
|
|
8
|
+
export function anyAction() {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Associates one or more action creators with a reducer function.
|
|
12
|
+
* @param {...(function|function(any, Action): any)} args - A list of action creators, followed by the reducer function.
|
|
13
|
+
* @returns {{ types: string[], reducerFn: function(any, Action): any }}
|
|
14
|
+
*/
|
|
15
|
+
export function on(...args) {
|
|
16
|
+
// The last argument is the reducer function.
|
|
17
|
+
const reducerFn = args.pop();
|
|
18
|
+
// All previous arguments are the action creators.
|
|
19
|
+
const actionCreators = args;
|
|
20
|
+
|
|
21
|
+
// Checks if the `anyAction` token was used.
|
|
22
|
+
const isCatchAll = actionCreators.includes(anyAction);
|
|
23
|
+
if (isCatchAll && actionCreators.length > 1) {
|
|
24
|
+
throw new Error('The `anyAction` token cannot be mixed with other action creators in a single `on` handler.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const types = actionCreators.map(creator => creator.type);
|
|
28
|
+
return { types, reducerFn, isCatchAll };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Factory function to create a reducer.
|
|
33
|
+
* A reducer is associated with a key that defines which top-level property of the state it manages.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} featureKey - The key for the state slice this reducer manages.
|
|
36
|
+
* @param {any} initialState - The initial state for this state slice.
|
|
37
|
+
* @param {...{ types: string[], reducerFn: function(any, Action): any }} ons - A list of handlers created with the `on` function.
|
|
38
|
+
* @returns {{path: string, initialState: any, reducerFn: function(any, Action): any}} The reducer object.
|
|
39
|
+
*/
|
|
40
|
+
export function createReducer(featureKey, initialState, ...ons) {
|
|
41
|
+
if (!featureKey || typeof featureKey !== 'string') {
|
|
42
|
+
throw new Error('Reducer featureKey must be a non-empty string.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Separates specific handlers from generic (catch-all) ones.
|
|
46
|
+
const specificHandlers = ons.filter(o => !o.isCatchAll);
|
|
47
|
+
const catchAllHandler = ons.find(o => o.isCatchAll);
|
|
48
|
+
|
|
49
|
+
const reducerFn = (state = initialState, action) => {
|
|
50
|
+
// First, try to find a specific 'on' handler for the action.
|
|
51
|
+
for (const handler of specificHandlers) {
|
|
52
|
+
if (handler.types.includes(action.type)) {
|
|
53
|
+
return handler.reducerFn(state, action);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If no specific handler matches, execute the 'any' handler, if it exists.
|
|
58
|
+
if (catchAllHandler) {
|
|
59
|
+
return catchAllHandler.reducerFn(state, action);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If nothing matches, return the state unchanged.
|
|
63
|
+
return state;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return { path: featureKey, initialState, reducerFn };
|
|
67
|
+
}
|
package/src/rxjs.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// This file acts as a secondary entry point for re-exporting
|
|
2
|
+
// a curated set of RxJS functionalities. This allows consumers
|
|
3
|
+
// of the library to import them via `rx-tiny-flux/rxjs`.
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
// Operators
|
|
7
|
+
map,
|
|
8
|
+
concatMap,
|
|
9
|
+
switchMap,
|
|
10
|
+
exhaustMap,
|
|
11
|
+
mergeMap,
|
|
12
|
+
delay,
|
|
13
|
+
filter,
|
|
14
|
+
tap,
|
|
15
|
+
catchError,
|
|
16
|
+
// Creation Functions
|
|
17
|
+
defer,
|
|
18
|
+
from,
|
|
19
|
+
of,
|
|
20
|
+
} from 'rxjs';
|
package/src/selectors.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a selector function that extracts a top-level state slice (feature) using a key.
|
|
3
|
+
* It is analogous to NgRx's `createFeatureSelector`.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} featureKey - The key for the top-level feature in the state object.
|
|
6
|
+
* @param {function(any): any} [projectionFn] - An optional function to transform the selected value.
|
|
7
|
+
* @returns {function(object): any} A function that receives the complete state and returns the selected part.
|
|
8
|
+
*/
|
|
9
|
+
export function createFeatureSelector(featureKey, projectionFn) {
|
|
10
|
+
return (state) => {
|
|
11
|
+
const selectedValue = state[featureKey];
|
|
12
|
+
// If a projection function was provided, apply it to the value. Otherwise, return the value directly.
|
|
13
|
+
return projectionFn ? projectionFn(selectedValue) : selectedValue;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a selector function that can compose multiple selectors, whose results
|
|
19
|
+
* are passed as arguments to a final projection function.
|
|
20
|
+
* It is analogous to NgRx's `createSelector`.
|
|
21
|
+
*
|
|
22
|
+
* @param {...Function} args - A list of input selector functions, followed by a projection function.
|
|
23
|
+
* @returns {function(object): any} The final composed selector function.
|
|
24
|
+
*/
|
|
25
|
+
export function createSelector(...args) {
|
|
26
|
+
// The last argument is always the projection function.
|
|
27
|
+
const projectionFn = args.pop();
|
|
28
|
+
// All previous arguments are the input selectors.
|
|
29
|
+
const inputSelectors = args;
|
|
30
|
+
|
|
31
|
+
if (typeof projectionFn !== 'function') {
|
|
32
|
+
throw new Error('The last argument to createSelector must be a projection function.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (state) => {
|
|
36
|
+
const inputs = inputSelectors.map(selector => selector(state));
|
|
37
|
+
return projectionFn(...inputs);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/store.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { BehaviorSubject, Subject, merge } from 'rxjs';
|
|
2
|
+
import { scan, shareReplay, startWith, tap, distinctUntilChanged, map } from 'rxjs/operators';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('./actions').Action} Action
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class Store {
|
|
9
|
+
/**
|
|
10
|
+
* @private
|
|
11
|
+
* @type {BehaviorSubject<object>}
|
|
12
|
+
*/
|
|
13
|
+
_state$;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @private
|
|
17
|
+
* @type {Subject<Action>}
|
|
18
|
+
*/
|
|
19
|
+
_actions$ = new Subject();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @private
|
|
23
|
+
* @type {Array<{path: string, initialState: any, reducerFn: function(any, Action): any}>}
|
|
24
|
+
*/
|
|
25
|
+
_reducers = [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} initialState - The initial state of the application.
|
|
29
|
+
*/
|
|
30
|
+
constructor(initialState = {}) {
|
|
31
|
+
// The initial state is now deep-cloned to prevent external mutations.
|
|
32
|
+
// `structuredClone` is modern and ideal, but `JSON.parse` is a safe fallback.
|
|
33
|
+
const initialStoreState = typeof structuredClone === 'function' ? structuredClone(initialState) : JSON.parse(JSON.stringify(initialState));
|
|
34
|
+
this._state$ = new BehaviorSubject(initialStoreState);
|
|
35
|
+
|
|
36
|
+
const dispatcher$ = this._actions$.pipe(
|
|
37
|
+
// Optional: log for debugging
|
|
38
|
+
tap((action) => console.log('Action Dispatched:', action))
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const state$ = dispatcher$.pipe(
|
|
42
|
+
scan((currentState, action) => {
|
|
43
|
+
// Clones the state to ensure immutability.
|
|
44
|
+
// For larger apps, a library like `lodash.cloneDeep` would be more robust.
|
|
45
|
+
const nextState = JSON.parse(JSON.stringify(currentState));
|
|
46
|
+
|
|
47
|
+
this._reducers.forEach(({ path: featureKey, reducerFn }) => {
|
|
48
|
+
// Gets the current state slice.
|
|
49
|
+
const stateSlice = nextState[featureKey];
|
|
50
|
+
|
|
51
|
+
// Executes the reducer to get the new slice.
|
|
52
|
+
const nextStateSlice = reducerFn(stateSlice, action);
|
|
53
|
+
|
|
54
|
+
// If the reducer returned a new value (not undefined) and it's different from the previous one,
|
|
55
|
+
// apply the change to the state object.
|
|
56
|
+
if (nextStateSlice !== undefined && stateSlice !== nextStateSlice) {
|
|
57
|
+
// Directly assign the new slice to the corresponding key in the state.
|
|
58
|
+
nextState[featureKey] = nextStateSlice;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return nextState;
|
|
63
|
+
}, initialStoreState),
|
|
64
|
+
startWith(initialState),
|
|
65
|
+
// Ensures new subscribers receive the last emitted state and shares the execution.
|
|
66
|
+
shareReplay(1)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Connects the calculated state stream back to our main BehaviorSubject.
|
|
70
|
+
state$.subscribe(this._state$);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Registers reducers in the store.
|
|
75
|
+
* @param {...{path: string, initialState: any, reducerFn: function(any, Action): any}} reducers
|
|
76
|
+
*/
|
|
77
|
+
registerReducers(...reducers) {
|
|
78
|
+
this._reducers.push(...reducers);
|
|
79
|
+
|
|
80
|
+
// Builds the initial state from the registered reducers
|
|
81
|
+
const currentState = this._state$.getValue();
|
|
82
|
+
const nextState = JSON.parse(JSON.stringify(currentState));
|
|
83
|
+
|
|
84
|
+
reducers.forEach(({ path: featureKey, initialState }) => {
|
|
85
|
+
// If the state slice has not been defined yet, apply the reducer's initial state.
|
|
86
|
+
if (nextState[featureKey] === undefined) {
|
|
87
|
+
nextState[featureKey] = initialState;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Emits the new constructed state
|
|
92
|
+
this._state$.next(nextState);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Registers effects in the store.
|
|
97
|
+
* @param {...function(import('rxjs').Observable<Action>): import('rxjs').Observable<Action>} effects
|
|
98
|
+
*/
|
|
99
|
+
registerEffects(...effects) {
|
|
100
|
+
const effectStreams = effects.map((effect) => effect(this._actions$));
|
|
101
|
+
// Merges all action streams returned by the effects and dispatches them back into the store.
|
|
102
|
+
merge(...effectStreams).subscribe((action) => this.dispatch(action));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Dispatches an action to the store, initiating the state update cycle.
|
|
107
|
+
* @param {Action} action
|
|
108
|
+
*/
|
|
109
|
+
dispatch(action) {
|
|
110
|
+
this._actions$.next(action);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Selects a slice of the state and returns it as an Observable.
|
|
115
|
+
* @param {function(object): any} selectorFn - The selector function.
|
|
116
|
+
* @returns {import('rxjs').Observable<any>}
|
|
117
|
+
*/
|
|
118
|
+
select(selectorFn) {
|
|
119
|
+
return this._state$.pipe(
|
|
120
|
+
map(state => selectorFn(state)),
|
|
121
|
+
// Emits only when the selected value has actually changed.
|
|
122
|
+
distinctUntilChanged()
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import jsonpath from 'jsonpath';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sets a value in an object using a jsonpath expression,
|
|
5
|
+
* creating the nested path if it doesn't exist.
|
|
6
|
+
* This function modifies the input object.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} obj The object to be modified.
|
|
9
|
+
* @param {string} path The jsonpath expression.
|
|
10
|
+
* @param {any} value The value to be set.
|
|
11
|
+
*/
|
|
12
|
+
export function setValueByPath(obj, path, value) {
|
|
13
|
+
// Tries to set the value. If the path doesn't exist, the `value` function with 3 arguments returns undefined.
|
|
14
|
+
if (jsonpath.value(obj, path, value) === undefined) {
|
|
15
|
+
const pathComponents = jsonpath.parse(path);
|
|
16
|
+
// Removes the last component to get the "parent" path.
|
|
17
|
+
pathComponents.pop();
|
|
18
|
+
const parentPath = jsonpath.stringify(pathComponents);
|
|
19
|
+
|
|
20
|
+
// Recursively calls the function to ensure the parent path exists,
|
|
21
|
+
// setting it as an empty object before trying to set the final value.
|
|
22
|
+
setValueByPath(obj, parentPath, {});
|
|
23
|
+
jsonpath.value(obj, path, value);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/zeppos.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file zeppos.js
|
|
3
|
+
* @description A smart plugin factory for integrating rx-tiny-flux with ZeppOS (via ZML).
|
|
4
|
+
* It exposes `dispatch` and a lifecycle-aware `subscribe` method,
|
|
5
|
+
* automatically handling unsubscriptions during the `onDestroy` lifecycle hook
|
|
6
|
+
* to prevent memory leaks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Factory function that creates the store plugin for ZML's BaseApp/BasePage.
|
|
11
|
+
* @param {object} instance - The App or Page instance (injected by ZML's plugin service).
|
|
12
|
+
* @param {object} store - The store instance from `rx-tiny-flux` passed via `.use(plugin, store)`.
|
|
13
|
+
* @returns {object} A mixin object with methods and lifecycle hooks to be merged.
|
|
14
|
+
*/
|
|
15
|
+
function storePlugin(instance, store) {
|
|
16
|
+
if (!store) {
|
|
17
|
+
console.error('[rx-tiny-flux] StorePlugin Error: Store instance was not provided on .use()');
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* A proxy to the store's dispatch method.
|
|
24
|
+
* @param {object} action - The action to be dispatched to the store.
|
|
25
|
+
*/
|
|
26
|
+
dispatch: store.dispatch.bind(store),
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Subscribes to a selector and automatically manages the subscription's lifecycle.
|
|
30
|
+
* @param {Function} selector - A function that selects a slice of the state.
|
|
31
|
+
* @param {Function} callback - The function to execute when the selected state changes.
|
|
32
|
+
* @returns {object} The subscription object, allowing for manual unsubscription if needed.
|
|
33
|
+
*/
|
|
34
|
+
subscribe(selector, callback) {
|
|
35
|
+
// 'this' refers to the App/Page instance.
|
|
36
|
+
// Initialize an internal array to hold subscriptions if it doesn't exist.
|
|
37
|
+
if (!this._subscriptions) {
|
|
38
|
+
this._subscriptions = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const subscription = store.select(selector).subscribe(callback);
|
|
42
|
+
this._subscriptions.push(subscription);
|
|
43
|
+
|
|
44
|
+
// Return the subscription object in case the developer needs to unsubscribe manually.
|
|
45
|
+
return subscription;
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Lifecycle hook that will be merged and called during the instance's destruction.
|
|
49
|
+
* This is the core of the automatic memory management feature.
|
|
50
|
+
*/
|
|
51
|
+
onDestroy() {
|
|
52
|
+
// 'this' refers to the App/Page instance.
|
|
53
|
+
if (this._subscriptions && this._subscriptions.length > 0) {
|
|
54
|
+
// console.log(`[rx-tiny-flux] Unsubscribing from ${this._subscriptions.length} subscriptions.`);
|
|
55
|
+
this._subscriptions.forEach((sub) => sub.unsubscribe());
|
|
56
|
+
this._subscriptions = []; // Clear the array.
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { storePlugin };
|