@alwatr/signal 9.26.0 → 9.29.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 (52) hide show
  1. package/README.md +197 -603
  2. package/dist/core/channel-signal.d.ts +12 -53
  3. package/dist/core/channel-signal.d.ts.map +1 -1
  4. package/dist/core/computed-signal.d.ts +19 -33
  5. package/dist/core/computed-signal.d.ts.map +1 -1
  6. package/dist/core/derived-signal.d.ts +71 -0
  7. package/dist/core/derived-signal.d.ts.map +1 -0
  8. package/dist/core/effect-signal.d.ts +15 -1
  9. package/dist/core/effect-signal.d.ts.map +1 -1
  10. package/dist/core/event-signal.d.ts +11 -4
  11. package/dist/core/event-signal.d.ts.map +1 -1
  12. package/dist/core/persistent-state-signal.d.ts +21 -2
  13. package/dist/core/persistent-state-signal.d.ts.map +1 -1
  14. package/dist/core/session-state-signal.d.ts +19 -2
  15. package/dist/core/session-state-signal.d.ts.map +1 -1
  16. package/dist/core/signal-base.d.ts +58 -38
  17. package/dist/core/signal-base.d.ts.map +1 -1
  18. package/dist/core/state-signal.d.ts +33 -14
  19. package/dist/core/state-signal.d.ts.map +1 -1
  20. package/dist/creators/channel.d.ts +1 -1
  21. package/dist/creators/channel.d.ts.map +1 -1
  22. package/dist/creators/derived.d.ts +31 -0
  23. package/dist/creators/derived.d.ts.map +1 -0
  24. package/dist/main.d.ts +2 -1
  25. package/dist/main.d.ts.map +1 -1
  26. package/dist/main.js +3 -3
  27. package/dist/main.js.map +16 -15
  28. package/dist/operators/debounce.d.ts +2 -3
  29. package/dist/operators/debounce.d.ts.map +1 -1
  30. package/dist/operators/filter.d.ts +14 -13
  31. package/dist/operators/filter.d.ts.map +1 -1
  32. package/dist/type.d.ts +68 -3
  33. package/dist/type.d.ts.map +1 -1
  34. package/package.json +6 -6
  35. package/src/core/channel-signal.ts +25 -68
  36. package/src/core/computed-signal.ts +50 -74
  37. package/src/core/derived-signal.ts +166 -0
  38. package/src/core/effect-signal.ts +23 -11
  39. package/src/core/event-signal.ts +14 -9
  40. package/src/core/persistent-state-signal.ts +21 -4
  41. package/src/core/session-state-signal.ts +19 -4
  42. package/src/core/signal-base.ts +98 -61
  43. package/src/core/state-signal.ts +48 -29
  44. package/src/creators/channel.ts +1 -2
  45. package/src/creators/derived.ts +34 -0
  46. package/src/main.ts +2 -1
  47. package/src/operators/debounce.ts +13 -23
  48. package/src/operators/filter.ts +20 -26
  49. package/src/type.ts +71 -3
  50. package/dist/operators/map.d.ts +0 -36
  51. package/dist/operators/map.d.ts.map +0 -1
  52. package/src/operators/map.ts +0 -48
package/README.md CHANGED
@@ -1,140 +1,152 @@
1
1
  # Alwatr Signal
2
2
 
