├── .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 sun moon ,
10 | then head back home as we've got more tales of awesome
11 | for your weary ears.
12 |
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 |
Prev
3 |
4 |
Next
5 |
6 |
--------------------------------------------------------------------------------
/docs/templates/chunks/main/_contact.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 | For bugs, issues and suggestions, the best way is to create issues or pull requests to
4 | the repository .
5 |
6 |
7 | For questions, feedback, etc. join the conversation on
8 | Gitter .
9 |
10 |
11 | You can also drop me an email anytime.
12 |
13 |
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 | pin s 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 |
--------------------------------------------------------------------------------