@alwatr/signal 9.11.2 → 9.12.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 CHANGED
@@ -31,6 +31,8 @@ There is also a fourth type for stateless events:
31
31
 
32
32
  4. **`EventSignal`**: A stateless signal for dispatching one-off events that don't have a persistent value.
33
33
 
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.
35
+
34
36
  ---
35
37
 
36
38
  ## Getting Started: A Practical Example
@@ -128,6 +130,179 @@ User: Jane has clicked 0 times.
128
130
  User: Jane has clicked 1 times.
129
131
  ```
130
132
 
133
+ ---
134
+
135
+ ## ChannelSignal: A Typed Message Bus
136
+
137
+ ### Why ChannelSignal?
138
+
139
+ 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
+
141
+ ```typescript
142
+ // ❌ Verbose and hard to manage
143
+ const openDrawerSignal = new EventSignal<{panel: string}>({name: 'open-drawer'});
144
+ const closeDrawerSignal = new EventSignal({name: 'close-drawer'});
145
+ const showToastSignal = new EventSignal<{message: string; type: 'info' | 'error'}>({name: 'show-toast'});
146
+ // ... and so on for every action in your app
147
+ ```
148
+
149
+ **`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
+
151
+ ### Architecture: O(1) Routing
152
+
153
+ 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
+
155
+ ### Creating a ChannelSignal
156
+
157
+ First, define a **message map** — a TypeScript interface that maps every valid message name to its payload type:
158
+
159
+ ```typescript
160
+ import {ChannelSignal} from '@alwatr/signal';
161
+
162
+ // Define the message map for your application
163
+ interface AppMessages {
164
+ 'open-drawer': {panel: string};
165
+ 'close-drawer': void; // no payload
166
+ 'show-toast': {message: string; type: 'info' | 'error'};
167
+ 'navigate': {path: string};
168
+ }
169
+
170
+ // Create the channel
171
+ const appChannel = new ChannelSignal<AppMessages>({name: 'app-channel'});
172
+ ```
173
+
174
+ ### Subscribing to Named Messages
175
+
176
+ 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
+
178
+ ```typescript
179
+ // Subscribe to 'open-drawer' messages
180
+ appChannel.on('open-drawer', (payload) => {
181
+ console.log(`Opening drawer: ${payload!.panel}`);
182
+ // TypeScript knows payload is {panel: string} | undefined
183
+ });
184
+
185
+ // Subscribe to 'show-toast' messages
186
+ appChannel.on('show-toast', (payload) => {
187
+ toast.show(payload!.message, payload!.type);
188
+ // TypeScript knows payload is {message: string; type: 'info' | 'error'} | undefined
189
+ });
190
+
191
+ // Subscribe to 'close-drawer' (no payload)
192
+ appChannel.on('close-drawer', (payload) => {
193
+ console.log('Closing drawer');
194
+ // TypeScript knows payload is void | undefined
195
+ });
196
+ ```
197
+
198
+ ### Dispatching Messages
199
+
200
+ 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
+
202
+ ```typescript
203
+ // Dispatch with payload
204
+ appChannel.dispatch('open-drawer', {panel: 'settings'}); // ✅ Type-safe
205
+ appChannel.dispatch('show-toast', {message: 'Saved!', type: 'info'}); // ✅
206
+
207
+ // Dispatch without payload
208
+ appChannel.dispatch('close-drawer'); // ✅
209
+
210
+ // ❌ TypeScript errors:
211
+ appChannel.dispatch('open-drawer', {panel: 123}); // Error: panel must be string
212
+ appChannel.dispatch('show-toast', {message: 'Hi'}); // Error: missing 'type'
213
+ appChannel.dispatch('unknown-action'); // Error: 'unknown-action' is not in AppMessages
214
+ ```
215
+
216
+ ### Unsubscribing
217
+
218
+ Just like other signals, `.on()` returns a `SubscribeResult` with an `unsubscribe()` method:
219
+
220
+ ```typescript
221
+ const sub = appChannel.on('navigate', (payload) => {
222
+ router.push(payload!.path);
223
+ });
224
+
225
+ // Later, when the component is destroyed:
226
+ sub.unsubscribe();
227
+ ```
228
+
229
+ ### One-Time Subscriptions
230
+
231
+ Use the `once` option to automatically unsubscribe after the first message:
232
+
233
+ ```typescript
234
+ appChannel.on(
235
+ 'app-ready',
236
+ () => {
237
+ console.log('App initialized!');
238
+ },
239
+ {once: true},
240
+ );
241
+ ```
242
+
243
+ ### Raw Stream Subscription (for Logging/Middleware)
244
+
245
+ 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
+
247
+ ```typescript
248
+ // Log every message for debugging
249
+ appChannel.subscribe((msg) => {
250
+ console.log(`[channel] ${String(msg.name)}`, msg.payload);
251
+ });
252
+ ```
253
+
254
+ **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
+
256
+ ### Use Cases
257
+
258
+ `ChannelSignal` is ideal for:
259
+
260
+ - **Action layers** in Unidirectional Data Flow architectures (like `@alwatr/action`)
261
+ - **Event buses** in component-based UIs (e.g., a global app event channel)
262
+ - **Command dispatching** in CQRS-style systems
263
+ - **Pub/sub messaging** where you have many distinct message types but want a single, centralized channel
264
+
265
+ ### Example: A Complete Action System
266
+
267
+ ```typescript
268
+ import {ChannelSignal} from '@alwatr/signal';
269
+
270
+ // Define all app actions
271
+ interface AppActions {
272
+ 'user-login': {username: string};
273
+ 'user-logout': void;
274
+ 'cart-add-item': {productId: number; quantity: number};
275
+ 'cart-remove-item': {productId: number};
276
+ 'navigate': {path: string};
277
+ }
278
+
279
+ const actionChannel = new ChannelSignal<AppActions>({name: 'app-actions'});
280
+
281
+ // Business logic subscribes to actions
282
+ actionChannel.on('user-login', (payload) => {
283
+ authService.login(payload!.username);
284
+ });
285
+
286
+ actionChannel.on('cart-add-item', (payload) => {
287
+ cartService.addItem(payload!.productId, payload!.quantity);
288
+ });
289
+
290
+ actionChannel.on('navigate', (payload) => {
291
+ router.push(payload!.path);
292
+ });
293
+
294
+ // UI dispatches actions (e.g., from button clicks)
295
+ loginButton.addEventListener('click', () => {
296
+ actionChannel.dispatch('user-login', {username: 'ali'});
297
+ });
298
+
299
+ addToCartButton.addEventListener('click', () => {
300
+ actionChannel.dispatch('cart-add-item', {productId: 42, quantity: 1});
301
+ });
302
+ ```
303
+
304
+ ---
305
+
131
306
  ## Advanced Topics
