@thi.ng/csp 3.1.0 → 3.2.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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2024-04-26T13:32:20Z
3
+ - **Last updated**: 2024-04-28T14:28:17Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,20 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ ## [3.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/csp@3.2.0) (2024-04-28)
13
+
14
+ #### 🚀 Features
15
+
16
+ - update Mult/PubSub unsub handling, add docs ([32ad70e](https://github.com/thi-ng/umbrella/commit/32ad70e))
17
+ - add optional auto-closing for Mult.unsubscribe(), PubSub.unsubscribeTopic()
18
+ - add docs
19
+
20
+ #### 🩹 Bug fixes
21
+
22
+ - update select() ([5e87c8d](https://github.com/thi-ng/umbrella/commit/5e87c8d))
23
+ - update select(), ensure write queue of selected channel is being updated
24
+ - mark Channel.updateQueue() as internal
25
+
12
26
  ## [3.1.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/csp@3.1.0) (2024-04-26)
13
27
 
14
28
  #### 🚀 Features
package/README.md CHANGED
@@ -15,6 +15,10 @@
15
15
  > GitHub](https://github.com/sponsors/postspectacular). Thank you! ❤️
16
16
 
17
17
  - [About](#about)
18
+ - [What is CSP?](#what-is-csp)
19
+ - [Buffering behaviors](#buffering-behaviors)
20
+ - [Channels](#channels)
21
+ - [Other channel types](#other-channel-types)
18
22
  - [Channel operators](#channel-operators)
19
23
  - [Status](#status)
20
24
  - [Related packages](#related-packages)
@@ -22,6 +26,7 @@
22
26
  - [Dependencies](#dependencies)
23
27
  - [Usage examples](#usage-examples)
24
28
  - [API](#api)
29
+ - [Ping pong](#ping-pong)
25
30
  - [PubSub](#pubsub)
26
31
  - [Authors](#authors)
27
32
  - [License](#license)
@@ -31,25 +36,71 @@
31
36
  Primitives & operators for Communicating Sequential Processes based on async/await and async iterables.
32
37
 
33
38
  This package was temporarily deprecated (throughout most of 2023), but meanwhile
34
- has been reanimated in the form of a **complete rewrite**, using a new, more
39
+ has been **reanimated in the form of a complete rewrite**, using a new, more
35
40
  simple and more modern approach afforded by contemporary ES language features
36
41
  (and widespread support for them).
37
42
 
38
43
  **This new/current implementation is in most cases NOT compatible with earlier
39
44
  versions**.
40
45
 
41
- Provided are:
46
+ ### What is CSP?
47
+
48
+ References:
49
+
50
+ - [Wikipedia](https://en.wikipedia.org/wiki/Communicating_sequential_processes)
51
+ - [Communicating Sequential Processes, C.A.R.
52
+ Hoare](https://dl.acm.org/doi/pdf/10.1145/359576.359585)
53
+
54
+ The key construct of this package is a read/write channel primitive which can be
55
+ customized with different buffer implementations to control blocking behaviors
56
+ and backpressure handling (aka attempting to write faster to a channel than
57
+ values are being read, essentially a memory management issue). Unbuffered CSP
58
+ channels are blocking on both the reader and writer side.
59
+
60
+ ### Buffering behaviors
61
+
62
+ The following channel buffer types/behaviors are included (from the
63
+ [thi.ng/buffers](https://github.com/thi-ng/umbrella/tree/develop/packages/buffers)
64
+ package), all accepting a max. capacity and all implementing the
65
+ [IReadWriteBuffer](https://docs.thi.ng/umbrella/buffers/interfaces/IReadWriteBuffer.html)
66
+ interface required by the channel:
67
+
68
+ - [`fifo`](https://docs.thi.ng/umbrella/buffers/functions/fifo.html): First in,
69
+ first out ring buffer. Writes to the channel will start blocking once the
70
+ buffer's capacity is reached, otherwise complete immediately. Likewise,
71
+ channel reads are non-blocking whilst there're more buffered values available.
72
+ Reads will only block if the buffer is empty.
73
+ - [`lifo`](https://docs.thi.ng/umbrella/buffers/functions/lifo.html): Last in,
74
+ first out. Write behavior is the same as with `fifo`, reads are in reverse
75
+ order (as the name indicates), i.e. the last value written will be the first
76
+ value read (i.e. stack behavior).
77
+ - [`sliding`](https://docs.thi.ng/umbrella/buffers/functions/sliding.html):
78
+ Sliding window ring buffer. Writes to the channel are **never** blocking!
79
+ Whilst the buffer is at full capacity, new writes will first expunge the
80
+ oldest buffered value (similar to [LRU
81
+ cache](https://github.com/thi-ng/umbrella/blob/develop/packages/cache/README.md#lru)
82
+ behavior). Read behavior is the same as for `fifo`.
83
+ - [`dropping`](https://docs.thi.ng/umbrella/buffers/functions/dropping.html):
84
+ Dropping value ring buffer. Writes to the channel are **never** blocking!
85
+ Whilst the buffer is at full capacity, new writes will be silently ignored.
86
+ Read behavior is the same as for `fifo`.
87
+
88
+ ### Channels
89
+
90
+ As mentioned previously,
91
+ [channels](https://docs.thi.ng/umbrella/csp/functions/channel.html) and their
92
+ [read](https://docs.thi.ng/umbrella/csp/classes/Channel.html#read),
93
+ [write](https://docs.thi.ng/umbrella/csp/classes/Channel.html#write) and
94
+ [close](https://docs.thi.ng/umbrella/csp/classes/Channel.html#close) operations
95
+ are the key building blocks for CSP.
96
+
97
+ ### Other channel types
42
98
 
43
- - [CSP `Channel`
44
- primitive](https://docs.thi.ng/umbrella/csp/classes/Channel.html) supporting a
45
- choice of buffer behaviors (fifo, sliding, dropping, see
46
- [thi.ng/buffers](https://github.com/thi-ng/umbrella/blob/develop/packages/buffers)
47
- for options)
48
- - Composable channel operators (see list below)
49
99
  - [`Mult`](https://docs.thi.ng/umbrella/csp/classes/Mult.html) for channel
50
- multiplexing (one-to-many splitting) and dynamic add/removal of subscribers
100
+ multiplexing (aka one-to-many splitting) and dynamic add/removal of
101
+ subscribers
51
102
  - [`PubSub`](https://docs.thi.ng/umbrella/csp/classes/PubSub.html) for
52
- topic-based subscriptions, each topic implemented as `Mult`
103
+ topic-based subscriptions, each topic implemented as a `Mult`
53
104
 
54
105
  ### Channel operators
55
106
 
@@ -129,6 +180,62 @@ directory is using this package:
129
180
 
130
181
  [Generated API docs](https://docs.thi.ng/umbrella/csp/)
131
182
 
183
+ ### Ping pong
184
+
185
+ ```ts tangle:export/readme-pingpong.ts
186
+ import { channel } from "@thi.ng/csp";
187
+
188
+ // create CSP channel for bi-directional communication
189
+ const chan = channel<number>();
190
+
191
+ // create first async process (ping)
192
+ (async () => {
193
+ while (true) {
194
+ // this op will block until a value becomes available in the channel
195
+ const x = await chan.read();
196
+ // if the channel was closed meanwhile, read() will deliver `undefined`
197
+ if (x === undefined || x > 5) {
198
+ console.log("stopping...");
199
+ // calling close() is idempotent
200
+ // any in-flight writes will still be readable
201
+ chan.close();
202
+ break;
203
+ }
204
+ console.log("ping", x);
205
+ // this op will also block until the other side is reading the value
206
+ await chan.write(x + 1);
207
+ }
208
+ console.log("ping done");
209
+ })();
210
+
211
+ // create second async process (pong, almost identical to ping)
212
+ (async () => {
213
+ while (true) {
214
+ // wait until value can be read (or channel closed)
215
+ const x = await chan.read();
216
+ // exit loop if channel closed
217
+ if (x === undefined) break;
218
+ console.log("pong", x);
219
+ // write next value & wait until other side read it
220
+ await chan.write(x + 1);
221
+ }
222
+ console.log("pong done");
223
+ })();
224
+
225
+ // kickoff
226
+ chan.write(0);
227
+
228
+ // ping 0
229
+ // pong 1
230
+ // ping 2
231
+ // pong 3
232
+ // ping 4
233
+ // pong 5
234
+ // stopping...
235
+ // ping done
236
+ // pong done
237
+ ```
238
+
132
239
  ### PubSub
133
240
 
134
241
  ```ts tangle:export/readme-pubsub.ts
package/channel.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import type { Fn, Maybe } from "@thi.ng/api";
2
2
  import type { ChannelBuffer, ChannelValue, IChannel } from "./api.js";
3
3
  export declare const MAX_READS = 1024;
4
- export declare const MAX_QUEUE = 1024;
4
+ export declare const MAX_WRITES = 1024;
5
5
  export interface ChannelOpts {
6
6
  id: string;
7
7
  }
8
+ /**
9
+ * Syntax sugar for creating a new CSP {@link Channel}. By default, the channel
10
+ * has a buffer capacity of 1 value, but supports custom buffer sizes and/or
11
+ * implementations (described in readme).
12
+ *
13
+ * @param opts
14
+ */
8
15
  export declare function channel<T>(opts?: Partial<ChannelOpts>): Channel<T>;
9
16
  export declare function channel<T>(buf: ChannelBuffer<T> | number, opts?: Partial<ChannelOpts>): Channel<T>;
10
17
  export declare class Channel<T> implements IChannel<T> {
@@ -14,14 +21,119 @@ export declare class Channel<T> implements IChannel<T> {
14
21
  reads: Fn<Maybe<T>, void>[];
15
22
  races: Fn<Channel<T>, void>[];
16
23
  protected state: number;
24
+ /**
25
+ * See {@link channel} for reference.
26
+ *
27
+ * @param opts
28
+ */
17
29
  constructor(opts?: Partial<ChannelOpts>);
18
30
  constructor(buf: ChannelBuffer<T> | number, opts?: Partial<ChannelOpts>);
31
+ /**
32
+ * Returns an async iterator of this channel, acting as adapter between the
33
+ * CSP world and the more generic ES async iterables. The iterator stops
34
+ * once the channel has been closed and no further values can be read.
35
+ *
36
+ * @remarks
37
+ * Multiple iterators will compete for new values. To ensure an iterator
38
+ * receives all of the channel's values, you must either ensure there's only
39
+ * a single iterator per channel or feed the channel into a {@link mult}
40
+ * first and create an iterator of a channel obtained via
41
+ * {@link Mult.subscribe}.
42
+ *
43
+ * @example
44
+ * ```ts tangle:../export/channel-iterator.ts
45
+ * import { channel } from "@thi.ng/csp";
46
+ *
47
+ * const chan = channel<number>();
48
+ *
49
+ * (async () => {
50
+ * // implicit iterator conversion of the channel
51
+ * for await(let x of chan) console.log("received", x);
52
+ * console.log("channel closed");
53
+ * })()
54
+ *
55
+ * chan.write(1);
56
+ * chan.write(2);
57
+ * chan.write(3);
58
+ * chan.close();
59
+ * ```
60
+ */
19
61
  [Symbol.asyncIterator](): AsyncIterableIterator<T>;
62
+ /**
63
+ * Attempts to read a value from the channel. The returned promise will
64
+ * block until such value becomes available or if the channel has been
65
+ * closed in the meantime. In that latter case, the promise will resolve to
66
+ * `undefined`.
67
+ *
68
+ * @remarks
69
+ * If a value is already available at the time of the function call, the
70
+ * promise resolves immediately.
71
+ *
72
+ * Note: There's a limit of in-flight {@link MAX_READS} allowed per channel.
73
+ * The promise will reject if that number is exceeded.
74
+ *
75
+ * Also see {@link Channel.poll}.
76
+ */
20
77
  read(): Promise<Maybe<T>>;
78
+ /**
79
+ * Similar to {@link Channel.read}, but not async and non-blocking. Will
80
+ * only succeed if the channel is readable (i.e. not yet closed) and if a
81
+ * value can be read immediately (without waiting). Returns the value or
82
+ * `undefined` if unsuccessful.
83
+ *
84
+ * @remarks
85
+ * Use {@link Channel.closed} to check if the channel is already closed.
86
+ */
21
87
  poll(): Maybe<T>;
88
+ /**
89
+ * Attempts to write a new value to the channel and returns a promise
90
+ * indicating success or failure. Depending on buffer capacity & behavior,
91
+ * the returned promise will block until the channel accept new values (i.e.
92
+ * until the next {@link Channel.read}) or if it has been closed in the
93
+ * meantime. In that latter case, the promise will resolve to false.
94
+ *
95
+ * @remarks
96
+ * If the channel's buffer accepts new writes or if a read op is already
97
+ * waiting at the time of the function call, the promise resolves
98
+ * immediately.
99
+ *
100
+ * If the buffer is already full, the write will be queued and only resolve
101
+ * when delivered. Note: As a fail-safe, there's a limit of queued
102
+ * {@link MAX_WRITES} allowed per channel. The promise will reject if that
103
+ * number is exceeded.
104
+ *
105
+ * Also see {@link Channel.offer}.
106
+ */
22
107
  write(msg: T): Promise<boolean>;
108
+ /**
109
+ * Similar to {@link Channel.write}, but not async and non-blocking. Will
110
+ * only succeed if the channel is writable (i.e. not yet closed/closing) and
111
+ * if a write is immediately possible (without queuing). Returns true, if
112
+ * successful.
113
+ *
114
+ * @param msg
115
+ */
23
116
  offer(msg: T): boolean;
117
+ /**
118
+ * Queues a "race" operation & returns a promise which resolves with the
119
+ * channel itself when the channel becomes readable, but no other queued
120
+ * read operations are waiting (which always have priority). If the channel
121
+ * is already closed, the promise resolves immediately.
122
+ *
123
+ * @remarks
124
+ * This op is used internally by {@link select} to choose a channel to read
125
+ * from next.
126
+ */
24
127
  race(): Promise<Channel<T>>;
128
+ /**
129
+ * Triggers closing of the channel (idempotent). Any queued writes remain
130
+ * readable, but new writes will be ignored.
131
+ *
132
+ * @remarks
133
+ * Whilst there're still values available for reading,
134
+ * {@link Channel.closed} will still return false since the channel state is
135
+ * still "closing", not yet fully "closed".
136
+ */
25
137
  close(): void;
26
138
  /**
27
139
  * Returns true if the channel is principally readable (i.e. not yet
@@ -38,9 +150,15 @@ export declare class Channel<T> implements IChannel<T> {
38
150
  /**
39
151
  * Returns true if the channel is fully closed and no further reads or
40
152
  * writes are possible.
153
+ *
154
+ * @remarks
155
+ * Whilst there're still values available for reading, this will still
156
+ * return false since the channel state is still "closing", not yet fully
157
+ * "closed".
41
158
  */
42
159
  closed(): boolean;
160
+ /** @internal */
161
+ updateQueue(): void;
43
162
  protected deliver(): void;
44
- protected updateQueue(): void;
45
163
  }
46
164
  //# sourceMappingURL=channel.d.ts.map
package/channel.js CHANGED
@@ -4,7 +4,7 @@ import { isPlainObject } from "@thi.ng/checks/is-plain-object";
4
4
  import { illegalState } from "@thi.ng/errors/illegal-state";
5
5
  import { __nextID } from "./idgen.js";
6
6
  const MAX_READS = 1024;
7
- const MAX_QUEUE = 1024;
7
+ const MAX_WRITES = 1024;
8
8
  const STATE_OPEN = 0;
9
9
  const STATE_CLOSING = 1;
10
10
  const STATE_CLOSED = 2;
@@ -35,6 +35,36 @@ class Channel {
35
35
  this.writes = isNumber(buf) ? fifo(buf) : buf;
36
36
  this.id = opts?.id ?? `chan-${__nextID()}`;
37
37
  }
38
+ /**
39
+ * Returns an async iterator of this channel, acting as adapter between the
40
+ * CSP world and the more generic ES async iterables. The iterator stops
41
+ * once the channel has been closed and no further values can be read.
42
+ *
43
+ * @remarks
44
+ * Multiple iterators will compete for new values. To ensure an iterator
45
+ * receives all of the channel's values, you must either ensure there's only
46
+ * a single iterator per channel or feed the channel into a {@link mult}
47
+ * first and create an iterator of a channel obtained via
48
+ * {@link Mult.subscribe}.
49
+ *
50
+ * @example
51
+ * ```ts tangle:../export/channel-iterator.ts
52
+ * import { channel } from "@thi.ng/csp";
53
+ *
54
+ * const chan = channel<number>();
55
+ *
56
+ * (async () => {
57
+ * // implicit iterator conversion of the channel
58
+ * for await(let x of chan) console.log("received", x);
59
+ * console.log("channel closed");
60
+ * })()
61
+ *
62
+ * chan.write(1);
63
+ * chan.write(2);
64
+ * chan.write(3);
65
+ * chan.close();
66
+ * ```
67
+ */
38
68
  async *[Symbol.asyncIterator]() {
39
69
  while (this.state < STATE_CLOSED) {
40
70
  const x = await this.read();
@@ -42,6 +72,21 @@ class Channel {
42
72
  yield x;
43
73
  }
44
74
  }
75
+ /**
76
+ * Attempts to read a value from the channel. The returned promise will
77
+ * block until such value becomes available or if the channel has been
78
+ * closed in the meantime. In that latter case, the promise will resolve to
79
+ * `undefined`.
80
+ *
81
+ * @remarks
82
+ * If a value is already available at the time of the function call, the
83
+ * promise resolves immediately.
84
+ *
85
+ * Note: There's a limit of in-flight {@link MAX_READS} allowed per channel.
86
+ * The promise will reject if that number is exceeded.
87
+ *
88
+ * Also see {@link Channel.poll}.
89
+ */
45
90
  read() {
46
91
  return new Promise((resolve) => {
47
92
  if (!this.readable()) {
@@ -59,6 +104,15 @@ class Channel {
59
104
  this.deliver();
60
105
  });
61
106
  }
107
+ /**
108
+ * Similar to {@link Channel.read}, but not async and non-blocking. Will
109
+ * only succeed if the channel is readable (i.e. not yet closed) and if a
110
+ * value can be read immediately (without waiting). Returns the value or
111
+ * `undefined` if unsuccessful.
112
+ *
113
+ * @remarks
114
+ * Use {@link Channel.closed} to check if the channel is already closed.
115
+ */
62
116
  poll() {
63
117
  const { reads, writes } = this;
64
118
  if (this.readable() && !reads.length && writes.readable()) {
@@ -68,6 +122,25 @@ class Channel {
68
122
  return msg;
69
123
  }
70
124
  }
125
+ /**
126
+ * Attempts to write a new value to the channel and returns a promise
127
+ * indicating success or failure. Depending on buffer capacity & behavior,
128
+ * the returned promise will block until the channel accept new values (i.e.
129
+ * until the next {@link Channel.read}) or if it has been closed in the
130
+ * meantime. In that latter case, the promise will resolve to false.
131
+ *
132
+ * @remarks
133
+ * If the channel's buffer accepts new writes or if a read op is already
134
+ * waiting at the time of the function call, the promise resolves
135
+ * immediately.
136
+ *
137
+ * If the buffer is already full, the write will be queued and only resolve
138
+ * when delivered. Note: As a fail-safe, there's a limit of queued
139
+ * {@link MAX_WRITES} allowed per channel. The promise will reject if that
140
+ * number is exceeded.
141
+ *
142
+ * Also see {@link Channel.offer}.
143
+ */
71
144
  write(msg) {
72
145
  return new Promise((resolve) => {
73
146
  if (!this.writable()) {
@@ -77,7 +150,7 @@ class Channel {
77
150
  const { reads, writes, races, queue } = this;
78
151
  const val = [msg, resolve];
79
152
  if (!(writes.writable() && writes.write(val))) {
80
- queue.length < MAX_QUEUE ? queue.push(val) : illegalState(
153
+ queue.length < MAX_WRITES ? queue.push(val) : illegalState(
81
154
  "max. queue capacity reached, reduce back pressure!"
82
155
  );
83
156
  }
@@ -88,6 +161,14 @@ class Channel {
88
161
  }
89
162
  });
90
163
  }
164
+ /**
165
+ * Similar to {@link Channel.write}, but not async and non-blocking. Will
166
+ * only succeed if the channel is writable (i.e. not yet closed/closing) and
167
+ * if a write is immediately possible (without queuing). Returns true, if
168
+ * successful.
169
+ *
170
+ * @param msg
171
+ */
91
172
  offer(msg) {
92
173
  if (this.writable() && this.writes.writable()) {
93
174
  this.write(msg);
@@ -95,6 +176,16 @@ class Channel {
95
176
  }
96
177
  return false;
97
178
  }
179
+ /**
180
+ * Queues a "race" operation & returns a promise which resolves with the
181
+ * channel itself when the channel becomes readable, but no other queued
182
+ * read operations are waiting (which always have priority). If the channel
183
+ * is already closed, the promise resolves immediately.
184
+ *
185
+ * @remarks
186
+ * This op is used internally by {@link select} to choose a channel to read
187
+ * from next.
188
+ */
98
189
  race() {
99
190
  return new Promise((resolve) => {
100
191
  if (!this.readable()) {
@@ -107,6 +198,15 @@ class Channel {
107
198
  }
108
199
  });
109
200
  }
201
+ /**
202
+ * Triggers closing of the channel (idempotent). Any queued writes remain
203
+ * readable, but new writes will be ignored.
204
+ *
205
+ * @remarks
206
+ * Whilst there're still values available for reading,
207
+ * {@link Channel.closed} will still return false since the channel state is
208
+ * still "closing", not yet fully "closed".
209
+ */
110
210
  close() {
111
211
  if (this.state >= STATE_CLOSING)
112
212
  return;
@@ -141,17 +241,16 @@ class Channel {
141
241
  /**
142
242
  * Returns true if the channel is fully closed and no further reads or
143
243
  * writes are possible.
244
+ *
245
+ * @remarks
246
+ * Whilst there're still values available for reading, this will still
247
+ * return false since the channel state is still "closing", not yet fully
248
+ * "closed".
144
249
  */
145
250
  closed() {
146
251
  return this.state === STATE_CLOSED;
147
252
  }
148
- deliver() {
149
- const { reads, writes } = this;
150
- const [msg, write] = writes.read();
151
- write(true);
152
- reads.shift()(msg);
153
- this.updateQueue();
154
- }
253
+ /** @internal */
155
254
  updateQueue() {
156
255
  const { queue, writes } = this;
157
256
  if (queue.length && writes.writable()) {
@@ -161,10 +260,17 @@ class Channel {
161
260
  this.state = STATE_CLOSED;
162
261
  }
163
262
  }
263
+ deliver() {
264
+ const { reads, writes } = this;
265
+ const [msg, write] = writes.read();
266
+ write(true);
267
+ reads.shift()(msg);
268
+ this.updateQueue();
269
+ }
164
270
  }
165
271
  export {
166
272
  Channel,
167
- MAX_QUEUE,
168
273
  MAX_READS,
274
+ MAX_WRITES,
169
275
  channel
170
276
  };
package/mult.d.ts CHANGED
@@ -1,16 +1,63 @@
1
1
  import type { IClosable, IWriteable } from "./api.js";
2
2
  import { Channel } from "./channel.js";
3
+ /**
4
+ * Syntax sugar for {@link Mult} ctor. Creates a new `Mult` which allows
5
+ * multiple child subscriptions, each receiving the same values written to (or
6
+ * received by) the Mult itself, i.e. it acts as a channel splitter, supporting
7
+ * dynamic subscriptions and unsubscriptions.
8
+ *
9
+ * @remarks
10
+ * If `src` is a channel, it will be used as input. If given a string, a new
11
+ * channel with the given ID will be created (for receiving values).
12
+ *
13
+ * @param arg
14
+ */
3
15
  export declare const mult: <T>(arg?: string | Channel<T>) => Mult<T>;
4
16
  export declare class Mult<T> implements IWriteable<T>, IClosable {
5
17
  protected src: Channel<any>;
6
18
  protected taps: Channel<any>[];
19
+ /**
20
+ * See {@link mult} for reference.
21
+ *
22
+ * @param arg
23
+ */
7
24
  constructor(arg?: string | Channel<T>);
8
25
  writable(): boolean;
9
26
  write(val: T): Promise<boolean>;
10
27
  close(): void;
11
28
  closed(): boolean;
29
+ /**
30
+ * Attaches (and possibly creates) a new subscription channel to receive any
31
+ * values received by the `Mult` itself. Returns it.
32
+ *
33
+ * @remarks
34
+ * The channel can later be detached again via {@link Mult.unsubscribe}.
35
+ *
36
+ * @param ch
37
+ */
12
38
  subscribe(ch?: Channel<T>): Channel<T>;
13
- unsubscribe(ch: Channel<T>): boolean;
39
+ /**
40
+ * Attempts to remove given subscription channel. Returns true if
41
+ * successful. If `close` is true (default), the given channel will also be
42
+ * closed (only if unsubscription was successful).
43
+ *
44
+ * @remarks
45
+ * See {@link Mult.subscribe} for reverse op.
46
+ *
47
+ * @param ch
48
+ * @param close
49
+ */
50
+ unsubscribe(ch: Channel<T>, close?: boolean): boolean;
51
+ /**
52
+ * Removes all child subscription channels and if `close` is true (default)
53
+ * also closes them.
54
+ *
55
+ * @remarks
56
+ * The `Mult` itself will remain active and will continue to accept new
57
+ * subscriptions.
58
+ *
59
+ * @param close
60
+ */
14
61
  unsubscribeAll(close?: boolean): void;
15
62
  protected process(): Promise<void>;
16
63
  }
package/mult.js CHANGED
@@ -4,6 +4,11 @@ const mult = (arg) => new Mult(arg);
4
4
  class Mult {
5
5
  src;
6
6
  taps = [];
7
+ /**
8
+ * See {@link mult} for reference.
9
+ *
10
+ * @param arg
11
+ */
7
12
  constructor(arg) {
8
13
  let id, src;
9
14
  if (typeof arg === "string") {
@@ -26,6 +31,15 @@ class Mult {
26
31
  closed() {
27
32
  return this.src.closed();
28
33
  }
34
+ /**
35
+ * Attaches (and possibly creates) a new subscription channel to receive any
36
+ * values received by the `Mult` itself. Returns it.
37
+ *
38
+ * @remarks
39
+ * The channel can later be detached again via {@link Mult.unsubscribe}.
40
+ *
41
+ * @param ch
42
+ */
29
43
  subscribe(ch) {
30
44
  if (!ch) {
31
45
  ch = new Channel({
@@ -37,14 +51,36 @@ class Mult {
37
51
  this.taps.push(ch);
38
52
  return ch;
39
53
  }
40
- unsubscribe(ch) {
54
+ /**
55
+ * Attempts to remove given subscription channel. Returns true if
56
+ * successful. If `close` is true (default), the given channel will also be
57
+ * closed (only if unsubscription was successful).
58
+ *
59
+ * @remarks
60
+ * See {@link Mult.subscribe} for reverse op.
61
+ *
62
+ * @param ch
63
+ * @param close
64
+ */
65
+ unsubscribe(ch, close = true) {
41
66
  const idx = this.taps.indexOf(ch);
42
67
  if (idx >= 0) {
43
68
  this.taps.splice(idx, 1);
69
+ close && ch.close();
44
70
  return true;
45
71
  }
46
72
  return false;
47
73
  }
74
+ /**
75
+ * Removes all child subscription channels and if `close` is true (default)
76
+ * also closes them.
77
+ *
78
+ * @remarks
79
+ * The `Mult` itself will remain active and will continue to accept new
80
+ * subscriptions.
81
+ *
82
+ * @param close
83
+ */
48
84
  unsubscribeAll(close = true) {
49
85
  if (close) {
50
86
  for (let t of this.taps)
package/ops.d.ts CHANGED
@@ -4,8 +4,8 @@ import { Channel } from "./channel.js";
4
4
  export declare const broadcast: <T>(src: Channel<T>, dest: Channel<T>[], close?: boolean) => Promise<void>;
5
5
  export declare const concat: <T>(dest: IWriteable<T> & IClosable, chans: Iterable<Channel<T>>, close?: boolean) => Promise<void>;
6
6
  /**
7
- * Consumes & collects all future values written to channel `chan` until
8
- * closed or the max. number of values has been collected (whatever comes
7
+ * Consumes & collects all queued and future values written to channel `chan`
8
+ * until closed or the max. number of values has been collected (whatever comes
9
9
  * first). Returns a promise of the result array.
10
10
  *
11
11
  * @param chan
@@ -13,12 +13,55 @@ export declare const concat: <T>(dest: IWriteable<T> & IClosable, chans: Iterabl
13
13
  * @param num
14
14
  */
15
15
  export declare const consume: <T>(chan: Channel<T>, res?: T[], num?: number) => Promise<T[]>;
16
+ /**
17
+ * Consumes all queued and future values written to channel `chan` until closed
18
+ * or the max. number of values has been reached (whatever comes first). Calls
19
+ * `fn` with each value read (presumably for side effects). Returns a void
20
+ * promise which resolves when the consumer is done.
21
+ *
22
+ * @param chan
23
+ * @param fn
24
+ * @param num
25
+ */
16
26
  export declare const consumeWith: <T>(chan: Channel<T>, fn: Fn2<T, Channel<T>, void>, num?: number) => Promise<void>;
27
+ /**
28
+ * Similar to {@link consume}, but only processes any current in-flight writes
29
+ * and returns a promise with an array of their values.
30
+ *
31
+ * @param chan
32
+ */
17
33
  export declare const drain: <T>(chan: Channel<T>) => Promise<Awaited<T>[]>;
34
+ /**
35
+ * Takes an async iterable and returns a new CSP {@link Channel}, which receives
36
+ * all values from `src`. If `close` is true (default), the channel will be
37
+ * automatically closed once the iterable is exhausted.
38
+ *
39
+ * @param src
40
+ * @param close
41
+ */
18
42
  export declare const fromAsyncIterable: <T>(src: AsyncIterable<T>, close?: boolean) => Channel<T>;
19
43
  export declare const merge: <T>(src: Channel<T>[], dest?: Channel<T>, close?: boolean) => Channel<T>;
20
44
  export declare const into: <T, DEST extends IWriteable<T> & IClosable>(chan: DEST, src: Iterable<T> | AsyncIterable<T>, close?: boolean) => Promise<void>;
21
45
  export declare const pipe: <T, DEST extends IWriteable<T> & IClosable>(src: Channel<T>, dest: DEST, close?: boolean) => DEST;
46
+ /**
47
+ * Takes one or more input channels and attempts to read from all of them at
48
+ * once (via {@link Channel.race}, a blocking op). Returns a promise which
49
+ * resolves once one of the inputs becomes available or was closed, selects that
50
+ * channel to read from it and returns tuple of `[value, channel]`.
51
+ *
52
+ * @param input
53
+ * @param xs
54
+ */
22
55
  export declare const select: <T>(input: Channel<T>, ...xs: Channel<T>[]) => Promise<[Maybe<T>, Channel<T>]>;
56
+ /**
57
+ * Returns a new {@link Channel} which will automatically close after `delay`
58
+ * milliseconds.
59
+ *
60
+ * @remarks
61
+ * Intended as utility for enforcing a timeout for {@link select}-style
62
+ * operations.
63
+ *
64
+ * @param delay
65
+ */
23
66
  export declare const timeout: (delay: number) => Channel<any>;
24
67
  //# sourceMappingURL=ops.d.ts.map
package/ops.js CHANGED
@@ -102,6 +102,7 @@ const select = async (input, ...xs) => {
102
102
  if (sel.writes.readable()) {
103
103
  const [msg, write] = sel.writes.read();
104
104
  write(true);
105
+ sel.updateQueue();
105
106
  return [msg, sel];
106
107
  }
107
108
  return [void 0, sel];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/csp",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Primitives & operators for Communicating Sequential Processes based on async/await and async iterables",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -106,5 +106,5 @@
106
106
  "status": "beta",
107
107
  "year": 2016
108
108
  },
109
- "gitHead": "0bec55821066c18eb977a7eabd42c0bb2b096d98\n"
109
+ "gitHead": "85861eb5760fd5c145dbd2d032b6f03400d6653e\n"
110
110
  }
package/pubsub.d.ts CHANGED
@@ -2,12 +2,31 @@ import type { IObjectOf } from "@thi.ng/api";
2
2
  import type { IClosable, IWriteable, TopicFn } from "./api.js";
3
3
  import { Channel } from "./channel.js";
4
4
  import { Mult } from "./mult.js";
5
+ /**
6
+ * Syntax sugar for {@link PubSub} ctor. Creates a new `PubSub` which allows
7
+ * multiple child subscriptions, based on given topic function.
8
+ *
9
+ * @remarks
10
+ * The topic function will be called for each received value and its result is
11
+ * used to determine which child subscription should receive the value. New
12
+ * topic (un)subscriptions can be created dynamically via
13
+ * {@link PubSub.subscribeTopic} and {@link PubSub.unsubscribeTopic} or
14
+ * {@link PubSub.unsubscribeAll}. Each topic subscription is a {@link Mult},
15
+ * which itself allows for multiple child subscriptions.
16
+ *
17
+ * @param fn
18
+ */
5
19
  export declare function pubsub<T>(fn: TopicFn<T>): PubSub<T>;
6
20
  export declare function pubsub<T>(src: Channel<T>, fn: TopicFn<T>): PubSub<T>;
7
21
  export declare class PubSub<T> implements IWriteable<T>, IClosable {
8
22
  protected src: Channel<T>;
9
23
  protected fn: TopicFn<T>;
10
24
  protected topics: IObjectOf<Mult<any>>;
25
+ /**
26
+ * See {@link pubsub} for reference.
27
+ *
28
+ * @param fn
29
+ */
11
30
  constructor(fn: TopicFn<T>);
12
31
  constructor(src: Channel<T>, fn: TopicFn<T>);
13
32
  writable(): boolean;
@@ -22,7 +41,25 @@ export declare class PubSub<T> implements IWriteable<T>, IClosable {
22
41
  * @param id - topic id
23
42
  */
24
43
  subscribeTopic<S extends T = T>(id: string): Channel<S>;
25
- unsubscribeTopic<S extends T = T>(id: string, ch: Channel<S>): boolean;
44
+ /**
45
+ * Attempts to remove a subscription channel for given topic `id`. Returns
46
+ * true if successful. If `close` is true (default), the given channel will
47
+ * also be closed (only if unsubscription was successful).
48
+ *
49
+ * @remarks
50
+ * See {@link Mult.subscribe} for reverse op.
51
+ *
52
+ * @param id
53
+ * @param ch
54
+ * @param close
55
+ */
56
+ unsubscribeTopic<S extends T = T>(id: string, ch: Channel<S>, close?: boolean): boolean;
57
+ /**
58
+ * Removes all child subscription channels for given topic `id` and if
59
+ * `close` is true (default) also closes them.
60
+ *
61
+ * @param close
62
+ */
26
63
  unsubscribeAll(id: string, close?: boolean): void;
27
64
  protected process(): Promise<void>;
28
65
  }
package/pubsub.js CHANGED
@@ -50,10 +50,28 @@ class PubSub {
50
50
  }
51
51
  return topic.subscribe();
52
52
  }
53
- unsubscribeTopic(id, ch) {
53
+ /**
54
+ * Attempts to remove a subscription channel for given topic `id`. Returns
55
+ * true if successful. If `close` is true (default), the given channel will
56
+ * also be closed (only if unsubscription was successful).
57
+ *
58
+ * @remarks
59
+ * See {@link Mult.subscribe} for reverse op.
60
+ *
61
+ * @param id
62
+ * @param ch
63
+ * @param close
64
+ */
65
+ unsubscribeTopic(id, ch, close = true) {
54
66
  const topic = this.topics[id];
55
- return topic?.unsubscribe(ch) ?? false;
67
+ return topic?.unsubscribe(ch, close) ?? false;
56
68
  }
69
+ /**
70
+ * Removes all child subscription channels for given topic `id` and if
71
+ * `close` is true (default) also closes them.
72
+ *
73
+ * @param close
74
+ */
57
75
  unsubscribeAll(id, close = true) {
58
76
  const topic = this.topics[id];
59
77
  topic?.unsubscribeAll(close);