├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── 404.html ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conf ├── rollup │ ├── base.js │ ├── es5.js │ └── es6.js └── typescript │ ├── base.json │ ├── build.json │ ├── es5.json │ ├── es6.json │ └── test.json ├── docs ├── agent.html ├── assets │ ├── 404-dark.svg │ ├── 404.svg │ ├── animations │ │ ├── .light-to-dark │ │ ├── control-dark.svg │ │ ├── control.svg │ │ ├── filter-dark.svg │ │ ├── filter.svg │ │ ├── map-dark.svg │ │ ├── map.svg │ │ ├── pack-dark.svg │ │ ├── pack.svg │ │ ├── pin-dark.svg │ │ └── pin.svg │ ├── copy-blue.svg │ ├── copy.svg │ ├── favicon.ico │ ├── gitter.svg │ ├── link-dark.svg │ ├── link.svg │ ├── logo-notype.svg │ ├── logo-type-dark.svg │ ├── logo-unframed.svg │ ├── main.js │ ├── search-handle.svg │ ├── search-ring.svg │ └── styles.css ├── check.html ├── composition.html ├── connective-v-rxjs.html ├── control.html ├── deep.html ├── emission.html ├── expr.html ├── filter.html ├── fork.html ├── gate.html ├── generate.js ├── group.html ├── handle-error.html ├── interfaces.html ├── invoke.html ├── join.html ├── map.html ├── memory.html ├── node.html ├── nodemon.json ├── overview.html ├── pack.html ├── pin.html ├── pipe.html ├── proxy.html ├── reduce.html ├── sampler.html ├── sequence.html ├── serve.js ├── sink.html ├── source.html ├── spread.html ├── state.html ├── templates │ ├── 404.njk │ ├── _base.njk │ ├── agent.njk │ ├── check.njk │ ├── chunks │ │ ├── _footer.njk │ │ ├── _install-cdn.njk │ │ ├── _install-npm.njk │ │ ├── _nav.njk │ │ ├── _prevnext.njk │ │ ├── _quick-dive.njk │ │ └── main │ │ │ ├── _contact.njk │ │ │ └── _examples.njk │ ├── composition.njk │ ├── connective-v-rxjs.njk │ ├── control.njk │ ├── deep.njk │ ├── emission.njk │ ├── expr.njk │ ├── filter.njk │ ├── fork.njk │ ├── gate.njk │ ├── group.njk │ ├── handle-error.njk │ ├── index.njk │ ├── interfaces.njk │ ├── invoke.njk │ ├── join.njk │ ├── map.njk │ ├── memory.njk │ ├── node.njk │ ├── overview.njk │ ├── pack.njk │ ├── pin.njk │ ├── pipe.njk │ ├── proxy.njk │ ├── reduce.njk │ ├── sampler.njk │ ├── sequence.njk │ ├── sink.njk │ ├── source.njk │ ├── spread.njk │ ├── state.njk │ ├── under-the-hood.njk │ ├── value.njk │ └── wrap.njk ├── under-the-hood.html ├── value.html └── wrap.html ├── index.html ├── logo.svg ├── package-lock.json ├── package.json ├── samples └── html │ ├── cool-fib.html │ ├── dblclick.html │ ├── delayed-broadcast.html │ ├── drag.html │ └── input-binding.html ├── src ├── agent │ ├── agent-like.ts │ ├── agent.ts │ ├── call.ts │ ├── check.ts │ ├── composition.ts │ ├── deep.ts │ ├── errors │ │ ├── child-not-defined.error.ts │ │ ├── child-type-mismatch.error.ts │ │ ├── improper-partial-flow.error.ts │ │ ├── insufficient-input.error.ts │ │ └── signature-mismatch.error.ts │ ├── expr.ts │ ├── gate.ts │ ├── handle-error.ts │ ├── index.ts │ ├── inline-composition.ts │ ├── invoke.ts │ ├── join.ts │ ├── keyed-deep.ts │ ├── node-like.ts │ ├── node-wrap.ts │ ├── node.ts │ ├── proxy.ts │ ├── sampler.ts │ ├── sequence.ts │ ├── signature.ts │ ├── simple-deep.ts │ ├── singleton.ts │ ├── state.ts │ ├── switch.ts │ └── test │ │ ├── agent-like.test.ts │ │ ├── call.test.ts │ │ ├── check.test.ts │ │ ├── composition.test.ts │ │ ├── expr.test.ts │ │ ├── gate.test.ts │ │ ├── handle-error.test.ts │ │ ├── index.ts │ │ ├── inline-composition.test.ts │ │ ├── invoke.test.ts │ │ ├── join.test.ts │ │ ├── keyed-deep.test.ts │ │ ├── node-like.test.ts │ │ ├── node-wrap.test.ts │ │ ├── node.test.ts │ │ ├── proxy.test.ts │ │ ├── sequence.test.ts │ │ ├── signature.test.ts │ │ ├── simple-deep.test.ts │ │ ├── singleton.test.ts │ │ ├── state.spec.ts │ │ ├── state.test.ts │ │ └── switch.test.ts ├── index.ts ├── pin │ ├── base.ts │ ├── connectible.ts │ ├── control.ts │ ├── errors │ │ ├── group-subscription.ts │ │ ├── locked.error.ts │ │ └── unresolved-observable.error.ts │ ├── filter.ts │ ├── fork.ts │ ├── group.ts │ ├── index.ts │ ├── map.ts │ ├── pack.ts │ ├── partial-flow.ts │ ├── pin-like.ts │ ├── pin-map.ts │ ├── pin.ts │ ├── pipe.ts │ ├── reduce.ts │ ├── sink.ts │ ├── source.ts │ ├── spread.ts │ ├── test │ │ ├── control.test.ts │ │ ├── filter.test.ts │ │ ├── fork.test.ts │ │ ├── group.test.ts │ │ ├── index.ts │ │ ├── map.test.ts │ │ ├── pack.test.ts │ │ ├── partial-flow.test.ts │ │ ├── pin-like.test.ts │ │ ├── pin-map.test.ts │ │ ├── pin.test.ts │ │ ├── reduce.test.ts │ │ ├── sink.test.ts │ │ ├── source.test.ts │ │ ├── spread.test.ts │ │ ├── value.test.ts │ │ └── wrap.test.ts │ ├── value.ts │ └── wrap.ts ├── shared │ ├── bindable.ts │ ├── clearable.ts │ ├── emission.ts │ ├── errors │ │ └── emission-error.ts │ ├── index.ts │ ├── test │ │ ├── bindable.test.ts │ │ ├── clearable.test.ts │ │ ├── emission.test.ts │ │ ├── index.ts │ │ └── tracker.test.ts │ ├── tracker.ts │ └── types.ts ├── test │ └── index.ts └── util │ ├── keyed-array-diff.ts │ └── random-tag.ts ├── test.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | Describe the bug as precisely as you can. Describe _what_ is the behavior you consider a bug. 12 | 13 | ### To Reproduce 14 | A way to reproduce the behavior. Ideally (in order of ideality): 15 | - A pull-request including a failing test 16 | - A code snippet executable independently (for example, on [stackblitz](https://stackblitz.com)) 17 | - A piece of code that highlights the issue 18 | 19 | ### Expected behavior 20 | Describe what do you expect to happen (like what the behavior of the code snippet you provided 21 | should be). In case you provide a pull-request for a test, you don't need this. 22 | 23 | ### Additional context 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | ### Why should it be added? 12 | Describe why do you need this feature, i.e. its cumbersome to do something that is of common-use, it is not possible to this other thing you want to do, etc. 13 | 14 | ### What should be added? 15 | Describe what is the feature you need. If applicable any of the following would also be extremely helpful: 16 | - A pull-request containing a failing test, that would pass if the feature is added, 17 | - A gist or code-snippet, describing how the change in the API would look like, 18 | - An independently executable code snippet (like on [stackblitz](stackblitz.com)) to elaborate what would the feature do. 19 | 20 | ### Additional context 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | samples/future/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | connective.dev -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Pledge 4 | 5 | We, as maintainers and contributors, pledge to ensure that everyone interested in 6 | contributing to this project feels encouraged and welcome to do so. This means ensuring that no one 7 | feels discouraged or barred from contributing to this project, regardless of their background, 8 | gender, ethnicity, belief, political alignment, age, keyword-per-minute record, shoe-size, 9 | their interest level in the project, etc, 10 | and regardless of the type and extent of their contribution, whether it is a passing and vague comment without 11 | any further elaboration or whether it is an extensive pull-request changing half of the code-base. 12 | 13 | ## Our Standards 14 | 15 | Following are guidelines that help maintain and grow a welcoming and effective community: 16 | 17 | - Reflect opinions on codes and words, not the person behind those codes/words. 18 | - Be tolerant of criticism. Do not take it personally, unless it is already breaking the first guideline. 19 | - Remain focused on progressing the discussion. Change your tone/perspective the moment you realise its not helping. 20 | - Genuinely be open to and embrace being proven wrong. 21 | 22 | The following are note-worthy examples of unacceptable behavior: 23 | 24 | - Any form of harassment towards any other participant, either public or private 25 | - Intentional use of language/imagery/etc counter-productive to the progress of the discussion 26 | - Any form of trolling, or any other destructive form of communication 27 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 28 | 29 | 30 | ## Our Responsibilities 31 | 32 | Project maintainers are responsible for clarifying the guidelines for acceptable 33 | behavior and are expected to take appropriate action in response to any instances 34 | of unacceptable behavior, or any behavior that might steer the community away 35 | from being a welcoming one for everyone. 36 | 37 | Project maintainers have the right and responsibility to remove, edit, or 38 | reject comments, commits, code, wiki edits, issues, and other contributions 39 | that are not aligned to this Code of Conduct, or to ban temporarily or 40 | permanently any contributor for other behaviors that they deem inappropriate, 41 | threatening, offensive, or harmful. 42 | 43 | ## Scope 44 | 45 | This Code of Conduct applies both within project spaces and in public spaces 46 | when an individual is representing this project or its community. Examples of 47 | representing a project or community include using an official project e-mail 48 | address, posting via an official social media account, or acting as an appointed 49 | representative at an online or offline event. Representation of a project may be 50 | further defined and clarified by project maintainers. 51 | 52 | ## Enforcement 53 | 54 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 55 | reported by contacting the project team at ghanizadeh.eugene@gmail.com. All 56 | complaints will be reviewed and investigated and will result in a response that 57 | is deemed necessary and appropriate to the circumstances. The project team is 58 | obligated to maintain confidentiality with regard to the reporter of an incident. 59 | Further details of specific enforcement policies may be posted separately. 60 | 61 | Project maintainers who do not follow or enforce the Code of Conduct in good 62 | faith may face temporary or permanent repercussions as determined by other 63 | members of the project's leadership. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 68 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 69 | 70 | [homepage]: https://www.contributor-covenant.org 71 | 72 | For answers to common questions about this code of conduct, see 73 | https://www.contributor-covenant.org/faq 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CONNECT platform 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /conf/rollup/base.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'dist/es6/index.js', 3 | output: { 4 | name: 'connective', 5 | format: 'iife', 6 | globals: { 7 | 'rxjs': 'rxjs', 8 | 'rxjs/operators': 'rxjs.operators', 9 | 'lodash.isequal': '_.isEqual', 10 | } 11 | }, 12 | external: ['rxjs', 'rxjs/operators', 'lodash.isequal'], 13 | } 14 | -------------------------------------------------------------------------------- /conf/rollup/es5.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import { uglify } from "rollup-plugin-uglify"; 3 | import base from './base'; 4 | 5 | 6 | export default Object.assign(base, { 7 | plugins: [ 8 | babel({ exclude: 'node_modules/**', presets: ["@babel/preset-env"],}), 9 | uglify(), 10 | ], 11 | output: Object.assign(base.output, { 12 | file: 'dist/bundles/connective.es5.min.js', 13 | }), 14 | }); 15 | -------------------------------------------------------------------------------- /conf/rollup/es6.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import base from './base'; 3 | 4 | 5 | export default Object.assign(base, { 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: Object.assign(base.output, { 10 | file: 'dist/bundles/connective.es6.min.js', 11 | }), 12 | }); 13 | -------------------------------------------------------------------------------- /conf/typescript/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "strictNullChecks": true, 5 | "strictFunctionTypes": true, 6 | "noImplicitThis": true, 7 | "alwaysStrict": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "typeRoots": [ 16 | "../../node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2017" 20 | ] 21 | }, 22 | "include": [ 23 | "../../src/**/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /conf/typescript/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base", 3 | "exclude": [ 4 | "../../test.ts", 5 | "../../src/test/**/*", 6 | "../../src/**/test/*", 7 | "../../src/**/*.test.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /conf/typescript/es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "../../dist/es5/" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /conf/typescript/es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build", 3 | "compilerOptions": { 4 | "inlineSources": true, 5 | "target": "es6", 6 | "outDir": "../../dist/es6/" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /conf/typescript/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build", 3 | "compilerOptions": { 4 | "target": "es5" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/assets/404-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404-dark 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/assets/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/assets/animations/.light-to-dark: -------------------------------------------------------------------------------- 1 | #e0e0e0 -> #424242 2 | #eeeeee -> #424242 3 | #757575 -> #e0e0e0 4 | -------------------------------------------------------------------------------- /docs/assets/copy-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/assets/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CONNECT-platform/connective/f1fe472aa9ca4a12468c4fc9b800897ae246fb9e/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/gitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gitter 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/assets/link-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | link 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/assets/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/logo-unframed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/assets/search-handle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | search 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/assets/search-ring.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | search 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/generate.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const nunjucks = require('nunjucks'); 4 | 5 | const root = path.join(__dirname, '../'); 6 | const templatePath = path.join(__dirname, 'templates'); 7 | const targetPath = path.join(root, 'docs/'); 8 | 9 | nunjucks.configure(templatePath); 10 | 11 | const render = (filename, outfile) => { 12 | console.log(`RENDERING ${filename} to ${outfile}`); 13 | let rendered = nunjucks.render(filename + '.njk'); 14 | fs.writeFile(outfile, rendered, error => { 15 | if (error) { 16 | console.log('ERROR: could not render ' + filename); 17 | console.log(error); 18 | } 19 | }); 20 | } 21 | 22 | 23 | module.exports = () => { 24 | console.log('GENERATING DOCS ...'); 25 | render('index', path.join(root, 'index.html')); 26 | render('404', path.join(root, '404.html')); 27 | 28 | let files = fs.readdirSync(templatePath) 29 | .filter(file => file.endsWith('.njk')) // only get templates 30 | .filter(file => !file.startsWith('_')) // remove the parent templates 31 | .map(file => file.substr(0, file.length - 4)) // remove the extension 32 | .filter(file => file != 'index' && file != '404') // index and 404 are already rendered 33 | ; 34 | 35 | files.forEach(file => render(file, path.join(targetPath, file + '.html'))); 36 | 37 | console.log('CLEANING UP ...'); 38 | 39 | fs.readdirSync(targetPath) 40 | .filter(file => file.endsWith('.html')) 41 | .map(file => file.substr(0, file.length - 5)) 42 | .filter(file => !files.includes(file)) 43 | .forEach(file => { 44 | console.log('REMOVING ' + file); 45 | fs.unlink(path.join(targetPath, file + '.html'), error => { 46 | if (error) { 47 | console.log('COULD NOT CLEAN ' + file); 48 | console.log(error); 49 | } 50 | }); 51 | }); 52 | 53 | console.log('DOCS GENERATED'); 54 | } 55 | -------------------------------------------------------------------------------- /docs/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": "templates/", 3 | "ext": "njk", 4 | "verbose": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/serve.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const app = express(); 4 | const generate = require('./generate'); 5 | 6 | const port = 3000; 7 | const root = path.join(__dirname, '../'); 8 | 9 | app.use(express.static('.')); 10 | 11 | app.get('/*', (req, res) => { 12 | res.sendFile(path.join(root, req.originalUrl + '.html'), {}, err => { 13 | if (err) res.sendFile(path.join(root, '404.html')); 14 | }); 15 | }); 16 | 17 | generate(); 18 | 19 | app.listen(port, () => { 20 | console.log(`CONNECTIVE docs being served on http://localhost:${port}`); 21 | }); 22 | -------------------------------------------------------------------------------- /docs/templates/404.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |
5 |


6 |

Page Not Found

7 |

Looks like the page you are looking for have ceased to exist, doesn't exist yet, or 8 | never had and never will exist outside of the history of a day-dreaming browser. 9 | Either way, rest a moment, enjoy the sunmoon, 10 | then head back home as we've got more tales of awesome 11 | for your weary ears.

12 |
13 | Go Back 14 | Home 15 |
16 |

17 | 18 |
19 | 20 | 21 |
22 |
23 | 24 | {% endblock %} 25 | 26 | {% block bottomlogo %} 27 | {% endblock %} 28 | 29 | {% block prevnext %} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /docs/templates/_base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CONNECTIVE 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 |
40 | 41 | {% block content %} 42 | {% endblock %} 43 |

44 | 45 | {% block prevnext %} 46 | {% include 'chunks/_prevnext.njk' %} 47 | {% endblock %} 48 | 49 | {% block bottomlogo %} 50 | 51 | {% endblock %} 52 |
53 | 54 |
55 |
56 | 57 | {% include 'chunks/_nav.njk' %} 58 | 59 | {% include 'chunks/_footer.njk' %} 60 | 61 |
62 | Copied to Clipboard! 63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/templates/check.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Check

7 | 8 | A check() is like a filter(), 9 | except that it has two outputs, one for passing values and one for failing values: 10 | 11 |
12 | /*!*/import { wrap, check, sink } from '@connectv/core';
13 | import { interval } from 'rxjs';
14 | 
15 | let even = document.getElementById('even');
16 | let odd = document.getElementById('odd');
17 | 
18 | 
19 | wrap(interval(1000))
20 | /*!*/.to(check(x => x % 2 == 0))  //--> separate by being even or odd
21 | /*!*/.serialTo(
22 | /*!*/  sink(v => even.innerHTML += ' ' + v),
23 | /*!*/  sink(v => odd.innerHTML += ' ' + v)
24 | ).subscribe();
25 | 
26 | 27 | If you need to connect to a check() explicitly instead of 28 | implicitly, you can do it like this: 29 | 30 |
let c = check(x => x % 2 == 0);
31 | 
32 | wrap(interval(1000)).to(c.input);
33 | c.pass.subscribe(v => even.innerHTML += ' ' + v);
34 | c.fail.subscribe(v => even.innerHTML += ' ' + v);
35 | 36 |

37 | 38 | Similar to filter(), you can pass 39 | asynchronous predicates to check(): 40 | 41 |
42 | /*!*/import { wrap, check, sink } from '@connectv/core';
43 | import { interval } from 'rxjs';
44 | 
45 | let timer = document.getElementById('timer');
46 | let even = document.getElementById('even');
47 | let odd = document.getElementById('odd');
48 | 
49 | 
50 | wrap(interval(500))
51 | .to(sink(v => timer.innerHTML = v))          //--> display timer for reference
52 | /*!*/.to(check((x, done) =>                       //--> return results with a delay
53 | /*!*/  setTimeout(() => done(x % 2 == 0), 2000)))
54 | .serialTo(
55 |   sink(v => even.innerHTML += ' ' + v),
56 |   sink(v => odd.innerHTML += ' ' + v)
57 | ).subscribe();
58 | 
59 | 60 |

Signature

61 | 62 | Every check() has one "value" input, 63 | one "pass" output and one "fail" output: 64 | 65 |
let c = check(x => x % 2 == 0);
66 | 
67 | x.in('value') == x.input;
68 | x.out('pass') == x.pass;
69 | x.out('fail') == x.fail;
70 | 71 |

72 | 73 |

Further reading

74 | 75 | 87 | 88 | {% endblock %} -------------------------------------------------------------------------------- /docs/templates/chunks/_footer.njk: -------------------------------------------------------------------------------- 1 | 25 | 26 | 32 | 33 | 34 | 35 |
36 |
-------------------------------------------------------------------------------- /docs/templates/chunks/_install-cdn.njk: -------------------------------------------------------------------------------- 1 |
<!-- Click on each line to copy it -->
2 | 
3 | <!-- Dependencies -->
4 | <script src="https://unpkg.com/rxjs/bundles/rxjs.umd.min.js"></script>
5 | <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.14/lodash.min.js"></script>
6 | 
7 | <script src="https://unpkg.com/@connectv/core/dist/bundles/connective.es5.min.js"></script>
8 | -------------------------------------------------------------------------------- /docs/templates/chunks/_install-npm.njk: -------------------------------------------------------------------------------- 1 |
npm i @connectv/core
2 | -------------------------------------------------------------------------------- /docs/templates/chunks/_nav.njk: -------------------------------------------------------------------------------- 1 | 94 | -------------------------------------------------------------------------------- /docs/templates/chunks/_prevnext.njk: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /docs/templates/chunks/main/_contact.njk: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /docs/templates/chunks/main/_examples.njk: -------------------------------------------------------------------------------- 1 | A Hellow World! example: 2 |
 4 | /*!*/import { wrap, map, filter } from '@connectv/core';
 5 | import { fromEvent } from 'rxjs';
 6 | 
 7 | let a = document.getElementById('a') as HTMLInputElement;
 8 | let p = document.getElementById('p');
 9 | 
10 | //
11 | // Will say hello to everyone but 'Donald'.
12 | // For obvious reasons.
13 | //
14 | 
15 | /*!*/wrap(fromEvent(a, 'input'))           // --> wrap the `Observable` in a `Pin`
16 | /*!*/.to(map(() => a.value))               // --> map the event to value of the input
17 | /*!*/.to(filter(name => name != 'Donald')) // --> filter 'Donald' out
18 | /*!*/.to(map(name => 'hellow ' + name))    // --> add 'hellow' to the name
19 | /*!*/.subscribe(msg => p.innerHTML = msg); // --> write it to the <p> element
20 | 
21 | 22 | A more elaborate example: 23 |
25 | /*!*/import { wrap, pipe, map, filter, sink } from '@connectv/core';
26 | import { fromEvent, timer } from 'rxjs';
27 | import { delay, debounceTime } from 'rxjs/operators';
28 | 
29 | let a = document.getElementById('a');
30 | let p = document.getElementById('p');
31 | 
32 | //
33 | // Will calculate fibonacci sequence up to given index, displaying every number in the
34 | // sequence along the way.
35 | //
36 | 
37 | // --> calculate next iteration step on fibonacci sequence
38 | /*!*/let m = map(([next, prev, l]) => [next + prev, next, l - 1]);
39 | 
40 | /*!*/wrap(fromEvent(a, 'input'))                    // --> wrap the `Observable` in a `Pin`
41 | /*!*/.to(pipe(debounceTime(1000)))                // --> wait for people to type in the number
42 | /*!*/.to(map(() => parseInt((a as any).value)))   // --> map the input event to value of the input
43 | /*!*/.to(map(n => [1, 0, n]))                     // --> map the number to start iteration
44 | /*!*/.to(filter(([_, __, l]) => l >= 0))          // --> check if we should do any iteration
45 | /*!*/.to(m)                                       // --> calculate next step
46 | /*!*/.to(pipe(delay(300)))                        // --> take a breath
47 | /*!*/.to(filter(([_, __, l]) => l > 0))           // --> check if we should continue
48 | /*!*/.to(m)                                       // --> back to the loop
49 | /*!*/.to(map(([_, f, __]) => f))                  // --> btw, lets take each number in the sequence
50 | /*!*/.to(sink(v => p.innerHTML = v))              // --> set the text of <p> to the fib number
51 | /*!*/.subscribe();                                // --> bind the whole thing.
52 | 
53 | -------------------------------------------------------------------------------- /docs/templates/group.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Group

7 | 8 | group() allows you to perform some operations on a group of 9 | pins instead of just one of them. Because of this, 10 | a group() can act like a pin in a lot of cases. 11 | 12 |