132
307
 
133
308
  ### Lifecycle Management and Memory Leaks
@@ -199,6 +374,14 @@ The `subscribe` method accepts an optional second argument to customize its beha
199
374
  - `config.name`: `string`
200
375
  - **`.dispatch(payload: T)`**: Dispatches an event to all listeners.
201
376
 
377
+ ### `ChannelSignal<TMap>`
378
+
379
+ - **`constructor(config)`**: Creates a new channel signal.
380
+ - `config.name`: `string`
381
+ - **`.dispatch(name, payload?)`**: Dispatches a named message. TypeScript enforces the correct payload type for each name.
382
+ - **`.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.
383
+ - **`.subscribe(callback, options?)`**: Subscribes to the **raw message stream** — receives every `{name, payload}` envelope regardless of name. Useful for logging and middleware.
384
+
202
385
  ### Common Methods
203
386
 
204
387
  - **`.subscribe(callback, options?)`**: Subscribes a listener. Returns `{ unsubscribe: () => void }`.
@@ -252,6 +435,10 @@ Contributions are welcome! Please read our [contribution guidelines](https://git
252
435
 
253
436
  4. **`EventSignal`**: یک سیگنال بدون حالت برای ارسال رویدادهای یک‌باره که مقدار پایداری ندارند.
254
437
 
438
+ و یک نوع پنجم برای مسیریابی پیام‌های چندگانه:
439
+
440
+ 5. **`ChannelSignal`**: یک Message Bus تایپ‌شده و بدون حالت. برخلاف `EventSignal` (یک سیگنال = یک نوع رویداد)، یک `ChannelSignal` واحد چندین نوع پیام با نام‌های مختلف را حمل می‌کند — هر کدام با نوع payload مخصوص خودشان — و آن‌ها را با سرعت **O(1)** به subscriber‌های مناسب هدایت می‌کند.
441
+
255
442
  ---
256
443
 
257
444
  ## شروع به کار: یک مثال عملی
@@ -349,6 +536,178 @@ User: Jane has clicked 0 times.
349
536
  User: Jane has clicked 1 times.
350
537
  ```