3
- [](https://www.google.com/search?q=alwatr+flux)
4
- [](https://www.google.com/search?q=alwatr+signal)
5
- [](https://www.google.com/search?q=alwatr)
6
- [](https://www.npmjs.com/package/%40alwatr/flux)
7
- [](https://www.npmjs.com/package/%40alwatr/signal)
8
-
9
3
  Alwatr Signal is a powerful, lightweight, and modern reactive programming library. It is inspired by the best concepts from major reactive libraries but engineered to be faster and more efficient than all of them. It provides a robust and elegant way to manage application state through a system of signals, offering fine-grained reactivity, predictability, and excellent performance.
10
4
 
11
- It's designed to be simple to learn, yet capable of handling complex state management scenarios.
5
+ It's designed to be simple to learn, yet capable of handling complex state management scenarios in browser and Bun/Node.js environments.
12
6
 
13
7
  ## Features
14
8
 
15
- - **Type-Safe**: Fully implemented in TypeScript for robust, type-safe code.
16
- - **Lightweight**: A small footprint with zero third-party dependencies.
9
+ - **Type-Safe**: Fully implemented in TypeScript with strong types for every signal.
10
+ - **Lightweight**: Extremely small footprint with zero third-party dependencies.
17
11
  - **Performant**: Smart change detection and batched updates prevent unnecessary computations and re-renders.
18
12
  - **Predictable**: Asynchronous, non-blocking notifications ensure a consistent and understandable data flow.
19
13
  - **Lifecycle Management**: Built-in `destroy()` methods for easy cleanup and memory leak prevention.
20
- - **Easy to Debug**: Unique `name` for each signal makes logging and tracing a breeze.
14
+ - **Easy to Debug**: Scoped logger for each signal makes logging and tracing a breeze.
21
15
 
22
- ## Core Concepts
16
+ ---
23
17
 
24
- Signals are the fundamental building blocks in Alwatr Signal. They are special objects that hold a value and can notify interested consumers when that value changes. There are three main types of signals:
18
+ ## Primitives
25
19
 
26
- 1. **`StateSignal`**: The foundation of reactivity. It holds a mutable value. When you `set()` a new value, it notifies all its dependents.
27
- 2. **`ComputedSignal`**: A read-only signal that derives its value from other signals. It automatically updates when its dependencies change. The result is memoized, so the calculation only runs when needed.
28
- 3. **`EffectSignal`**: The bridge to the "outside world." It executes a side effect (like logging or rendering) in response to changes in the signals it depends on.
20
+ There are five core types of signals available:
29
21
 
30
- There is also a fourth type for stateless events:
22
+ 1. **`StateSignal<T>`**: The foundation of reactivity. It holds a mutable value. When you `set()` a new value, it notifies all its dependents.
23
+ 2. **`EventSignal<T>`**: A stateless signal for dispatching transient, one-off events that don't have a persistent value (e.g. user clicks).
24
+ 3. **`ComputedSignal<T>`**: A read-only signal that derives its value from other signals. It automatically updates when its dependencies change. The result is memoized, so the calculation only runs when needed.
25
+ 4. **`EffectSignal`**: The bridge to the side-effects. It executes a function (like logging, DOM rendering, or API requests) in response to changes in the signals it depends on.
26
+ 5. **`ChannelSignal<TMap>`**: A stateless, typed message bus. A single `ChannelSignal` carries multiple named message types — each with its own payload type — and routes them in **O(1)** to the right subscribers.
31
27
 
32
- 4. **`EventSignal`**: A stateless signal for dispatching one-off events that don't have a persistent value.
28
+ Additionally, two persistent state signals are available:
33
29
 
34
- 5. **`ChannelSignal`**: A stateless, typed message bus. Unlike `EventSignal` (one signal = one event type), a single `ChannelSignal` carries multiple named message types — each with its own payload type — and routes them in **O(1)** to the right subscribers.
30
+ - **`PersistentStateSignal<T>`**: Extends `StateSignal` with automatic browser `localStorage` persistence, debounced writes, and full Back/Forward Cache (BFCache) lifecycle integration.
31
+ - **`SessionStateSignal<T>`**: Extends `StateSignal` with automatic browser `sessionStorage` persistence, debounced writes, and full BFCache lifecycle integration.
35
32
 
36
33
  ---
37
34
 
38
- ## Getting Started: A Practical Example
35
+ ## API & Usage Examples
39
36
 
40
- Let's build a simple reactive system to see how the different signal types work together.
37
+ ### 1. StateSignal
41
38
 
42
- ### 1. Install
39
+ `StateSignal` represents a piece of mutable state. It always has a value, and new subscribers immediately receive the current value by default.
43
40
 
44
- First, ensure you have the package installed:
41
+ ```typescript
42
+ import {StateSignal, createStateSignal} from '@alwatr/signal';
45
43
 
46
- ```bash
47
- npm i @alwatr/signal
48
- ```
44
+ // Creation via factory function (preferred) or new class instance:
45
+ const counter = createStateSignal<number>({
46
+ name: 'app-counter',
47
+ initialValue: 0,
48
+ });
49
49
 
50
- ### 2. Create State Signals
50
+ // Get the current value
51
+ console.log(counter.get()); // Outputs: 0
51
52
 
52
- `StateSignal` is where your application's state lives. Let's create signals for a user's name and a counter.
53
+ // Subscribe to changes
54
+ const subscription = counter.subscribe((value) => {
55
+ console.log(`Counter updated to: ${value}`);
56
+ });
53
57
 
54
- ```typescript
55
- import {StateSignal} from '@alwatr/signal';
58
+ // Update value
59
+ counter.set(1); // Prints: "Counter updated to: 1"
56
60
 
57
- // A signal to hold the user's first name.
58
- const firstName = new StateSignal<string>({
59
- name: 'user-firstName',
60
- initialValue: 'John',
61
- });
61
+ // Update based on the previous value (functional state update)
62
+ counter.update((current) => current + 1); // Prints: "Counter updated to: 2"
62
63
 
63
- // A signal to hold a simple counter.
64
- const counter = new StateSignal<number>({
65
- name: 'app-counter',
66
- initialValue: 0,
67
- });
64
+ // Unsubscribe when no longer needed
65
+ subscription.unsubscribe();
68
66
  ```
69
67
 
70
- ### 3. Create a Computed Signal
68
+ ---
69
+
70
+ ### 2. EventSignal
71
71
 
72
- A `ComputedSignal` combines other signals into a new, read-only value. Let's create a `fullName` signal that automatically updates when `firstName` changes.
72
+ `EventSignal` is stateless and is used for dispatching transient events. Subscribers only get notified of future emissions.
73
73
 
74
74
  ```typescript
75
- import {ComputedSignal} from '@alwatr/signal';
75
+ import {createEventSignal} from '@alwatr/signal';
76
+
77
+ const clickSignal = createEventSignal<{x: number; y: number}>({
78
+ name: 'user-click',
79
+ });
76
80
 
77
- const fullName = new ComputedSignal<string>({
78
- name: 'user-fullName',
79
- deps: [firstName], // This computed signal depends on firstName.
80
- get: () => `User: ${firstName.get()}`,
81
+ // Subscribe to future click events
82
+ clickSignal.subscribe((pos) => {
83
+ console.log(`Clicked at coordinates: ${pos.x}, ${pos.y}`);
81
84
  });
82
85
 
83
- console.log(fullName.get()); // Outputs: "User: John"
86
+ // Dispatch event (runs asynchronously on a microtask)
87
+ clickSignal.dispatch({x: 150, y: 300});
84
88
  ```
85
89
 
86
- ### 4. Create an Effect Signal
90
+ ---
91
+
92
+ ### 3. ComputedSignal
87
93
 
88
- An `EffectSignal` runs a side effect whenever one of its dependencies changes. This is perfect for logging, updating the DOM, or making network requests.
94
+ `ComputedSignal` reactively calculates and memoizes a value based on upstream dependency signals. It automatically recalculates on changes and must be destroyed when no longer needed to prevent memory leaks.
89
95
 
90
96
  ```typescript
91
- import {EffectSignal} from '@alwatr/signal';
97
+ import {createStateSignal, createComputedSignal} from '@alwatr/signal';
92
98
 
93
- const loggerEffect = new EffectSignal({
94
- deps: [fullName, counter], // This effect depends on fullName and counter.
95
- run: () => {
96
- console.log(`${fullName.get()} has clicked ${counter.get()} times.`);
97
- },
99
+ const firstName = createStateSignal<string>({name: 'first-name', initialValue: 'John'});
100
+ const lastName = createStateSignal<string>({name: 'last-name', initialValue: 'Doe'});
101
+
102
+ const fullName = createComputedSignal<string>({
103
+ name: 'full-name',
104
+ deps: [firstName, lastName],
105
+ get: () => `${firstName.get()} ${lastName.get()}`,
98
106
  });
107
+
108
+ console.log(fullName.get()); // "John Doe"
109
+
110
+ // Triggers asynchronous evaluation batching (macrotask)
111
+ firstName.set('Jane');
112
+ // Later in the next macrotask tick:
113
+ console.log(fullName.get()); // "Jane Doe"
114
+
115
+ // Always destroy when done
116
+ fullName.destroy();
99
117
  ```
100
118
 
101
- ### 5. Putting It All Together
119
+ ---
120
+
121
+ ### 4. EffectSignal
102
122
 
103
- Now, let's see the magic happen. When we update a `StateSignal`, the changes automatically propagate through the system.
123
+ `EffectSignal` runs side-effects in response to dependency changes. It batches changes and runs in a separate macrotask.
104
124
 
105
125
  ```typescript
106
- // Subscribe to changes for demonstration
107
- fullName.subscribe((newFullName) => {
108
- console.log(`Full name signal updated to: ${newFullName}`);
109
- });
126
+ import {createStateSignal, createEffect} from '@alwatr/signal';
110
127
 
111
- // Let's change the first name.
112
- firstName.set('Jane');
113
- // This will trigger:
114
- // 1. `fullName` to recalculate its value.
115
- // 2. The `fullName.subscribe` callback to run.
116
- // 3. The `loggerEffect` to run.
117
-
118
- // Let's increment the counter.
119
- counter.set(1);
120
- // This will trigger:
121
- // 1. The `loggerEffect` to run again.
122
- ```
128
+ const count = createStateSignal<number>({name: 'count', initialValue: 0});
129
+
130
+ const logEffect = createEffect({
131
+ name: 'log-effect',
132
+ deps: [count],
133
+ run: () => {
134
+ console.log(`The count changed to: ${count.get()}`);
135
+ },
136
+ runImmediately: true, // Run the effect once on creation
137
+ });
123
138
 
124
- The output would be:
139
+ count.set(5);
125
140
 
126
- ```
127
- User: John
128
- Full name signal updated to: User: Jane
129
- User: Jane has clicked 0 times.
130
- User: Jane has clicked 1 times.
141
+ // Always destroy to clean up dependency subscriptions
142
+ logEffect.destroy();
131
143
  ```
132
144
 
133
145
  ---
134
146
 
135
- ## ChannelSignal: A Typed Message Bus
147
+ ### 5. ChannelSignal: A Typed Message Bus
136
148
 
137
- ### Why ChannelSignal?
149
+ #### Why ChannelSignal?
138
150
 
139
151
  In real-world applications, you often need to dispatch many different types of events or messages — for example, `'open-drawer'`, `'close-drawer'`, `'show-toast'`, `'navigate'`, etc. You could create a separate `EventSignal` for each one, but that quickly becomes unwieldy:
140
152
 
@@ -148,16 +160,16 @@ const showToastSignal = new EventSignal<{message: string; type: 'info' | 'error'
148
160
 
149
161
  **`ChannelSignal` solves this problem.** It's a single signal that acts as a **typed message bus** — one channel, many named message types. Think of it as a Go-style channel or a pub/sub topic with full TypeScript type safety.
150
162
 
151
- ### Architecture: O(1) Routing
163
+ #### Architecture: O(1) Routing
152
164
 
153
165
  Internally, `ChannelSignal` uses a `Map<name, Set<handler>>` to route messages. When you dispatch a message with name `'A'`, only the handlers registered for `'A'` are invoked — **O(1) lookup**, regardless of how many other names are subscribed. This is a critical performance optimization for applications with hundreds or thousands of directives/components listening to different actions.
154
166
 
155
- ### Creating a ChannelSignal
167
+ #### Creating a ChannelSignal
156
168
 
157
169
  First, define a **message map** — a TypeScript interface that maps every valid message name to its payload type:
158
170
 
159
171
  ```typescript
160
- import {ChannelSignal} from '@alwatr/signal';
172
+ import {createChannelSignal} from '@alwatr/signal';
161
173
 
162
174
  // Define the message map for your application
163
175
  interface AppMessages {
@@ -168,34 +180,33 @@ interface AppMessages {
168
180
  }
169
181
 
170
182
  // Create the channel
171
- const appChannel = new ChannelSignal<AppMessages>({name: 'app-channel'});
183
+ const appChannel = createChannelSignal<AppMessages>({name: 'app-channel'});
172
184
  ```
173
185
 
174
- ### Subscribing to Named Messages
186
+ #### Subscribing to Named Messages
175
187
 
176
188
  Use `.on(name, handler)` to subscribe to a specific message. The handler receives the **payload directly** (not the full `{name, payload}` envelope) — since the name is already known at subscription time, passing it again would be redundant.
177
189
 
178
190
  ```typescript
179
191
  // Subscribe to 'open-drawer' messages
180
192
  appChannel.on('open-drawer', (payload) => {
181
- console.log(`Opening drawer: ${payload!.panel}`);
182
- // TypeScript knows payload is {panel: string} | undefined
193
+ console.log(`Opening drawer: ${payload.panel}`);
194
+ // TypeScript knows payload is {panel: string}
183
195
  });
184
196
 
185
197
  // Subscribe to 'show-toast' messages
186
198
  appChannel.on('show-toast', (payload) => {
187
- toast.show(payload!.message, payload!.type);
188
- // TypeScript knows payload is {message: string; type: 'info' | 'error'} | undefined
199
+ toast.show(payload.message, payload.type);
200
+ // TypeScript knows payload is {message: string; type: 'info' | 'error'}
189
201
  });
190
202
 
191
203
  // Subscribe to 'close-drawer' (no payload)
192
- appChannel.on('close-drawer', (payload) => {
204
+ appChannel.on('close-drawer', () => {
193
205
  console.log('Closing drawer');
194
- // TypeScript knows payload is void | undefined
195
206
  });
196
207
  ```
197
208
 
198
- ### Dispatching Messages
209
+ #### Dispatching Messages
199
210
 
200
211
  Use `.dispatch(name, payload)` to send a message. TypeScript enforces that the payload matches the type declared for that name in the message map.
201
212
 
@@ -213,20 +224,20 @@ appChannel.dispatch('show-toast', {message: 'Hi'}); // Error: missing 'type'
213
224
  appChannel.dispatch('unknown-action'); // Error: 'unknown-action' is not in AppMessages
214
225
  ```
215
226
 
216
- ### Unsubscribing
227
+ #### Unsubscribing
217
228
 
218
229
  Just like other signals, `.on()` returns a `SubscribeResult` with an `unsubscribe()` method:
219
230
 
220
231
  ```typescript
221
232
  const sub = appChannel.on('navigate', (payload) => {
222
- router.push(payload!.path);
233
+ router.push(payload.path);
223
234
  });
224
235
 
225
236
  // Later, when the component is destroyed:
226
237
  sub.unsubscribe();
227
238
  ```
228
239
 
229
- ### One-Time Subscriptions
240
+ #### One-Time Subscriptions
230
241
 
231
242
  Use the `once` option to automatically unsubscribe after the first message:
232
243
 
@@ -240,7 +251,7 @@ appChannel.on(
240
251
  );
241
252
  ```
242
253
 
243
- ### Raw Stream Subscription (for Logging/Middleware)
254
+ #### Raw Stream Subscription (for Logging/Middleware)
244
255
 
245
256
  If you need to observe **all** messages regardless of name — for example, for logging, analytics, or middleware — use `.subscribe()` instead of `.on()`. This receives the full `{name, payload}` envelope:
246
257
 
@@ -253,7 +264,7 @@ appChannel.subscribe((msg) => {
253
264
 
254
265
  **Important:** `.subscribe()` is **not** filtered by name — it receives every message. For normal use cases, prefer `.on(name, handler)` to keep subscriptions focused and performant.
255
266
 
256
- ### Use Cases
267
+ #### Use Cases
257
268
 
258
269
  `ChannelSignal` is ideal for:
259
270
 
@@ -262,14 +273,14 @@ appChannel.subscribe((msg) => {
262
273
  - **Command dispatching** in CQRS-style systems
263
274
  - **Pub/sub messaging** where you have many distinct message types but want a single, centralized channel
264
275
 
265
- ### Example: A Complete Action System
276
+ #### Example: A Complete Action System
266
277
 
267
278
  > **Note:** `@alwatr/action` is a higher-level package built on top of `ChannelSignal`. It adds declarative HTML attribute support (`on-action="click->add-to-cart:42"`), modifier chaining, payload resolvers, and DOM lifecycle management. For production use, prefer `@alwatr/action` over wiring `ChannelSignal` manually.
268
279
 
269
280
  The example below shows what `@alwatr/action` does internally — and how you can use `ChannelSignal` directly when you need a pure-code action bus without DOM integration:
270
281
 
271
282
  ```typescript
272
- import {ChannelSignal} from '@alwatr/signal';
283
+ import {createChannelSignal} from '@alwatr/signal';
273
284
 
274
285
  // Define all app actions and their payload types
275
286
  interface AppActions {
@@ -281,19 +292,19 @@ interface AppActions {
281
292
  }
282
293
 
283
294
  // One channel for the entire action layer
284
- const actionChannel = new ChannelSignal<AppActions>({name: 'app-actions'});
295
+ const actionChannel = createChannelSignal<AppActions>({name: 'app-actions'});
285
296
 
286
297
  // Business logic subscribes — O(1) routing, no cross-action interference
287
298
  actionChannel.on('user-login', (payload) => {
288
- authService.login(payload!.username);
299
+ authService.login(payload.username);
289
300
  });
290
301
 
291
302
  actionChannel.on('cart-add-item', (payload) => {
292
- cartService.addItem(payload!.productId, payload!.quantity);
303
+ cartService.addItem(payload.productId, payload.quantity);
293
304
  });
294
305
 
295
306
  actionChannel.on('navigate', (payload) => {
296
- router.push(payload!.path);
307
+ router.push(payload.path);
297
308
  });
298
309
 
299
310
  // UI dispatches actions
@@ -308,577 +319,160 @@ addToCartButton.addEventListener('click', () => {
308
319
 
309
320
  ---
310
321
 
311
- ## Advanced Topics
322
+ ### 6. Persistent & Session State Signals
312
323
 
313
- ### Lifecycle Management and Memory Leaks
314
-
315
- Signals that depend on other signals (like `ComputedSignal` and `EffectSignal`) create subscriptions internally. If you don't clean these up, they can lead to memory leaks.
316
-
317
- **Always call `destroy()` on `ComputedSignal` and `EffectSignal` when they are no longer needed.**
324
+ State signals backed by web storage with built-in write debouncing (default 1000ms) and page/BFcache lifecycle listeners.
318
325
 
319
326
  ```typescript
320
- // Create a computed signal
321
- const isEven = new ComputedSignal({
322
- deps: [counter],
323
- get: () => counter.get() % 2 === 0,
324
- });
325
-
326
- // ... use it for a while ...
327
-
328
- // When the component/logic using it is about to be removed:
329
- isEven.destroy();
330
- ```
331
-
332
- Calling `destroy()` unsubscribes the signal from all its dependencies, allowing it to be safely garbage collected.
333
-
334
- ### Asynchronous Notifications
335
-
336
- Alwatr Signal uses a predictable asynchronous model for notifications:
337
-
338
- - **`StateSignal` and `EventSignal`** schedule notifications on the **microtask** queue (`Promise.resolve().then(...)`). This ensures that multiple synchronous `set()` calls within the same event loop tick are batched, and listeners are notified shortly after, but not immediately.
339
- - **`ComputedSignal` and `EffectSignal`** schedule their recalculations/runs on the **macrotask** queue (e.g., `setTimeout(..., 0)`). This is a crucial optimization. If multiple dependencies change in the same event loop, the computed signal will only recalculate _once_ per tick, avoiding redundant work.
340
-
341
- ### Subscription Options
342
-
343
- The `subscribe` method accepts an optional second argument to customize its behavior:
344
-
345
- - `once: true`: The listener is called only once and then automatically removed.
346
- - `priority: true`: The listener is moved to the front of the queue and is executed before other listeners.
347
- - `receivePrevious: false` (For `StateSignal` only): Prevents the listener from being called immediately with the current value upon subscription.
348
-
349
- ## API Overview
350
-
351
- ### `StateSignal<T>`
352
-
353
- - **`constructor(config)`**: Creates a new state signal.
354
- - `config.name`: `string`
355
- - `config.initialValue`: `T`
356
- - **`.get()`**: `T` - Gets the current value.
357
- - **`.set(newValue: T)`**: Sets a new value and notifies listeners.
358
-
359
- ### `ComputedSignal<T>`
360
-
361
- - **`constructor(config)`**: Creates a new computed signal.
362
- - `config.name`: `string`
363
- - `config.deps`: `IReadonlySignal<unknown>[]` - Array of dependency signals.
364
- - `config.get`: `() => T` - The function to compute the value.
365
- - **`.get()`**: `T` - Gets the current (memoized) value.
366
- - **`.destroy()`**: Cleans up the signal's subscriptions. **(Important!)**
367
-
368
- ### `EffectSignal`
369
-
370
- - **`constructor(config)`**: Creates a new effect signal.
371
- - `config.deps`: `IReadonlySignal<unknown>[]` - Array of dependency signals.
372
- - `config.run`: `() => void | Promise<void>` - The side effect function.
373
- - `config.runImmediately`: `boolean` (optional) - Whether to run the effect on creation.
374
- - **`.destroy()`**: Cleans up the signal's subscriptions. **(Important!)**
375
-
376
- ### `EventSignal<T>`
327
+ import {createPersistentStateSignal, createSessionStateSignal} from '@alwatr/signal';
377
328
 
378
- - **`constructor(config)`**: Creates a new event signal.
379
- - `config.name`: `string`
380
- - **`.dispatch(payload: T)`**: Dispatches an event to all listeners.
381
-
382
- ### `PersistentStateSignal<T>`
383
-
384
- - **`constructor(config)`**: Creates a state signal that persists in `localStorage`.
385
- - `config.name`: `string`
386
- - `config.initialValue`: `T`
387
- - config.schemaVersion: number
388
- - `config.parse`: `(value: string) => T` (optional)
389
- - `config.stringify`: `(value: T) => string` (optional)
390
- - Has all methods of `StateSignal`.
391
- - **Reliable Save & BFCache Support**: Automatically flushes pending writes on `pagehide` and re-syncs state on `pageshow` when restoring from BFCache.
392
- - **`.remove()`**: Removes the value from storage without destroying the signal.
393
-
394
- ### `SessionStateSignal<T>`
395
-
396
- - **`constructor(config)`**: Creates a state signal that persists in `sessionStorage` (tab-scoped).
397
- - `config.name`: `string`
398
- - `config.initialValue`: `T`
399
- - `config.parse`: `(value: string) => T` (optional)
400
- - `config.stringify`: `(value: T) => string` (optional)
401
- - Has all methods of `StateSignal`.
402
- - **Reliable Save & BFCache Support**: Automatically flushes pending writes on `pagehide` and re-syncs state on `pageshow` when restoring from BFCache.
403
- - **`.remove()`**: Removes the value from storage without destroying the signal.
404
-
405
- ### `ChannelSignal<TMap>`
406
-
407
- - **`constructor(config)`**: Creates a new channel signal.
408
- - `config.name`: `string`
409
- - **`.dispatch(name, payload?)`**: Dispatches a named message. TypeScript enforces the correct payload type for each name.
410
- - **`.on(name, handler, options?)`**: Subscribes to a specific named message. The handler receives the `payload` directly (not the full envelope). Uses an internal `Map` for **O(1)** routing. Supports `once` option.
411
- - **`.subscribe(callback, options?)`**: Subscribes to the **raw message stream** — receives every `{name, payload}` envelope regardless of name. Useful for logging and middleware.
412
-
413
- ### Common Methods
414
-
415
- - **`.subscribe(callback, options?)`**: Subscribes a listener. Returns `{ unsubscribe: () => void }`.
416
- - **`.untilNext()`**: Returns a `Promise` that resolves with the next value/payload.
417
- - **`.destroy()`**: (On all but `StateSignal`) Cleans up the signal.
418
-
419
- ---
420
-
421
- ## 🌊 Part of Alwatr Flux
422
-
423
- `@alwatr/signal` is the **State Layer** of the [Alwatr Flux](https://github.com/Alwatr/alwatr/tree/next/pkg/flux) architecture — a complete Unidirectional Data Flow system for building scalable Progressive Web Applications.
424
-
425
- ```
426
- View → Action (@alwatr/action) → Controller → State (@alwatr/signal) → View
427
- ```
428
-
429
- In the Flux architecture, signals serve as the **single source of truth**. Controllers update signals after processing actions, and the View layer subscribes to signals to re-render only the affected parts of the UI — no Virtual DOM, no full-tree reconciliation.
430
-
431
- **The full Flux bundle** (`@alwatr/flux`) includes signals, actions, directives, page-ready, and storage — everything you need to build a complete reactive application from a single import.
432
-
433
- ```typescript
434
- // Use @alwatr/flux for the complete architecture
435
- import {createStateSignal, onAction, setupActionDelegation} from '@alwatr/flux';
436
-
437
- // Or use @alwatr/signal standalone for just the reactive primitives
438
- import {createStateSignal, createComputedSignal} from '@alwatr/signal';
439
- ```
440
-
441
- → [View the complete Flux documentation](https://github.com/Alwatr/alwatr/tree/next/pkg/flux)
442
-
443
- ---
444
-
445
- ## Sponsors
446
-
447
- The following companies, organizations, and individuals support flux ongoing maintenance and development. Become a Sponsor to get your logo on our README and website.
448
-
449
- ## Contributing
450
-
451
- Contributions are welcome! Please read our [contribution guidelines](https://github.com/Alwatr/.github/blob/next/CONTRIBUTING.md) before submitting a pull request.
452
-
453
- ---
454
-
455
- <br>
456
- <br>
457
- <br>
458
-
459
- # Alwatr Signal (راهنمای فارسی)
460
-
461
- [](https://www.google.com/search?q=alwatr+flux)
462
- [](https://www.google.com/search?q=alwatr+signal)
463
- [](https://www.google.com/search?q=alwatr)
464
- [](https://www.npmjs.com/package/%40alwatr/flux)
465
- [](https://www.npmjs.com/package/%40alwatr/signal)
466
-
467
- کتابخانه Alwatr Signal یک ابزار قدرتمند، سبک و مدرن برای برنامه‌نویسی واکنشی (Reactive Programming) است. این کتابخانه با الگوبرداری از بهترین مفاهیم بزرگترین کتابخانه‌های واکنشی طراحی شده، اما مهندسی آن به گونه‌ای است که از تمام آن‌ها سریع‌تر و کارآمدتر باشد. این کتابخانه روشی استوار و زیبا برای مدیریت وضعیت برنامه از طریق سیگنال‌ها ارائه می‌دهد و واکنش‌پذیری دقیق (fine-grained reactivity)، پیش‌بینی‌پذیری و عملکرد عالی را به ارمغان می‌آورد.
468
-
469
- طراحی آن به گونه‌ای است که یادگیری آن ساده باشد، اما در عین حال قادر به مدیریت سناریوهای پیچیده مدیریت وضعیت نیز باشد.
470
-
471
- ## ویژگی‌ها
472
-
473
- - **ایمنی نوع (Type-Safe)**: به طور کامل با TypeScript پیاده‌سازی شده تا کدی قوی و ایمن از نظر نوع داشته باشید.
474
- - **سبک**: حجم بسیار کم و بدون هیچ وابستگی (dependency) خارجی.
475
- - **عملکرد بالا**: تشخیص هوشمند تغییرات و به‌روزرسانی‌های دسته‌ای از محاسبات و رندرهای غیرضروری جلوگیری می‌کند.
476
- - **پیش‌بینی‌پذیر**: نوتیفیکیشن‌های ناهمزمان (asynchronous) و غیرمسدودکننده (non-blocking) جریان داده‌ای سازگار و قابل فهم را تضمین می‌کنند.
477
- - **مدیریت چرخه حیات (Lifecycle)**: متدهای داخلی `destroy()` برای پاک‌سازی آسان و جلوگیری از نشت حافظه (memory leak).
478
- - **اشکال‌زدایی آسان**: شناسه‌های منحصر به فرد (`name`) برای هر سیگنال، لاگ‌گیری و ردیابی را بسیار ساده می‌کند.
479
-
480
- ## مفاهیم اصلی
481
-
482
- سیگنال‌ها بلوک‌های سازنده اصلی در Alwatr Signal هستند. آن‌ها اشیاء خاصی هستند که یک مقدار را نگه می‌دارند و می‌توانند مصرف‌کنندگان علاقه‌مند را هنگام تغییر آن مقدار مطلع کنند. سه نوع اصلی سیگنال وجود دارد:
483
-
484
- 1. **`StateSignal`**: پایه و اساس واکنش‌پذیری. این سیگنال یک مقدار قابل تغییر را نگه می‌دارد. وقتی شما مقدار جدیدی را `set()` می‌کنید، تمام وابستگان خود را مطلع می‌سازد.
485
- 2. **`ComputedSignal`**: یک سیگنال فقط-خواندنی (read-only) که مقدار خود را از سیگنال‌های دیگر استخراج می‌کند. این سیگنال به طور خودکار با تغییر وابستگی‌هایش به‌روز می‌شود. نتیجه کش (memoized) می‌شود، بنابراین محاسبات فقط در صورت نیاز انجام می‌شود.
486
- 3. **`EffectSignal`**: پلی به "دنیای بیرون". این سیگنال یک اثر جانبی (side effect) مانند لاگ‌گیری یا رندر کردن را در پاسخ به تغییرات سیگنال‌هایی که به آن‌ها وابسته است، اجرا می‌کند.
487
-
488
- یک نوع چهارم نیز برای رویدادهای بدون حالت وجود دارد:
489
-
490
- 4. **`EventSignal`**: یک سیگنال بدون حالت برای ارسال رویدادهای یک‌باره که مقدار پایداری ندارند.
491
-
492
- و یک نوع پنجم برای مسیریابی پیام‌های چندگانه:
493
-
494
- 5. **`ChannelSignal`**: یک Message Bus تایپ‌شده و بدون حالت. برخلاف `EventSignal` (یک سیگنال = یک نوع رویداد)، یک `ChannelSignal` واحد چندین نوع پیام با نام‌های مختلف را حمل می‌کند — هر کدام با نوع payload مخصوص خودشان — و آن‌ها را با سرعت **O(1)** به subscriber‌های مناسب هدایت می‌کند.
495
-
496
- ---
497
-
498
- ## شروع به کار: یک مثال عملی
499
-
500
- بیایید یک سیستم واکنشی ساده بسازیم تا ببینیم انواع مختلف سیگنال‌ها چگونه با هم کار می‌کنند.
501
-
502
- ### ۱. نصب
503
-
504
- ابتدا، اطمینان حاصل کنید که بسته را نصب کرده‌اید:
505
-
506
- ```bash
507
- npm i @alwatr/signal
508
- ```
509
-
510
- ### ۲. ایجاد `StateSignal`
511
-
512
- `StateSignal` جایی است که وضعیت برنامه شما زندگی می‌کند. بیایید سیگنال‌هایی برای نام یک کاربر و یک شمارنده ایجاد کنیم.
513
-
514
- ```typescript
515
- import {StateSignal} from '@alwatr/signal';
516
-
517
- // سیگنالی برای نگهداری نام کوچک کاربر
518
- const firstName = new StateSignal<string>({
519
- name: 'user-firstName',
520
- initialValue: 'John',
521
- });
522
-
523
- // سیگنالی برای نگهداری یک شمارنده ساده
524
- const counter = new StateSignal<number>({
525
- name: 'app-counter',
526
- initialValue: 0,
329
+ // PersistentStateSignal saves state to localStorage
330
+ const theme = createPersistentStateSignal<'light' | 'dark'>({
331
+ name: 'app-theme',
332
+ initialValue: 'light',
333
+ storageKey: 'theme-preference', // optional storage key
334
+ saveDebounceDelay: 500, // debounce time in ms
527
335
  });
528
- ```
529
336
 
530
- ### ۳. ایجاد `ComputedSignal`
337
+ theme.set('dark'); // Automatically schedules storage save
531
338
 
532
- یک `ComputedSignal` سیگنال‌های دیگر را ترکیب کرده و یک مقدار جدید و فقط-خواندنی ایجاد می‌کند. بیایید یک سیگنال `fullName` بسازیم که با تغییر `firstName` به طور خودکار به‌روز شود.
533
-
534
- ```typescript
535
- import {ComputedSignal} from '@alwatr/signal';
536
-
537
- const fullName = new ComputedSignal<string>({
538
- name: 'user-fullName',
539
- deps: [firstName], // این سیگنال محاسباتی به firstName وابسته است
540
- get: () => `User: ${firstName.get()}`,
541
- });
542
-
543
- console.log(fullName.get()); // خروجی: "User: John"
544
- ```
545
-
546
- ### ۴. ایجاد `EffectSignal`
547
-
548
- یک `EffectSignal` هر زمان که یکی از وابستگی‌هایش تغییر کند، یک اثر جانبی اجرا می‌کند. این برای لاگ‌گیری، به‌روزرسانی DOM یا ارسال درخواست‌های شبکه عالی است.
549
-
550
- ```typescript
551
- import {EffectSignal} from '@alwatr/signal';
552
-
553
- const loggerEffect = new EffectSignal({
554
- deps: [fullName, counter], // این افکت به fullName و counter وابسته است
555
- run: () => {
556
- console.log(`${fullName.get()} has clicked ${counter.get()} times.`);
557
- },
558
- });
559
- ```
560
-
561
- ### ۵. کنار هم قرار دادن همه چیز
562
-
563
- حالا، بیایید جادو را ببینیم. وقتی ما یک `StateSignal` را به‌روز می‌کنیم، تغییرات به طور خودکار در سراسر سیستم پخش می‌شوند.
564
-
565
- ```typescript
566
- // برای نمایش، در تغییرات مشترک می‌شویم
567
- fullName.subscribe((newFullName) => {
568
- console.log(`Full name signal updated to: ${newFullName}`);
339
+ // SessionStateSignal saves state to sessionStorage
340
+ const wizardStep = createSessionStateSignal<number>({
341
+ name: 'wizard-step',
342
+ initialValue: 1,
569
343
  });
570
344
 
571
- // بیایید نام کوچک را تغییر دهیم
572
- firstName.set('Jane');
573
- // این کار باعث می‌شود:
574
- // ۱. `fullName` مقدار خود را دوباره محاسبه کند.
575
- // ۲. کال‌بک `fullName.subscribe` اجرا شود.
576
- // ۳. `loggerEffect` اجرا شود.
577
-
578
- // بیایید شمارنده را افزایش دهیم
579
- counter.set(1);
580
- // این کار باعث می‌شود:
581
- // ۱. `loggerEffect` دوباره اجرا شود.
582
- ```
583
-
584
- خروجی به این صورت خواهد بود:
345
+ // Remove persisted data from storage without destroying the signal:
346
+ wizardStep.remove();
585
347
 
586
- ```
587
- User: John
588
- Full name signal updated to: User: Jane
589
- User: Jane has clicked 0 times.
590
- User: Jane has clicked 1 times.
348
+ // Clean up when unmounting
349
+ theme.destroy();
350
+ wizardStep.destroy();
591
351
  ```
592
352
 
593
353
  ---
594
354
 
595
- ## ChannelSignal: یک Message Bus تایپ‌شده
355
+ ## Operators
596
356
 
597
- ### چرا ChannelSignal؟
357
+ The package includes utility operators to transform and debounce signals:
598
358
 
599
- در برنامه‌های واقعی، اغلب نیاز دارید انواع مختلفی از رویدادها یا پیام‌ها را dispatch کنید — مثلاً `'open-drawer'`، `'close-drawer'`، `'show-toast'`، `'navigate'` و غیره. می‌توانید برای هر کدام یک `EventSignal` جداگانه بسازید، اما این رویکرد به سرعت دست‌وپاگیر می‌شود:
359
+ ### 1. `createDebouncedSignal`
600
360
 
601
- ```typescript
602
- // ❌ پرحجم و سخت برای مدیریت
603
- const openDrawerSignal = new EventSignal<{panel: string}>({name: 'open-drawer'});
604
- const closeDrawerSignal = new EventSignal({name: 'close-drawer'});
605
- const showToastSignal = new EventSignal<{message: string; type: 'info' | 'error'}>({name: 'show-toast'});
606
- // ... و به همین ترتیب برای هر action در برنامه
607
- ```
608
-
609
- **`ChannelSignal` این مشکل را حل می‌کند.** یک سیگنال واحد است که به عنوان یک **Message Bus تایپ‌شده** عمل می‌کند — یک کانال، انواع پیام‌های مختلف. مثل یک Go-style channel یا یک pub/sub topic با ایمنی کامل TypeScript.
610
-
611
- ### معماری: مسیریابی O(1)
612
-
613
- در داخل، `ChannelSignal` از یک `Map<name, Set<handler>>` برای مسیریابی پیام‌ها استفاده می‌کند. وقتی پیامی با نام `'A'` dispatch می‌شود، فقط handler‌هایی که برای `'A'` ثبت شده‌اند فراخوانی می‌شوند — **جستجوی O(1)**، صرف‌نظر از اینکه چه تعداد نام دیگری subscribe شده باشند. این یک بهینه‌سازی حیاتی برای برنامه‌هایی است که صدها یا هزاران directive/component دارند که به action‌های مختلف گوش می‌دهند.
614
-
615
- ### ساخت یک ChannelSignal
616
-
617
- ابتدا یک **message map** تعریف کنید — یک interface در TypeScript که هر نام پیام معتبر را به نوع payload آن نگاشت می‌کند:
361
+ Creates a new computed signal that debounces updates from a source signal.
618
362
 
619
363
  ```typescript
620
- import {ChannelSignal} from '@alwatr/signal';
364
+ import {createStateSignal, createDebouncedSignal} from '@alwatr/signal';
621
365
 
622
- // تعریف message map برای برنامه
623
- interface AppMessages {
624
- 'open-drawer': {panel: string};
625
- 'close-drawer': void; // بدون payload
626
- 'show-toast': {message: string; type: 'info' | 'error'};
627
- 'navigate': {path: string};
628
- }
366
+ const searchInput = createStateSignal<string>({name: 'search-input', initialValue: ''});
629
367
 
630
- // ساخت channel
631
- const appChannel = new ChannelSignal<AppMessages>({name: 'app-channel'});
632
- ```
633
-
634
- ### Subscribe به پیام‌های نام‌دار
635
-
636
- از `.on(name, handler)` برای subscribe به یک پیام خاص استفاده کنید. handler مستقیماً **payload** را دریافت می‌کند (نه envelope کامل `{name, payload}`) — چون نام در زمان subscribe مشخص است، ارسال مجدد آن اضافی خواهد بود.
637
-
638
- ```typescript
639
- // Subscribe به پیام‌های 'open-drawer'
640
- appChannel.on('open-drawer', (payload) => {
641
- console.log(`Opening drawer: ${payload!.panel}`);
642
- // TypeScript می‌داند payload از نوع {panel: string} | undefined است
643
- });
644
-
645
- // Subscribe به پیام‌های 'show-toast'
646
- appChannel.on('show-toast', (payload) => {
647
- toast.show(payload!.message, payload!.type);
648
- // TypeScript می‌داند payload از نوع {message: string; type: 'info' | 'error'} | undefined است
368
+ // Debounces input updates by 300ms
369
+ const debouncedSearch = createDebouncedSignal(searchInput, {
370
+ delay: 300,
649
371
  });
650
372
 
651
- // Subscribe به 'close-drawer' (بدون payload)
652
- appChannel.on('close-drawer', () => {
653
- console.log('Closing drawer');
654
- });
373
+ // Must be destroyed to clean up internal debouncer timers and subscriptions
374
+ debouncedSearch.destroy();
655
375
  ```
656
376
 
657
- ### Dispatch پیام‌ها
377
+ ### 2. `createFilteredSignal`
658
378
 
659
- از `.dispatch(name, payload)` برای ارسال پیام استفاده کنید. TypeScript اعمال می‌کند که payload با نوع تعریف‌شده برای آن نام در message map مطابقت داشته باشد.
379
+ Creates a new computed signal that only emits values satisfying a predicate function.
660
380
 
661
381
  ```typescript
662
- // Dispatch با payload
663
- appChannel.dispatch('open-drawer', {panel: 'settings'}); // ✅ Type-safe
664
- appChannel.dispatch('show-toast', {message: 'ذخیره شد!', type: 'info'}); // ✅
382
+ import {createStateSignal, createFilteredSignal} from '@alwatr/signal';
665
383
 
666
- // Dispatch بدون payload
667
- appChannel.dispatch('close-drawer'); //
668
-
669
- // ❌ خطاهای TypeScript:
670
- appChannel.dispatch('open-drawer', {panel: 123}); // خطا: panel باید string باشد
671
- appChannel.dispatch('show-toast', {message: 'سلام'}); // خطا: 'type' وجود ندارد
672
- appChannel.dispatch('unknown-action'); // خطا: 'unknown-action' در AppMessages نیست
673
- ```
674
-
675
- ### Unsubscribe کردن
676
-
677
- مثل سایر سیگنال‌ها، `.on()` یک `SubscribeResult` با متد `unsubscribe()` برمی‌گرداند:
678
-
679
- ```typescript
680
- const sub = appChannel.on('navigate', (payload) => {
681
- router.push(payload!.path);
682
- });
384
+ const numberSignal = createStateSignal<number>({name: 'number', initialValue: 0});
385
+ const evenNumberSignal = createFilteredSignal(numberSignal, (n) => n % 2 === 0);
683
386
 
684
- // بعداً، وقتی کامپوننت destroy می‌شود:
685
- sub.unsubscribe();
387
+ evenNumberSignal.destroy();
686
388
  ```
687
389
 
688
- ### Subscribe یک‌باره
390
+ ### 3. `createMappedSignal`
689
391
 
690
- از گزینه `once` برای unsubscribe خودکار بعد از اولین پیام استفاده کنید:
392
+ Transforms values from a source signal using a projection function.
691
393
 
692
394
  ```typescript
693
- appChannel.on(
694
- 'app-ready',
695
- () => {
696
- console.log('برنامه آماده است!');
697
- },
698
- {once: true},
699
- );
700
- ```
701
-
702
- ### Subscribe به جریان خام (برای لاگ‌گیری/Middleware)
395
+ import {createStateSignal, createMappedSignal} from '@alwatr/signal';
703
396
 
704
- اگر نیاز دارید **همه** پیام‌ها را صرف‌نظر از نام مشاهده کنید — مثلاً برای لاگ‌گیری، analytics یا middleware — از `.subscribe()` به جای `.on()` استفاده کنید. این متد envelope کامل `{name, payload}` را دریافت می‌کند:
397
+ const userSignal = createStateSignal({name: 'user', initialValue: {name: 'John', age: 30}});
398
+ const userNameSignal = createMappedSignal(userSignal, (user) => user.name);
705
399
 
706
- ```typescript
707
- // لاگ کردن همه پیام‌ها برای debugging
708
- appChannel.subscribe((msg) => {
709
- console.log(`[channel] ${String(msg.name)}`, msg.payload);
710
- });
400
+ console.log(userNameSignal.get()); // John
711
401
  ```
712
402
 
713
- **نکته مهم:** `.subscribe()` بر اساس نام فیلتر **نمی‌شود** — هر پیامی را دریافت می‌کند. برای موارد عادی، `.on(name, handler)` را ترجیح دهید تا subscriptionها متمرکز و کارآمد بمانند.
714
-
715
- ### موارد استفاده
716
-
717
- `ChannelSignal` برای موارد زیر ایده‌آل است:
718
-
719
- - **لایه Action** در معماری‌های Unidirectional Data Flow (مثل `@alwatr/action`)
720
- - **Event Bus** در UI‌های مبتنی بر کامپوننت (مثلاً یک کانال رویداد سراسری برنامه)
721
- - **Command Dispatching** در سیستم‌های CQRS-style
722
- - **Pub/Sub Messaging** جایی که انواع پیام‌های مختلف دارید اما می‌خواهید یک کانال مرکزی داشته باشید
403
+ ---
723
404
 
724
- ### مثال کامل: یک سیستم Action
405
+ ## Advanced Subscription Options
725
406
 
726
- > **نکته:** پکیج `@alwatr/action` یک لایه بالاتر است که روی `ChannelSignal` ساخته شده. این پکیج پشتیبانی از attribute‌های HTML (`on-action="click->add-to-cart:42"`)، modifier chaining، payload resolver و مدیریت lifecycle DOM را اضافه می‌کند. برای استفاده در پروداکشن، `@alwatr/action` را به جای wiring مستقیم `ChannelSignal` ترجیح دهید.
407
+ When subscribing to signals, you can customize the behavior with the `SubscribeOptions` parameter:
727
408
 
728
- مثال زیر نشان می‌دهد که `@alwatr/action` در داخل چه کاری انجام می‌دهد — و چگونه می‌توانید `ChannelSignal` را مستقیماً زمانی که به یک action bus خالص بدون DOM integration نیاز دارید استفاده کنید:
409
+ - `once?: boolean`: If `true`, the listener runs once and automatically unsubscribes.
410
+ - `priority?: boolean`: If `true`, the listener is added to the front of the queue and executes before standard observers.
411
+ - `receivePrevious?: boolean`: (For `StateSignal` only, default `true`). If `false`, the listener ignores the current value and only runs on future updates.
729
412
 
730
413
  ```typescript
731
- import {ChannelSignal} from '@alwatr/signal';
732
-
733
- // تعریف همه action‌های برنامه و نوع payload آن‌ها
734
- interface AppActions {
735
- 'user-login': {username: string};
736
- 'user-logout': void;
737
- 'cart-add-item': {productId: number; quantity: number};
738
- 'cart-remove-item': {productId: number};
739
- 'navigate': {path: string};
740
- }
741
-
742
- // یک کانال برای کل لایه action
743
- const actionChannel = new ChannelSignal<AppActions>({name: 'app-actions'});
744
-
745
- // منطق تجاری subscribe می‌کند — routing با O(1)، بدون تداخل بین action‌ها
746
- actionChannel.on('user-login', (payload) => {
747
- authService.login(payload!.username);
748
- });
749
-
750
- actionChannel.on('cart-add-item', (payload) => {
751
- cartService.addItem(payload!.productId, payload!.quantity);
752
- });
753
-
754
- actionChannel.on('navigate', (payload) => {
755
- router.push(payload!.path);
756
- });
757
-
758
- // UI اقدام به dispatch action می‌کند
759
- loginButton.addEventListener('click', () => {
760
- actionChannel.dispatch('user-login', {username: 'ali'});
761
- });
762
-
763
- addToCartButton.addEventListener('click', () => {
764
- actionChannel.dispatch('cart-add-item', {productId: 42, quantity: 1});
414
+ // Subscribe with options
415
+ mySignal.subscribe((val) => console.log(val), {
416
+ once: true,
417
+ priority: true,
765
418
  });
766
419
  ```
767
420
 
768
421
  ---
769
422
 
770
- ## مباحث پیشرفته
771
-
772
- ### مدیریت چرخه حیات و نشت حافظه
423
+ ## Lifecycle Management and Memory Leaks
773
424
 
774
- سیگنال‌هایی که به سیگنال‌های دیگر وابسته‌اند (مانند `ComputedSignal` و `EffectSignal`) به صورت داخلی اشتراک (subscription) ایجاد می‌کنند. اگر این اشتراک‌ها را پاک‌سازی نکنید، می‌توانند منجر به نشت حافظه شوند.
425
+ Signals that depend on other signals (like `ComputedSignal` and `EffectSignal`) create subscriptions internally. If you don't clean these up, they can lead to memory leaks.
775
426
 
776
- **همیشه متد `destroy()` را روی `ComputedSignal` و `EffectSignal` زمانی که دیگر به آن‌ها نیازی نیست، فراخوانی کنید.**
427
+ **Always call `destroy()` on `ComputedSignal` and `EffectSignal` when they are no longer needed.**
777
428
 
778
429
  ```typescript
779
- // یک سیگنال محاسباتی ایجاد کنید
780
- const isEven = new ComputedSignal({
430
+ import {createStateSignal, createComputedSignal} from '@alwatr/signal';
431
+
432
+ const counter = createStateSignal<number>({name: 'counter', initialValue: 0});
433
+
434
+ // Create a computed signal
435
+ const isEven = createComputedSignal<boolean>({
436
+ name: 'is-even',
781
437
  deps: [counter],
782
438
  get: () => counter.get() % 2 === 0,
783
439
  });
784
440
 
785
- // ... مدتی از آن استفاده کنید ...
441
+ // ... use it for a while ...
786
442
 
787
- // زمانی که کامپوننت/منطقی که از آن استفاده می‌کند در شرف حذف شدن است:
443
+ // When the component/logic using it is about to be removed:
788
444
  isEven.destroy();
789
445
  ```
790
446
 
791
- فراخوانی `destroy()` اشتراک سیگنال را از تمام وابستگی‌هایش لغو می‌کند و به جمع‌آورنده زباله (garbage collector) اجازه می‌دهد آن را با خیال راحت پاک کند.
792
-
793
- ### نوتیفیکیشن‌های ناهمزمان (Asynchronous)
794
-
795
- Alwatr Signal از یک مدل ناهمزمان قابل پیش‌بینی برای نوتیفیکیشن‌ها استفاده می‌کند:
796
-
797
- - **`StateSignal` و `EventSignal`** نوتیفیکیشن‌ها را در صف **microtask** (`Promise.resolve().then(...)`) زمان‌بندی می‌کنند. این تضمین می‌کند که چندین فراخوانی `set()` همزمان در یک تیک حلقه رویداد (event loop) دسته‌بندی شده و شنوندگان کمی بعد، اما نه بلافاصله، مطلع می‌شوند.
798
- - **`ComputedSignal` و `EffectSignal`** محاسبات/اجراهای خود را در صف **macrotask** (مانند `setTimeout(..., 0)`) زمان‌بندی می‌کنند. این یک بهینه‌سازی حیاتی است. اگر چندین وابستگی در یک حلقه رویداد تغییر کنند، سیگنال محاسباتی فقط _یک بار_ در هر تیک دوباره محاسبه می‌شود و از کار اضافی جلوگیری می‌کند.
799
-
800
- ### گزینه‌های اشتراک (`subscribe`)
801
-
802
- متد `subscribe` یک آرگومان دوم اختیاری برای سفارشی‌سازی رفتار خود می‌پذیرد:
803
-
804
- - `once: true`: شنونده فقط یک بار فراخوانی شده و سپس به طور خودکار حذف می‌شود.
805
- - `priority: true`: شنونده به ابتدای صف منتقل شده و قبل از سایر شنوندگان اجرا می‌شود.
806
- - `receivePrevious: false` (فقط برای `StateSignal`): از فراخوانی فوری شنونده با مقدار فعلی در هنگام اشتراک جلوگیری می‌کند.
807
-
808
- ## مرور کلی API
809
-
810
- ### `StateSignal<T>`
811
-
812
- - **`constructor(config)`**: یک سیگنال وضعیت جدید ایجاد می‌کند.
813
- - `config.name`: `string`
814
- - `config.initialValue`: `T`
815
- - **`.get()`**: `T` - مقدار فعلی را دریافت می‌کند.
816
- - **`.set(newValue: T)`**: مقدار جدیدی را تنظیم کرده و شنوندگان را مطلع می‌کند.
817
-
818
- ### `ComputedSignal<T>`
819
-
820
- - **`constructor(config)`**: یک سیگنال محاسباتی جدید ایجاد می‌کند.
821
- - `config.name`: `string`
822
- - `config.deps`: `IReadonlySignal<unknown>[]` - آرایه‌ای از سیگنال‌های وابسته.
823
- - `config.get`: `() => T` - تابعی برای محاسبه مقدار.
824
- - **`.get()`**: `T` - مقدار فعلی (کش شده) را دریافت می‌کند.
825
- - **`.destroy()`**: اشتراک‌های سیگنال را پاک‌سازی می‌کند. **(مهم!)**
826
-
827
- ### `EffectSignal`
828
-
829
- - **`constructor(config)`**: یک سیگنال افکت جدید ایجاد می‌کند.
830
- - `config.deps`: `IReadonlySignal<unknown>[]` - آرایه‌ای از سیگنال‌های وابسته.
831
- - `config.run`: `() => void | Promise<void>` - تابع اثر جانبی.
832
- - `config.runImmediately`: `boolean` (اختیاری) - آیا افکت در هنگام ایجاد اجرا شود یا خیر.
833
- - **`.destroy()`**: اشتراک‌های سیگنال را پاک‌سازی می‌کند. **(مهم!)**
447
+ Calling `destroy()` unsubscribes the signal from all its dependencies, allowing it to be safely garbage collected.
834
448
 
835
- ### `EventSignal<T>`
449
+ ---
836
450
 
837
- - **`constructor(config)`**: یک سیگنال رویداد جدید ایجاد می‌کند.
838
- - `config.name`: `string`
839
- - **`.dispatch(payload: T)`**: یک رویداد را به همه شنوندگان ارسال می‌کند.
451
+ ## Asynchronous Scheduling
840
452
 
841
- ### `PersistentStateSignal<T>`
453
+ To prevent performance degradation, Alwatr Signal employs an asynchronous execution strategy:
842
454
 
843
- - **`constructor(config)`**: یک سیگنال وضعیت ایجاد می‌کند که در `localStorage` ماندگار است.
844
- - `config.name`: `string`
845
- - `config.initialValue`: `T`
846
- - config.schemaVersion: number
847
- - `config.parse`: `(value: string) => T` (اختیاری)
848
- - `config.stringify`: `(value: T) => string` (اختیاری)
849
- - دارای تمامی متدهای `StateSignal` است.
850
- - **ذخیره مطمئن و پشتیبانی از BFCache**: به طور خودکار داده‌ها را در رویداد `pagehide` ذخیره می‌کند و در صورت بازگشت از BFCache (رویداد `pageshow`)، سیگنال را دوباره با حافظه همگام‌سازی (Sync) می‌کند.
851
- - **`.remove()`**: مقدار را از حافظه (storage) بدون از بین بردن سیگنال پاک می‌کند.
455
+ - **`StateSignal` & `EventSignal`**: Notifications are pushed to the **microtask** queue (`Promise.resolve()`). This batches multiple synchronous modifications and delivers them at the end of the current task.
456
+ - **`ComputedSignal` & `EffectSignal`**: Recalculations and side-effects are scheduled as **macrotasks** (`delay.nextMacrotask`). If multiple dependency signals change in the same tick, the computed/effect signal updates once, preventing costly redundant evaluations.
852
457
 
853
- ### `SessionStateSignal<T>`
458
+ ---
854
459
 
855
- - **`constructor(config)`**: یک سیگنال وضعیت ایجاد می‌کند که در `sessionStorage` (در سطح tab) ماندگار است.
856
- - `config.name`: `string`
857
- - `config.initialValue`: `T`
858
- - `config.parse`: `(value: string) => T` (اختیاری)
859
- - `config.stringify`: `(value: T) => string` (اختیاری)
860
- - دارای تمامی متدهای `StateSignal` است.
861
- - **ذخیره مطمئن و پشتیبانی از BFCache**: به طور خودکار داده‌ها را در رویداد `pagehide` ذخیره می‌کند و در صورت بازگشت از BFCache (رویداد `pageshow`)، سیگنال را دوباره با حافظه همگام‌سازی (Sync) می‌کند.
862
- - **`.remove()`**: مقدار را از حافظه (storage) بدون از بین بردن سیگنال پاک می‌کند.
460
+ ## 🌊 Part of Alwatr Flux
863
461
 
864
- ### `ChannelSignal<TMap>`
462
+ `@alwatr/signal` serves as the **State Layer** of the **Alwatr Flux** unidirectional architecture:
865
463
 
866
- - **`constructor(config)`**: یک channel signal جدید ایجاد می‌کند.
867
- - `config.name`: `string`
868
- - **`.dispatch(name, payload?)`**: یک پیام با نام مشخص ارسال می‌کند. TypeScript نوع صحیح payload را برای هر نام اعمال می‌کند.
869
- - **`.on(name, handler, options?)`**: به یک پیام با نام مشخص subscribe می‌کند. handler مستقیماً `payload` را دریافت می‌کند (نه envelope کامل). از یک `Map` داخلی برای routing **O(1)** استفاده می‌کند. از گزینه `once` پشتیبانی می‌کند.
870
- - **`.subscribe(callback, options?)`**: به **جریان خام پیام‌ها** subscribe می‌کند — هر envelope `{name, payload}` را صرف‌نظر از نام دریافت می‌کند. برای لاگ‌گیری و middleware مفید است.
464
+ ```
465
+ View Action (@alwatr/action) → Controller → State (@alwatr/signal) → View
466
+ ```
871
467
 
872
- ### متدهای مشترک
468
+ For full UI integration, check out the `@alwatr/flux` bundle package.
873
469
 
874
- - **`.subscribe(callback, options?)`**: یک شنونده را مشترک می‌کند. `{ unsubscribe: () => void }` را برمی‌گرداند.
875
- - **`.untilNext()`**: یک `Promise` برمی‌گرداند که با مقدار/پیام بعدی resolve می‌شود.
876
- - **`.destroy()`**: (روی همه سیگنال‌ها به جز `StateSignal`) سیگنال را پاک‌سازی می‌کند.
470
+ ---
877
471
 
878
- ## حامیان (Sponsors)
472
+ ## Sponsors
879
473
 
880
- شرکت‌ها، سازمان‌ها و افراد زیر از نگهداری و توسعه مداوم flux حمایت می‌کنند. با تبدیل شدن به یک حامی، لوگوی خود را در README و وب‌سایت ما قرار دهید.
474
+ Flux and Alwatr packages are supported by our sponsors. Become a Sponsor to place your logo here.
881
475
 
882
- ## مشارکت (Contributing)
476
+ ## Contributing
883
477
 
884
- از مشارکت‌ها استقبال می‌شود! لطفاً قبل از ارسال pull request، [راهنمای مشارکت ما](https://github.com/Alwatr/.github/blob/next/CONTRIBUTING.md) را مطالعه کنید.
478
+ Contributions are welcome! Please read our [contribution guidelines](https://github.com/Alwatr/.github/blob/next/CONTRIBUTING.md) before submitting pull requests.