@alwatr/signal 9.26.0 → 9.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -603
- package/dist/core/channel-signal.d.ts +12 -53
- package/dist/core/channel-signal.d.ts.map +1 -1
- package/dist/core/computed-signal.d.ts +19 -33
- package/dist/core/computed-signal.d.ts.map +1 -1
- package/dist/core/derived-signal.d.ts +71 -0
- package/dist/core/derived-signal.d.ts.map +1 -0
- package/dist/core/effect-signal.d.ts +15 -1
- package/dist/core/effect-signal.d.ts.map +1 -1
- package/dist/core/event-signal.d.ts +11 -4
- package/dist/core/event-signal.d.ts.map +1 -1
- package/dist/core/persistent-state-signal.d.ts +21 -2
- package/dist/core/persistent-state-signal.d.ts.map +1 -1
- package/dist/core/session-state-signal.d.ts +19 -2
- package/dist/core/session-state-signal.d.ts.map +1 -1
- package/dist/core/signal-base.d.ts +58 -38
- package/dist/core/signal-base.d.ts.map +1 -1
- package/dist/core/state-signal.d.ts +33 -14
- package/dist/core/state-signal.d.ts.map +1 -1
- package/dist/creators/channel.d.ts +1 -1
- package/dist/creators/channel.d.ts.map +1 -1
- package/dist/creators/derived.d.ts +31 -0
- package/dist/creators/derived.d.ts.map +1 -0
- package/dist/main.d.ts +2 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +16 -15
- package/dist/operators/debounce.d.ts +2 -3
- package/dist/operators/debounce.d.ts.map +1 -1
- package/dist/operators/filter.d.ts +14 -13
- package/dist/operators/filter.d.ts.map +1 -1
- package/dist/type.d.ts +68 -3
- package/dist/type.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/core/channel-signal.ts +25 -68
- package/src/core/computed-signal.ts +50 -74
- package/src/core/derived-signal.ts +166 -0
- package/src/core/effect-signal.ts +23 -11
- package/src/core/event-signal.ts +14 -9
- package/src/core/persistent-state-signal.ts +21 -4
- package/src/core/session-state-signal.ts +19 -4
- package/src/core/signal-base.ts +98 -61
- package/src/core/state-signal.ts +48 -29
- package/src/creators/channel.ts +1 -2
- package/src/creators/derived.ts +34 -0
- package/src/main.ts +2 -1
- package/src/operators/debounce.ts +13 -23
- package/src/operators/filter.ts +20 -26
- package/src/type.ts +71 -3
- package/dist/operators/map.d.ts +0 -36
- package/dist/operators/map.d.ts.map +0 -1
- 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
|
|
16
|
-
- **Lightweight**:
|
|
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**:
|
|
14
|
+
- **Easy to Debug**: Scoped logger for each signal makes logging and tracing a breeze.
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
---
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
## Primitives
|
|
25
19
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
Additionally, two persistent state signals are available:
|
|
33
29
|
|
|
34
|
-
|
|
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
|
-
##
|
|
35
|
+
## API & Usage Examples
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
### 1. StateSignal
|
|
41
38
|
|
|
42
|
-
|
|
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
|
-
|
|
41
|
+
```typescript
|
|
42
|
+
import {StateSignal, createStateSignal} from '@alwatr/signal';
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
+
// Get the current value
|
|
51
|
+
console.log(counter.get()); // Outputs: 0
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
// Subscribe to changes
|
|
54
|
+
const subscription = counter.subscribe((value) => {
|
|
55
|
+
console.log(`Counter updated to: ${value}`);
|
|
56
|
+
});
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
// Update value
|
|
59
|
+
counter.set(1); // Prints: "Counter updated to: 1"
|
|
56
60
|
|
|
57
|
-
//
|
|
58
|
-
|
|
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
|
-
//
|
|
64
|
-
|
|
65
|
-
name: 'app-counter',
|
|
66
|
-
initialValue: 0,
|
|
67
|
-
});
|
|
64
|
+
// Unsubscribe when no longer needed
|
|
65
|
+
subscription.unsubscribe();
|
|
68
66
|
```
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 2. EventSignal
|
|
71
71
|
|
|
72
|
-
|
|
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 {
|
|
75
|
+
import {createEventSignal} from '@alwatr/signal';
|
|
76
|
+
|
|
77
|
+
const clickSignal = createEventSignal<{x: number; y: number}>({
|
|
78
|
+
name: 'user-click',
|
|
79
|
+
});
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
86
|
+
// Dispatch event (runs asynchronously on a microtask)
|
|
87
|
+
clickSignal.dispatch({x: 150, y: 300});
|
|
84
88
|
```
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### 3. ComputedSignal
|
|
87
93
|
|
|
88
|
-
|
|
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 {
|
|
97
|
+
import {createStateSignal, createComputedSignal} from '@alwatr/signal';
|
|
92
98
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### 4. EffectSignal
|
|
102
122
|
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
fullName.subscribe((newFullName) => {
|
|
108
|
-
console.log(`Full name signal updated to: ${newFullName}`);
|
|
109
|
-
});
|
|
126
|
+
import {createStateSignal, createEffect} from '@alwatr/signal';
|
|
110
127
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
139
|
+
count.set(5);
|
|
125
140
|
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
147
|
+
### 5. ChannelSignal: A Typed Message Bus
|
|
136
148
|
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
183
|
+
const appChannel = createChannelSignal<AppMessages>({name: 'app-channel'});
|
|
172
184
|
```
|
|
173
185
|
|
|
174
|
-
|
|
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
|
|
182
|
-
// TypeScript knows payload is {panel: string}
|
|
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
|
|
188
|
-
// TypeScript knows payload is {message: string; type: 'info' | 'error'}
|
|
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', (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
299
|
+
authService.login(payload.username);
|
|
289
300
|
});
|
|
290
301
|
|
|
291
302
|
actionChannel.on('cart-add-item', (payload) => {
|
|
292
|
-
cartService.addItem(payload
|
|
303
|
+
cartService.addItem(payload.productId, payload.quantity);
|
|
293
304
|
});
|
|
294
305
|
|
|
295
306
|
actionChannel.on('navigate', (payload) => {
|
|
296
|
-
router.push(payload
|
|
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
|
-
|
|
322
|
+
### 6. Persistent & Session State Signals
|
|
312
323
|
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
337
|
+
theme.set('dark'); // Automatically schedules storage save
|
|
531
338
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
588
|
-
|
|
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
|
-
##
|
|
355
|
+
## Operators
|
|
596
356
|
|
|
597
|
-
|
|
357
|
+
The package includes utility operators to transform and debounce signals:
|
|
598
358
|
|
|
599
|
-
|
|
359
|
+
### 1. `createDebouncedSignal`
|
|
600
360
|
|
|
601
|
-
|
|
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 {
|
|
364
|
+
import {createStateSignal, createDebouncedSignal} from '@alwatr/signal';
|
|
621
365
|
|
|
622
|
-
|
|
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
|
-
//
|
|
631
|
-
const
|
|
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
|
-
//
|
|
652
|
-
|
|
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
|
-
###
|
|
377
|
+
### 2. `createFilteredSignal`
|
|
658
378
|
|
|
659
|
-
|
|
379
|
+
Creates a new computed signal that only emits values satisfying a predicate function.
|
|
660
380
|
|
|
661
381
|
```typescript
|
|
662
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
-
|
|
685
|
-
sub.unsubscribe();
|
|
387
|
+
evenNumberSignal.destroy();
|
|
686
388
|
```
|
|
687
389
|
|
|
688
|
-
###
|
|
390
|
+
### 3. `createMappedSignal`
|
|
689
391
|
|
|
690
|
-
|
|
392
|
+
Transforms values from a source signal using a projection function.
|
|
691
393
|
|
|
692
394
|
```typescript
|
|
693
|
-
|
|
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
|
-
|
|
397
|
+
const userSignal = createStateSignal({name: 'user', initialValue: {name: 'John', age: 30}});
|
|
398
|
+
const userNameSignal = createMappedSignal(userSignal, (user) => user.name);
|
|
705
399
|
|
|
706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
+
## Advanced Subscription Options
|
|
725
406
|
|
|
726
|
-
|
|
407
|
+
When subscribing to signals, you can customize the behavior with the `SubscribeOptions` parameter:
|
|
727
408
|
|
|
728
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
+
**Always call `destroy()` on `ComputedSignal` and `EffectSignal` when they are no longer needed.**
|
|
777
428
|
|
|
778
429
|
```typescript
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
+
---
|
|
836
450
|
|
|
837
|
-
|
|
838
|
-
- `config.name`: `string`
|
|
839
|
-
- **`.dispatch(payload: T)`**: یک رویداد را به همه شنوندگان ارسال میکند.
|
|
451
|
+
## Asynchronous Scheduling
|
|
840
452
|
|
|
841
|
-
|
|
453
|
+
To prevent performance degradation, Alwatr Signal employs an asynchronous execution strategy:
|
|
842
454
|
|
|
843
|
-
- **`
|
|
844
|
-
|
|
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
|
-
|
|
458
|
+
---
|
|
854
459
|
|
|
855
|
-
|
|
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
|
-
|
|
462
|
+
`@alwatr/signal` serves as the **State Layer** of the **Alwatr Flux** unidirectional architecture:
|
|
865
463
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
875
|
-
- **`.untilNext()`**: یک `Promise` برمیگرداند که با مقدار/پیام بعدی resolve میشود.
|
|
876
|
-
- **`.destroy()`**: (روی همه سیگنالها به جز `StateSignal`) سیگنال را پاکسازی میکند.
|
|
470
|
+
---
|
|
877
471
|
|
|
878
|
-
##
|
|
472
|
+
## Sponsors
|
|
879
473
|
|
|
880
|
-
|
|
474
|
+
Flux and Alwatr packages are supported by our sponsors. Become a Sponsor to place your logo here.
|
|
881
475
|
|
|
882
|
-
##
|
|
476
|
+
## Contributing
|
|
883
477
|
|
|
884
|
-
|
|
478
|
+
Contributions are welcome! Please read our [contribution guidelines](https://github.com/Alwatr/.github/blob/next/CONTRIBUTING.md) before submitting pull requests.
|