351
538
 
539
+ ---
540
+
541
+ ## ChannelSignal: یک Message Bus تایپ‌شده
542
+
543
+ ### چرا ChannelSignal؟
544
+
545
+ در برنامه‌های واقعی، اغلب نیاز دارید انواع مختلفی از رویدادها یا پیام‌ها را dispatch کنید — مثلاً `'open-drawer'`، `'close-drawer'`، `'show-toast'`، `'navigate'` و غیره. می‌توانید برای هر کدام یک `EventSignal` جداگانه بسازید، اما این رویکرد به سرعت دست‌وپاگیر می‌شود:
546
+
547
+ ```typescript
548
+ // ❌ پرحجم و سخت برای مدیریت
549
+ const openDrawerSignal = new EventSignal<{panel: string}>({name: 'open-drawer'});
550
+ const closeDrawerSignal = new EventSignal({name: 'close-drawer'});
551
+ const showToastSignal = new EventSignal<{message: string; type: 'info' | 'error'}>({name: 'show-toast'});
552
+ // ... و به همین ترتیب برای هر action در برنامه
553
+ ```
554
+
555
+ **`ChannelSignal` این مشکل را حل می‌کند.** یک سیگنال واحد است که به عنوان یک **Message Bus تایپ‌شده** عمل می‌کند — یک کانال، انواع پیام‌های مختلف. مثل یک Go-style channel یا یک pub/sub topic با ایمنی کامل TypeScript.
556
+
557
+ ### معماری: مسیریابی O(1)
558
+
559
+ در داخل، `ChannelSignal` از یک `Map<name, Set<handler>>` برای مسیریابی پیام‌ها استفاده می‌کند. وقتی پیامی با نام `'A'` dispatch می‌شود، فقط handler‌هایی که برای `'A'` ثبت شده‌اند فراخوانی می‌شوند — **جستجوی O(1)**، صرف‌نظر از اینکه چه تعداد نام دیگری subscribe شده باشند. این یک بهینه‌سازی حیاتی برای برنامه‌هایی است که صدها یا هزاران directive/component دارند که به action‌های مختلف گوش می‌دهند.
560
+
561
+ ### ساخت یک ChannelSignal
562
+
563
+ ابتدا یک **message map** تعریف کنید — یک interface در TypeScript که هر نام پیام معتبر را به نوع payload آن نگاشت می‌کند:
564
+
565
+ ```typescript
566
+ import {ChannelSignal} from '@alwatr/signal';
567
+
568
+ // تعریف message map برای برنامه
569
+ interface AppMessages {
570
+ 'open-drawer': {panel: string};
571
+ 'close-drawer': void; // بدون payload
572
+ 'show-toast': {message: string; type: 'info' | 'error'};
573
+ 'navigate': {path: string};
574
+ }
575
+
576
+ // ساخت channel
577
+ const appChannel = new ChannelSignal<AppMessages>({name: 'app-channel'});
578
+ ```
579
+
580
+ ### Subscribe به پیام‌های نام‌دار
581
+
582
+ از `.on(name, handler)` برای subscribe به یک پیام خاص استفاده کنید. handler مستقیماً **payload** را دریافت می‌کند (نه envelope کامل `{name, payload}`) — چون نام در زمان subscribe مشخص است، ارسال مجدد آن اضافی خواهد بود.
583
+
584
+ ```typescript
585
+ // Subscribe به پیام‌های 'open-drawer'
586
+ appChannel.on('open-drawer', (payload) => {
587
+ console.log(`Opening drawer: ${payload!.panel}`);
588
+ // TypeScript می‌داند payload از نوع {panel: string} | undefined است
589
+ });
590
+
591
+ // Subscribe به پیام‌های 'show-toast'
592
+ appChannel.on('show-toast', (payload) => {
593
+ toast.show(payload!.message, payload!.type);
594
+ // TypeScript می‌داند payload از نوع {message: string; type: 'info' | 'error'} | undefined است
595
+ });
596
+
597
+ // Subscribe به 'close-drawer' (بدون payload)
598
+ appChannel.on('close-drawer', () => {
599
+ console.log('Closing drawer');
600
+ });
601
+ ```
602
+
603
+ ### Dispatch پیام‌ها
604
+
605
+ از `.dispatch(name, payload)` برای ارسال پیام استفاده کنید. TypeScript اعمال می‌کند که payload با نوع تعریف‌شده برای آن نام در message map مطابقت داشته باشد.
606
+
607
+ ```typescript
608
+ // Dispatch با payload
609
+ appChannel.dispatch('open-drawer', {panel: 'settings'}); // ✅ Type-safe
610
+ appChannel.dispatch('show-toast', {message: 'ذخیره شد!', type: 'info'}); // ✅
611
+
612
+ // Dispatch بدون payload
613
+ appChannel.dispatch('close-drawer'); // ✅
614
+
615
+ // ❌ خطاهای TypeScript:
616
+ appChannel.dispatch('open-drawer', {panel: 123}); // خطا: panel باید string باشد
617
+ appChannel.dispatch('show-toast', {message: 'سلام'}); // خطا: 'type' وجود ندارد
618
+ appChannel.dispatch('unknown-action'); // خطا: 'unknown-action' در AppMessages نیست
619
+ ```
620
+
621
+ ### Unsubscribe کردن
622
+
623
+ مثل سایر سیگنال‌ها، `.on()` یک `SubscribeResult` با متد `unsubscribe()` برمی‌گرداند:
624
+
625
+ ```typescript
626
+ const sub = appChannel.on('navigate', (payload) => {
627
+ router.push(payload!.path);
628
+ });
629
+
630
+ // بعداً، وقتی کامپوننت destroy می‌شود:
631
+ sub.unsubscribe();
632
+ ```
633
+
634
+ ### Subscribe یک‌باره
635
+
636
+ از گزینه `once` برای unsubscribe خودکار بعد از اولین پیام استفاده کنید:
637
+
638
+ ```typescript
639
+ appChannel.on(
640
+ 'app-ready',
641
+ () => {
642
+ console.log('برنامه آماده است!');
643
+ },
644
+ {once: true},
645
+ );
646
+ ```
647
+
648
+ ### Subscribe به جریان خام (برای لاگ‌گیری/Middleware)
649
+
650
+ اگر نیاز دارید **همه** پیام‌ها را صرف‌نظر از نام مشاهده کنید — مثلاً برای لاگ‌گیری، analytics یا middleware — از `.subscribe()` به جای `.on()` استفاده کنید. این متد envelope کامل `{name, payload}` را دریافت می‌کند:
651
+
652
+ ```typescript
653
+ // لاگ کردن همه پیام‌ها برای debugging
654
+ appChannel.subscribe((msg) => {
655
+ console.log(`[channel] ${String(msg.name)}`, msg.payload);
656
+ });
657
+ ```
658
+
659
+ **نکته مهم:** `.subscribe()` بر اساس نام فیلتر **نمی‌شود** — هر پیامی را دریافت می‌کند. برای موارد عادی، `.on(name, handler)` را ترجیح دهید تا subscriptionها متمرکز و کارآمد بمانند.
660
+
661
+ ### موارد استفاده
662
+
663
+ `ChannelSignal` برای موارد زیر ایده‌آل است:
664
+
665
+ - **لایه Action** در معماری‌های Unidirectional Data Flow (مثل `@alwatr/action`)
666
+ - **Event Bus** در UI‌های مبتنی بر کامپوننت (مثلاً یک کانال رویداد سراسری برنامه)
667
+ - **Command Dispatching** در سیستم‌های CQRS-style
668
+ - **Pub/Sub Messaging** جایی که انواع پیام‌های مختلف دارید اما می‌خواهید یک کانال مرکزی داشته باشید
669
+
670
+ ### مثال کامل: یک سیستم Action
671
+
672
+ ```typescript
673
+ import {ChannelSignal} from '@alwatr/signal';
674
+
675
+ // تعریف همه action‌های برنامه
676
+ interface AppActions {
677
+ 'user-login': {username: string};
678
+ 'user-logout': void;
679
+ 'cart-add-item': {productId: number; quantity: number};
680
+ 'cart-remove-item': {productId: number};
681
+ 'navigate': {path: string};
682
+ }
683
+
684
+ const actionChannel = new ChannelSignal<AppActions>({name: 'app-actions'});
685
+
686
+ // منطق تجاری به action‌ها subscribe می‌کند
687
+ actionChannel.on('user-login', (payload) => {
688
+ authService.login(payload!.username);
689
+ });
690
+
691
+ actionChannel.on('cart-add-item', (payload) => {
692
+ cartService.addItem(payload!.productId, payload!.quantity);
693
+ });
694
+
695
+ actionChannel.on('navigate', (payload) => {
696
+ router.push(payload!.path);
697
+ });
698
+
699
+ // UI اقدام به dispatch action می‌کند (مثلاً از کلیک دکمه)
700
+ loginButton.addEventListener('click', () => {
701
+ actionChannel.dispatch('user-login', {username: 'ali'});
702
+ });
703
+
704
+ addToCartButton.addEventListener('click', () => {
705
+ actionChannel.dispatch('cart-add-item', {productId: 42, quantity: 1});
706
+ });
707
+ ```
708
+
709
+ ---
710
+
352
711
  ## مباحث پیشرفته