13 | 14 | Whenever you call .to() and .from() methods of a pin, the result will 15 | be a group of pins containing all pins you passed to the method. 16 | 17 |

18 | 19 |

Connecting

20 | 21 | When you use .to() or .from() methods on a group, all the pins in the group 22 | will be connected to (or receive a connection from) the given pin: 23 | 24 |
25 |   /*!*/import { source, group, map } from '@connectv/core';
26 | 
27 | let a = source();
28 | /*!*/let g = group(map(x => 'x' + x), map(x => 'y' + x));
29 | a.to(g).subscribe(console.log); //--> a goes to both `map()`s in g
30 | 
31 | a.send('A');
32 | 
33 | 34 | When you call .to() on a group passing another group to it (or multiple pins), all pins in the 35 | first group will be connected to the all pins in the second group: 36 | 37 |
38 |   /*!*/import { source, group, map, pin } from '@connectv/core';
39 | 
40 | let a = source();
41 | /*!*/let g1 = group(map(x => 'x' + x), map(x => 'y' + x));
42 | /*!*/let g2 = group(map(x => 'a' + x), map(x => 'b' + x));
43 | /*!*/
44 | /*!*/a.to(g1).to(g2).to(pin()).subscribe(console.log);
45 | 
46 | a.send(1);
47 | 
48 | 49 |

Subscribing

50 | 51 | You can use the .subscribe() method on a group to subscribe to all of its pins: 52 | 53 |
54 |   /*!*/import { source, group } from '@connectv/core';
55 | 
56 | let a = source();
57 | let b = source();
58 | 
59 | /*!*/group(a, b).subscribe(console.log);
60 | 
61 | a.send('hellow');
62 | b.send('world');
63 | 
64 | 65 | Similarly, you can call .bind() method on a group. Note that this will only affect pins that 66 | have a .bind() method (other pins in the group will remain unaffected). 67 | 68 |

69 | 70 |

Clearing up

71 | 72 | For clearing up, you can also call .clear() method on a group, which will simply invoke 73 | the .clear() method of all of the pins in it. 74 | 75 |

76 | 77 |

Further reading

78 | 79 | 91 | 92 | {% endblock %} 93 | -------------------------------------------------------------------------------- /docs/templates/handle-error.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

HandleError

7 | 8 | By default, if an error is thrown somewhere in a reactive flow, the whole flow or parts of it will shut-down afterwards. 9 | handleError() allows you to catch errors and handle them gracefully: 10 | 11 |
 12 |   /*!*/import { source, map, handleError, sink } from '@connectv/core';
 13 | 
 14 | let a = source();
 15 | 
 16 | //
 17 | //--> this flow does not have error handling, so it will die
 18 | //--> out after an error occurs..
 19 | //
 20 | a.to(map(x => {
 21 | /*!*/  if (x == 2) throw new Error();
 22 |   else return x;
 23 | })).subscribe(v => console.log('A:: ' + v));
 24 | 
 25 | //
 26 | //--> this flow has error handling, so it will continue after
 27 | //--> an error occurs.
 28 | //
 29 | a.to(map(x => {
 30 | /*!*/  if (x == 2) throw new Error();
 31 |   else return x;
 32 | }))
 33 | /*!*/.to(handleError())
 34 | .serialTo(sink(v => console.log('B:: ' + v)))
 35 | .subscribe();
 36 | 
 37 | a.send(1);  //--> logged by both
 38 | a.send(2);  //--> ignored by both
 39 | a.send(3);  //--> logged by 'B:: ' only, since 'A:: ' flow is dead.
 40 | 
41 | 42 |

Catching errors

43 | 44 | Accessing the thrown error object with implicit connection 45 | looks like this: 46 | 47 |
 48 |   /*!*/import { source, map, group, handleError, pin } from '@connectv/core';
 49 | 
 50 | let a = source();
 51 | 
 52 | a.to(map(x => {
 53 |   if (x == 2) throw new Error();
 54 |   else return x;
 55 | }))
 56 | /*!*/.to(handleError())
 57 | /*!*/.serialTo(
 58 | /*!*/  pin(),                                     //--> the usual output
 59 | /*!*/  map(e => `error for  ${e.emission.value}`) //--> the error output
 60 | ).subscribe(console.log);
 61 | 
 62 | a.send(1);
 63 | a.send(2);
 64 | a.send(3);
 65 | 
66 | 67 | You could have also accessed it explicitly via its .out("error") output 68 | (or using .error shortcut property): 69 | 70 |
/*!*/let h = handleError();
 71 | 
 72 | ...
 73 | 
 74 | /*!*/h.error
 75 |   .to(map(e => `error for ${e.emission.value}`))
 76 |   .subscribe(console.log);
 77 | 
78 | 79 |

80 | 81 |

Signature

82 | 83 | Each handleError() has an "input" input (short-hand property: .input), 84 | on which it receives incoming emissions. When no error has occured, it will simply relay the emission on its 85 | "output" output (short-hand property: .output). In case of error, the error object 86 | will be emitted via its "error" output (short-hand property: .error): 87 | 88 |
let h = handleError();
 89 | 
 90 | h.in("input") == h.input    //--> receives emissions on this
 91 | h.out("output") == h.output //--> emits them on this where there is no error
 92 | h.out("error") == h.error   //--> emits errors on this pin.
93 | 94 |

95 | 96 |

Further reading

97 | 98 | 110 | 111 | {% endblock %} 112 | -------------------------------------------------------------------------------- /docs/templates/pack.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Pack

7 | 8 | pack() emits with the latest value of all sources, packed together. 9 |
10 | 11 | 12 |
13 | 14 | You can use pack() to collect values from multiple sources: 15 | 16 |
17 |   /*!*/import { wrap, pack, group, map } from '@connectv/core';
18 | import { fromEvent } from 'rxjs';
19 | 
20 | let s = document.getElementById('s') as HTMLInputElement;
21 | let n = document.getElementById('n') as HTMLInputElement;
22 | let p = document.getElementById('p');
23 | 
24 | group(
25 |   wrap(fromEvent(s, 'input')).to(map(() => s.value)),  //--> pick a salute
26 |   wrap(fromEvent(n, 'input')).to(map(() => n.value))   //--> pick a name
27 | )
28 | /*!*/.to(pack())
29 | .to(map(v => v.join(' ')))                             //--> pack returns an array, lets join it
30 | .subscribe(v => p.innerHTML = v);                      //--> say hi
31 | 
32 | 33 |

First emission

34 | 35 | pack() waits for all connected sources to emit at least once before its first emission: 36 | 37 |
38 |   /*!*/import { wrap, pack, filter, map, group } from '@connectv/core';
39 | import { fromEvent, interval } from 'rxjs';
40 | 
41 | let a = document.getElementById('a') as HTMLInputElement;
42 | let b = document.getElementById('b') as HTMLInputElement;
43 | let p = document.getElementById('p');
44 | 
45 | group(
46 |   wrap(interval(1000)),
47 |   wrap(fromEvent(a, 'input')).to(map(() => a.checked)),
48 |   wrap(fromEvent(b, 'input')).to(map(() => b.checked))
49 | )
50 | /*!*/.to(pack())
51 | .to(filter(v => v[1] && v[2]))       //--> only let values through if both checkboxes are checked
52 | .to(map(v => v[0]))                  //--> map to the interval's value
53 | .subscribe(v => p.innerHTML = v);    //--> display the beautiful
54 | 
55 | 56 |

57 | 58 |

Further reading

59 | 60 | 72 | 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /docs/templates/pipe.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Pipe

7 | 8 | pipe() allows you to use any of RxJS's 9 | 10 | pipeable operators 11 | 12 | in your reactive flows: 13 | 14 |
15 |   /*!*/import { wrap, pipe, map } from '@connectv/core';
16 | import { fromEvent } from 'rxjs';
17 | /*!*/import { throttleTime } from 'rxjs/operators';
18 | 
19 | let a = document.getElementById('a') as HTMLInputElement;
20 | let p = document.getElementById('p');
21 | 
22 | wrap(fromEvent(a, 'input'))
23 | .to(map(() => a.value))             //--> get the input value
24 | /*!*/.to(pipe(throttleTime(1000)))       //--> throttle a bit
25 | .subscribe(v => p.innerHTML = v);
26 | 
27 | 28 |

Emissions

29 | 30 | The operators are not passed raw emitted data/events, but rather Emission 31 | objects that contain them: 32 | 33 |
34 |   /*!*/import { value, spread, pipe } from '@connectv/core';
35 | /*!*/import { tap } from 'rxjs/operators';
36 | 
37 | value([1, 2, 3, 4])
38 | .to(spread())
39 | .subscribe(console.log);      //--> values are logged
40 | 
41 | value([1, 2, 3, 4])
42 | .to(spread())
43 | /*!*/.to(pipe(tap(console.log)))   //--> `Emission` objects are logged
44 | .subscribe();
45 | 
46 | 47 | In a lot of cases you don't need to work with incoming emissions directly. In cases that you do, you can 48 | access the actual value of the emission using its .value property: 49 | 50 |
51 |   import { value, spread, pipe } from '@connectv/core';
52 | import { timer } from 'rxjs';
53 | /*!*/import { delayWhen } from 'rxjs/operators';
54 | 
55 | value([1, 2, 3, 4])
56 | .to(spread())
57 | /*!*/.to(pipe(delayWhen(e => timer(1000 - e.value * 10)))) //--> delay proportional to inverse of the value
58 | /*!*/                                                      //... so that the array is reversed
59 | .subscribe(console.log);
60 | 
61 | 62 | The operators should also return observables that will emit emissions (Observable<Emission>). 63 | You can use .fork() method on an incoming emission to create a new one with a new value, 64 | or Emission.from() function to create an emission from multiple incoming emissions. 65 | 66 |

67 | 68 |

Further reading

69 | 70 | 77 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /docs/templates/reduce.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Reduce

7 | 8 | reduce() will aggregate incoming values using given aggregate function: 9 | 10 |
11 |   /*!*/import { value, spread, reduce } from '@connectv/core';
12 | 
13 | value([1, 2, 3, 4])
14 | .to(spread())
15 | /*!*/.to(reduce((total, each) => total + each))
16 | .subscribe(console.log);
17 | 
18 | 19 |

Initial value

20 | 21 | When no initial value is passed to reduce() (like the example above), the first incoming value will be used 22 | as the initial value. You can provide an initial value like this: 23 | 24 |
25 |   /*!*/import { value, spread, reduce } from '@connectv/core';
26 | 
27 | value([1, 2, 3, 4])
28 | .to(spread())
29 | /*!*/.to(reduce((total, each) => total * each, -1)) //--> so all values will be negative
30 | .subscribe(console.log);
31 | 
32 | 33 |

Function purity

34 | 35 | The aggregate function MUST be pure, i.e. it should give the same result with the same inputs. Impure 36 | aggregate functions might result in unpredictable flow behavior. 37 | 38 |

39 | 40 |

Further reading

41 | 42 | 54 | 55 | 56 | 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /docs/templates/sampler.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Sampler

7 | 8 | A sampler() will hold incoming values, passing the latest one when it receives a signal on its 9 | .control: 10 | 11 |
12 | /*!*/import { wrap, sampler, sink } from '@connectv/core';
13 | import { fromEvent, interval } from 'rxjs';
14 | 
15 | let t = document.getElementById('t');
16 | let p = document.getElementById('p');
17 | 
18 | /*!*/let s = sampler();
19 | 
20 | /*!*/wrap(fromEvent(document, 'click')).to(s.control); //--> sample on click
21 | 
22 | wrap(interval(1000))
23 | .to(sink(v => t.innerHTML = v))     //--> display the value of the timer
24 | /*!*/.to(s)                              //--> send it to sampler
25 | .subscribe(v => p.innerHTML = v);   //--> display sampled value
26 | 
27 | 28 |

Further reading

29 | 30 | 37 | 38 | {% endblock %} -------------------------------------------------------------------------------- /docs/templates/sink.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Sink

7 | 8 | A sink() acts as a consumer of incoming events/data: 9 | 10 |
11 |   /*!*/import { source, map, filter, sink } from '@connectv/core';
12 | 
13 | let a = source();
14 | /*!*/let b = sink(x => console.log(x));
15 | 
16 | a.to(map(x => x * 2)).to(b);
17 | a.to(filter(x => x % 2 == 0)).to(map(x => x * 10)).to(b);
18 | 
19 | /*!*/b.bind();
20 | a.send(2);
21 | a.send(3);
22 | 
23 | 24 | sink() has a .bind() method which will lock the sink and cause it to receive 25 | events/data from the rest of the flow. .bind() will lock the portion of the flow that the sink is reliant 26 | on, much like .subscribe() method. 27 | 28 |

29 | 30 | You can also place a sink in the middle of your reactive flow to do something according to incoming data/events without 31 | transforming them: 32 | 33 |
34 |   /*!*/import { wrap, group, map, filter, sink } from '@connectv/core';
35 | import { fromEvent } from 'rxjs';
36 | 
37 | let a = document.getElementById('a') as HTMLInputElement;
38 | let p = document.getElementById('p');
39 | 
40 | wrap(fromEvent(a, 'input'))
41 | .to(map(() => a.value))             //--> get the input value
42 | /*!*/.to(sink(x => console.log(x)))      //--> log it
43 | .to(filter(x => x % 2 == 1))        //--> filter for odd ones
44 | .subscribe(v => p.innerHTML = v);   //--> put them on the page
45 | 
46 | 47 |

Further reading

48 | 49 | 61 | 62 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /docs/templates/source.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Source

7 | 8 | You can use source() to manually send events/data to your reactive flows: 9 | 10 |
11 |   /*!*/import { source, map, group } from '@connectv/core';
12 | 
13 | /*!*/let a = source();
14 | /*!*/let b = source();
15 | 
16 | group(a, b).to(map(v => 'from:: ' + v)).subscribe(console.log);
17 | 
18 | /*!*/a.send('A');
19 | /*!*/b.send('B');
20 | 
21 | setInterval(() => a.send('A'), 1000);
22 | document.addEventListener('click', () => b.send('B'));
23 | 
24 | 25 | A source is a Pin and have all of its properties, though connecting to a source 26 | can lead to unpredictable behavior. 27 | 28 |

Clearing up

29 | 30 | Like other pin types, source() has a .clear() method that you should 31 | call when you are done with your source. This will clean up the flow and send the complete signal down 32 | the line for any such callback to handle: 33 | 34 |
35 |   import { source, pin } from '@connectv/core';
36 | 
37 | let a = source();
38 | 
39 | a.to(pin())
40 |  .subscribe(
41 |    v => { console.log('GOT:: ' + v); },  //--> this is the usual callback
42 |    error => { console.log('ERROR!'); },  //--> this is the error callback
43 | /*!*/   () => { console.log('COMPLETE!'); }   //--> this will be called when the flow is closed off
44 |  );
45 | 
46 | a.send(12);
47 | a.send('YOLO!');
48 | /*!*/a.clear();
49 | 
50 | 51 |

52 | 53 |

Further reading

54 | 55 | 67 | 68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /docs/templates/spread.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Spread

7 | 8 | spread() will spread an incoming array into multiple emissions: 9 | 10 |
11 |   /*!*/import { wrap, control, group, spread, gate, pipe, map, filter } from '@connectv/core';
12 | import { fromEvent } from 'rxjs';
13 | import { delay, debounceTime } from 'rxjs/operators';
14 | 
15 | let i = document.getElementById('i') as HTMLInputElement;
16 | let p = document.getElementById('p');
17 | 
18 | let g = gate();
19 | 
20 | wrap(fromEvent(i, 'input'))
21 | .to(pipe(debounceTime(1000)))        //--> wait for typing to finish
22 | .to(map(() => i.value.split(',')))   //--> split the string
23 | /*!*/.to(spread())                        //--> spread the comma separated list
24 | .to(map(x => x.trim()))              //--> trim each word
25 | .to(filter(x => x.length > 3))       //--> ignore super short ones
26 | .to(g.input);
27 | 
28 | group(control(), g.output)           //--> the control() is not connected
29 | .to(pipe(delay(1000)))               //... to anything, so it will emit initially
30 | .to(g.control);
31 | 
32 | g.output.subscribe(v => p.innerHTML = v);
33 | 
34 | 35 | spread() will just relay incoming values that are not arrays: 36 | 37 |
38 |   /*!*/import { source, spread } from '@connectv/core';
39 | 
40 | let a = source();
41 | /*!*/a.to(spread()).subscribe(console.log);
42 | 
43 | a.send([1, 2, 3]); //--> spread the values
44 | a.send(4);         //--> just relay 4
45 | a.send([5, 6]);    //--> spread again
46 | a.send(7);         //--> relay again
47 | 
48 | 49 |

Further reading

50 | 51 | 63 | 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /docs/templates/value.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Value

7 | 8 | value() is like control(), 9 | except it will emit the given value: 10 | 11 |
12 |   /*!*/import { wrap, value } from '@connectv/core';
13 | import { fromEvent } from 'rxjs';
14 | 
15 | let btn = document.getElementById('btn');
16 | 
17 | /*!*/wrap(fromEvent(btn, 'click')).to(value('HELLOW!')).subscribe(console.log);
18 | 
19 | 20 | A value() can also be the source of a flow: 21 | 22 |
23 |   /*!*/import { value, spread, pipe } from '@connectv/core';
24 | import { timer } from 'rxjs';
25 | import { delayWhen } from 'rxjs/operators';
26 | 
27 | /*!*/value([1, 2, 3, 4, 5])                 //--> start with this array
28 | .to(spread())                          //--> spread it
29 | .to(pipe(                              //--> delay based on value
30 |   delayWhen(                           //--> note that in pipe(), you get
31 |     e => timer(1000 - e.value * 100))  //... emissions not values,
32 |   )                                    //... hence 'e.value'
33 | )
34 | .subscribe(console.log);
35 | 
36 | 37 |

Further reading

38 | 39 | 51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /docs/templates/wrap.njk: -------------------------------------------------------------------------------- 1 | {% extends '_base.njk' %} 2 | 3 | {% block content %} 4 |

5 | 6 |

Wrap

7 | 8 | wrap() allows you to turn any RxJS observable into a pin and use it 9 | in your reactive flows: 10 | 11 |
12 |   /*!*/import { wrap, map, group } from '@connectv/core';
13 | import { interval, fromEvent } from 'rxjs';
14 | 
15 | group(
16 | /*!*/  wrap(interval(1000)),
17 | /*!*/  wrap(fromEvent(document, 'click'))
18 | )
19 | .to(map(v => 'GOT:: ' + v))
20 | .subscribe(console.log);
21 | 
22 | 23 | Wrap is a (special kind of) Pin and so inherits a lot of its properties and behaviors, 24 | though connecting another pin to it is prohibited. 25 | 26 |

27 | 28 |

Further reading

29 | 30 | 42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@connectv/core", 3 | "version": "0.2.8", 4 | "description": "agent-based reactive programming library for typescript/javascript", 5 | "keywords": [ 6 | "connective", 7 | "react", 8 | "reactive", 9 | "rx", 10 | "rxjs", 11 | "agent", 12 | "actor", 13 | "actor model", 14 | "async", 15 | "asynchronous", 16 | "event", 17 | "events", 18 | "stream", 19 | "flow", 20 | "event-flow", 21 | "event flow", 22 | "data flow", 23 | "data-flow" 24 | ], 25 | "homepage": "https://connective.dev", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/CONNECT-platform/connective.git" 29 | }, 30 | "author": "Eugene Ghanizadeh Khoub ", 31 | "license": "MIT", 32 | "bugs": "https://github.com/CONNECT-platform/connective/issues", 33 | "main": "dist/es5/index.js", 34 | "module": "dist/es6/index.js", 35 | "types": "dist/es6/index.d.ts", 36 | "scripts": { 37 | "build": "tsc -p conf/typescript/es5.json && tsc -p conf/typescript/es6.json", 38 | "pack": "rollup -c conf/rollup/es6.js && rollup -c conf/rollup/es5.js", 39 | "test": "ts-node --project conf/typescript/test.json test.ts", 40 | "docs": "nodemon ./docs/serve.js --config ./docs/nodemon.json" 41 | }, 42 | "files": [ 43 | "dist/es6", 44 | "dist/es5", 45 | "dist/bundles", 46 | "logo.svg" 47 | ], 48 | "sideEffects": false, 49 | "devDependencies": { 50 | "@babel/core": "^7.9.0", 51 | "@babel/preset-env": "^7.9.5", 52 | "@types/chai": "^4.2.11", 53 | "@types/mocha": "^5.2.7", 54 | "@types/node": "^12.12.35", 55 | "chai": "^4.2.0", 56 | "express": "^4.17.1", 57 | "mocha": "^6.2.3", 58 | "nodemon": "^2.0.3", 59 | "nunjucks": "^3.2.1", 60 | "rollup": "^1.32.1", 61 | "rollup-plugin-babel": "^4.4.0", 62 | "rollup-plugin-terser": "^5.3.0", 63 | "rollup-plugin-uglify": "^6.0.4", 64 | "ts-node": "^8.8.2", 65 | "tslib": "^1.11.1", 66 | "typescript": "^3.8.3" 67 | }, 68 | "dependencies": { 69 | "@types/lodash.isequal": "^4.5.5", 70 | "lodash.isequal": "^4.5.0", 71 | "rxjs": "^6.5.5" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /samples/html/cool-fib.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Enter a number

9 | 10 |

11 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /samples/html/dblclick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /samples/html/delayed-broadcast.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 |

type a sentence and wait (around 2 seconds) to see what happens.

14 | 15 |

