├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── Channel.spec.ts ├── channels.spec.ts ├── index.spec.ts └── operators │ ├── broadcast.spec.ts │ ├── delay.spec.ts │ ├── filter.spec.ts │ ├── fromAsyncIterable.spec.ts │ ├── fromAsyncIterableDelayed.spec.ts │ ├── fromIterable.spec.ts │ ├── fromIterableDelayed.spec.ts │ ├── map.spec.ts │ └── pipe.spec.ts ├── assets └── pingpong.gif ├── docs ├── .vuepress │ ├── config.js │ └── public │ │ └── assets │ │ └── pingpong.gif ├── README.md └── guide │ ├── README.md │ ├── channels.md │ └── operators.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── Channel.ts ├── ChannelWrapper.ts ├── ChannelsUtilities.ts ├── Selectable.ts ├── index.ts └── operators │ ├── broadcast.ts │ ├── delay.ts │ ├── filter.ts │ ├── fromAsyncIterable.ts │ ├── fromAsyncIterableDelayed.ts │ ├── fromIterable.ts │ ├── fromIterableDelayed.ts │ ├── map.ts │ └── pipe.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: ['@typescript-eslint'], 4 | extends: ['plugin:@typescript-eslint/recommended'], 5 | rules: { 6 | "@typescript-eslint/no-explicit-any": "off", 7 | "@typescript-eslint/no-use-before-define": "off", 8 | "@typescript-eslint/no-parameter-properties": "off", 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dist 2 | dist/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage/ 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | 6 | script: 7 | - npm run test:ci 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Andrea Simone Costa 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @jfet97/csp 2 | 3 | A library for Communicating Sequential Processes, built on top of `async/await` and the asynchronous iterable interface. 4 | 5 | [![npm version](https://badge.fury.io/js/%40jfet97%2Fcsp.svg)](https://badge.fury.io/js/%40jfet97%2Fcsp) 6 | 7 | ## Installation 8 | 9 | This library requires `async/await` and `for-await-of` support. 10 | 11 | ``` 12 | $ npm install --save @jfet97/csp 13 | ``` 14 | 15 | ## Docs 16 | 17 | You can find the documentation [here](https://jfet97.github.io/csp/). 18 | 19 | 20 | ## Example Usage 21 | 22 | Below is a trivial example of usage, that plays on the standard ping-pong example. 23 | 24 | ```javascript 25 | const { Channel } = require('@jfet97/csp'); 26 | 27 | const timeout = ms => new Promise(resolve => setTimeout(resolve, ms)); 28 | 29 | const wiff = new Channel(); 30 | const waff = new Channel(); 31 | 32 | const createBall = () => ({ hits: 0, status: '' }); 33 | 34 | const createBat = async (inbound, outbound) => { 35 | while (true) { 36 | const ball = await inbound.take(); // wait for an incoming ball 37 | ball.hits++; 38 | ball.status = ball.status === 'wiff!' ? 'waff!' : 'wiff!'; 39 | console.log(`🎾 Ball hit ${ball.hits} time(s), ${ball.status}`); 40 | await timeout(500); // assume it's going to take a bit to hit the ball 41 | await outbound.put(ball); // smash the ball back 42 | } 43 | }; 44 | 45 | createBat(waff, wiff); // create a bat that will wiff waffs 46 | createBat(wiff, waff); // create a bat that will waff wiffs 47 | 48 | waff.put(createBall()); 49 | ``` 50 | 51 | ![ping pong](/assets/pingpong.gif?raw=true) 52 | 53 | 54 | ## Async Iteration Protocol 55 | Channels implement the async iterable interface, so you can transform the following illustrative code: 56 | 57 | ```javascript 58 | async function process (inbound, outbound) { 59 | while (true) { 60 | const msg = await inbound.take(); 61 | // do stuff with msg 62 | await outbound.put(res); 63 | } 64 | }; 65 | ``` 66 | 67 | into a cleaner version, thanks to the powerful `for-await-of`: 68 | 69 | ```javascript 70 | async function process (inbound, outbound) { 71 | for await(const msg of inbound) { 72 | // do stuff with msg 73 | await outbound.put(res); 74 | } 75 | }; 76 | ``` 77 | 78 | ## Credits 79 | 80 | Thanks to [Joe Harlow](https://twitter.com/someonedodgy) for his work on this topic. If you are unfamiliar with CSP, I encourage you to see [his talk](https://pusher.com/sessions/meetup/the-js-roundabout/csp-in-js) where he describe a simpler version of this library as well. 81 | 82 | ## Contributions 83 | 84 | Contributions are welcomed and appreciated! 85 | 86 | 1. Fork this repository. 87 | 2. Make your changes, documenting your new code with comments. 88 | 3. Submit a pull request with a sane commit message. 89 | 90 | Feel free to get in touch if you have any questions. 91 | 92 | ## License 93 | 94 | Please see the `LICENSE` file for more information. 95 | -------------------------------------------------------------------------------- /__tests__/Channel.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../src'; 2 | 3 | type Input = string | number; 4 | 5 | 6 | const msg = (() => { 7 | let i = 0; 8 | return () => ++i; 9 | })(); 10 | 11 | 12 | describe('[csp] Channel.alts', () => { 13 | 14 | const chan1 = new Channel(); 15 | const chan2 = new Channel(); 16 | const res = Channel.alts(chan1, chan2); 17 | 18 | test('should return an instance of a Promise', () => { 19 | expect(res instanceof Promise).toBe(true); 20 | }); 21 | 22 | }); 23 | 24 | describe('[csp] Channel.alts, chan1 ready', () => { 25 | 26 | const chan1 = new Channel(); 27 | const chan2 = new Channel(); 28 | const m = msg(); 29 | const result = Channel.alts(chan1, chan2); 30 | 31 | test('should receive the correct value', async () => { 32 | await chan1.put(m); 33 | const res = await result; 34 | expect(res).toEqual(m); 35 | }); 36 | 37 | }); 38 | 39 | describe('[csp] Channel.alts, chan2 ready', () => { 40 | 41 | const chan1 = new Channel(); 42 | const chan2 = new Channel(); 43 | const m = msg(); 44 | const result = Channel.alts(chan1, chan2); 45 | 46 | test('should receive the correct value', async () => { 47 | await chan2.put(m); 48 | const res = await result; 49 | expect(res).toEqual(m); 50 | }); 51 | 52 | }); 53 | 54 | describe('[csp] Channel.select Array, chan1 ready', () => { 55 | 56 | const chan1 = new Channel(); 57 | const chan2 = new Channel(); 58 | const m = msg(); 59 | const result = Channel.select([chan1, chan2]); 60 | 61 | test('should receive the correct key and value', async () => { 62 | await chan1.put(m); 63 | const [key, res] = await result; 64 | 65 | expect(key).toEqual(0); 66 | expect(res).toEqual(m); 67 | }); 68 | 69 | }); 70 | 71 | describe('[csp] Channel.select Array, chan2 ready', () => { 72 | 73 | const chan1 = new Channel(); 74 | const chan2 = new Channel(); 75 | const m = msg(); 76 | const result = Channel.select([chan1, chan2]); 77 | 78 | test('should receive the correct key and value', async () => { 79 | await chan2.put(m); 80 | const [key, res] = await result; 81 | 82 | expect(key).toEqual(1); 83 | expect(res).toEqual(m); 84 | }); 85 | 86 | }); 87 | 88 | describe('[csp] Channel.select Map, chan1 ready', () => { 89 | 90 | const chan1 = new Channel(); 91 | const chan2 = new Channel(); 92 | const m = msg(); 93 | const key1 = Symbol(); 94 | const key2 = Symbol(); 95 | const result = Channel.select(new Map>([[key1, chan1], [key2, chan2]])); 96 | 97 | test('should receive the correct key and value', async () => { 98 | await chan1.put(m); 99 | const [key, res] = await result; 100 | 101 | expect(key).toEqual(key1); 102 | expect(res).toEqual(m); 103 | }); 104 | 105 | }); 106 | 107 | describe('[csp] Channel.select Map, chan2 ready', () => { 108 | 109 | const chan1 = new Channel(); 110 | const chan2 = new Channel(); 111 | const m = msg(); 112 | const putter = chan2.put(m); 113 | const key1 = Symbol(); 114 | const key2 = Symbol(); 115 | const result = Channel.select(new Map([[key1, chan1], [key2, chan2]])); 116 | 117 | test('should receive the correct key and value', async () => { 118 | await putter; 119 | const [key, res] = await result; 120 | 121 | expect(key).toEqual(key2); 122 | expect(res).toEqual(m); 123 | }); 124 | 125 | }); 126 | 127 | describe('[csp] Channel.select Object, chan1 ready', () => { 128 | 129 | const chan1 = new Channel(); 130 | const chan2 = new Channel(); 131 | const m = msg(); 132 | const result = Channel.select({ a: chan1, b: chan2 }); 133 | 134 | test('should receive the correct key and value', async () => { 135 | await chan1.put(m); 136 | const [key, res] = await result; 137 | 138 | expect(key).toEqual('a'); 139 | expect(res).toEqual(m); 140 | }); 141 | 142 | }); 143 | 144 | describe('[csp] Channel.select Object, chan2 ready', () => { 145 | 146 | const chan1 = new Channel(); 147 | const chan2 = new Channel(); 148 | const m = msg(); 149 | const putter = chan2.put(m); 150 | const result = Channel.select({ a: chan1, b: chan2 }); 151 | 152 | test('should receive the correct key and value', async () => { 153 | await putter; 154 | const [key, res] = await result; 155 | 156 | expect(key).toEqual('b'); 157 | expect(res).toEqual(m); 158 | }); 159 | 160 | }); 161 | 162 | describe('[csp] Channel.select Set, chan1 ready', () => { 163 | 164 | const chan1 = new Channel(); 165 | const chan2 = new Channel(); 166 | const m = msg(); 167 | const result = Channel.select(new Set([chan1, chan2])); 168 | 169 | test('should receive the correct key and value', async () => { 170 | await chan1.put(m); 171 | const [key, res] = await result; 172 | 173 | expect(key).toEqual(chan1); 174 | expect(res).toEqual(m); 175 | }); 176 | 177 | }); 178 | 179 | describe('[csp] Channel.select Set, chan2 ready', () => { 180 | 181 | const chan1 = new Channel(); 182 | const chan2 = new Channel(); 183 | const m = msg(); 184 | const putter = chan2.put(m); 185 | const result = Channel.select(new Set([chan1, chan2])); 186 | 187 | test('should receive the correct key and value', async () => { 188 | await putter; 189 | const [key, res] = await result; 190 | 191 | expect(key).toEqual(chan2); 192 | expect(res).toEqual(m); 193 | }); 194 | 195 | }); 196 | 197 | import '../src/operators/fromIterable'; 198 | 199 | describe('[csp] Channel.merge', () => { 200 | 201 | test('should resolve the correct value', async () => { 202 | const chan1 = new Channel(); 203 | const chan2 = new Channel(); 204 | 205 | const result = Channel.merge(chan1, chan2); 206 | 207 | chan1.fromIterable([1, 2, 3]); 208 | chan2.fromIterable([4, 5, 6]); 209 | 210 | expect(await result.drain()).toEqual([1,4,2,5,3,6]); 211 | }); 212 | 213 | }); 214 | 215 | describe('[csp] Channel.mergeDelayed', () => { 216 | 217 | test('should resolve the correct value', async () => { 218 | const chan1 = new Channel(); 219 | const chan2 = new Channel(); 220 | 221 | const result = Channel.mergeDelayed(chan1, chan2); 222 | 223 | chan1.fromIterable([1, 2, 3]); 224 | chan2.fromIterable([4, 5, 6]); 225 | 226 | // thanks to mergeDelayed only the first value contained in chan1 and the first value 227 | // contained in chan2 will flow into result 228 | 229 | expect(await result.drain()).toEqual([1,4]); 230 | }); 231 | 232 | }); 233 | -------------------------------------------------------------------------------- /__tests__/channels.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../src'; 2 | 3 | 4 | const msg = (() => { 5 | let i = 0; 6 | return () => ++i; 7 | })(); 8 | 9 | 10 | describe('[csp] put', () => { 11 | 12 | test('should return an instance of a Promise', () => { 13 | const chan = new Channel(); 14 | const res = chan.put('foo'); 15 | expect(res instanceof Promise).toBe(true); 16 | }); 17 | 18 | }); 19 | 20 | describe('[csp] take', () => { 21 | 22 | test('should return an instance of a Promise', () => { 23 | const chan = new Channel(); 24 | const res = chan.take(); 25 | expect(res instanceof Promise).toBe(true); 26 | }); 27 | 28 | }); 29 | 30 | describe('[csp] drain', () => { 31 | 32 | const chan = new Channel(); 33 | const messages = [msg(), msg(), msg(), msg(), msg()]; 34 | messages.forEach(m => chan.put(m)); 35 | const res = chan.drain(); 36 | 37 | test('should return an instance of a Promise', () => { 38 | expect(res instanceof Promise).toBe(true); 39 | }) 40 | 41 | test('should drain the channel', async () => { 42 | expect(await res).toEqual(messages); 43 | }) 44 | 45 | }); 46 | 47 | describe('[csp] take, already put', () => { 48 | 49 | test('should resolve the correct value', async () => { 50 | const chan = new Channel(); 51 | const m = msg(); 52 | chan.put(m); 53 | const res = await chan.take(); 54 | expect(res).toEqual(m); 55 | }); 56 | 57 | }); 58 | 59 | describe('[csp] put, already taking', () => { 60 | 61 | test('should resolve the correct value', async () => { 62 | const chan = new Channel(); 63 | const m = msg(); 64 | const result = chan.take(); 65 | await chan.put(m); 66 | const res = await result; 67 | expect(res).toEqual(m); 68 | }); 69 | 70 | }); 71 | 72 | describe('[csp] take with asynciterable interface', () => { 73 | 74 | test('should resolve the correct value', async () => { 75 | const chan = new Channel(); 76 | const m = msg(); 77 | chan.put(m); 78 | const res = (await chan[Symbol.asyncIterator]().next()).value; 79 | expect(res).toEqual(m); 80 | }); 81 | 82 | }); -------------------------------------------------------------------------------- /__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../src'; 2 | 3 | describe("it works", () => { 4 | 5 | test("should work", () => { 6 | expect(true).toBe(true); 7 | }); 8 | 9 | }); 10 | 11 | describe('[csp] channel', () => { 12 | 13 | const chan = new Channel(); 14 | 15 | test("should contain 4 symbol properties", () => { 16 | expect(Object.getOwnPropertySymbols(chan.getInnerChannel()).length).toBe(4); 17 | }); 18 | 19 | test("should contain 1 symbol property", () => { 20 | expect(Object.getOwnPropertySymbols(Object.getPrototypeOf(chan)).length).toBe(1); 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /__tests__/operators/broadcast.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/broadcast'; 3 | 4 | 5 | const msg = (() => { 6 | let i = 0; 7 | return () => ++i; 8 | })(); 9 | 10 | 11 | describe('[csp] operator broadcast', () => { 12 | 13 | const source = new Channel(); 14 | const dest1 = new Channel(); 15 | const dest2 = new Channel(); 16 | const dest3 = new Channel(); 17 | 18 | source.broadcast(dest1, dest2, dest3); 19 | 20 | const m = msg(); 21 | source.put(m); 22 | 23 | test('should resolve the correct value', async () => { 24 | expect(await dest1.take()).toBe(m); 25 | expect(await dest2.take()).toBe(m); 26 | expect(await dest3.take()).toBe(m); 27 | }); 28 | 29 | }); -------------------------------------------------------------------------------- /__tests__/operators/delay.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/delay'; 3 | 4 | 5 | const msg = (() => { 6 | let i = 0; 7 | return () => ++i; 8 | })(); 9 | 10 | 11 | describe('[csp] operator delay', () => { 12 | 13 | test('should resolve the correct value', async () => { 14 | const chan = new Channel(); 15 | 16 | const m = msg(); 17 | chan.put(m); 18 | 19 | const now = process.hrtime()[0]; 20 | await chan.delay(3000).take(); 21 | const later = process.hrtime()[0]; 22 | const timeDifferenceInSeconds = later - now; 23 | 24 | expect(timeDifferenceInSeconds).toBeGreaterThanOrEqual(3); 25 | 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /__tests__/operators/filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/filter'; 3 | 4 | 5 | const msg = (() => { 6 | let i = 0; 7 | return () => ++i; 8 | })(); 9 | 10 | 11 | describe('[csp] operator filter', () => { 12 | 13 | const chan = new Channel(); 14 | const m = msg(), n = msg(), o = msg(), p = msg(); 15 | chan.put(m); 16 | chan.put(n); 17 | chan.put(o); 18 | chan.put(p); 19 | const resCh = chan.filter(v => Boolean(v % 2)); 20 | 21 | test('should resolve the correct value', async() => { 22 | const v = await resCh.take(); 23 | 24 | expect(v).toBe(m); 25 | }); 26 | 27 | test('should resolve the correct value', async() => { 28 | const v = await resCh.take(); 29 | 30 | expect(v).toBe(o); 31 | }); 32 | 33 | }); -------------------------------------------------------------------------------- /__tests__/operators/fromAsyncIterable.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/fromAsyncIterable'; 3 | 4 | 5 | describe('[csp] operator fromAsyncIterable', () => { 6 | 7 | const chan = new Channel(); 8 | const asyncIterable = { 9 | async *[Symbol.asyncIterator]() { 10 | yield* [1, 2, 3, 4, 5]; 11 | } 12 | }; 13 | 14 | test('should resolve the correct value', async () => { 15 | 16 | (async function process1() { 17 | chan.fromAsyncIterable(asyncIterable); 18 | })(); 19 | 20 | await (async function process2() { 21 | 22 | expect(await chan.take()).toEqual(1); 23 | 24 | // fromAsyncIterable() does not wait take operations after entering a value into the channel 25 | // so after a null timeout (needed by how microtasks are resolved) we can drain 26 | // all the remaining values from the channel 27 | // because the internal for-await-of can iterate without disruptions 28 | expect(await chan.drain()).toEqual([2, 3, 4, 5]); 29 | })(); 30 | }); 31 | 32 | }); -------------------------------------------------------------------------------- /__tests__/operators/fromAsyncIterableDelayed.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/fromAsyncIterableDelayed'; 3 | 4 | 5 | describe('[csp] operator fromAsyncIterable', () => { 6 | 7 | const chan = new Channel(); 8 | const asyncIterable = { 9 | async *[Symbol.asyncIterator]() { 10 | yield* [1, 2, 3, 4, 5]; 11 | } 12 | }; 13 | 14 | test('should resolve the correct value', async () => { 15 | 16 | (async function process1() { 17 | chan.fromAsyncIterableDelayed(asyncIterable); 18 | })(); 19 | 20 | await (async function process2() { 21 | 22 | expect(await chan.take()).toEqual(1); 23 | 24 | // fromAsyncIterable() does wait a take operations after entering a value into the channel 25 | // so after a null timeout (needed by how microtasks are resolved) we can drain 26 | // only the next value, 27 | expect(await chan.drain()).toEqual([2]); 28 | })(); 29 | }); 30 | 31 | }); -------------------------------------------------------------------------------- /__tests__/operators/fromIterable.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/fromIterable'; 3 | 4 | 5 | describe('[csp] operator fromIterable', () => { 6 | 7 | const chan = new Channel(); 8 | const iterable = [1, 2, 3]; 9 | chan.fromIterable(iterable); 10 | 11 | test('should resolve the correct values', async () => { 12 | expect(await chan.take()).toBe(1); 13 | expect(await chan.take()).toBe(2); 14 | expect(await chan.take()).toBe(3); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /__tests__/operators/fromIterableDelayed.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/fromIterableDelayed'; 3 | 4 | 5 | describe('[csp] operator fromIterableDelayed', () => { 6 | 7 | const chan = new Channel(); 8 | const iterable = { 9 | *[Symbol.iterator]() { 10 | let i = 0; 11 | while (true) { 12 | yield i++; 13 | } 14 | } 15 | }; 16 | 17 | chan.fromIterableDelayed(iterable); 18 | 19 | test('should resolve the correct values', async () => { 20 | expect(await chan.take()).toBe(0); 21 | expect(await chan.take()).toBe(1); 22 | expect(await chan.take()).toBe(2); 23 | expect(await chan.take()).toBe(3); 24 | }); 25 | 26 | }); -------------------------------------------------------------------------------- /__tests__/operators/map.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/map'; 3 | 4 | 5 | const msg = (() => { 6 | let i = 0; 7 | return () => ++i; 8 | })(); 9 | 10 | 11 | describe('[csp] operator map', () => { 12 | 13 | const chan = new Channel(); 14 | const m = msg(); 15 | const n = msg(); 16 | chan.put(m); 17 | 18 | test('should resolve the correct value', async() => { 19 | const res = await chan.map(v => n * v).take(); 20 | 21 | expect(res).toBe(m*n); 22 | }); 23 | 24 | }); -------------------------------------------------------------------------------- /__tests__/operators/pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from '../../src'; 2 | import '../../src/operators/fromIterable'; 3 | import '../../src/operators/pipe'; 4 | 5 | 6 | describe('[csp] operator pipe', () => { 7 | 8 | const source = new Channel(); 9 | const dest = new Channel(); 10 | 11 | // pipe the channels 12 | const resCh = source.pipe(dest); 13 | test('should resolve the correct value', () => { 14 | expect(resCh).toBe(dest); 15 | }); 16 | 17 | // put three numbers into the source 18 | source.fromIterable([1, 2, 3]); 19 | 20 | test('should resolve the correct values', async () => { 21 | // before the next value is taken from the source channel, pipe() will await 22 | // a take operation (implicitily contained into the drain() method) 23 | expect(await dest.drain()).toEqual([1]); 24 | expect(await dest.drain()).toEqual([2]); 25 | expect(await dest.drain()).toEqual([3]); 26 | }) 27 | }); -------------------------------------------------------------------------------- /assets/pingpong.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfet97/csp/3f977d292cbcc08c6bfff290cb12029facabe972/assets/pingpong.gif -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: '@jfet97/csp', 3 | description: 'A js library for Communicating Sequential Processes', 4 | base: '/csp/', 5 | themeConfig: { 6 | nav: [ 7 | { 8 | text: 'Home', 9 | link: '/', 10 | }, 11 | { 12 | text: 'Guide', 13 | link: '/guide/', 14 | }, 15 | { 16 | text: 'API', 17 | link: 'https://jfet97.github.io/csp/api/', 18 | }, 19 | { 20 | text: 'GitHub', 21 | link: 'https://github.com/jfet97/csp', 22 | } 23 | ], 24 | sidebar: [ 25 | { 26 | title: 'Guide', 27 | collapsable: false, 28 | children: [ 29 | '/guide/', 30 | '/guide/channels', 31 | '/guide/operators', 32 | ] 33 | }, 34 | ], 35 | sidebarDepth: 2, 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/pingpong.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfet97/csp/3f977d292cbcc08c6bfff290cb12029facabe972/docs/.vuepress/public/assets/pingpong.gif -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: assets/pingpong.gif 4 | actionText: Get Started → 5 | actionLink: /guide/ 6 | footer: MIT Licensed | Copyright © 2019-present Andrea Simone Costa 7 | --- 8 | ::: warning 9 | Node v10.0.0 or greater is required. 10 | ::: 11 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | --- 4 | 5 | # Introduction 6 | 7 | This is a library for CSP in JavaScript, built on top of `async/await` and the asynchronous iterable interface. 8 | 9 | ## What is CSP? 10 | 11 | CSP stands for __Communicating Sequential Processes__, a model to coordinate concurrency that was described by Richard Hoare in a book of the same name from 1978. \ 12 | CSP is based on two main primitives: __processes__ and __channels__. 13 | 14 | Due to the single-thread nature of JavaScript, the term process does not refer to an OS process. \ 15 | It alludes to an entity in the code designed to fulfill a specific task, a piece of code that can complete a unit of work independently. 16 | 17 | ES2017 took to us __async functions__, which correspond to the above description. Their execution can be paused thanks to the __await__ keyword, therefore this type of function can be run concurrently faking threads. 18 | 19 | CSP says that processes cannot share memory. What if they need to communicate with each other? \ 20 | In such case [channels](/guide/channels.html) come into action! 21 | 22 | ## Installation 23 | 24 | ```sh 25 | $ npm install --save @jfet97/csp 26 | ``` 27 | 28 | ## Example Usage 29 | 30 | Below is a trivial example of usage, that plays on the standard ping-pong example. 31 | 32 | ```js 33 | const { Channel } = require('@jfet97/csp'); 34 | // or... 35 | import { Channel } from '@jfet97/csp'; 36 | 37 | const timeout = ms => new Promise(resolve => setTimeout(resolve, ms)); 38 | 39 | const wiff = new Channel(); 40 | const waff = new Channel(); 41 | 42 | const createBall = () => ({ hits: 0, status: '' }); 43 | 44 | const createBat = async (inbound, outbound) => { 45 | while (true) { 46 | const ball = await inbound.take(); // wait for an incoming ball 47 | ball.hits++; 48 | ball.status = ball.status === 'wiff!' ? 'waff!' : 'wiff!'; 49 | console.log(`🎾 Ball hit ${ball.hits} time(s), ${ball.status}`); 50 | await timeout(500); // assume it's going to take a bit to hit the ball 51 | await outbound.put(ball); // smash the ball back 52 | } 53 | }; 54 | 55 | createBat(waff, wiff); // create a bat that will wiff waffs 56 | createBat(wiff, waff); // create a bat that will waff wiffs 57 | 58 | waff.put(createBall()); 59 | ``` 60 | 61 | With the following result: 62 | 63 | ![pingpong](/csp/assets/pingpong.gif) 64 | 65 | ## Async Iteration Protocol 66 | 67 | Channels implement the async iterable interface, so you can transform the following illustrative code: 68 | 69 | ```js 70 | async function process (inbound, outbound) { 71 | while (true) { 72 | const msg = await inbound.take(); 73 | // do stuff with msg 74 | await outbound.put(res); 75 | } 76 | }; 77 | ``` 78 | 79 | into a cleaner version, thanks to the powerful `for-await-of`: 80 | 81 | ```js 82 | async function process (inbound, outbound) { 83 | for await(const msg of inbound) { 84 | // do stuff with msg 85 | await outbound.put(res); 86 | } 87 | }; 88 | ``` 89 | 90 | ## Credits 91 | 92 | Thanks to [Joe Harlow](https://twitter.com/someonedodgy) for his work on this topic. If you are unfamiliar with CSP, I encourage you to see [his talk](https://pusher.com/sessions/meetup/the-js-roundabout/csp-in-js) where he describe a simpler version of this library as well. -------------------------------------------------------------------------------- /docs/guide/channels.md: -------------------------------------------------------------------------------- 1 | # Channels 2 | 3 | Channels are the pipes that connect concurrent processes. You can send values into channels from one process and receive those values into another process. 4 | 5 | ## Channel Constructor 6 | 7 | This constructor constructs a new `channel` and returns it. A channel exposes some methods to interact with it. 8 | 9 | ```js 10 | const chan = new Channel(); 11 | ``` 12 | 13 | ## Methods 14 | 15 | ### put 16 | 17 | `channel.put(message)` -> `Promise` 18 | 19 | The `put` method takes a `message` and put it into the channel on which it was called. The `put` method returns a `Promise` which can be optionally awaited and will resolve when something is ready to take the `message` from the `channel`. 20 | 21 | ```javascript 22 | const chan = new Channel(); 23 | chan.put(42); 24 | 25 | // ...or... 26 | 27 | await chan.put(42); 28 | ``` 29 | 30 | ### take 31 | 32 | `channel.take()` -> `Promise` 33 | 34 | The `take` method requires no arguments. The `take` method returns a `Promise` which should always be awaited and will resolve with a message, when a message is available. 35 | 36 | ```javascript 37 | const chan = new Channel(); 38 | chan.put(42); 39 | 40 | const msg = await chan.take(); // will receive 42 41 | ``` 42 | 43 | ### drain 44 | 45 | `channel.drain()` -> `Promise` 46 | 47 | The `drain` method requires no arguments. The `drain` method will drain all messages until the channel empties, returning a `Promise` that will resolve into an array of messages. 48 | 49 | ```javascript 50 | const chan = new Channel(); 51 | chan.put(42); 52 | chan.put(41); 53 | chan.put(40); 54 | chan.put(39); 55 | 56 | const msgs = await chan.drain(); // will receive [ 42, 41, 40, 39 ] 57 | ``` 58 | 59 | ### [Symbol.asyncIterator] 60 | 61 | `channel[Symbol.asyncIterator]()` -> `AsyncIterableIterator` 62 | 63 | Return an async iterator that will iterate over the channel. This enables the following syntax: 64 | 65 | ```javascript 66 | for await(const msg of chan) { 67 | // do stuff with each message 68 | } 69 | ``` 70 | 71 | that is a valid substitute of: 72 | 73 | ```javascript 74 | while(true) { 75 | const msg = await chan.take(); 76 | // do stuff with each message 77 | } 78 | ``` 79 | 80 | ## Static Utilities 81 | 82 | ### alts 83 | 84 | `Channel.alts(...channels)` -> `Promise` 85 | 86 | The `alts` static method will race taking values from multiple `channels`. 87 | 88 | ```javascript 89 | const chan1 = new Channel(); 90 | const chan2 = new Channel(); 91 | 92 | chan2.put(42); 93 | const msg = await Channel.alts(chan1, chan2); // will receive 42 94 | ``` 95 | 96 | ### select 97 | 98 | `Channel.select(Map<*, channel>|Set|Array|Object)` -> `Promise` 99 | 100 | The `select` static method will race taking values from multiple `channels`, similar to `alts`, but will also return the key of the channel that was selected. 101 | 102 | ```javascript 103 | const chan1 = new Channel(); 104 | const chan2 = new Channel(); 105 | 106 | chan2.put(42); 107 | const channels = [chan1, chan2]; 108 | const result = await Channel.select(channels); // will receive [1, 42] 109 | ``` 110 | 111 | Works with `Map` and `Set` as well as with plain-old javascript arrays and objects. 112 | 113 | ### merge 114 | 115 | `Channel.merge(...channels)` -> `channel` 116 | 117 | The `merge` static method will merge together multiple `channels`, returning a new one that will receive each value inserted into one of its input `channels`. 118 | As soon as a value is available from one of the input `channels`, it will be putted into the resulting `channel`. 119 | 120 | ```javascript 121 | const chan1 = new Channel(); 122 | const chan2 = new Channel(); 123 | 124 | const resCh = Channel.merge(chan1, chan2); 125 | 126 | chan1.put(1); 127 | chan1.put(2); 128 | chan1.put(3); 129 | chan2.put(4); 130 | chan2.put(5); 131 | chan2.put(6); 132 | 133 | // the 'merge' static method let all the values contained into the input channels to flow 134 | const result = await resCh.drain(); // will receive [1, 4, 2, 5, 3, 6] 135 | ``` 136 | 137 | ### mergeDelayed 138 | 139 | `Channel.mergeDelayed(...channels)` -> `channel` 140 | 141 | The `mergeDelayed` static method will merge together multiple `channels`, returning a new one that will receive each value inserted into one of its input `channels`. 142 | Before taking the next value from one of the input `channels`, a corresponding `take` operation will be waited (implicitly contained into the `drain` method). 143 | 144 | ```javascript 145 | const chan1 = new Channel(); 146 | const chan2 = new Channel(); 147 | 148 | const resCh = Channel.mergeDelayed(chan1, chan2); 149 | 150 | chan1.put(1); 151 | chan1.put(2); 152 | chan1.put(3); 153 | chan2.put(4); 154 | chan2.put(5); 155 | chan2.put(6); 156 | 157 | // the 'mergeDelayed' static method let only one value contained 158 | // into each input channels (if present) to flow 159 | const result = await resCh.drain(); // will receive [1, 4] 160 | ``` -------------------------------------------------------------------------------- /docs/guide/operators.md: -------------------------------------------------------------------------------- 1 | # Operators 2 | 3 | Operators are optionally addable methods that enable various manipulation of channels. 4 | 5 | ## Instances Methods 6 | 7 | These methods could be added to the instances created by the `Channel` constructor. 8 | 9 | ### broadcast 10 | 11 | `source.broadcast(...channels)` -> `source` 12 | 13 | The `broadcast` method enables multicasting from one `channel` to multiple `channels`. As soon as a value is inserted into the source, it will be emitted to listening channels. 14 | 15 | ```js 16 | require("@jfet97/csp/dist/operators/broadcast"); 17 | // or... 18 | import "@jfet97/csp/dist/operators/broadcast"; 19 | 20 | 21 | const source = new Channel(); 22 | const dest1 = new Channel(); 23 | const dest2 = new Channel(); 24 | const dest3 = new Channel(); 25 | 26 | source.broadcast(dest1, dest2, dest3); 27 | 28 | const m = 42; 29 | source.put(m); 30 | 31 | const res1 = await dest1.take(); // will receive 42 32 | const res2 = await dest1.take(); // will receive 42 33 | const res3 = await dest1.take(); // will receive 42 34 | ``` 35 | 36 | ### delay 37 | 38 | `source.broadcast(number)` -> `channel` 39 | 40 | The `delay` method creates a new channel that will receive all the values coming from its `source`, but with a delay expressed in milliseconds. 41 | 42 | ```js 43 | require("@jfet97/csp/dist/operators/delay"); 44 | // or... 45 | import "@jfet97/csp/dist/operators/delay"; 46 | 47 | 48 | const source = new Channel(); 49 | const delayed = source.delay(3000); 50 | 51 | source.put(42); 52 | 53 | const res = await delayed.take(); // will receive 42 after 3 seconds 54 | ``` 55 | 56 | ### filter 57 | 58 | `source.filter(value => boolean)` -> `channel` 59 | 60 | The `filter` method takes a `predicate` function and returns a new channel. Each value inserted into the `source` will be passed to the `predicate`, and only those who make the function to return `true` will be inserted into the returned `channel`. The others will be discarded. 61 | 62 | ```js 63 | require("@jfet97/csp/dist/operators/filter"); 64 | // or... 65 | import "@jfet97/csp/dist/operators/filter"; 66 | 67 | 68 | const source = new Channel(); 69 | const resCh = source.filter(v => Boolean(v % 2)); 70 | 71 | source.put(1); 72 | source.put(2); 73 | source.put(3); 74 | source.put(4); 75 | 76 | const result1 = await resCh.take(); // will receive 1 77 | const result2 = await resCh.take(); // will receive 3 78 | ``` 79 | 80 | ### map 81 | 82 | `source.map(value => value)` -> `channel` 83 | 84 | The `map` method takes a `mapper` function and returns a new channel. Each value inserted into the `source` will be passed to the `mapper` function and the result of each computation will be inserted into the returned `channel`. 85 | 86 | ```js 87 | require("@jfet97/csp/dist/operators/map"); 88 | // or... 89 | import "@jfet97/csp/dist/operators/map"; 90 | 91 | 92 | const source = new Channel(); 93 | const resCh = source.filter(v => 10 * v); 94 | 95 | source.put(1); 96 | source.put(2); 97 | source.put(3); 98 | source.put(4); 99 | 100 | const result1 = await resCh.take(); // will receive 10 101 | const result2 = await resCh.take(); // will receive 20 102 | const result3 = await resCh.take(); // will receive 30 103 | const result4 = await resCh.take(); // will receive 40 104 | ``` 105 | 106 | ### pipe 107 | 108 | `source.pipe(dest)` -> `dest` 109 | 110 | The `pipe` method simply takes alle the values from the `source` and insert them into the `dest`. It returns the destination `channel` to allow chained operations on it. 111 | 112 | ```js 113 | require("@jfet97/csp/dist/operators/pipe"); 114 | // or... 115 | import "@jfet97/csp/dist/operators/pipe"; 116 | 117 | 118 | const source = new Channel(); 119 | const dest = new Channel(); 120 | source.pipe(dest); 121 | 122 | source.put(1); 123 | source.put(2); 124 | source.put(3); 125 | source.put(4); 126 | 127 | const result1 = await dest.take(); // will receive 1 128 | const result2 = await dest.take(); // will receive 2 129 | const result3 = await dest.take(); // will receive 3 130 | const result4 = await dest.take(); // will receive 4 131 | ``` 132 | 133 | ### fromIterable 134 | 135 | `channel.fromIterable(iterable)` -> `channel` 136 | 137 | The `fromIterable` method takes all the values from a synchronous iterable and puts them all synchronously into the `channel`. 138 | Do not use with endless iterables. 139 | 140 | ```js 141 | require("@jfet97/csp/dist/operators/fromIterable"); 142 | // or... 143 | import "@jfet97/csp/dist/operators/fromIterable"; 144 | 145 | 146 | const chan = new Channel(); 147 | const iterable = [1, 2, 3]; 148 | 149 | chan.fromIterable(iterable); 150 | 151 | const result = await chan.drain(); // will receive [1, 2, 3] 152 | ``` 153 | 154 | ### fromIterableDelayed 155 | 156 | `channel.fromIterableDelayed(iterable)` -> `channel` 157 | 158 | The `fromIterableDelayed` method takes all the values from a synchronous iterable and puts them into the `channel`, waiting that a value is taken from the `channel` before put the next one. 159 | 160 | ```js 161 | require("@jfet97/csp/dist/operators/fromIterableDelayed"); 162 | // or... 163 | import "@jfet97/csp/dist/operators/fromIterableDelayed"; 164 | 165 | 166 | const chan = new Channel(); 167 | const iterable = [1, 2, 3]; 168 | 169 | chan.fromIterableDelayed(iterable); 170 | 171 | const result = await chan.drain(); // will receive [1] 172 | ``` 173 | 174 | ### fromAsyncIterable 175 | 176 | `channel.fromAsyncIterable(asyncIterable)` -> `channel` 177 | 178 | The `fromAsyncIterable` method takes each values from an asynchronous iterable and puts them into the `channel`. 179 | A take operation won't be waited, therefore as soon as a new value is available it will be inserted into the `channel`. 180 | 181 | ```js 182 | require("@jfet97/csp/dist/operators/fromAsyncIterable"); 183 | // or... 184 | import "@jfet97/csp/dist/operators/fromAsyncIterable"; 185 | 186 | 187 | const chan = new Channel(); 188 | const asyncIterable = { 189 | async *[Symbol.asyncIterator]() { 190 | yield* [1, 2, 3, 4, 5]; 191 | } 192 | }; 193 | 194 | chan.fromAsyncIterable(asyncIterable); 195 | 196 | const result = await chan.drain(); // will receive [1, 2, 3, 4, 5] 197 | ``` 198 | 199 | ### fromAsyncIterableDelayed 200 | 201 | `channel.fromAsyncIterableDelayed(asyncIterable)` -> `channel` 202 | 203 | The `fromAsyncIterableDelayed` method takes all the values from an asynchronous iterable and puts them into the `channel`, waiting that a value is taken from the `channel` before put the next one. 204 | 205 | ```js 206 | require("@jfet97/csp/dist/operators/fromAsyncIterableDelayed"); 207 | // or... 208 | import "@jfet97/csp/dist/operators/fromAsyncIterableDelayed"; 209 | 210 | 211 | const chan = new Channel(); 212 | const asyncIterable = { 213 | async *[Symbol.asyncIterator]() { 214 | yield* [1, 2, 3, 4, 5]; 215 | } 216 | }; 217 | 218 | chan.fromAsyncIterableDelayed(asyncIterable); 219 | 220 | const result = await chan.drain(); // will receive [1] 221 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transform": { 3 | "^.+\\.ts?$": "ts-jest" 4 | }, 5 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)?$", 6 | "moduleFileExtensions": [ 7 | "ts", 8 | "js", 9 | "json", 10 | "node" 11 | ], 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jfet97/csp", 3 | "version": "1.0.9", 4 | "description": "communicating sequential processes in javascript", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.build.json", 9 | "dev": "tsc -w -p tsconfig.build.json", 10 | "test": "jest --watchAll --coverage", 11 | "test:ci": "jest", 12 | "docs:dev": "vuepress dev docs", 13 | "docs:build": "vuepress build docs", 14 | "api:build": "typedoc", 15 | "deploy": "gh-pages -d /docs/.vuepress/dist -t" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "keywords": [ 21 | "CSP", 22 | "communicating", 23 | "sequential", 24 | "processes", 25 | "javascript", 26 | "channel" 27 | ], 28 | "author": "Andrea Simone Costa ", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@types/jest": "^24.0.11", 32 | "@types/tape": "^4.2.33", 33 | "@typescript-eslint/eslint-plugin": "^1.5.0", 34 | "@typescript-eslint/parser": "^1.5.0", 35 | "eslint": "^5.15.3", 36 | "eslint-plugin-import": "^2.16.0", 37 | "gh-pages": "^2.0.1", 38 | "jest": "^24.5.0", 39 | "ts-jest": "^24.0.0", 40 | "ts-node": "^8.0.3", 41 | "typedoc": "^0.14.2", 42 | "typedoc-plugin-nojekyll": "^1.0.1", 43 | "typescript": "^3.3.3333", 44 | "vuepress": "^0.14.10", 45 | "webpack-dev-middleware": "^3.6.0" 46 | }, 47 | "dependencies": {} 48 | } 49 | -------------------------------------------------------------------------------- /src/Channel.ts: -------------------------------------------------------------------------------- 1 | const messages = Symbol('messages'); 2 | const putters = Symbol('putters'); 3 | const takers = Symbol('takers'); 4 | const racers = Symbol('racers'); 5 | 6 | /** 7 | * This interface represent a channel. 8 | * It has 4 FIFO stacks plus some methods to operate with those sstacks. 9 | */ 10 | interface Channel { 11 | /** 12 | * This stack will contain all the messages sended by the channel's users 13 | */ 14 | [messages]: T[]; 15 | /** 16 | * This stack will contain all the 'resolve' functions of the putters. 17 | * When a process put a value into the channel a Promise is returned. 18 | * The 'resolve' function of that Promise ends here 19 | */ 20 | [putters]: (() => void)[]; 21 | /** 22 | * This stack will contain all the 'resolve' functions of the takers. 23 | * When a process take a value from the channel a Promise is returned. 24 | * The 'resolve' function of that Promise ends here 25 | */ 26 | [takers]: ((msg: T) => void)[]; 27 | /** 28 | * This stack will contain all the 'resolve' functions of the racers. 29 | * When a process execute Channel.alts or Channel.select a Promise is returned. 30 | * The 'resolve' function of that Promise ends here 31 | */ 32 | [racers]: ((ch: Channel) => void)[]; 33 | /** 34 | * This method allow us to use a channel as an async Iterable 35 | */ 36 | [Symbol.asyncIterator]: (() => AsyncIterableIterator); 37 | /** 38 | * 39 | * If there is already a taker or a racer waiting a message, the returned promise will be immediately resolved 40 | * 41 | * If both a taker and a racer were waiting a message the priority is given to the taker that will retrieve 42 | * the message 43 | * 44 | * @param msg A value that will be forwarded into the channel 45 | * @returns A promise that will be fulfilled when someone takes the msg from the channel 46 | */ 47 | put(msg: T): Promise; 48 | /** 49 | * 50 | * If there is already a message ready to be taken, the returned promise will be immediately resolved and the message read 51 | * 52 | * @returns A promise that will be fulfilled when someone put a message into the channel 53 | */ 54 | take(): Promise; 55 | /** 56 | * If there are no messages to be taken, the returned array will be empty 57 | * 58 | * @returns A promise that will be fulfilled with an array containing all the messages present into a channel 59 | */ 60 | drain(): Promise; 61 | /** 62 | * Transform a Channel into a Promise that wraps the channel itself. The promise will be fulfilled 63 | * immediately if here is already a message ready to be taken. Otherwise the promise will be fulfilled with the channel 64 | * when a process do a put operation, but only if there was no a waiting taker 65 | * 66 | * @returns A promise wrapping a Channel 67 | */ 68 | race(): Promise>; 69 | prependMessage(msg: T): void; 70 | waitATakerOrARacer(resolve: () => void): void; 71 | isThereAlreadyAPendingTaker(): boolean; 72 | unwaitOldestPutter(): void; 73 | retrieveOldestMessage(): T; 74 | retrieveOldestTaker(): ((msg: T) => void); 75 | waitAPutter(resolve: (msg: T) => void): void; 76 | isThereAlreadyAPendingPutter(): boolean; 77 | waitTheChannel(resolve: (ch: Channel) => void): void; 78 | retrieveOldestRacer(): ((ch: Channel) => void); 79 | isThereAPendingRacer(): boolean; 80 | areThereMessages(): boolean; 81 | fulfillTheRacer(racer: (ch: Channel) => void): void; 82 | } 83 | 84 | /** 85 | * See the [[Channel]] interface for more details. 86 | */ 87 | class ChannelImp implements Channel { 88 | 89 | 90 | 91 | public [messages]: T[]; 92 | public [putters]: (() => void)[]; 93 | public [takers]: ((msg: T) => void)[]; 94 | public [racers]: ((ch: Channel) => void)[]; 95 | 96 | public constructor() { 97 | this[messages] = []; 98 | this[putters] = []; 99 | this[takers] = []; 100 | this[racers] = []; 101 | } 102 | 103 | public async *[Symbol.asyncIterator](): AsyncIterableIterator { 104 | while (true) { 105 | yield await this.take(); 106 | } 107 | } 108 | 109 | public put(msg: T): Promise { 110 | return new Promise(resolve => { 111 | this.prependMessage(msg); 112 | this.waitATakerOrARacer(resolve); 113 | if (this.isThereAlreadyAPendingTaker()) { 114 | this.unwaitOldestPutter(); 115 | const msg = this.retrieveOldestMessage(); 116 | const taker = this.retrieveOldestTaker(); 117 | forwardMessage(taker, msg); 118 | } else if (this.isThereAPendingRacer()) { 119 | const racer = this.retrieveOldestRacer(); 120 | this.fulfillTheRacer(racer); 121 | } 122 | }); 123 | } 124 | 125 | public take(): Promise { 126 | return new Promise(resolve => { 127 | this.waitAPutter(resolve); 128 | 129 | if (this.isThereAlreadyAPendingPutter()) { 130 | this.unwaitOldestPutter(); 131 | const msg = this.retrieveOldestMessage(); 132 | const taker = this.retrieveOldestTaker(); 133 | forwardMessage(taker, msg); 134 | } 135 | }); 136 | } 137 | 138 | /** 139 | * Some data streams inserted into a channel are asynchronous 140 | * for example those coming from operators like fromAsyncIterable, fromAsyncIterableDelayed, 141 | * pipe, and those coming from static utilities like merge and mergeDelayed. 142 | 143 | * If values were inserted using above mentioned functions and, subsequently, 144 | * the drain method is called, we have to see those values into the channel. 145 | 146 | * The solution is to defer the drain method into a subsequent 147 | * microtask with the lowest priority due to the setTimeout behaviour. 148 | */ 149 | public async drain(): Promise { 150 | await new Promise(resolve => setTimeout(resolve, 0)); 151 | 152 | const msgs = []; 153 | while (this.areThereMessages()) { 154 | msgs.push(this.take()); 155 | } 156 | return Promise.all(msgs); 157 | } 158 | 159 | public race(): Promise> { 160 | return new Promise(resolve => { 161 | this.waitTheChannel(resolve); 162 | 163 | if (this.isThereAlreadyAPendingPutter()) { 164 | const racer = this.retrieveOldestRacer(); 165 | this.fulfillTheRacer(racer); 166 | } 167 | }); 168 | } 169 | 170 | public prependMessage(msg: T): void { 171 | this[messages].unshift(msg); 172 | } 173 | 174 | public waitATakerOrARacer(resolve: () => void): void { 175 | this[putters].unshift(resolve); 176 | } 177 | public isThereAlreadyAPendingTaker(): boolean { 178 | return !!this[takers].length; 179 | } 180 | public unwaitOldestPutter(): void { 181 | const resolve = this[putters].pop() 182 | resolve(); 183 | } 184 | public retrieveOldestMessage(): T { 185 | return this[messages].pop(); 186 | } 187 | public retrieveOldestTaker(): ((msg: T) => void) { 188 | return this[takers].pop(); 189 | } 190 | public waitAPutter(resolve: (msg: T) => void): void { 191 | this[takers].unshift(resolve); 192 | } 193 | public isThereAlreadyAPendingPutter(): boolean { 194 | return !!this[putters].length; 195 | } 196 | public waitTheChannel(resolve: (ch: Channel) => void): void { 197 | this[racers].unshift(resolve); 198 | } 199 | public retrieveOldestRacer(): ((ch: Channel) => void) { 200 | return this[racers].pop(); 201 | } 202 | public isThereAPendingRacer(): boolean { 203 | return !!this[racers].length; 204 | } 205 | public areThereMessages(): boolean { 206 | return !!this[messages].length; 207 | } 208 | public fulfillTheRacer(racer: (ch: Channel) => void): void { 209 | racer(this); 210 | } 211 | } 212 | 213 | /* atomic private methods */ 214 | /** 215 | * Responsible of sending a value to a taker 216 | */ 217 | function forwardMessage(taker: (msg: T) => void, msg: T): void { 218 | taker(msg); 219 | } 220 | 221 | // exports 222 | export { Channel, ChannelImp, racers }; -------------------------------------------------------------------------------- /src/ChannelWrapper.ts: -------------------------------------------------------------------------------- 1 | import { Channel, ChannelImp } from './Channel'; 2 | import { alts, select } from './ChannelsUtilities'; 3 | import { SelectableImp } from './Selectable'; 4 | 5 | /** 6 | * This interface describe a class that 7 | * will wrap a channel to simplify and reduce the allowed operation 8 | * Library's users will use ChannelWrappers instead of Channels 9 | */ 10 | interface ChannelWrapper { 11 | /** 12 | * Return the inner contained Channel 13 | */ 14 | getInnerChannel(): Channel; 15 | put(msg: T): Promise; 16 | take(): Promise; 17 | drain(): Promise; 18 | [Symbol.asyncIterator]: (() => AsyncIterableIterator); 19 | } 20 | 21 | // exported class 22 | class ChannelWrapperImp implements ChannelWrapper{ 23 | /** 24 | * A reference to the real Channel that is wrapped 25 | */ 26 | private __ch__: Channel; 27 | 28 | public getInnerChannel(): Channel { 29 | return this.__ch__; 30 | } 31 | 32 | public constructor(ch: ChannelImp = new ChannelImp()) { 33 | this.__ch__ = ch; 34 | } 35 | 36 | public put(msg: T): Promise { 37 | return this.__ch__.put(msg); 38 | } 39 | 40 | public take(): Promise { 41 | return this.__ch__.take(); 42 | } 43 | 44 | public drain(): Promise { 45 | return this.__ch__.drain(); 46 | } 47 | 48 | /** 49 | * Dear, old generators delegation :) 50 | */ 51 | public async *[Symbol.asyncIterator](): AsyncIterableIterator { 52 | yield* this.__ch__; 53 | } 54 | 55 | /** 56 | * The alts static method will race taking values from multiple channels. 57 | * It will perform a conversion between ChannelWrapper and the Channels needed by the alts async function 58 | */ 59 | public static alts(...chs: ChannelWrapper[]): Promise { 60 | const channels = chs.map(ch => ch.getInnerChannel()); 61 | return alts(...channels); 62 | } 63 | 64 | /** 65 | * The select static method will race taking values from multiple channels, similar to alts, but will also return the key of the channel that was selected. 66 | * It will perform a conversion between ChannelWrapper and the Channels needed by [[select]] 67 | */ 68 | public static async select(sel: { [k: string]: ChannelWrapper } | Map> | Set> | ChannelWrapper[]): Promise<[any, S]> { 69 | // convert ChannelWrapper into Channel 70 | // because Selectable works only with the latterone 71 | let res; 72 | 73 | if (sel instanceof Map) { 74 | const selEntries = [...sel.entries()]; 75 | res = new Map(selEntries.map(([key, ch]): [any, Channel] => [key, ch.getInnerChannel()])); 76 | } else if (sel instanceof Set) { 77 | res = new Set([...sel.values()].map((ch): Channel => ch.getInnerChannel())); 78 | } else if (Array.isArray(sel)) { 79 | res = sel.map((ch): Channel => ch.getInnerChannel()); 80 | } else { 81 | // plain js object 82 | 83 | // to erase as soon as typescript supports es2019 features: use Object.fromEntries instead 84 | const fromEntries = function fromEntries(iterable: any): any { 85 | return [...iterable] 86 | .reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {}) 87 | } 88 | res = fromEntries(Object.entries(sel).map(([key, ch]): [string, Channel] => [key, ch.getInnerChannel()])); 89 | } 90 | 91 | let selectRes = await select(new SelectableImp(res)); 92 | if (sel instanceof Set) { 93 | // selectRes = [keyOfWinnerChannel, msg] 94 | // the key of the winner channel contained in a Set have to be the channel itself but 95 | // selectRes[0] contains an instance of Channel. We need the initial instance of ChannelWrapper 96 | // that wraps the winner instance of Channel 97 | selectRes[0] = [...sel.values()].find((value) => value.getInnerChannel() === selectRes[0]) 98 | } 99 | return selectRes; 100 | } 101 | 102 | /** 103 | * The merge static method will merge together multiple channels, returning a new one that will 104 | * receive each value inserted into one of its input channels. As soon as a value is available 105 | * from one of the input channels, it will be putted into the resulting channel. 106 | * @returns A fresh new channel that will receive all the values inserted into input channels 107 | */ 108 | public static merge(...chs: ChannelWrapper[]): ChannelWrapper { 109 | 110 | const outCh = new ChannelWrapperImp(); 111 | 112 | const mergeProcessFactory = async (source: ChannelWrapper): Promise => { 113 | // as soon as a value is available from one of the input channels, put it immediately into the output channel 114 | for await (const msg of source) { 115 | outCh.put(msg); 116 | } 117 | } 118 | 119 | for (const ch of chs) { 120 | mergeProcessFactory(ch); 121 | } 122 | 123 | return outCh; 124 | } 125 | 126 | /** 127 | * The mergeDelayed static method will merge together multiple channels, returning a new one that will 128 | * receive each value inserted into one of its input channels. Before taking the next value from one of the input channels, 129 | * a corresponding take operation will be waited (implicitly contained into the drain method). 130 | * @returns A fresh new channel that will receive all the values inserted into input channels 131 | */ 132 | public static mergeDelayed(...chs: ChannelWrapper[]): ChannelWrapper { 133 | 134 | const outCh = new ChannelWrapperImp(); 135 | 136 | const mergeProcessFactory = async (source: ChannelWrapper): Promise => { 137 | // before request the next value from one of the input channels, each process will wait the take operation 138 | // that will be (eventually) performed on the just inserted message 139 | for await (const msg of source) { 140 | await outCh.put(msg); 141 | } 142 | } 143 | 144 | for (const ch of chs) { 145 | mergeProcessFactory(ch); 146 | } 147 | 148 | return outCh; 149 | } 150 | } 151 | 152 | export { 153 | ChannelWrapperImp, 154 | ChannelWrapper 155 | }; -------------------------------------------------------------------------------- /src/ChannelsUtilities.ts: -------------------------------------------------------------------------------- 1 | import { Selectable } from './Selectable'; 2 | import { Channel, racers } from './Channel'; 3 | 4 | /** 5 | * @param chs An array of Channels 6 | * @returns A Promise that will fullfill with a message contained into the winner channel 7 | */ 8 | async function alts(...chs: Channel[]): Promise { 9 | // transform each channel in a Promise that will fulfill when 10 | // the corrisponding channel receive a message 11 | const racingChannels = chs.map(ch => ch.race()); 12 | 13 | const winningChannel = await Promise.race(racingChannels); 14 | 15 | const losersChannels = chs.filter(c => c !== winningChannel); 16 | removeLosersRacersFromTheirChannels(losersChannels); 17 | 18 | winningChannel.unwaitOldestPutter(); 19 | const msg = winningChannel.retrieveOldestMessage() 20 | return msg; 21 | } 22 | 23 | /** 24 | * @param chs A selectable. 25 | * @returns A Promise that will fullfill with a message contained into the winner channel and its key. 26 | */ 27 | async function select(selectable: Selectable): Promise<[any, T]> { 28 | 29 | // transform each channel in a Promise that will fulfill when 30 | // the corrisponding channel receive a message 31 | const racingSelectables = selectable.mapToPromise(async ch => { 32 | // waitingRacer is a promise that will resolve into the ch passed to race 33 | const waitingRacer = ch.race(); 34 | return waitingRacer; 35 | }); 36 | 37 | const racingChannels = racingSelectables.revertToArray(); 38 | 39 | const winningChannel = await Promise.race(racingChannels); 40 | const winningChannelKey = selectable.keyOf(winningChannel); 41 | 42 | 43 | const losersChannels = selectable.filter(ch => ch !== winningChannel).revertToArray(); 44 | removeLosersRacersFromTheirChannels(losersChannels); 45 | 46 | winningChannel.unwaitOldestPutter(); 47 | const msg = winningChannel.retrieveOldestMessage() 48 | return [winningChannelKey, msg]; 49 | } 50 | 51 | function removeLosersRacersFromTheirChannels(chs: Channel[]): void { 52 | chs.forEach(c => c[racers].pop()); 53 | } 54 | 55 | export { alts, select }; 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Selectable.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from './Channel'; 2 | 3 | /** 4 | * Interface for the Selectable Class. 5 | * A selectable could be: a plain Object, a Map, a Set and an Array as well of channels. 6 | */ 7 | interface Selectable { 8 | /** A reference to the selectable object */ 9 | sel: { [k: string]: Channel } | Map> | Set> | Channel[]; 10 | /** 11 | * A method that map each channel contained into the selectable into a Promise containg that channel . 12 | * @param fn A proper mapper function. 13 | */ 14 | mapToPromise(fn: (c: Channel) => Promise>): SelectableP; 15 | /** 16 | * Whatever the selectable is, this method convert it into an array of channels. 17 | * Warning: Map and Object keys will be lost. 18 | */ 19 | revertToArray(): Channel[]; 20 | // map(fn: (c: Channel) => Channel): Selectable; 21 | /** 22 | * A method that execute the filter operation. 23 | * @param predicate A proper predicate function . 24 | */ 25 | filter(predicate: (c: Channel) => boolean): Selectable; 26 | /** 27 | * A method that return the key of a specific channel. 28 | * If the selectable is a Set, the channel itself will be returned. 29 | * The not found channel status is not contemplated because this method is called only in a safe context. 30 | * @param searchedCh A channel of which we are looking for the key. 31 | */ 32 | keyOf(searchedCh: Channel): any; 33 | } 34 | 35 | /** 36 | * Interface for the Selectable PClass. 37 | * A selectableP could be: a plain Object, a Map, a Set and an Array as well of channels. 38 | * Differently from the normal selectable, the channel is always wrapped into a Promise. 39 | */ 40 | interface SelectableP { 41 | /** A reference to the selectableP object*/ 42 | sel: { [k: string]: Promise> } | Map>> | Set>> | Promise>[]; 43 | /** 44 | * Whatever the selectable is, this method convert it into an array of Promise. 45 | * Warning: Map and Object keys will be lost. 46 | */ 47 | revertToArray(): Promise>[]; 48 | } 49 | 50 | /** 51 | * Implementation for the Selectable Class. 52 | * See the [[Selectable]] interface for more details. 53 | */ 54 | class SelectableImp implements Selectable { 55 | 56 | public constructor(public sel: { [k: string]: Channel } | Map> | Set> | Channel[]) { 57 | } 58 | 59 | /* we don't need it yet, but she is ready 60 | map(fn: (c: Channel) => Channel): Selectable { 61 | let res; 62 | if (this.sel instanceof Map) { 63 | const selEntries = [...this.sel.entries()]; 64 | res = new Map(selEntries.map(([key, ch]): [any, Channel] => [key, fn(ch)])); 65 | } else if (this.sel instanceof Set) { 66 | res = new Set([...this.sel.values()].map(ch => fn(ch))); 67 | } else if (Array.isArray(this.sel)) { 68 | res = this.sel.map(ch => fn(ch)); 69 | } else { 70 | // plain js object 71 | 72 | // to erase as soon as typescript supports es2019 features: use Object.fromEntries instead 73 | const fromEntries = function fromEntries(iterable: any) { 74 | return [...iterable] 75 | .reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {}) 76 | } 77 | res = fromEntries(Object.entries(this.sel).map(([key, ch]) => [key, fn(ch)])); 78 | } 79 | return new SelectableImp(res); 80 | }*/ 81 | 82 | public mapToPromise(fn: (c: Channel) => Promise>): SelectableP { 83 | let res; 84 | if (this.sel instanceof Map) { 85 | const selEntries = [...this.sel.entries()]; 86 | res = new Map(selEntries.map(([key, ch]): [any, Promise>] => [key, fn(ch)])); 87 | } else if (this.sel instanceof Set) { 88 | res = new Set([...this.sel.values()].map((ch): Promise> => fn(ch))); 89 | } else if (Array.isArray(this.sel)) { 90 | res = this.sel.map((ch): Promise> => fn(ch)); 91 | } else { 92 | // plain js object 93 | 94 | // to erase as soon as typescript supports es2019 features: use Object.fromEntries instead 95 | const fromEntries = function fromEntries(iterable: any): any { 96 | return [...iterable] 97 | .reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {}) 98 | } 99 | res = fromEntries(Object.entries(this.sel).map(([key, ch]): [string, Promise>] => [key, fn(ch)])); 100 | } 101 | return new SelectablePImp(res); 102 | } 103 | 104 | public filter(predicate: (c: Channel) => boolean): Selectable { 105 | let res; 106 | if (this.sel instanceof Set) { 107 | const values = [...this.sel.values()]; 108 | const filteredValues = values.filter(predicate); 109 | res = new Set(filteredValues); 110 | } else if (this.sel instanceof Map) { 111 | const entries = [...this.sel.entries()]; 112 | const filteredEntries = entries.filter(([, value]) => predicate(value)); 113 | res = new Map(filteredEntries); 114 | } else if (Array.isArray(this.sel)) { 115 | const values = [...this.sel.values()]; 116 | const filteredValues = values.filter(predicate); 117 | res = filteredValues; 118 | } else { 119 | // plain js object 120 | 121 | // to erase as soon as typescript supports es2019 features: use Object.fromEntries instead 122 | const fromEntries = function fromEntries(iterable: any): any { 123 | return [...iterable] 124 | .reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {}) 125 | } 126 | 127 | res = fromEntries(Object.entries(this.sel).filter(([, value]) => predicate(value))); 128 | } 129 | return new SelectableImp(res); 130 | } 131 | 132 | public revertToArray(): Channel[] { 133 | let res; 134 | if ((this.sel instanceof Set) || (this.sel instanceof Map) || (Array.isArray(this.sel))) { 135 | res = [...this.sel.values()]; 136 | } else { 137 | // plain js object 138 | res = Object.values(this.sel); 139 | } 140 | return (res as Channel[]); 141 | } 142 | 143 | public keyOf(searchedCh: Channel): any { 144 | let res; 145 | if (this.sel instanceof Map) { 146 | const selEntries = [...this.sel.entries()]; 147 | const [key] = selEntries.find(([, ch]) => ch === searchedCh); 148 | res = key; 149 | } else if (this.sel instanceof Set) { 150 | res = searchedCh; 151 | } else if (Array.isArray(this.sel)) { 152 | res = this.sel.findIndex(ch => ch === searchedCh); 153 | } else { 154 | // plain js object 155 | const selEntries = Object.entries(this.sel); 156 | const [key] = selEntries.find(([, ch]) => ch === searchedCh); 157 | res = key; 158 | } 159 | return res; 160 | } 161 | } 162 | 163 | /** 164 | * Implementation for the Selectable Class. 165 | * See the [[SelectableP]] interface for more details. 166 | */ 167 | class SelectablePImp implements SelectableP { 168 | public constructor(public sel: { [k: string]: Promise> } | Map>> | Set>> | Promise>[]) { } 169 | 170 | public revertToArray(): Promise>[] { 171 | let res; 172 | if ((this.sel instanceof Set) || (this.sel instanceof Map) || (Array.isArray(this.sel))) { 173 | res = [...this.sel.values()]; 174 | } else { 175 | // plain js object 176 | res = Object.values(this.sel); 177 | } 178 | return (res as Promise>[]); 179 | } 180 | } 181 | 182 | export { Selectable, SelectableImp }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ChannelWrapperImp as Channel } from './ChannelWrapper'; -------------------------------------------------------------------------------- /src/operators/broadcast.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * As soon as a value is inserted into the source, it will be emitted into listening channels 5 | * @param chs array of channels 6 | * @returns the channel on which the method was called 7 | */ 8 | function broadcast(this: ChannelWrapper, ...chs: ChannelWrapper[]): ChannelWrapper { 9 | 10 | // start the async process that will broadcast messages coming from the input channel (this) 11 | // sending them to the listening channels 12 | (async () => { 13 | for await (const msg of this) { 14 | chs.forEach(ch => ch.put(msg)); 15 | } 16 | })(); 17 | 18 | return this; 19 | } 20 | 21 | ChannelWrapperImp.prototype.broadcast = broadcast; 22 | 23 | declare module '../ChannelWrapper' { 24 | interface ChannelWrapperImp { 25 | broadcast: typeof broadcast; 26 | } 27 | } -------------------------------------------------------------------------------- /src/operators/delay.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | const timeout = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); 4 | 5 | /** 6 | * The delay method creates a new channel that will receive all the values coming from its source, 7 | * but with a delay expressed in milliseconds. 8 | * @param ms amount of ms of delay 9 | * @returns a fresh new channel that will receive all the value inserted into the channel on which this method was called, 10 | * but delayed by a specific amount of time 11 | */ 12 | function delay(this: ChannelWrapper, ms: number): ChannelWrapper { 13 | const outCh = new ChannelWrapperImp(); 14 | 15 | // start the async process that will delay messages coming from the input channel (this) 16 | // sending them to the output channel 17 | (async () => { 18 | for await (const msg of this) { 19 | await timeout(ms); 20 | 21 | // wait for something ready to take the message before asking the next one 22 | // to the input channel (this) 23 | await outCh.put(msg); 24 | } 25 | })(); 26 | 27 | return outCh; 28 | } 29 | 30 | ChannelWrapperImp.prototype.delay = delay; 31 | 32 | declare module '../ChannelWrapper' { 33 | interface ChannelWrapperImp { 34 | delay: typeof delay; 35 | } 36 | } -------------------------------------------------------------------------------- /src/operators/filter.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * The filter method takes a predicate function and returns a new channel. Each value inserted into 5 | * the channel on which this method was called will be passed to the predicate, and only those who make the function to return true 6 | * will be inserted into the returned channel. The others will be discarded. 7 | * @param predicateFn the predicate function 8 | * @return a fresh new channel 9 | */ 10 | function filter(this: ChannelWrapper, predicateFn: (msg: T) => boolean): ChannelWrapper { 11 | const outCh = new ChannelWrapperImp(); 12 | 13 | // start the async process that will filter messages coming from the input channel (this) 14 | // sending them to the output channel if the check contained into the predicateFn is passed 15 | (async () => { 16 | for await (const msg of this) { 17 | const wasCheckPassed = predicateFn(msg); 18 | 19 | // wait for something ready to take the message before asking the next one 20 | // to the input channel (this) only if the check was passed 21 | await wasCheckPassed ? outCh.put(msg) : null; 22 | } 23 | })(); 24 | 25 | return outCh; 26 | } 27 | 28 | ChannelWrapperImp.prototype.filter = filter; 29 | 30 | declare module '../ChannelWrapper' { 31 | interface ChannelWrapperImp { 32 | filter: typeof filter; 33 | } 34 | } -------------------------------------------------------------------------------- /src/operators/fromAsyncIterable.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * As soon as a value is ready to be taken from the async iterable, put it immediately into the 5 | * channel on which this method was called 6 | * @param ait an asyncIterable 7 | * @return the channel on which the method was called 8 | */ 9 | function fromAsyncIterable(this: ChannelWrapper, ait: AsyncIterable): ChannelWrapper { 10 | 11 | (async () => { 12 | for await (const msg of ait) { 13 | this.put(msg); 14 | } 15 | })(); 16 | 17 | return this; 18 | } 19 | 20 | ChannelWrapperImp.prototype.fromAsyncIterable = fromAsyncIterable; 21 | 22 | declare module '../ChannelWrapper' { 23 | interface ChannelWrapperImp { 24 | fromAsyncIterable: typeof fromAsyncIterable; 25 | } 26 | } -------------------------------------------------------------------------------- /src/operators/fromAsyncIterableDelayed.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * As soon as a value is ready to be taken from the async iterable put it into the channel on which this method was called, 5 | * but wait until something is ready to take it before requesting the next one and putting it into the channel 6 | * @param ait an asyncIiterable 7 | * @return the channel on which the method was called 8 | */ 9 | 10 | function fromAsyncIterableDelayed(this: ChannelWrapper, ait: AsyncIterable): ChannelWrapper { 11 | 12 | (async () => { 13 | for await (const msg of ait) { 14 | await this.put(msg); 15 | } 16 | })(); 17 | 18 | return this; 19 | } 20 | 21 | ChannelWrapperImp.prototype.fromAsyncIterableDelayed = fromAsyncIterableDelayed; 22 | 23 | declare module '../ChannelWrapper' { 24 | interface ChannelWrapperImp { 25 | fromAsyncIterableDelayed: typeof fromAsyncIterableDelayed; 26 | } 27 | } -------------------------------------------------------------------------------- /src/operators/fromIterable.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * Take all the values from a sync iterable and put them immediately into the 5 | * channel on which this method was called 6 | * @param it an iterable 7 | * @return the channel on which the method was called 8 | */ 9 | function fromIterable(this: ChannelWrapper, it: Iterable): ChannelWrapper { 10 | 11 | for(const msg of it) { 12 | this.put(msg); 13 | } 14 | 15 | return this; 16 | } 17 | 18 | ChannelWrapperImp.prototype.fromIterable = fromIterable; 19 | 20 | declare module '../ChannelWrapper' { 21 | interface ChannelWrapperImp { 22 | fromIterable: typeof fromIterable; 23 | } 24 | } -------------------------------------------------------------------------------- /src/operators/fromIterableDelayed.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * Take all the values from the sync iterable and put them into the channel on which this method was called, 5 | * waiting that a value is taken from the channel before putting the next one. 6 | * @param it an iterable 7 | * @return the channel on which the method was called 8 | */ 9 | function fromIterableDelayed(this: ChannelWrapper, it: Iterable): ChannelWrapper { 10 | 11 | (async () => { 12 | for (const msg of it) { 13 | await this.put(msg); 14 | } 15 | })() 16 | 17 | return this; 18 | } 19 | 20 | ChannelWrapperImp.prototype.fromIterableDelayed = fromIterableDelayed; 21 | 22 | declare module '../ChannelWrapper' { 23 | interface ChannelWrapperImp { 24 | fromIterableDelayed: typeof fromIterableDelayed; 25 | } 26 | } -------------------------------------------------------------------------------- /src/operators/map.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * The map method takes a mapper function and returns a new channel. 5 | * Each value inserted into the channel on which this method was called 6 | * will be passed to the mapper function and the result of each computation will be inserted 7 | * into the returned channel. 8 | * @param mapperFn the mapper function 9 | * @return a fresh new channel 10 | */ 11 | function map(this: ChannelWrapper, mapperFn: (msg: T) => T): ChannelWrapper { 12 | const outCh = new ChannelWrapperImp(); 13 | 14 | // start the async process that will map messages coming from the input channel (this) 15 | // sending them to the output channel 16 | (async () => { 17 | for await(const msg of this) { 18 | const outmsg = mapperFn(msg); 19 | // wait for something ready to take the message before asking the next one 20 | // to the input channel (this) 21 | await outCh.put(outmsg); 22 | } 23 | })(); 24 | 25 | return outCh; 26 | } 27 | 28 | ChannelWrapperImp.prototype.map = map; 29 | 30 | declare module '../ChannelWrapper' { 31 | interface ChannelWrapperImp { 32 | map: typeof map; 33 | } 34 | } -------------------------------------------------------------------------------- /src/operators/pipe.ts: -------------------------------------------------------------------------------- 1 | import { ChannelWrapper, ChannelWrapperImp } from '../ChannelWrapper'; 2 | 3 | /** 4 | * Supplies to the out channel all the values taken from the channel on which this method was called 5 | * return the out channel to allow pipe chaining 6 | * @param outCh the destination channel 7 | * @returns the destination channel 8 | */ 9 | function pipe(this: ChannelWrapper, outCh: ChannelWrapper): ChannelWrapper { 10 | 11 | (async () => { 12 | for await (const msg of this) { 13 | await outCh.put(msg); 14 | } 15 | })(); 16 | 17 | return outCh; 18 | } 19 | 20 | ChannelWrapperImp.prototype.pipe = pipe; 21 | 22 | declare module '../ChannelWrapper' { 23 | interface ChannelWrapperImp { 24 | pipe: typeof pipe; 25 | } 26 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["es2018"], 5 | "module": "None", 6 | "moduleResolution": "Node", 7 | "target": "ES2017", 8 | "preserveConstEnums": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitAny": true, 12 | "noImplicitUseStrict": true, 13 | "downlevelIteration": true, 14 | "declaration": true 15 | }, 16 | "include": [ "src/**/*" ], 17 | "exclude": [ 18 | "node_modules", 19 | "test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2018" 5 | ], 6 | "module": "None", 7 | "moduleResolution": "Node", 8 | "target": "ES2017", 9 | "outDir": "./dist", 10 | "preserveConstEnums": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitAny": true, 14 | "noImplicitUseStrict": true, 15 | "downlevelIteration": true, 16 | "declaration": true 17 | }, 18 | "include": [ 19 | "src/**/*", 20 | "test/**/*" 21 | ], 22 | "typedocOptions": { 23 | "mode": "modules", 24 | "out": "docs/.vuepress/dist/api" 25 | } 26 | } --------------------------------------------------------------------------------