synstate 0.1.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.
Files changed (268) hide show
  1. package/README.md +878 -0
  2. package/dist/core/class/child-observable-class.d.mts +37 -0
  3. package/dist/core/class/child-observable-class.d.mts.map +1 -0
  4. package/dist/core/class/child-observable-class.mjs +134 -0
  5. package/dist/core/class/child-observable-class.mjs.map +1 -0
  6. package/dist/core/class/index.d.mts +4 -0
  7. package/dist/core/class/index.d.mts.map +1 -0
  8. package/dist/core/class/index.mjs +4 -0
  9. package/dist/core/class/index.mjs.map +1 -0
  10. package/dist/core/class/observable-base-class.d.mts +28 -0
  11. package/dist/core/class/observable-base-class.d.mts.map +1 -0
  12. package/dist/core/class/observable-base-class.mjs +116 -0
  13. package/dist/core/class/observable-base-class.mjs.map +1 -0
  14. package/dist/core/class/root-observable-class.d.mts +12 -0
  15. package/dist/core/class/root-observable-class.d.mts.map +1 -0
  16. package/dist/core/class/root-observable-class.mjs +35 -0
  17. package/dist/core/class/root-observable-class.mjs.map +1 -0
  18. package/dist/core/combine/combine.d.mts +35 -0
  19. package/dist/core/combine/combine.d.mts.map +1 -0
  20. package/dist/core/combine/combine.mjs +94 -0
  21. package/dist/core/combine/combine.mjs.map +1 -0
  22. package/dist/core/combine/index.d.mts +4 -0
  23. package/dist/core/combine/index.d.mts.map +1 -0
  24. package/dist/core/combine/index.mjs +4 -0
  25. package/dist/core/combine/index.mjs.map +1 -0
  26. package/dist/core/combine/merge.d.mts +28 -0
  27. package/dist/core/combine/merge.d.mts.map +1 -0
  28. package/dist/core/combine/merge.mjs +52 -0
  29. package/dist/core/combine/merge.mjs.map +1 -0
  30. package/dist/core/combine/zip.d.mts +26 -0
  31. package/dist/core/combine/zip.d.mts.map +1 -0
  32. package/dist/core/combine/zip.mjs +63 -0
  33. package/dist/core/combine/zip.mjs.map +1 -0
  34. package/dist/core/create/from-array.d.mts +21 -0
  35. package/dist/core/create/from-array.d.mts.map +1 -0
  36. package/dist/core/create/from-array.mjs +47 -0
  37. package/dist/core/create/from-array.mjs.map +1 -0
  38. package/dist/core/create/from-promise.d.mts +25 -0
  39. package/dist/core/create/from-promise.d.mts.map +1 -0
  40. package/dist/core/create/from-promise.mjs +51 -0
  41. package/dist/core/create/from-promise.mjs.map +1 -0
  42. package/dist/core/create/from-subscribable.d.mts +3 -0
  43. package/dist/core/create/from-subscribable.d.mts.map +1 -0
  44. package/dist/core/create/from-subscribable.mjs +22 -0
  45. package/dist/core/create/from-subscribable.mjs.map +1 -0
  46. package/dist/core/create/index.d.mts +8 -0
  47. package/dist/core/create/index.d.mts.map +1 -0
  48. package/dist/core/create/index.mjs +8 -0
  49. package/dist/core/create/index.mjs.map +1 -0
  50. package/dist/core/create/interval.d.mts +21 -0
  51. package/dist/core/create/interval.d.mts.map +1 -0
  52. package/dist/core/create/interval.mjs +74 -0
  53. package/dist/core/create/interval.mjs.map +1 -0
  54. package/dist/core/create/of.d.mts +20 -0
  55. package/dist/core/create/of.d.mts.map +1 -0
  56. package/dist/core/create/of.mjs +44 -0
  57. package/dist/core/create/of.mjs.map +1 -0
  58. package/dist/core/create/source.d.mts +29 -0
  59. package/dist/core/create/source.d.mts.map +1 -0
  60. package/dist/core/create/source.mjs +29 -0
  61. package/dist/core/create/source.mjs.map +1 -0
  62. package/dist/core/create/timer.d.mts +20 -0
  63. package/dist/core/create/timer.d.mts.map +1 -0
  64. package/dist/core/create/timer.mjs +64 -0
  65. package/dist/core/create/timer.mjs.map +1 -0
  66. package/dist/core/index.d.mts +7 -0
  67. package/dist/core/index.d.mts.map +1 -0
  68. package/dist/core/index.mjs +37 -0
  69. package/dist/core/index.mjs.map +1 -0
  70. package/dist/core/operators/audit-time.d.mts +3 -0
  71. package/dist/core/operators/audit-time.d.mts.map +1 -0
  72. package/dist/core/operators/audit-time.mjs +50 -0
  73. package/dist/core/operators/audit-time.mjs.map +1 -0
  74. package/dist/core/operators/debounce-time.d.mts +31 -0
  75. package/dist/core/operators/debounce-time.d.mts.map +1 -0
  76. package/dist/core/operators/debounce-time.mjs +73 -0
  77. package/dist/core/operators/debounce-time.mjs.map +1 -0
  78. package/dist/core/operators/filter.d.mts +28 -0
  79. package/dist/core/operators/filter.d.mts.map +1 -0
  80. package/dist/core/operators/filter.mjs +38 -0
  81. package/dist/core/operators/filter.mjs.map +1 -0
  82. package/dist/core/operators/index.d.mts +18 -0
  83. package/dist/core/operators/index.d.mts.map +1 -0
  84. package/dist/core/operators/index.mjs +18 -0
  85. package/dist/core/operators/index.mjs.map +1 -0
  86. package/dist/core/operators/map-with-index.d.mts +39 -0
  87. package/dist/core/operators/map-with-index.d.mts.map +1 -0
  88. package/dist/core/operators/map-with-index.mjs +73 -0
  89. package/dist/core/operators/map-with-index.mjs.map +1 -0
  90. package/dist/core/operators/merge-map.d.mts +34 -0
  91. package/dist/core/operators/merge-map.d.mts.map +1 -0
  92. package/dist/core/operators/merge-map.mjs +75 -0
  93. package/dist/core/operators/merge-map.mjs.map +1 -0
  94. package/dist/core/operators/pairwise.d.mts +27 -0
  95. package/dist/core/operators/pairwise.d.mts.map +1 -0
  96. package/dist/core/operators/pairwise.mjs +59 -0
  97. package/dist/core/operators/pairwise.mjs.map +1 -0
  98. package/dist/core/operators/scan.d.mts +30 -0
  99. package/dist/core/operators/scan.d.mts.map +1 -0
  100. package/dist/core/operators/scan.mjs +56 -0
  101. package/dist/core/operators/scan.mjs.map +1 -0
  102. package/dist/core/operators/skip-if-no-change.d.mts +33 -0
  103. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -0
  104. package/dist/core/operators/skip-if-no-change.mjs +68 -0
  105. package/dist/core/operators/skip-if-no-change.mjs.map +1 -0
  106. package/dist/core/operators/skip-until.d.mts +3 -0
  107. package/dist/core/operators/skip-until.d.mts.map +1 -0
  108. package/dist/core/operators/skip-until.mjs +33 -0
  109. package/dist/core/operators/skip-until.mjs.map +1 -0
  110. package/dist/core/operators/skip-while.d.mts +4 -0
  111. package/dist/core/operators/skip-while.d.mts.map +1 -0
  112. package/dist/core/operators/skip-while.mjs +40 -0
  113. package/dist/core/operators/skip-while.mjs.map +1 -0
  114. package/dist/core/operators/switch-map.d.mts +31 -0
  115. package/dist/core/operators/switch-map.d.mts.map +1 -0
  116. package/dist/core/operators/switch-map.mjs +70 -0
  117. package/dist/core/operators/switch-map.mjs.map +1 -0
  118. package/dist/core/operators/take-until.d.mts +32 -0
  119. package/dist/core/operators/take-until.d.mts.map +1 -0
  120. package/dist/core/operators/take-until.mjs +60 -0
  121. package/dist/core/operators/take-until.mjs.map +1 -0
  122. package/dist/core/operators/take-while.d.mts +4 -0
  123. package/dist/core/operators/take-while.d.mts.map +1 -0
  124. package/dist/core/operators/take-while.mjs +42 -0
  125. package/dist/core/operators/take-while.mjs.map +1 -0
  126. package/dist/core/operators/throttle-time.d.mts +23 -0
  127. package/dist/core/operators/throttle-time.d.mts.map +1 -0
  128. package/dist/core/operators/throttle-time.mjs +68 -0
  129. package/dist/core/operators/throttle-time.mjs.map +1 -0
  130. package/dist/core/operators/with-buffered-from.d.mts +4 -0
  131. package/dist/core/operators/with-buffered-from.d.mts.map +1 -0
  132. package/dist/core/operators/with-buffered-from.mjs +45 -0
  133. package/dist/core/operators/with-buffered-from.mjs.map +1 -0
  134. package/dist/core/operators/with-current-value-from.d.mts +4 -0
  135. package/dist/core/operators/with-current-value-from.d.mts.map +1 -0
  136. package/dist/core/operators/with-current-value-from.mjs +37 -0
  137. package/dist/core/operators/with-current-value-from.mjs.map +1 -0
  138. package/dist/core/operators/with-initial-value.d.mts +26 -0
  139. package/dist/core/operators/with-initial-value.d.mts.map +1 -0
  140. package/dist/core/operators/with-initial-value.mjs +47 -0
  141. package/dist/core/operators/with-initial-value.mjs.map +1 -0
  142. package/dist/core/types/id.d.mts +4 -0
  143. package/dist/core/types/id.d.mts.map +1 -0
  144. package/dist/core/types/id.mjs +2 -0
  145. package/dist/core/types/id.mjs.map +1 -0
  146. package/dist/core/types/index.d.mts +6 -0
  147. package/dist/core/types/index.d.mts.map +1 -0
  148. package/dist/core/types/index.mjs +3 -0
  149. package/dist/core/types/index.mjs.map +1 -0
  150. package/dist/core/types/observable-family.d.mts +68 -0
  151. package/dist/core/types/observable-family.d.mts.map +1 -0
  152. package/dist/core/types/observable-family.mjs +2 -0
  153. package/dist/core/types/observable-family.mjs.map +1 -0
  154. package/dist/core/types/observable-kind.d.mts +4 -0
  155. package/dist/core/types/observable-kind.d.mts.map +1 -0
  156. package/dist/core/types/observable-kind.mjs +2 -0
  157. package/dist/core/types/observable-kind.mjs.map +1 -0
  158. package/dist/core/types/observable.d.mts +83 -0
  159. package/dist/core/types/observable.d.mts.map +1 -0
  160. package/dist/core/types/observable.mjs +10 -0
  161. package/dist/core/types/observable.mjs.map +1 -0
  162. package/dist/core/types/types.d.mts +16 -0
  163. package/dist/core/types/types.d.mts.map +1 -0
  164. package/dist/core/types/types.mjs +2 -0
  165. package/dist/core/types/types.mjs.map +1 -0
  166. package/dist/core/utils/id-maker.d.mts +5 -0
  167. package/dist/core/utils/id-maker.d.mts.map +1 -0
  168. package/dist/core/utils/id-maker.mjs +17 -0
  169. package/dist/core/utils/id-maker.mjs.map +1 -0
  170. package/dist/core/utils/index.d.mts +5 -0
  171. package/dist/core/utils/index.d.mts.map +1 -0
  172. package/dist/core/utils/index.mjs +5 -0
  173. package/dist/core/utils/index.mjs.map +1 -0
  174. package/dist/core/utils/max-depth.d.mts +3 -0
  175. package/dist/core/utils/max-depth.d.mts.map +1 -0
  176. package/dist/core/utils/max-depth.mjs +8 -0
  177. package/dist/core/utils/max-depth.mjs.map +1 -0
  178. package/dist/core/utils/observable-utils.d.mts +3 -0
  179. package/dist/core/utils/observable-utils.d.mts.map +1 -0
  180. package/dist/core/utils/observable-utils.mjs +7 -0
  181. package/dist/core/utils/observable-utils.mjs.map +1 -0
  182. package/dist/core/utils/utils.d.mts +4 -0
  183. package/dist/core/utils/utils.d.mts.map +1 -0
  184. package/dist/core/utils/utils.mjs +38 -0
  185. package/dist/core/utils/utils.mjs.map +1 -0
  186. package/dist/entry-point.d.mts +2 -0
  187. package/dist/entry-point.d.mts.map +1 -0
  188. package/dist/entry-point.mjs +40 -0
  189. package/dist/entry-point.mjs.map +1 -0
  190. package/dist/globals.d.mts +4 -0
  191. package/dist/index.d.mts +3 -0
  192. package/dist/index.d.mts.map +1 -0
  193. package/dist/index.mjs +40 -0
  194. package/dist/index.mjs.map +1 -0
  195. package/dist/tsconfig.json +1 -0
  196. package/dist/types.d.mts +2 -0
  197. package/dist/utils/create-event-emitter.d.mts +39 -0
  198. package/dist/utils/create-event-emitter.d.mts.map +1 -0
  199. package/dist/utils/create-event-emitter.mjs +57 -0
  200. package/dist/utils/create-event-emitter.mjs.map +1 -0
  201. package/dist/utils/create-reducer.d.mts +34 -0
  202. package/dist/utils/create-reducer.d.mts.map +1 -0
  203. package/dist/utils/create-reducer.mjs +49 -0
  204. package/dist/utils/create-reducer.mjs.map +1 -0
  205. package/dist/utils/create-state.d.mts +61 -0
  206. package/dist/utils/create-state.d.mts.map +1 -0
  207. package/dist/utils/create-state.mjs +92 -0
  208. package/dist/utils/create-state.mjs.map +1 -0
  209. package/dist/utils/index.d.mts +4 -0
  210. package/dist/utils/index.d.mts.map +1 -0
  211. package/dist/utils/index.mjs +4 -0
  212. package/dist/utils/index.mjs.map +1 -0
  213. package/package.json +71 -0
  214. package/src/core/class/child-observable-class.mts +232 -0
  215. package/src/core/class/index.mts +3 -0
  216. package/src/core/class/observable-base-class.mts +186 -0
  217. package/src/core/class/observable.class.test.mts +89 -0
  218. package/src/core/class/root-observable-class.mts +68 -0
  219. package/src/core/combine/combine.mts +144 -0
  220. package/src/core/combine/index.mts +3 -0
  221. package/src/core/combine/merge.mts +84 -0
  222. package/src/core/combine/zip.mts +149 -0
  223. package/src/core/create/from-array.mts +58 -0
  224. package/src/core/create/from-promise.mts +58 -0
  225. package/src/core/create/from-subscribable.mts +37 -0
  226. package/src/core/create/index.mts +7 -0
  227. package/src/core/create/interval.mts +99 -0
  228. package/src/core/create/of.mts +54 -0
  229. package/src/core/create/source.mts +59 -0
  230. package/src/core/create/timer.mts +84 -0
  231. package/src/core/index.mts +6 -0
  232. package/src/core/operators/audit-time.mts +77 -0
  233. package/src/core/operators/debounce-time.mts +96 -0
  234. package/src/core/operators/filter.mts +125 -0
  235. package/src/core/operators/index.mts +17 -0
  236. package/src/core/operators/map-with-index.mts +168 -0
  237. package/src/core/operators/merge-map.mts +108 -0
  238. package/src/core/operators/pairwise.mts +77 -0
  239. package/src/core/operators/scan.mts +81 -0
  240. package/src/core/operators/skip-if-no-change.mts +91 -0
  241. package/src/core/operators/skip-until.mts +54 -0
  242. package/src/core/operators/skip-while.mts +77 -0
  243. package/src/core/operators/switch-map.mts +101 -0
  244. package/src/core/operators/take-until.mts +80 -0
  245. package/src/core/operators/take-while.mts +103 -0
  246. package/src/core/operators/throttle-time.mts +95 -0
  247. package/src/core/operators/with-buffered-from.mts +68 -0
  248. package/src/core/operators/with-current-value-from.mts +58 -0
  249. package/src/core/operators/with-initial-value.mts +76 -0
  250. package/src/core/types/id.mts +5 -0
  251. package/src/core/types/index.mts +5 -0
  252. package/src/core/types/observable-family.mts +259 -0
  253. package/src/core/types/observable-kind.mts +5 -0
  254. package/src/core/types/observable.mts +218 -0
  255. package/src/core/types/types.mts +40 -0
  256. package/src/core/utils/id-maker.mts +31 -0
  257. package/src/core/utils/index.mts +4 -0
  258. package/src/core/utils/max-depth.mts +7 -0
  259. package/src/core/utils/observable-utils.mts +10 -0
  260. package/src/core/utils/utils.mts +51 -0
  261. package/src/core/utils/utils.test.mts +88 -0
  262. package/src/entry-point.mts +1 -0
  263. package/src/globals.d.mts +4 -0
  264. package/src/index.mts +2 -0
  265. package/src/utils/create-event-emitter.mts +62 -0
  266. package/src/utils/create-reducer.mts +55 -0
  267. package/src/utils/create-state.mts +138 -0
  268. package/src/utils/index.mts +3 -0
