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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Bernardo Baumblatt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Readme.md ADDED
@@ -0,0 +1,297 @@
1
+ # Rx-Tiny-Flux
2
+
3
+ `Rx-Tiny-Flux` is a lightweight, minimalist state management library for pure JavaScript projects, heavily inspired by the patterns of NgRx and Redux. It leverages the power of RxJS for reactive state management.
4
+
5
+ The primary dependencies are `rxjs` and `jsonpath`.
6
+
7
+ > **A Case Study on AI-Assisted Development:** This entire library was developed in just a few hours as a case study to explore the capabilities of **Gemini Code Assist**. It demonstrates how modern AI coding assistants can significantly accelerate the development process, from initial scaffolding to complex feature implementation and refinement.
8
+
9
+ ## Core Concepts
10
+
11
+ The library is built around a few core concepts:
12
+
13
+ * **Store:** A single, centralized object that holds the application's state.
14
+ * **Actions:** Plain objects that describe events or "things that happened" in your application.
15
+ * **Reducers:** Pure functions that determine how the state changes in response to actions.
16
+ * **Effects:** Functions that handle side effects, such as API calls, which can dispatch new actions.
17
+ * **Selectors:** Pure functions used to query and derive data from the state.
18
+
19
+ ---
20
+
21
+ ## Actions
22
+
23
+ Actions are the only source of information for the store. You dispatch them to trigger state changes. Use the `createAction` factory function to create them.
24
+
25
+ ```javascript
26
+ import { createAction } from 'rx-tiny-flux';
27
+
28
+ // An action creator for an event without a payload
29
+ const increment = createAction('[Counter] Increment');
30
+
31
+ // An action creator for an event with a payload
32
+ const add = createAction('[Counter] Add');
33
+
34
+ // Dispatching the actions
35
+ store.dispatch(increment()); // { type: '[Counter] Increment' }
36
+ store.dispatch(add(10)); // { type: '[Counter] Add', payload: 10 }
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Reducers
42
+
43
+ Reducers specify how the application's state changes in response to actions. A reducer is a pure function that takes the previous state slice and an action, and returns the next state slice.
44
+
45
+ Use `createReducer` along with the `on` and `anyAction` helpers to define your reducer logic.
46
+
47
+ ```javascript
48
+ // Library imports
49
+ import { createReducer, on, anyAction } from 'rx-tiny-flux';
50
+
51
+ // Your application's action imports
52
+ import { increment, decrement, incrementSuccess } from './actions';
53
+
54
+ // This reducer manages the 'counter' slice of the state.
55
+ const counterReducer = createReducer(
56
+ // 1. The jsonpath to the state slice
57
+ '$.counter',
58
+ // 2. The initial state for this slice
59
+ { value: 0, lastUpdate: null },
60
+ // 3. Handlers for specific actions
61
+ on(increment, incrementSuccess, (state) => ({
62
+ ...state,
63
+ value: state.value + 1,
64
+ lastUpdate: new Date().toISOString(),
65
+ })),
66
+ on(decrement, (state) => ({
67
+ ...state,
68
+ value: state.value - 1,
69
+ lastUpdate: new Date().toISOString(),
70
+ }))
71
+ );
72
+
73
+ // This reducer manages the 'log' slice and reacts to ANY action.
74
+ const logReducer = createReducer(
75
+ '$.log',
76
+ [], // Initial state for this slice
77
+ // Using the `anyAction` token to create a handler that catches all actions.
78
+ on(anyAction, (state, action) => [...state, `Action: ${action.type} at ${new Date().toISOString()}`])
79
+ );
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Effects
85
+
86
+ Effects are used to handle side effects, such as asynchronous operations (e.g., API calls). An effect listens for dispatched actions, performs some work, and can dispatch new actions as a result.
87
+
88
+ Use `createEffect` and the `ofType` operator to build effects.
89
+
90
+ ```javascript
91
+ // Core library imports
92
+ import { createEffect, ofType } from 'rx-tiny-flux';
93
+ // RxJS operators are imported from the secondary entry point
94
+ import { from, map, concatMap } from 'rx-tiny-flux/rxjs';
95
+
96
+ // Your application's action imports
97
+ import { incrementAsync, incrementSuccess } from './actions';
98
+
99
+ // A function that simulates an API call (e.g., fetch) which returns a Promise
100
+ const fakeApiCall = () => new Promise(resolve => setTimeout(() => resolve({ success: true }), 1000));
101
+
102
+ const incrementAsyncEffect = createEffect((actions$) =>
103
+ actions$.pipe(
104
+ // Listens only for the 'incrementAsync' action
105
+ ofType(incrementAsync),
106
+ // Use concatMap to handle the async operation.
107
+ // It waits for the inner Observable (from the Promise) to complete.
108
+ concatMap(() =>
109
+ from(fakeApiCall()).pipe( // `from` converts the Promise into an Observable
110
+ map(() => incrementSuccess()) // On success, map the result to a new action
111
+ )
112
+ )
113
+ )
114
+ );
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Using RxJS Operators
120
+
121
+ For use in restricted environments like **ZeppOS**, where direct dependencies are not allowed, `Rx-Tiny-Flux` re-exports a curated set of common RxJS operators and creation functions from a dedicated entry point: `rx-tiny-flux/rxjs`. This allows you to build complex effects without needing to import from `rxjs` directly in your application code.
122
+
123
+ **Available Exports:**
124
+
125
+ * **Creation Functions:** Used to create new Observables.
126
+ * `from`: Converts a Promise, an array, or an iterable into an Observable.
127
+ * `of`: Emits a variable number of arguments as a sequence and then completes.
128
+ * `defer`: Creates an Observable that, on subscription, calls an Observable factory to make a fresh Observable for each new Subscriber.
129
+
130
+ * **Higher-Order Mapping Operators:** Used to manage inner Observables, typically for handling asynchronous operations like API calls.
131
+ * `concatMap`: Maps to an inner Observable and processes them sequentially.
132
+ * `switchMap`: Maps to an inner Observable but cancels the previous inner subscription if a new outer value arrives.
133
+ * `mergeMap`: Maps to an inner Observable and processes them in parallel.
134
+ * `exhaustMap`: Maps to an inner Observable but ignores new outer values while the current inner Observable is still active.
135
+
136
+ * **Utility Operators:** Used for transformation, filtering, and side effects.
137
+ * `map`: Applies a given project function to each value emitted by the source Observable.
138
+ * `filter`: Emits only those values from the source Observable that pass a predicate function.
139
+ * `tap`: Perform a side effect for every emission on the source Observable, but return an Observable that is identical to the source.
140
+ * `delay`: Delays the emission of items from the source Observable by a given timeout.
141
+ * `catchError`: Catches errors on the source Observable and returns a new Observable or throws an error.
142
+
143
+ ```javascript
144
+ // Instead of: import { map, from } from 'rxjs';
145
+ // You can do:
146
+ import { createEffect, ofType } from 'rx-tiny-flux';
147
+ import { map, from } from 'rx-tiny-flux/rxjs';
148
+
149
+ // ... your effect implementation
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Selectors
155
+
156
+ Selectors are pure functions used for obtaining slices of store state. The library provides two factory functions for this:
157
+
158
+ 1. `createFeatureSelector`: Selects a top-level slice of the state using a `jsonpath` expression.
159
+ 2. `createSelector`: Composes multiple selectors to compute derived data.
160
+
161
+ ```javascript
162
+ import { createFeatureSelector, createSelector } from 'rx-tiny-flux';
163
+
164
+ // 1. We use `createFeatureSelector` with a jsonpath expression to get a top-level slice of the state.
165
+ const selectCounterSlice = createFeatureSelector('$.counter');
166
+
167
+ // 2. We use `createSelector` to compose and extract more granular data from the slice.
168
+ const selectCounterValue = createSelector(
169
+ selectCounterSlice,
170
+ (counter) => counter?.value // Added 'optional chaining' for safety
171
+ );
172
+
173
+ const selectLastUpdate = createSelector(
174
+ selectCounterSlice,
175
+ (counter) => counter?.lastUpdate
176
+ );
177
+
178
+ // Using the selector with the store
179
+ store.select(selectCounterValue).subscribe((value) => {
180
+ console.log(`Counter value is now: ${value}`);
181
+ });
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Putting It All Together: The Store
187
+
188
+ The `Store` is the central piece that brings everything together. You instantiate it, register your reducers and effects, and then use it to dispatch actions and select state.
189
+
190
+ ```javascript
191
+ // Library imports
192
+ import { Store } from 'rx-tiny-flux';
193
+
194
+ // Import all your application's reducers, effects, and actions
195
+ import { counterReducer, logReducer } from './path/to/your/reducers';
196
+ import { incrementAsyncEffect } from './path/to/your/effects';
197
+ import { increment, incrementAsync } from './path/to/your/actions';
198
+
199
+ // 1. The Store can start with an empty state.
200
+ const initialState = {};
201
+
202
+ // 2. Create a new Store instance
203
+ const store = new Store(initialState);
204
+
205
+ // 3. Register all reducers. The store will build its initial state from them.
206
+ store.registerReducers(counterReducer, logReducer);
207
+
208
+ // 4. Register all effects.
209
+ store.registerEffects(incrementAsyncEffect);
210
+
211
+ // 5. Dispatch actions to trigger state changes and side effects.
212
+ console.log('Dispatching actions...');
213
+ store.dispatch(increment());
214
+ store.dispatch(increment());
215
+ store.dispatch(incrementAsync()); // Will trigger the effect
216
+
217
+ // 6. Select and subscribe to state changes.
218
+ // It's crucial to capture the subscription object so we can unsubscribe later.
219
+ const counterSubscription = store.select(selectCounterValue).subscribe((value) => {
220
+ console.log(`Counter value is now: ${value}`);
221
+ });
222
+
223
+ // 7. Clean Up (Unsubscribe)
224
+ // In any real application, you must unsubscribe to prevent memory leaks.
225
+ // When the subscription is no longer needed (e.g., a component is destroyed),
226
+ // call the .unsubscribe() method.
227
+ counterSubscription.unsubscribe();
228
+ ```
229
+
230
+ ### ZeppOS Integration (via ZML)
231
+
232
+ For developers using the `ZML` library on the ZeppOS platform, `rx-tiny-flux` offers an optional plugin that seamlessly integrates the store with the `BaseApp` and `BasePage` component lifecycle.
233
+
234
+ This plugin injects `dispatch` and `subscribe` methods into your component's instance. Most importantly, the `subscribe` method is lifecycle-aware: it automatically tracks all subscriptions and unsubscribes from them when the component's `onDestroy` hook is called, preventing common memory leaks.
235
+
236
+ #### How to Use
237
+
238
+ 1. **Create and configure your store** as usual.
239
+ 2. **Import the `storePlugin`** from `rx-tiny-flux/zeppos`.
240
+ 3. **Register the plugin** globally for `BaseApp` and `BasePage` using the static `.use()` method, passing your store instance.
241
+ 4. **Use `this.dispatch()` and `this.subscribe()`** inside your pages.
242
+
243
+ Here is a complete example:
244
+
245
+ ```javascript
246
+ // app.js - Your application's entry point
247
+ import { BaseApp, BasePage } from '@zeppos/zml';
248
+ import { Store } from 'rx-tiny-flux';
249
+ import { storePlugin } from 'rx-tiny-flux/zeppos';
250
+
251
+ // 1. Import your reducers, actions, etc.
252
+ import { counterReducer } from './path/to/reducers';
253
+ import { selectCounterValue } from './path/to/selectors';
254
+ import { increment } from './path/to/actions';
255
+
256
+ // 2. Create your store instance
257
+ const store = new Store({});
258
+ store.registerReducers(counterReducer);
259
+
260
+ // 3. Register the plugin globally for App and Pages
261
+ BaseApp.use(storePlugin, store);
262
+ BasePage.use(storePlugin, store);
263
+
264
+ App(BaseApp({
265
+ // ... your App config
266
+ }));
267
+ ```
268
+
269
+ ```javascript
270
+ // page/index.js - An example page
271
+ import { BasePage, ui } from '@zeppos/zml';
272
+ import { selectCounterValue } from '../path/to/selectors';
273
+ import { increment } from './path/to/actions';
274
+
275
+ Page(BasePage({
276
+ build() {
277
+ const myText = ui.createWidget(ui.widget.TEXT, { /* ... */ });
278
+
279
+ // 4. Use `this.subscribe` to listen to state changes
280
+ this.subscribe(selectCounterValue, (value) => {
281
+ myText.setProperty(ui.prop.TEXT, `Counter: ${value}`);
282
+ });
283
+
284
+ // Use `this.dispatch` to dispatch actions
285
+ ui.createWidget(ui.widget.BUTTON, {
286
+ // ...
287
+ click_func: () => this.dispatch(increment()),
288
+ });
289
+ },
290
+
291
+ onDestroy() {
292
+ // No need to unsubscribe manually!
293
+ // The storePlugin will do it for you automatically.
294
+ console.log('Page destroyed, subscriptions cleaned up.');
295
+ }
296
+ }));
297
+ ```