16 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /samples/html/drag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /samples/html/input-binding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 | 12 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/agent/agent-like.ts: -------------------------------------------------------------------------------- 1 | import { Clearable } from '../shared/clearable'; 2 | 3 | import { PinLike } from '../pin/pin-like'; 4 | import { PinMap } from '../pin/pin-map'; 5 | 6 | import { isSignature, Signature } from './signature'; 7 | 8 | 9 | /** 10 | * 11 | * Denotes objects that can behave like an [agent](https://connective.dev/docs/agent). 12 | * 13 | */ 14 | export interface AgentLike extends Clearable { 15 | 16 | /** 17 | * 18 | * @param label 19 | * @returns the input pin corresponding to given label 20 | * @throws an error if given label is not allowed by the agent's 21 | * [signature](https://connective.dev/docs/agent#signature). 22 | * 23 | */ 24 | in(label: string | number): PinLike; 25 | 26 | /** 27 | * 28 | * @param label 29 | * @returns the output pin corresponding to given label 30 | * @throws an error if given label is not allowed by the agent's 31 | * [signature](https://connective.dev/docs/agent#signature). 32 | * 33 | */ 34 | out(label: string | number): PinLike; 35 | 36 | /** 37 | * 38 | * A `PinMap` object referencing all of input pins of the agent. 39 | * 40 | */ 41 | inputs: PinMap; 42 | 43 | /** 44 | * 45 | * A `PinMap` object referencing all of output pins of the agent. 46 | * 47 | */ 48 | outputs: PinMap; 49 | 50 | /** 51 | * 52 | * The [signature](https://connective.dev/docs/agent#signature) of the agent. 53 | * 54 | */ 55 | signature: Signature; 56 | } 57 | 58 | 59 | /** 60 | * 61 | * 62 | * @param whatever 63 | * @returns `true` if `whatever` satisfies `AgentLike` interface. 64 | * 65 | */ 66 | export function isAgentLike(whatever: any): whatever is AgentLike { 67 | return whatever !== undefined && (typeof whatever.in == 'function') && (typeof whatever.out == 'function') 68 | && whatever.inputs instanceof PinMap && whatever.outputs instanceof PinMap && 69 | whatever.signature !== undefined && isSignature(whatever.signature); 70 | } 71 | -------------------------------------------------------------------------------- /src/agent/call.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | import emission from '../shared/emission'; 4 | 5 | import map from '../pin/map'; 6 | import value from '../pin/value'; 7 | import source, { Source } from '../pin/source'; 8 | 9 | import { Agent } from './agent'; 10 | 11 | 12 | export type AgentFactory = () => Agent; 13 | export type ExecResult = { label: string; value: any }; 14 | 15 | 16 | /** 17 | * 18 | * Creates a [map](https://connective.dev/docs/map) pin. This map pin will 19 | * expect objects whose keys matches agents that will be created by given factory. 20 | * For each such object, the factory will be called and a new instance of the agent 21 | * will be created, the provided inputs (key-values of the incoming object) will 22 | * be fed to its inputs, and its first ouput will be passed on. 23 | * 24 | * @param factory the agent factory to create new instances per incoming object 25 | * @param sub a callback to handle the subscription object holding the reference to all 26 | * subscriptions created in response to each incoming object 27 | * @param unsub a callback to handle when the created subscriptions of each incoming 28 | * object are unsubscribed from 29 | * @param outs an optional function to be used to determine possible outputs instead of utilizing 30 | * each created agent's signature. 31 | * 32 | */ 33 | export function exec(factory: AgentFactory, 34 | sub?: (s: Subscription) => void, 35 | unsub?: (s: Subscription) => void, 36 | outs?: () => string[] 37 | ) { 38 | return map((data, done, error, context) => { 39 | let _agent = factory(); 40 | let _sources = <{[input: string]: Source}>{}; 41 | let _subs = new Subscription(); 42 | 43 | let _cleanup = () => { 44 | Object.values(_sources).forEach(s => s.clear()); 45 | _agent.clear(); 46 | _subs.unsubscribe(); 47 | if (unsub) unsub(_subs); 48 | }; 49 | 50 | if (data) 51 | Object.keys(data).forEach((input) => _agent.in(input).from(_sources[input] = source())); 52 | 53 | let _outs = _agent.signature.outputs || []; 54 | if (outs) _outs = outs(); 55 | _outs.forEach((label) => { 56 | _subs.add(_agent.out(label).subscribe( 57 | value => { _cleanup(); done({ label, value }); }, 58 | err => { _cleanup(); error(err); } 59 | )); 60 | }); 61 | 62 | if (sub) sub(_subs); 63 | 64 | if (data) 65 | Object.entries(data).forEach(([input, value]) => _sources[input].emit(emission(value, context))); 66 | }); 67 | } 68 | 69 | 70 | /** 71 | * 72 | * Creates an agent using given agent factory, feed its inputs based on key-value 73 | * pairs of given data, and return a pin who will emit the first output of the created agent. 74 | * 75 | * @param factory 76 | * @param data 77 | * 78 | */ 79 | export function call(factory: AgentFactory, data: {[input: string]: any}) { return value(data).to(exec(factory)); } 80 | 81 | 82 | export default call; 83 | -------------------------------------------------------------------------------- /src/agent/check.ts: -------------------------------------------------------------------------------- 1 | import { PinLike } from '../pin/pin-like'; 2 | import { filter, FilterFunc, FilterFuncSync, FilterFuncAsync } from '../pin/filter'; 3 | import { map } from '../pin/map'; 4 | 5 | import { Agent } from './agent'; 6 | 7 | 8 | /** 9 | * 10 | * Represents [check](https://connective.dev/docs/check) agents. 11 | * 12 | */ 13 | export class Check extends Agent { 14 | private core: PinLike; 15 | 16 | /** 17 | * 18 | * @param predicate the predicate function to pass or fail incoming values against. 19 | * 20 | */ 21 | constructor(readonly predicate: FilterFunc) { 22 | super({ 23 | inputs: ['value'], 24 | outputs: ['pass', 'fail'] 25 | }); 26 | 27 | if (predicate.length <= 1) 28 | this.core = this.input.to(map((v: any) => [v, (predicate as FilterFuncSync)(v)])); 29 | else 30 | this.core = this.input.to(map((v : any, done, error, context) => 31 | predicate(v, res => done([v, res]), error, context))); 32 | } 33 | 34 | /** 35 | * 36 | * Shortcut for `.in('value')`, the main value input for this check. 37 | * [Read this](https://connective.dev/docs/check#signature) for more details. 38 | * 39 | */ 40 | public get input(): PinLike { return this.in('value'); } 41 | 42 | /** 43 | * 44 | * Shortcut for `.out('pass')`, the output for values passing the criteria outline by given predicate. 45 | * [Read this](https://connective.dev/docs/check#signature) for more details. 46 | * 47 | */ 48 | public get pass(): PinLike { return this.out('pass'); } 49 | 50 | /** 51 | * 52 | * Shortcut for `.out('fail')`, the output for values failing the criteria outline by given predicate. 53 | * [Read this](https://connective.dev/docs/check#signature) for more details. 54 | * 55 | */ 56 | public get fail(): PinLike { return this.out('fail'); } 57 | 58 | protected createOutput(label: string): PinLike { 59 | this.checkOutput(label); 60 | if (label == 'pass') { 61 | return this.core 62 | .to(filter(([_, v]: [any, boolean]) => v)) 63 | .to(map(([v, _]: [any, boolean]) => v)) 64 | } 65 | else { 66 | return this.core 67 | .to(filter(([_, v]: [any, boolean]) => !v)) 68 | .to(map(([v, _]: [any, boolean]) => v)) 69 | } 70 | } 71 | 72 | protected createEntries() { return [this.input]; } 73 | protected createExits() { return [this.pass, this.fail]; } 74 | } 75 | 76 | 77 | /** 78 | * 79 | * Creates a [check](https://connective.dev/docs/check) agent. A check agent 80 | * will pass or fail incoming values based on given predicate, passing them through 81 | * the corresponding outputs. 82 | * [Checkout the docs](https://connective.dev/docs/check) for examples and further information. 83 | * 84 | * @param func the predicate to test incoming values against 85 | * 86 | */ 87 | export function check(func: FilterFunc) { return new Check(func); } 88 | 89 | 90 | export default check; -------------------------------------------------------------------------------- /src/agent/deep.ts: -------------------------------------------------------------------------------- 1 | import { KeyFunc } from "../util/keyed-array-diff"; 2 | 3 | import { State } from "./state"; 4 | import { SimpleDeep, DeepChildFactory, DeepAccessor } from "./simple-deep"; 5 | import { KeyedDeep } from "./keyed-deep"; 6 | import { EqualityFunc } from "."; 7 | 8 | 9 | export function deep(state: State): SimpleDeep; 10 | export function deep(state: State, key: KeyFunc): KeyedDeep; 11 | /** 12 | * 13 | * Creates a [deep state](https://connective.dev/docs/deep) from given state. 14 | * You can track indexes, properties and keyed entities on deep states as bound 15 | * reactive states. 16 | * [Checkout the docs](https://connective.dev/docs/deep) for examples and further information. 17 | * 18 | * @param state the state to be used as the basis of the returned deep state 19 | * @param key the key function to be used to track entities in the deep state 20 | * 21 | */ 22 | export function deep(state: State, key?: KeyFunc): SimpleDeep | KeyedDeep { 23 | if (key) return new KeyedDeep(state, key); 24 | else return new SimpleDeep(state); 25 | } 26 | 27 | 28 | /** 29 | * 30 | * Returns a deep child factory that creates [keyed deep](https://connective.dev/docs/deep#keyed-deep) sub-states 31 | * with given key function. Pass this to `.sub()` or `.key()` on [deep states](https://connective.dev/docs/deep) 32 | * to have keyed sub-states. 33 | * 34 | * @param keyfunc the key function to be used 35 | * 36 | */ 37 | export function keyed(keyfunc: KeyFunc): DeepChildFactory { 38 | return (accessor: DeepAccessor, compare: EqualityFunc) => new KeyedDeep(accessor, keyfunc, compare); 39 | } 40 | 41 | 42 | export default deep; -------------------------------------------------------------------------------- /src/agent/errors/child-not-defined.error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This error is thrown when a non-defined child of a composition 4 | * is accessed. 5 | * 6 | */ 7 | export class ChildNotDefined extends Error { 8 | constructor(name: string) { 9 | super(`No child with name ${name} is defined.`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/agent/errors/child-type-mismatch.error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This error is thrown when a child of a composition is not an agent 4 | * but is accessed as one. 5 | * 6 | */ 7 | export class ChildIsNotAgent extends Error { 8 | constructor(name: string) { 9 | super(`Child ${name} is not an Agent.`); 10 | } 11 | } 12 | 13 | /** 14 | * 15 | * This error is thrown when a child of a composition is not a pin 16 | * but is accessed as one. 17 | * 18 | */ 19 | export class ChildIsNotPin extends Error { 20 | constructor(name: string) { 21 | super(`Child ${name} is not a Pin.`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/agent/errors/improper-partial-flow.error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This error is thrown when an agent which has no explicit specification 4 | * of its entry and exit pins is used as a partial flow. 5 | * [Read this](https://connective.dev/docs/agent#implicit-connection) for more 6 | * information on partial flows. 7 | * 8 | */ 9 | export class ImproperPartialFlow extends Error { 10 | constructor(object: any) { 11 | super(`${object.constructor?object.constructor.name:object} is not a properly defined PartialFlow. 12 | For more information, follow this link: 13 | https://connective.dev/docs/agent#implicit-connection`); 14 | } 15 | } -------------------------------------------------------------------------------- /src/agent/errors/insufficient-input.error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This error is thrown when a node is not provided with its 4 | * required inputs. 5 | * 6 | */ 7 | export class InsufficientInputs extends Error { 8 | constructor(readonly missing: string[]) { 9 | super(`Following inputs are missing from provided data: ${missing}. 10 | Read this for more information: 11 | https://connective.dev/docs/node#optional`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/agent/errors/signature-mismatch.error.ts: -------------------------------------------------------------------------------- 1 | import { Signature } from '../signature'; 2 | 3 | 4 | /** 5 | * 6 | * This error is thrown when a not matching input on a [signature](https://connective.dev/docs/agent#signature) 7 | * is accessed. 8 | * 9 | */ 10 | export class InputNotInSignature extends Error { 11 | constructor( 12 | readonly input: string, 13 | readonly signature: Signature 14 | ) { 15 | super(`Input ${input} not in signature {inputs: ${signature.inputs}}. 16 | Read this for more information: 17 | https://connective.dev/docs/agent#signature`); 18 | } 19 | } 20 | 21 | 22 | /** 23 | * 24 | * This error is thrown when a not matching output on a [signature](https://connective.dev/docs/agent#signature) 25 | * is accessed. 26 | * 27 | */ 28 | export class OutputNotInSignature extends Error { 29 | constructor( 30 | readonly output: string, 31 | readonly signature: Signature 32 | ) { 33 | super(`Output ${output} not in signature {outputs: ${signature.outputs}}. 34 | Read this for more information: 35 | https://connective.dev/docs/agent#signature`); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/agent/expr.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCallback, ContextType } from '../shared/types'; 2 | 3 | import { Node, NodeInputs, NodeOutput } from './node'; 4 | 5 | 6 | export type ExprNoArgFunc = (error: ErrorCallback, context: ContextType) => any; 7 | export type ExprWithArgFunc = (...args: any[]) => any; 8 | export type ExprFunc = ExprNoArgFunc | ExprWithArgFunc; 9 | 10 | 11 | /** 12 | * 13 | * Represents [expression](https://connective.dev/docs/expr) agents. 14 | * 15 | */ 16 | export class Expr extends Node { 17 | /** 18 | * 19 | * The expression function 20 | * 21 | */ 22 | readonly func: any; 23 | 24 | constructor(func: ExprNoArgFunc); 25 | constructor(inputs: string[], func: ExprWithArgFunc); 26 | /** 27 | * 28 | * @param inputsOrFunc either a list of names for the inputs of the 29 | * [signature](https://connective.dev/docs/agent#signature) or the expr function 30 | * @param func the expr function (if this is provided, the first parameter must be alist of string) 31 | * 32 | */ 33 | constructor(inputsOrFunc?: string[] | ExprNoArgFunc, func?: ExprWithArgFunc){ 34 | super({ 35 | inputs: (typeof inputsOrFunc === 'function')?[]:inputsOrFunc, 36 | required: (typeof inputsOrFunc === 'function')?[]:inputsOrFunc, 37 | outputs: ['result'] 38 | }); 39 | 40 | this.func = func?func:inputsOrFunc; 41 | } 42 | 43 | protected run(inputs: NodeInputs, output: NodeOutput, error: ErrorCallback, context: ContextType) { 44 | let _ilist = this.signature.inputs?this.signature.inputs.map(i => inputs[i]):[]; 45 | try { 46 | let val = this.func.apply(undefined, _ilist.concat(context)); 47 | if (typeof val === 'function') 48 | val.apply(undefined, [(out: any) => output('result', out), error]); 49 | else 50 | output('result', val); 51 | } catch (err) { 52 | error(err); 53 | } 54 | } 55 | 56 | /** 57 | * 58 | * Shortcut for `.out('result')`. The result of the evaluation of the 59 | * expression will be emitted via this output. 60 | * 61 | */ 62 | public get result() { return this.out('result'); } 63 | } 64 | 65 | 66 | export function expr(func: ExprFunc): Expr; 67 | export function expr(inputs: string[], func: ExprFunc): Expr; 68 | /** 69 | * 70 | * Creates an [expr](https://connective.dev/docs/expr) agent. 71 | * Expr agents turn a function into an agent. 72 | * [Checkout the docs](https://connective.dev/docs/expr) for examples and further information. 73 | * 74 | * @param inputsOrFunc either a list of names for the inputs of the signature or the function to convert 75 | * @param func the function to convert (if provided, the first argument must be a list of strings) 76 | * 77 | */ 78 | export function expr(inputsOrFunc?: string[] | ExprFunc, func?: ExprFunc): Expr { 79 | if (func) return new Expr(inputsOrFunc as string[], func); 80 | else { 81 | let func = inputsOrFunc as ExprFunc; 82 | return new Expr( 83 | Array.apply(0, {length: func.length}).map((_:0, i:number) => i.toString()), 84 | func 85 | ); 86 | } 87 | } 88 | 89 | 90 | export default expr; 91 | -------------------------------------------------------------------------------- /src/agent/gate.ts: -------------------------------------------------------------------------------- 1 | import { Control } from '../pin/control'; 2 | import map from '../pin/map'; 3 | import filter from '../pin/filter'; 4 | 5 | import group from '../pin/group'; 6 | 7 | import { Agent } from './agent'; 8 | import { NodeLike } from './node-like'; 9 | 10 | 11 | /** 12 | * 13 | * Represents [gate](https://connective.dev/docs/gate) agents. 14 | * 15 | */ 16 | export class Gate extends Agent implements NodeLike { 17 | private _control: Control; 18 | 19 | constructor() { 20 | super({inputs: ['value'], outputs: ['value']}); 21 | this._control = new Control(); 22 | } 23 | 24 | /** 25 | * 26 | * Shortcut for `.in('value')`, the input pin receiving values. 27 | * [Read this](https://connective.dev/docs/gate#signature) for more details. 28 | * 29 | */ 30 | public get input() { return this.in('value'); } 31 | 32 | /** 33 | * 34 | * Shortcut for `.out('value')`, the output emitting allowed values. 35 | * [Read this](https://connective.dev/docs/gate#signature) for more details. 36 | * 37 | */ 38 | public get output() { return this.out('value'); } 39 | 40 | /** 41 | * 42 | * Each pin connected to this pin should emit a boolean value for each 43 | * value sent to `.input`, and if all are true, the value is emitted via `.output`. 44 | * [Read this](https://connective.dev/docs/gate) for more details. 45 | * 46 | */ 47 | public get control() { return this._control; } 48 | 49 | protected createOutput(label: string) { 50 | this.checkOutput(label); 51 | return group(this.control, this.input) 52 | .to(new Control()) 53 | .to(filter(([ctrl, _]: [any[], any]) => ctrl.every(signal => !!signal))) 54 | .to(map(([_, input]: [any, any]) => input)); 55 | } 56 | 57 | protected createEntries() { return [this.input]; } 58 | protected createExits() { return [this.output]; } 59 | 60 | clear() { 61 | this.control.clear(); 62 | return super.clear(); 63 | } 64 | } 65 | 66 | 67 | /** 68 | * 69 | * Creates a [gate](https://connective.dev/docs/gate) agent. 70 | * Gate agents await a control signal for each incoming value and either pass it along 71 | * or drop it based on the boolean value of the control signal. 72 | * [Checkout the docs](https://connective.dev/docs/gate) for examples and further information. 73 | * 74 | */ 75 | export function gate() { return new Gate(); } 76 | 77 | 78 | export default gate; 79 | -------------------------------------------------------------------------------- /src/agent/handle-error.ts: -------------------------------------------------------------------------------- 1 | import { retry, tap, share } from 'rxjs/operators'; 2 | 3 | import emission from '../shared/emission'; 4 | import { isEmissionError } from '../shared/errors/emission-error'; 5 | 6 | import { PinLike } from '../pin/pin-like'; 7 | import pin from '../pin/pin'; 8 | import group from '../pin/group'; 9 | import source, { Source } from '../pin/source'; 10 | import pipe from '../pin/pipe'; 11 | import { block } from '../pin/filter'; 12 | 13 | import { Agent } from './agent'; 14 | 15 | 16 | /** 17 | * 18 | * Represents [handle error](https://connective.dev/docs/handle-error) agents. 19 | * 20 | */ 21 | export class HandleError extends Agent { 22 | private _err: Source; 23 | private _gate: PinLike; 24 | 25 | constructor() { 26 | super({ 27 | inputs: ['input'], 28 | outputs: ['output', 'error'], 29 | }); 30 | 31 | this._err = source(); 32 | this._gate = this.input.to(pipe( 33 | tap(null, error => { 34 | if (isEmissionError(error)) 35 | this._err.emit(emission(error, error.emission.context)); 36 | else 37 | this._err.send(error); 38 | }), 39 | retry(), 40 | share(), 41 | )); 42 | } 43 | 44 | protected createOutput(label: string) { 45 | this.checkOutput(label); 46 | if (label == 'error') 47 | return group(this._err, this._gate.to(block())).to(pin()); 48 | else 49 | return this._gate; 50 | } 51 | 52 | protected createEntries() { return [this.input] } 53 | protected createExits() { return [this.output, this.error ] } 54 | 55 | public clear() { 56 | this._err.clear(); 57 | return super.clear(); 58 | } 59 | 60 | /** 61 | * 62 | * Shortcut for `.in('input')`, the input pin receiving values. 63 | * [Read this](https://connective.dev/docs/handle-error#signature) for more details. 64 | * 65 | */ 66 | public get input() { return this.in('input'); } 67 | 68 | /** 69 | * 70 | * Shortcut for `.out('output')`, which will emit error-free values. 71 | * [Read this](https://connective.dev/docs/handle-error#signature) for more details. 72 | * 73 | */ 74 | public get output() { return this.out('output'); } 75 | 76 | /** 77 | * 78 | * Shortcut for `.out('error')`, which will emit errors. 79 | * [Read this](https://connective.dev/docs/handle-error#signature) for more details. 80 | * 81 | */ 82 | public get error() { return this.out('error'); } 83 | } 84 | 85 | 86 | /** 87 | * 88 | * Creates a [handle error](https://connective.dev/docs/handle-error) agent. 89 | * Handle error agents will pass on incoming values, but also will catch errors 90 | * occuring upstream and pass them along, stopping the flow from closing in resposne to such errors. 91 | * [Checkout the docs](https://connective.dev/docs/handle-error) for examples and further information. 92 | * 93 | */ 94 | export function handleError() { return new HandleError(); } 95 | 96 | 97 | export default handleError; 98 | -------------------------------------------------------------------------------- /src/agent/index.ts: -------------------------------------------------------------------------------- 1 | import { AgentLike, isAgentLike } from './agent-like'; 2 | import { Agent } from './agent'; 3 | import { Composition } from './composition'; 4 | import { expr, Expr, ExprFunc, ExprNoArgFunc, ExprWithArgFunc } from './expr'; 5 | import { gate, Gate } from './gate'; 6 | import { NodeLike, isNodeLike } from './node-like'; 7 | import { nodeWrap, NodeWrap } from './node-wrap'; 8 | import { node, Node, NodeInputs, NodeOutput, NodeRunFunc, NodeSignature } from './node'; 9 | import { proxy, Proxy } from './proxy'; 10 | import { Signature, isSignature } from './signature'; 11 | import { state, State, EqualityFunc } from './state'; 12 | import { _switch, Switch } from './switch'; 13 | import { handleError, HandleError } from './handle-error'; 14 | import { sequence, Sequence, SequenceToken, SequenceTokenIndicator } from './sequence'; 15 | import { join, peekJoin, Join } from './join'; 16 | import { invoke, Invoke } from './invoke'; 17 | import { check, Check } from './check'; 18 | 19 | import { exec, call, AgentFactory } from './call'; 20 | import { singleton } from './singleton'; 21 | import { sampler } from './sampler'; 22 | import { composition } from './inline-composition'; 23 | 24 | import { KeyFunc, AdditionList, DeletionList, MoveList } from '../util/keyed-array-diff'; 25 | import { KeyedDeep, ChangeMap } from './keyed-deep'; 26 | import { SimpleDeep, DeepAccessor, DeepChildFactory } from './simple-deep'; 27 | import { deep, keyed } from './deep'; 28 | 29 | import { ChildNotDefined } from './errors/child-not-defined.error'; 30 | import { ChildIsNotAgent, ChildIsNotPin } from './errors/child-type-mismatch.error'; 31 | import { InsufficientInputs } from './errors/insufficient-input.error'; 32 | import { InputNotInSignature, OutputNotInSignature } from './errors/signature-mismatch.error'; 33 | 34 | export { 35 | expr, gate, nodeWrap, proxy, state, check, _switch, handleError, sequence, join, peekJoin, invoke, node, 36 | Expr, Gate, NodeWrap, Proxy, State, Check, Switch, HandleError, Sequence, Join, Invoke, Node, 37 | Agent, AgentLike, isAgentLike, AgentFactory, 38 | Composition, composition, 39 | NodeLike, isNodeLike, NodeInputs, NodeOutput, NodeRunFunc, 40 | Signature, NodeSignature, isSignature, 41 | ExprFunc, ExprNoArgFunc, ExprWithArgFunc, 42 | SequenceToken, SequenceTokenIndicator, 43 | EqualityFunc, 44 | exec, call, singleton, sampler, 45 | ChildIsNotAgent, ChildIsNotPin, ChildNotDefined, 46 | InsufficientInputs, InputNotInSignature, OutputNotInSignature, 47 | deep, keyed, SimpleDeep, KeyedDeep, 48 | DeepAccessor, DeepChildFactory, 49 | KeyFunc, ChangeMap, AdditionList, MoveList, DeletionList, 50 | } 51 | -------------------------------------------------------------------------------- /src/agent/inline-composition.ts: -------------------------------------------------------------------------------- 1 | import { PinLike } from '../pin/pin-like'; 2 | 3 | import { Agent } from './agent'; 4 | import { Signature } from './signature'; 5 | import { Composition } from './composition'; 6 | 7 | 8 | type _ChildType = PinLike | Agent; 9 | type _PinDict = {[name: string]: PinLike}; 10 | 11 | export type TrackFunc = (...children: _ChildType[]) => void; 12 | export type CompositionFactory = (track: TrackFunc) => [_PinDict | PinLike[], _PinDict | PinLike[]]; 13 | 14 | 15 | class InlineComposition extends Composition { 16 | readonly inpins: _PinDict | PinLike[]; 17 | readonly outpins: _PinDict | PinLike[]; 18 | 19 | constructor(readonly factory: CompositionFactory, signature: Signature) { 20 | super(signature); 21 | [this.inpins, this.outpins] = this.factory( 22 | (...children: _ChildType[]) => children.forEach(child => this.add(child))); 23 | } 24 | 25 | init() {} 26 | wire() {} 27 | build() {} 28 | 29 | createInput(label: string) { return (this.inpins as any)[label]; } 30 | createOutput(label: string) { return (this.outpins as any)[label]; } 31 | 32 | createEntries() { return Object.values(this.inpins); } 33 | createExits() { return Object.values(this.outpins); } 34 | } 35 | 36 | 37 | export function composition(factory: CompositionFactory): () => InlineComposition; 38 | export function composition(signature: Signature, factory: CompositionFactory): () => InlineComposition; 39 | /** 40 | * 41 | * Creates a [composition](https://connective.dev/docs/composition) using given factory function. 42 | * [Checkout the docs](https://connective.dev/docs/composition) for examples and further information. 43 | * 44 | * @param factoryOrSignature either the [signature](https://connective.dev/docs/agent#signature) of 45 | * the composition or the factory function creating it. If signature is not provided, the factory function 46 | * will be invoked once to deduce the signature. 47 | * @param factory the factory function for creating the composition. If provided, the first parameter must 48 | * be a signature. 49 | * 50 | */ 51 | export function composition(factoryOrSignature: CompositionFactory | Signature, factory?: CompositionFactory) { 52 | let signature: Signature; 53 | if (!factory) { 54 | factory = factoryOrSignature as CompositionFactory; 55 | let tracked = <_ChildType[]>[]; 56 | let s = factory((...children) => { tracked = tracked.concat(children); }); 57 | signature = { inputs: Object.keys(s[0]), outputs: Object.keys(s[1]) }; 58 | tracked.forEach(thing => thing.clear()); 59 | } 60 | else { 61 | signature = factoryOrSignature as Signature; 62 | } 63 | 64 | let func = () => new InlineComposition(factory as CompositionFactory, signature); 65 | (func as any).signature = signature; 66 | return func; 67 | } 68 | 69 | 70 | export default composition; -------------------------------------------------------------------------------- /src/agent/invoke.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | import { PinLike } from '../pin/pin-like'; 4 | import control, { Control } from '../pin/control'; 5 | import map from '../pin/map'; 6 | import filter from '../pin/filter'; 7 | import pack from '../pin/pack'; 8 | 9 | import { Signature } from './signature'; 10 | import { Agent } from './agent'; 11 | import { NodeLike } from './node-like'; 12 | import { exec, AgentFactory, ExecResult } from './call'; 13 | 14 | 15 | /** 16 | * 17 | * Represents [invoke](https://connective.dev/docs/invoke) agents. 18 | * 19 | */ 20 | export class Invoke extends Agent implements NodeLike { 21 | private _relay: PinLike; 22 | private _control: Control; 23 | private _all_subs: Subscription = new Subscription(); 24 | 25 | private _control_required = true; 26 | 27 | /** 28 | * 29 | * @param ref the agent factory to be used in response to each set of incoming data 30 | * @param signature an optional signature denoting the signature of the agents that 31 | * are to be created. If not provided and not directly deducable from the factory function itself, 32 | * the factory function will be invoked once to deduce the signature. 33 | * 34 | */ 35 | constructor(readonly ref: AgentFactory, signature?: Signature) { 36 | super(signature || (ref as any).signature || ref().clear().signature); 37 | 38 | this._control = new Control(); 39 | 40 | this._relay = pack(control(this.inputs), this._control.to(map(() => this._control_required = false))) 41 | .to(filter(() => !this._control_required)) 42 | .to(map((_: any) => { 43 | if (this._control.connected) 44 | this._control_required = true; 45 | return _[0]; 46 | })) 47 | .to(exec(this.ref, s => this._all_subs.add(s), s => this._all_subs.remove(s), 48 | () => this.outputs.entries.map(([label, _]) => label))); 49 | } 50 | 51 | protected createOutput(label: string) { 52 | this.checkOutput(label); 53 | return this._relay 54 | .to(filter((data: ExecResult) => data.label == label)) 55 | .to(map((data: ExecResult) => data.value)); 56 | } 57 | 58 | protected createEntries() { return (this.signature.inputs || []).map(i => this.in(i)); } 59 | protected createExits() { return this.signature.outputs.map(o => this.out(o)); } 60 | 61 | /** 62 | * 63 | * You can control when the agent creates the inner-agent and runs it on latest set of 64 | * incoming values by emitting to `.control`. 65 | * 66 | */ 67 | public get control() { return this._control; } 68 | 69 | public clear() { 70 | this._relay.clear(); 71 | this._control.clear(); 72 | this._all_subs.unsubscribe(); 73 | return super.clear(); 74 | } 75 | } 76 | 77 | 78 | /** 79 | * 80 | * Creates an [invoke](https://connective.dev/docs/invoke) agent. Invoke 81 | * agents create an inner-agent using the given factory in response to each set of incoming inputs 82 | * and emit the first output of the inner-agent in response. 83 | * [Checkout the docs](https://connective.dev/docs/invoke) for examples and further information. 84 | * 85 | * @param ref the agent factory to be used to create inner-agents 86 | * @param signature the signature of the inner-agents. If not provided and not deducable from 87 | * the factory function, the factory function will be invoked once to deduce this. 88 | * 89 | */ 90 | export function invoke(ref: AgentFactory, signature?: Signature) { return new Invoke(ref, signature); } 91 | 92 | 93 | export default invoke; 94 | -------------------------------------------------------------------------------- /src/agent/node-like.ts: -------------------------------------------------------------------------------- 1 | import { Control } from '../pin/control'; 2 | 3 | import { isAgentLike, AgentLike } from './agent-like'; 4 | 5 | 6 | /** 7 | * 8 | * Denotes objects that behave like a [node](https://connective.dev/docs/node). 9 | * 10 | */ 11 | export interface NodeLike extends AgentLike{ 12 | /** 13 | * 14 | * You can typically control the behavior of a `NodeLike` by emitting 15 | * values to its `.control`, for example making it wait for a cue even if all 16 | * of its input parameters are ready. 17 | * 18 | */ 19 | control: Control; 20 | } 21 | 22 | 23 | /** 24 | * 25 | * @param whatever 26 | * @returns `true` if `whatever` is `NodeLike` 27 | * 28 | */ 29 | export function isNodeLike(whatever: any): whatever is NodeLike { 30 | return whatever !== undefined && whatever.control instanceof Control && isAgentLike(whatever); 31 | } 32 | -------------------------------------------------------------------------------- /src/agent/node-wrap.ts: -------------------------------------------------------------------------------- 1 | import { PinLike } from '../pin/pin-like'; 2 | import control, { Control } from '../pin/control'; 3 | import pack from '../pin/pack'; 4 | import map from '../pin/map'; 5 | import filter from '../pin/filter'; 6 | 7 | import { Agent } from './agent'; 8 | import { AgentLike } from './agent-like'; 9 | import { NodeLike } from './node-like'; 10 | import { Node } from './node'; 11 | 12 | 13 | /** 14 | * 15 | * A class to wrap an agent so that it behaves like a [node](https://connective.dev/docs/node). 16 | * 17 | */ 18 | export class NodeWrap extends Agent implements NodeLike { 19 | private _control: Control; 20 | private _pack: PinLike; 21 | 22 | private _control_required = true; 23 | 24 | /** 25 | * 26 | * @param core the original agent to be wrapped. 27 | * 28 | */ 29 | constructor(readonly core: AgentLike) { 30 | super(core.signature); 31 | 32 | this._control = control(); 33 | this._pack = pack( 34 | this.inputs, 35 | this._control.to(map(() => this._control_required = false)) 36 | ) 37 | .to(filter(() => !this._control_required)) 38 | .to(map((all: any) => { 39 | if (this._control.connected) 40 | this._control_required = true; 41 | return all[0]; 42 | })); 43 | 44 | this.track(core.inputs.subscribe((label, pin) => { 45 | this._pack.to(map((all: any) => all[label])).to(pin); 46 | })); 47 | 48 | this.track(core.outputs.subscribe((label, pin) => { 49 | pin.to(this.out(label)); 50 | })); 51 | } 52 | 53 | public get control(): Control { return this._control; } 54 | 55 | protected createInput(label: string) { 56 | this.core.in(label); 57 | return super.createInput(label); 58 | } 59 | 60 | protected createOutput(label: string) { 61 | this.core.out(label); 62 | return super.createOutput(label); 63 | } 64 | 65 | clear() { 66 | this._control.clear(); 67 | this._pack.clear(); 68 | this.core.clear(); 69 | return super.clear(); 70 | } 71 | } 72 | 73 | 74 | /** 75 | * 76 | * Wraps given agent in a `NodeWrap`, making it behave like a 77 | * [node](https://connective.dev/docs/node): 78 | * 79 | * - It will wait for all of its inputs to emit at least once before first execution 80 | * - Re-executes any time a new value is emitted from any of the inputs 81 | * - Waits for its `.control` if its connected before each execution 82 | * - Responds with the first output of the wrapped agent for each execution 83 | * 84 | * @param agent 85 | * 86 | */ 87 | export function nodeWrap(agent: AgentLike): NodeLike { 88 | if (agent instanceof Node) return agent; 89 | return new NodeWrap(agent); 90 | } 91 | 92 | 93 | export default nodeWrap; 94 | -------------------------------------------------------------------------------- /src/agent/proxy.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | import { Emission } from '../shared/emission'; 4 | 5 | import { Source } from '../pin/source'; 6 | 7 | import { Signature } from './signature'; 8 | import { Agent } from './agent'; 9 | import { AgentLike } from './agent-like'; 10 | 11 | 12 | /** 13 | * 14 | * Represents [proxy](https://connective.dev/docs/proxy) agents. 15 | * 16 | */ 17 | export class Proxy extends Agent { 18 | /** 19 | * 20 | * Proxies given agent, connecting it to the rest of the flow 21 | * that the proxy itself is connected to. 22 | * 23 | * @param agent 24 | * @returns a [subscription](https://rxjs-dev.firebaseapp.com/guide/subscription) object 25 | * that can be unsubscribed (call `.unsubscribe()`) to unproxy given agent. 26 | * 27 | */ 28 | public proxy(agent: AgentLike): Subscription { 29 | 30 | let subs = new Subscription(() => { 31 | this.untrack(subs); 32 | }); 33 | 34 | this.inputs.entries.forEach(entry => agent.in(entry[0]).from(entry[1])); 35 | this.outputs.entries.forEach(entry => { 36 | subs.add(agent.out(entry[0]).observable.subscribe((emission: Emission) => { 37 | (entry[1] as Source).emit(emission); 38 | })); 39 | }); 40 | 41 | return this.track(subs); 42 | } 43 | 44 | protected createOutput(label: string) { 45 | this.checkOutput(label); 46 | return new Source(); 47 | } 48 | } 49 | 50 | 51 | /** 52 | * 53 | * Creates a [proxy](https://connective.dev/docs/proxy) agent. 54 | * [Checkout the docs](https://connective.dev/docs/proxy) for examples and further information. 55 | * 56 | * @param signature the signature of the proxied agent (or a projection of the signature that needs 57 | * to be proxied). 58 | * 59 | */ 60 | export function proxy(signature: Signature) { return new Proxy(signature); } 61 | 62 | 63 | export default proxy; 64 | -------------------------------------------------------------------------------- /src/agent/sampler.ts: -------------------------------------------------------------------------------- 1 | import expr from './expr'; 2 | 3 | 4 | /** 5 | * 6 | * Creates a [sampler](https://connective.dev/docs/sampler). 7 | * A sampler passes on the last received value when receiving 8 | * a signal on its `.control`. 9 | * [Checkout the docs](https://connective.dev/docs/sampler) for examples and further information. 10 | * 11 | */ 12 | export function sampler() { return expr((x: any) => x); } -------------------------------------------------------------------------------- /src/agent/signature.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Denotes [signature](https://connective.dev/docs/agent#signature) of agents. 4 | * 5 | */ 6 | export interface Signature { 7 | /** 8 | * 9 | * names of the inputs of the agent 10 | * 11 | */ 12 | inputs?: string[]; 13 | 14 | /** 15 | * 16 | * names of the outputs of the agent 17 | * 18 | */ 19 | outputs: string[]; 20 | } 21 | 22 | 23 | /** 24 | * 25 | * @param whatever 26 | * @returns `true` if `whatever` is a `Signature`. 27 | * 28 | */ 29 | export function isSignature(whatever: any): whatever is Signature { 30 | return whatever !== undefined && whatever.outputs !== undefined && whatever.outputs.length !== undefined && 31 | (whatever.inputs === undefined || whatever.inputs.length !== undefined); 32 | } 33 | -------------------------------------------------------------------------------- /src/agent/singleton.ts: -------------------------------------------------------------------------------- 1 | import { isBindable } from '../shared/bindable'; 2 | 3 | import { Agent } from './agent'; 4 | 5 | 6 | export function singleton() { 7 | return function(_Class: T) : {new(...args: any[]): Agent} & T { 8 | let agent = new _Class(); 9 | if (isBindable(agent)) { 10 | agent.bind(); 11 | } 12 | 13 | return class extends _Class { 14 | static readonly instance = agent; 15 | } 16 | } 17 | } 18 | 19 | 20 | export default singleton; 21 | -------------------------------------------------------------------------------- /src/agent/switch.ts: -------------------------------------------------------------------------------- 1 | import { PinLike } from '../pin/pin-like'; 2 | import filter from '../pin/filter'; 3 | 4 | import { Agent } from './agent'; 5 | 6 | 7 | export class Switch extends Agent { 8 | readonly cases : any[]; 9 | 10 | constructor(...cases: any[]) { 11 | super({ 12 | inputs: ['target'], 13 | outputs: cases.map((_, index) => index.toString()), 14 | }); 15 | 16 | this.cases = cases; 17 | } 18 | 19 | public get target() { return this.in('target'); } 20 | public case(index: number) { return this.out(index); } 21 | 22 | protected createOutput(label: string): PinLike { 23 | this.checkOutput(label); 24 | let _case = this.cases[label as any]; 25 | 26 | return this.target 27 | .to((typeof _case === 'function')? 28 | filter(_case): 29 | filter((value: any) => _case === value)) 30 | ; 31 | } 32 | } 33 | 34 | 35 | export function _switch(...cases: any[]) { return new Switch(...cases); } 36 | 37 | 38 | export default _switch; 39 | -------------------------------------------------------------------------------- /src/agent/test/agent-like.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { PinMap } from '../../pin/pin-map'; 4 | 5 | import { isAgentLike } from '../agent-like'; 6 | 7 | 8 | describe('isAgentLike()', () => { 9 | it('should be true for stuff that are `AgentLike` and false for whatever else.', () => { 10 | isAgentLike({ 11 | in(){}, out(){}, 12 | inputs: new PinMap(), outputs: new PinMap(), 13 | signature: {outputs: []} 14 | }).should.be.true; 15 | 16 | isAgentLike({ 17 | in(){}, out(){}, 18 | inputs: new PinMap(), outputs: new PinMap(), 19 | }).should.be.false; 20 | 21 | isAgentLike({ 22 | in(){}, 23 | inputs: new PinMap(), outputs: new PinMap(), 24 | signature: {outputs: []} 25 | }).should.be.false; 26 | 27 | isAgentLike({ 28 | in(){}, out(){}, 29 | inputs: new PinMap(), outputs: 42, 30 | signature: {outputs: []} 31 | }).should.be.false; 32 | 33 | isAgentLike(42).should.be.false; 34 | isAgentLike(undefined).should.be.false; 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/agent/test/call.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import call from '../call'; 4 | import expr from '../expr'; 5 | 6 | 7 | describe('call()', () => { 8 | it('should create an agent using given factory and pass it given data.', done => { 9 | call(() => expr((a: any, b: any) => a + b), {0: 2, 1: 3}) 10 | .subscribe(v => { 11 | v.should.eql({label: 'result', value: 5}); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/agent/test/check.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import emission from '../../shared/emission'; 4 | import source from '../../pin/source'; 5 | import sink from '../../pin/sink'; 6 | 7 | import check, { Check } from '../check'; 8 | 9 | 10 | describe('Check', () => { 11 | it('should pass values passing given predicate through `.pass`, others through its `.fail`', () => { 12 | let a = source(); 13 | let c = check((x: any) => x % 2 == 0); 14 | let passed = []; 15 | let failed = []; 16 | 17 | a.to(c.input); 18 | c.pass.subscribe(v => passed.push(v)); 19 | c.fail.subscribe(v => failed.push(v)); 20 | 21 | a.send(1); a.send(2); a.send(3); a.send(4); 22 | passed.should.eql([2, 4]); 23 | failed.should.eql([1, 3]); 24 | }); 25 | 26 | it('should work properly with async predicates.', done => { 27 | let a = source(); 28 | let c = check((x: any, done) => setTimeout(() => done(x % 2 == 0), 1)); 29 | let passed = []; 30 | let failed = []; 31 | 32 | a.to(c.input); 33 | c.pass.subscribe(v => passed.push(v)); 34 | c.fail.subscribe(v => failed.push(v)); 35 | 36 | a.send(1); a.send(2); a.send(3); a.send(4); 37 | setTimeout(() => { 38 | passed.should.eql([2, 4]); 39 | failed.should.eql([1, 3]); 40 | done(); 41 | }, 10); 42 | }); 43 | 44 | it('should handle errors in sync predicates.', done => { 45 | let a = source(); 46 | let c = check(() => { throw new Error('hellow') }); 47 | a.to(c.input); 48 | c.pass.subscribe(() => {}, () => done()); 49 | a.send(); 50 | }); 51 | 52 | it('should pass an error callback to async predicates.', done => { 53 | let a = source(); 54 | let c = check((_: any, done, err) => err('hellow')); 55 | a.to(c.input); 56 | c.pass.subscribe(() => {}, () => done()); 57 | a.send(); 58 | }); 59 | 60 | it('should provide the async predicate with context as well.', done => { 61 | let a = source(); 62 | let c = check((_: any, __, ___, ctx) => { 63 | ctx.x.should.equal(42); 64 | done(); 65 | }); 66 | a.to(c.input); 67 | c.pass.subscribe(); 68 | a.emit(emission(0, { x : 42 })); 69 | }); 70 | 71 | it('should be serially connectible.', () => { 72 | let a = source(); 73 | let odd = []; 74 | let even = []; 75 | 76 | a.to(check((x: number) => x % 2 == 0)).serialTo( 77 | sink(v => even.push(v)), 78 | sink(v => odd.push(v)) 79 | ).subscribe(); 80 | 81 | a.send(1); a.send(2); a.send(3); a.send(4); 82 | even.should.eql([2, 4]); 83 | odd.should.eql([1, 3]); 84 | }); 85 | 86 | describe('.input', () => { 87 | it('should be equal to `.in("value")`', () => { 88 | let c = check(() => false); 89 | c.input.should.equal(c.in("value")); 90 | }); 91 | }); 92 | 93 | describe('.pass', () => { 94 | it('should be equal to `.out("pass")`', () => { 95 | let c = check(() => false); 96 | c.pass.should.equal(c.out("pass")); 97 | }); 98 | }); 99 | 100 | describe('.fail', () => { 101 | it('should be equal to `.out("fail")`', () => { 102 | let c = check(() => false); 103 | c.fail.should.equal(c.out("fail")); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('check()', () => { 109 | it('should create a `Check` with the given predicate.', () => { 110 | let f = () => false; 111 | let c = check(f); 112 | c.should.be.instanceOf(Check); 113 | c.predicate.should.equal(f); 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /src/agent/test/expr.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { ErrorCallback, ContextType } from '../../shared/types'; 4 | import emission from '../../shared/emission'; 5 | 6 | import { Node } from '../node'; 7 | import { Expr } from '../expr'; 8 | import expr from '../expr'; 9 | 10 | import { Source } from '../../pin/source'; 11 | 12 | 13 | describe('Expr', () => { 14 | it('should be a subclass of Node.', () => { 15 | new Expr(() => {}).should.be.instanceof(Node); 16 | }); 17 | 18 | it('should run given function.', done => { 19 | let e = new Expr(['a', 'b'], (a: any, b: any) => a + b); 20 | let a = new Source(); a.to(e.in('a')); 21 | let b = new Source(); b.to(e.in('b')); 22 | e.result.subscribe(res => { 23 | res.should.equal(5); 24 | done(); 25 | }); 26 | 27 | a.send(2); 28 | b.send(3); 29 | }); 30 | 31 | it('should throw an error if not all parameters are provided.', done => { 32 | new Expr(['a'], (a: any) => a). 33 | result.subscribe(() => {}, () => done()); 34 | }); 35 | 36 | it('should run given function instantly if no inputs are outlined.', done => { 37 | new Expr(() => true).result.subscribe(() => done()); 38 | }); 39 | 40 | it('should handle erros occuring in function execution.', done => { 41 | new Expr(() => { throw new Error(); }). 42 | result.subscribe(() => {}, () => done()); 43 | }); 44 | 45 | it('should also pass the context to the function.', done => { 46 | let e = new Expr(['i'], (_, ctx: ContextType) => { 47 | ctx.name.should.equal('the dude'); 48 | done(); 49 | }); 50 | 51 | let a = new Source(); a.to(e.in('i')); 52 | e.result.subscribe(); 53 | a.emit(emission('whatever', {name: 'the dude'})); 54 | }); 55 | 56 | it('should run the result of the function as an async callback if the result is a function itself.', done => { 57 | new Expr(() => (done: any) => done('hellow')). 58 | result.subscribe(res => { 59 | res.should.equal('hellow'); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should also provide the proper error callback to the async callback.', done => { 65 | new Expr(() => (_: any, err: ErrorCallback) => err('yup')). 66 | result.subscribe(() => {}, () => done()); 67 | }); 68 | 69 | describe('.result', () => { 70 | it('should be equal to `.out("result")`', () => { 71 | let e = new Expr(() => {}); 72 | e.result.should.equal(e.out('result')); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('expr()', () => { 78 | it('should return a proper Expr.', done => { 79 | let e = expr(['a', 'b'], (a: any, b: any) => a * b); 80 | e.should.be.instanceof(Expr); 81 | 82 | let a = new Source(); a.to(e.in('a')); 83 | let b = new Source(); b.to(e.in('b')); 84 | e.result.subscribe(res => { 85 | res.should.equal(6); 86 | done(); 87 | }); 88 | 89 | a.send(2); 90 | b.send(3); 91 | }); 92 | 93 | it('should create numeric inputs for the signature if no named inputs are given but the given function has inputs.', done => { 94 | let e = expr((a: any, b: any) => b - a); 95 | let a = new Source(); a.to(e.in(0)); 96 | let b = new Source(); b.to(e.in(1)); 97 | 98 | e.result.subscribe(val => { 99 | val.should.equal(1); 100 | done(); 101 | }); 102 | 103 | a.send(2); 104 | b.send(3); 105 | }); 106 | 107 | it('should also pass the context in `rest` param if automatically creating a signature.', done => { 108 | let e = expr((_:any, ...[ctx]: [any, ContextType]) => { 109 | ctx.name.should.equal('the dude'); 110 | done(); 111 | }); 112 | 113 | let a = new Source(); a.to(e.in(0)); 114 | e.result.subscribe(); 115 | a.emit(emission('whatever', {name: 'the dude'})); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/agent/test/gate.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import source from '../../pin/source'; 4 | 5 | import { Gate } from '../gate'; 6 | 7 | 8 | describe('Gate', () => { 9 | it('should only pass values that receive a `true` signal.', () => { 10 | let res: number[] = []; 11 | let g = new Gate(); 12 | let a = source(); a.to(g.input); 13 | let c = source(); c.to(g.control); 14 | 15 | g.output.subscribe(v => res.push(v)); 16 | 17 | a.send(42); c.send(true); 18 | a.send(24); c.send(false); 19 | a.send(13); c.send(true); 20 | 21 | res.should.eql([42, 13]); 22 | }); 23 | 24 | it('should match signals and values.', () => { 25 | let res: number[] = []; 26 | let g = new Gate(); 27 | let a = source(); a.to(g.input); 28 | let c = source(); c.to(g.control); 29 | 30 | g.output.subscribe(v => res.push(v)); 31 | 32 | c.send(true); a.send(42); 33 | c.send(false); c.send(true); 34 | a.send(24); a.send(13); 35 | 36 | res.should.eql([42, 13]); 37 | }); 38 | 39 | it('should wait for a boolean signal for each value.', () => { 40 | let res: number[] = []; 41 | let g = new Gate(); 42 | let a = source(); a.to(g.input); 43 | let c = source(); c.to(g.control); 44 | 45 | g.output.subscribe(v => res.push(v)); 46 | 47 | a.send(42); a.send(24); a.send(13); 48 | res.should.eql([]); 49 | 50 | c.send(true); res.should.eql([42]); 51 | c.send(false); res.should.eql([42]); 52 | c.send(true); res.should.eql([42, 13]); 53 | }); 54 | 55 | it('should wait for all connections to its control and only allow those who receive `true` by all.', () => { 56 | let res: number[] = []; 57 | let g = new Gate(); 58 | let a = source(); a.to(g.input); 59 | let c1 = source(); c1.to(g.control); 60 | let c2 = source(); c2.to(g.control); 61 | 62 | g.output.subscribe(v => res.push(v)); 63 | 64 | a.send(42); a.send(24); a.send(13); 65 | 66 | c1.send(true); res.should.eql([]); 67 | c2.send(true); res.should.eql([42]); 68 | 69 | c1.send(true); c2.send(false); 70 | res.should.eql([42]); 71 | 72 | c1.send(true); res.should.eql([42]); 73 | c2.send(true); res.should.eql([42, 13]); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/agent/test/index.ts: -------------------------------------------------------------------------------- 1 | describe('agent', () => { 2 | require('./signature.test'); 3 | require('./agent-like.test'); 4 | require('./node-like.test'); 5 | require('./switch.test'); 6 | require('./state.test'); 7 | require('./node.test'); 8 | require('./expr.test'); 9 | require('./proxy.test'); 10 | require('./gate.test'); 11 | require('./composition.test'); 12 | require('./node-wrap.test'); 13 | require('./sequence.test'); 14 | require('./handle-error.test'); 15 | require('./join.test'); 16 | require('./invoke.test'); 17 | require('./call.test'); 18 | require('./singleton.test'); 19 | require('./check.test'); 20 | require('./inline-composition.test'); 21 | require('./simple-deep.test'); 22 | require('./keyed-deep.test'); 23 | }); 24 | -------------------------------------------------------------------------------- /src/agent/test/inline-composition.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import pin from '../../pin/pin'; 4 | import map from '../../pin/map'; 5 | import sink from '../../pin/sink'; 6 | import source from '../../pin/source'; 7 | 8 | import { composition } from '../inline-composition'; 9 | 10 | describe('composition()', () => { 11 | it('should create compositions based on given factory function.', done => { 12 | let C = composition(() => { 13 | let input = pin(); 14 | let output = input.to(map((x: any) => x * 2)); 15 | return [[input], [output]]; 16 | }); 17 | 18 | let a = source(); 19 | a.to(C()).subscribe(v => { 20 | v.should.equal(4); 21 | done(); 22 | }); 23 | 24 | a.send(2); 25 | }); 26 | 27 | it('should deduce the signature from returned pins.', () => { 28 | let C = composition(() => { 29 | let input = pin(); 30 | let output = input.to(map((x: any) => x * 2)); 31 | return [{input}, {output}]; 32 | }); 33 | 34 | let c = C(); 35 | c.signature.should.eql({ inputs: ['input'], outputs: ['output'] }); 36 | }); 37 | 38 | it('should provide the factory with a function that will add given agents to the children of the composition.', 39 | done => { 40 | let C = composition(track => { 41 | let i = pin(); 42 | track(i.to(sink(() => done()))); 43 | return [[i], [pin()]]; 44 | }); 45 | 46 | let a = source(); 47 | let c = C(); 48 | 49 | a.to(c); 50 | c.bind(); 51 | 52 | a.send(); 53 | }) 54 | }); -------------------------------------------------------------------------------- /src/agent/test/node-like.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { PinMap } from '../../pin/pin-map'; 4 | import control from '../../pin/control'; 5 | 6 | import { isNodeLike } from '../node-like'; 7 | 8 | 9 | describe('isNodeLike()', () => { 10 | it('should be true for stuff that are `NodeLike` and false for whatever else.', () => { 11 | isNodeLike({ 12 | in(){}, out(){}, 13 | inputs: new PinMap(), outputs: new PinMap(), 14 | signature: {outputs: []}, 15 | control: control(), 16 | }).should.be.true; 17 | 18 | isNodeLike({ 19 | in(){}, out(){}, 20 | inputs: new PinMap(), outputs: new PinMap(), 21 | signature: {outputs: []} 22 | }).should.be.false; 23 | 24 | isNodeLike({ 25 | in(){}, out(){}, 26 | inputs: new PinMap(), outputs: new PinMap(), 27 | signature: {outputs: []}, 28 | control: 'hellow', 29 | }).should.be.false; 30 | 31 | isNodeLike({ 32 | in(){}, out(){}, 33 | inputs: new PinMap(), outputs: new PinMap(), 34 | control: control(), 35 | }).should.be.false; 36 | 37 | isNodeLike(true).should.be.false; 38 | isNodeLike(undefined).should.be.false; 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/agent/test/node-wrap.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import source from '../../pin/source'; 4 | 5 | import expr from '../expr'; 6 | import { NodeWrap } from '../node-wrap'; 7 | import { Composition } from '../composition'; 8 | 9 | 10 | describe('NodeWrap', () => { 11 | it('should wait for all connected inputs before feeding them to wrapped agent.', done => { 12 | class C extends Composition { 13 | constructor() { super({inputs: ['a', 'b'], outputs: ['o']})} 14 | build() { 15 | this.add(expr((a: number, b: number) => a + b)); 16 | } 17 | wire() { 18 | this.in('a').to(this.agent(0).in(0)); 19 | this.in('b').to(this.agent(0).in(1)); 20 | this.out('o').from(this.agent(0).out('result')); 21 | } 22 | } 23 | 24 | let c = new NodeWrap(new C()); 25 | let a = source(); a.to(c.in('a')); 26 | let b = source(); b.to(c.in('b')); 27 | c.out('o').subscribe(val => { 28 | val.should.equal(5); 29 | done(); 30 | }); 31 | 32 | a.send(2); 33 | b.send(3); 34 | }); 35 | 36 | it('should only wait for connected inputs before feeding them to wrapped agent.', done => { 37 | class C extends Composition { 38 | constructor() { super({inputs: ['a', 'b'], outputs: ['o']})} 39 | build() { 40 | this.add(expr((_: number, b: number) => { 41 | if (b) return 'two'; 42 | else return 'one'; 43 | })); 44 | } 45 | wire() { 46 | this.in('a').to(this.agent(0).in(0)); 47 | this.in('b').to(this.agent(0).in(1)); 48 | this.out('o').from(this.agent(0).out('result')); 49 | } 50 | } 51 | 52 | let c = new NodeWrap(new C()); 53 | let a = source(); a.to(c.in('a')); 54 | c.out('o').subscribe(val => { 55 | val.should.equal('one'); 56 | done(); 57 | }); 58 | 59 | a.send(2); 60 | }); 61 | 62 | it('should wait for its control before feeding inputs to wrapped agent.', () => { 63 | class C extends Composition { 64 | constructor() { super({inputs: ['i'], outputs: ['o']}) } 65 | build() {} 66 | wire() { this.in('i').to(this.out('o')); } 67 | } 68 | 69 | let c = new NodeWrap(new C()); 70 | let res: number[] = []; 71 | 72 | let a = source(); a.to(c.in('i')); 73 | let b = source(); b.to(c.control); 74 | 75 | c.out('o').subscribe(val => res.push(val)); 76 | 77 | a.send(2); 78 | res.should.eql([]); 79 | b.send(); 80 | res.should.eql([2]); 81 | }); 82 | 83 | it('should wait for its control each time.', () => { 84 | class C extends Composition { 85 | constructor() { super({inputs: ['i'], outputs: ['o']}) } 86 | build() {} 87 | wire() { this.in('i').to(this.out('o')); } 88 | } 89 | 90 | let c = new NodeWrap(new C()); 91 | let res: number[] = []; 92 | 93 | let a = source(); a.to(c.in('i')); 94 | let b = source(); b.to(c.control); 95 | 96 | c.out('o').subscribe(val => res.push(val)); 97 | 98 | a.send(2); 99 | b.send(); 100 | res.should.eql([2]); 101 | 102 | a.send(3); 103 | res.should.eql([2]); 104 | b.send(); 105 | res.should.eql([2, 3]); 106 | }); 107 | 108 | it('should re-execute upon control signal.', () => { 109 | class C extends Composition { 110 | constructor() { super({inputs: ['i'], outputs: ['o']}) } 111 | build() {} 112 | wire() { this.in('i').to(this.out('o')); } 113 | } 114 | 115 | let c = new NodeWrap(new C()); 116 | let res: number[] = []; 117 | 118 | let a = source(); a.to(c.in('i')); 119 | let b = source(); b.to(c.control); 120 | 121 | c.out('o').subscribe(val => res.push(val)); 122 | 123 | a.send(2); 124 | b.send(); 125 | b.send(); 126 | res.should.eql([2, 2]); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /src/agent/test/proxy.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { Source } from '../../pin/source'; 4 | 5 | import { Proxy } from '../proxy'; 6 | import expr from '../expr'; 7 | 8 | 9 | describe('Proxy', () => { 10 | it('should proxy a given signature in the graph that can be later linked to actual agents.', () => { 11 | let res = []; 12 | let p = new Proxy({inputs: ['a'], outputs: ['result']}); 13 | let a = new Source(); a.to(p.in('a')); 14 | let e = expr((x: number) => x * 2); 15 | p.out('result').to(e.in(0)); 16 | e.result.subscribe((x: number) => res.push(x)); 17 | 18 | a.send(1); 19 | res.should.eql([]); 20 | 21 | p.proxy(expr(['a'], (a: number) => a + 1)); 22 | a.send(1); 23 | res.should.include(4); 24 | }); 25 | 26 | it('should be able to proxy multiple agents, channeling all their outputs to the proxied output.', () => { 27 | let res = []; 28 | let p = new Proxy({inputs: ['a'], outputs: ['result']}); 29 | let a = new Source(); a.to(p.in('a')); 30 | let e = expr((x: number) => x * 3); 31 | p.out('result').to(e.in(0)); 32 | e.result.subscribe((x: number) => res.push(x)); 33 | 34 | p.proxy(expr(['a'], (a: number) => a + 1)); 35 | p.proxy(expr(['a'], (a: number) => a + 2)); 36 | a.send(1); 37 | 38 | res.should.have.members([6, 9]); 39 | res.length.should.equal(2); 40 | }); 41 | 42 | it('should remove a proxied agent via unsubscribing the `Subscription` returned by `.proxy()`', () => { 43 | let res = []; 44 | let p = new Proxy({inputs: ['a'], outputs: ['result']}); 45 | let a = new Source(); a.to(p.in('a')); 46 | let e = expr((x: number) => x * 5); 47 | p.out('result').to(e.in(0)); 48 | e.result.subscribe((x: number) => res.push(x)); 49 | 50 | p.proxy(expr(['a'], (a: number) => a + 1)); 51 | let sub = p.proxy(expr(['a'], (a: number) => a + 2)); 52 | a.send(1); 53 | sub.unsubscribe(); 54 | a.send(3); 55 | 56 | res.should.have.members([10, 15, 20]); 57 | res.length.should.equal(3); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/agent/test/signature.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { isSignature } from '../signature'; 4 | 5 | 6 | describe('isSignature()', () => { 7 | it('should be true for stuff that are signatures and false for whatever else.', () => { 8 | isSignature({inputs: [], outputs: []}).should.be.true; 9 | isSignature({inputs: 2, outputs: []}).should.be.false; 10 | isSignature({outputs: []}).should.be.true; 11 | isSignature('hellow').should.be.false; 12 | isSignature(undefined).should.be.false; 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/agent/test/singleton.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import singleton from '../singleton'; 4 | import { Composition } from '../composition'; 5 | 6 | 7 | describe('singleton()', () => { 8 | it('should create an instance of given agent class.', done => { 9 | @singleton() 10 | class C extends Composition { 11 | constructor() { super({outputs: []})} 12 | build(){} 13 | wire(){ 14 | done(); 15 | } 16 | } 17 | 18 | C; 19 | }); 20 | 21 | it('should invoke the `.bind()` function if it exists.', done => { 22 | @singleton() 23 | class C extends Composition { 24 | constructor() { super({outputs: []})} 25 | build(){} 26 | wire(){} 27 | bind() { done(); return super.bind(); } 28 | } 29 | 30 | C; 31 | }); 32 | 33 | it('should put the instance in `.instance` static member.', () => { 34 | @singleton() 35 | class C extends Composition { 36 | constructor() { super({outputs: []})} 37 | build(){} 38 | wire(){} 39 | } 40 | 41 | (C as any).instance.should.be.instanceof(Composition); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/agent/test/state.test.ts: -------------------------------------------------------------------------------- 1 | import { State } from '../state'; 2 | 3 | import { testStateSpec } from './state.spec'; 4 | 5 | 6 | describe('State', () => { 7 | testStateSpec((...args: []) => new State(...args)); 8 | }); 9 | -------------------------------------------------------------------------------- /src/agent/test/switch.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { Source } from '../../pin/source'; 4 | import { Switch } from '../switch'; 5 | 6 | 7 | describe('Switch', () => { 8 | it('should activate output based on which given `case` matched the given `value`.', () => { 9 | let a = new Source(); 10 | let s = new Switch(1, 'hellow', false); 11 | 12 | s.target.from(a); 13 | 14 | let res = ''; 15 | s.case(0).subscribe(() => res = 'A'); 16 | s.case(1).subscribe(() => res = 'B'); 17 | s.case(2).subscribe(() => res = 'C'); 18 | 19 | a.send('hellow'); 20 | res.should.equal('B'); 21 | 22 | a.send(false); 23 | res.should.equal('C'); 24 | }); 25 | 26 | it('should check with `cases` that are functions by calling them on the given data.', () => { 27 | let a = new Source(); 28 | let b = new Switch( 29 | (n: number) => n % 2 == 0, 30 | (n: number) => n % 2 == 1, 31 | ); 32 | 33 | b.target.from(a); 34 | 35 | let res = ''; 36 | b.case(0).subscribe(() => res = 'even'); 37 | b.case(1).subscribe(() => res = 'odd'); 38 | 39 | a.send(2); 40 | res.should.equal('even'); 41 | 42 | a.send(3); 43 | res.should.equal('odd'); 44 | }); 45 | 46 | it('should work with async functions in `cases` as well.', done => { 47 | let a = new Source(); 48 | let b = new Switch( 49 | (n: number) => n % 2 == 0, 50 | (n: number, done: (_: boolean) => void) => { 51 | setTimeout(() => done(n % 2 == 1), 5); 52 | }, 53 | ); 54 | 55 | b.target.from(a); 56 | 57 | let res = ''; 58 | b.case(0).subscribe(() => res = 'even'); 59 | b.case(1).subscribe(val => { 60 | val.should.equal(3); 61 | res.should.not.equal('even'); 62 | done(); 63 | }); 64 | a.send(3); 65 | }); 66 | 67 | it('should hanlde errors thrown by sync case functions.', done => { 68 | let s = new Switch(() => { throw new Error('well ...')}); 69 | let a = new Source(); a.to(s.target); 70 | 71 | s.case(0).subscribe(() => {}, () => done()); 72 | a.send(); 73 | }); 74 | 75 | it('should allow async case functions to throw errors through the second callback passed to them.', done => { 76 | let s = new Switch((_:any, __:any, err: any) => err(new Error())); 77 | let a = new Source(); a.to(s.target); 78 | 79 | s.case(0).subscribe(() => {}, () => done()); 80 | a.send(); 81 | }); 82 | 83 | it('should activate as many outputs as they match.', () => { 84 | let a = new Source(); 85 | let b = new Switch(1, () => true); 86 | let calls = 0; 87 | 88 | b.target.from(a); 89 | b.case(0).subscribe(() => calls++); 90 | b.case(1).subscribe(() => calls++); 91 | 92 | a.send(1); 93 | calls.should.equal(2); 94 | }); 95 | 96 | describe('.target', () => { 97 | it('should be equal to `.in("target")`', () => { 98 | let s = new Switch(); 99 | s.target.should.equal(s.in('target')); 100 | }); 101 | }); 102 | 103 | describe('.case()', () => { 104 | it('should be equal to `.out()`', () => { 105 | let s = new Switch(true); 106 | s.case(0).should.equal(s.out(0)); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent/index'; 2 | export * from './pin/index'; 3 | export * from './shared/index'; 4 | -------------------------------------------------------------------------------- /src/pin/control.ts: -------------------------------------------------------------------------------- 1 | import { zip, of, Observable } from 'rxjs'; 2 | import { map } from 'rxjs/operators'; 3 | 4 | import emission, { Emission } from '../shared/emission'; 5 | 6 | import { Pin } from './pin'; 7 | import { PinLike } from './pin-like'; 8 | import { PinMap } from './pin-map'; 9 | 10 | 11 | const _UNSET = {}; 12 | 13 | /** 14 | * 15 | * Represents [control](https://connective.dev/docs/control) pins. 16 | * 17 | */ 18 | export class Control extends Pin { 19 | constructor(readonly val: any = _UNSET) { super(); } 20 | 21 | /** 22 | * 23 | * Resolves underlying observable, by 24 | * [zipping](https://rxjs-dev.firebaseapp.com/api/index/function/zip) 25 | * corresponding observables of inbound pins. 26 | * 27 | * If a `PinMap` is passed to the constructor, it will instead 28 | * resolve to zip of all of the instantiated pins of that `PinMap`. 29 | * 30 | * If a value is passed to the constructor, and there are no inbound 31 | * pins, it will resolve to `of()`. 32 | * 33 | * @param inbound 34 | * 35 | */ 36 | protected resolve(inbound: PinLike[]): Observable { 37 | if (this.val instanceof PinMap) { 38 | let _entries = this.val.entries; 39 | if (_entries.length == 0) return of(emission()); 40 | return zip(..._entries.map(entry => entry[1].observable)) 41 | .pipe(map( 42 | emissions => Emission.from(emissions, _entries.reduce( 43 | (_map, entry, index) => { 44 | _map[entry[0]] = emissions[index].value; 45 | return _map; 46 | } 47 | , <{[label: string]: any}>{})) 48 | )); 49 | } 50 | else if (inbound.length == 0) return of(emission(this.val)); 51 | else { 52 | let _zipped = zip(...inbound.map(pin => pin.observable)); 53 | if (this.val !== _UNSET) 54 | return _zipped.pipe(map(emissions => Emission.from(emissions, this.val))); 55 | else return _zipped.pipe(map(emissions => Emission.from(emissions))); 56 | }; 57 | } 58 | } 59 | 60 | /** 61 | * 62 | * Creates a [control](https://connective.dev/docs/control) pin. 63 | * 64 | * @param val if provided, the control pin will emit the given value when 65 | * all pins connected to it emit, otherwise it will emit the array concatenation 66 | * of received values. If no pins are connected to it, then it will emit the value 67 | * to any subscriber (or to any pin that this pin is connected to, when a subscription 68 | * is called somwhere down the chain). 69 | * 70 | * If a `PinMap` is given as the value, then after resolution, the control will be 71 | * connected to all "realised" pins of the given pinmap. 72 | * 73 | */ 74 | export function control(val?: any) { return new Control(val); } 75 | 76 | 77 | export default control; 78 | -------------------------------------------------------------------------------- /src/pin/errors/group-subscription.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This error is thrown when you access `.observable` on a [group](https://connective.dev/docs/group), 4 | * since a group does not have an underlying observable. 5 | * 6 | */ 7 | export class GroupObservableError extends Error { 8 | constructor() { 9 | super('A group of pins does not have an observable.'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pin/errors/locked.error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This error is thrown when you connect a pin to a locked pin. 4 | * [Read this](https://connective.dev/docs/pin#subscribing-and-binding) 5 | * for more information on when a pin is locked. 6 | * 7 | */ 8 | export class PinLockedError extends Error { 9 | constructor() { 10 | super(`Attempted to modify pin after it was locked. 11 | Check the following link for more info: 12 | https://connective.dev/docs/pin#subscribing-and-binding`); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pin/errors/unresolved-observable.error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This is thrown when the underlying observable of a pin could not 4 | * be resolved. This typically indicates a problematic custom pin type. 5 | * 6 | */ 7 | export class UnresolvedPinObservableError extends Error { 8 | constructor() { 9 | super('Unresolved pin observable.'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pin/filter.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { filter as _filter, map, mergeMap, share } from 'rxjs/operators'; 3 | 4 | import { ResolveCallback, ErrorCallback, ContextType } from '../shared/types'; 5 | import { EmissionError } from '../shared/errors/emission-error'; 6 | 7 | import { Pipe } from './pipe'; 8 | 9 | 10 | export type FilterFuncSync = (value: any) => boolean; 11 | export type FilterFuncAsync = (value: any, 12 | callback: ResolveCallback, 13 | error: ErrorCallback, 14 | context: ContextType) => void; 15 | export type FilterFunc = FilterFuncSync | FilterFuncAsync; 16 | 17 | 18 | /** 19 | * 20 | * Represents [filter](https://connective.dev/docs/filter) pins. 21 | * 22 | */ 23 | export class Filter extends Pipe { 24 | /** 25 | * 26 | * The predicate of this filter pin. 27 | * 28 | */ 29 | readonly filter: FilterFunc; 30 | 31 | constructor(_func: FilterFunc) { 32 | super( 33 | (_func.length <= 1)? 34 | ([_filter(emission => { 35 | try { 36 | return (_func as FilterFuncSync)(emission.value); 37 | } catch(error) { 38 | throw new EmissionError(error, emission); 39 | } 40 | })]): 41 | ([ 42 | mergeMap(emission => 43 | new Observable(subscriber => { 44 | _func(emission.value, (res: boolean) => { 45 | subscriber.next(res); 46 | subscriber.complete(); 47 | }, 48 | (error: Error | string) => { 49 | subscriber.error(new EmissionError(error, emission)); 50 | }, 51 | emission.context); 52 | }) 53 | .pipe(_filter(_ => !!_), map(_ => emission)) 54 | ), 55 | share() 56 | ]) 57 | ); 58 | 59 | this.filter = _func; 60 | } 61 | } 62 | 63 | 64 | /** 65 | * 66 | * Creates a [filter](https://connective.dev/docs/filter) pin using given predicate. 67 | * A filter pin will pass some values through and not others based on given predicate. 68 | * [Checkout the docs](https://connective.dev/docs/filter) for examples and further information. 69 | * 70 | * @param filter 71 | * 72 | */ 73 | export function filter(filter: FilterFunc) { return new Filter(filter); } 74 | 75 | /** 76 | * 77 | * Creates a [filter](https://connective.dev/docs/filter) that never allows any value through. 78 | * 79 | */ 80 | export function block() { return new Filter(() => false); } 81 | 82 | 83 | export default filter; 84 | -------------------------------------------------------------------------------- /src/pin/fork.ts: -------------------------------------------------------------------------------- 1 | import { map, share } from 'rxjs/operators'; 2 | 3 | import emission from '../shared/emission'; 4 | import createRandomTag from '../util/random-tag'; 5 | 6 | import { Pipe } from './pipe'; 7 | 8 | 9 | const _DefaultForkTagLength = 10; 10 | 11 | 12 | /** 13 | * 14 | * Represents [fork](https://connective.dev/docs/fork) pins. 15 | * 16 | */ 17 | export class Fork extends Pipe { 18 | constructor(len: number = _DefaultForkTagLength) { 19 | super([ 20 | map(e => { 21 | let __fork = [].concat(e.context.__fork || []); 22 | __fork.push(Fork._create_fork_tag(len)); 23 | return emission(e.value, Object.assign({}, e.context, { __fork })); 24 | }), 25 | share(), 26 | ]) 27 | } 28 | 29 | private static _create_fork_tag(len: number = _DefaultForkTagLength): string { 30 | return createRandomTag(len); 31 | } 32 | } 33 | 34 | 35 | /** 36 | * 37 | * Creates a [fork](https://connective.dev/docs/fork) pin. 38 | * [Checkout the docs](https://connective.dev/docs/fork) for examples and further information. 39 | * 40 | * @param len the length of the fork-tag that will be added to the context of each emission. 41 | * 42 | */ 43 | export function fork(len: number = _DefaultForkTagLength) { return new Fork(len); } 44 | 45 | 46 | export default fork; 47 | -------------------------------------------------------------------------------- /src/pin/index.ts: -------------------------------------------------------------------------------- 1 | import { control, Control } from './control'; 2 | import { filter, block, Filter, FilterFunc, FilterFuncAsync, FilterFuncSync } from './filter'; 3 | import { group, Group } from './group'; 4 | import { map, Map, MapFunc, MapFuncAsync, MapFuncSync } from './map'; 5 | import { pack, Pack } from './pack'; 6 | import { pin, Pin } from './pin'; 7 | import { pipe, Pipe } from './pipe'; 8 | import { sink, Sink, SinkFunc } from './sink'; 9 | import { fork, Fork } from './fork'; 10 | import { source, Source } from './source'; 11 | import { reduce, Reduce } from './reduce'; 12 | import { spread, Spread } from './spread'; 13 | import { value } from './value'; 14 | import { wrap } from './wrap'; 15 | 16 | import { PinLike, isPinLike } from './pin-like'; 17 | import { PinMap, PinMapFactory, PinMapSusbcriber } from './pin-map'; 18 | 19 | import { Connectible } from './connectible'; 20 | 21 | import { partialFlow, PartialFlow } from './partial-flow'; 22 | 23 | import { GroupObservableError } from './errors/group-subscription'; 24 | import { PinLockedError } from './errors/locked.error'; 25 | import { UnresolvedPinObservableError } from './errors/unresolved-observable.error'; 26 | 27 | export { 28 | control, filter, group, map, pack, pin, pipe, sink, fork, source, spread, reduce, value, wrap, block, 29 | Control, Filter, Group, Map, Pack, Pin, Pipe, Sink, Fork, Source, Spread, Reduce, 30 | PinLike, isPinLike, 31 | PinMap, PinMapFactory, PinMapSusbcriber, 32 | Connectible, partialFlow, PartialFlow, 33 | FilterFunc, FilterFuncAsync, FilterFuncSync, 34 | MapFunc, MapFuncAsync, MapFuncSync, 35 | SinkFunc, 36 | GroupObservableError, PinLockedError, UnresolvedPinObservableError, 37 | } 38 | -------------------------------------------------------------------------------- /src/pin/map.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { map as _map, mergeMap, share } from 'rxjs/operators'; 3 | 4 | import { ResolveCallback, ErrorCallback, ContextType } from '../shared/types'; 5 | import { Emission } from '../shared/emission'; 6 | import { EmissionError } from '../shared/errors/emission-error'; 7 | 8 | import { Pipe } from './pipe'; 9 | 10 | 11 | export type MapFuncSync = (value: any) => any; 12 | export type MapFuncAsync = (value: any, 13 | callback: ResolveCallback, 14 | error: ErrorCallback, 15 | context: ContextType) => void; 16 | export type MapFunc = MapFuncSync | MapFuncAsync; 17 | 18 | 19 | /** 20 | * 21 | * Represents [map](https://connective.dev/docs/map) pins. 22 | * 23 | */ 24 | export class Map extends Pipe { 25 | /** 26 | * 27 | * The transformation of this map pin. 28 | * 29 | */ 30 | readonly map: MapFunc; 31 | 32 | constructor(_func: MapFunc) { 33 | super( 34 | (_func.length <= 1)? 35 | ([_map(emission => { 36 | try { 37 | return emission.fork((_func as MapFuncSync)(emission.value)); 38 | } catch(error) { 39 | throw new EmissionError(error, emission); 40 | } 41 | })]): 42 | ([ 43 | mergeMap(emission => 44 | new Observable(subscriber => { 45 | _func(emission.value, (res: any) => { 46 | subscriber.next(emission.fork(res)); 47 | subscriber.complete(); 48 | }, 49 | (error: Error | string) => { 50 | subscriber.error(new EmissionError(error, emission)); 51 | }, 52 | emission.context); 53 | }) 54 | ), 55 | share() 56 | ]) 57 | ); 58 | 59 | this.map = _func; 60 | } 61 | } 62 | 63 | 64 | /** 65 | * 66 | * Creates a [map](https://connective.dev/docs/map) pin using given transformation. 67 | * A map pin will transform incoming values based on given transformation. 68 | * [Checkout the docs](https://connective.dev/docs/map) for examples and further information. 69 | * 70 | * @param map 71 | * 72 | */ 73 | export function map(map: MapFunc) { return new Map(map); } 74 | 75 | 76 | export default map; 77 | -------------------------------------------------------------------------------- /src/pin/pack.ts: -------------------------------------------------------------------------------- 1 | import { combineLatest, of } from 'rxjs'; 2 | import { map } from 'rxjs/operators'; 3 | 4 | import emission, { Emission } from '../shared/emission'; 5 | 6 | import group from './group'; 7 | import { Pin } from './pin'; 8 | import { PinLike } from './pin-like'; 9 | import { PinMap } from './pin-map'; 10 | 11 | 12 | /** 13 | * 14 | * Represents [pack](https://connective.dev/docs/pack) pins. 15 | * 16 | */ 17 | export class Pack extends Pin { 18 | constructor(readonly pinmap?: PinMap) { 19 | super(); 20 | 21 | if (pinmap) 22 | this.track(pinmap.subscribe((_: string, pin: PinLike) => pin.to(this))); 23 | } 24 | 25 | /** 26 | * 27 | * Resolves the underlying observable by 28 | * [combining the latest values](https://rxjs-dev.firebaseapp.com/api/index/function/combineLatest) 29 | * from corresponding observables of inbound pins. 30 | * 31 | * If a `PinMap` is passed to the constructor, it will instead resolve 32 | * by combining the latest values from instantiated pins of the passed `PinMap`. 33 | * 34 | * @param inbound 35 | * 36 | */ 37 | protected resolve(inbound: PinLike[]) { 38 | if (this.pinmap) { 39 | let _entries = this.pinmap.entries; 40 | if (_entries.length == 0) return of(emission()); 41 | return combineLatest(..._entries.map(entry => entry[1].observable)) 42 | .pipe(map( 43 | emissions => Emission.from(emissions, _entries.reduce( 44 | (_map, entry, index) => { 45 | _map[entry[0]] = emissions[index].value; 46 | return _map; 47 | } 48 | , <{[label: string]: any}>{})) 49 | )) 50 | } 51 | else 52 | return combineLatest( 53 | ...inbound 54 | .map(pin => pin.observable)) 55 | .pipe(map(emissions => Emission.from(emissions))); 56 | } 57 | } 58 | 59 | 60 | /** 61 | * 62 | * Creates a [pack](https://connective.dev/docs/pack) pin. 63 | * 64 | * @param stuff If passed, the pin will be connected to all given pins. 65 | * If any of the stuff is a `PinMap` instead of a `Pin`, then upon resolution 66 | * the pack will be connected to all of its realized pins. 67 | * 68 | */ 69 | export function pack(...stuff: (PinMap|PinLike)[]) { 70 | let _mapped = stuff.map(each => (each instanceof PinMap)?new Pack(each):each); 71 | if (_mapped.length == 0) return new Pack(); 72 | if (_mapped.length == 1) return (_mapped[0] instanceof Pack)?_mapped[0]:_mapped[0].to(new Pack()); 73 | return group(..._mapped).to(new Pack()); 74 | } 75 | 76 | 77 | export default pack; 78 | -------------------------------------------------------------------------------- /src/pin/pin-map.ts: -------------------------------------------------------------------------------- 1 | import { Subject, Subscription } from 'rxjs'; 2 | 3 | import { Tracker } from '../shared/tracker'; 4 | 5 | import { PinLike } from './pin-like'; 6 | import { Pin } from './pin'; 7 | 8 | 9 | export type PinMapFactory = (label: string) => PinLike; 10 | export type PinMapSusbcriber = (label: string, pin: PinLike) => void; 11 | 12 | 13 | /** 14 | * 15 | * Represents a map of labelled pins. The labelled pins are created 16 | * first time they are requested, allowing for possibly huge 17 | * maps without high memory cost. 18 | * 19 | */ 20 | export class PinMap extends Tracker { 21 | private _pins: {[label: string]: PinLike} = {}; 22 | private _subject: Subject<[string, PinLike]> | undefined; 23 | 24 | /** 25 | * 26 | * @param factory will be used to create each new pin. 27 | * 28 | */ 29 | constructor( 30 | readonly factory: PinMapFactory = () => new Pin() 31 | ) { 32 | super(); 33 | } 34 | 35 | /** 36 | * 37 | * Fetches the pin with the given label, and create it if not 38 | * created already. 39 | * 40 | * @param label 41 | * 42 | */ 43 | public get(label: string): PinLike { 44 | if (!(label in this._pins)) { 45 | let _pin = this.factory(label); 46 | this._pins[label] = _pin; 47 | if (this._subject) this._subject.next([label, _pin]); 48 | return _pin; 49 | } 50 | 51 | return this._pins[label]; 52 | } 53 | 54 | /** 55 | * 56 | * Checks if a pin with given label is created, without 57 | * creating the pin. 58 | * 59 | * @param label 60 | * 61 | */ 62 | public instantiated(label: string): boolean { 63 | return label in this._pins; 64 | } 65 | 66 | /** 67 | * 68 | * @returns an array of all created pins. 69 | * 70 | */ 71 | public get pins(): PinLike[] { 72 | return Object.values(this._pins); 73 | } 74 | 75 | /** 76 | * 77 | * @returns an entry list (pairs of `[string, Pin]`) of created pins. 78 | * 79 | */ 80 | public get entries(): [string, PinLike][] { 81 | return Object.entries(this._pins); 82 | } 83 | 84 | /** 85 | * 86 | * Subscribes to the event of creation of a new pin. The subscriber function 87 | * will also be invoked on all of the already created pairs. 88 | * 89 | * @param subscriber 90 | * @returns a [`Subscription`](https://rxjs-dev.firebaseapp.com/guide/subscription) object 91 | * that you can later unsubscribe from using `.unsubscribe()` 92 | * 93 | */ 94 | public subscribe(subscriber: PinMapSusbcriber): Subscription { 95 | if (!this._subject) 96 | this._subject = new Subject<[string, PinLike]>(); 97 | 98 | this.entries.forEach(entry => subscriber(...entry)); 99 | return this.track(this._subject.subscribe(entry => subscriber(...entry))); 100 | } 101 | 102 | /** 103 | * 104 | * Clears all the created pins and remove references to them, 105 | * also will remove all subscriptions. 106 | * 107 | */ 108 | public clear(): this { 109 | this.pins.forEach(pin => pin.clear()); 110 | this._pins = {}; 111 | 112 | if (this._subject) { 113 | this._subject.complete(); 114 | this._subject = undefined; 115 | } 116 | 117 | return super.clear(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/pin/pin.ts: -------------------------------------------------------------------------------- 1 | import { Observable, merge } from 'rxjs'; 2 | 3 | import { Emission } from '../shared/emission'; 4 | 5 | import { PinLike } from './pin-like'; 6 | import { Connectible } from './connectible'; 7 | 8 | 9 | /** 10 | * 11 | * Represents the basic [pin](https://connective.dev/docs/pin) object. 12 | * This pin type gets locked when its observable is realized, 13 | * will resolve only when its observable is not realized and its resolution 14 | * will be merged observable of all of the incoming pins' observables. 15 | * 16 | */ 17 | export class Pin extends Connectible { 18 | /** 19 | * 20 | * Determines if this pin is locked, based on whether or not its underlying 21 | * observable has been resolved or not. 22 | * 23 | * @param observable 24 | * 25 | */ 26 | protected isLocked(observable: Observable | undefined) { 27 | return observable !== undefined; 28 | } 29 | 30 | /** 31 | * 32 | * Determines whether this pin should resolve its underlying observable, 33 | * based on whether or not its underlying observable has been resolved or not. 34 | * 35 | * @param _ 36 | * @param observable 37 | * 38 | */ 39 | protected shouldResolve(_: PinLike[], observable: Observable | undefined) { 40 | return observable === undefined; 41 | } 42 | 43 | /** 44 | * 45 | * Resolves its underlying observable, by 46 | * [mergeing](https://rxjs-dev.firebaseapp.com/api/index/function/merge) 47 | * corresponding observables of inbound pins. 48 | * 49 | * @param inbound 50 | * 51 | */ 52 | protected resolve(inbound: PinLike[]): Observable { 53 | return (inbound.length == 1)? 54 | inbound[0].observable: 55 | merge(...inbound.map(pin => pin.observable)); 56 | } 57 | } 58 | 59 | 60 | /** 61 | * 62 | * Creates a typical [pin](https://connective.dev/docs/pin) object. 63 | * [Checkout the docs](https://connective.dev/docs/pin) for examples and further information. 64 | * 65 | */ 66 | export function pin() { return new Pin(); } 67 | 68 | 69 | export default pin; 70 | -------------------------------------------------------------------------------- /src/pin/pipe.ts: -------------------------------------------------------------------------------- 1 | import { merge, OperatorFunction } from 'rxjs'; 2 | 3 | import { Emission } from '../shared/emission'; 4 | 5 | import { Pin } from './pin'; 6 | import { PinLike } from './pin-like'; 7 | 8 | 9 | export type PipeFunc = OperatorFunction; 10 | 11 | 12 | /** 13 | * 14 | * Represents [pipe](https://connective.dev/docs/pipe) pins. 15 | * 16 | */ 17 | export class Pipe extends Pin { 18 | /** 19 | * 20 | * The list of pipe functions that constitute this pipe. 21 | * 22 | */ 23 | readonly pipes: PipeFunc[]; 24 | 25 | constructor(pipes: PipeFunc[]) { 26 | super(); 27 | this.pipes = pipes; 28 | } 29 | 30 | /** 31 | * 32 | * Resolves the underling observable of the pin, by 33 | * [mergeing](https://rxjs-dev.firebaseapp.com/api/index/function/merge) 34 | * observables of inbound pins and piping them through specified 35 | * [pipeable operators](https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md). 36 | * 37 | * @param inbound 38 | * 39 | */ 40 | protected resolve(inbound: PinLike[]) { 41 | return this.pipes.reduce( 42 | (observable, pipe) => observable.pipe(pipe), 43 | (inbound.length == 1)? 44 | inbound[0].observable: 45 | merge(...inbound.map(pin => pin.observable)) 46 | ); 47 | } 48 | } 49 | 50 | 51 | /** 52 | * 53 | * Creates a [pipe](https://connective.dev/docs/pipe) pin using given pipe functions. 54 | * You can utilize this to use RxJS's pipeable operators in CONNECTIVE flows. 55 | * [Checkout the docs](https://connective.dev/docs/pipe) for examples and further information. 56 | * 57 | * @param pipes 58 | * 59 | */ 60 | export function pipe(...pipes: PipeFunc[]) { return new Pipe(pipes); } 61 | 62 | 63 | export default pipe; 64 | -------------------------------------------------------------------------------- /src/pin/reduce.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { map, mergeMap, share } from 'rxjs/operators'; 3 | 4 | import { ResolveCallback, ErrorCallback, ContextType } from '../shared/types'; 5 | import { Emission } from '../shared/emission'; 6 | import { EmissionError } from '../shared/errors/emission-error'; 7 | 8 | import { Pipe } from './pipe'; 9 | 10 | 11 | export type ReduceFuncSync = (acc: any, cur: any) => any; 12 | export type ReduceFuncAsync = (acc: any, cur: any, 13 | callback: ResolveCallback, 14 | error: ErrorCallback, 15 | emissionContext: ContextType, 16 | accContext: ContextType) => void; 17 | export type ReduceFunc = ReduceFuncSync | ReduceFuncAsync; 18 | 19 | 20 | const _Unset = {}; 21 | 22 | // 23 | // TODO: switch to concat map for async reducers 24 | // 25 | /** 26 | * 27 | * Represents [reduce](https://connective.dev/docs/reduce) pins. 28 | * 29 | */ 30 | export class Reduce extends Pipe { 31 | private _acc: Emission | undefined = undefined; 32 | 33 | /** 34 | * 35 | * @param reduce is the reduction function 36 | * @param start is the start value 37 | * 38 | */ 39 | constructor(readonly reduce: ReduceFunc, readonly start: any = _Unset) { 40 | super( 41 | (reduce.length <= 2)? 42 | ([map((emission: Emission) => { 43 | if (!this._acc) { 44 | this._acc = this._init(emission, start); 45 | if (start === _Unset) return this._acc; 46 | } 47 | 48 | this._acc = Emission.from([this._acc, emission], 49 | (reduce as ReduceFuncSync)(this._acc.value, emission.value)); 50 | return this._acc; 51 | })]): 52 | ([ 53 | mergeMap(emission => 54 | new Observable(subscriber => { 55 | if (!this._acc) { 56 | this._acc = this._init(emission, start); 57 | 58 | if (start === _Unset) { 59 | subscriber.next(this._acc); 60 | subscriber.complete(); 61 | return; 62 | } 63 | } 64 | 65 | reduce(this._acc.value, emission.value, 66 | (res: any) => { 67 | this._acc = Emission.from([this._acc, emission], res); 68 | subscriber.next(this._acc); 69 | subscriber.complete(); 70 | }, 71 | (error: Error | string) => { 72 | subscriber.error(new EmissionError(error, emission)); 73 | }, 74 | emission.context, this._acc.context); 75 | }) 76 | ), 77 | share()]) 78 | ); 79 | } 80 | 81 | private _init(emission: Emission, start: any): Emission { 82 | if (start !== _Unset) return emission.fork(start); 83 | else return emission; 84 | } 85 | } 86 | 87 | 88 | /** 89 | * 90 | * Creates a [reduce](https://connective.dev/docs/reduce) pin. 91 | * A reduce pin can be used to aggregate values over multiple emissions, with an 92 | * aggregator function updating the aggregate value based on each incoming emission. 93 | * [Checkout the docs](https://connective.dev/docs/reduce) for examples and further information. 94 | * 95 | * @param reduce the reduction function 96 | * @param start the start value. If not provided, the value of first incoming emission will be used. 97 | * 98 | */ 99 | export function reduce(reduce: ReduceFunc, start: any = _Unset) { return new Reduce(reduce, start); } 100 | 101 | 102 | export default reduce; 103 | -------------------------------------------------------------------------------- /src/pin/sink.ts: -------------------------------------------------------------------------------- 1 | import { tap } from 'rxjs/operators'; 2 | 3 | import { Emission } from '../shared/emission'; 4 | import { Bindable } from '../shared/bindable'; 5 | import { ContextType } from '../shared/types'; 6 | 7 | import { Pipe } from './pipe'; 8 | 9 | 10 | export type SinkFunc = (value: any, context: ContextType) => void; 11 | 12 | 13 | /** 14 | * 15 | * Represents [sink](https://connective.dev/docs/sink) pins. 16 | * 17 | */ 18 | export class Sink extends Pipe implements Bindable { 19 | private _bound = false; 20 | 21 | constructor(readonly func: SinkFunc = () => {}) { 22 | super([tap((emission: Emission) => func(emission.value, emission.context))]); 23 | } 24 | 25 | /** 26 | * 27 | * @returns `true` if this sink is already bound. 28 | * 29 | */ 30 | public get bound() { return this._bound; } 31 | 32 | /** 33 | * 34 | * Binds this sink if it is not already bound. Binding 35 | * Basically ensures that the pin is subscribed to and that its side-effect 36 | * will be enacted. 37 | * 38 | */ 39 | bind(): this { 40 | if (!this._bound) { 41 | this._bound = true; 42 | this.track(this.subscribe()); 43 | } 44 | 45 | return this; 46 | } 47 | } 48 | 49 | 50 | /** 51 | * 52 | * Creates a [sink](https://connective.dev/docs/sink) pin. 53 | * Sink pins can be used to do something with the data of a flow, outside the scope of the flow 54 | * (like logging them, etc). 55 | * [Checkout the docs](https://connective.dev/docs/sink) for examples and further information. 56 | * 57 | * @param func 58 | * 59 | */ 60 | export function sink(func?: SinkFunc) { return new Sink(func); } 61 | 62 | 63 | export default sink; 64 | -------------------------------------------------------------------------------- /src/pin/source.ts: -------------------------------------------------------------------------------- 1 | import { Subject, Observable } from 'rxjs'; 2 | 3 | import { ContextType } from '../shared/types'; 4 | import emission, { Emission } from '../shared/emission'; 5 | 6 | import { PinLike } from './pin-like'; 7 | import { Connectible } from './connectible'; 8 | 9 | 10 | /** 11 | * 12 | * Represents [source](https://connective.dev/docs/source) pins. 13 | * 14 | */ 15 | export class Source extends Connectible { 16 | constructor(private _subject = new Subject()){ 17 | super(); 18 | } 19 | 20 | /** 21 | * 22 | * This source will send given value, perhaps with given context. 23 | * Will create a new [emission](https://connective.dev/docs/emission) object. 24 | * 25 | * @param value the value to send 26 | * @param context the emission context 27 | * 28 | */ 29 | public send(value?: any, context?: ContextType) { 30 | this.emit(emission(value, context)); 31 | } 32 | 33 | /** 34 | * 35 | * Will emit the given emission object. 36 | * 37 | * @param emission 38 | * 39 | */ 40 | public emit(emission: Emission) { 41 | this._subject.next(emission); 42 | } 43 | 44 | /** 45 | * 46 | * @note this sends a complete notification through-out the flow. 47 | * Pins that are merely reliant on this source will also be unusable 48 | * afterwards. 49 | * 50 | */ 51 | clear() { 52 | this._subject.complete(); 53 | this._subject = new Subject(); 54 | 55 | return super.clear(); 56 | } 57 | 58 | /** 59 | * 60 | * Determines if any pin is connected to this pin. 61 | * 62 | */ 63 | protected isConnected() { 64 | return this.tracking || super.isConnected(); 65 | } 66 | 67 | /** 68 | * 69 | * Resolves the underlying observable of this pin by subscribing the 70 | * subject of this pin to all inbound pins. 71 | * 72 | * @param inbound 73 | * 74 | */ 75 | protected resolve(inbound: PinLike[]) { 76 | inbound.forEach(pin => { 77 | this.track(pin.observable.subscribe(this._subject)); 78 | }); 79 | 80 | inbound.length = 0; 81 | return this._subject; 82 | } 83 | 84 | /** 85 | * 86 | * Determines whether this pin is locked. A source is never locked. 87 | * 88 | */ 89 | protected isLocked() { return false; } 90 | 91 | /** 92 | * 93 | * Determines whether should resolve the underlying observable. 94 | * 95 | * @param inbound 96 | * @param observable 97 | * 98 | */ 99 | protected shouldResolve(inbound: PinLike[], observable: Observable | undefined) { 100 | return inbound.length > 0 || !observable; 101 | } 102 | } 103 | 104 | 105 | /** 106 | * 107 | * Creates a [source](https://connective.dev/docs/source) pin. 108 | * A source pin can be used as the starting point of a reactive flow. 109 | * [Checkout the docs](https://connective.dev/docs/source) for examples and further information. 110 | * 111 | */ 112 | export function source(sub?: Subject) { return new Source(sub); } 113 | 114 | 115 | export default source; 116 | -------------------------------------------------------------------------------- /src/pin/spread.ts: -------------------------------------------------------------------------------- 1 | import { from, of, Observable } from 'rxjs'; 2 | import { mergeMap } from 'rxjs/operators'; 3 | 4 | import { Emission } from '../shared/emission'; 5 | 6 | import { Pipe } from './pipe'; 7 | 8 | 9 | /** 10 | * 11 | * Represents [spread](https://connective.dev/docs/spread) pins. 12 | * 13 | */ 14 | export class Spread extends Pipe { 15 | constructor() { 16 | super([ 17 | mergeMap(emission => 18 | (emission.value.map)? 19 | >from(emission.value.map((v: any) => emission.fork(v))): 20 | of(emission) 21 | ) 22 | ]) 23 | } 24 | } 25 | 26 | 27 | /** 28 | * 29 | * Creates a [spread](https://connective.dev/docs/spread) pin. A spread pin can be used 30 | * to spread contents of an array over multiple emissions. 31 | * [Checkout the docs](https://connective.dev/docs/spread) for examples and further information. 32 | * 33 | */ 34 | export function spread() { return new Spread(); } 35 | 36 | 37 | export default spread; 38 | -------------------------------------------------------------------------------- /src/pin/test/control.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import emission from '../../shared/emission'; 4 | 5 | import group from '../group'; 6 | import { Source } from '../source'; 7 | import { Control } from '../control'; 8 | import { Pin } from '../pin'; 9 | import { PinMap } from '../pin-map'; 10 | 11 | 12 | describe('Control', () => { 13 | it('should be a `Pin`.', () => { 14 | new Control().should.be.instanceof(Pin); 15 | }); 16 | 17 | it('should only send data when all of its inbound pins have sent data.', () => { 18 | let c = false; 19 | let a = new Source(); let b = new Source(); 20 | 21 | group(a, b).to(new Control()).subscribe(() => c = true); 22 | 23 | a.send(); c.should.be.false; 24 | b.send(); c.should.be.true; 25 | }); 26 | 27 | it('should again wait for all of its inbound pins for subsequent data.', () => { 28 | let c = 0; 29 | let a = new Source(); let b = new Source(); 30 | group(a, b).to(new Control()).subscribe(() => c++); 31 | 32 | a.send(); b.send(); c.should.equal(1); 33 | a.send(); c.should.equal(1); 34 | b.send(); c.should.equal(2); 35 | }); 36 | 37 | it('should send data when not connected to any pin.', done => { 38 | new Control().subscribe(() => done()); 39 | }); 40 | 41 | it('should aggregate incoming values.', done => { 42 | let a = new Source(); 43 | let b = new Source(); 44 | group(a, b).to(new Control()).subscribe(val => { 45 | val.sort().should.eql([1, 2]); 46 | done(); 47 | }); 48 | 49 | a.send(1); 50 | b.send(2); 51 | }); 52 | 53 | it('should wait for all instantiated pins of a pin map and send their data in a key-value object', done => { 54 | let pm = new PinMap(); 55 | let c = new Control(pm); 56 | let a = new Source(); a.to(pm.get('x')); 57 | let b = new Source(); b.to(pm.get('y')); 58 | c.subscribe(data => { 59 | data.x.should.equal(2); 60 | data.y.should.equal(3); 61 | done(); 62 | }); 63 | 64 | a.send(2); 65 | b.send(3); 66 | }); 67 | 68 | it('should merge the context of incoming emissions.', done => { 69 | let a = new Source(); 70 | let b = new Source(); 71 | group(a, b).to(new Control()).observable.subscribe(emission => { 72 | emission.context.x.should.equal(2); 73 | emission.context.y.should.equal(3); 74 | done(); 75 | }); 76 | 77 | a.emit(emission(undefined, { x : 2 })); 78 | b.emit(emission(undefined, { y : 3 })); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/pin/test/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import emission from '../../shared/emission'; 4 | 5 | import { Source } from '../source'; 6 | import pin from '../pin'; 7 | import filter from '../filter'; 8 | 9 | 10 | describe('filter()', () => { 11 | it('should return a `PinLike` that only passes values that match the given function.', () => { 12 | let a = new Source(); 13 | let res: number[] = []; 14 | a.to(filter((n: number) => n % 2 == 0)).subscribe(x => res.push(x)); 15 | a.send(1); a.send(2); 16 | a.send(3); a.send(4); 17 | 18 | res.should.eql([2, 4]); 19 | }); 20 | 21 | it('should also work with an async function.', () => { 22 | let a = new Source(); 23 | let res: number[] = []; 24 | a.to(filter((n: number, c: (r: boolean)=>void) => c(n % 2 == 1))).subscribe(x => res.push(x)); 25 | a.send(1); a.send(2); 26 | a.send(3); a.send(4); 27 | 28 | res.should.eql([1, 3]); 29 | }); 30 | 31 | it('should hanlde errors of a sync function.', done => { 32 | let a = new Source(); 33 | a.to(filter(() => { throw new Error() })).subscribe(() => {}, () => done()); 34 | a.send(); 35 | }); 36 | 37 | it('should provide an async function with an error callback.', done => { 38 | let a = new Source(); 39 | a.to(filter((_: any, __: any, err: any) => { err(new Error()); })).subscribe(() => {}, () => done()); 40 | a.send(); 41 | }); 42 | 43 | it('should not share a sync func.', () => { 44 | let a = new Source(); let r = 0; 45 | a.to(filter(() => r+=1)).to(pin(), pin()).subscribe(); 46 | a.send(); 47 | r.should.equal(2); 48 | }); 49 | 50 | it('should share an async func.', () => { 51 | let a = new Source(); let r = 0; 52 | a.to(filter((_, done) => done(!!(r+=1)))).to(pin(), pin()).subscribe(); 53 | a.send(); 54 | r.should.equal(1); 55 | }); 56 | 57 | it('should provide the async function also with context.', done => { 58 | let a = new Source(); 59 | a.to(filter((_, __, ___, ctx) => { 60 | ctx.x.should.equal(2); 61 | done(); 62 | })).subscribe(); 63 | 64 | a.emit(emission(42, {x: 2})); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/pin/test/fork.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import fork, { Fork } from '../fork'; 4 | import pin from '../pin'; 5 | import source from '../source'; 6 | 7 | 8 | describe('Fork', () => { 9 | it('should send the emission to outgoing pins with the same fork tag.', () => { 10 | let a = source(); 11 | let b = pin(); let c = pin(); 12 | 13 | a.to(fork()).to(b, c); 14 | 15 | let _b_fork: any = undefined; let _c_fork: any = undefined; 16 | b.observable.subscribe(e => _b_fork = e.context.__fork); 17 | c.observable.subscribe(e => _c_fork = e.context.__fork); 18 | 19 | a.send(2); 20 | _b_fork.should.eql(_c_fork); 21 | }); 22 | 23 | it('should create unique fork tags for each incoming emission.', () => { 24 | let a = source(); 25 | let b = pin(); let c = pin(); 26 | 27 | a.to(fork()).to(b, c); 28 | 29 | let _b_fork: any[] = []; let _c_fork: any[] = []; 30 | b.observable.subscribe(e => _b_fork.push(e.context.__fork)); 31 | c.observable.subscribe(e => _c_fork.push(e.context.__fork)); 32 | 33 | a.send(2); 34 | a.send(42); 35 | _b_fork[0].should.eql(_c_fork[0]); 36 | _b_fork[1].should.eql(_c_fork[1]); 37 | _b_fork[0].should.not.eql(_b_fork[1]); 38 | }); 39 | 40 | it('should preserve already set fork tags when chain forking.', () => { 41 | let a = source(); 42 | let b = pin(); let c = pin(); 43 | 44 | a.to(fork()).to(b, c.from(fork())); 45 | 46 | let _b_fork: any = undefined; let _c_fork: any = undefined; 47 | b.observable.subscribe(e => _b_fork = e.context.__fork); 48 | c.observable.subscribe(e => _c_fork = e.context.__fork); 49 | 50 | a.send(2); 51 | 52 | _b_fork.length.should.equal(1); 53 | _c_fork.length.should.equal(2); 54 | _b_fork[0].should.equal(_c_fork[0]); 55 | _b_fork[0].should.not.equal(_c_fork[1]); 56 | }); 57 | }); 58 | 59 | describe('fork()', () => { 60 | it('should create a `Fork` pin.', () => { 61 | fork().should.be.instanceof(Fork); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/pin/test/index.ts: -------------------------------------------------------------------------------- 1 | describe('pin', () => { 2 | require('./pin-like.test'); 3 | require('./group.test'); 4 | require('./source.test'); 5 | require('./pin.test'); 6 | require('./wrap.test'); 7 | require('./control.test'); 8 | require('./filter.test'); 9 | require('./map.test'); 10 | require('./pack.test'); 11 | require('./pin-map.test'); 12 | require('./sink.test'); 13 | require('./value.test'); 14 | require('./fork.test'); 15 | require('./reduce.test'); 16 | require('./spread.test'); 17 | require('./partial-flow.test'); 18 | }); 19 | -------------------------------------------------------------------------------- /src/pin/test/map.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import emission from '../../shared/emission'; 4 | 5 | import { Source } from '../source'; 6 | import pin from '../pin'; 7 | import map from '../map'; 8 | 9 | 10 | describe('map()', () => { 11 | it('should return a `PinLike` that maps any passed value by given function.', () => { 12 | let a = new Source(); 13 | let res: number[] = []; 14 | a.to(map((n: number) => n * 2)).subscribe(n => res.push(n)); 15 | a.send(1); 16 | a.send(2); 17 | res.should.eql([2, 4]); 18 | }); 19 | 20 | it('should also work with an async function.', () => { 21 | let a = new Source(); 22 | let res: number[] = []; 23 | a.to(map((n: number, cb: any) => cb(n * 2 + 1))).subscribe(x => res.push(x)); 24 | a.send(1); a.send(2); 25 | 26 | res.should.eql([3, 5]); 27 | }); 28 | 29 | it('should not keep the order for an async map.', done => { 30 | let a = new Source(); 31 | let res: number[] = []; 32 | a.to(map((n: number, cb: any) => setTimeout(() => cb(n * 2 + 1), 5 - n))) 33 | .subscribe( 34 | x => res.push(x), 35 | () => {}, 36 | () => { 37 | res.should.eql([5, 3]); 38 | done(); 39 | } 40 | ); 41 | 42 | a.send(1); a.send(2); a.clear(); 43 | }); 44 | 45 | it('should hanlde errors of a sync function.', done => { 46 | let a = new Source(); 47 | a.to(map(() => { throw new Error() })).subscribe(() => {}, () => done()); 48 | a.send(); 49 | }); 50 | 51 | it('should provide an async function with an error callback.', done => { 52 | let a = new Source(); 53 | a.to(map((_: any, __: any, err: any) => { err(new Error()); })).subscribe(() => {}, () => done()); 54 | a.send(); 55 | }); 56 | 57 | it('should not share a sync func.', () => { 58 | let a = new Source(); let r = 0; 59 | a.to(map(() => r+=1)).to(pin(), pin()).subscribe(); 60 | a.send(); 61 | r.should.equal(2); 62 | }); 63 | 64 | it('should share an async func.', () => { 65 | let a = new Source(); let r = 0; 66 | a.to(map((_, done) => done(r+=1))).to(pin(), pin()).subscribe(); 67 | a.send(); 68 | r.should.equal(1); 69 | }); 70 | 71 | it('should provide an async function with context as well.', done => { 72 | let a = new Source(); 73 | a.to(map((_, __, ___, ctx) => { 74 | ctx.x.should.equal(2); 75 | done(); 76 | })).subscribe(); 77 | 78 | a.emit(emission(42, {x: 2})); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/pin/test/pack.test.ts: -------------------------------------------------------------------------------- 1 | import { should, expect } from 'chai'; should(); 2 | 3 | import emission from '../../shared/emission'; 4 | 5 | import group from '../group'; 6 | import { Source } from '../source'; 7 | import { PinMap } from '../pin-map'; 8 | import pack from '../pack'; 9 | 10 | 11 | describe('pack()', () => { 12 | it('should wait for all incoming pins and send their data.', done => { 13 | let a = new Source(); 14 | let b = new Source(); 15 | group(a, b).to(pack()).subscribe(data => { 16 | data.should.eql(['hellow', 'world']); 17 | done(); 18 | }); 19 | 20 | a.send('hellow'); 21 | b.send('world'); 22 | }); 23 | 24 | it('should receive the incoming pins in constructor as well.', done => { 25 | let a = new Source(); 26 | let b = new Source(); 27 | pack(a,b).subscribe(data => { 28 | data.should.eql(['hellow', 'world']); 29 | done(); 30 | }); 31 | 32 | a.send('hellow'); 33 | b.send('world'); 34 | }); 35 | 36 | it('should wait for all instantiated pins of a pin map and send their data in a key-value object', done => { 37 | let pm = new PinMap(); 38 | let p = pack(pm); 39 | let a = new Source(); a.to(pm.get('x')); 40 | let b = new Source(); b.to(pm.get('y')); 41 | p.subscribe(data => { 42 | data.x.should.equal(2); 43 | data.y.should.equal(3); 44 | done(); 45 | }); 46 | 47 | a.send(2); 48 | b.send(3); 49 | }); 50 | 51 | it('should work properly if connected to a pinmap that already has some pins.', done => { 52 | let pm = new PinMap(); 53 | let a = new Source(); a.to(pm.get('x')); 54 | let b = new Source(); b.to(pm.get('y')); 55 | let p = pack(pm); 56 | p.subscribe(data => { 57 | data.x.should.equal(2); 58 | data.y.should.equal(3); 59 | done(); 60 | }); 61 | 62 | a.send(2); 63 | b.send(3); 64 | }); 65 | 66 | it('should send data instantly if connected pinmap has no pins.', done => { 67 | pack(new PinMap()).observable.subscribe(() => done()); 68 | }); 69 | 70 | it('should also handle a combination of pins and pinmaps.', done => { 71 | let pm = new PinMap(); let pm2 = new PinMap(); 72 | let a = new Source(); 73 | let b = new Source(); b.to(pm.get('x')); 74 | let c = new Source(); c.to(pm.get('y')); 75 | let d = new Source(); 76 | let e = new Source(); e.to(pm2.get('I')); 77 | 78 | pack(a, pm, d, pm2).subscribe(data => { 79 | data[0].should.equal(42); 80 | data[1].x.should.equal('world'); 81 | expect(data[1].y).to.be.undefined; 82 | data[2].should.be.false; 83 | expect(data[3].I).to.be.undefined; 84 | done(); 85 | }); 86 | 87 | a.send(42); b.send('world'); c.send(); d.send(false); e.send(undefined); 88 | }); 89 | 90 | it('should merge context of incoming emissions.', done => { 91 | let a = new Source(); 92 | let b = new Source(); 93 | let pm = new PinMap(); 94 | let c = new Source(); c.to(pm.get('x')); 95 | let d = new Source(); d.to(pm.get('y')); 96 | 97 | pack(a, b, pm).observable.subscribe(emission => { 98 | emission.context.x.should.equal(2); 99 | emission.context.y.should.equal(3); 100 | emission.context.w.should.equal(4); 101 | emission.context.z.should.equal(5); 102 | done(); 103 | }); 104 | 105 | a.emit(emission(undefined, { x : 2 })); 106 | b.emit(emission(undefined, { y : 3 })); 107 | c.emit(emission(undefined, { w : 4 })); 108 | d.emit(emission(undefined, { z : 5 })); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/pin/test/partial-flow.test.ts: -------------------------------------------------------------------------------- 1 | import { should, expect } from 'chai'; should(); 2 | 3 | import { group, Group } from '../group'; 4 | import { source, Source } from '../source'; 5 | import { pin, Pin } from '../pin'; 6 | import { map } from '../map'; 7 | import { partialFlow, PartialFlow } from '../partial-flow'; 8 | 9 | 10 | const testFlow = () => { 11 | let i1 = pin(); let i2 = pin(); 12 | let o1 = pin(); let o2 = pin(); 13 | i1.to(map((x: any) => '1-2:: ' + x)).to(o2); 14 | i2.to(map((x: any) => '2-1:: ' + x)).to(o1); 15 | return <[Pin[], Pin[]]>[[i1, i2], [o1, o2]]; 16 | }; 17 | 18 | 19 | describe('PartialFlow', () => { 20 | it('should be connectible properly via `.to()`.', () => { 21 | let a = source(); 22 | let res = []; 23 | 24 | a.to(partialFlow(testFlow)).to(pin()).subscribe(v => res.push(v)); 25 | 26 | a.send('hellow'); 27 | 28 | res.length.should.equal(2); 29 | res.should.include('1-2:: hellow'); 30 | res.should.include('2-1:: hellow'); 31 | }); 32 | 33 | it('should be connectible properly via `.from()`', () => { 34 | let b = pin(); 35 | let res = []; 36 | 37 | let a = b.from(partialFlow(testFlow)).from(source()) as Source; 38 | b.subscribe(v => res.push(v)); 39 | 40 | a.send('hellow'); 41 | 42 | res.length.should.equal(2); 43 | res.should.include('1-2:: hellow'); 44 | res.should.include('2-1:: hellow'); 45 | }); 46 | 47 | it('should be connectible serially using `serialTo()`', () => { 48 | let a1 = source(); let a2 = source(); 49 | let b1 = pin(); let b2 = pin(); 50 | let r1 = []; let r2 = []; 51 | 52 | 53 | group(a1, a2).serialTo(partialFlow(testFlow)).serialTo(b1, b2); 54 | b1.subscribe(v => r1.push(v)); 55 | b2.subscribe(v => r2.push(v)); 56 | 57 | a1.send('A1'); a2.send('A2'); 58 | 59 | r1.should.eql(['2-1:: A2']); 60 | r2.should.eql(['1-2:: A1']); 61 | }); 62 | 63 | it('should be connectible serially using `serialFrom()`', () => { 64 | let a1 = source(); let a2 = source(); 65 | let b1 = pin(); let b2 = pin(); 66 | let r1 = []; let r2 = []; 67 | 68 | 69 | group(b1, b2).serialFrom(partialFlow(testFlow)).serialFrom(a1, a2); 70 | b1.subscribe(v => r1.push(v)); 71 | b2.subscribe(v => r2.push(v)); 72 | 73 | a1.send('A1'); a2.send('A2'); 74 | 75 | r1.should.eql(['2-1:: A2']); 76 | r2.should.eql(['1-2:: A1']); 77 | }); 78 | }); -------------------------------------------------------------------------------- /src/pin/test/pin-like.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { pin } from '../pin'; 6 | import { isPinLike } from '../pin-like'; 7 | 8 | 9 | describe('isPinLike()', () => { 10 | it('should return true for objects that are `PinLike` and false for those who are not.', () => { 11 | isPinLike({ 12 | from() {}, to() {}, subscribe() {}, 13 | observable: new Observable() 14 | }).should.be.true; 15 | 16 | isPinLike({ 17 | from() {}, subscribe() {}, 18 | observable: new Observable() 19 | }).should.be.false; 20 | 21 | isPinLike({ 22 | from() {}, to() {}, 23 | observable: new Observable() 24 | }).should.be.false; 25 | 26 | isPinLike({ 27 | from() {}, to() {}, subscribe() {}, 28 | }).should.be.false; 29 | 30 | isPinLike({}).should.be.false; 31 | isPinLike(undefined).should.be.false; 32 | }); 33 | 34 | it('should not realize the observable of the pin-like.', () => { 35 | let p = pin(); 36 | p.locked.should.be.false; 37 | isPinLike(p); 38 | p.locked.should.be.false; 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/pin/test/reduce.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import emission from '../../shared/emission'; 4 | 5 | import reduce from '../reduce'; 6 | import source from '../source'; 7 | import pin from '../pin'; 8 | 9 | 10 | describe('reduce()', () => { 11 | it('should reduce incoming values based on given starting value and reduce function.', () => { 12 | let res = []; 13 | 14 | let a = source(); 15 | a.to(reduce((x: number, y: number) => x + y, 0)).subscribe(v => res.push(v)); 16 | 17 | a.send(1); 18 | a.send(2); 19 | a.send(3); 20 | res.should.eql([1, 3, 6]); 21 | }); 22 | 23 | it('should assume the first value as the starting value if no starting value is provided.', () => { 24 | let res = []; 25 | 26 | let a = source(); 27 | a.to(reduce((x: number, y: number) => x * y)).subscribe(v => res.push(v)); 28 | 29 | a.send(1); 30 | a.send(2); 31 | a.send(3); 32 | res.should.eql([1, 2, 6]); 33 | }); 34 | 35 | it('should properly work with 0 as starting value', () => { 36 | let res: number = 0; 37 | 38 | let a = source(); 39 | a.to(reduce((x: number, y: number) => x + y)).subscribe(v => res = v); 40 | a.send(0); 41 | a.send(0) 42 | a.send(0); 43 | a.send(32); 44 | a.send(0); 45 | 46 | res.should.equal(32); 47 | }); 48 | 49 | it('should work properly with async reduce functions as well.', () => { 50 | let res = []; 51 | 52 | let a = source(); 53 | a.to(reduce((x: number, y: number, cb) => cb(x * y))).subscribe(v => res.push(v)); 54 | 55 | a.send(1); 56 | a.send(2); 57 | a.send(3); 58 | res.should.eql([1, 2, 6]); 59 | }); 60 | 61 | it('should handle errors of a sync function.', done => { 62 | let a = source(); 63 | a.to(reduce(() => { throw new Error() })).subscribe(() => {}, () => done()); 64 | a.send(); a.send(); 65 | }); 66 | 67 | it('should provide an async func with an error callback.', done => { 68 | let a = source(); 69 | a.to(reduce((_: any, __:any, ___: any, err: any) => { err(new Error()); })).subscribe(() => {}, () => done()); 70 | a.send(); a.send(); 71 | }); 72 | 73 | it('should not share the sync func.', () => { 74 | let a = source(); let r = 0; 75 | a.to(reduce(() => r+=1)).to(pin(), pin()).subscribe(); 76 | a.send(); a.send(); 77 | r.should.equal(3); 78 | }); 79 | 80 | it('should share an async func.', () => { 81 | let a = source(); let r = 0; 82 | a.to(reduce((_, __, done) => done(r+=1))).to(pin(), pin()).subscribe(); 83 | a.send(); a.send(); 84 | r.should.equal(1); 85 | }); 86 | 87 | it('should provide an async function with context of emission and inbound as well.', done => { 88 | let a = source(); 89 | a.to(reduce((_, __, ___, ____, ctx, accCtx) => { 90 | accCtx.x.should.equal(2); 91 | ctx.y.should.equal(3); 92 | done(); 93 | })).subscribe(); 94 | 95 | a.emit(emission(42, {x: 2})); 96 | a.emit(emission(42, {y: 3})); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/pin/test/sink.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { of } from 'rxjs'; 4 | 5 | import emission from '../../shared/emission'; 6 | 7 | import sink, { Sink } from '../sink'; 8 | import wrap from '../wrap'; 9 | import { Source } from '../source'; 10 | import { Pin } from '../pin'; 11 | 12 | 13 | describe('sink()', () => { 14 | it('should be a lock the connected graph before it when its `.bind()` is called.', () => { 15 | let s = sink(); 16 | let a = new Pin(); 17 | a.to(new Pin(), new Pin()).to(s); 18 | s.bind(); 19 | a.locked.should.be.true; 20 | }); 21 | 22 | it('should invoke the given sink func upon `.bind()`', done => { 23 | ( 24 | wrap(of(42)) 25 | .to(sink((val: any) => { 26 | val.should.equal(42); 27 | done(); 28 | })) as Sink 29 | ).bind(); 30 | }); 31 | 32 | it('should not invoke the function after `.clear()`', () => { 33 | let c = 0; 34 | let a = new Source(); 35 | let s1 = sink(() => c++); 36 | let s2 = sink(() => c++); 37 | a.to(s1, s2); 38 | 39 | a.send(); 40 | c.should.equal(0); 41 | 42 | s1.bind(); 43 | a.send(); 44 | c.should.equal(1); 45 | 46 | s2.bind(); 47 | a.send(); 48 | c.should.equal(3); 49 | 50 | s1.clear(); 51 | a.send(); 52 | c.should.equal(4); 53 | }); 54 | 55 | it('should also call the function when `.bind()` is not called but the pin chain is actualized.', done => { 56 | let p = new Pin(); 57 | let a = new Source(); 58 | a.to(sink(() => done())).to(p); 59 | 60 | p.subscribe(); 61 | a.send(42); 62 | }); 63 | 64 | it('should provide the sink func with context.', done => { 65 | let a = new Source(); 66 | a.to(sink((_, ctx) => { 67 | ctx.x.should.equal(42); 68 | done(); 69 | })).subscribe(); 70 | 71 | a.emit(emission(2, {x: 42})); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/pin/test/source.test.ts: -------------------------------------------------------------------------------- 1 | import { should, expect } from 'chai'; should(); 2 | 3 | import group from '../group'; 4 | import { Source } from '../source'; 5 | import { Pin } from '../pin'; 6 | 7 | 8 | describe('Source', () => { 9 | describe('.send()', () => { 10 | it('should send data retrievable via `.subscribe()`.', done => { 11 | let s = new Source(); 12 | s.subscribe(data => { 13 | data.should.equal(42); 14 | done(); 15 | }); 16 | s.send(42); 17 | }); 18 | 19 | it('should be able to send context as well, retrievable via `.observable`', done => { 20 | let s = new Source(); 21 | s.observable.subscribe(emission => { 22 | expect(emission.context).not.to.be.undefined; 23 | emission.context.x.should.equal(42); 24 | done(); 25 | }); 26 | s.send(undefined, {x: 42}); 27 | }); 28 | }); 29 | 30 | describe('.from()', () => { 31 | it('should receive data from another pin.', done => { 32 | let a = new Source(); let b = new Source(); 33 | a.to(b).subscribe(data => { 34 | data.should.equal('Howdy!!'); 35 | done(); 36 | }); 37 | a.send('Howdy!!'); 38 | }); 39 | 40 | it('should be able to receive from multiple sources.', () => { 41 | let _ = 0; 42 | let a = new Source(); let b = new Source(); 43 | group(a, b).to(new Source()).subscribe(n => _ += n); 44 | 45 | a.send(1); b.send(2); a.send(3); 46 | _.should.equal(6); 47 | }); 48 | 49 | it('should also receive context.', done => { 50 | let a = new Source(); 51 | 52 | a.to(new Source()).observable.subscribe(emission => { 53 | emission.context.x.should.equal(42); 54 | done(); 55 | }); 56 | 57 | a.send('whatever', {x : 42}); 58 | }); 59 | 60 | it('should not lock pins.', () => { 61 | let a = new Pin(); 62 | new Source().from(a); 63 | a.locked.should.be.false; 64 | }); 65 | }); 66 | 67 | describe('.to()', () => { 68 | it('should channel data to another pin.', done => { 69 | let a = new Source(); 70 | a.to(new Source()).subscribe(() => done()); 71 | a.send(); 72 | }); 73 | }); 74 | 75 | describe('.clear()', () => { 76 | it('should clear the incoming connections.', () => { 77 | let a = new Source(); let b = new Source(); 78 | let called = false; 79 | a.to(b).subscribe(() => called = true); 80 | 81 | a.send(); 82 | called.should.be.true; 83 | 84 | b.clear(); called = false; 85 | 86 | a.send(); 87 | called.should.be.false; 88 | }); 89 | 90 | it('should clear outgoing connections as well.', () => { 91 | let a = new Source(); let b = new Source(); let called = false; 92 | a.to(b).subscribe(() => called = true); 93 | 94 | a.send(); 95 | called.should.be.true; 96 | 97 | a.clear(); called = false; 98 | 99 | a.send(); 100 | called.should.be.false; 101 | }); 102 | 103 | it('should be usable after being cleared.', () => { 104 | let a = new Source(); let b = new Source(); 105 | let called = false; a.to(b); 106 | 107 | a.clear().to(b); 108 | b.subscribe(() => called = true); 109 | a.send(); 110 | called.should.be.true; 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/pin/test/spread.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import source from '../source'; 4 | import spread from '../spread'; 5 | 6 | 7 | describe('spread()', () => { 8 | it('should spread an array into separate emissions.', () => { 9 | let r = []; 10 | let a = source(); 11 | a.to(spread()).subscribe(v => r.push(v)); 12 | a.send([1, 2, 3]); 13 | r.should.eql([1, 2, 3]); 14 | }); 15 | 16 | it('should simply pass on values that are not arrays.', done => { 17 | let a = source(); 18 | a.to(spread()).subscribe(v => { 19 | v.should.equal(42); 20 | done(); 21 | }); 22 | 23 | a.send(42); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/pin/test/value.test.ts: -------------------------------------------------------------------------------- 1 | import { should, expect } from 'chai'; should(); 2 | 3 | import group from '../group'; 4 | import { Source } from '../source'; 5 | import { Pin } from '../pin'; 6 | import value from '../value'; 7 | 8 | 9 | describe('value()', () => { 10 | it('should be a `Pin`.', () => { 11 | value(42).should.be.instanceof(Pin); 12 | }); 13 | 14 | it('should only send given value when all of its inbound pins have sent data.', () => { 15 | let c = undefined; 16 | let a = new Source(); let b = new Source(); 17 | 18 | group(a, b).to(value('hellow')).subscribe(val => c = val); 19 | 20 | a.send(); expect(c).to.be.undefined; 21 | b.send(); expect(c).to.equal('hellow'); 22 | }); 23 | 24 | it('should again wait for all of its inbound pins for subsequently resending its value.', () => { 25 | let c = 0; 26 | let a = new Source(); let b = new Source(); 27 | group(a, b).to(value(3)).subscribe(v => c += v); 28 | 29 | a.send(); b.send(); c.should.equal(3); 30 | a.send(); c.should.equal(3); 31 | b.send(); c.should.equal(6); 32 | }); 33 | 34 | it('should send its value when not connected to any pin.', done => { 35 | value(42).subscribe(val => { 36 | val.should.equal(42); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/pin/test/wrap.test.ts: -------------------------------------------------------------------------------- 1 | import { should, expect } from 'chai'; should(); 2 | 3 | import { from, of } from 'rxjs'; 4 | 5 | import group from '../group'; 6 | import wrap from '../wrap'; 7 | import { Pin } from '../pin'; 8 | 9 | 10 | describe('wrap()', () => { 11 | it('should return a `PinLike` wrapping a given observable.', done => { 12 | let p = wrap(of('hellow')); 13 | 14 | p.subscribe(data => { 15 | data.should.equal('hellow'); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should return a `PinLike` that cannot be connected to.', () => { 21 | expect(() => wrap(of(42)).from(new Pin())).to.throw(); 22 | }); 23 | 24 | it('should return a `PinLike` that can connect to other pins.', () => { 25 | let _ : number[] = []; 26 | 27 | group( 28 | wrap(of(1)), 29 | wrap(from([2, 3, 4])) 30 | ) 31 | .to(new Pin()) 32 | .subscribe(n => _.push(n)); 33 | 34 | _.should.have.members([4, 3, 2, 1]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/pin/value.ts: -------------------------------------------------------------------------------- 1 | import control from './control'; 2 | 3 | 4 | /** 5 | * 6 | * Creates a [value](https://connective.dev/docs/value) pin. A value 7 | * pin will emit its value each time all connected pins emit, or emit it 8 | * per subscription when no pins are connected to it. 9 | * [Checkout the docs](https://connective.dev/docs/value) for examples and further information. 10 | * 11 | * @param val 12 | * 13 | */ 14 | export function value(val: any) { return control(val); } 15 | 16 | 17 | export default value; 18 | -------------------------------------------------------------------------------- /src/pin/wrap.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { map } from 'rxjs/operators'; 3 | 4 | import emission, { Emission } from '../shared/emission'; 5 | 6 | import { PinLockedError } from './errors/locked.error'; 7 | import { PinLike } from './pin-like'; 8 | import { BasePin } from './base'; 9 | 10 | 11 | /** 12 | * 13 | * Represents [wrap](https://connective.dev/docs/wrap) pins. 14 | * 15 | */ 16 | class Wrapper extends BasePin { 17 | readonly observable: Observable; 18 | constructor(observable: Observable) { 19 | super(); 20 | this.observable = observable.pipe(map(v => emission(v))); 21 | } 22 | 23 | connect(_: PinLike): this { 24 | throw new PinLockedError(); 25 | } 26 | } 27 | 28 | 29 | /** 30 | * 31 | * Creates a [wrap](https://connective.dev/docs/wrap) pin. A wrap pin 32 | * wraps a given observable so that it can be connected to other pins. Because 33 | * its observable is already realized, you cannot connect other pins to a wrap pin. 34 | * [Checkout the docs](https://connective.dev/docs/wrap) for examples and further information. 35 | * 36 | * @param observable 37 | * 38 | */ 39 | export function wrap(observable: Observable) { return new Wrapper(observable); } 40 | 41 | 42 | export default wrap; 43 | -------------------------------------------------------------------------------- /src/shared/bindable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Denotes that this object can (and perhaps should be) bound at some point, 4 | * using its `.bind()` method. 5 | * 6 | */ 7 | export interface Bindable { 8 | bind(): any; 9 | } 10 | 11 | 12 | /** 13 | * 14 | * Checks if given object matches [Bindable](https://connective.dev/docs/interfaces#bindable) interface. 15 | * Basically checks if `.bind()` method exists. 16 | * 17 | * @param whatever 18 | * @return `true` if `any` is `Bindable` 19 | * 20 | */ 21 | export function isBindable(whatever: any): whatever is Bindable { 22 | return !!(whatever.bind) && typeof whatever.bind === 'function'; 23 | } -------------------------------------------------------------------------------- /src/shared/clearable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Denotes that this object can (and perhaps should be) cleared at some point, 4 | * using its `.clear()` method. 5 | * 6 | */ 7 | export interface Clearable { 8 | clear(): any; 9 | } 10 | 11 | 12 | /** 13 | * 14 | * Checks if given object matches [Clearable](https://connective.dev/docs/interfaces#clearable) interface. 15 | * Basically checks if `.clear()` method exists. 16 | * 17 | * @param whatever 18 | * @return `true` if `any` is `Clerable` 19 | * 20 | */ 21 | export function isClearable(whatever: any): whatever is Clearable { 22 | return !!(whatever.clear) && typeof whatever.clear === 'function'; 23 | } -------------------------------------------------------------------------------- /src/shared/errors/emission-error.ts: -------------------------------------------------------------------------------- 1 | import { Emission } from '../emission'; 2 | 3 | 4 | /** 5 | * 6 | * Represents when an error has occured during handling an emission. 7 | * You can retrieve the emission that resulted in the error via `.emission` property, 8 | * and you can retrieve the original error via `.original` property. 9 | * 10 | */ 11 | export class EmissionError extends Error { 12 | readonly original: Error; 13 | 14 | constructor(original: Error | string, readonly emission: Emission) { 15 | super(original instanceof Error?original.message:original); 16 | if (original instanceof Error) this.original = original; 17 | else this.original = new Error(original); 18 | } 19 | 20 | public get message(): string { return this.original.message; } 21 | public get stack(): string | undefined { return this.original.stack; } 22 | } 23 | 24 | 25 | /** 26 | * 27 | * Checks if an object is an `EmissionError` (this is needed due to some issues 28 | * with Typescript's typechecking on Errors). 29 | * 30 | */ 31 | export function isEmissionError(err: any): err is EmissionError { 32 | return err instanceof Error && 33 | (err as any).original instanceof Error && 34 | (err as any).emission instanceof Emission; 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | import { emission, Emission, MergedEmissionContextVal } from './emission'; 2 | import { Clearable, isClearable } from './clearable'; 3 | import { Bindable, isBindable } from './bindable'; 4 | import { ErrorCallback, ResolveCallback, NotifyCallback, ContextType } from './types'; 5 | import { EmissionError } from './errors/emission-error'; 6 | 7 | export { 8 | emission, Emission, MergedEmissionContextVal, 9 | Clearable, isClearable, 10 | Bindable, isBindable, 11 | ErrorCallback, ResolveCallback, NotifyCallback, ContextType, 12 | EmissionError 13 | } -------------------------------------------------------------------------------- /src/shared/test/bindable.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { isBindable } from '../bindable'; 4 | 5 | 6 | describe('bindable', () => { 7 | describe('.isBindable()', () => { 8 | it('should return if something satisfies bindable interface.', () => { 9 | class X { bind(): this { return this; } } 10 | class Y {} 11 | let Z = { bind: () => {} }; 12 | let W = 42; 13 | 14 | isBindable(new X()).should.be.true; 15 | isBindable(new Y()).should.be.false; 16 | isBindable(Z).should.be.true; 17 | isBindable(W).should.be.false; 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/shared/test/clearable.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { isClearable } from '../clearable'; 4 | 5 | 6 | describe('clearable', () => { 7 | describe('.isClearable()', () => { 8 | it('should return if something satisfies clearable interface.', () => { 9 | class X { clear(): this { return this; } } 10 | class Y {} 11 | let Z = { clear: () => {} }; 12 | let W = 42; 13 | 14 | isClearable(new X()).should.be.true; 15 | isClearable(new Y()).should.be.false; 16 | isClearable(Z).should.be.true; 17 | isClearable(W).should.be.false; 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/shared/test/emission.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import emission, { Emission, MergedEmissionContextVal } from '../emission'; 4 | 5 | 6 | describe('Emission', () => { 7 | describe('.fork()', () => { 8 | it('should create a new Emission with same context and updated value.', () => { 9 | let ctx = {x : 42}; 10 | let a = new Emission(1, ctx).fork(2); 11 | a.value.should.equal(2); 12 | a.context.should.equal(ctx); 13 | }); 14 | }); 15 | }); 16 | 17 | describe('Emission.from()', () => { 18 | it('should create a new emission from some other emissions, with the value being an array of original emissions.', () => { 19 | Emission.from([emission(42), emission(31)]).value.should.eql([42, 31]); 20 | }); 21 | 22 | it('should accept a replacement value', () => { 23 | Emission.from([emission(42), emission(31)], 'well ...').value.should.equal('well ...'); 24 | }); 25 | 26 | it('should merge the context of original emissions', () => { 27 | let e = Emission.from([emission(null, {x: 42}), emission(null, {y: 31})]); 28 | e.context.x.should.equal(42); 29 | e.context.y.should.equal(31); 30 | }); 31 | 32 | it('should store overriding context values in `MergedEmissionContextVal` objects referencing all original values.', () => { 33 | let e = Emission.from([emission(null, {x: 42}), emission(null, {x: 31})]); 34 | e.context.x.should.be.instanceof(MergedEmissionContextVal); 35 | e.context.x.values.should.eql([42, 31]); 36 | }); 37 | 38 | it('should keep the context as flat as possible after chain mergers.', () => { 39 | let e = Emission.from([ 40 | Emission.from([emission(null, {x: 42, z: 21}), emission(null, {x: 31, y: 3})]), 41 | Emission.from([emission(null, {y: 5, x: 'hellow'}), emission(null, {y: 6, x: 13})]) 42 | ]); 43 | 44 | e.context.z.should.equal(21); 45 | e.context.x.values.should.eql([42, 31, 'hellow', 13]); 46 | e.context.y.values.should.eql([3, 5, 6]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/shared/test/index.ts: -------------------------------------------------------------------------------- 1 | import { Bindable, Clearable } from ".."; 2 | 3 | describe('shared', () => { 4 | require('./bindable.test'); 5 | require('./clearable.test'); 6 | require('./emission.test'); 7 | require('./tracker.test'); 8 | 9 | it('should be feasible to define inline `Bindable|Clearable`s without explicit typecasting.', () => { 10 | (function(_: Bindable | Clearable){})({ 11 | clear() { return this; } 12 | }); 13 | 14 | (function(_: Bindable | Clearable){})({ 15 | bind() { return this; } 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/shared/test/tracker.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; should(); 2 | 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Tracker } from '../tracker'; 6 | import { isClearable } from '../clearable'; 7 | 8 | 9 | describe('Tracker', () => { 10 | it('should be clearable.', () => { 11 | isClearable(new Tracker()).should.be.true; 12 | }); 13 | 14 | it('should unsubscribe tracked subscriptions when cleared.', done => { 15 | class T extends Tracker { 16 | constructor() { 17 | super(); 18 | this.track(new Subscription(() => done())); 19 | }; 20 | } 21 | 22 | new T().clear(); 23 | }); 24 | 25 | describe('.untrack()', () => { 26 | it('should remove subscription from tracked subscriptions.', done => { 27 | class T extends Tracker { 28 | constructor() { 29 | super(); 30 | this.track(new Subscription(() => done())); 31 | // 32 | // if this does not remove the sub, done() will be called twice. 33 | // 34 | this.untrack(this.track(new Subscription(() => done()))); 35 | }; 36 | } 37 | 38 | new T().clear(); 39 | }); 40 | }); 41 | 42 | describe('.tracking', () => { 43 | it('should return true when something is being tracked, false otherwise.', () => { 44 | class T extends Tracker { 45 | constructor() { 46 | super(); 47 | this.tracking.should.be.false; 48 | this.track(new Subscription()); 49 | this.tracking.should.be.true; 50 | } 51 | } 52 | 53 | new T(); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/shared/tracker.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | import { Clearable } from './clearable'; 4 | 5 | 6 | /** 7 | * 8 | * A parent class for sub-classes who would want to track 9 | * some [`Subscription`s](https://rxjs-dev.firebaseapp.com/guide/subscription) 10 | * and clear them later. 11 | * 12 | */ 13 | export class Tracker implements Clearable { 14 | _sub: Subscription | undefined; 15 | 16 | /** 17 | * 18 | * Tracks given subscription, to clear it up later when 19 | * `.clear()` is called. 20 | * 21 | * @param sub 22 | * @returns the given subscription (for convenience). 23 | * 24 | */ 25 | protected track(sub: Subscription): Subscription { 26 | if (!this._sub) { 27 | this._sub = new Subscription(); 28 | } 29 | 30 | this._sub.add(sub); 31 | return sub; 32 | } 33 | 34 | /** 35 | * 36 | * Untracks given subscription, removing it from subscriptions 37 | * it will clear up when `.clear()` is called. This is useful when you 38 | * clear up some subscriptions yourself before clearing the tracker object. 39 | * 40 | * @param sub 41 | * 42 | */ 43 | protected untrack(sub: Subscription): this { 44 | if (this._sub) this._sub.remove(sub); 45 | return this; 46 | } 47 | 48 | /** 49 | * 50 | * @returns `true` if this tracker object was ever tracking anything. 51 | * @returns `true` even after you `.untrack()` everything. 52 | * @returns `false` after invoking `.clear()`. 53 | * 54 | */ 55 | protected get tracking(): boolean { return !!this._sub; } 56 | 57 | /** 58 | * 59 | * Clears out all tracked subscriptions by unsibscribing them. 60 | * Also clears out all references to tracked subscriptions. 61 | * 62 | * @warning most tracker objects will become useless after calling `.clear()` on them, 63 | * so do not call this prematurely! 64 | * 65 | */ 66 | public clear(): this { 67 | if (this._sub) { 68 | this._sub.unsubscribe(); 69 | this._sub = undefined; 70 | } 71 | 72 | return this; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from "rxjs"; 2 | 3 | export type ErrorCallback = (error: Error | string) => void; 4 | export type ResolveCallback = (value: T) => void; 5 | export type NotifyCallback = () => void; 6 | export type ContextType = {[keys: string]: any}; 7 | export type TrackCallback = (sub: Subscription) => void; -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | describe('connective', () => { 2 | require('../shared/test'); 3 | require('../pin/test'); 4 | require('../agent/test'); 5 | }); 6 | -------------------------------------------------------------------------------- /src/util/keyed-array-diff.ts: -------------------------------------------------------------------------------- 1 | export type KeyFunc = (obj: any) => string | number; 2 | export type KeyMap = {[key: string]: {item: any, index: string}}; 3 | 4 | export type AdditionList = { 5 | /** 6 | * 7 | * The index that the item was added on. 8 | * 9 | */ 10 | index: string, 11 | 12 | /** 13 | * 14 | * The added item 15 | * 16 | */ 17 | item: any 18 | }[]; 19 | 20 | export type DeletionList = { 21 | /** 22 | * 23 | * The index the deleted item used to be on 24 | * 25 | */ 26 | index: string, 27 | 28 | /** 29 | * 30 | * The deleted item 31 | * 32 | */ 33 | item: any 34 | }[]; 35 | 36 | export type MoveList = { 37 | /** 38 | * 39 | * The index the item used to be on 40 | * 41 | */ 42 | oldIndex: string, 43 | /** 44 | * 45 | * The new index of the item 46 | * 47 | */ 48 | newIndex: string, 49 | 50 | /** 51 | * 52 | * The moved item 53 | * 54 | */ 55 | item: any 56 | }[]; 57 | 58 | export type ChangeMap = { 59 | /** 60 | * 61 | * List of items added to the list 62 | * 63 | */ 64 | additions: AdditionList, 65 | 66 | /** 67 | * 68 | * List of items removed from the list 69 | * 70 | */ 71 | deletions: DeletionList, 72 | 73 | /** 74 | * 75 | * List of items moved around in the list 76 | * 77 | */ 78 | moves: MoveList 79 | }; 80 | 81 | 82 | export function diff(value: any, oldKeyMap: KeyMap, keyfunc: KeyFunc): { 83 | changes: ChangeMap, 84 | newKeyMap: KeyMap 85 | } { 86 | const additions: AdditionList = []; 87 | const deletions: DeletionList = []; 88 | const moves: MoveList = []; 89 | 90 | const newKeyMap = Object.entries(value).reduce((map, [index, item]) => { 91 | const _key = keyfunc(item); 92 | map[_key] = { index, item }; 93 | if (!(_key in oldKeyMap)) 94 | additions.push({ index, item }); 95 | return map; 96 | }, {}); 97 | 98 | Object.entries(oldKeyMap).forEach(([_key, entry]) => { 99 | if (!(_key in newKeyMap)) deletions.push(entry); 100 | else { 101 | const _newEntry = newKeyMap[_key]; 102 | if (_newEntry.index != entry.index) 103 | moves.push({ 104 | oldIndex: entry.index, 105 | newIndex: _newEntry.index, 106 | item: entry.item 107 | }); 108 | } 109 | }); 110 | 111 | return { 112 | changes: { additions, deletions, moves }, 113 | newKeyMap, 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /src/util/random-tag.ts: -------------------------------------------------------------------------------- 1 | export const _DefaultRandomTagCharset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'; 2 | export const _DefaultRandomTagLength = 10; 3 | 4 | 5 | export function createRandomTag(len = _DefaultRandomTagLength, charset = _DefaultRandomTagCharset) { 6 | let res = ''; 7 | for (let i = 0; i < len; i++) 8 | res += charset[Math.floor(Math.random() * charset.length)]; 9 | return res; 10 | } 11 | 12 | 13 | export default createRandomTag; -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | const Mocha = require('mocha'); 2 | import * as path from 'path'; 3 | 4 | const mocha = new Mocha(); 5 | const root = path.join(__dirname, 'src/'); 6 | 7 | const test = (file: string) => mocha.addFile(path.join(root, file)); 8 | 9 | test('test/index.ts'); 10 | 11 | mocha.run(console.log); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./conf/typescript/base", 3 | "compilerOptions": { 4 | "target": "es5" 5 | } 6 | } 7 | --------------------------------------------------------------------------------