package/README.md ADDED
@@ -0,0 +1,878 @@
1
+ # SyncFlow
2
+
3
+ [![npm version](https://img.shields.io/npm/v/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge)
4
+ [![npm downloads](https://img.shields.io/npm/dm/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge)
5
+ [![License](https://img.shields.io/npm/l/ts-data-forge.svg)](./LICENSE)
6
+ [![codecov](https://codecov.io/gh/noshiro-pf/ts-data-forge/branch/main/graph/badge.svg?token=69TA40HACZ)](https://codecov.io/gh/noshiro-pf/ts-data-forge)
7
+
8
+ **SyncFlow** is a lightweight, 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
+
10
+ ## Features
11
+
12
+ - 🎯 **Simple State Management**: Easy-to-use `createState` and `createReducer` for global state
13
+ - 📡 **Event System**: Built-in `createValueEmitter`, `createEventEmitter` for event-driven architecture
14
+ - 🔄 **Reactive Updates**: Automatic propagation of state changes to subscribers
15
+ - 🎨 **Type-Safe**: Full TypeScript support with precise type inference
16
+ - 🚀 **Lightweight**: Minimal bundle size, zero external runtime dependencies
17
+ - ⚡ **Framework Agnostic**: Works with React, Vue, Svelte, or vanilla JavaScript
18
+ - 🔧 **Flexible**: Simple state management with optional advanced features
19
+
20
+ ## Documentation
21
+
22
+ - API reference: <https://noshiro-pf.github.io/synstate/>
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm add synstate
28
+ ```
29
+
30
+ Or with other package managers:
31
+
32
+ ```bash
33
+ # Yarn
34
+ yarn add synstate
35
+
36
+ # pnpm
37
+ pnpm add synstate
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### Simple State Management
43
+
44
+ ```tsx
45
+ import { createState } from 'synstate';
46
+
47
+ // Create a reactive state
48
+ const [state, setState, { updateState }] = createState(0);
49
+
50
+ // Subscribe to changes (in React components, Vue watchers, etc.)
51
+ state.subscribe((count: number) => {
52
+ console.log('Count:', count);
53
+ });
54
+
55
+ // Update state
56
+ setState(1);
57
+
58
+ updateState((prev: number) => prev + 1);
59
+ ```
60
+
61
+ ### Event Emitter
62
+
63
+ ```tsx
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
+ });
75
+
76
+ // Emit events
77
+ emitUserLoggedIn({ id: 1, name: 'Alice' });
78
+ ```
79
+
80
+ ### With React
81
+
82
+ ```tsx
83
+ import * as React from 'react';
84
+ import { createState } from 'synstate';
85
+
86
+ // Global state (outside component)
87
+ const [userState, setUserState, { getSnapshot }] = createState({
88
+ name: '',
89
+ email: '',
90
+ });
91
+
92
+ const UserProfile = (): React.JSX.Element => {
93
+ const [user, setUser] = React.useState(getSnapshot());
94
+
95
+ React.useEffect(() => {
96
+ const subscription = userState.subscribe(setUser);
97
+
98
+ return () => {
99
+ subscription.unsubscribe();
100
+ };
101
+ }, []);
102
+
103
+ return (
104
+ <div>
105
+ <p>
106
+ {'Name: '}
107
+ {user.name}
108
+ </p>
109
+ <button
110
+ onClick={() => {
111
+ setUserState({
112
+ name: 'Alice',
113
+ email: 'alice@example.com',
114
+ });
115
+ }}
116
+ >
117
+ {'Set User'}
118
+ </button>
119
+ </div>
120
+ );
121
+ };
122
+ ```
123
+
124
+ ## Core Concepts
125
+
126
+ ### State Management
127
+
128
+ SyncFlow provides simple, intuitive APIs for managing application state:
129
+
130
+ - **`createState`**: Create mutable state with getter/setter
131
+ - **`createReducer`**: Redux-style state management
132
+ - **`createBooleanState`**: Specialized state for boolean values
133
+
134
+ ### Event System
135
+
136
+ Built-in event emitter for event-driven patterns:
137
+
138
+ - **`createValueEmitter`**: Create type-safe event emitters
139
+ - **`createEventEmitter`**: Create event emitters without payload
140
+
141
+ ### Observable (Optional Advanced Feature)
142
+
143
+ 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
+
145
+ ```tsx
146
+ import * as React from 'react';
147
+ import { createState } from 'synstate';
148
+
149
+ // Create global state
150
+ export const [counterState, , { updateState, resetState, getSnapshot }] =
151
+ createState(0);
152
+
153
+ // Component 1
154
+ const Counter = (): React.JSX.Element => {
155
+ const [count, setCount] = React.useState(getSnapshot());
156
+
157
+ React.useEffect(() => {
158
+ const sub = counterState.subscribe(setCount);
159
+
160
+ return () => {
161
+ sub.unsubscribe();
162
+ };
163
+ }, []);
164
+
165
+ return (
166
+ <div>
167
+ <p>
168
+ {'Count: '}
169
+ {count}
170
+ </p>
171
+ <button
172
+ onClick={() => {
173
+ updateState((n: number) => n + 1);
174
+ }}
175
+ >
176
+ {'Increment'}
177
+ </button>
178
+ </div>
179
+ );
180
+ };
181
+
182
+ // Component 2 (synced automatically)
183
+ const ResetButton = (): React.JSX.Element => (
184
+ <button
185
+ onClick={() => {
186
+ resetState();
187
+ }}
188
+ >
189
+ {'Reset'}
190
+ </button>
191
+ );
192
+ ```
193
+
194
+ ## API Reference
195
+
196
+ ### State Management (Recommended)
197
+
198
+ #### createState
199
+
200
+ Create reactive state with getter and setter:
201
+
202
+ ```tsx
203
+ import * as React from 'react';
204
+ import { createEventEmitter, createValueEmitter } from 'synstate';
205
+
206
+ // Global events
207
+ export const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<
208
+ Readonly<{
209
+ id: number;
210
+ name: string;
211
+ }>
212
+ >();
213
+
214
+ export const [userLoggedOut$, emitUserLoggedOut] = createEventEmitter();
215
+
216
+ // Component that emits events
217
+ const LoginButton = (): React.JSX.Element => {
218
+ const handleLogin = React.useCallback(() => {
219
+ (async () => {
220
+ const user = await loginUser();
221
+
222
+ emitUserLoggedIn(user);
223
+ })().catch(() => {});
224
+ }, []);
225
+
226
+ return <button onClick={handleLogin}>{'Login'}</button>;
227
+ };
228
+
229
+ // Component that listens to events
230
+ const NotificationPage = (): React.JSX.Element => {
231
+ const [message, setMessage] = React.useState('');
232
+
233
+ React.useEffect(() => {
234
+ const sub1 = userLoggedIn$.subscribe((user) => {
235
+ setMessage(`Welcome, ${user.name}!`);
236
+ });
237
+
238
+ const sub2 = userLoggedOut$.subscribe(() => {
239
+ setMessage('Logged out');
240
+ });
241
+
242
+ return () => {
243
+ sub1.unsubscribe();
244
+
245
+ sub2.unsubscribe();
246
+ };
247
+ }, []);
248
+
249
+ return message !== '' ? (
250
+ <div className={'notification'}>{message}</div>
251
+ ) : (
252
+ <>{null}</>
253
+ );
254
+ };
255
+
256
+ const loginUser = async (): Promise<
257
+ Readonly<{
258
+ id: number;
259
+ name: string;
260
+ }>
261
+ > => ({ id: 1, name: 'Alice' });
262
+ ```
263
+
264
+ #### createBooleanState
265
+
266
+ Specialized state for boolean values:
267
+
268
+ ```tsx
269
+ import * as React from 'react';
270
+ import { createReducer } from 'synstate';
271
+
272
+ type Todo = Readonly<{ id: number; text: string; done: boolean }>;
273
+
274
+ type Action = Readonly<
275
+ | { type: 'add'; text: string }
276
+ | { type: 'toggle'; id: number }
277
+ | { type: 'remove'; id: number }
278
+ >;
279
+
280
+ const [todoState, dispatch, getSnapshot] = createReducer<
281
+ readonly Todo[],
282
+ Action
283
+ >((todos, action) => {
284
+ switch (action.type) {
285
+ case 'add':
286
+ return [
287
+ ...todos,
288
+ {
289
+ id: Date.now(),
290
+ text: action.text,
291
+ done: false,
292
+ },
293
+ ];
294
+ case 'toggle':
295
+ return todos.map((t) =>
296
+ t.id === action.id ? { ...t, done: !t.done } : t,
297
+ );
298
+ case 'remove':
299
+ return todos.filter((t) => t.id !== action.id);
300
+ }
301
+ }, []);
302
+
303
+ const TodoList = (): React.JSX.Element => {
304
+ const [todos, setTodos] = React.useState(getSnapshot());
305
+
306
+ React.useEffect(() => {
307
+ const sub = todoState.subscribe(setTodos);
308
+
309
+ return () => {
310
+ sub.unsubscribe();
311
+ };
312
+ }, []);
313
+
314
+ return (
315
+ <div>
316
+ {todos.map((todo) => (
317
+ <div key={todo.id}>
318
+ <input
319
+ checked={todo.done}
320
+ type={'checkbox'}
321
+ onChange={() => {
322
+ dispatch({
323
+ type: 'toggle',
324
+ id: todo.id,
325
+ });
326
+ }}
327
+ />
328
+ <span>{todo.text}</span>
329
+ </div>
330
+ ))}
331
+ <button
332
+ onClick={() => {
333
+ dispatch({
334
+ type: 'add',
335
+ text: 'New Todo',
336
+ });
337
+ }}
338
+ >
339
+ {'Add Todo'}
340
+ </button>
341
+ </div>
342
+ );
343
+ };
344
+ ```
345
+
346
+ #### createReducer
347
+
348
+ Create state with reducer pattern (like Redux):
349
+
350
+ ```tsx
351
+ import * as React from 'react';
352
+ import { createBooleanState } from 'synstate';
353
+
354
+ export const [darkModeState, { toggle, getSnapshot }] =
355
+ createBooleanState(false);
356
+
357
+ const ThemeToggle = (): React.JSX.Element => {
358
+ const [isDark, setIsDark] = React.useState(getSnapshot());
359
+
360
+ React.useEffect(() => {
361
+ const sub = darkModeState.subscribe(setIsDark);
362
+
363
+ return () => {
364
+ sub.unsubscribe();
365
+ };
366
+ }, []);
367
+
368
+ React.useEffect(() => {
369
+ document.body.className = isDark ? 'dark' : 'light';
370
+ }, [isDark]);
371
+
372
+ return (
373
+ <button
374
+ onClick={() => {
375
+ toggle();
376
+ }}
377
+ >
378
+ {isDark ? '🌙' : '☀️'}
379
+ </button>
380
+ );
381
+ };
382
+ ```
383
+
384
+ ### Event System
385
+
386
+ #### createValueEmitter
387
+
388
+ Create type-safe event emitter with payload:
389
+
390
+ ```tsx
391
+ import * as React from 'react';
392
+ import { createEventEmitter, createState, createValueEmitter } from 'synstate';
393
+
394
+ // Events
395
+ const [onItemAdded$, emitItemAdded] = createValueEmitter<string>();
396
+
397
+ const [onClearAll$, emitClearAll] = createEventEmitter();
398
+
399
+ // State
400
+ const [itemsState, setItemsState, { updateState, getSnapshot }] = createState<
401
+ readonly string[]
402
+ >([]);
403
+
404
+ // Setup event handlers
405
+ onItemAdded$.subscribe((item) => {
406
+ updateState((items: readonly string[]) => [...items, item]);
407
+ });
408
+
409
+ onClearAll$.subscribe(() => {
410
+ setItemsState([]);
411
+ });
412
+
413
+ // Component 1: Add items
414
+ const ItemInput = (): React.JSX.Element => {
415
+ const [input, setInput] = React.useState('');
416
+
417
+ return (
418
+ <div>
419
+ <input
420
+ value={input}
421
+ onChange={(e) => {
422
+ setInput(e.target.value);
423
+ }}
424
+ />
425
+ <button
426
+ onClick={() => {
427
+ emitItemAdded(input);
428
+
429
+ setInput('');
430
+ }}
431
+ >
432
+ {'Add'}
433
+ </button>
434
+ </div>
435
+ );
436
+ };
437
+
438
+ // Component 2: Display items
439
+ const ItemList = (): React.JSX.Element => {
440
+ const [items, setItems] = React.useState(getSnapshot());
441
+
442
+ React.useEffect(() => {
443
+ const sub = itemsState.subscribe(setItems);
444
+
445
+ return () => {
446
+ sub.unsubscribe();
447
+ };
448
+ }, []);
449
+
450
+ return (
451
+ <div>
452
+ <ul>
453
+ {items.map((item, i) => (
454
+ <li key={i}>{item}</li>
455
+ ))}
456
+ </ul>
457
+ <button onClick={emitClearAll}>{'Clear All'}</button>
458
+ </div>
459
+ );
460
+ };
461
+ ```
462
+
463
+ #### createEventEmitter
464
+
465
+ Create event emitter without payload:
466
+
467
+ ```tsx
468
+ import * as React from 'react';
469
+ import {
470
+ createState,
471
+ debounceTime,
472
+ filter,
473
+ fromPromise,
474
+ type Observable,
475
+ switchMap,
476
+ } from 'synstate';
477
+ import { Result } from 'ts-data-forge';
478
+
479
+ const [searchState, setSearchState] = createState('');
480
+
481
+ // Advanced reactive pipeline (optional feature)
482
+ const searchResults$: Observable<
483
+ Result<readonly Readonly<{ id: string; name: string }>[], unknown>
484
+ > = searchState
485
+ .pipe(debounceTime(300))
486
+ .pipe(filter((query) => query.length > 2))
487
+ .pipe(
488
+ switchMap((query) =>
489
+ fromPromise(
490
+ fetch(`/api/search?q=${query}`).then(
491
+ (r) =>
492
+ r.json() as Promise<
493
+ readonly Readonly<{ id: string; name: string }>[]
494
+ >,
495
+ ),
496
+ ),
497
+ ),
498
+ );
499
+
500
+ const SearchBox = (): React.JSX.Element => {
501
+ const [results, setResults] = React.useState<
502
+ readonly Readonly<{ id: string; name: string }>[]
503
+ >([]);
504
+
505
+ React.useEffect(() => {
506
+ const sub = searchResults$.subscribe((result) => {
507
+ if (Result.isOk(result)) {
508
+ setResults(result.value);
509
+ }
510
+ });
511
+
512
+ return () => {
513
+ sub.unsubscribe();
514
+ };
515
+ }, []);
516
+
517
+ return (
518
+ <div>
519
+ <input
520
+ placeholder={'Search...'}
521
+ onChange={(e) => {
522
+ setSearchState(e.target.value);
523
+ }}
524
+ />
525
+ <ul>
526
+ {results.map((item) => (
527
+ <li key={item.id}>{item.name}</li>
528
+ ))}
529
+ </ul>
530
+ </div>
531
+ );
532
+ };
533
+ ```
534
+
535
+ ### Advanced Features (Optional)
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)
567
+
568
+ ```tsx
569
+ import { createState } from 'synstate';
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
+ ```
599
+
600
+ ### Event-Driven Architecture (React)
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
+ );
790
+
791
+ const SearchBox = (): React.JSX.Element => {
792
+ const [results, setResults] = React.useState<
793
+ readonly Readonly<{ id: string; name: string }>[]
794
+ >([]);
795
+
796
+ React.useEffect(() => {
797
+ const sub = searchResults$.subscribe((result) => {
798
+ if (Result.isOk(result)) {
799
+ setResults(result.value);
800
+ }
801
+ });
802
+
803
+ return () => {
804
+ sub.unsubscribe();
805
+ };
806
+ }, []);
807
+
808
+ return (
809
+ <div>
810
+ <input
811
+ placeholder={'Search...'}
812
+ onChange={(e) => {
813
+ setSearchState(e.target.value);
814
+ }}
815
+ />
816
+ <ul>
817
+ {results.map((item) => (
818
+ <li key={item.id}>{item.name}</li>
819
+ ))}
820
+ </ul>
821
+ </div>
822
+ );
823
+ };
824
+ ```
825
+
826
+ ## Why SyncFlow?
827
+
828
+ ### Simple State Management, Not Complex Reactive Programming
829
+
830
+ Unlike RxJS, which can make code harder to read with many operators and complex streams, SyncFlow 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
+
832
+ **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
+
834
+ ### Key Differences from RxJS
835
+
836
+ - **Focus on State & Events**: Designed for state management and event-driven architecture
837
+ - **Simpler API**: Most use cases covered by `createState`, `createReducer`, and `createValueEmitter`
838
+ - **Better Readability**: No need for complex operator chains in everyday code
839
+ - **Optional Complexity**: Advanced features available when needed
840
+
841
+ ### Use Cases
842
+
843
+ **Use SyncFlow when you need:**
844
+
845
+ - ✅ Global state management across components
846
+ - ✅ Event-driven communication between components
847
+ - ✅ Type-safe event emitters
848
+ - ✅ Redux-like state with reducers
849
+ - ✅ Simple reactive patterns (debounce, throttle, etc.)
850
+
851
+ **Consider other solutions when:**
852
+
853
+ - ❌ You need complex stream processing (use RxJS)
854
+ - ❌ Your app is simple enough for React Context alone
855
+
856
+ ## Type Safety
857
+
858
+ SyncFlow maintains full type information:
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
+ ```
871
+
872
+ ## License
873
+
874
+ This project is licensed under the [Apache License 2.0](./LICENSE).
875
+
876
+ ## Repository
877
+
878
+ <https://github.com/noshiro-pf/synstate>