@thi.ng/fibers 0.1.1 → 0.2.1

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**: 2023-08-05T08:00:59Z
3
+ - **Last updated**: 2023-08-10T12:25:09Z
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,33 @@ 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
+ ## [0.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/fibers@0.2.0) (2023-08-10)
13
+
14
+ #### 🚀 Features
15
+
16
+ - ensure no pre-existing parent in Fiber.fork() ([612adf9](https://github.com/thi-ng/umbrella/commit/612adf9))
17
+ - add auto terminate option, update child handling ([e59063d](https://github.com/thi-ng/umbrella/commit/e59063d))
18
+ - add shuffle() operator, update deps ([b3efa79](https://github.com/thi-ng/umbrella/commit/b3efa79))
19
+ - add CSP primitives ([d8fa8ce](https://github.com/thi-ng/umbrella/commit/d8fa8ce))
20
+ - add fiber-based Channel class
21
+ - add various buffer implementations
22
+ - fifo
23
+ - lifo
24
+ - sliding
25
+ - dropping
26
+
27
+ #### ⏱ Performance improvements
28
+
29
+ - rewrite FIFOBuffer as ring buffer ([ebac714](https://github.com/thi-ng/umbrella/commit/ebac714))
30
+ - use old impl as basis for LIFOBuffer only
31
+ - update other buffer types to use new ring buffer impl
32
+ - add min. capacity assertion in ctors
33
+
34
+ #### ♻️ Refactoring
35
+
36
+ - minor update all() ([52836a8](https://github.com/thi-ng/umbrella/commit/52836a8))
37
+ - update arg types in various ops ([cb3c253](https://github.com/thi-ng/umbrella/commit/cb3c253))
38
+
12
39
  ### [0.1.1](https://github.com/thi-ng/umbrella/tree/@thi.ng/fibers@0.1.1) (2023-08-05)
13
40
 
14
41
  #### 🩹 Bug fixes
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <!-- This file is generated - DO NOT EDIT! -->
2
+ <!-- Please see: https://github.com/thi-ng/umbrella/blob/develop/CONTRIBUTING.md#changes-to-readme-files -->
2
3
 
3
- # ![@thi.ng/fibers](https://media.thi.ng/umbrella/banners-20220914/thing-fibers.svg?d08288cb)
4
+ # ![@thi.ng/fibers](https://media.thi.ng/umbrella/banners-20230807/thing-fibers.svg?d08288cb)
4
5
 
5
6
  [![npm version](https://img.shields.io/npm/v/@thi.ng/fibers.svg)](https://www.npmjs.com/package/@thi.ng/fibers)
6
7
  ![npm downloads](https://img.shields.io/npm/dm/@thi.ng/fibers.svg)
@@ -13,6 +14,10 @@ This project is part of the
13
14
  - [Basic usage](#basic-usage)
14
15
  - [Fiber operators](#fiber-operators)
15
16
  - [Composition via transducers](#composition-via-transducers)
17
+ - [CSP primitives (Communicating Sequential Processes)](#csp-primitives-communicating-sequential-processes)
18
+ - [Buffering behaviors](#buffering-behaviors)
19
+ - [Channels](#channels)
20
+ - [CSP ping/pong example](#csp-pingpong-example)
16
21
  - [Status](#status)
17
22
  - [Installation](#installation)
18
23
  - [Dependencies](#dependencies)
@@ -114,6 +119,7 @@ The following operators act as basic composition helpers to construct more elabo
114
119
  - [`forkAll`](https://docs.thi.ng/umbrella/fibers/classes/Fiber.html#forkAll): create & attach multiple child processes
115
120
  - [`join`](https://docs.thi.ng/umbrella/fibers/classes/Fiber.html#join): wait for all child processes to complete
116
121
  - [`sequence`](https://docs.thi.ng/umbrella/fibers/functions/sequence.html): execute fibers in sequence
122
+ - [`shuffle`](https://docs.thi.ng/umbrella/fibers/functions/shuffle.html): execute fibers in constantly randomized order
117
123
  - [`timeSlice`](https://docs.thi.ng/umbrella/fibers/functions/timeSlice.html): execute fiber in batches of N milliseconds
118
124
  - [`until`](https://docs.thi.ng/umbrella/fibers/functions/until.html): wait until predicate is truthy
119
125
  - [`untilEvent`](https://docs.thi.ng/umbrella/fibers/functions/untilEvent.html): wait until event occurs
@@ -173,6 +179,177 @@ sequence(
173
179
  // part 7
174
180
  ```
175
181
 
182
+ ### CSP primitives (Communicating Sequential Processes)
183
+
184
+ References:
185
+
186
+ - [Wikipedia](https://en.wikipedia.org/wiki/Communicating_sequential_processes)
187
+ - [Communicating Sequential Processes, C.A.R.
188
+ Hoare](https://dl.acm.org/doi/pdf/10.1145/359576.359585)
189
+
190
+ In addition to the operators above, the basic fiber implementation can also be
191
+ used to construct other types of primitives, like those required for
192
+ channel-based communication between processes, as proposed by Tony Hoare. The
193
+ package includes a fiber-based read/write channel primitive which can be
194
+ customized with different buffer implementations to control blocking behaviors
195
+ and backpressure handling (aka attempting to write faster to a channel than
196
+ values are being read, essentially a memory management issue).
197
+
198
+ #### Buffering behaviors
199
+
200
+ The following channel buffer types/behaviors are included, all accepting a max.
201
+ capacity and all implementing the
202
+ [IReadWriteBuffer](https://docs.thi.ng/umbrella/fibers/interfaces/IReadWriteBuffer.html)
203
+ interface required by the channel:
204
+
205
+ - [`fifo`](https://docs.thi.ng/umbrella/fibers/functions/fifo.html): First in,
206
+ first out ring buffer. Writes to the channel will start blocking once the
207
+ buffer's capacity is reached, otherwise complete immediately. Likewise,
208
+ channel reads are non-blocking whilst there're more buffered values available.
209
+ Reads will only block if the buffer is empty.
210
+ - [`lifo`](https://docs.thi.ng/umbrella/fibers/functions/lifo.html): Last in,
211
+ first out. Read/write behavior is mostly the same as with `fifo`, with the
212
+ important difference, that (as the name indicates), the last value written
213
+ will be the first value read (i.e. stack behavior).
214
+ - [`sliding`](https://docs.thi.ng/umbrella/fibers/functions/sliding.html):
215
+ Sliding window ring buffer. Writes to the channel are **never** blocking! Once
216
+ the buffer's capacity is reached, a new write will first expunge the oldest
217
+ buffered value (similar to LRU cache behavior). Read behavior is the same as
218
+ for `fifo`.
219
+ - [`dropping`](https://docs.thi.ng/umbrella/fibers/functions/dropping.html):
220
+ Dropping value ring buffer. Writes to the channel are **never** blocking!
221
+ Whilst the buffer's capacity is reached, new writes will be silently ignored.
222
+ Read behavior is the same as for `fifo`.
223
+
224
+ #### Channels
225
+
226
+ As mentioned previously,
227
+ [channels](https://docs.thi.ng/umbrella/fibers/functions/channel.html) and their
228
+ [read](https://docs.thi.ng/umbrella/fibers/classes/Channel.html#read),
229
+ [write](https://docs.thi.ng/umbrella/fibers/classes/Channel.html#write) and
230
+ [close](https://docs.thi.ng/umbrella/fibers/classes/Channel.html#close)
231
+ operations are the key building blocks for CSP. In this fiber-based
232
+ implementation, all channel operations are executed in individual fibers to deal
233
+ with the potential blocking behaviors. This is demonstrated in the simple
234
+ example below.
235
+
236
+ In general, due to fibers not being true multi-threaded processes (all are
237
+ executed in the single thread of the JS engine), any number of fibers can read
238
+ or write to a channel.
239
+
240
+ Channels can be created like so:
241
+
242
+ ```ts
243
+ // create unbuffered channel with single value capacity
244
+ const chan1 = channel();
245
+
246
+ // create channel with a FIFO buffer, capacity: 2 values
247
+ const chan2 = channel(2);
248
+
249
+ // create channel with a sliding window buffer and custom ID & logger
250
+ const chan3 = channel(
251
+ sliding(3),
252
+ { id: "main", logger: new ConsoleLogger("chan") }
253
+ );
254
+ ```
255
+
256
+ #### CSP ping/pong example
257
+
258
+ ```ts tangle:export/pingpong.ts
259
+ import { channel, fiber, wait } from "@thi.ng/fibers";
260
+ import { ConsoleLogger } from "@thi.ng/logger";
261
+
262
+ // create idle main fiber with custom options
263
+ const app = fiber(null, {
264
+ id: "main",
265
+ logger: new ConsoleLogger("app"),
266
+ // if true, fiber automatically terminates once all child fibers are done
267
+ terminate: true,
268
+ });
269
+
270
+ // create CSP channels (w/ default config)
271
+ const ping = channel<number>();
272
+ const pong = channel<number>();
273
+
274
+ // attach ping/pong child processes
275
+ app.forkAll(
276
+ // ping
277
+ function* () {
278
+ while (ping.readable()) {
279
+ // blocking read op
280
+ // (waits until value is available in `ping` channel)
281
+ const x = yield* ping.read();
282
+ // check if channel was closed meanwhile
283
+ if (x === undefined) break;
284
+ console.log("PING", x);
285
+ // possibly blocking (in general) write op to other channel
286
+ yield* pong.write(x);
287
+ // slowdown
288
+ yield* wait(100);
289
+ }
290
+ },
291
+ // pong (very similar)
292
+ function* () {
293
+ while (pong.readable()) {
294
+ const x = yield* pong.read();
295
+ if (x === undefined) break;
296
+ console.log("PONG", x);
297
+ // trigger next iteration
298
+ yield* ping.write(x + 1);
299
+ }
300
+ },
301
+ // channel managment
302
+ function* () {
303
+ // kickoff ping/pong
304
+ yield* ping.write(0);
305
+ yield* wait(1000);
306
+ // wait for both channels to close
307
+ yield* ping.close();
308
+ yield* pong.close();
309
+ }
310
+ );
311
+ app.run();
312
+
313
+ // [DEBUG] app: forking fib-0
314
+ // [DEBUG] app: forking fib-1
315
+ // [DEBUG] app: forking fib-2
316
+ // [DEBUG] app: running main...
317
+ // [DEBUG] app: init main
318
+ // [DEBUG] app: init fib-0
319
+ // [DEBUG] app: init fib-1
320
+ // [DEBUG] app: init fib-2
321
+ // PING 0
322
+ // PONG 0
323
+ // PING 1
324
+ // PONG 1
325
+ // ...
326
+ // PING 9
327
+ // PONG 9
328
+ // [DEBUG] app: done fib-2 undefined
329
+ // [DEBUG] app: deinit fib-2
330
+ // [DEBUG] app: done fib-1 undefined
331
+ // [DEBUG] app: deinit fib-1
332
+ // [DEBUG] app: done fib-0 undefined
333
+ // [DEBUG] app: deinit fib-0
334
+ // [DEBUG] app: cancel main
335
+ // [DEBUG] app: deinit main
336
+ ```
337
+
338
+ Additional CSP operators are planned, but since everything here is based on
339
+ fibers, the various channel operations can be already combined with the
340
+ [available fiber operators/combinators](#fiber-operators)...
341
+
342
+ For example, a channel read or write op can be combined with a timeout:
343
+
344
+ ```ts
345
+ const res = (yield* withTimeout(chan.read(), 1000)).deref();
346
+ if (res !== undefined) {
347
+ console.log("read value", x);
348
+ } else {
349
+ console.log("read timeout");
350
+ }
351
+ ```
352
+
176
353
  ## Status
177
354
 
178
355
  **ALPHA** - bleeding edge / work-in-progress
@@ -199,14 +376,18 @@ For Node.js REPL:
199
376
  const fibers = await import("@thi.ng/fibers");
200
377
  ```
201
378
 
202
- Package sizes (brotli'd, pre-treeshake): ESM: 1.88 KB
379
+ Package sizes (brotli'd, pre-treeshake): ESM: 2.25 KB
203
380
 
204
381
  ## Dependencies
205
382
 
206
383
  - [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
384
+ - [@thi.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays)
207
385
  - [@thi.ng/bench](https://github.com/thi-ng/umbrella/tree/develop/packages/bench)
208
386
  - [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
387
+ - [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
388
+ - [@thi.ng/idgen](https://github.com/thi-ng/umbrella/tree/develop/packages/idgen)
209
389
  - [@thi.ng/logger](https://github.com/thi-ng/umbrella/tree/develop/packages/logger)
390
+ - [@thi.ng/random](https://github.com/thi-ng/umbrella/tree/develop/packages/random)
210
391
 
211
392
  ## Usage examples
212
393
 
package/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Fn, Fn2, IIDGen } from "@thi.ng/api";
1
+ import type { Fn, Fn2, IClear, IIDGen } from "@thi.ng/api";
2
2
  import type { ILogger } from "@thi.ng/logger";
3
3
  import type { Fiber } from "./fiber.js";
4
4
  export type FiberFactory<T = any> = (f: Fiber<T>) => Generator<unknown, T, unknown>;
@@ -22,6 +22,15 @@ export interface FiberOpts {
22
22
  * @internal
23
23
  */
24
24
  parent: Fiber;
25
+ /**
26
+ * If true (default: false), the fiber cancels itself once it has no more
27
+ * children. See {@link Fiber.cancel}.
28
+ *
29
+ * @remarks
30
+ * When enabling this option, ensure that the fiber has child processes
31
+ * attached **before** execution!
32
+ */
33
+ terminate: boolean;
25
34
  /**
26
35
  * User init handler
27
36
  */
@@ -59,4 +68,27 @@ export declare const EVENT_FIBER_CANCELED = "fiber-canceled";
59
68
  */
60
69
  export declare const EVENT_FIBER_ERROR = "fiber-error";
61
70
  export type FiberEventType = typeof EVENT_FIBER_DONE | typeof EVENT_FIBER_CANCELED | typeof EVENT_FIBER_ERROR | "*";
71
+ export interface IReadBuffer<T> {
72
+ /**
73
+ * Returns true iff the buffer has at least one value available for reading.
74
+ */
75
+ readable(): boolean;
76
+ /**
77
+ * Unguarded read operation. Assumes the caller checked
78
+ * {@link IReadBuffer.readable} immediately before. Returns next value from
79
+ * buffer.
80
+ */
81
+ read(): T;
82
+ }
83
+ export interface IReadWriteBuffer<T> extends IReadBuffer<T>, IClear {
84
+ /**
85
+ * Returns true iff the buffer has at least one slot available for writing.
86
+ */
87
+ writable(): boolean;
88
+ /**
89
+ * Unguarded write operation. Assumes the caller checked
90
+ * {@link IReadWriteBuffer.writable} immediately before.
91
+ */
92
+ write(x: T): void;
93
+ }
62
94
  //# sourceMappingURL=api.d.ts.map
package/csp.d.ts ADDED
@@ -0,0 +1,193 @@
1
+ import type { FiberOpts, IReadWriteBuffer } from "./api.js";
2
+ import { Fiber } from "./fiber.js";
3
+ declare const STATE_OPEN = 0;
4
+ declare const STATE_CLOSING = 1;
5
+ declare const STATE_CLOSED = 2;
6
+ type CSPState = typeof STATE_OPEN | typeof STATE_CLOSING | typeof STATE_CLOSED;
7
+ /**
8
+ * Fiber-based CSP channel implementation, supporting any
9
+ * {@link IReadWriteBuffer} implementation to customize read/write behaviors
10
+ * (and ordering). By default uses a single value {@link fifo} buffer impl.
11
+ */
12
+ export declare class Channel<T> {
13
+ opts?: Partial<FiberOpts> | undefined;
14
+ protected buffer: IReadWriteBuffer<T>;
15
+ protected state: CSPState;
16
+ constructor(buffer?: IReadWriteBuffer<T> | number, opts?: Partial<FiberOpts> | undefined);
17
+ /**
18
+ * Returns a new fiber which attempts to read a value from the channel and
19
+ * "blocks" until that value is available. Unless the channel has meanwhile
20
+ * been closed, the fiber returns the read value (otherwise: `undefined`).
21
+ *
22
+ * @remarks
23
+ * Depending on chosen back buffer behavior/implementation and
24
+ * {@link Channel.close}, read requests might still be successful whilst the
25
+ * channel is closing and there're still buffered values.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const val = yield* chan.read();
30
+ * ```
31
+ */
32
+ read(): Fiber<T | undefined>;
33
+ /**
34
+ * Returns a new fiber which attempts to write the given `value` to the
35
+ * channel and "blocks" until channel is writable (which depends on the
36
+ * channel's buffer implementation).
37
+ *
38
+ * @remarks
39
+ * Once the channel has been closed (or still is closing, see
40
+ * {@link Channel.close}), all write requests will be silently ignored.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * yield* chan.write(23);
45
+ * ```
46
+ */
47
+ write(val: T): Fiber<any>;
48
+ /**
49
+ * Returns new fiber which closes the channel. By default this op will defer
50
+ * the full closing until all buffered values have been read (by another
51
+ * fiber), however any writes will already become unavailable/ignored even
52
+ * at this stage (also see {@link Channel.write}). If `wait=false`, the
53
+ * channel will be closed immediately, the backing buffered cleared and any
54
+ * in-flight reads or writes will be canceled.
55
+ *
56
+ * @param wait
57
+ */
58
+ close(wait?: boolean): Fiber<any>;
59
+ /**
60
+ * Returns true if the channel is principally readable (i.e. not yet
61
+ * closed), however there might not be any values available yet and reads
62
+ * might block.
63
+ */
64
+ readable(): boolean;
65
+ /**
66
+ * Returns true if the channel is principally writable (i.e. not closing or
67
+ * closed), however depending on buffer behavior the channel might not yet
68
+ * accept new values and writes might block.
69
+ */
70
+ writable(): boolean;
71
+ /**
72
+ * Returns true if the channel is fully closed and no further reads or
73
+ * writes are possible.
74
+ */
75
+ closed(): boolean;
76
+ }
77
+ /**
78
+ * Functional syntax sugar for {@link Channel} ctor, using provided backing
79
+ * buffer and shared options for all fibers returned by the channel for its
80
+ * various operations.
81
+ *
82
+ * @remarks
83
+ * If `buffer` is given as number, a {@link fifo} buffer of given capacity will
84
+ * be used.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * // create unbuffered channel with single value capacity
89
+ * const chan = channel();
90
+ *
91
+ * // create channel with a fixed buffer capacity of 3 values
92
+ * const chan2 = channel(3);
93
+ *
94
+ * // create channel with a sliding window buffer and custom ID & logger
95
+ * const chan3 = channel(
96
+ * sliding(3),
97
+ * { id: "main", logger: new ConsoleLogger("chan") }
98
+ * );
99
+ * ```
100
+ *
101
+ * @param buffer
102
+ * @param opts
103
+ */
104
+ export declare const channel: <T>(buffer?: number | IReadWriteBuffer<T> | undefined, opts?: Partial<FiberOpts>) => Channel<T>;
105
+ /**
106
+ * First-in, first-out ring buffer implementation for use with {@link Channel}.
107
+ */
108
+ export declare class FIFOBuffer<T> implements IReadWriteBuffer<T> {
109
+ protected buf: (T | undefined)[];
110
+ protected rpos: number;
111
+ protected wpos: number;
112
+ constructor(cap?: number);
113
+ clear(): void;
114
+ readable(): boolean;
115
+ read(): NonNullable<T>;
116
+ writable(): boolean;
117
+ write(x: T): void;
118
+ }
119
+ /**
120
+ * Returns a {@link FIFOBuffer} ring buffer with given capacity for use with
121
+ * {@link channel}.
122
+ *
123
+ * @remarks
124
+ * With this implementation, writes to the channel will only start blocking once
125
+ * the buffer's capacity is reached, otherwise complete immediately. Likewise,
126
+ * channel reads are non-blocking whilst there're more buffered values
127
+ * available. Reads will only block if the buffer is empty.
128
+ *
129
+ * Also see {@link lifo}.
130
+ *
131
+ * @param cap
132
+ */
133
+ export declare const fifo: <T>(cap: number) => FIFOBuffer<T>;
134
+ export declare class LIFOBuffer<T> implements IReadWriteBuffer<T> {
135
+ protected cap: number;
136
+ protected buf: T[];
137
+ constructor(cap?: number);
138
+ clear(): void;
139
+ readable(): boolean;
140
+ read(): NonNullable<T>;
141
+ writable(): boolean;
142
+ write(x: T): void;
143
+ }
144
+ /**
145
+ * Returns a {@link LIFOBuffer} with given capacity for use with
146
+ * {@link channel}.
147
+ *
148
+ * @remarks
149
+ * Read/write behavior is mostly the same as with {@link fifo}, with the
150
+ * important difference, that (as the name indicates), the last value written
151
+ * will be the first value read (i.e. stack behavior).
152
+ *
153
+ * @param cap
154
+ */
155
+ export declare const lifo: <T>(cap: number) => LIFOBuffer<T>;
156
+ export declare class SlidingBuffer<T> extends FIFOBuffer<T> {
157
+ writable(): boolean;
158
+ write(x: T): void;
159
+ }
160
+ /**
161
+ * Returns a {@link SlidingBuffer} with given capacity for use with
162
+ * {@link channel}.
163
+ *
164
+ * @remarks
165
+ * With this implementation, writes to the channel are **never** blocking! Once
166
+ * the buffer's capacity is reached, a new write will first expunge the oldest
167
+ * buffered value. Read behavior is the same as for {@link fifo}.
168
+ *
169
+ * Also see {@link dropping} for alternative behavior.
170
+ *
171
+ * @param cap
172
+ */
173
+ export declare const sliding: <T>(cap: number) => SlidingBuffer<T>;
174
+ export declare class DroppingBuffer<T> extends FIFOBuffer<T> {
175
+ writable(): boolean;
176
+ write(x: T): void;
177
+ }
178
+ /**
179
+ * Returns a {@link DroppingBuffer} with given capacity for use with
180
+ * {@link channel}.
181
+ *
182
+ * @remarks
183
+ * With this implementation, writes to the channel are **never** blocking!
184
+ * Whilst the buffer's capacity is reached, new writes will be silently ignored.
185
+ * Read behavior is the same as for {@link fifo}.
186
+ *
187
+ * Also see {@link sliding} for alternative behavior.
188
+ *
189
+ * @param cap
190
+ */
191
+ export declare const dropping: <T>(cap: number) => DroppingBuffer<T>;
192
+ export {};
193
+ //# sourceMappingURL=csp.d.ts.map
package/csp.js ADDED
@@ -0,0 +1,286 @@
1
+ import { isNumber } from "@thi.ng/checks";
2
+ import { assert } from "@thi.ng/errors/assert";
3
+ import { Fiber, fiber } from "./fiber.js";
4
+ const STATE_OPEN = 0;
5
+ const STATE_CLOSING = 1;
6
+ const STATE_CLOSED = 2;
7
+ /**
8
+ * Fiber-based CSP channel implementation, supporting any
9
+ * {@link IReadWriteBuffer} implementation to customize read/write behaviors
10
+ * (and ordering). By default uses a single value {@link fifo} buffer impl.
11
+ */
12
+ export class Channel {
13
+ constructor(buffer = 1, opts) {
14
+ this.opts = opts;
15
+ this.state = STATE_OPEN;
16
+ this.buffer = isNumber(buffer) ? new FIFOBuffer(buffer) : buffer;
17
+ }
18
+ /**
19
+ * Returns a new fiber which attempts to read a value from the channel and
20
+ * "blocks" until that value is available. Unless the channel has meanwhile
21
+ * been closed, the fiber returns the read value (otherwise: `undefined`).
22
+ *
23
+ * @remarks
24
+ * Depending on chosen back buffer behavior/implementation and
25
+ * {@link Channel.close}, read requests might still be successful whilst the
26
+ * channel is closing and there're still buffered values.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const val = yield* chan.read();
31
+ * ```
32
+ */
33
+ read() {
34
+ const $this = this;
35
+ return fiber(function* (ctx) {
36
+ while ($this.readable()) {
37
+ // wait until channel is readable
38
+ if ($this.buffer.readable()) {
39
+ const val = $this.buffer.read();
40
+ ctx.logger?.debug("read", val);
41
+ return val;
42
+ }
43
+ else if ($this.state === STATE_CLOSING) {
44
+ return;
45
+ }
46
+ yield;
47
+ }
48
+ }, this.opts);
49
+ }
50
+ /**
51
+ * Returns a new fiber which attempts to write the given `value` to the
52
+ * channel and "blocks" until channel is writable (which depends on the
53
+ * channel's buffer implementation).
54
+ *
55
+ * @remarks
56
+ * Once the channel has been closed (or still is closing, see
57
+ * {@link Channel.close}), all write requests will be silently ignored.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * yield* chan.write(23);
62
+ * ```
63
+ */
64
+ write(val) {
65
+ const $this = this;
66
+ return fiber(function* (ctx) {
67
+ while ($this.writable()) {
68
+ // wait until channel is writable
69
+ if ($this.buffer.writable()) {
70
+ ctx.logger?.debug("write", val);
71
+ $this.buffer.write(val);
72
+ return;
73
+ }
74
+ yield;
75
+ }
76
+ }, this.opts);
77
+ }
78
+ /**
79
+ * Returns new fiber which closes the channel. By default this op will defer
80
+ * the full closing until all buffered values have been read (by another
81
+ * fiber), however any writes will already become unavailable/ignored even
82
+ * at this stage (also see {@link Channel.write}). If `wait=false`, the
83
+ * channel will be closed immediately, the backing buffered cleared and any
84
+ * in-flight reads or writes will be canceled.
85
+ *
86
+ * @param wait
87
+ */
88
+ close(wait = true) {
89
+ const $this = this;
90
+ return fiber(function* (ctx) {
91
+ if ($this.state >= STATE_CLOSING)
92
+ return;
93
+ if (wait) {
94
+ ctx.logger?.debug("waiting to close...");
95
+ $this.state = STATE_CLOSING;
96
+ while ($this.buffer.readable())
97
+ yield;
98
+ }
99
+ $this.state = STATE_CLOSED;
100
+ $this.buffer.clear();
101
+ ctx.logger?.debug("channel closed");
102
+ }, this.opts);
103
+ }
104
+ /**
105
+ * Returns true if the channel is principally readable (i.e. not yet
106
+ * closed), however there might not be any values available yet and reads
107
+ * might block.
108
+ */
109
+ readable() {
110
+ return this.state <= STATE_CLOSING;
111
+ }
112
+ /**
113
+ * Returns true if the channel is principally writable (i.e. not closing or
114
+ * closed), however depending on buffer behavior the channel might not yet
115
+ * accept new values and writes might block.
116
+ */
117
+ writable() {
118
+ return this.state === STATE_OPEN;
119
+ }
120
+ /**
121
+ * Returns true if the channel is fully closed and no further reads or
122
+ * writes are possible.
123
+ */
124
+ closed() {
125
+ return this.state === STATE_CLOSED;
126
+ }
127
+ }
128
+ /**
129
+ * Functional syntax sugar for {@link Channel} ctor, using provided backing
130
+ * buffer and shared options for all fibers returned by the channel for its
131
+ * various operations.
132
+ *
133
+ * @remarks
134
+ * If `buffer` is given as number, a {@link fifo} buffer of given capacity will
135
+ * be used.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * // create unbuffered channel with single value capacity
140
+ * const chan = channel();
141
+ *
142
+ * // create channel with a fixed buffer capacity of 3 values
143
+ * const chan2 = channel(3);
144
+ *
145
+ * // create channel with a sliding window buffer and custom ID & logger
146
+ * const chan3 = channel(
147
+ * sliding(3),
148
+ * { id: "main", logger: new ConsoleLogger("chan") }
149
+ * );
150
+ * ```
151
+ *
152
+ * @param buffer
153
+ * @param opts
154
+ */
155
+ export const channel = (buffer, opts) => new Channel(buffer, opts);
156
+ /**
157
+ * First-in, first-out ring buffer implementation for use with {@link Channel}.
158
+ */
159
+ export class FIFOBuffer {
160
+ constructor(cap = 1) {
161
+ this.rpos = 0;
162
+ this.wpos = 0;
163
+ assert(cap >= 1, `capacity must be >= 1`);
164
+ this.buf = new Array(cap + 1);
165
+ }
166
+ clear() {
167
+ this.buf.fill(undefined);
168
+ }
169
+ readable() {
170
+ return this.rpos !== this.wpos;
171
+ }
172
+ read() {
173
+ const { buf, rpos } = this;
174
+ const val = buf[rpos];
175
+ buf[rpos] = undefined;
176
+ this.rpos = (rpos + 1) % buf.length;
177
+ return val;
178
+ }
179
+ writable() {
180
+ return (this.wpos + 1) % this.buf.length !== this.rpos;
181
+ }
182
+ write(x) {
183
+ const { buf, wpos } = this;
184
+ buf[wpos] = x;
185
+ this.wpos = (wpos + 1) % buf.length;
186
+ }
187
+ }
188
+ /**
189
+ * Returns a {@link FIFOBuffer} ring buffer with given capacity for use with
190
+ * {@link channel}.
191
+ *
192
+ * @remarks
193
+ * With this implementation, writes to the channel will only start blocking once
194
+ * the buffer's capacity is reached, otherwise complete immediately. Likewise,
195
+ * channel reads are non-blocking whilst there're more buffered values
196
+ * available. Reads will only block if the buffer is empty.
197
+ *
198
+ * Also see {@link lifo}.
199
+ *
200
+ * @param cap
201
+ */
202
+ export const fifo = (cap) => new FIFOBuffer(cap);
203
+ export class LIFOBuffer {
204
+ constructor(cap = 1) {
205
+ this.cap = cap;
206
+ this.buf = [];
207
+ assert(cap >= 1, `capacity must be >= 1`);
208
+ }
209
+ clear() {
210
+ this.buf.length = 0;
211
+ }
212
+ readable() {
213
+ return this.buf.length > 0;
214
+ }
215
+ read() {
216
+ return this.buf.pop();
217
+ }
218
+ writable() {
219
+ return this.buf.length < this.cap;
220
+ }
221
+ write(x) {
222
+ this.buf.push(x);
223
+ }
224
+ }
225
+ /**
226
+ * Returns a {@link LIFOBuffer} with given capacity for use with
227
+ * {@link channel}.
228
+ *
229
+ * @remarks
230
+ * Read/write behavior is mostly the same as with {@link fifo}, with the
231
+ * important difference, that (as the name indicates), the last value written
232
+ * will be the first value read (i.e. stack behavior).
233
+ *
234
+ * @param cap
235
+ */
236
+ export const lifo = (cap) => new LIFOBuffer(cap);
237
+ export class SlidingBuffer extends FIFOBuffer {
238
+ writable() {
239
+ return true;
240
+ }
241
+ write(x) {
242
+ if (!super.writable()) {
243
+ const { buf, rpos } = this;
244
+ buf[rpos] = undefined;
245
+ this.rpos = (rpos + 1) % buf.length;
246
+ }
247
+ super.write(x);
248
+ }
249
+ }
250
+ /**
251
+ * Returns a {@link SlidingBuffer} with given capacity for use with
252
+ * {@link channel}.
253
+ *
254
+ * @remarks
255
+ * With this implementation, writes to the channel are **never** blocking! Once
256
+ * the buffer's capacity is reached, a new write will first expunge the oldest
257
+ * buffered value. Read behavior is the same as for {@link fifo}.
258
+ *
259
+ * Also see {@link dropping} for alternative behavior.
260
+ *
261
+ * @param cap
262
+ */
263
+ export const sliding = (cap) => new SlidingBuffer(cap);
264
+ export class DroppingBuffer extends FIFOBuffer {
265
+ writable() {
266
+ return true;
267
+ }
268
+ write(x) {
269
+ if (super.writable())
270
+ super.write(x);
271
+ }
272
+ }
273
+ /**
274
+ * Returns a {@link DroppingBuffer} with given capacity for use with
275
+ * {@link channel}.
276
+ *
277
+ * @remarks
278
+ * With this implementation, writes to the channel are **never** blocking!
279
+ * Whilst the buffer's capacity is reached, new writes will be silently ignored.
280
+ * Read behavior is the same as for {@link fifo}.
281
+ *
282
+ * Also see {@link sliding} for alternative behavior.
283
+ *
284
+ * @param cap
285
+ */
286
+ export const dropping = (cap) => new DroppingBuffer(cap);
package/fiber.d.ts CHANGED
@@ -14,7 +14,8 @@ export declare class Fiber<T = any> implements IDeref<T | undefined>, IID<string
14
14
  gen: Nullable<Generator<unknown, T>>;
15
15
  idgen?: IIDGen<string>;
16
16
  state: State;
17
- children?: Fiber[];
17
+ autoTerminate: boolean;
18
+ children: Fiber[];
18
19
  value?: T;
19
20
  error?: Error;
20
21
  logger?: ILogger;
@@ -92,7 +93,7 @@ export declare class Fiber<T = any> implements IDeref<T | undefined>, IID<string
92
93
  * @param f
93
94
  * @param opts
94
95
  */
95
- fork<F>(body?: Nullable<MaybeFiber<F>>, opts?: Partial<FiberOpts>): Fiber<F>;
96
+ fork<F>(body?: Nullable<MaybeFiber<F>>, opts?: Partial<FiberOpts>): Fiber<any>;
96
97
  /**
97
98
  * Calls {@link Fiber.fork} for all given fibers and returns them as array.
98
99
  *
package/fiber.js CHANGED
@@ -1,17 +1,22 @@
1
+ var Fiber_1;
1
2
  import { __decorate } from "tslib";
2
3
  import { INotifyMixin } from "@thi.ng/api/mixins/inotify";
3
4
  import { isFunction } from "@thi.ng/checks/is-function";
5
+ import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
4
6
  import { illegalState } from "@thi.ng/errors/illegal-state";
5
7
  import { monotonic, prefixed } from "@thi.ng/idgen";
6
8
  import { EVENT_FIBER_CANCELED, EVENT_FIBER_DONE, EVENT_FIBER_ERROR, STATE_ACTIVE, STATE_CANCELED, STATE_DONE, STATE_ERROR, STATE_NEW, } from "./api.js";
7
9
  let DEFAULT_ID_GEN = prefixed("fib-", monotonic());
8
10
  export const setDefaultIDGen = (gen) => (DEFAULT_ID_GEN = gen);
9
11
  const NO_RESULT = { done: false, value: undefined };
10
- export let Fiber = class Fiber {
12
+ export let Fiber = Fiber_1 = class Fiber {
11
13
  constructor(gen, opts) {
12
14
  this.state = STATE_NEW;
15
+ this.autoTerminate = false;
16
+ this.children = [];
13
17
  this.value = undefined;
14
18
  if (opts) {
19
+ this.autoTerminate = !!opts.terminate;
15
20
  this.logger = opts.logger;
16
21
  this.parent = opts.parent;
17
22
  this.user = {
@@ -29,7 +34,7 @@ export let Fiber = class Fiber {
29
34
  else {
30
35
  this.idgen = DEFAULT_ID_GEN;
31
36
  }
32
- if (!this.id && this.idgen)
37
+ if (this.idgen)
33
38
  this.id = this.idgen.next();
34
39
  this.gen = isFunction(gen) ? gen(this) : gen;
35
40
  }
@@ -63,7 +68,7 @@ export let Fiber = class Fiber {
63
68
  * @param id
64
69
  */
65
70
  childForID(id) {
66
- return this.children?.find((f) => f.id === id);
71
+ return this.children.find((f) => f.id === id);
67
72
  }
68
73
  /**
69
74
  * Adds given `body` as child process to this fiber. If not already a
@@ -118,14 +123,20 @@ export let Fiber = class Fiber {
118
123
  fork(body, opts) {
119
124
  if (!this.isActive())
120
125
  illegalState(`fiber (id: ${this.id}) not active`);
121
- const $fiber = fiber(body, {
122
- parent: this,
123
- logger: this.logger,
124
- idgen: this.idgen,
125
- ...opts,
126
- });
127
- if (!this.children)
128
- this.children = [];
126
+ let $fiber;
127
+ if (body instanceof Fiber_1) {
128
+ if (body.parent)
129
+ illegalArgs(`fiber id: ${body.id} already has a parent`);
130
+ $fiber = body;
131
+ }
132
+ else {
133
+ $fiber = fiber(body, {
134
+ parent: this,
135
+ logger: this.logger,
136
+ idgen: this.idgen,
137
+ ...opts,
138
+ });
139
+ }
129
140
  this.children.push($fiber);
130
141
  this.logger?.debug("forking", $fiber.id);
131
142
  return $fiber;
@@ -151,7 +162,7 @@ export let Fiber = class Fiber {
151
162
  */
152
163
  *join() {
153
164
  this.logger?.debug("waiting for children...");
154
- while (this.children?.length)
165
+ while (this.children.length)
155
166
  yield;
156
167
  }
157
168
  /**
@@ -174,7 +185,7 @@ export let Fiber = class Fiber {
174
185
  case STATE_ACTIVE:
175
186
  try {
176
187
  const { children } = this;
177
- if (children) {
188
+ if (children.length) {
178
189
  for (let i = 0, n = children.length; i < n;) {
179
190
  const child = children[i];
180
191
  if (child.state > STATE_ACTIVE ||
@@ -186,6 +197,10 @@ export let Fiber = class Fiber {
186
197
  i++;
187
198
  }
188
199
  }
200
+ else if (this.autoTerminate) {
201
+ this.cancel();
202
+ break;
203
+ }
189
204
  const res = this.gen ? this.gen.next(this) : NO_RESULT;
190
205
  if (res.done)
191
206
  this.done(res.value);
@@ -206,8 +221,7 @@ export let Fiber = class Fiber {
206
221
  deinit() {
207
222
  this.logger?.debug("deinit", this.id);
208
223
  this.user?.deinit?.(this);
209
- if (this.children)
210
- this.children = undefined;
224
+ this.children.length = 0;
211
225
  this.gen = null;
212
226
  }
213
227
  /**
@@ -221,10 +235,8 @@ export let Fiber = class Fiber {
221
235
  if (!this.isActive())
222
236
  return;
223
237
  this.logger?.debug("cancel", this.id);
224
- if (this.children) {
225
- for (let child of this.children)
226
- child.cancel();
227
- }
238
+ for (let child of this.children)
239
+ child.cancel();
228
240
  this.deinit();
229
241
  this.state = STATE_CANCELED;
230
242
  this.idgen?.free(this.id);
@@ -245,10 +257,8 @@ export let Fiber = class Fiber {
245
257
  return;
246
258
  this.logger?.debug("done", this.id, value);
247
259
  this.value = value;
248
- if (this.children) {
249
- for (let child of this.children)
250
- child.done();
251
- }
260
+ for (let child of this.children)
261
+ child.done();
252
262
  this.deinit();
253
263
  this.state = STATE_DONE;
254
264
  this.idgen?.free(this.id);
@@ -272,10 +282,8 @@ export let Fiber = class Fiber {
272
282
  this.logger
273
283
  ? this.logger.severe(`error ${this.id}:`, err)
274
284
  : console.warn(`error ${this.id}:`, err);
275
- if (this.children) {
276
- for (let child of this.children)
277
- child.cancel();
278
- }
285
+ for (let child of this.children)
286
+ child.cancel();
279
287
  this.state = STATE_ERROR;
280
288
  this.error = err;
281
289
  this.deinit();
@@ -334,7 +342,7 @@ export let Fiber = class Fiber {
334
342
  return this;
335
343
  }
336
344
  };
337
- Fiber = __decorate([
345
+ Fiber = Fiber_1 = __decorate([
338
346
  INotifyMixin
339
347
  ], Fiber);
340
348
  /**
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./api.js";
2
+ export * from "./csp.js";
2
3
  export * from "./fiber.js";
3
4
  export * from "./ops.js";
4
5
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./api.js";
2
+ export * from "./csp.js";
2
3
  export * from "./fiber.js";
3
4
  export * from "./ops.js";
package/ops.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Fn0, Predicate } from "@thi.ng/api";
2
- import { type FiberFactory, type FiberOpts, type MaybeFiber } from "./api.js";
2
+ import type { IRandom } from "@thi.ng/random";
3
+ import { type FiberOpts, type MaybeFiber, type State } from "./api.js";
3
4
  import { Fiber } from "./fiber.js";
4
5
  /**
5
6
  * Returns co-routine which "blocks" for given number of milliseconds or
@@ -21,7 +22,7 @@ export declare function waitFrames(delay: number): Generator<undefined, void, un
21
22
  * @param fibers
22
23
  * @param opts
23
24
  */
24
- export declare const sequence: (fibers: Iterable<Fiber | FiberFactory | Generator>, opts?: Partial<FiberOpts>) => Fiber<unknown>;
25
+ export declare const sequence: (fibers: Iterable<MaybeFiber>, opts?: Partial<FiberOpts>) => Fiber<unknown>;
25
26
  /**
26
27
  * Returns a fiber which executes given fibers as child processes until **one**
27
28
  * of them is finished/terminated. That child fiber itself will be the result.
@@ -44,7 +45,7 @@ export declare const sequence: (fibers: Iterable<Fiber | FiberFactory | Generato
44
45
  * @param fibers
45
46
  * @param opts
46
47
  */
47
- export declare const first: (fibers: (Fiber | FiberFactory | Generator)[], opts?: Partial<FiberOpts>) => Fiber<Fiber<any>>;
48
+ export declare const first: (fibers: Iterable<MaybeFiber>, opts?: Partial<FiberOpts>) => Fiber<Fiber<any>>;
48
49
  /**
49
50
  * Returns a fiber which executes given fibers as child processes until **all**
50
51
  * of them are finished/terminated.
@@ -55,7 +56,7 @@ export declare const first: (fibers: (Fiber | FiberFactory | Generator)[], opts?
55
56
  * @param fibers
56
57
  * @param opts
57
58
  */
58
- export declare const all: (fibers: MaybeFiber[], opts?: Partial<FiberOpts>) => Fiber<void>;
59
+ export declare const all: (fibers: Iterable<MaybeFiber>, opts?: Partial<FiberOpts>) => Fiber<void>;
59
60
  /**
60
61
  * Syntax sugar common use cases of {@link first} where a child fiber should be
61
62
  * limited to a max. time period before giving up.
@@ -68,11 +69,11 @@ export declare const all: (fibers: MaybeFiber[], opts?: Partial<FiberOpts>) => F
68
69
  * if (res.deref() != null) { ... }
69
70
  * ```
70
71
  *
71
- * @param fiber
72
+ * @param body
72
73
  * @param timeout
73
74
  * @param opts
74
75
  */
75
- export declare const withTimeout: (fiber: MaybeFiber, timeout: number, opts?: Partial<FiberOpts>) => Fiber<Fiber<any>>;
76
+ export declare const withTimeout: (body: MaybeFiber, timeout: number, opts?: Partial<FiberOpts>) => Fiber<Fiber<any>>;
76
77
  /**
77
78
  * Higher-order fiber which repeatedly executes given `fiber` until its
78
79
  * completion, but does so in a time-sliced manner, such that the fiber never
@@ -82,7 +83,7 @@ export declare const withTimeout: (fiber: MaybeFiber, timeout: number, opts?: Pa
82
83
  * @param maxTime
83
84
  * @param opts
84
85
  */
85
- export declare const timeSlice: (body: Fiber | FiberFactory | Generator, maxTime: number, opts?: Partial<FiberOpts>) => Fiber<void>;
86
+ export declare const timeSlice: (body: MaybeFiber, maxTime: number, opts?: Partial<FiberOpts>) => Fiber<void>;
86
87
  /**
87
88
  * Returns a fiber which "blocks" until given predicate function returns true.
88
89
  *
@@ -127,4 +128,55 @@ export declare const untilPromise: <T>(promise: PromiseLike<T>) => Fiber<T>;
127
128
  * @param opts
128
129
  */
129
130
  export declare const untilEvent: (target: EventTarget, type: string, opts?: Partial<FiberOpts>) => Fiber<unknown>;
131
+ /**
132
+ * Custom fiber implementation for {@link shuffle}.
133
+ */
134
+ export declare class Shuffle extends Fiber {
135
+ rnd: IRandom;
136
+ constructor(fibers: Iterable<MaybeFiber>, opts?: Partial<FiberOpts & {
137
+ rnd: IRandom;
138
+ }>);
139
+ next(): State;
140
+ }
141
+ /**
142
+ * Higher-order fiber for creating a constantly randomized execution order of
143
+ * given `fibers`, e.g. for distributing workloads. Creates and returns a new
144
+ * fiber as parent of the given `fibers` which then shuffles their execution
145
+ * order on each {@link Fiber.next} invocation/update. The fiber terminates when
146
+ * all children are done.
147
+ *
148
+ * @remarks
149
+ * The `rnd` option can be used to customize the
150
+ * [`IRandom`](https://docs.thi.ng/umbrella/random/interfaces/IRandom.html)
151
+ * implementation used for shuffling. Defaults to
152
+ * [`SYSTEM`](https://docs.thi.ng/umbrella/random/variables/SYSTEM.html).
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * import { repeatedly } from "@thi.ng/transducers";
157
+ *
158
+ * // create & run fiber with 4 children, executing in random order
159
+ * shuffle(
160
+ * repeatedly(
161
+ * (id) => function*() { while(true) { console.log(`worker #{id}`); yield; } },
162
+ * 4
163
+ * )
164
+ * ).run()
165
+ *
166
+ * // worker #0
167
+ * // worker #1
168
+ * // worker #3
169
+ * // worker #3
170
+ * // worker #2
171
+ * // worker #0
172
+ * // worker #2
173
+ * // ...
174
+ * ```
175
+ *
176
+ * @param fibers
177
+ * @param opts
178
+ */
179
+ export declare const shuffle: (fibers: Iterable<MaybeFiber>, opts?: Partial<FiberOpts & {
180
+ rnd: IRandom;
181
+ }>) => Shuffle;
130
182
  //# sourceMappingURL=ops.d.ts.map
package/ops.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { shuffle as $shuffle } from "@thi.ng/arrays/shuffle";
1
2
  import { now, timeDiff } from "@thi.ng/bench/now";
3
+ import { SYSTEM } from "@thi.ng/random/system";
2
4
  import { STATE_ACTIVE, STATE_DONE, STATE_ERROR, } from "./api.js";
3
5
  import { Fiber, fiber } from "./fiber.js";
4
6
  /**
@@ -65,7 +67,7 @@ export const sequence = (fibers, opts) => fiber(function* (ctx) {
65
67
  * @param opts
66
68
  */
67
69
  export const first = (fibers, opts) => fiber(function* (ctx) {
68
- const $fibers = fibers.map((f) => ctx.fork(f));
70
+ const $fibers = [...fibers].map((f) => ctx.fork(f));
69
71
  while (true) {
70
72
  for (let f of $fibers) {
71
73
  if (!f.isActive())
@@ -85,8 +87,7 @@ export const first = (fibers, opts) => fiber(function* (ctx) {
85
87
  * @param opts
86
88
  */
87
89
  export const all = (fibers, opts) => fiber((ctx) => {
88
- for (let f of fibers)
89
- ctx.fork(f);
90
+ ctx.forkAll(...fibers);
90
91
  return ctx.join();
91
92
  }, opts);
92
93
  /**
@@ -101,11 +102,11 @@ export const all = (fibers, opts) => fiber((ctx) => {
101
102
  * if (res.deref() != null) { ... }
102
103
  * ```
103
104
  *
104
- * @param fiber
105
+ * @param body
105
106
  * @param timeout
106
107
  * @param opts
107
108
  */
108
- export const withTimeout = (fiber, timeout, opts) => first([fiber, wait(timeout)], opts);
109
+ export const withTimeout = (body, timeout, opts) => first([body, wait(timeout)], opts);
109
110
  /**
110
111
  * Higher-order fiber which repeatedly executes given `fiber` until its
111
112
  * completion, but does so in a time-sliced manner, such that the fiber never
@@ -196,3 +197,58 @@ export const untilEvent = (target, type, opts) => {
196
197
  },
197
198
  });
198
199
  };
200
+ /**
201
+ * Custom fiber implementation for {@link shuffle}.
202
+ */
203
+ export class Shuffle extends Fiber {
204
+ constructor(fibers, opts) {
205
+ super((ctx) => ctx.join(), opts);
206
+ this.rnd = opts?.rnd || SYSTEM;
207
+ this.forkAll(...fibers);
208
+ }
209
+ next() {
210
+ if (!this.isActive())
211
+ return this.state;
212
+ $shuffle(this.children, this.children.length, this.rnd);
213
+ return super.next();
214
+ }
215
+ }
216
+ /**
217
+ * Higher-order fiber for creating a constantly randomized execution order of
218
+ * given `fibers`, e.g. for distributing workloads. Creates and returns a new
219
+ * fiber as parent of the given `fibers` which then shuffles their execution
220
+ * order on each {@link Fiber.next} invocation/update. The fiber terminates when
221
+ * all children are done.
222
+ *
223
+ * @remarks
224
+ * The `rnd` option can be used to customize the
225
+ * [`IRandom`](https://docs.thi.ng/umbrella/random/interfaces/IRandom.html)
226
+ * implementation used for shuffling. Defaults to
227
+ * [`SYSTEM`](https://docs.thi.ng/umbrella/random/variables/SYSTEM.html).
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * import { repeatedly } from "@thi.ng/transducers";
232
+ *
233
+ * // create & run fiber with 4 children, executing in random order
234
+ * shuffle(
235
+ * repeatedly(
236
+ * (id) => function*() { while(true) { console.log(`worker #{id}`); yield; } },
237
+ * 4
238
+ * )
239
+ * ).run()
240
+ *
241
+ * // worker #0
242
+ * // worker #1
243
+ * // worker #3
244
+ * // worker #3
245
+ * // worker #2
246
+ * // worker #0
247
+ * // worker #2
248
+ * // ...
249
+ * ```
250
+ *
251
+ * @param fibers
252
+ * @param opts
253
+ */
254
+ export const shuffle = (fibers, opts) => new Shuffle(fibers, opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/fibers",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Process hierarchies & operators for cooperative multitasking",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -34,16 +34,18 @@
34
34
  "test": "testament test"
35
35
  },
36
36
  "dependencies": {
37
- "@thi.ng/api": "^8.9.0",
38
- "@thi.ng/bench": "^3.4.1",
39
- "@thi.ng/checks": "^3.4.0",
40
- "@thi.ng/errors": "^2.3.0",
41
- "@thi.ng/idgen": "^2.2.0",
42
- "@thi.ng/logger": "^1.4.16"
37
+ "@thi.ng/api": "^8.9.2",
38
+ "@thi.ng/arrays": "^2.5.17",
39
+ "@thi.ng/bench": "^3.4.3",
40
+ "@thi.ng/checks": "^3.4.2",
41
+ "@thi.ng/errors": "^2.3.2",
42
+ "@thi.ng/idgen": "^2.2.2",
43
+ "@thi.ng/logger": "^1.4.18",
44
+ "@thi.ng/random": "^3.5.3"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@microsoft/api-extractor": "^7.36.3",
46
- "@thi.ng/testament": "^0.3.18",
48
+ "@thi.ng/testament": "^0.3.20",
47
49
  "rimraf": "^5.0.1",
48
50
  "tools": "^0.0.1",
49
51
  "typedoc": "^0.24.8",
@@ -73,6 +75,9 @@
73
75
  "./api": {
74
76
  "default": "./api.js"
75
77
  },
78
+ "./csp": {
79
+ "default": "./csp.js"
80
+ },
76
81
  "./fiber": {
77
82
  "default": "./fiber.js"
78
83
  },
@@ -84,5 +89,5 @@
84
89
  "status": "alpha",
85
90
  "year": 2023
86
91
  },
87
- "gitHead": "fdea6ba9d36111f7346827ed81e7549f10343a13\n"
92
+ "gitHead": "ad9ac3232c6fc5fc8a0df75ac82fc1e0e9fb0258\n"
88
93
  }