353
712
 
354
713
  ### مدیریت چرخه حیات و نشت حافظه
@@ -420,6 +779,14 @@ Alwatr Signal از یک مدل ناهمزمان قابل پیش‌بینی بر
420
779
  - `config.name`: `string`
421
780
  - **`.dispatch(payload: T)`**: یک رویداد را به تمام شنوندگان ارسال می‌کند.
422
781
 
782
+ ### `ChannelSignal<TMap>`
783
+
784
+ - **`constructor(config)`**: یک channel signal جدید ایجاد می‌کند.
785
+ - `config.name`: `string`
786
+ - **`.dispatch(name, payload?)`**: یک پیام با نام مشخص ارسال می‌کند. TypeScript نوع صحیح payload را برای هر نام اعمال می‌کند.
787
+ - **`.on(name, handler, options?)`**: به یک پیام با نام مشخص subscribe می‌کند. handler مستقیماً `payload` را دریافت می‌کند (نه envelope کامل). از یک `Map` داخلی برای routing **O(1)** استفاده می‌کند. از گزینه `once` پشتیبانی می‌کند.
788
+ - **`.subscribe(callback, options?)`**: به **جریان خام پیام‌ها** subscribe می‌کند — هر envelope `{name, payload}` را صرف‌نظر از نام دریافت می‌کند. برای لاگ‌گیری و middleware مفید است.
789
+
423
790
  ### متدهای مشترک
