synstate 0.1.0 → 0.1.1
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 +201 -0
- package/README.md +124 -350
- package/assets/synstate-icon.png +0 -0
- package/dist/core/combine/combine.d.mts +32 -2
- package/dist/core/combine/combine.d.mts.map +1 -1
- package/dist/core/combine/combine.mjs +32 -2
- package/dist/core/combine/combine.mjs.map +1 -1
- package/dist/core/combine/merge.d.mts +30 -4
- package/dist/core/combine/merge.d.mts.map +1 -1
- package/dist/core/combine/merge.mjs +30 -4
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +28 -3
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +28 -3
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/from-array.d.mts +21 -3
- package/dist/core/create/from-array.d.mts.map +1 -1
- package/dist/core/create/from-array.mjs +21 -3
- package/dist/core/create/from-array.mjs.map +1 -1
- package/dist/core/create/from-promise.d.mts +29 -7
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +29 -7
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/from-subscribable.d.mts +58 -0
- package/dist/core/create/from-subscribable.d.mts.map +1 -1
- package/dist/core/create/from-subscribable.mjs +58 -0
- package/dist/core/create/from-subscribable.mjs.map +1 -1
- package/dist/core/create/interval.d.mts +29 -4
- package/dist/core/create/interval.d.mts.map +1 -1
- package/dist/core/create/interval.mjs +29 -4
- package/dist/core/create/interval.mjs.map +1 -1
- package/dist/core/create/of.d.mts +22 -3
- package/dist/core/create/of.d.mts.map +1 -1
- package/dist/core/create/of.mjs +22 -3
- package/dist/core/create/of.mjs.map +1 -1
- package/dist/core/create/source.d.mts +20 -1
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/create/timer.d.mts +23 -4
- package/dist/core/create/timer.d.mts.map +1 -1
- package/dist/core/create/timer.mjs +23 -4
- package/dist/core/create/timer.mjs.map +1 -1
- package/dist/core/operators/audit-time.d.mts +59 -0
- package/dist/core/operators/audit-time.d.mts.map +1 -1
- package/dist/core/operators/audit-time.mjs +59 -0
- package/dist/core/operators/audit-time.mjs.map +1 -1
- package/dist/core/operators/debounce-time.d.mts +22 -2
- package/dist/core/operators/debounce-time.d.mts.map +1 -1
- package/dist/core/operators/debounce-time.mjs +22 -2
- package/dist/core/operators/debounce-time.mjs.map +1 -1
- package/dist/core/operators/filter.d.mts +26 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/map-with-index.d.mts +19 -4
- package/dist/core/operators/map-with-index.d.mts.map +1 -1
- package/dist/core/operators/map-with-index.mjs +19 -4
- package/dist/core/operators/map-with-index.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +47 -5
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +47 -5
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/pairwise.d.mts +30 -1
- package/dist/core/operators/pairwise.d.mts.map +1 -1
- package/dist/core/operators/pairwise.mjs +30 -1
- package/dist/core/operators/pairwise.mjs.map +1 -1
- package/dist/core/operators/scan.d.mts +23 -1
- package/dist/core/operators/scan.d.mts.map +1 -1
- package/dist/core/operators/scan.mjs +23 -1
- package/dist/core/operators/scan.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts +25 -1
- package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs +25 -1
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.d.mts +50 -0
- package/dist/core/operators/skip-until.d.mts.map +1 -1
- package/dist/core/operators/skip-until.mjs +50 -0
- package/dist/core/operators/skip-until.mjs.map +1 -1
- package/dist/core/operators/skip-while.d.mts +48 -0
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +48 -0
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +39 -5
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +39 -5
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-until.d.mts +20 -1
- package/dist/core/operators/take-until.d.mts.map +1 -1
- package/dist/core/operators/take-until.mjs +20 -1
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts +47 -0
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +47 -0
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/operators/throttle-time.d.mts +44 -5
- package/dist/core/operators/throttle-time.d.mts.map +1 -1
- package/dist/core/operators/throttle-time.mjs +44 -5
- package/dist/core/operators/throttle-time.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts +53 -0
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +53 -0
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.d.mts +55 -0
- package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
- package/dist/core/operators/with-current-value-from.mjs +55 -0
- package/dist/core/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/operators/with-initial-value.d.mts +24 -2
- package/dist/core/operators/with-initial-value.d.mts.map +1 -1
- package/dist/core/operators/with-initial-value.mjs +24 -2
- package/dist/core/operators/with-initial-value.mjs.map +1 -1
- package/dist/core/types/observable-family.d.mts +7 -7
- package/dist/utils/create-event-emitter.d.mts +20 -2
- package/dist/utils/create-event-emitter.d.mts.map +1 -1
- package/dist/utils/create-event-emitter.mjs +20 -2
- package/dist/utils/create-event-emitter.mjs.map +1 -1
- package/dist/utils/create-reducer.d.mts +13 -1
- package/dist/utils/create-reducer.d.mts.map +1 -1
- package/dist/utils/create-reducer.mjs +13 -1
- package/dist/utils/create-reducer.mjs.map +1 -1
- package/dist/utils/create-state.d.mts +24 -4
- package/dist/utils/create-state.d.mts.map +1 -1
- package/dist/utils/create-state.mjs +24 -4
- package/dist/utils/create-state.mjs.map +1 -1
- package/package.json +13 -12
- package/src/core/combine/combine.mts +32 -2
- package/src/core/combine/merge.mts +30 -4
- package/src/core/combine/zip.mts +28 -3
- package/src/core/create/from-array.mts +21 -3
- package/src/core/create/from-promise.mts +29 -7
- package/src/core/create/from-subscribable.mts +58 -0
- package/src/core/create/interval.mts +29 -4
- package/src/core/create/of.mts +22 -3
- package/src/core/create/source.mts +20 -1
- package/src/core/create/timer.mts +23 -4
- package/src/core/operators/audit-time.mts +59 -0
- package/src/core/operators/debounce-time.mts +22 -2
- package/src/core/operators/filter.mts +26 -1
- package/src/core/operators/map-with-index.mts +19 -4
- package/src/core/operators/merge-map.mts +47 -5
- package/src/core/operators/pairwise.mts +30 -1
- package/src/core/operators/scan.mts +23 -1
- package/src/core/operators/skip-if-no-change.mts +25 -1
- package/src/core/operators/skip-until.mts +50 -0
- package/src/core/operators/skip-while.mts +48 -0
- package/src/core/operators/switch-map.mts +39 -5
- package/src/core/operators/take-until.mts +20 -1
- package/src/core/operators/take-while.mts +47 -0
- package/src/core/operators/throttle-time.mts +44 -5
- package/src/core/operators/with-buffered-from.mts +53 -0
- package/src/core/operators/with-current-value-from.mts +55 -0
- package/src/core/operators/with-initial-value.mts +24 -2
- package/src/core/types/observable-family.mts +7 -7
- package/src/utils/create-event-emitter.mts +20 -2
- package/src/utils/create-reducer.mts +13 -1
- package/src/utils/create-state.mts +24 -4
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SynState
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[](https://codecov.io/gh/noshiro-pf/ts-data-forge)
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./assets/synstate-icon.png" alt="SynState Logo" width="400" />
|
|
5
|
+
</p>
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/synstate)
|
|
10
|
+
[](https://www.npmjs.com/package/synstate)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
[](https://codecov.io/gh/noshiro-pf/synstate)
|
|
13
|
+
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
**SynState** is a lightweight, high-performance, type-safe state management library for TypeScript/JavaScript. Perfect for building reactive global state and event-driven systems in React, Vue, and other frameworks.
|
|
9
17
|
|
|
10
18
|
## Features
|
|
11
19
|
|
|
@@ -13,13 +21,14 @@
|
|
|
13
21
|
- 📡 **Event System**: Built-in `createValueEmitter`, `createEventEmitter` for event-driven architecture
|
|
14
22
|
- 🔄 **Reactive Updates**: Automatic propagation of state changes to subscribers
|
|
15
23
|
- 🎨 **Type-Safe**: Full TypeScript support with precise type inference
|
|
16
|
-
- 🚀 **Lightweight**: Minimal bundle size
|
|
17
|
-
- ⚡ **
|
|
18
|
-
-
|
|
24
|
+
- 🚀 **Lightweight**: Minimal bundle size with only one external runtime dependency ([ts-data-forge](https://www.npmjs.com/package/ts-data-forge))
|
|
25
|
+
- ⚡ **High Performance**: Optimized for fast state updates and minimal re-renders
|
|
26
|
+
- 🌐 **Framework Agnostic**: Works with React, Vue, Svelte, or vanilla JavaScript
|
|
27
|
+
- 🔧 **Flexible**: Simple state management with optional advanced Observable-based features (operators like `map`, `filter`, `debounceTime`, `throttleTime`, and combinators like `merge`, `combine`)
|
|
19
28
|
|
|
20
29
|
## Documentation
|
|
21
30
|
|
|
22
|
-
- API reference: <https://noshiro-pf.github.io/synstate/>
|
|
31
|
+
- API reference: TBD <!-- <https://noshiro-pf.github.io/synstate/> -->
|
|
23
32
|
|
|
24
33
|
## Installation
|
|
25
34
|
|
|
@@ -42,39 +51,26 @@ pnpm add synstate
|
|
|
42
51
|
### Simple State Management
|
|
43
52
|
|
|
44
53
|
```tsx
|
|
45
|
-
import { createState } from 'synstate';
|
|
46
|
-
|
|
47
54
|
// Create a reactive state
|
|
48
55
|
const [state, setState, { updateState }] = createState(0);
|
|
49
56
|
|
|
57
|
+
const mut_history: number[] = [];
|
|
58
|
+
|
|
50
59
|
// Subscribe to changes (in React components, Vue watchers, etc.)
|
|
51
60
|
state.subscribe((count: number) => {
|
|
52
|
-
|
|
61
|
+
mut_history.push(count);
|
|
53
62
|
});
|
|
54
63
|
|
|
64
|
+
assert.deepStrictEqual(mut_history, [0]);
|
|
65
|
+
|
|
55
66
|
// Update state
|
|
56
67
|
setState(1);
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Event Emitter
|
|
69
|
+
assert.deepStrictEqual(mut_history, [0, 1]);
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
import { createValueEmitter } from 'synstate';
|
|
65
|
-
|
|
66
|
-
type User = Readonly<{ id: number; name: string }>;
|
|
67
|
-
|
|
68
|
-
// Create event emitter
|
|
69
|
-
const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<User>();
|
|
70
|
-
|
|
71
|
-
// Subscribe to events
|
|
72
|
-
userLoggedIn$.subscribe((user) => {
|
|
73
|
-
console.log('User logged in:', user.name);
|
|
74
|
-
});
|
|
71
|
+
updateState((prev: number) => prev + 1);
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
emitUserLoggedIn({ id: 1, name: 'Alice' });
|
|
73
|
+
assert.deepStrictEqual(mut_history, [0, 1, 2]);
|
|
78
74
|
```
|
|
79
75
|
|
|
80
76
|
### With React
|
|
@@ -125,7 +121,7 @@ const UserProfile = (): React.JSX.Element => {
|
|
|
125
121
|
|
|
126
122
|
### State Management
|
|
127
123
|
|
|
128
|
-
|
|
124
|
+
SynState provides simple, intuitive APIs for managing application state:
|
|
129
125
|
|
|
130
126
|
- **`createState`**: Create mutable state with getter/setter
|
|
131
127
|
- **`createReducer`**: Redux-style state management
|
|
@@ -142,6 +138,66 @@ Built-in event emitter for event-driven patterns:
|
|
|
142
138
|
|
|
143
139
|
For advanced use cases, you can use observables to build complex reactive data flows. However, most applications will only need `createState`, `createReducer`, and `createValueEmitter`.
|
|
144
140
|
|
|
141
|
+
## API Reference
|
|
142
|
+
|
|
143
|
+
<!-- ### State Management (Recommended)
|
|
144
|
+
|
|
145
|
+
#### createState
|
|
146
|
+
|
|
147
|
+
Create reactive state with getter and setter.
|
|
148
|
+
|
|
149
|
+
#### createBooleanState
|
|
150
|
+
|
|
151
|
+
Specialized state for boolean values.
|
|
152
|
+
|
|
153
|
+
#### createReducer
|
|
154
|
+
|
|
155
|
+
Create state with reducer pattern (like Redux).
|
|
156
|
+
|
|
157
|
+
### Event System
|
|
158
|
+
|
|
159
|
+
#### createValueEmitter
|
|
160
|
+
|
|
161
|
+
Create type-safe event emitter with payload.
|
|
162
|
+
|
|
163
|
+
#### createEventEmitter
|
|
164
|
+
|
|
165
|
+
Create event emitter without payload.
|
|
166
|
+
-->
|
|
167
|
+
|
|
168
|
+
### Advanced Features (Optional)
|
|
169
|
+
|
|
170
|
+
For complex scenarios, SynState provides observable-based APIs:
|
|
171
|
+
|
|
172
|
+
#### Creation Functions
|
|
173
|
+
|
|
174
|
+
- `source<T>()`: Create a new observable source
|
|
175
|
+
- `of(value)`: Create observable from a single value
|
|
176
|
+
- `fromArray(array)`: Create observable from array
|
|
177
|
+
- `fromPromise(promise)`: Create observable from promise
|
|
178
|
+
- `interval(ms)`: Emit values at intervals
|
|
179
|
+
- `timer(delay)`: Emit after delay
|
|
180
|
+
|
|
181
|
+
#### Operators
|
|
182
|
+
|
|
183
|
+
- `filter(predicate)`: Filter values
|
|
184
|
+
- `map(fn)`: Transform values
|
|
185
|
+
- `scan(reducer, seed)`: Accumulate values
|
|
186
|
+
- `debounceTime(ms)`: Debounce emissions
|
|
187
|
+
- `throttleTime(ms)`: Throttle emissions
|
|
188
|
+
- `skipIfNoChange()`: Skip duplicate values
|
|
189
|
+
- `takeUntil(notifier)`: Complete on notifier emission
|
|
190
|
+
|
|
191
|
+
#### Combination
|
|
192
|
+
|
|
193
|
+
- `combine(observables)`: Combine latest values from multiple sources
|
|
194
|
+
- `merge(observables)`: Merge multiple streams
|
|
195
|
+
- `zip(observables)`: Pair values by index
|
|
196
|
+
|
|
197
|
+
## Examples
|
|
198
|
+
|
|
199
|
+
### Global Counter State (React)
|
|
200
|
+
|
|
145
201
|
```tsx
|
|
146
202
|
import * as React from 'react';
|
|
147
203
|
import { createState } from 'synstate';
|
|
@@ -191,13 +247,7 @@ const ResetButton = (): React.JSX.Element => (
|
|
|
191
247
|
);
|
|
192
248
|
```
|
|
193
249
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
### State Management (Recommended)
|
|
197
|
-
|
|
198
|
-
#### createState
|
|
199
|
-
|
|
200
|
-
Create reactive state with getter and setter:
|
|
250
|
+
### Event-Driven Architecture (React)
|
|
201
251
|
|
|
202
252
|
```tsx
|
|
203
253
|
import * as React from 'react';
|
|
@@ -261,9 +311,7 @@ const loginUser = async (): Promise<
|
|
|
261
311
|
> => ({ id: 1, name: 'Alice' });
|
|
262
312
|
```
|
|
263
313
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
Specialized state for boolean values:
|
|
314
|
+
### Todo List with Reducer (React)
|
|
267
315
|
|
|
268
316
|
```tsx
|
|
269
317
|
import * as React from 'react';
|
|
@@ -343,9 +391,7 @@ const TodoList = (): React.JSX.Element => {
|
|
|
343
391
|
};
|
|
344
392
|
```
|
|
345
393
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
Create state with reducer pattern (like Redux):
|
|
394
|
+
### Boolean State (Dark Mode)
|
|
349
395
|
|
|
350
396
|
```tsx
|
|
351
397
|
import * as React from 'react';
|
|
@@ -381,11 +427,7 @@ const ThemeToggle = (): React.JSX.Element => {
|
|
|
381
427
|
};
|
|
382
428
|
```
|
|
383
429
|
|
|
384
|
-
###
|
|
385
|
-
|
|
386
|
-
#### createValueEmitter
|
|
387
|
-
|
|
388
|
-
Create type-safe event emitter with payload:
|
|
430
|
+
### Cross-Component Communication
|
|
389
431
|
|
|
390
432
|
```tsx
|
|
391
433
|
import * as React from 'react';
|
|
@@ -460,9 +502,9 @@ const ItemList = (): React.JSX.Element => {
|
|
|
460
502
|
};
|
|
461
503
|
```
|
|
462
504
|
|
|
463
|
-
|
|
505
|
+
// Events
|
|
464
506
|
|
|
465
|
-
|
|
507
|
+
### Advanced: Search with Debounce
|
|
466
508
|
|
|
467
509
|
```tsx
|
|
468
510
|
import * as React from 'react';
|
|
@@ -532,302 +574,46 @@ const SearchBox = (): React.JSX.Element => {
|
|
|
532
574
|
};
|
|
533
575
|
```
|
|
534
576
|
|
|
535
|
-
### Advanced
|
|
536
|
-
|
|
537
|
-
For complex scenarios, SyncFlow provides observable-based APIs:
|
|
538
|
-
|
|
539
|
-
#### Creation Functions
|
|
540
|
-
|
|
541
|
-
- `source<T>()`: Create a new observable source
|
|
542
|
-
- `of(value)`: Create observable from a single value
|
|
543
|
-
- `fromArray(array)`: Create observable from array
|
|
544
|
-
- `fromPromise(promise)`: Create observable from promise
|
|
545
|
-
- `interval(ms)`: Emit values at intervals
|
|
546
|
-
- `timer(delay)`: Emit after delay
|
|
547
|
-
|
|
548
|
-
#### Operators
|
|
549
|
-
|
|
550
|
-
- `filter(predicate)`: Filter values
|
|
551
|
-
- `map(fn)`: Transform values
|
|
552
|
-
- `scan(reducer, seed)`: Accumulate values
|
|
553
|
-
- `debounceTime(ms)`: Debounce emissions
|
|
554
|
-
- `throttleTime(ms)`: Throttle emissions
|
|
555
|
-
- `skipIfNoChange()`: Skip duplicate values
|
|
556
|
-
- `takeUntil(notifier)`: Complete on notifier emission
|
|
557
|
-
|
|
558
|
-
#### Combination
|
|
559
|
-
|
|
560
|
-
- `combine(observables)`: Combine latest values from multiple sources
|
|
561
|
-
- `merge(observables)`: Merge multiple streams
|
|
562
|
-
- `zip(observables)`: Pair values by index
|
|
563
|
-
|
|
564
|
-
## Examples
|
|
565
|
-
|
|
566
|
-
### Global Counter State (React)
|
|
577
|
+
### Advanced: Event Emitter with Throttle
|
|
567
578
|
|
|
568
579
|
```tsx
|
|
569
|
-
import {
|
|
570
|
-
import { useState, useEffect } from 'react';
|
|
571
|
-
|
|
572
|
-
// Create global state
|
|
573
|
-
export const counterState = createState(0);
|
|
574
|
-
|
|
575
|
-
// Component 1
|
|
576
|
-
function Counter() {
|
|
577
|
-
const [count, setCount] = useState(counterState.getSnapshot());
|
|
578
|
-
|
|
579
|
-
useEffect(() => {
|
|
580
|
-
const sub = counterState.state.subscribe(setCount);
|
|
581
|
-
return () => sub.unsubscribe();
|
|
582
|
-
}, []);
|
|
583
|
-
|
|
584
|
-
return (
|
|
585
|
-
<div>
|
|
586
|
-
<p>Count: {count}</p>
|
|
587
|
-
<button onClick={() => counterState.updateState((n) => n + 1)}>
|
|
588
|
-
Increment
|
|
589
|
-
</button>
|
|
590
|
-
</div>
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Component 2 (synced automatically)
|
|
595
|
-
function ResetButton() {
|
|
596
|
-
return <button onClick={() => counterState.resetState()}>Reset</button>;
|
|
597
|
-
}
|
|
598
|
-
```
|
|
580
|
+
import { createEventEmitter, throttleTime } from 'synstate';
|
|
599
581
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
```tsx
|
|
603
|
-
import { createValueEmitter } from 'synstate';
|
|
604
|
-
import { useEffect } from 'react';
|
|
605
|
-
|
|
606
|
-
// Global events
|
|
607
|
-
export const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<{
|
|
608
|
-
id: number;
|
|
609
|
-
name: string;
|
|
610
|
-
}>();
|
|
611
|
-
|
|
612
|
-
export const [userLoggedOut$, emitUserLoggedOut] = createEventEmitter();
|
|
613
|
-
|
|
614
|
-
// Component that emits events
|
|
615
|
-
function LoginButton() {
|
|
616
|
-
const handleLogin = async () => {
|
|
617
|
-
const user = await loginUser();
|
|
618
|
-
emitUserLoggedIn(user);
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
return <button onClick={handleLogin}>Login</button>;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Component that listens to events
|
|
625
|
-
function Notification() {
|
|
626
|
-
const [message, setMessage] = useState('');
|
|
627
|
-
|
|
628
|
-
useEffect(() => {
|
|
629
|
-
const sub1 = userLoggedIn$.subscribe((user) => {
|
|
630
|
-
setMessage(`Welcome, ${user.name}!`);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
const sub2 = userLoggedOut$.subscribe(() => {
|
|
634
|
-
setMessage('Logged out');
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
return () => {
|
|
638
|
-
sub1.unsubscribe();
|
|
639
|
-
sub2.unsubscribe();
|
|
640
|
-
};
|
|
641
|
-
}, []);
|
|
642
|
-
|
|
643
|
-
return message ? <div className="notification">{message}</div> : null;
|
|
644
|
-
}
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### Todo List with Reducer (React)
|
|
648
|
-
|
|
649
|
-
```tsx
|
|
650
|
-
import { createReducer } from 'synstate';
|
|
651
|
-
import { useState, useEffect } from 'react';
|
|
652
|
-
|
|
653
|
-
type Todo = { id: number; text: string; done: boolean };
|
|
654
|
-
type Action =
|
|
655
|
-
| { type: 'add'; text: string }
|
|
656
|
-
| { type: 'toggle'; id: number }
|
|
657
|
-
| { type: 'remove'; id: number };
|
|
658
|
-
|
|
659
|
-
const todoState = createReducer<Todo[], Action>((todos, action) => {
|
|
660
|
-
switch (action.type) {
|
|
661
|
-
case 'add':
|
|
662
|
-
return [
|
|
663
|
-
...todos,
|
|
664
|
-
{
|
|
665
|
-
id: Date.now(),
|
|
666
|
-
text: action.text,
|
|
667
|
-
done: false,
|
|
668
|
-
},
|
|
669
|
-
];
|
|
670
|
-
case 'toggle':
|
|
671
|
-
return todos.map((t) =>
|
|
672
|
-
t.id === action.id ? { ...t, done: !t.done } : t,
|
|
673
|
-
);
|
|
674
|
-
case 'remove':
|
|
675
|
-
return todos.filter((t) => t.id !== action.id);
|
|
676
|
-
}
|
|
677
|
-
}, []);
|
|
678
|
-
|
|
679
|
-
function TodoList() {
|
|
680
|
-
const [todos, setTodos] = useState(todoState.getSnapshot());
|
|
681
|
-
|
|
682
|
-
useEffect(() => {
|
|
683
|
-
const sub = todoState.state.subscribe(setTodos);
|
|
684
|
-
return () => sub.unsubscribe();
|
|
685
|
-
}, []);
|
|
686
|
-
|
|
687
|
-
return (
|
|
688
|
-
<div>
|
|
689
|
-
{todos.map((todo) => (
|
|
690
|
-
<div key={todo.id}>
|
|
691
|
-
<input
|
|
692
|
-
type="checkbox"
|
|
693
|
-
checked={todo.done}
|
|
694
|
-
onChange={() =>
|
|
695
|
-
todoState.dispatch({
|
|
696
|
-
type: 'toggle',
|
|
697
|
-
id: todo.id,
|
|
698
|
-
})
|
|
699
|
-
}
|
|
700
|
-
/>
|
|
701
|
-
<span>{todo.text}</span>
|
|
702
|
-
</div>
|
|
703
|
-
))}
|
|
704
|
-
<button
|
|
705
|
-
onClick={() =>
|
|
706
|
-
todoState.dispatch({
|
|
707
|
-
type: 'add',
|
|
708
|
-
text: 'New Todo',
|
|
709
|
-
})
|
|
710
|
-
}
|
|
711
|
-
>
|
|
712
|
-
Add Todo
|
|
713
|
-
</button>
|
|
714
|
-
</div>
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
### Boolean State (Dark Mode)
|
|
720
|
-
|
|
721
|
-
```tsx
|
|
722
|
-
import { createBooleanState } from 'synstate';
|
|
723
|
-
import { useState, useEffect } from 'react';
|
|
724
|
-
|
|
725
|
-
export const darkModeState = createBooleanState(false);
|
|
726
|
-
|
|
727
|
-
function ThemeToggle() {
|
|
728
|
-
const [isDark, setIsDark] = useState(darkModeState.getSnapshot());
|
|
729
|
-
|
|
730
|
-
useEffect(() => {
|
|
731
|
-
const sub = darkModeState.state.subscribe(setIsDark);
|
|
732
|
-
return () => sub.unsubscribe();
|
|
733
|
-
}, []);
|
|
734
|
-
|
|
735
|
-
useEffect(() => {
|
|
736
|
-
document.body.className = isDark ? 'dark' : 'light';
|
|
737
|
-
}, [isDark]);
|
|
738
|
-
|
|
739
|
-
return (
|
|
740
|
-
<button onClick={() => darkModeState.toggle()}>
|
|
741
|
-
{isDark ? '🌙' : '☀️'}
|
|
742
|
-
</button>
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
### Cross-Component Communication
|
|
748
|
-
|
|
749
|
-
```tsx
|
|
750
|
-
import { createValueEmitter, createState } from 'synstate';
|
|
751
|
-
import { useState, useEffect } from 'react';
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
// Events
|
|
755
|
-
|
|
756
|
-
### Advanced: Search with Debounce
|
|
757
|
-
|
|
758
|
-
```tsx
|
|
759
|
-
import * as React from 'react';
|
|
760
|
-
import {
|
|
761
|
-
createState,
|
|
762
|
-
debounceTime,
|
|
763
|
-
filter,
|
|
764
|
-
fromPromise,
|
|
765
|
-
type Observable,
|
|
766
|
-
switchMap,
|
|
767
|
-
} from 'synstate';
|
|
768
|
-
import { Result } from 'ts-data-forge';
|
|
769
|
-
|
|
770
|
-
const [searchState, setSearchState] = createState('');
|
|
771
|
-
|
|
772
|
-
// Advanced reactive pipeline (optional feature)
|
|
773
|
-
const searchResults$: Observable<
|
|
774
|
-
Result<readonly Readonly<{ id: string; name: string }>[], unknown>
|
|
775
|
-
> = searchState
|
|
776
|
-
.pipe(debounceTime(300))
|
|
777
|
-
.pipe(filter((query) => query.length > 2))
|
|
778
|
-
.pipe(
|
|
779
|
-
switchMap((query) =>
|
|
780
|
-
fromPromise(
|
|
781
|
-
fetch(`/api/search?q=${query}`).then(
|
|
782
|
-
(r) =>
|
|
783
|
-
r.json() as Promise<
|
|
784
|
-
readonly Readonly<{ id: string; name: string }>[]
|
|
785
|
-
>,
|
|
786
|
-
),
|
|
787
|
-
),
|
|
788
|
-
),
|
|
789
|
-
);
|
|
582
|
+
// Create event emitter
|
|
583
|
+
const [refreshClicked, onRefreshClick] = createEventEmitter();
|
|
790
584
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
585
|
+
// Subscribe to events
|
|
586
|
+
refreshClicked.subscribe(() => {
|
|
587
|
+
console.log('Refresh Clicked');
|
|
588
|
+
});
|
|
795
589
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (Result.isOk(result)) {
|
|
799
|
-
setResults(result.value);
|
|
800
|
-
}
|
|
801
|
-
});
|
|
590
|
+
// Throttle refresh clicks to prevent rapid successive executions
|
|
591
|
+
const throttledRefresh = refreshClicked.pipe(throttleTime(2000));
|
|
802
592
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
593
|
+
throttledRefresh.subscribe(() => {
|
|
594
|
+
console.log('Executing refresh...');
|
|
595
|
+
// Actual refresh logic here
|
|
596
|
+
// This will be called at most once every 2 seconds
|
|
597
|
+
});
|
|
807
598
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
{results.map((item) => (
|
|
818
|
-
<li key={item.id}>{item.name}</li>
|
|
819
|
-
))}
|
|
820
|
-
</ul>
|
|
821
|
-
</div>
|
|
822
|
-
);
|
|
823
|
-
};
|
|
599
|
+
const DataTable = (): React.JSX.Element => (
|
|
600
|
+
<div>
|
|
601
|
+
<button onClick={onRefreshClick}>{'Refresh'}</button>
|
|
602
|
+
<p>
|
|
603
|
+
{'Data: '}
|
|
604
|
+
{/* Display data here */}
|
|
605
|
+
</p>
|
|
606
|
+
</div>
|
|
607
|
+
);
|
|
824
608
|
```
|
|
825
609
|
|
|
826
|
-
## Why
|
|
610
|
+
## Why SynState?
|
|
827
611
|
|
|
828
612
|
### Simple State Management, Not Complex Reactive Programming
|
|
829
613
|
|
|
830
|
-
|
|
614
|
+
SynState is a state management library for web frontends, similar to Redux, Jotai, Zustand, and MobX. It provides APIs for creating and managing global state across your application.
|
|
615
|
+
|
|
616
|
+
Under the hood, SynState is built on Observable patterns similar to those provided by RxJS. However, unlike RxJS, which can make code harder to read with many operators and complex streams, SynState focuses on **simple, readable state management and event handling**. Most applications only need `createState`, `createReducer`, and `createValueEmitter` - clean, straightforward APIs that developers understand immediately.
|
|
831
617
|
|
|
832
618
|
**Advanced reactive features are optional** and only used when you actually need them (like debouncing search input). The library doesn't force you into a reactive programming mindset.
|
|
833
619
|
|
|
@@ -840,7 +626,7 @@ Unlike RxJS, which can make code harder to read with many operators and complex
|
|
|
840
626
|
|
|
841
627
|
### Use Cases
|
|
842
628
|
|
|
843
|
-
**Use
|
|
629
|
+
**Use SynState when you need:**
|
|
844
630
|
|
|
845
631
|
- ✅ Global state management across components
|
|
846
632
|
- ✅ Event-driven communication between components
|
|
@@ -855,19 +641,7 @@ Unlike RxJS, which can make code harder to read with many operators and complex
|
|
|
855
641
|
|
|
856
642
|
## Type Safety
|
|
857
643
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
```tsx
|
|
861
|
-
const userState = createState({ name: 'Alice', age: 25 });
|
|
862
|
-
// state type: Observable<{ name: string; age: number }>
|
|
863
|
-
|
|
864
|
-
const snapshot = userState.getSnapshot();
|
|
865
|
-
// snapshot type: { name: string; age: number }
|
|
866
|
-
|
|
867
|
-
const [onClick$, emitClick] = createValueEmitter<MouseEvent>();
|
|
868
|
-
// onClick$ type: Observable<MouseEvent>
|
|
869
|
-
// emitClick type: (event: MouseEvent) => void
|
|
870
|
-
```
|
|
644
|
+
SynState maintains full type information.
|
|
871
645
|
|
|
872
646
|
## License
|
|
873
647
|
|
|
Binary file
|
|
@@ -9,21 +9,51 @@ import { type CombineObservableRefined, type Observable } from '../types/index.m
|
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```ts
|
|
12
|
+
* // Timeline:
|
|
13
|
+
* //
|
|
14
|
+
* // name$ "Alice" "Bob"
|
|
15
|
+
* // age$ 25 30
|
|
16
|
+
* // user$ ["Alice",25] ["Bob",25] ["Bob",30]
|
|
17
|
+
* //
|
|
18
|
+
* // Explanation:
|
|
19
|
+
* // - combine waits for all sources to emit at least once
|
|
20
|
+
* // - Then emits the latest value from all sources whenever any source emits
|
|
21
|
+
* // - Always emits an array with the latest values from each source
|
|
22
|
+
*
|
|
12
23
|
* const name$ = source<string>();
|
|
13
24
|
*
|
|
14
25
|
* const age$ = source<number>();
|
|
15
26
|
*
|
|
16
27
|
* const user$ = combine([name$, age$]);
|
|
17
28
|
*
|
|
29
|
+
* const mut_history: (readonly [string, number])[] = [];
|
|
30
|
+
*
|
|
18
31
|
* user$.subscribe(([name_, age]) => {
|
|
19
|
-
*
|
|
32
|
+
* mut_history.push([name_, age]);
|
|
20
33
|
* });
|
|
21
34
|
*
|
|
22
|
-
* name$.next('Alice');
|
|
35
|
+
* name$.next('Alice'); // nothing logged (age$ hasn't emitted yet)
|
|
36
|
+
*
|
|
37
|
+
* assert.deepStrictEqual(mut_history, []);
|
|
23
38
|
*
|
|
24
39
|
* age$.next(25); // logs: { name: 'Alice', age: 25 }
|
|
25
40
|
*
|
|
41
|
+
* assert.deepStrictEqual(mut_history, [['Alice', 25]]);
|
|
42
|
+
*
|
|
26
43
|
* name$.next('Bob'); // logs: { name: 'Bob', age: 25 }
|
|
44
|
+
*
|
|
45
|
+
* assert.deepStrictEqual(mut_history, [
|
|
46
|
+
* ['Alice', 25],
|
|
47
|
+
* ['Bob', 25],
|
|
48
|
+
* ]);
|
|
49
|
+
*
|
|
50
|
+
* age$.next(30); // logs: { name: 'Bob', age: 30 }
|
|
51
|
+
*
|
|
52
|
+
* assert.deepStrictEqual(mut_history, [
|
|
53
|
+
* ['Alice', 25],
|
|
54
|
+
* ['Bob', 25],
|
|
55
|
+
* ['Bob', 30],
|
|
56
|
+
* ]);
|
|
27
57
|
* ```
|
|
28
58
|
*/
|
|
29
59
|
export declare const combine: <const OS extends NonEmptyArray<Observable<unknown>>>(parents: OS) => CombineObservableRefined<OS>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combine.d.mts","sourceRoot":"","sources":["../../../src/core/combine/combine.mts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,wBAAwB,EAI7B,KAAK,UAAU,EAIhB,MAAM,oBAAoB,CAAC;AAE5B
|
|
1
|
+
{"version":3,"file":"combine.d.mts","sourceRoot":"","sources":["../../../src/core/combine/combine.mts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,wBAAwB,EAI7B,KAAK,UAAU,EAIhB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,CAAC,EAAE,SAAS,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,EACzE,SAAS,EAAE,KACV,wBAAwB,CAAC,EAAE,CAIgB,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,aAAa,SAZI,EAAE,SAAS,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,WAChE,EAAE,KACV,wBAAwB,CAAC,EAAE,CAUM,CAAC"}
|