424
791
 
425
792
  - **`.subscribe(callback, options?)`**: یک شنونده را مشترک می‌کند. `{ unsubscribe: () => void }` را برمی‌گرداند.
@@ -0,0 +1,177 @@
1
+ import { type AlwatrLogger } from '@alwatr/logger';
2
+ import { SignalBase } from './signal-base.js';
3
+ import type { SignalConfig, SubscribeOptions, SubscribeResult, ListenerCallback } from '../type.js';
4
+ /**
5
+ * A single message dispatched through a `ChannelSignal`.
6
+ *
7
+ * `name` identifies the message type (e.g. `'open-drawer'`, `'add-to-cart'`).
8
+ * `payload` is the optional value attached to the message — its type is narrowed
9
+ * by the generic map `TMap` at the class level.
10
+ *
11
+ * @template TMap A record mapping message names to their payload types.
12
+ * @template K The specific message name key (inferred, not set manually).
13
+ */
14
+ export type ChannelMessage<TMap extends Record<string, unknown>, K extends keyof TMap = keyof TMap> = K extends keyof TMap ? {
15
+ name: K;
16
+ payload?: TMap[K];
17
+ } : never;
18
+ /**
19
+ * A typed handler for a specific named message on a `ChannelSignal`.
20
+ * Receives only the `payload` — the name is already known at subscription time.
21
+ *
22
+ * @template TMap A record mapping message names to their payload types.
23
+ * @template K The specific message name key.
24
+ */
25
+ export type ChannelHandler<TMap extends Record<string, unknown>, K extends keyof TMap> = (payload: TMap[K] | undefined) => void | Promise<void>;
26
+ /**
27
+ * Configuration for creating a `ChannelSignal`.
28
+ */
29
+ export interface ChannelSignalConfig extends SignalConfig {
30
+ }
31
+ /**
32
+ * A stateless multi-channel signal that acts as a typed O(1) message bus.
33
+ *
34
+ * `ChannelSignal` is ideal when you need a single signal to carry multiple
35
+ * distinct message types — each identified by a `name` — rather than creating
36
+ * a separate `EventSignal` for every event.
37
+ *
38
+ * ### Routing architecture
39
+ *
40
+ * Internally, `on()` subscriptions are stored in a per-name `Map` of handler
41
+ * sets. When a message is dispatched, only the handlers registered for that
42
+ * specific name are invoked — O(1) lookup regardless of how many distinct
43
+ * names are subscribed. The inherited `SignalBase` observer list is used
44
+ * exclusively by `subscribe()`, which receives the raw message stream for
45
+ * logging or middleware purposes.
46
+ *
47
+ * ### Type safety
48
+ *
49
+ * The generic parameter `TMap` is a record that maps every valid message name
50
+ * to its payload type. TypeScript enforces the correct payload type at both
51
+ * `dispatch` and `on` call sites.
52
+ *
53
+ * @template TMap A record mapping message names to their payload types.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * interface AppMessages {
58
+ * 'open-drawer': {panel: string};
59
+ * 'close-drawer': void;
60
+ * 'show-toast': {message: string; type: 'info' | 'error'};
61
+ * }
62
+ *
63
+ * const appChannel = new ChannelSignal<AppMessages>({name: 'app-channel'});
64
+ *
65
+ * // Subscribe to a specific message — handler receives payload directly
66
+ * appChannel.on('open-drawer', (payload) => {
67
+ * openDrawer(payload!.panel);
68
+ * });
69
+ *
70
+ * // Dispatch a typed message
71
+ * appChannel.dispatch('open-drawer', {panel: 'settings'});
72
+ * appChannel.dispatch('close-drawer'); // no payload needed
73
+ * ```
74
+ */
75
+ export declare class ChannelSignal<TMap extends Record<string, unknown>> extends SignalBase<ChannelMessage<TMap>> {
76
+ /**
77
+ * The logger instance for this signal.
78
+ * @protected
79
+ */
80
+ protected logger_: AlwatrLogger;
81
+ /**
82
+ * Per-name handler registry for O(1) routing.
83
+ *
84
+ * Each key is a message name; the value is a Set of `{handler, once}` entries
85
+ * registered via `on()`. Kept separate from `SignalBase`'s observer list so
86
+ * that `subscribe()` (raw stream) and `on()` (named routing) never interfere.
87
+ *
88
+ * @private
89
+ */
90
+ private readonly namedHandlers__;
91
+ constructor(config: ChannelSignalConfig);
92
+ /**
93
+ * Dispatches a named message to:
94
+ * 1. All handlers registered via `on(name, …)` for this specific name — O(1).
95
+ * 2. All raw-stream subscribers registered via `subscribe()` — O(N subscribers).
96
+ *
97
+ * The notification is scheduled as a microtask to ensure non-blocking,
98
+ * consistent delivery — matching the behavior of `EventSignal`.
99
+ *
100
+ * TypeScript enforces that `payload` matches the type declared for `name`
101
+ * in `TMap`. If the payload type is `void` or `undefined`, the argument can
102
+ * be omitted entirely.
103
+ *
104
+ * @param name The message name (must be a key of `TMap`).
105
+ * @param payload The optional payload for the message.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * channel.dispatch('open-drawer', {panel: 'settings'});
110
+ * channel.dispatch('close-drawer'); // no payload needed
111
+ * ```
112
+ */
113
+ dispatch<K extends keyof TMap>(name: K, payload?: TMap[K]): void;
114
+ /**
115
+ * Subscribes to a specific named message on this channel.
116
+ *
117
+ * Uses an internal per-name handler map for O(1) routing — dispatching a
118
+ * message with name `'A'` will never invoke handlers registered for `'B'`.
119
+ *
120
+ * The handler receives the `payload` directly (not the full `{name, payload}`
121
+ * envelope) — since the name is already known at subscription time, passing
122
+ * it again would be redundant.
123
+ *
124
+ * @param name The message name to listen for.
125
+ * @param handler Callback invoked with the payload each time the named message
126
+ * is dispatched.
127
+ * @param options Standard subscribe options. Only `once` is supported here;
128
+ * `priority` applies to `subscribe()` (raw stream) only.
129
+ * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const sub = channel.on('open-drawer', (payload) => {
134
+ * openDrawer(payload!.panel);
135
+ * });
136
+ *
137
+ * // Stop listening when the component is destroyed
138
+ * sub.unsubscribe();
139
+ * ```
140
+ */
141
+ on<K extends keyof TMap>(name: K, handler: ChannelHandler<TMap, K>, options?: Pick<SubscribeOptions, 'once'>): SubscribeResult;
142
+ /**
143
+ * Subscribes to **all** messages dispatched on this channel, regardless of name.
144
+ *
145
+ * Use this when you need to observe the raw message stream — for example,
146
+ * for logging, debugging, or middleware-style processing.
147
+ *
148
+ * Prefer `on(name, handler)` for normal use cases to keep subscriptions
149
+ * focused and type-safe.
150
+ *
151
+ * @param callback The function called with every `ChannelMessage`.
152
+ * @param options Standard subscribe options.
153
+ * @returns A `SubscribeResult` with an `unsubscribe()` method.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * // Log every message for debugging
158
+ * channel.subscribe((msg) => console.log('[channel]', msg.name, msg.payload));
159
+ * ```
160
+ */
161
+ subscribe(callback: ListenerCallback<ChannelMessage<TMap>>, options?: SubscribeOptions): SubscribeResult;
162
+ /**
163
+ * Core routing method — called inside the microtask scheduled by `dispatch`.
164
+ *
165
+ * 1. Looks up the per-name handler set in O(1).
166
+ * 2. Invokes each handler, removing `once` entries after their first call.
167
+ * 3. Notifies raw-stream subscribers via `SignalBase.notify_()`.
168
+ *
169
+ * @private
170
+ */
171
+ private route__;
172
+ /**
173
+ * Destroys the signal, clearing all named handlers and raw-stream subscribers.
174
+ */
175
+ destroy(): void;
176
+ }
177
+ //# sourceMappingURL=channel-signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-signal.d.ts","sourceRoot":"","sources":["../../src/core/channel-signal.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAE/D,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAE5C,OAAO,KAAK,EAAC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAC,MAAM,YAAY,CAAC;AAIlG;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,GAAG,MAAM,IAAI,IAChG,CAAC,SAAS,MAAM,IAAI,GAAG;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;CAAC,GAAG,KAAK,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,IAAI,CACvF,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,KACzB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,YAAY;CAAG;AAI5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,aAAa,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,UAAU,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IACvG;;;OAGG;IACH,SAAS,CAAC,OAAO,EAAE,YAAY,CAAC;IAEhC;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CACpB;gBAEA,MAAM,EAAE,mBAAmB;IAMvC;;;;;;;;;;;;;;;;;;;;OAoBG;IACI,QAAQ,CAAC,CAAC,SAAS,MAAM,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;IAMvE;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACI,EAAE,CAAC,CAAC,SAAS,MAAM,IAAI,EAC5B,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,EAChC,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,GACvC,eAAe;IA2BlB;;;;;;;;;;;;;;;;;;OAkBG;IACa,SAAS,CACvB,QAAQ,EAAE,gBAAgB,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAChD,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe;IAKlB;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO;IAyBf;;OAEG;IACa,OAAO,IAAI,IAAI;CAIhC"}
@@ -68,6 +68,7 @@ export declare abstract class SignalBase<T> {
68
68
  * Executes a given observer's callback with the provided value, handling both synchronous and asynchronous callbacks.
69
69
  */
70
70
  private executeObserver__;
71
+ private pendingRejects__;
71
72
  /**
72
73
  * Returns a Promise that resolves with the next value dispatched by the signal.
73
74
  * This provides an elegant way to wait for a single, future event using `async/await`.
@@ -1 +1 @@
1
- {"version":3,"file":"signal-base.d.ts","sourceRoot":"","sources":["../../src/core/signal-base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAC,MAAM,YAAY,CAAC;AAC7G,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAEjD;;;;;GAKG;AACH,8BAAsB,UAAU,CAAC,CAAC;IAoCpB,SAAS,CAAC,OAAO,EAAE,YAAY;IAnC3C;;;OAGG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAEzC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,kBAAkB,oBAA2B;IAChE,SAAS,CAAC,QAAQ,CAAC,UAAU,oBAA2B;IAExD;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;OAKG;IACH,IAAW,WAAW,IAAI,OAAO,CAEhC;gBAEqB,OAAO,EAAE,YAAY;IAI3C;;;;;OAKG;IACH,SAAS,CAAC,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI;IAYvD;;;;;;;;OAQG;IACI,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe;IAkB5F;;;;;;;;OAQG;IACH,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAiBjC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;;;;;OAYG;IACI,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC;IAY9B;;;;;;OAMG;IACI,OAAO,IAAI,IAAI;IAatB;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,IAAI;CAMlC"}
1
+ {"version":3,"file":"signal-base.d.ts","sourceRoot":"","sources":["../../src/core/signal-base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAC,MAAM,YAAY,CAAC;AAC7G,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAEjD;;;;;GAKG;AACH,8BAAsB,UAAU,CAAC,CAAC;IAoCpB,SAAS,CAAC,OAAO,EAAE,YAAY;IAnC3C;;;OAGG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAEzC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,kBAAkB,oBAA2B;IAChE,SAAS,CAAC,QAAQ,CAAC,UAAU,oBAA2B;IAExD;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;OAKG;IACH,IAAW,WAAW,IAAI,OAAO,CAEhC;gBAEqB,OAAO,EAAE,YAAY;IAI3C;;;;;OAKG;IACH,SAAS,CAAC,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI;IAYvD;;;;;;;;OAQG;IACI,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe;IAkB5F;;;;;;;;OAQG;IACH,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAiBjC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,gBAAgB,CAAqC;IAE7D;;;;;;;;;;;;OAYG;IACI,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC;IAmB9B;;;;;;OAMG;IACI,OAAO,IAAI,IAAI;IAqBtB;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,IAAI;CAMlC"}
@@ -0,0 +1,39 @@
1
+ import { ChannelSignal } from '../core/channel-signal.js';
2
+ import type { ChannelSignalConfig } from '../core/channel-signal.js';
3
+ /**
4
+ * Creates a stateless multi-channel signal that acts as a typed message bus.
5
+ *
6
+ * `ChannelSignal` is ideal when you need a single signal to carry multiple
7
+ * distinct message types — each identified by a `name` — rather than creating
8
+ * a separate `EventSignal` for every event.
9
+ *
10
+ * The generic parameter `TMap` is a record that maps every valid message name
11
+ * to its payload type, giving you full type safety at both dispatch and subscribe sites.
12
+ *
13
+ * @template TMap A record mapping message names to their payload types.
14
+ *
15
+ * @param config The configuration for the channel signal.
16
+ * @returns A new instance of `ChannelSignal`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * interface AppMessages {
21
+ * 'open-drawer': {panel: string};
22
+ * 'close-drawer': void;
23
+ * 'show-toast': {message: string; type: 'info' | 'error'};
24
+ * }
25
+ *
26
+ * const appChannel = createChannelSignal<AppMessages>({name: 'app-channel'});
27
+ *
28
+ * // Subscribe to a specific message
29
+ * appChannel.on('show-toast', (payload) => {
30
+ * toast.show(payload!.message, payload!.type);
31
+ * });
32
+ *
33
+ * // Dispatch a message
34
+ * appChannel.dispatch('show-toast', {message: 'Saved!', type: 'info'});
35
+ * appChannel.dispatch('close-drawer');
36
+ * ```
37
+ */
38
+ export declare function createChannelSignal<TMap extends Record<string, unknown>>(config: ChannelSignalConfig): ChannelSignal<TMap>;
39
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/creators/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,2BAA2B,CAAC;AAExD,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,2BAA2B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtE,MAAM,EAAE,mBAAmB,GAC1B,aAAa,CAAC,IAAI,CAAC,CAErB"}
package/dist/main.d.ts CHANGED
@@ -5,12 +5,14 @@ export * from './core/computed-signal.js';
5
5
  export * from './core/effect-signal.js';
6
6
  export * from './core/persistent-state-signal.js';
7
7
  export * from './core/session-state-signal.js';
8
+ export * from './core/channel-signal.js';
8
9
  export * from './creators/event.js';
9
10
  export * from './creators/state.js';
10
11
  export * from './creators/computed.js';
11
12
  export * from './creators/effect.js';
12
13
  export * from './creators/persistent-state.js';
13
14
  export * from './creators/session-state.js';
15
+ export * from './creators/channel.js';
14
16
  export * from './operators/debounce.js';
15
17
  export * from './operators/filter.js';
16
18
  export * from './operators/map.js';
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mCAAmC,CAAC;AAClD,cAAc,gCAAgC,CAAC;AAE/C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAE5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AAEnC,mBAAmB,WAAW,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mCAAmC,CAAC;AAClD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AAEzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC;AAEtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AAEnC,mBAAmB,WAAW,CAAC"}