├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── rollup.config.umd.js ├── src ├── lib │ ├── Lib.ts │ └── sodium │ │ ├── Cell.ts │ │ ├── CellLoop.ts │ │ ├── CellSink.ts │ │ ├── CoalesceHandler.ts │ │ ├── IOAction.ts │ │ ├── Lambda.ts │ │ ├── Lazy.ts │ │ ├── LazyCell.ts │ │ ├── Listener.ts │ │ ├── MillisecondsTimerSystem.ts │ │ ├── Operational.ts │ │ ├── Router.ts │ │ ├── SecondsTimerSystem.ts │ │ ├── Stream.ts │ │ ├── StreamSink.ts │ │ ├── TimerSystem.ts │ │ ├── Transaction.ts │ │ ├── Tuple2.ts │ │ ├── Unit.ts │ │ └── Vertex.ts └── tests │ ├── integration │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── App-Main.ts │ │ │ └── output │ │ │ │ └── DomWriter.ts │ │ └── webpage │ │ │ ├── index.html │ │ │ └── static │ │ │ └── styles.css │ ├── tsconfig.json │ ├── wallaby.conf.js │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js │ ├── test-utils │ ├── Sanctuary.ts │ └── Sequence.ts │ └── unit │ ├── AccumCell.spec.ts │ ├── Cell.spec.ts │ ├── CellLoop.spec.ts │ ├── CellSink.spec.ts │ ├── DoubleSnapshot.spec.ts │ ├── FantasyLand.spec.ts │ ├── IOAction.spec.ts │ ├── InnerLoop.spec.ts │ ├── MultipleLoop.spec.ts │ ├── NestedLift.spec.ts │ ├── Rank.spec.ts │ ├── Router.spec.ts │ ├── StreamSink.spec.ts │ └── Timer.spec.ts ├── tsconfig.json └── wallaby.conf.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Build artifacts 7 | dist 8 | 9 | # Rollup cache 10 | .rpt2_cache 11 | 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules 39 | jspm_packages 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # custom 48 | *~ 49 | .vscode 50 | .idea 51 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/test 2 | dist/test 3 | tsconfig.json 4 | webpack.config.js 5 | examples 6 | .rpt2_cache 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | 5 | install: 6 | - npm install 7 | script: 8 | - npm run test 9 | after_success: 10 | - npm run build 11 | 12 | deploy: 13 | provider: npm 14 | email: 15 | secure: inOaTqSMh3j8gaONOB9sKgMGbZ4JwJU/rRL5FlM/F4cg3vzG1yI/l1QrrxZZ/mJGFt2tPxhQwhZkP22qpeYH4ukoyp3ec0criloc2ON59x6Q63tlJx3LGWNel0mx+Aj7roghCz6Bw2Uar6XhyKqDim8m0p3TnBOwUGs32ikhYbGLiWveR0B6U9LYWsnT8r3qez+JpQkLfMxxMNyuYyAdrY/GKJK1jYiMA4z6ubh7twdmdJH2GBW7skPbcdcmEeCwzSHSFdKkJJXaHM4KopB363s80yjmWU+FBS7oeQXYqYggna3TNdeyGhUrLOmT0zwAWno2aca2me2de7T6XzA7X8NCB8CpyWeYB51MhshG6EspRS/BSuYJqZ90BrSuYNW3nbTCkutyZYYUCgNwNz/ng1EtRprllBykg+nvDtJQoyejc5faYnihb2kqFE27p26lezF2525iZTdKNcb5V9wsPaQDTMgKdvbk9+BEPwoUZdyDeWfJ6edf+mcxd+9KkK9RYg0FLH6PSYgRWz6f1apDObeZfqAL+5cwD186L/YK6q+cNuDxrnnkDCkV+IIpL8YeCahV9PAw4vIifir/6CeBxfYrf+eU8eJFR9XrxRHBohDmg7m9I1CqdScwGtTcmH1u5bCzrj0qlaKSb4ZnLoBRMlTtrEw5KCJ2KdBfKPR7GDA= 16 | api_key: 17 | secure: sQLLjy457hzDEbM8fOweUlGEXwjXUvKHwz6bDtOcEG4Z/KtM3f3ULTLkieeeugZPzWBWdw7oyBceOPM6z1spUYA8C47UT9uESVjd8vDZ+s2M4xHDPQDHpm+FqjHMIBNMJvSpD6HMSSYQXjOFm8vwoZ6p4dIPEH5fYflk7G4HocV6l5MTQ6q/EYMR0cISLwuTIiQlhpWXa2jc1jq3zg7pc/cUlXhsNyaIr10BmiWF81CKrj4EsULmNHRlYVTAnIBIxEM4kFfKjWHfO4enIaPBpkEhvdFm33TanSkC8aG0wFYlWYqnaR34+iJqsB7IH9POYwHUbwBi+33Fz2YN/OjCWPQkglGxLfn9lLQrXI9WxTTxiMBymnRpF+HobkWsJlxD1wF01AIdlBGuTRWv0cOl/JGo22toWA5qkN2Ur9VPAU2Tj5osl3+RM+x9i4yDFX5zHrkLjsolhRMyc+CG+2o26Q+jPc2Lh9kIWHMnyKNvn86OrcETYd67wDz5cDbSmsGtpXQCy2TetP1mU1ypGFJQ20h9JmWkD/K6GC6YHLfX3+oX0VfHtYwlznLLWsoajFFVpNhS14qmA9ChZsaYBF32N0GtHmddlVpv7Kio2U1zcG4cIn2lflrtwQ68CmL/T4M6b9yPDRednucf1aEIzwF+EODsKRK98tkleXBkqqbe50o= 18 | skip_cleanup: true 19 | tag: latest 20 | on: 21 | tags: true 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/SodiumFRP/sodium-typescript.svg?branch=master)](https://travis-ci.org/SodiumFRP/sodium-typescript) 2 | [![npm version](https://badge.fury.io/js/sodiumjs.svg)](https://badge.fury.io/js/sodiumjs) 3 | [![Bower version](https://badge.fury.io/bo/sodiumjs.svg)](https://badge.fury.io/bo/sodiumjs) 4 | [![Downloads](http://img.shields.io/npm/dm/sodiumjs.svg)](https://npmjs.org/package/sodiumjs) 5 | 6 | # Sodium 7 | 8 | A Functional Reactive Programming (FRP) library for TypeScript/JavaScript 9 | 10 | ## Examples 11 | 12 | Here are some demos from the community you can try in your browser: 13 | 14 | * [Book Examples](https://github.com/graforlock/sodium-typescript-examples) 15 | * [Petrol Pump](https://github.com/huanhulan/petrol_pump/) 16 | * [Reactive Drawing Pad](https://github.com/graforlock/reactive-drawing-pad/tree/master) 17 | * [Misc Playground (drum machine, animation, etc.)](https://github.com/dakom/sodium-typescript-playground) 18 | 19 | ## Prerequisite: Node.js 20 | 21 | Install [Node.js® and npm](https://nodejs.org/en/download/current/) if they are not already on your machine. 22 | 23 | ## Installation 24 | 25 | ### via NPM 26 | ```bash 27 | $> npm install sodiumjs 28 | $> npm install -g sodiumjs 29 | ``` 30 | 31 | ### via Yarn 32 | ```bash 33 | $> yarn add sodiumjs 34 | $> yarn global add sodiumjs 35 | ``` 36 | 37 | ### via html include 38 | ``` 39 | 40 | ``` 41 | 42 | _this requires also including [sanctuary-type-classes](https://github.com/sanctuary-js/sanctuary-type-classes) and [typescript-collections](https://github.com/basarat/typescript-collections) dependencies_ 43 | 44 | ## How to use 45 | 46 | ### Import 47 | ```javascript 48 | import { Cell } from 'sodiumjs'; 49 | ``` 50 | ### ES6 51 | 52 | ```javascript 53 | const c = new Cell(12); 54 | ``` 55 | 56 | ### TypeScript 57 | ```javascript 58 | const c = new Cell(12); 59 | ``` 60 | 61 | ### In a browser 62 | 63 | ```html 64 | 67 | ``` 68 | 69 | ## Development 70 | 71 | The usual `npm run build/test/clean` commands are available to produce the distribution package. 72 | 73 | However, a more comfortable iteration style may be using the the live integration testing approach: 74 | 75 | 1. cd `src/tests/integration` 76 | 2. `npm run dev:auto-reload` (or just `npm run dev` without live reloading) 77 | 78 | This starts up a local development server and showcases integration with a [webpack](https://webpack.github.io/) app. 79 | 80 | Changes to the core lib are then seen live since it uses a local alias rather than reference the lastest build or distribution of the library 81 | 82 | Sodium library code is in [src/lib](src/lib) 83 | 84 | Packaging/tree-shaking and bundling of the library is done with [Rollup](https://rollupjs.org/) 85 | 86 | Testing is via [Jest](https://facebook.github.io/jest/) 87 | 88 | ## License 89 | 90 | Distributed under [BSD 3-Clause](https://opensource.org/licenses/BSD-3-Clause) 91 | 92 | ## Announcement 93 | 94 | Stephen Blackheath, 9 Jul 2016 95 | 96 | I am very happy to announce that the Typescript implementation of Sodium is ready! 97 | 98 | It features a newly developed scheme for memory management, which was needed 99 | because Javascript has no finalizers. Memory management in Sodium is 100% 100 | automatic. 101 | 102 | This scheme imposes one small requirement on the API: You must declare any Sodium 103 | objects held inside closures using wrapper functions lambda1, lambda2 .. 104 | lambda6 - depending on the number of arguments that the closure has. 105 | These take a second argument, which is a list of the Sodium objects contained 106 | in the context of the closure. For example: 107 | 108 | csw = csw_str.map(lambda1(sw => sw == "sa" ? sa : sb, [sa, sb])), 109 | 110 | This allows Sodium to track all dependencies. There are some limitations to this 111 | scheme - for example, it can't track dependencies if you poke arbitrary Sodium 112 | objects into a StreamSink, but I think these should not affect any normal usages. 113 | Time will tell. 114 | 115 | CHANGELOG 116 | 117 | 1.0.5 Migrate build environment over to fuse-box. 118 | Begin adding fantasy-land compatibility 119 | 120 | 1.0.0 Add snapshot3(), snapshot4(), snapshot5() and snapshot6(). 121 | Fix a serious bug in TimerSystem where timers sometimes don't fire. 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sodiumjs", 3 | "version": "3.0.7", 4 | "description": "A Functional Reactive Programming (FRP) library for JavaScript", 5 | "author": "Stephen Blackheath", 6 | "license": "BSD-3-Clause", 7 | "homepage": "https://github.com/SodiumFRP/sodium-typescript", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/SodiumFRP/sodium-typescript.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/SodiumFRP/sodium-typescript/issues" 14 | }, 15 | "keywords": [ 16 | "frp", 17 | "functional", 18 | "reactive", 19 | "typescript", 20 | "sodium" 21 | ], 22 | "main": "dist/sodium.cjs.js", 23 | "module": "dist/sodium.esm.js", 24 | "typings": "./dist/typings/Lib.d.ts", 25 | "scripts": { 26 | "install:example": "cd example && npm install", 27 | "clean:all": "npm-run-all clean clean:example", 28 | "build:all": "npm-run-all build build:example", 29 | "clean": "rimraf ./dist", 30 | "build": "npm-run-all clean rollup:build:umd rollup:build typings:emit", 31 | "typings:emit": "tsc --emitDeclarationOnly true", 32 | "build:example": "npm-run-all clean:example _build:example", 33 | "_build:example": "cd example && npm run build", 34 | "clean:example": "cd example && npm run clean", 35 | "dev": "npm-run-all -s clean -p rollup:watch", 36 | "rollup:build": "cross-env NODE_ENV=production rollup -c", 37 | "rollup:build:umd": "cross-env NODE_ENV=production rollup -c rollup.config.umd.js", 38 | "rollup:watch": "cross-env NODE_ENV=production rollup -c -w", 39 | "test": "jest", 40 | "prepublishOnly": "npm run build" 41 | }, 42 | "jest": { 43 | "transform": { 44 | "^.+\\.tsx?$": "ts-jest" 45 | }, 46 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 47 | "moduleFileExtensions": [ 48 | "ts", 49 | "tsx", 50 | "js", 51 | "jsx", 52 | "json" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@types/jest": "23.3.1", 57 | "@types/node": "^10.9.4", 58 | "cross-env": "^5.2.0", 59 | "fantasy-laws": "^1.1.0", 60 | "jest": "23.5.0", 61 | "jsverify": "^0.8.3", 62 | "live-server": "1.2.0", 63 | "minify": "3.0.5", 64 | "npm-run-all": "4.1.3", 65 | "rimraf": "^2.6.2", 66 | "rollup": "0.65.0", 67 | "rollup-plugin-replace": "2.0.0", 68 | "rollup-plugin-typescript2": "0.17.0", 69 | "rollup-plugin-uglify": "5.0.2", 70 | "sanctuary": "0.15.0", 71 | "ts-jest": "23.1.4", 72 | "ts-node": "7.0.1", 73 | "tslib": "1.9.3", 74 | "typescript": "^3.0.3", 75 | "uglify-es": "3.3.10" 76 | }, 77 | "dependencies": { 78 | "typescript-collections": "^1.3.2", 79 | "sanctuary-type-classes": "^9.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import pkg from './package.json'; 3 | import replace from 'rollup-plugin-replace'; 4 | //import uglify from 'rollup-plugin-uglify'; 5 | //import { minify } from 'uglify-es'; 6 | 7 | export default [ 8 | { 9 | input: './src/lib/Lib.ts', 10 | external: [ 'typescript-collections', 'sanctuary-type-classes'], 11 | output: [ 12 | { file: pkg.module, format: 'es', sourcemap: true }, 13 | { file: pkg.main, format: 'cjs', sourcemap: true }, 14 | ], 15 | plugins: [ 16 | replace({ 17 | 'process.env.NODE_ENV': JSON.stringify( process.env['NODE_ENV'] ) 18 | }), 19 | 20 | typescript({ 21 | tsconfigOverride: { 22 | compilerOptions: { 23 | declaration: false //will be run as a separate step via tsc which is more thorough 24 | } 25 | }, 26 | useTsconfigDeclarationDir: true, 27 | }) 28 | 29 | //uglify({}, minify) 30 | ] 31 | } 32 | ]; 33 | -------------------------------------------------------------------------------- /rollup.config.umd.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import replace from 'rollup-plugin-replace'; 3 | import {uglify} from 'rollup-plugin-uglify'; 4 | import { minify } from 'uglify-es'; 5 | 6 | export default [ 7 | { 8 | input: './src/lib/Lib.ts', 9 | external: [ 'typescript-collections', 'sanctuary-type-classes'], 10 | output: [ 11 | { file: "dist/sodium.umd.min.js", name: "Sodium", format: 'umd', sourcemap: true, 12 | globals: { 13 | 'typescript-collections': 'Collections', 14 | 'sanctuary-type-classes': 'Z' 15 | } 16 | }, 17 | ], 18 | plugins: [ 19 | replace({ 20 | 'process.env.NODE_ENV': JSON.stringify( process.env['NODE_ENV'] ) 21 | }), 22 | 23 | typescript({ 24 | tsconfigOverride: { 25 | compilerOptions: { 26 | declaration: false //will be run as a separate step via tsc which is more thorough 27 | } 28 | }, 29 | useTsconfigDeclarationDir: true, 30 | }), 31 | 32 | uglify({}, minify) 33 | ] 34 | } 35 | ]; 36 | -------------------------------------------------------------------------------- /src/lib/Lib.ts: -------------------------------------------------------------------------------- 1 | export { lambda1, lambda2, lambda3, lambda4, lambda5, lambda6 } from "./sodium/Lambda"; 2 | export { Stream, StreamLoop } from "./sodium/Stream"; 3 | export { StreamSink } from "./sodium/StreamSink"; 4 | export { Cell } from "./sodium/Cell"; 5 | export { CellLoop } from "./sodium/CellLoop"; 6 | export { CellSink } from "./sodium/CellSink"; 7 | export { Router } from "./sodium/Router"; 8 | export { Transaction } from "./sodium/Transaction"; 9 | export { Tuple2 } from "./sodium/Tuple2"; 10 | export { Unit } from "./sodium/Unit"; 11 | export { Operational } from "./sodium/Operational"; 12 | export { getTotalRegistrations, Vertex } from "./sodium/Vertex"; 13 | export { TimerSystemImpl, TimerSystem } from "./sodium/TimerSystem"; 14 | export { SecondsTimerSystem } from "./sodium/SecondsTimerSystem"; 15 | export { MillisecondsTimerSystem } from "./sodium/MillisecondsTimerSystem"; 16 | export { IOAction } from "./sodium/IOAction"; 17 | -------------------------------------------------------------------------------- /src/lib/sodium/Cell.ts: -------------------------------------------------------------------------------- 1 | import { Lambda1, Lambda1_deps, Lambda1_toFunction, 2 | Lambda2, Lambda2_deps, Lambda2_toFunction, 3 | Lambda3, Lambda3_deps, Lambda3_toFunction, 4 | Lambda4, Lambda4_deps, Lambda4_toFunction, 5 | Lambda5, Lambda5_deps, Lambda5_toFunction, 6 | Lambda6, Lambda6_deps, Lambda6_toFunction, 7 | toSources, lambda1 } from "./Lambda"; 8 | import { Source, Vertex } from "./Vertex"; 9 | import { Transaction } from "./Transaction"; 10 | import { Lazy } from "./Lazy"; 11 | import { Listener } from "./Listener"; 12 | import { Stream, StreamWithSend } from "./Stream"; 13 | import { Operational } from "./Operational"; 14 | import { Tuple2 } from "./Tuple2"; 15 | 16 | class LazySample { 17 | constructor(cell : Cell) { 18 | this.cell = cell; 19 | } 20 | cell : Cell; 21 | hasValue : boolean = false; 22 | value : A = null; 23 | } 24 | 25 | class ApplyState { 26 | constructor() {} 27 | f : (a : A) => B = null; 28 | f_present : boolean = false; 29 | a : A = null; 30 | a_present : boolean = false; 31 | } 32 | 33 | export class Cell { 34 | private str : Stream; 35 | protected value : A; 36 | protected valueUpdate : A; 37 | private cleanup : () => void; 38 | protected lazyInitValue : Lazy; // Used by LazyCell 39 | private vertex : Vertex; 40 | 41 | constructor(initValue : A, str? : Stream) { 42 | this.value = initValue; 43 | if (!str) { 44 | this.str = new Stream(); 45 | this.vertex = new Vertex("ConstCell", 0, []); 46 | } 47 | else 48 | Transaction.run(() => this.setStream(str)); 49 | } 50 | 51 | protected setStream(str : Stream) { 52 | this.str = str; 53 | const me = this, 54 | src = new Source( 55 | str.getVertex__(), 56 | () => { 57 | return str.listen_(me.vertex, (a : A) => { 58 | if (me.valueUpdate == null) { 59 | Transaction.currentTransaction.last(() => { 60 | me.value = me.valueUpdate; 61 | me.lazyInitValue = null; 62 | me.valueUpdate = null; 63 | }); 64 | } 65 | me.valueUpdate = a; 66 | }, false); 67 | } 68 | ); 69 | this.vertex = new Vertex("Cell", 0, [src]); 70 | // We do a trick here of registering the source for the duration of the current 71 | // transaction so that we are guaranteed to catch any stream events that 72 | // occur in the same transaction. 73 | // 74 | // A new temporary vertex null is constructed here as a performance work-around to avoid 75 | // having too many children in Vertex.NULL as a deregister operation is O(n^2) where 76 | // n is the number of children in the vertex. 77 | let tmpVertexNULL = new Vertex("Cell::setStream", 1e12, []); 78 | this.vertex.register(tmpVertexNULL); 79 | Transaction.currentTransaction.last(() => { 80 | this.vertex.deregister(tmpVertexNULL); 81 | }); 82 | } 83 | 84 | getVertex__() : Vertex { 85 | return this.vertex; 86 | } 87 | 88 | getStream__() : Stream { // TO DO: Figure out how to hide this 89 | return this.str; 90 | } 91 | 92 | /** 93 | * Sample the cell's current value. 94 | *

95 | * It should generally be avoided in favour of {@link listen(Handler)} so you don't 96 | * miss any updates, but in many circumstances it makes sense. 97 | *

98 | * NOTE: In the Java and other versions of Sodium, using sample() inside map(), filter() and 99 | * merge() is encouraged. In the Javascript/Typescript version, not so much, for the 100 | * following reason: The memory management is different in the Javascript version, and this 101 | * requires us to track all dependencies. In order for the use of sample() inside 102 | * a closure to be correct, the cell that was sample()d inside the closure would have to be 103 | * declared explicitly using the helpers lambda1(), lambda2(), etc. Because this is 104 | * something that can be got wrong, we don't encourage this kind of use of sample() in 105 | * Javascript. Better and simpler to use snapshot(). 106 | *

107 | * NOTE: If you need to sample() a cell, you have to make sure it's "alive" in terms of 108 | * memory management or it will ignore updates. To make a cell work correctly 109 | * with sample(), you have to ensure that it's being used. One way to guarantee this is 110 | * to register a dummy listener on the cell. It will also work to have it referenced 111 | * by something that is ultimately being listened to. 112 | */ 113 | sample() : A { 114 | return Transaction.run(() => { return this.sampleNoTrans__(); }); 115 | } 116 | 117 | sampleNoTrans__() : A { // TO DO figure out how to hide this 118 | return this.value; 119 | } 120 | 121 | /** 122 | * A variant of {@link sample()} that works with {@link CellLoop}s when they haven't been looped yet. 123 | * It should be used in any code that's general enough that it could be passed a {@link CellLoop}. 124 | * @see Stream#holdLazy(Lazy) Stream.holdLazy() 125 | */ 126 | sampleLazy() : Lazy { 127 | const me = this; 128 | return Transaction.run(() => me.sampleLazyNoTrans__()); 129 | } 130 | 131 | sampleLazyNoTrans__() : Lazy { // TO DO figure out how to hide this 132 | const me = this, 133 | s = new LazySample(me); 134 | Transaction.currentTransaction.sample(() => { 135 | s.value = me.valueUpdate != null ? me.valueUpdate : me.sampleNoTrans__(); 136 | s.hasValue = true; 137 | s.cell = null; 138 | }); 139 | return new Lazy(() => { 140 | if (s.hasValue) 141 | return s.value; 142 | else 143 | return s.cell.sample(); 144 | }); 145 | } 146 | 147 | /** 148 | * Transform the cell's value according to the supplied function, so the returned Cell 149 | * always reflects the value of the function applied to the input Cell's value. 150 | * @param f Function to apply to convert the values. It must be referentially transparent. 151 | */ 152 | map(f : ((a : A) => B) | Lambda1) : Cell { 153 | const c = this; 154 | return Transaction.run(() => 155 | Operational.updates(c).map(f).holdLazy(c.sampleLazy().map(Lambda1_toFunction(f))) 156 | ); 157 | } 158 | 159 | /** 160 | * Lift a binary function into cells, so the returned Cell always reflects the specified 161 | * function applied to the input cells' values. 162 | * @param fn Function to apply. It must be referentially transparent. 163 | */ 164 | lift(b : Cell, 165 | fn0 : ((a : A, b : B) => C) | 166 | Lambda2) : Cell { 167 | const fn = Lambda2_toFunction(fn0), 168 | cf = this.map((aa : A) => (bb : B) => fn(aa, bb)); 169 | return Cell.apply(cf, b, 170 | toSources(Lambda2_deps(fn0))); 171 | } 172 | 173 | /** 174 | * Lift a ternary function into cells, so the returned Cell always reflects the specified 175 | * function applied to the input cells' values. 176 | * @param fn Function to apply. It must be referentially transparent. 177 | */ 178 | lift3(b : Cell, c : Cell, 179 | fn0 : ((a : A, b : B, c : C) => D) | 180 | Lambda3) : Cell { 181 | const fn = Lambda3_toFunction(fn0), 182 | mf : (aa : A) => (bb : B) => (cc : C) => D = 183 | (aa : A) => (bb : B) => (cc : C) => fn(aa, bb, cc), 184 | cf = this.map(mf); 185 | return Cell.apply( 186 | Cell.apply D>(cf, b), 187 | c, 188 | toSources(Lambda3_deps(fn0))); 189 | } 190 | 191 | /** 192 | * Lift a quaternary function into cells, so the returned Cell always reflects the specified 193 | * function applied to the input cells' values. 194 | * @param fn Function to apply. It must be referentially transparent. 195 | */ 196 | lift4(b : Cell, c : Cell, d : Cell, 197 | fn0 : ((a : A, b : B, c : C, d : D) => E) | 198 | Lambda4) : Cell { 199 | const fn = Lambda4_toFunction(fn0), 200 | mf : (aa : A) => (bb : B) => (cc : C) => (dd : D) => E = 201 | (aa : A) => (bb : B) => (cc : C) => (dd : D) => fn(aa, bb, cc, dd), 202 | cf = this.map(mf); 203 | return Cell.apply( 204 | Cell.apply( 205 | Cell.apply (d : D) => E>(cf, b), 206 | c), 207 | d, 208 | toSources(Lambda4_deps(fn0))); 209 | } 210 | 211 | /** 212 | * Lift a 5-argument function into cells, so the returned Cell always reflects the specified 213 | * function applied to the input cells' values. 214 | * @param fn Function to apply. It must be referentially transparent. 215 | */ 216 | lift5(b : Cell, c : Cell, d : Cell, e : Cell, 217 | fn0 : ((a : A, b : B, c : C, d : D, e : E) => F) | 218 | Lambda5) : Cell { 219 | const fn = Lambda5_toFunction(fn0), 220 | mf : (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => F = 221 | (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => fn(aa, bb, cc, dd, ee), 222 | cf = this.map(mf); 223 | return Cell.apply( 224 | Cell.apply( 225 | Cell.apply( 226 | Cell.apply (d : D) => (e : E) => F>(cf, b), 227 | c), 228 | d), 229 | e, 230 | toSources(Lambda5_deps(fn0))); 231 | } 232 | 233 | /** 234 | * Lift a 6-argument function into cells, so the returned Cell always reflects the specified 235 | * function applied to the input cells' values. 236 | * @param fn Function to apply. It must be referentially transparent. 237 | */ 238 | lift6(b : Cell, c : Cell, d : Cell, e : Cell, f : Cell, 239 | fn0 : ((a : A, b : B, c : C, d : D, e : E, f : F) => G) | 240 | Lambda6) : Cell { 241 | const fn = Lambda6_toFunction(fn0), 242 | mf : (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => (ff : F) => G = 243 | (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => (ff : F) => fn(aa, bb, cc, dd, ee, ff), 244 | cf = this.map(mf); 245 | return Cell.apply( 246 | Cell.apply( 247 | Cell.apply( 248 | Cell.apply( 249 | Cell.apply (d : D) => (e : E) => (f : F) => G>(cf, b), 250 | c), 251 | d), 252 | e), 253 | f, 254 | toSources(Lambda6_deps(fn0))); 255 | } 256 | 257 | /** 258 | * High order depenency traking. If any newly created sodium objects within a value of a cell of a sodium object 259 | * happen to accumulate state, this method will keep the accumulation of state up to date. 260 | */ 261 | public tracking(extractor: (a: A) => (Stream|Cell)[]) : Cell { 262 | const out = new StreamWithSend(null); 263 | let vertex = new Vertex("tracking", 0, [ 264 | new Source( 265 | this.vertex, 266 | () => { 267 | let cleanup2: ()=>void = () => {}; 268 | let updateDeps = 269 | (a: A) => { 270 | let lastCleanups2 = cleanup2; 271 | let deps = extractor(a).map(dep => dep.getVertex__()); 272 | for (let i = 0; i < deps.length; ++i) { 273 | let dep = deps[i]; 274 | vertex.childrn.push(dep); 275 | dep.increment(Vertex.NULL); 276 | } 277 | cleanup2 = () => { 278 | for (let i = 0; i < deps.length; ++i) { 279 | let dep = deps[i]; 280 | for (let j = 0; j < vertex.childrn.length; ++j) { 281 | if (vertex.childrn[j] === dep) { 282 | vertex.childrn.splice(j, 1); 283 | break; 284 | } 285 | } 286 | dep.decrement(Vertex.NULL); 287 | } 288 | }; 289 | lastCleanups2(); 290 | }; 291 | updateDeps(this.sample()); 292 | var cleanup1 = 293 | Operational.updates(this).listen_( 294 | vertex, 295 | (a: A) => { 296 | updateDeps(a); 297 | out.send_(a); 298 | }, 299 | false 300 | ); 301 | return () => { 302 | cleanup1(); 303 | cleanup2(); 304 | } 305 | } 306 | ) 307 | ]); 308 | out.setVertex__(vertex); 309 | return out.holdLazy(this.sampleLazy()); 310 | } 311 | 312 | /** 313 | * Lift an array of cells into a cell of an array. 314 | */ 315 | public static liftArray(ca : Cell[]) : Cell { 316 | return Cell._liftArray(ca, 0, ca.length); 317 | } 318 | 319 | private static _liftArray(ca : Cell[], fromInc: number, toExc: number) : Cell { 320 | if (toExc - fromInc == 0) { 321 | return new Cell([]); 322 | } else if (toExc - fromInc == 1) { 323 | return ca[fromInc].map(a => [a]); 324 | } else { 325 | let pivot = Math.floor((fromInc + toExc) / 2); 326 | // the thunk boxing/unboxing here is a performance hack for lift when there are simutaneous changing cells. 327 | return Cell._liftArray(ca, fromInc, pivot).lift( 328 | Cell._liftArray(ca, pivot, toExc), 329 | (array1, array2) => () => array1.concat(array2) 330 | ) 331 | .map(x => x()); 332 | } 333 | } 334 | 335 | /** 336 | * Apply a value inside a cell to a function inside a cell. This is the 337 | * primitive for all function lifting. 338 | */ 339 | static apply(cf : Cell<(a : A) => B>, ca : Cell, sources? : Source[]) : Cell { 340 | return Transaction.run(() => { 341 | let pumping = false; 342 | const state = new ApplyState(), 343 | out = new StreamWithSend(), 344 | cf_updates = Operational.updates(cf), 345 | ca_updates = Operational.updates(ca), 346 | pump = () => { 347 | if (pumping) { 348 | return; 349 | } 350 | pumping = true; 351 | Transaction.currentTransaction.prioritized(out.getVertex__(), () => { 352 | let f = state.f_present ? state.f : cf.sampleNoTrans__(); 353 | let a = state.a_present ? state.a : ca.sampleNoTrans__(); 354 | out.send_(f(a)); 355 | pumping = false; 356 | }); 357 | }, 358 | src1 = new Source( 359 | cf_updates.getVertex__(), 360 | () => { 361 | return cf_updates.listen_(out.getVertex__(), (f : (a : A) => B) => { 362 | state.f = f; 363 | state.f_present = true; 364 | pump(); 365 | }, false); 366 | } 367 | ), 368 | src2 = new Source( 369 | ca_updates.getVertex__(), 370 | () => { 371 | return ca_updates.listen_(out.getVertex__(), (a : A) => { 372 | state.a = a; 373 | state.a_present = true; 374 | pump(); 375 | }, false); 376 | } 377 | ); 378 | out.setVertex__(new Vertex("apply", 0, 379 | [src1, src2].concat(sources ? sources : []) 380 | )); 381 | return out.holdLazy(new Lazy(() => 382 | cf.sampleNoTrans__()(ca.sampleNoTrans__()) 383 | )); 384 | }); 385 | } 386 | 387 | /** 388 | * Unwrap a cell inside another cell to give a time-varying cell implementation. 389 | */ 390 | static switchC(cca : Cell>) : Cell { 391 | return Transaction.run(() => { 392 | const za = cca.sampleLazy().map((ba : Cell) => ba.sample()), 393 | out = new StreamWithSend(); 394 | let outValue: A = null; 395 | let pumping = false; 396 | const pump = () => { 397 | if (pumping) { 398 | return; 399 | } 400 | pumping = true; 401 | Transaction.currentTransaction.prioritized(out.getVertex__(), () => { 402 | out.send_(outValue); 403 | outValue = null; 404 | pumping = false; 405 | }); 406 | }; 407 | let last_ca : Cell = null; 408 | const cca_value = Operational.value(cca), 409 | src = new Source( 410 | cca_value.getVertex__(), 411 | () => { 412 | let kill2 : () => void = last_ca === null ? null : 413 | Operational.value(last_ca).listen_(out.getVertex__(), 414 | (a : A) => { outValue = a; pump(); }, false); 415 | const kill1 = cca_value.listen_(out.getVertex__(), (ca : Cell) => { 416 | last_ca = ca; 417 | // Connect before disconnect to avoid memory bounce, when switching to same cell twice. 418 | let nextKill2 = Operational.value(ca).listen_(out.getVertex__(), 419 | (a : A) => { 420 | outValue = a; 421 | pump(); 422 | }, 423 | false); 424 | if (kill2 !== null) 425 | kill2(); 426 | kill2 = nextKill2; 427 | }, false); 428 | return () => { kill1(); kill2(); }; 429 | } 430 | ); 431 | out.setVertex__(new Vertex("switchC", 0, [src])); 432 | return out.holdLazy(za); 433 | }); 434 | } 435 | 436 | /** 437 | * Unwrap a stream inside a cell to give a time-varying stream implementation. 438 | */ 439 | static switchS(csa : Cell>) : Stream { 440 | return Transaction.run(() => { 441 | const out = new StreamWithSend(), 442 | h2 = (a : A) => { 443 | out.send_(a); 444 | }, 445 | src = new Source( 446 | csa.getVertex__(), 447 | () => { 448 | let kill2 = csa.sampleNoTrans__().listen_(out.getVertex__(), h2, false); 449 | const kill1 = csa.getStream__().listen_(out.getVertex__(), (sa : Stream) => { 450 | // Connect before disconnect to avoid memory bounce, when switching to same stream twice. 451 | let nextKill2 = sa.listen_(out.getVertex__(), h2, true); 452 | kill2(); 453 | kill2 = nextKill2; 454 | }, false); 455 | return () => { kill1(); kill2(); }; 456 | } 457 | ); 458 | out.setVertex__(new Vertex("switchS", 0, [src])); 459 | return out; 460 | }); 461 | } 462 | 463 | /** 464 | * When transforming a value from a larger type to a smaller type, it is likely for duplicate changes to become 465 | * propergated. This function insures only distinct changes get propergated. 466 | */ 467 | calm(eq: (a:A,b:A)=>boolean): Cell { 468 | return Operational 469 | .updates(this) 470 | .collectLazy( 471 | this.sampleLazy(), 472 | (newValue, oldValue) => { 473 | let result: A; 474 | if (eq(newValue, oldValue)) { 475 | result = null; 476 | } else { 477 | result = newValue; 478 | } 479 | return new Tuple2(result, newValue); 480 | } 481 | ) 482 | .filterNotNull() 483 | .holdLazy(this.sampleLazy()); 484 | } 485 | 486 | /** 487 | * This function is the same as calm, except you do not need to pass an eq function. This function will use (===) 488 | * as its eq function. I.E. calling calmRefEq() is the same as calm((a,b) => a === b). 489 | */ 490 | calmRefEq(): Cell { 491 | return this.calm((a, b) => a === b); 492 | } 493 | 494 | /** 495 | * Listen for updates to the value of this cell. This is the observer pattern. The 496 | * returned {@link Listener} has a {@link Listener#unlisten()} method to cause the 497 | * listener to be removed. This is an OPERATIONAL mechanism is for interfacing between 498 | * the world of I/O and for FRP. 499 | * @param h The handler to execute when there's a new value. 500 | * You should make no assumptions about what thread you are called on, and the 501 | * handler should not block. You are not allowed to use {@link CellSink#send(Object)} 502 | * or {@link StreamSink#send(Object)} in the handler. 503 | * An exception will be thrown, because you are not meant to use this to create 504 | * your own primitives. 505 | */ 506 | listen(h : (a : A) => void) : () => void { 507 | return Transaction.run(() => { 508 | return Operational.value(this).listen(h); 509 | }); 510 | } 511 | 512 | /** 513 | * Fantasy-land Algebraic Data Type Compatability. 514 | * Cell satisfies the Functor, Apply, Applicative categories 515 | * @see {@link https://github.com/fantasyland/fantasy-land} for more info 516 | */ 517 | 518 | //of :: Applicative f => a -> f a 519 | static 'fantasy-land/of'(a:A):Cell { 520 | return new Cell(a); 521 | } 522 | 523 | //map :: Functor f => f a ~> (a -> b) -> f b 524 | 'fantasy-land/map'(f : ((a : A) => B)) : Cell { 525 | return this.map(f); 526 | } 527 | 528 | //ap :: Apply f => f a ~> f (a -> b) -> f b 529 | 'fantasy-land/ap'(cf: Cell<(a : A) => B>):Cell { 530 | return Cell.apply(cf, this); 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/lib/sodium/CellLoop.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from "./Cell"; 2 | import { Lazy } from "./Lazy"; 3 | import { LazyCell } from "./LazyCell"; 4 | import { Transaction } from "./Transaction"; 5 | import { StreamLoop } from "./Stream"; 6 | 7 | /** 8 | * A forward reference for a {@link Cell} equivalent to the Cell that is referenced. 9 | */ 10 | export class CellLoop extends LazyCell { 11 | constructor() { 12 | super(null, new StreamLoop()); 13 | } 14 | 15 | /** 16 | * Resolve the loop to specify what the CellLoop was a forward reference to. It 17 | * must be invoked inside the same transaction as the place where the CellLoop is used. 18 | * This requires you to create an explicit transaction with {@link Transaction#run(Lambda0)} 19 | * or {@link Transaction#runVoid(Runnable)}. 20 | */ 21 | loop(a_out : Cell) : void { 22 | const me = this; 23 | Transaction.run(() => { 24 | (>me.getStream__()).loop(a_out.getStream__()); 25 | me.lazyInitValue = a_out.sampleLazy(); 26 | }); 27 | } 28 | 29 | sampleNoTrans__() : A 30 | { 31 | if (!(>this.getStream__()).assigned__) 32 | throw new Error("CellLoop sampled before it was looped"); 33 | return super.sampleNoTrans__(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/sodium/CellSink.ts: -------------------------------------------------------------------------------- 1 | import { Lambda1, Lambda1_deps, Lambda1_toFunction, 2 | Lambda2, Lambda2_deps, Lambda2_toFunction } from "./Lambda"; 3 | import { Cell } from "./Cell"; 4 | import { StreamSink } from "./StreamSink"; 5 | import { Transaction } from "./Transaction"; 6 | 7 | /** 8 | * A cell that allows values to be pushed into it, acting as an interface between the 9 | * world of I/O and the world of FRP. Code that exports CellSinks for read-only use 10 | * should downcast to {@link Cell}. 11 | */ 12 | export class CellSink extends Cell { 13 | /** 14 | * Construct a writable cell with the specified initial value. If multiple values are 15 | * sent in the same transaction, the specified function is used to combine them. 16 | * 17 | * If the function is not supplied, then an exception will be thrown in this case. 18 | */ 19 | constructor(initValue : A, f? : ((l : A, r : A) => A) | Lambda2) { 20 | super(initValue, new StreamSink(f)); 21 | } 22 | 23 | /** 24 | * Send a value, modifying the value of the cell. send(A) may not be used inside 25 | * handlers registered with {@link Stream#listen(Handler)} or {@link Cell#listen(Handler)}. 26 | * An exception will be thrown, because CellSink is for interfacing I/O to FRP only. 27 | * You are not meant to use this to define your own primitives. 28 | * @param a Value to push into the cell. 29 | */ 30 | send(a : A) : void { 31 | (>this.getStream__()).send(a); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/sodium/CoalesceHandler.ts: -------------------------------------------------------------------------------- 1 | import { Lambda1, Lambda1_deps, Lambda1_toFunction, 2 | Lambda2, Lambda2_deps, Lambda2_toFunction, 3 | toSources } from "./Lambda"; 4 | import { Transaction } from "./Transaction"; 5 | import { StreamWithSend } from "./Stream"; 6 | import { Vertex } from "./Vertex"; 7 | 8 | export class CoalesceHandler 9 | { 10 | constructor(f : ((l : A, r : A) => A) | Lambda2, out : StreamWithSend) 11 | { 12 | this.f = Lambda2_toFunction(f); 13 | this.out = out; 14 | this.out.getVertex__().sources = this.out.getVertex__().sources.concat( 15 | toSources(Lambda2_deps(f))); 16 | this.accumValid = false; 17 | } 18 | private f : (l : A, r : A) => A; 19 | private out : StreamWithSend; 20 | private accumValid : boolean; 21 | private accum : A; 22 | private verbose : boolean; 23 | send_(a : A) { 24 | if (this.accumValid) 25 | this.accum = this.f(this.accum, a); 26 | else { 27 | Transaction.currentTransaction.prioritized(this.out.getVertex__(), () => { 28 | this.out.send_(this.accum); 29 | this.accumValid = false; 30 | this.accum = null; 31 | }); 32 | this.accum = a; 33 | this.accumValid = true; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/sodium/IOAction.ts: -------------------------------------------------------------------------------- 1 | import { Stream, StreamWithSend } from "./Stream"; 2 | import { Vertex, Source } from "./Vertex"; 3 | import { Transaction } from "./Transaction"; 4 | 5 | export class IOAction { 6 | /*! 7 | * Convert a function that performs asynchronous I/O taking input A 8 | * and returning a value of type B into an I/O action of type 9 | * (sa : Stream) => Stream 10 | */ 11 | static fromAsync(performIO : (a : A, result : (b : B) => void) => void) 12 | : (sa : Stream) => Stream { 13 | return (sa : Stream) => { 14 | const out = new StreamWithSend(null); 15 | out.setVertex__(new Vertex("map", 0, [ 16 | new Source( 17 | sa.getVertex__(), 18 | () => { 19 | return sa.listen_(out.getVertex__(), (a : A) => { 20 | performIO(a, (b : B) => { 21 | Transaction.run(() => { 22 | out.send_(b); 23 | }); 24 | }); 25 | }, false); 26 | } 27 | ) 28 | ] 29 | )); 30 | return out; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/sodium/Lambda.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from "./Stream"; 2 | import { Cell } from "./Cell"; 3 | import { Source } from "./Vertex"; 4 | 5 | export class Lambda1 { 6 | constructor(f : (a : A) => B, 7 | deps : Array|Cell>) { 8 | this.f = f; 9 | this.deps = deps; 10 | } 11 | f : (a : A) => B; 12 | deps : Array|Cell>; 13 | } 14 | 15 | export function lambda1(f : (a : A) => B, 16 | deps : Array|Cell>) : Lambda1 17 | { 18 | return new Lambda1(f, deps); 19 | } 20 | 21 | export function Lambda1_deps(f : ((a : A) => B) | Lambda1) : Array|Cell> { 22 | if (f instanceof Lambda1) 23 | return f.deps; 24 | else 25 | return []; 26 | } 27 | 28 | export function Lambda1_toFunction(f : ((a : A) => B) | Lambda1) : (a : A) => B { 29 | if (f instanceof Lambda1) 30 | return f.f; 31 | else 32 | return <(a : A) => B>f; 33 | } 34 | 35 | export class Lambda2 { 36 | constructor(f : (a : A, b : B) => C, 37 | deps : Array|Cell>) { 38 | this.f = f; 39 | this.deps = deps; 40 | } 41 | f : (a : A, b : B) => C; 42 | deps : Array|Cell>; 43 | } 44 | 45 | export function lambda2(f : (a : A, b : B) => C, 46 | deps : Array|Cell>) : Lambda2 47 | { 48 | return new Lambda2(f, deps); 49 | } 50 | 51 | export function Lambda2_deps(f : ((a : A, b : B) => C) | Lambda2) : Array|Cell> { 52 | if (f instanceof Lambda2) 53 | return f.deps; 54 | else 55 | return []; 56 | } 57 | 58 | export function Lambda2_toFunction(f : ((a : A, b : B) => C) | Lambda2) : (a : A, b : B) => C { 59 | if (f instanceof Lambda2) 60 | return f.f; 61 | else 62 | return <(a : A, b : B) => C>f; 63 | } 64 | 65 | export class Lambda3 { 66 | constructor(f : (a : A, b : B, c : C) => D, 67 | deps : Array|Cell>) { 68 | this.f = f; 69 | this.deps = deps; 70 | } 71 | f : (a : A, b : B, c : C) => D; 72 | deps : Array|Cell>; 73 | } 74 | 75 | export function lambda3(f : (a : A, b : B, c : C) => D, 76 | deps : Array|Cell>) : Lambda3 77 | { 78 | return new Lambda3(f, deps); 79 | } 80 | 81 | export function Lambda3_deps(f : ((a : A, b : B, c : C) => D) 82 | | Lambda3) : Array|Cell> { 83 | if (f instanceof Lambda3) 84 | return f.deps; 85 | else 86 | return []; 87 | } 88 | 89 | export function Lambda3_toFunction(f : ((a : A, b : B, c : C) => D) | Lambda3) : (a : A, b : B, c : C) => D { 90 | if (f instanceof Lambda3) 91 | return f.f; 92 | else 93 | return <(a : A, b : B, c : C) => D>f; 94 | } 95 | 96 | export class Lambda4 { 97 | constructor(f : (a : A, b : B, c : C, d : D) => E, 98 | deps : Array|Cell>) { 99 | this.f = f; 100 | this.deps = deps; 101 | } 102 | f : (a : A, b : B, c : C, d : D) => E; 103 | deps : Array|Cell>; 104 | } 105 | 106 | export function lambda4(f : (a : A, b : B, c : C, d : D) => E, 107 | deps : Array|Cell>) : Lambda4 108 | { 109 | return new Lambda4(f, deps); 110 | } 111 | 112 | export function Lambda4_deps(f : ((a : A, b : B, c : C, d : D) => E) 113 | | Lambda4) : Array|Cell> { 114 | if (f instanceof Lambda4) 115 | return f.deps; 116 | else 117 | return []; 118 | } 119 | 120 | export function Lambda4_toFunction(f : ((a : A, b : B, c : C, d : D) => E) 121 | | Lambda4) : (a : A, b : B, c : C, d : D) => E { 122 | if (f instanceof Lambda4) 123 | return f.f; 124 | else 125 | return <(a : A, b : B, c : C, d : D) => E>f; 126 | } 127 | 128 | export class Lambda5 { 129 | constructor(f : (a : A, b : B, c : C, d : D, e : E) => F, 130 | deps : Array|Cell>) { 131 | this.f = f; 132 | this.deps = deps; 133 | } 134 | f : (a : A, b : B, c : C, d : D, e : E) => F; 135 | deps : Array|Cell>; 136 | } 137 | 138 | export function lambda5(f : (a : A, b : B, c : C, d : D, e : E) => F, 139 | deps : Array|Cell>) : Lambda5 140 | { 141 | return new Lambda5(f, deps); 142 | } 143 | 144 | export function Lambda5_deps(f : ((a : A, b : B, c : C, d : D, e : E) => F) 145 | | Lambda5) : Array|Cell> { 146 | if (f instanceof Lambda5) 147 | return f.deps; 148 | else 149 | return []; 150 | } 151 | 152 | export function Lambda5_toFunction(f : ((a : A, b : B, c : C, d : D, e : E) => F) 153 | | Lambda5) : (a : A, b : B, c : C, d : D, e : E) => F { 154 | if (f instanceof Lambda5) 155 | return f.f; 156 | else 157 | return <(a : A, b : B, c : C, d : D, e : E) => F>f; 158 | } 159 | 160 | export class Lambda6 { 161 | constructor(f : (a : A, b : B, c : C, d : D, e : E, f : F) => G, 162 | deps : Array|Cell>) { 163 | this.f = f; 164 | this.deps = deps; 165 | } 166 | f : (a : A, b : B, c : C, d : D, e : E, f : F) => G; 167 | deps : Array|Cell>; 168 | } 169 | 170 | export function lambda6(f : (a : A, b : B, c : C, d : D, e : E, f : F) => G, 171 | deps : Array|Cell>) : Lambda6 172 | { 173 | return new Lambda6(f, deps); 174 | } 175 | 176 | export function Lambda6_deps(f : ((a : A, b : B, c : C, d : D, e : E, f : F) => G) 177 | | Lambda6) : Array|Cell> { 178 | if (f instanceof Lambda6) 179 | return f.deps; 180 | else 181 | return []; 182 | } 183 | 184 | export function Lambda6_toFunction(f : ((a : A, b : B, c : C, d : D, e : E, f : F) => G) 185 | | Lambda6) : (a : A, b : B, c : C, d : D, e : E, f : F) => G { 186 | if (f instanceof Lambda6) 187 | return f.f; 188 | else 189 | return <(a : A, b : B, c : C, d : D, e : E, f : F) => G>f; 190 | } 191 | 192 | export function toSources(deps : Array|Cell>) : Source[] { 193 | const ss : Source[] = []; 194 | for (let i = 0; i < deps.length; i++) { 195 | const dep = deps[i]; 196 | ss.push(new Source(dep.getVertex__(), null)); 197 | } 198 | return ss; 199 | } 200 | -------------------------------------------------------------------------------- /src/lib/sodium/Lazy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A representation for a value that may not be available until the current 3 | * transaction is closed. 4 | */ 5 | export class Lazy { 6 | constructor(f : () => A) { 7 | this.f = f; 8 | } 9 | 10 | private f : () => A; 11 | 12 | /** 13 | * Get the value if available, throwing an exception if not. 14 | * In the general case this should only be used in subsequent transactions to 15 | * when the Lazy was obtained. 16 | */ 17 | get() : A { 18 | return this.f(); 19 | } 20 | 21 | /** 22 | * Map the lazy value according to the specified function, so the returned Lazy reflects 23 | * the value of the function applied to the input Lazy's value. 24 | * @param f Function to apply to the contained value. It must be referentially transparent. 25 | */ 26 | map(f : (a : A) => B) { 27 | return new Lazy(() => { return f(this.f()); }); 28 | } 29 | 30 | /** 31 | * Lift a binary function into lazy values, so the returned Lazy reflects 32 | * the value of the function applied to the input Lazys' values. 33 | */ 34 | lift(b : Lazy, f : (a : A, b : B) => C) : Lazy { 35 | return new Lazy(() => { return f(this.f(), b.f()); }); 36 | } 37 | 38 | /** 39 | * Lift a ternary function into lazy values, so the returned Lazy reflects 40 | * the value of the function applied to the input Lazys' values. 41 | */ 42 | lift3(b : Lazy, c : Lazy, f : (a : A, b : B, c : C) => D) : Lazy { 43 | return new Lazy(() => { return f(this.f(), b.f(), c.f()); }); 44 | } 45 | 46 | /** 47 | * Lift a quaternary function into lazy values, so the returned Lazy reflects 48 | * the value of the function applied to the input Lazys' values. 49 | */ 50 | lift4(b : Lazy, c : Lazy, d : Lazy, f : (a : A, b : B, c : C, d : D) => E) : Lazy { 51 | return new Lazy(() => { return f(this.f(), b.f(), c.f(), d.f()); }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/sodium/LazyCell.ts: -------------------------------------------------------------------------------- 1 | import { Lazy } from "./Lazy"; 2 | import { Cell } from "./Cell"; 3 | import { Stream } from "./Stream"; 4 | import { Transaction } from "./Transaction"; 5 | 6 | export class LazyCell extends Cell { 7 | constructor(lazyInitValue : Lazy, str? : Stream) { 8 | super(null, null); 9 | Transaction.run(() => { 10 | if (str) 11 | this.setStream(str); 12 | this.lazyInitValue = lazyInitValue; 13 | }); 14 | } 15 | 16 | sampleNoTrans__() : A { // Override 17 | if (this.value == null && this.lazyInitValue != null) { 18 | this.value = this.lazyInitValue.get(); 19 | this.lazyInitValue = null; 20 | } 21 | return this.value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/sodium/Listener.ts: -------------------------------------------------------------------------------- 1 | import { Source, Vertex } from "./Vertex"; 2 | 3 | export class Listener { 4 | constructor(h : (a : A) => void, target : Vertex) { 5 | this.h = h; 6 | this.target = target; 7 | } 8 | h : (a : A) => void; 9 | target : Vertex; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/sodium/MillisecondsTimerSystem.ts: -------------------------------------------------------------------------------- 1 | import { TimerSystem, TimerSystemImpl } from "./TimerSystem"; 2 | 3 | /** 4 | * A timer system implementation using milliseconds as the time unit. 5 | */ 6 | export class MillisecondsTimerSystem extends TimerSystem { 7 | constructor() { 8 | super(new MillisecondsTimerSystemImpl()); 9 | } 10 | } 11 | 12 | class MillisecondsTimerSystemImpl extends TimerSystemImpl { 13 | /** 14 | * Set a timer that will execute the specified callback at the specified time. 15 | * @return A function that can be used to cancel the timer. 16 | */ 17 | setTimer(t : number, callback : () => void) : () => void 18 | { 19 | let timeout = setTimeout(callback, Math.max(t - this.now(), 0)); 20 | return () => { clearTimeout(timeout); } 21 | } 22 | 23 | /** 24 | * Return the current clock time. 25 | */ 26 | now() : number 27 | { 28 | return Date.now(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/sodium/Operational.ts: -------------------------------------------------------------------------------- 1 | import { Stream, StreamWithSend } from "./Stream"; 2 | import { Cell } from "./Cell"; 3 | import { Transaction } from "./Transaction"; 4 | import { Unit } from "./Unit"; 5 | import { Source, Vertex } from "./Vertex"; 6 | 7 | export class Operational { 8 | /** 9 | * A stream that gives the updates/steps for a {@link Cell}. 10 | *

11 | * This is an OPERATIONAL primitive, which is not part of the main Sodium 12 | * API. It breaks the property of non-detectability of cell steps/updates. 13 | * The rule with this primitive is that you should only use it in functions 14 | * that do not allow the caller to detect the cell updates. 15 | */ 16 | static updates(c : Cell) : Stream { 17 | /* Don't think this is needed 18 | const out = new StreamWithSend(null); 19 | out.setVertex__(new Vertex("updates", 0, [ 20 | new Source( 21 | c.getStream__().getVertex__(), 22 | () => { 23 | return c.getStream__().listen_(out.getVertex__(), (a : A) => { 24 | out.send_(a); 25 | }, false); 26 | } 27 | ), 28 | new Source( 29 | c.getVertex__(), 30 | () => { 31 | return () => { }; 32 | } 33 | ) 34 | ] 35 | )); 36 | return out; 37 | */ 38 | return c.getStream__(); 39 | } 40 | 41 | /** 42 | * A stream that is guaranteed to fire once in the transaction where value() is invoked, giving 43 | * the current value of the cell, and thereafter behaves like {@link updates(Cell)}, 44 | * firing for each update/step of the cell's value. 45 | *

46 | * This is an OPERATIONAL primitive, which is not part of the main Sodium 47 | * API. It breaks the property of non-detectability of cell steps/updates. 48 | * The rule with this primitive is that you should only use it in functions 49 | * that do not allow the caller to detect the cell updates. 50 | */ 51 | static value(c : Cell) : Stream { 52 | return Transaction.run(() => { 53 | const sSpark = new StreamWithSend(); 54 | Transaction.currentTransaction.prioritized(sSpark.getVertex__(), () => { 55 | sSpark.send_(Unit.UNIT); 56 | }); 57 | const sInitial = sSpark.snapshot1(c); 58 | return Operational.updates(c).orElse(sInitial); 59 | }); 60 | } 61 | 62 | /** 63 | * Push each event onto a new transaction guaranteed to come before the next externally 64 | * initiated transaction. Same as {@link split(Stream)} but it works on a single value. 65 | */ 66 | static defer(s : Stream) : Stream { 67 | return Operational.split(s.map((a : A) => { 68 | return [a]; 69 | })); 70 | } 71 | 72 | /** 73 | * Push each event in the list onto a newly created transaction guaranteed 74 | * to come before the next externally initiated transaction. Note that the semantics 75 | * are such that two different invocations of split() can put events into the same 76 | * new transaction, so the resulting stream's events could be simultaneous with 77 | * events output by split() or {@link defer(Stream)} invoked elsewhere in the code. 78 | */ 79 | static split(s : Stream>) : Stream { 80 | const out = new StreamWithSend(null); 81 | out.setVertex__(new Vertex("split", 0, [ 82 | new Source( 83 | s.getVertex__(), 84 | () => { 85 | out.getVertex__().childrn.push(s.getVertex__()); 86 | let cleanups: (()=>void)[] = []; 87 | cleanups.push( 88 | s.listen_(Vertex.NULL, (as : Array) => { 89 | for (let i = 0; i < as.length; i++) { 90 | Transaction.currentTransaction.post(i, () => { 91 | Transaction.run(() => { 92 | out.send_(as[i]); 93 | }); 94 | }); 95 | } 96 | }, false) 97 | ); 98 | cleanups.push(() => { 99 | let chs = out.getVertex__().childrn; 100 | for (let i = chs.length-1; i >= 0; --i) { 101 | if (chs[i] == s.getVertex__()) { 102 | chs.splice(i, 1); 103 | break; 104 | } 105 | } 106 | }); 107 | return () => { 108 | cleanups.forEach(cleanup => cleanup()); 109 | cleanups.splice(0, cleanups.length); 110 | } 111 | } 112 | ) 113 | ] 114 | )); 115 | return out; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/lib/sodium/Router.ts: -------------------------------------------------------------------------------- 1 | import { Dictionary, Set } from 'typescript-collections'; 2 | import { Stream, StreamWithSend } from './Stream'; 3 | import { Vertex, Source } from './Vertex'; 4 | 5 | export class Router { 6 | private _inStream: Stream; 7 | private _table: Dictionary[]>; 8 | private _vertex: Vertex; 9 | 10 | public constructor(inStream: Stream, selector: (a: A) => K[], keyToStr?: (k: K)=>string) { 11 | this._inStream = inStream; 12 | this._table = new Dictionary(keyToStr); 13 | this._vertex = 14 | new Vertex( 15 | "Router", 16 | this._inStream.getVertex__().rank + 1, // <-- estimated rank only, may be adjusted by ensureBiggerThan 17 | [] 18 | ); 19 | this._vertex.addSource( 20 | new Source( 21 | this._inStream.getVertex__(), 22 | () => 23 | this._inStream.listen_( 24 | this._vertex, 25 | (a: A) => { 26 | let ks = selector(a); 27 | let visited = new Set(keyToStr); 28 | let outs: StreamWithSend[] = []; 29 | for (let i = 0; i < ks.length; ++i) { 30 | let k = ks[i]; 31 | if (visited.contains(k)) { 32 | continue; 33 | } 34 | visited.add(k); 35 | let outs2 = this._table.getValue(k); 36 | if (outs2 != undefined) { 37 | for (let j = 0; j < outs2.length; ++j) { 38 | outs.push(outs2[j]); 39 | } 40 | } 41 | } 42 | for (let i = 0; i < outs.length; ++i) { 43 | outs[i].send_(a); 44 | } 45 | }, 46 | true 47 | ) 48 | ) 49 | ); 50 | } 51 | 52 | public filterMatches(k: K): Stream { 53 | let out = new StreamWithSend(); 54 | let vertex = 55 | new Vertex( 56 | "Router::filterMatches", 57 | this._vertex.rank + 1, // <-- estimated rank only, may be adjusted by ensureBiggerThan 58 | [ 59 | new Source( 60 | this._vertex, 61 | () => { 62 | this._vertex.increment(out.getVertex__()); 63 | let outs: StreamWithSend[] = this._table.getValue(k); 64 | if (outs == undefined) { 65 | outs = []; 66 | this._table.setValue(k, outs); 67 | } 68 | outs.push(out); 69 | return () => { 70 | this._vertex.decrement(out.getVertex__()); 71 | let outs2 = this._table.getValue(k); 72 | for (let i = outs2.length-1; i >= 0; --i) { 73 | if (outs2[i] == out) { 74 | outs2.splice(i, 1); 75 | break; 76 | } 77 | } 78 | if (outs2.length == 0) { 79 | this._table.remove(k); 80 | } 81 | }; 82 | } 83 | ) 84 | ] 85 | ); 86 | out.setVertex__(vertex); 87 | return out; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/lib/sodium/SecondsTimerSystem.ts: -------------------------------------------------------------------------------- 1 | import { TimerSystem, TimerSystemImpl } from "./TimerSystem"; 2 | 3 | /** 4 | * A timer system implementation using seconds as the time unit. 5 | */ 6 | export class SecondsTimerSystem extends TimerSystem { 7 | constructor() { 8 | super(new SecondsTimerSystemImpl()); 9 | } 10 | } 11 | 12 | class SecondsTimerSystemImpl extends TimerSystemImpl { 13 | /** 14 | * Set a timer that will execute the specified callback at the specified time. 15 | * @return A function that can be used to cancel the timer. 16 | */ 17 | setTimer(t : number, callback : () => void) : () => void 18 | { 19 | let timeout = setTimeout(callback, Math.max((t - this.now()) * 1000, 0)); 20 | return () => { clearTimeout(timeout); } 21 | } 22 | 23 | /** 24 | * Return the current clock time. 25 | */ 26 | now() : number 27 | { 28 | return Date.now() * 0.001; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/sodium/Stream.ts: -------------------------------------------------------------------------------- 1 | import { Lambda1, Lambda1_deps, Lambda1_toFunction, 2 | Lambda2, Lambda2_deps, Lambda2_toFunction, 3 | Lambda3, Lambda3_deps, Lambda3_toFunction, 4 | Lambda4, Lambda4_deps, Lambda4_toFunction, 5 | Lambda5, Lambda5_deps, Lambda5_toFunction, 6 | Lambda6, Lambda6_deps, Lambda6_toFunction, 7 | toSources } from "./Lambda"; 8 | import { Source, Vertex } from "./Vertex"; 9 | import { Transaction } from "./Transaction"; 10 | import { CoalesceHandler } from "./CoalesceHandler"; 11 | import { Cell } from "./Cell"; 12 | //import { StreamLoop } from "./StreamLoop"; 13 | import { Listener } from "./Listener"; 14 | import { Tuple2 } from "./Tuple2"; 15 | import { Lazy } from "./Lazy"; 16 | import { LazyCell } from "./LazyCell"; 17 | import * as Z from "sanctuary-type-classes"; 18 | 19 | class MergeState { 20 | constructor() {} 21 | left : A = null; 22 | left_present : boolean = false; 23 | right : A = null; 24 | right_present : boolean = false; 25 | } 26 | 27 | export class Stream { 28 | constructor(vertex? : Vertex) { 29 | this.vertex = vertex ? vertex : new Vertex("Stream", 0, []); 30 | } 31 | 32 | getVertex__() : Vertex { 33 | return this.vertex; 34 | } 35 | 36 | protected vertex : Vertex; 37 | protected listeners : Array> = []; 38 | protected firings : A[] = []; 39 | 40 | /** 41 | * Transform the stream's event values according to the supplied function, so the returned 42 | * Stream's event values reflect the value of the function applied to the input 43 | * Stream's event values. 44 | * @param f Function to apply to convert the values. It may construct FRP logic or use 45 | * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the 46 | * cell. Apart from this the function must be referentially transparent. 47 | */ 48 | map(f : ((a : A) => B) | Lambda1) : Stream { 49 | const out = new StreamWithSend(null); 50 | const ff = Lambda1_toFunction(f); 51 | out.vertex = new Vertex("map", 0, [ 52 | new Source( 53 | this.vertex, 54 | () => { 55 | return this.listen_(out.vertex, (a : A) => { 56 | out.send_(ff(a)); 57 | }, false); 58 | } 59 | ) 60 | ].concat(toSources(Lambda1_deps(f))) 61 | ); 62 | return out; 63 | } 64 | 65 | /** 66 | * Transform the stream's event values into the specified constant value. 67 | * @param b Constant value. 68 | */ 69 | mapTo(b : B) : Stream { 70 | const out = new StreamWithSend(null); 71 | out.vertex = new Vertex("mapTo", 0, [ 72 | new Source( 73 | this.vertex, 74 | () => { 75 | return this.listen_(out.vertex, (a : A) => { 76 | out.send_(b); 77 | }, false); 78 | } 79 | ) 80 | ] 81 | ); 82 | return out; 83 | } 84 | 85 | /** 86 | * Variant of {@link Stream#merge(Stream, Lambda2)} that merges two streams and will drop an event 87 | * in the simultaneous case. 88 | *

89 | * In the case where two events are simultaneous (i.e. both 90 | * within the same transaction), the event from this will take precedence, and 91 | * the event from s will be dropped. 92 | * If you want to specify your own combining function, use {@link Stream#merge(Stream, Lambda2)}. 93 | * s1.orElse(s2) is equivalent to s1.merge(s2, (l, r) -> l). 94 | *

95 | * The name orElse() is used instead of merge() to make it really clear that care should 96 | * be taken, because events can be dropped. 97 | */ 98 | orElse(s : Stream) : Stream { 99 | return this.merge(s, (left : A, right: A) => { 100 | return left; 101 | }); 102 | } 103 | 104 | /** 105 | * Merge two streams of the same type into one, so that events on either input appear 106 | * on the returned stream. 107 | *

108 | * If the events are simultaneous (that is, one event from this and one from s 109 | * occurring in the same transaction), combine them into one using the specified combining function 110 | * so that the returned stream is guaranteed only ever to have one event per transaction. 111 | * The event from this will appear at the left input of the combining function, and 112 | * the event from s will appear at the right. 113 | * @param f Function to combine the values. It may construct FRP logic or use 114 | * {@link Cell#sample()}. Apart from this the function must be referentially transparent. 115 | */ 116 | merge(s : Stream, f : ((left : A, right : A) => A) | Lambda2) : Stream { 117 | const ff = Lambda2_toFunction(f); 118 | const mergeState = new MergeState(); 119 | let pumping = false; 120 | const out = new StreamWithSend(null); 121 | const pump = () => { 122 | if (pumping) { 123 | return; 124 | } 125 | pumping = true; 126 | Transaction.currentTransaction.prioritized(out.getVertex__(), () => { 127 | if (mergeState.left_present && mergeState.right_present) { 128 | out.send_(ff(mergeState.left, mergeState.right)); 129 | } else if (mergeState.left_present) { 130 | out.send_(mergeState.left); 131 | } else if (mergeState.right_present) { 132 | out.send_(mergeState.right); 133 | } 134 | mergeState.left = null; 135 | mergeState.left_present = false; 136 | mergeState.right = null; 137 | mergeState.right_present = false; 138 | pumping = false; 139 | }); 140 | }; 141 | const vertex = new Vertex("merge", 0, 142 | [ 143 | new Source( 144 | this.vertex, 145 | () => this.listen_(out.vertex, (a : A) => { 146 | mergeState.left = a; 147 | mergeState.left_present = true; 148 | pump(); 149 | }, false) 150 | ), 151 | new Source( 152 | s.vertex, 153 | () => s.listen_(out.vertex, (a : A) => { 154 | mergeState.right = a; 155 | mergeState.right_present = true; 156 | pump(); 157 | }, false) 158 | ) 159 | ].concat(toSources(Lambda2_deps(f))) 160 | ); 161 | out.vertex = vertex; 162 | return out; 163 | } 164 | 165 | /** 166 | * Return a stream that only outputs events for which the predicate returns true. 167 | */ 168 | filter(f : ((a : A) => boolean) | Lambda1) : Stream { 169 | const out = new StreamWithSend(null); 170 | const ff = Lambda1_toFunction(f); 171 | out.vertex = new Vertex("filter", 0, [ 172 | new Source( 173 | this.vertex, 174 | () => { 175 | return this.listen_(out.vertex, (a : A) => { 176 | if (ff(a)) 177 | out.send_(a); 178 | }, false); 179 | } 180 | ) 181 | ].concat(toSources(Lambda1_deps(f))) 182 | ); 183 | return out; 184 | } 185 | 186 | /** 187 | * Return a stream that only outputs events that have present 188 | * values, discarding null values. 189 | */ 190 | filterNotNull() : Stream { 191 | const out = new StreamWithSend(null); 192 | out.vertex = new Vertex("filterNotNull", 0, [ 193 | new Source( 194 | this.vertex, 195 | () => { 196 | return this.listen_(out.vertex, (a : A) => { 197 | if (a !== null) 198 | out.send_(a); 199 | }, false); 200 | } 201 | ) 202 | ] 203 | ); 204 | return out; 205 | } 206 | 207 | /** 208 | * Return a stream that only outputs events from the input stream 209 | * when the specified cell's value is true. 210 | */ 211 | gate(c : Cell) : Stream { 212 | return this.snapshot(c, (a : A, pred : boolean) => { 213 | return pred ? a : null; 214 | }).filterNotNull(); 215 | } 216 | 217 | /** 218 | * Variant of {@link snapshot(Cell, Lambda2)} that captures the cell's value 219 | * at the time of the event firing, ignoring the stream's value. 220 | */ 221 | snapshot1(c : Cell) : Stream { 222 | const out = new StreamWithSend(null); 223 | out.vertex = new Vertex("snapshot1", 0, [ 224 | new Source( 225 | this.vertex, 226 | () => { 227 | return this.listen_(out.vertex, (a : A) => { 228 | out.send_(c.sampleNoTrans__()); 229 | }, false); 230 | } 231 | ), 232 | new Source(c.getVertex__(), null) 233 | ] 234 | ); 235 | return out; 236 | } 237 | 238 | /** 239 | * Return a stream whose events are the result of the combination using the specified 240 | * function of the input stream's event value and the value of the cell at that time. 241 | *

242 | * There is an implicit delay: State updates caused by event firings being held with 243 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until 244 | * the following transaction. To put this another way, {@link Stream#snapshot(Cell, Lambda2)} 245 | * always sees the value of a cell as it was before any state changes from the current 246 | * transaction. 247 | */ 248 | snapshot(b : Cell, f_ : ((a : A, b : B) => C) | Lambda2) : Stream 249 | { 250 | const out = new StreamWithSend(null); 251 | const ff = Lambda2_toFunction(f_); 252 | out.vertex = new Vertex("snapshot", 0, [ 253 | new Source( 254 | this.vertex, 255 | () => { 256 | return this.listen_(out.vertex, (a : A) => { 257 | out.send_(ff(a, b.sampleNoTrans__())); 258 | }, false); 259 | } 260 | ), 261 | new Source(b.getVertex__(), null) 262 | ].concat(toSources(Lambda2_deps(f_))) 263 | ); 264 | return out; 265 | } 266 | 267 | /** 268 | * Return a stream whose events are the result of the combination using the specified 269 | * function of the input stream's event value and the value of the cells at that time. 270 | *

271 | * There is an implicit delay: State updates caused by event firings being held with 272 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until 273 | * the following transaction. To put this another way, snapshot() 274 | * always sees the value of a cell as it was before any state changes from the current 275 | * transaction. 276 | */ 277 | snapshot3(b : Cell, c : Cell, f_ : ((a : A, b : B, c : C) => D) | Lambda3) : Stream 278 | { 279 | const out = new StreamWithSend(null); 280 | const ff = Lambda3_toFunction(f_); 281 | out.vertex = new Vertex("snapshot", 0, [ 282 | new Source( 283 | this.vertex, 284 | () => { 285 | return this.listen_(out.vertex, (a : A) => { 286 | out.send_(ff(a, b.sampleNoTrans__(), c.sampleNoTrans__())); 287 | }, false); 288 | } 289 | ), 290 | new Source(b.getVertex__(), null), 291 | new Source(c.getVertex__(), null) 292 | ].concat(toSources(Lambda3_deps(f_))) 293 | ); 294 | return out; 295 | } 296 | 297 | /** 298 | * Return a stream whose events are the result of the combination using the specified 299 | * function of the input stream's event value and the value of the cells at that time. 300 | *

301 | * There is an implicit delay: State updates caused by event firings being held with 302 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until 303 | * the following transaction. To put this another way, snapshot() 304 | * always sees the value of a cell as it was before any state changes from the current 305 | * transaction. 306 | */ 307 | snapshot4(b : Cell, c : Cell, d : Cell, 308 | f_ : ((a : A, b : B, c : C, d : D) => E) | Lambda4) : Stream 309 | { 310 | const out = new StreamWithSend(null); 311 | const ff = Lambda4_toFunction(f_); 312 | out.vertex = new Vertex("snapshot", 0, [ 313 | new Source( 314 | this.vertex, 315 | () => { 316 | return this.listen_(out.vertex, (a : A) => { 317 | out.send_(ff(a, b.sampleNoTrans__(), c.sampleNoTrans__(), 318 | d.sampleNoTrans__())); 319 | }, false); 320 | } 321 | ), 322 | new Source(b.getVertex__(), null), 323 | new Source(c.getVertex__(), null), 324 | new Source(d.getVertex__(), null) 325 | ].concat(toSources(Lambda4_deps(f_))) 326 | ); 327 | return out; 328 | } 329 | 330 | /** 331 | * Return a stream whose events are the result of the combination using the specified 332 | * function of the input stream's event value and the value of the cells at that time. 333 | *

334 | * There is an implicit delay: State updates caused by event firings being held with 335 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until 336 | * the following transaction. To put this another way, snapshot() 337 | * always sees the value of a cell as it was before any state changes from the current 338 | * transaction. 339 | */ 340 | snapshot5(b : Cell, c : Cell, d : Cell, e : Cell, 341 | f_ : ((a : A, b : B, c : C, d : D, e : E) => F) | Lambda5) : Stream 342 | { 343 | const out = new StreamWithSend(null); 344 | const ff = Lambda5_toFunction(f_); 345 | out.vertex = new Vertex("snapshot", 0, [ 346 | new Source( 347 | this.vertex, 348 | () => { 349 | return this.listen_(out.vertex, (a : A) => { 350 | out.send_(ff(a, b.sampleNoTrans__(), c.sampleNoTrans__(), 351 | d.sampleNoTrans__(), e.sampleNoTrans__())); 352 | }, false); 353 | } 354 | ), 355 | new Source(b.getVertex__(), null), 356 | new Source(c.getVertex__(), null), 357 | new Source(d.getVertex__(), null), 358 | new Source(e.getVertex__(), null) 359 | ].concat(toSources(Lambda5_deps(f_))) 360 | ); 361 | return out; 362 | } 363 | 364 | /** 365 | * Return a stream whose events are the result of the combination using the specified 366 | * function of the input stream's event value and the value of the cells at that time. 367 | *

368 | * There is an implicit delay: State updates caused by event firings being held with 369 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until 370 | * the following transaction. To put this another way, snapshot() 371 | * always sees the value of a cell as it was before any state changes from the current 372 | * transaction. 373 | */ 374 | snapshot6(b : Cell, c : Cell, d : Cell, e : Cell, f : Cell, 375 | f_ : ((a : A, b : B, c : C, d : D, e : E, f : F) => G) | Lambda6) : Stream 376 | { 377 | const out = new StreamWithSend(null); 378 | const ff = Lambda6_toFunction(f_); 379 | out.vertex = new Vertex("snapshot", 0, [ 380 | new Source( 381 | this.vertex, 382 | () => { 383 | return this.listen_(out.vertex, (a : A) => { 384 | out.send_(ff(a, b.sampleNoTrans__(), c.sampleNoTrans__(), 385 | d.sampleNoTrans__(), e.sampleNoTrans__(), 386 | f.sampleNoTrans__())); 387 | }, false); 388 | } 389 | ), 390 | new Source(b.getVertex__(), null), 391 | new Source(c.getVertex__(), null), 392 | new Source(d.getVertex__(), null), 393 | new Source(e.getVertex__(), null), 394 | new Source(f.getVertex__(), null) 395 | ].concat(toSources(Lambda6_deps(f_))) 396 | ); 397 | return out; 398 | } 399 | 400 | /** 401 | * Create a {@link Cell} with the specified initial value, that is updated 402 | * by this stream's event values. 403 | *

404 | * There is an implicit delay: State updates caused by event firings don't become 405 | * visible as the cell's current value as viewed by {@link Stream#snapshot(Cell, Lambda2)} 406 | * until the following transaction. To put this another way, 407 | * {@link Stream#snapshot(Cell, Lambda2)} always sees the value of a cell as it was before 408 | * any state changes from the current transaction. 409 | */ 410 | hold(initValue : A) : Cell { 411 | return new Cell(initValue, this); 412 | } 413 | 414 | /** 415 | * A variant of {@link hold(Object)} with an initial value captured by {@link Cell#sampleLazy()}. 416 | */ 417 | holdLazy(initValue : Lazy) : Cell { 418 | return new LazyCell(initValue, this); 419 | } 420 | 421 | /** 422 | * Transform an event with a generalized state loop (a Mealy machine). The function 423 | * is passed the input and the old state and returns the new state and output value. 424 | * @param f Function to apply to update the state. It may construct FRP logic or use 425 | * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the 426 | * cell. Apart from this the function must be referentially transparent. 427 | */ 428 | collect(initState : S, f : ((a : A, s : S) => Tuple2) | Lambda2>) : Stream { 429 | return this.collectLazy(new Lazy(() => { return initState; }), f); 430 | } 431 | 432 | /** 433 | * A variant of {@link collect(Object, Lambda2)} that takes an initial state returned by 434 | * {@link Cell#sampleLazy()}. 435 | */ 436 | collectLazy(initState : Lazy, f : ((a : A, s : S) => Tuple2) | Lambda2>) : Stream { 437 | const ea = this; 438 | return Transaction.run(() => { 439 | const es = new StreamLoop(), 440 | s = es.holdLazy(initState), 441 | ebs = ea.snapshot(s, f), 442 | eb = ebs.map((bs : Tuple2) => { return bs.a; }), 443 | es_out = ebs.map((bs : Tuple2) => { return bs.b; }); 444 | es.loop(es_out); 445 | return eb; 446 | }); 447 | } 448 | 449 | /** 450 | * Accumulate on input event, outputting the new state each time. 451 | * @param f Function to apply to update the state. It may construct FRP logic or use 452 | * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the 453 | * cell. Apart from this the function must be referentially transparent. 454 | */ 455 | accum(initState : S, f : ((a : A, s : S) => S) | Lambda2) : Cell { 456 | return this.accumLazy(new Lazy(() => { return initState; }), f); 457 | } 458 | 459 | /** 460 | * A variant of {@link accum(Object, Lambda2)} that takes an initial state returned by 461 | * {@link Cell#sampleLazy()}. 462 | */ 463 | accumLazy(initState : Lazy, f : ((a : A, s : S) => S) | Lambda2) : Cell { 464 | const ea = this; 465 | return Transaction.run(() => { 466 | const es = new StreamLoop(), 467 | s = es.holdLazy(initState), 468 | es_out = ea.snapshot(s, f); 469 | es.loop(es_out); 470 | return es_out.holdLazy(initState); 471 | }); 472 | } 473 | 474 | /** 475 | * Return a stream that outputs only one value: the next event of the 476 | * input stream, starting from the transaction in which once() was invoked. 477 | */ 478 | once() : Stream { 479 | /* 480 | return Transaction.run(() => { 481 | const ev = this, 482 | out = new StreamWithSend(); 483 | let la : () => void = null; 484 | la = ev.listen_(out.vertex, (a : A) => { 485 | if (la !== null) { 486 | out.send_(a); 487 | la(); 488 | la = null; 489 | } 490 | }, false); 491 | return out; 492 | }); 493 | */ 494 | // We can't use the implementation above, because unregistering 495 | // listeners triggers the exception 496 | // "send() was invoked before listeners were registered" 497 | // We can revisit this another time. For now we will use the less 498 | // efficient implementation below. 499 | const me = this; 500 | return Transaction.run(() => me.gate(me.mapTo(false).hold(true))); 501 | } 502 | 503 | listen(h : (a : A) => void) : () => void { 504 | return Transaction.run<() => void>(() => { 505 | return this.listen_(Vertex.NULL, h, false); 506 | }); 507 | } 508 | 509 | listen_(target : Vertex, 510 | h : (a : A) => void, 511 | suppressEarlierFirings : boolean) : () => void { 512 | if (this.vertex.register(target)) 513 | Transaction.currentTransaction.requestRegen(); 514 | const listener = new Listener(h, target); 515 | this.listeners.push(listener); 516 | if (!suppressEarlierFirings && this.firings.length != 0) { 517 | const firings = this.firings.slice(); 518 | Transaction.currentTransaction.prioritized(target, () => { 519 | // Anything sent already in this transaction must be sent now so that 520 | // there's no order dependency between send and listen. 521 | for (let i = 0; i < firings.length; i++) 522 | h(firings[i]); 523 | }); 524 | } 525 | return () => { 526 | let removed = false; 527 | for (let i = 0; i < this.listeners.length; i++) { 528 | if (this.listeners[i] == listener) { 529 | this.listeners.splice(i, 1); 530 | removed = true; 531 | break; 532 | } 533 | } 534 | if (removed) 535 | this.vertex.deregister(target); 536 | }; 537 | } 538 | 539 | 540 | /** 541 | * Fantasy-land Algebraic Data Type Compatibility. 542 | * Stream satisfies the Functor and Monoid Categories (and hence Semigroup) 543 | * @see {@link https://github.com/fantasyland/fantasy-land} for more info 544 | */ 545 | 546 | //map :: Functor f => f a ~> (a -> b) -> f b 547 | 'fantasy-land/map'(f : ((a : A) => B)) : Stream { 548 | return this.map(f); 549 | } 550 | 551 | //concat :: Semigroup a => a ~> a -> a 552 | 'fantasy-land/concat'(a:Stream) : Stream { 553 | return this.merge(a, (left:any, right) => { 554 | return (Z.Semigroup.test(left)) ? Z.concat(left, right) : left; 555 | }); 556 | } 557 | 558 | //empty :: Monoid m => () -> m 559 | 'fantasy-land/empty'() : Stream { 560 | return new Stream(); 561 | } 562 | } 563 | 564 | export class StreamWithSend extends Stream { 565 | constructor(vertex? : Vertex) { 566 | super(vertex); 567 | } 568 | 569 | setVertex__(vertex : Vertex) { // TO DO figure out how to hide this 570 | this.vertex = vertex; 571 | } 572 | 573 | send_(a : A) : void { 574 | if (this.firings.length == 0) 575 | Transaction.currentTransaction.last(() => { 576 | this.firings = []; 577 | }); 578 | this.firings.push(a); 579 | const listeners = this.listeners.slice(); 580 | for (let i = 0; i < listeners.length; i++) { 581 | const h = listeners[i].h; 582 | Transaction.currentTransaction.prioritized(listeners[i].target, () => { 583 | Transaction.currentTransaction.inCallback++; 584 | try { 585 | h(a); 586 | Transaction.currentTransaction.inCallback--; 587 | } 588 | catch (err) { 589 | Transaction.currentTransaction.inCallback--; 590 | throw err; 591 | } 592 | }); 593 | } 594 | } 595 | } 596 | 597 | /** 598 | * A forward reference for a {@link Stream} equivalent to the Stream that is referenced. 599 | */ 600 | export class StreamLoop extends StreamWithSend { 601 | assigned__ : boolean = false; // to do: Figure out how to hide this 602 | 603 | constructor() 604 | { 605 | super(); 606 | this.vertex.name = "StreamLoop"; 607 | if (Transaction.currentTransaction === null) 608 | throw new Error("StreamLoop/CellLoop must be used within an explicit transaction"); 609 | } 610 | 611 | /** 612 | * Resolve the loop to specify what the StreamLoop was a forward reference to. It 613 | * must be invoked inside the same transaction as the place where the StreamLoop is used. 614 | * This requires you to create an explicit transaction with {@link Transaction#run(Lambda0)} 615 | * or {@link Transaction#runVoid(Runnable)}. 616 | */ 617 | loop(sa_out : Stream) : void { 618 | if (this.assigned__) 619 | throw new Error("StreamLoop looped more than once"); 620 | this.assigned__ = true; 621 | this.vertex.addSource( 622 | new Source( 623 | sa_out.getVertex__(), 624 | () => { 625 | return sa_out.listen_(this.vertex, (a : A) => { 626 | this.send_(a); 627 | }, false); 628 | } 629 | ) 630 | ); 631 | } 632 | } 633 | -------------------------------------------------------------------------------- /src/lib/sodium/StreamSink.ts: -------------------------------------------------------------------------------- 1 | import { Lambda1, Lambda1_deps, Lambda1_toFunction, 2 | Lambda2, Lambda2_deps, Lambda2_toFunction } from "./Lambda"; 3 | import { StreamWithSend } from "./Stream"; 4 | import { CoalesceHandler } from "./CoalesceHandler"; 5 | import { Transaction } from "./Transaction"; 6 | import { Vertex } from './Vertex'; 7 | 8 | /** 9 | * A stream that allows values to be pushed into it, acting as an interface between the 10 | * world of I/O and the world of FRP. Code that exports StreamSinks for read-only use 11 | * should downcast to {@link Stream}. 12 | */ 13 | export class StreamSink extends StreamWithSend { 14 | private disableListenCheck: boolean = false; 15 | 16 | constructor(f? : ((l : A, r : A) => A) | Lambda2) { 17 | super(); 18 | if (!f) 19 | f = <(l : A, r : A) => A>((l : A, r : A) => { 20 | throw new Error("send() called more than once per transaction, which isn't allowed. Did you want to combine the events? Then pass a combining function to your StreamSink constructor."); 21 | }); 22 | this.coalescer = new CoalesceHandler(f, this); 23 | } 24 | 25 | private coalescer : CoalesceHandler; 26 | 27 | send(a : A) : void { 28 | Transaction.run( 29 | () => { 30 | // We throw this error if we send into FRP logic that has been constructed 31 | // but nothing is listening to it yet. We need to do it this way because 32 | // it's the only way to manage memory in a language with no finalizers. 33 | if (!this.disableListenCheck) { 34 | if (this.vertex.refCount() == 0) { 35 | throw new Error("send() was invoked before listeners were registered"); 36 | } 37 | } 38 | // 39 | if (Transaction.currentTransaction.inCallback > 0) 40 | throw new Error("You are not allowed to use send() inside a Sodium callback"); 41 | this.coalescer.send_(a); 42 | } 43 | ) 44 | } 45 | 46 | listen_(target : Vertex, 47 | h : (a : A) => void, 48 | suppressEarlierFirings : boolean) : () => void { 49 | let result = super.listen_(target, h, suppressEarlierFirings); 50 | this.disableListenCheck = true; 51 | return result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/sodium/TimerSystem.ts: -------------------------------------------------------------------------------- 1 | import { Vertex, Source } from "./Vertex"; 2 | import * as Collections from 'typescript-collections'; 3 | import { Stream, StreamWithSend } from "./Stream"; 4 | import { StreamSink } from "./StreamSink"; 5 | import { Cell } from "./Cell"; 6 | import { CellSink } from "./CellSink"; 7 | import { Transaction } from "./Transaction"; 8 | 9 | /** 10 | * An interface for implementations of FRP timer systems. 11 | */ 12 | export abstract class TimerSystemImpl { 13 | /** 14 | * Set a timer that will execute the specified callback at the specified time. 15 | * @return A function that can be used to cancel the timer. 16 | */ 17 | abstract setTimer(t : number, callback : () => void) : () => void; 18 | 19 | /** 20 | * Return the current clock time. 21 | */ 22 | abstract now() : number; 23 | } 24 | 25 | let nextSeq : number = 0; 26 | 27 | class Event { 28 | constructor(t : number, sAlarm : StreamWithSend) { 29 | this.t = t; 30 | this.sAlarm = sAlarm; 31 | this.seq = ++nextSeq; 32 | } 33 | t : number; 34 | sAlarm : StreamWithSend; 35 | seq : number; // Used to guarantee uniqueness 36 | } 37 | 38 | export class TimerSystem { 39 | constructor(impl : TimerSystemImpl) { 40 | Transaction.run(() => { 41 | this.impl = impl; 42 | this.tMinimum = 0; 43 | const timeSnk = new CellSink(impl.now()); 44 | this.time = timeSnk; 45 | // A dummy listener to time to keep it alive even when there are no other listeners. 46 | this.time.listen((t : number) => { }); 47 | Transaction.onStart(() => { 48 | // Ensure the time is always increasing from the FRP's point of view. 49 | const t = this.tMinimum = Math.max(this.tMinimum, impl.now()); 50 | // Pop and execute all events earlier than or equal to t (the current time). 51 | while (true) { 52 | let ev : Event = null; 53 | if (!this.eventQueue.isEmpty()) { 54 | let mev = this.eventQueue.minimum(); 55 | if (mev.t <= t) { 56 | ev = mev; 57 | // TO DO: Detect infinite loops! 58 | } 59 | } 60 | if (ev != null) { 61 | timeSnk.send(ev.t); 62 | Transaction.run(() => ev.sAlarm.send_(ev.t)); 63 | } 64 | else 65 | break; 66 | } 67 | timeSnk.send(t); 68 | }); 69 | }); 70 | } 71 | 72 | private impl : TimerSystemImpl; 73 | private tMinimum : number; // A guard to allow us to guarantee that the time as 74 | // seen by the FRP is always increasing. 75 | 76 | /** 77 | * A cell giving the current clock time. 78 | */ 79 | time : Cell; 80 | 81 | private eventQueue : Collections.BSTree = new Collections.BSTree((a, b) => { 82 | if (a.t < b.t) return -1; 83 | if (a.t > b.t) return 1; 84 | if (a.seq < b.seq) return -1; 85 | if (a.seq > b.seq) return 1; 86 | return 0; 87 | }); 88 | 89 | /** 90 | * A timer that fires at the specified time, which can be null, meaning 91 | * that the alarm is not set. 92 | */ 93 | at(tAlarm : Cell) : Stream { 94 | let current : Event = null, 95 | cancelCurrent : () => void = null, 96 | active : boolean = false, 97 | tAl : number = null, 98 | sampled : boolean = false; 99 | const sAlarm = new StreamWithSend(null), 100 | updateTimer = () => { 101 | if (cancelCurrent !== null) { 102 | cancelCurrent(); 103 | this.eventQueue.remove(current); 104 | } 105 | cancelCurrent = null; 106 | current = null; 107 | if (active) { 108 | if (!sampled) { 109 | sampled = true; 110 | tAl = tAlarm.sampleNoTrans__(); 111 | } 112 | if (tAl !== null) { 113 | current = new Event(tAl, sAlarm); 114 | this.eventQueue.add(current); 115 | cancelCurrent = this.impl.setTimer(tAl, () => { 116 | // Correction to ensure the clock time appears to be >= the 117 | // alarm time. It can be a few milliseconds early, and 118 | // this breaks things otherwise, because it doesn't think 119 | // it's time to fire the alarm yet. 120 | this.tMinimum = Math.max(this.tMinimum, tAl); 121 | // Open and close a transaction to trigger queued 122 | // events to run. 123 | Transaction.run(() => {}); 124 | }); 125 | } 126 | } 127 | }; 128 | sAlarm.setVertex__(new Vertex("at", 0, [ 129 | new Source( 130 | tAlarm.getVertex__(), 131 | () => { 132 | active = true; 133 | sampled = false; 134 | Transaction.currentTransaction.prioritized(sAlarm.getVertex__(), updateTimer); 135 | const kill = tAlarm.getStream__().listen_(sAlarm.getVertex__(), (oAlarm : number) => { 136 | tAl = oAlarm; 137 | sampled = true; 138 | updateTimer(); 139 | }, false); 140 | return () => { 141 | active = false; 142 | updateTimer(); 143 | kill(); 144 | }; 145 | } 146 | ) 147 | ] 148 | )); 149 | return sAlarm; 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/lib/sodium/Transaction.ts: -------------------------------------------------------------------------------- 1 | import {Vertex} from './Vertex'; 2 | import * as Collections from 'typescript-collections'; 3 | 4 | export class Entry 5 | { 6 | constructor(rank: Vertex, action: () => void) 7 | { 8 | this.rank = rank; 9 | this.action = action; 10 | this.seq = Entry.nextSeq++; 11 | } 12 | 13 | private static nextSeq: number = 0; 14 | rank: Vertex; 15 | action: () => void; 16 | seq: number; 17 | 18 | toString(): string 19 | { 20 | return this.seq.toString(); 21 | } 22 | } 23 | 24 | export class Transaction 25 | { 26 | public static currentTransaction: Transaction = null; 27 | private static onStartHooks: (() => void)[] = []; 28 | private static runningOnStartHooks: boolean = false; 29 | 30 | constructor() {} 31 | 32 | inCallback: number = 0; 33 | private toRegen: boolean = false; 34 | 35 | requestRegen(): void 36 | { 37 | this.toRegen = true; 38 | } 39 | 40 | prioritizedQ: Collections.PriorityQueue = new Collections.PriorityQueue((a, b) => 41 | { 42 | // Note: Low priority numbers are treated as "greater" according to this 43 | // comparison, so that the lowest numbers are highest priority and go first. 44 | if (a.rank.rank < b.rank.rank) return 1; 45 | if (a.rank.rank > b.rank.rank) return -1; 46 | if (a.seq < b.seq) return 1; 47 | if (a.seq > b.seq) return -1; 48 | return 0; 49 | }); 50 | private entries: Collections.Set = new Collections.Set((a) => a.toString()); 51 | private sampleQ: Array<() => void> = []; 52 | private lastQ: Array<() => void> = []; 53 | private postQ: Array<() => void> = null; 54 | private static collectCyclesAtEnd: boolean = false; 55 | 56 | prioritized(target: Vertex, action: () => void): void 57 | { 58 | const e = new Entry(target, action); 59 | this.prioritizedQ.enqueue(e); 60 | this.entries.add(e); 61 | } 62 | 63 | sample(h: () => void): void 64 | { 65 | this.sampleQ.push(h); 66 | } 67 | 68 | last(h: () => void): void 69 | { 70 | this.lastQ.push(h); 71 | } 72 | 73 | public static _collectCyclesAtEnd(): void 74 | { 75 | Transaction.run(() => Transaction.collectCyclesAtEnd = true); 76 | } 77 | 78 | /** 79 | * Add an action to run after all last() actions. 80 | */ 81 | post(childIx: number, action: () => void): void 82 | { 83 | if (this.postQ == null) 84 | this.postQ = []; 85 | // If an entry exists already, combine the old one with the new one. 86 | while (this.postQ.length <= childIx) 87 | this.postQ.push(null); 88 | const existing = this.postQ[childIx], 89 | neu = 90 | existing === null ? action 91 | : () => 92 | { 93 | existing(); 94 | action(); 95 | }; 96 | this.postQ[childIx] = neu; 97 | } 98 | 99 | // If the priority queue has entries in it when we modify any of the nodes' 100 | // ranks, then we need to re-generate it to make sure it's up-to-date. 101 | private checkRegen(): void 102 | { 103 | if (this.toRegen) 104 | { 105 | this.toRegen = false; 106 | this.prioritizedQ.clear(); 107 | const es = this.entries.toArray(); 108 | for (let i: number = 0; i < es.length; i++) 109 | this.prioritizedQ.enqueue(es[i]); 110 | } 111 | } 112 | 113 | public isActive() : boolean 114 | { 115 | return Transaction.currentTransaction ? true : false; 116 | } 117 | 118 | close(): void 119 | { 120 | while(true) 121 | { 122 | while (true) 123 | { 124 | this.checkRegen(); 125 | if (this.prioritizedQ.isEmpty()) break; 126 | const e = this.prioritizedQ.dequeue(); 127 | this.entries.remove(e); 128 | e.action(); 129 | } 130 | 131 | const sq = this.sampleQ; 132 | this.sampleQ = []; 133 | for (let i = 0; i < sq.length; i++) 134 | sq[i](); 135 | 136 | if(this.prioritizedQ.isEmpty() && this.sampleQ.length < 1) break; 137 | } 138 | 139 | for (let i = 0; i < this.lastQ.length; i++) 140 | this.lastQ[i](); 141 | this.lastQ = []; 142 | if (this.postQ != null) 143 | { 144 | for (let i = 0; i < this.postQ.length; i++) 145 | { 146 | if (this.postQ[i] != null) 147 | { 148 | const parent = Transaction.currentTransaction; 149 | try 150 | { 151 | if (i > 0) 152 | { 153 | Transaction.currentTransaction = new Transaction(); 154 | try 155 | { 156 | this.postQ[i](); 157 | Transaction.currentTransaction.close(); 158 | } 159 | catch (err) 160 | { 161 | Transaction.currentTransaction.close(); 162 | throw err; 163 | } 164 | } 165 | else 166 | { 167 | Transaction.currentTransaction = null; 168 | this.postQ[i](); 169 | } 170 | Transaction.currentTransaction = parent; 171 | } 172 | catch (err) 173 | { 174 | Transaction.currentTransaction = parent; 175 | throw err; 176 | } 177 | } 178 | } 179 | this.postQ = null; 180 | } 181 | } 182 | 183 | /** 184 | * Add a runnable that will be executed whenever a transaction is started. 185 | * That runnable may start transactions itself, which will not cause the 186 | * hooks to be run recursively. 187 | * 188 | * The main use case of this is the implementation of a time/alarm system. 189 | */ 190 | static onStart(r: () => void): void 191 | { 192 | Transaction.onStartHooks.push(r); 193 | } 194 | 195 | public static run(f: () => A): A 196 | { 197 | const transWas: Transaction = Transaction.currentTransaction; 198 | if (transWas === null) 199 | { 200 | if (!Transaction.runningOnStartHooks) 201 | { 202 | Transaction.runningOnStartHooks = true; 203 | try 204 | { 205 | for (let i = 0; i < Transaction.onStartHooks.length; i++) 206 | Transaction.onStartHooks[i](); 207 | } 208 | finally 209 | { 210 | Transaction.runningOnStartHooks = false; 211 | } 212 | } 213 | Transaction.currentTransaction = new Transaction(); 214 | } 215 | try 216 | { 217 | const a: A = f(); 218 | if (transWas === null) 219 | { 220 | Transaction.currentTransaction.close(); 221 | Transaction.currentTransaction = null; 222 | if (Transaction.collectCyclesAtEnd) { 223 | Vertex.collectCycles(); 224 | Transaction.collectCyclesAtEnd = false; 225 | } 226 | } 227 | return a; 228 | } 229 | catch (err) 230 | { 231 | if (transWas === null) 232 | { 233 | Transaction.currentTransaction.close(); 234 | Transaction.currentTransaction = null; 235 | } 236 | throw err; 237 | } 238 | } 239 | } 240 | 241 | 242 | -------------------------------------------------------------------------------- /src/lib/sodium/Tuple2.ts: -------------------------------------------------------------------------------- 1 | export class Tuple2 { 2 | constructor(a : A, b : B) { 3 | this.a = a; 4 | this.b = b; 5 | } 6 | a : A; 7 | b : B; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/sodium/Unit.ts: -------------------------------------------------------------------------------- 1 | export class Unit { 2 | static UNIT : Unit = new Unit(); 3 | constructor() {} 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/sodium/Vertex.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from "./Transaction"; 2 | import { Set } from "typescript-collections"; 3 | 4 | let totalRegistrations : number = 0; 5 | export function getTotalRegistrations() : number { 6 | return totalRegistrations; 7 | } 8 | 9 | export class Source { 10 | // Note: 11 | // When register_ == null, a rank-independent source is constructed (a vertex which is just kept alive for the 12 | // lifetime of vertex that contains this source). 13 | // When register_ != null it is likely to be a rank-dependent source, but this will depend on the code inside register_. 14 | // 15 | // rank-independent souces DO NOT bump up the rank of the vertex containing those sources. 16 | // rank-depdendent sources DO bump up the rank of the vertex containing thoses sources when required. 17 | constructor( 18 | origin : Vertex, 19 | register_ : () => () => void 20 | ) { 21 | if (origin === null) 22 | throw new Error("null origin!"); 23 | this.origin = origin; 24 | this.register_ = register_; 25 | } 26 | origin : Vertex; 27 | private register_ : () => () => void; 28 | private registered : boolean = false; 29 | private deregister_ : () => void = null; 30 | 31 | register(target : Vertex) : void { 32 | if (!this.registered) { 33 | this.registered = true; 34 | if (this.register_ !== null) 35 | this.deregister_ = this.register_(); 36 | else { 37 | // Note: The use of Vertex.NULL here instead of "target" is not a bug, this is done to create a 38 | // rank-independent source. (see note at constructor for more details.). The origin vertex still gets 39 | // added target vertex's children for the memory management algorithm. 40 | this.origin.increment(Vertex.NULL); 41 | target.childrn.push(this.origin); 42 | this.deregister_ = () => { 43 | this.origin.decrement(Vertex.NULL); 44 | for (let i = target.childrn.length-1; i >= 0; --i) { 45 | if (target.childrn[i] === this.origin) { 46 | target.childrn.splice(i, 1); 47 | break; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | deregister(target : Vertex) : void { 55 | if (this.registered) { 56 | this.registered = false; 57 | if (this.deregister_ !== null) 58 | this.deregister_(); 59 | } 60 | } 61 | } 62 | 63 | export enum Color { black, gray, white, purple }; 64 | let roots : Vertex[] = []; 65 | let nextID : number = 0; 66 | let verbose : boolean = false; 67 | 68 | export function setVerbose(v : boolean) : void { verbose = v; } 69 | 70 | export function describeAll(v : Vertex, visited : Set) 71 | { 72 | if (visited.contains(v.id)) return; 73 | console.log(v.descr()); 74 | visited.add(v.id); 75 | let chs = v.children(); 76 | for (let i = 0; i < chs.length; i++) 77 | describeAll(chs[i], visited); 78 | } 79 | 80 | export class Vertex { 81 | static NULL : Vertex = new Vertex("user", 1e12, []); 82 | static collectingCycles : boolean = false; 83 | static toBeFreedList : Vertex[] = []; 84 | id : number; 85 | 86 | constructor(name : string, rank : number, sources : Source[]) { 87 | this.name = name; 88 | this.rank = rank; 89 | this.sources = sources; 90 | this.id = nextID++; 91 | } 92 | name : string; 93 | rank : number; 94 | sources : Source[]; 95 | targets : Vertex[] = []; 96 | childrn : Vertex[] = []; 97 | refCount() : number { return this.targets.length; }; 98 | visited : boolean = false; 99 | register(target : Vertex) : boolean { 100 | return this.increment(target); 101 | } 102 | deregister(target : Vertex) : void { 103 | if (verbose) 104 | console.log("deregister "+this.descr()+" => "+target.descr()); 105 | this.decrement(target); 106 | Transaction._collectCyclesAtEnd(); 107 | } 108 | private incRefCount(target : Vertex) : boolean { 109 | let anyChanged : boolean = false; 110 | if (this.refCount() == 0) { 111 | for (let i = 0; i < this.sources.length; i++) 112 | this.sources[i].register(this); 113 | } 114 | this.targets.push(target); 115 | target.childrn.push(this); 116 | if (target.ensureBiggerThan(this.rank)) 117 | anyChanged = true; 118 | totalRegistrations++; 119 | return anyChanged; 120 | } 121 | 122 | private decRefCount(target : Vertex) : void { 123 | if (verbose) 124 | console.log("DEC "+this.descr()); 125 | let matched = false; 126 | for (let i = target.childrn.length-1; i >= 0; i--) 127 | if (target.childrn[i] === this) { 128 | target.childrn.splice(i, 1); 129 | break; 130 | } 131 | for (let i = 0; i < this.targets.length; i++) 132 | if (this.targets[i] === target) { 133 | this.targets.splice(i, 1); 134 | matched = true; 135 | break; 136 | } 137 | if (matched) { 138 | if (this.refCount() == 0) { 139 | for (let i = 0; i < this.sources.length; i++) 140 | this.sources[i].deregister(this); 141 | } 142 | totalRegistrations--; 143 | } 144 | } 145 | 146 | addSource(src : Source) : void { 147 | this.sources.push(src); 148 | if (this.refCount() > 0) 149 | src.register(this); 150 | } 151 | 152 | private ensureBiggerThan(limit : number) : boolean { 153 | if (this.visited) { 154 | // Undoing cycle detection for now until TimerSystem.ts ranks are checked. 155 | //throw new Error("Vertex cycle detected."); 156 | return false; 157 | } 158 | if (this.rank > limit) 159 | return false; 160 | 161 | this.visited = true; 162 | this.rank = limit + 1; 163 | for (let i = 0; i < this.targets.length; i++) 164 | this.targets[i].ensureBiggerThan(this.rank); 165 | this.visited = false; 166 | return true; 167 | } 168 | 169 | descr() : string { 170 | let colStr : string = null; 171 | switch (this.color) { 172 | case Color.black: colStr = "black"; break; 173 | case Color.gray: colStr = "gray"; break; 174 | case Color.white: colStr = "white"; break; 175 | case Color.purple: colStr = "purple"; break; 176 | } 177 | let str = this.id+" "+this.name+" ["+this.refCount()+"/"+this.refCountAdj+"] "+colStr+" ->"; 178 | let chs = this.children(); 179 | for (let i = 0; i < chs.length; i++) { 180 | str = str + " " + chs[i].id; 181 | } 182 | return str; 183 | } 184 | 185 | // -------------------------------------------------------- 186 | // Synchronous Cycle Collection algorithm presented in "Concurrent 187 | // Cycle Collection in Reference Counted Systems" by David F. Bacon 188 | // and V.T. Rajan. 189 | 190 | color : Color = Color.black; 191 | buffered : boolean = false; 192 | refCountAdj : number = 0; 193 | 194 | children() : Vertex[] { return this.childrn; } 195 | 196 | increment(referrer : Vertex) : boolean { 197 | return this.incRefCount(referrer); 198 | } 199 | 200 | decrement(referrer : Vertex) : void { 201 | this.decRefCount(referrer); 202 | if (this.refCount() == 0) 203 | this.release(); 204 | else 205 | this.possibleRoots(); 206 | } 207 | 208 | release() : void { 209 | this.color = Color.black; 210 | if (!this.buffered) 211 | this.free(); 212 | } 213 | 214 | free() : void { 215 | while (this.targets.length > 0) 216 | this.decRefCount(this.targets[0]); 217 | } 218 | 219 | possibleRoots() : void { 220 | if (this.color != Color.purple) { 221 | this.color = Color.purple; 222 | if (!this.buffered) { 223 | this.buffered = true; 224 | roots.push(this); 225 | } 226 | } 227 | } 228 | 229 | static collectCycles() : void { 230 | if (Vertex.collectingCycles) { 231 | return; 232 | } 233 | try { 234 | Vertex.collectingCycles = true; 235 | Vertex.markRoots(); 236 | Vertex.scanRoots(); 237 | Vertex.collectRoots(); 238 | for (let i = Vertex.toBeFreedList.length-1; i >= 0; --i) { 239 | let vertex = Vertex.toBeFreedList.splice(i, 1)[0]; 240 | vertex.free(); 241 | } 242 | } finally { 243 | Vertex.collectingCycles = false; 244 | } 245 | } 246 | 247 | static markRoots() : void { 248 | const newRoots : Vertex[] = []; 249 | // check refCountAdj was restored to zero before mark roots 250 | if (verbose) { 251 | let stack: Vertex[] = roots.slice(0); 252 | let visited: Set = new Set(); 253 | while (stack.length != 0) { 254 | let vertex = stack.pop(); 255 | if (visited.contains(vertex.id)) { 256 | continue; 257 | } 258 | visited.add(vertex.id); 259 | if (vertex.refCountAdj != 0) { 260 | console.log("markRoots(): reachable refCountAdj was not reset to zero: " + vertex.descr()); 261 | } 262 | for (let i = 0; i < vertex.childrn.length; ++i) { 263 | let child = vertex.childrn[i]; 264 | stack.push(child); 265 | } 266 | } 267 | } 268 | // 269 | for (let i = 0; i < roots.length; i++) { 270 | if (verbose) 271 | console.log("markRoots "+roots[i].descr()); // ### 272 | if (roots[i].color == Color.purple) { 273 | roots[i].markGray(); 274 | newRoots.push(roots[i]); 275 | } 276 | else { 277 | roots[i].buffered = false; 278 | if (roots[i].color == Color.black && roots[i].refCount() == 0) 279 | Vertex.toBeFreedList.push(roots[i]); 280 | } 281 | } 282 | roots = newRoots; 283 | } 284 | 285 | static scanRoots() : void { 286 | for (let i = 0; i < roots.length; i++) 287 | roots[i].scan(); 288 | } 289 | 290 | static collectRoots() : void { 291 | for (let i = 0; i < roots.length; i++) { 292 | roots[i].buffered = false; 293 | roots[i].collectWhite(); 294 | } 295 | if (verbose) { // double check adjRefCount is zero for all vertices reachable by roots 296 | let stack: Vertex[] = roots.slice(0); 297 | let visited: Set = new Set(); 298 | while (stack.length != 0) { 299 | let vertex = stack.pop(); 300 | if (visited.contains(vertex.id)) { 301 | continue; 302 | } 303 | visited.add(vertex.id); 304 | if (vertex.refCountAdj != 0) { 305 | console.log("collectRoots(): reachable refCountAdj was not reset to zero: " + vertex.descr()); 306 | } 307 | for (let i = 0; i < vertex.childrn.length; ++i) { 308 | let child = vertex.childrn[i]; 309 | stack.push(child); 310 | } 311 | } 312 | } 313 | roots = []; 314 | } 315 | 316 | markGray() : void { 317 | if (this.color != Color.gray) { 318 | this.color = Color.gray; 319 | let chs = this.children(); 320 | for (let i = 0; i < chs.length; i++) { 321 | chs[i].refCountAdj--; 322 | if (verbose) 323 | console.log("markGray "+this.descr()); 324 | chs[i].markGray(); 325 | } 326 | } 327 | } 328 | 329 | scan() : void { 330 | if (verbose) 331 | console.log("scan "+this.descr()); 332 | if (this.color == Color.gray) { 333 | if (this.refCount()+this.refCountAdj > 0) 334 | this.scanBlack(); 335 | else { 336 | this.color = Color.white; 337 | if (verbose) 338 | console.log("scan WHITE "+this.descr()); 339 | let chs = this.children(); 340 | for (let i = 0; i < chs.length; i++) 341 | chs[i].scan(); 342 | } 343 | } 344 | } 345 | 346 | scanBlack() : void { 347 | this.refCountAdj = 0; 348 | this.color = Color.black; 349 | let chs = this.children(); 350 | for (let i = 0; i < chs.length; i++) { 351 | if (verbose) 352 | console.log("scanBlack "+this.descr()); 353 | if (chs[i].color != Color.black) 354 | chs[i].scanBlack(); 355 | } 356 | } 357 | 358 | collectWhite() : void { 359 | if (this.color == Color.white && !this.buffered) { 360 | if (verbose) 361 | console.log("collectWhite "+this.descr()); 362 | this.color = Color.black; 363 | this.refCountAdj = 0; 364 | let chs = this.children(); 365 | for (let i = 0; i < chs.length; i++) 366 | chs[i].collectWhite(); 367 | Vertex.toBeFreedList.push(this); 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/tests/integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sodium-typescript-example", 3 | "version": "0.0.1-alpha", 4 | "description": "Sodium Typescript Example", 5 | "scripts": { 6 | "clean": "rimraf ./dist", 7 | "dev": "cross-env NODE_ENV=dev webpack-dev-server --progress --open --config webpack.dev.js", 8 | "dev:auto-reload": "cross-env NODE_ENV=dev-auto-reload webpack-dev-server --progress --open --config webpack.dev.js", 9 | "build": "cross-env NODE_ENV=production webpack --progress --config webpack.prod.js && cp -R ./src/webpage/static ./dist/", 10 | "dist:server": "http-server ./dist -o" 11 | }, 12 | "jest": { 13 | "transform": { 14 | "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" 15 | }, 16 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 17 | "moduleFileExtensions": [ 18 | "ts", 19 | "tsx", 20 | "js", 21 | "jsx", 22 | "json" 23 | ] 24 | }, 25 | "author": "Stephen Blackheath", 26 | "license": "BSD-3-Clause", 27 | "devDependencies": { 28 | "@types/jest": "23.1.5", 29 | "@types/node": "10.5.2", 30 | "@types/react": "16.4.6", 31 | "@types/react-dom": "16.0.6", 32 | "awesome-typescript-loader": "5.2.0", 33 | "chokidar": "2.0.4", 34 | "clean-webpack-plugin": "0.1.19", 35 | "cors": "^2.8.4", 36 | "cross-env": "5.2.0", 37 | "css-loader": "1.0.0", 38 | "express": "^4.21.2", 39 | "glob": "7.1.2", 40 | "html-loader": "0.5.5", 41 | "html-webpack-plugin": "3.2.0", 42 | "install": "^0.12.1", 43 | "jest": "23.4.0", 44 | "npm": "^6.1.0", 45 | "npm-run-all": "4.1.3", 46 | "null-loader": "0.1.1", 47 | "raw-loader": "0.5.1", 48 | "rimraf": "2.6.2", 49 | "serve-index": "^1.9.1", 50 | "shelljs": "0.8.5", 51 | "source-map-loader": "0.2.3", 52 | "style-loader": "0.21.0", 53 | "ts-jest": "23.0.0", 54 | "ts-node": "7.0.0", 55 | "tsconfig-paths": "3.4.2", 56 | "typescript": "2.9.2", 57 | "uglify-js": "3.4.4", 58 | "uglifyjs-webpack-plugin": "1.2.7", 59 | "webpack": "4.16.0", 60 | "webpack-cli": "^3.0.8", 61 | "webpack-dev-server": "3.1.11", 62 | "webpack-merge": "4.1.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/tests/integration/src/app/App-Main.ts: -------------------------------------------------------------------------------- 1 | import {Transaction, Stream, CellLoop, MillisecondsTimerSystem} from "lib/Lib"; 2 | import {makeDomWriter} from "./output/DomWriter"; 3 | 4 | const domWriter = makeDomWriter(document.getElementById("app")); 5 | 6 | const periodic = (sys,period) => Transaction.run(() => { 7 | const oAlarm = new CellLoop(); 8 | const sAlarm = sys.at(oAlarm); 9 | 10 | oAlarm.loop( 11 | sAlarm 12 | .map(t => t + period) 13 | .hold(sys.time.sample() + period)); 14 | return oAlarm; 15 | 16 | }); 17 | 18 | periodic(new MillisecondsTimerSystem(), 1).listen(domWriter.update); 19 | //periodic(new MillisecondsTimerSystem(), 1).listen(console.log); -------------------------------------------------------------------------------- /src/tests/integration/src/app/output/DomWriter.ts: -------------------------------------------------------------------------------- 1 | const renderTimestamp = (ts:number):string => { 2 | const padLeft = n => v => String(Array(n).fill("0") + v.toString()).slice(-n); 3 | 4 | const format2 = padLeft(2); 5 | const format3 = padLeft(3); 6 | const output = document.getElementById("output"); 7 | 8 | const now = new Date(ts); 9 | 10 | const hr = format2(now.getHours()); 11 | const min = format2(now.getMinutes()); 12 | const sec = format2(now.getSeconds()); 13 | const ms = format3(now.getMilliseconds()); 14 | 15 | return `${hr}:${min}:${sec}:${ms}`; 16 | } 17 | 18 | export const makeDomWriter = (appElement) => { 19 | const appContainer = document.createElement("div"); 20 | appElement.appendChild(appContainer); 21 | 22 | const appLabel = document.createElement("h1"); 23 | appLabel.setAttribute("style", "text-align: center; width: 100%"); 24 | appLabel.innerText = "Sodium Example Clock"; 25 | appContainer.appendChild(appLabel); 26 | 27 | const tsText = document.createTextNode(""); 28 | const tsContainer = document.createElement("h1"); 29 | tsContainer.setAttribute("style", "text-align: center; width: 100%"); 30 | tsContainer.appendChild(tsText); 31 | appContainer.appendChild(tsContainer); 32 | 33 | 34 | return { 35 | update: (ts:number) => tsText.textContent = renderTimestamp(ts) 36 | } 37 | } -------------------------------------------------------------------------------- /src/tests/integration/src/webpage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Sodium Example 10 | 11 | 12 |

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/tests/integration/src/webpage/static/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Core 3 | */ 4 | html,body { 5 | width: 100%; 6 | height: 100%; 7 | padding:0; 8 | margin:0; 9 | font-family: Verdana, Geneva, Tahoma, sans-serif 10 | } -------------------------------------------------------------------------------- /src/tests/integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "baseUrl": ".", 6 | "paths": { 7 | "*": [ 8 | "node_modules/@types/*", 9 | "*" 10 | ], 11 | "lib/*": ["../../lib/*"], 12 | }, 13 | "outDir": "dist/tsc", 14 | "noImplicitAny": false, 15 | "preserveConstEnums": true, 16 | "sourceMap": true, 17 | "target": "es5", 18 | "jsx": "react", 19 | "lib": [ 20 | "es6", 21 | "es5", 22 | "dom" 23 | ] 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.tsx" 28 | ], 29 | 30 | "exclude": [ 31 | "node_modules", 32 | "dist", 33 | "build", 34 | "coverage" 35 | ] 36 | } -------------------------------------------------------------------------------- /src/tests/integration/wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | var path = require('path'); 3 | 4 | return { 5 | files: [ 6 | 'src/**/*.ts?(x)', 7 | 'src/**/*.snap', 8 | '!src/**/*.spec.ts?(x)' 9 | ], 10 | tests: [ 11 | 'src/**/*.spec.ts?(x)' 12 | ], 13 | 14 | env: { 15 | type: 'node', 16 | runner: 'node' 17 | }, 18 | 19 | testFramework: 'jest', 20 | 21 | setup: function (wallaby) { 22 | const jestConfig = require('./package.json').jest; 23 | jestConfig.modulePaths = jestConfig.modulePaths.map(p => p.replace('', wallaby.projectCacheDir)); 24 | wallaby.testFramework.configure(jestConfig); 25 | }, 26 | 27 | debug: true 28 | }; 29 | }; -------------------------------------------------------------------------------- /src/tests/integration/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | app: path.resolve('./src/app/App-Main.ts') 9 | }, 10 | output: { 11 | path: path.resolve("./dist"), 12 | filename: "[name].bundle.js", 13 | sourceMapFilename: "[name].bundle.map", 14 | }, 15 | 16 | module: { 17 | rules: [ 18 | { 19 | enforce: "pre", 20 | test: /\.tsx?$/, 21 | exclude: ["node_modules"], 22 | use: ["awesome-typescript-loader", "source-map-loader"] 23 | }, 24 | { test: /\.html$/, loader: "html-loader" }, 25 | { test: /\.css$/, loaders: ["style-loader", "css-loader"] }, 26 | { test: /\.glsl$/, loader: 'raw-loader'} 27 | ] 28 | }, 29 | resolve: { 30 | extensions: [".tsx", ".ts", ".js"], 31 | alias: { 32 | "lib": path.resolve(__dirname, '../../lib/') 33 | } 34 | }, 35 | plugins: [ 36 | 37 | new CleanWebpackPlugin(['dist']), 38 | 39 | new HtmlWebpackPlugin({ 40 | template: path.resolve(__dirname, './src/webpage/index.html'), 41 | hash: true, 42 | }), 43 | 44 | new webpack.DefinePlugin({ 45 | 'process.env': { 46 | 'NODE_ENV': JSON.stringify(process.env['NODE_ENV']) 47 | } 48 | }), 49 | ], 50 | 51 | 52 | }; -------------------------------------------------------------------------------- /src/tests/integration/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | console.log(process.env["NODE_ENV"]); 7 | 8 | const rules = (process.env["NODE_ENV"] === "dev-auto-reload") 9 | ? [] 10 | : [{test: path.resolve(__dirname, 'node_modules/webpack-dev-server/client'), loader: "null-loader"}] 11 | 12 | module.exports = merge(common, { 13 | devtool: "inline-source-map", 14 | module: { 15 | rules: rules 16 | }, 17 | devServer: { 18 | //contentBase: path.join(__dirname, "dist/"), 19 | contentBase: path.resolve(__dirname, './src/webpage'), 20 | compress: true, 21 | port: 3000, 22 | headers: { "Access-Control-Allow-Origin": "*" } 23 | }, 24 | plugins: [ 25 | 26 | ] 27 | }); 28 | -------------------------------------------------------------------------------- /src/tests/integration/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 6 | 7 | 8 | module.exports = merge(common, { 9 | devtool: "source-map", 10 | plugins: [ 11 | 12 | new UglifyJSPlugin({ 13 | sourceMap: true 14 | }), 15 | 16 | ] 17 | }); -------------------------------------------------------------------------------- /src/tests/test-utils/Sanctuary.ts: -------------------------------------------------------------------------------- 1 | import {create, env} from 'sanctuary'; 2 | 3 | 4 | const checkTypes = false; //process.env.BUILD_TYPE !== 'build'; 5 | export const S = create({checkTypes, env}); -------------------------------------------------------------------------------- /src/tests/test-utils/Sequence.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | lambda1, 4 | lambda2, 5 | StreamSink, 6 | StreamLoop, 7 | CellSink, 8 | Transaction, 9 | Tuple2, 10 | Operational, 11 | Cell, 12 | CellLoop, 13 | getTotalRegistrations 14 | } from '../../lib/Lib'; 15 | 16 | import { S } from "../test-utils/Sanctuary"; 17 | 18 | type TestSequencer = (aCells:Array>) => Cell>; 19 | 20 | export const testSequence = (seq:TestSequencer) => (done) => { 21 | const aStreams = [ 22 | new StreamSink(), 23 | new StreamSink(), 24 | new StreamSink() 25 | ] 26 | 27 | const aCells: Array> = aStreams.map(stream => stream.hold("")); 28 | 29 | const cArrays: Cell> = seq(aCells); 30 | 31 | let idx = 0; 32 | 33 | 34 | const kill = cArrays.listen((sArr: Array) => { 35 | const res = sArr 36 | .filter(val => val.length) 37 | .join(" "); 38 | 39 | let target: string; 40 | 41 | switch (idx++) { 42 | case 0: target = ""; break; 43 | case 1: target = "Hello"; break; 44 | case 2: target = "Hello World"; break; 45 | case 3: target = "Do World"; break; 46 | case 4: target = "Do"; break; 47 | case 5: target = "Do Good"; break; 48 | case 6: target = "Do Good !"; break; 49 | } 50 | 51 | expect(res).toBe(target); 52 | if (idx === 7) { 53 | done(); 54 | } 55 | 56 | 57 | }); 58 | 59 | 60 | aStreams[0].send("Hello"); 61 | aStreams[1].send("World"); 62 | aStreams[0].send("Do"); 63 | aStreams[1].send(""); 64 | aStreams[1].send("Good"); 65 | aStreams[2].send("!"); 66 | 67 | kill(); 68 | } -------------------------------------------------------------------------------- /src/tests/unit/AccumCell.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | lambda1, 4 | StreamSink, 5 | StreamLoop, 6 | CellSink, 7 | Transaction, 8 | Tuple2, 9 | Operational, 10 | Cell, 11 | CellLoop, 12 | getTotalRegistrations 13 | } from '../../lib/Lib'; 14 | 15 | afterEach(() => { 16 | if (getTotalRegistrations() != 0) { 17 | throw new Error('listeners were not deregistered'); 18 | } 19 | }); 20 | 21 | test('accum() over a Cell via switchC', (done) => { 22 | const out = new Array(); 23 | const sHello = new StreamSink(); 24 | const sUpper = new StreamSink(); 25 | const cHello = sHello.accum("", (val, acc) => acc + val); 26 | const cFinal = Cell.switchC(sUpper.accum(cHello, (flag, acc) => 27 | acc.map((str:string) => flag ? str.toUpperCase() : str.toLowerCase()) 28 | )); 29 | 30 | const kill = cFinal.listen(a => { 31 | out.push(a); 32 | if (out.length === 6) { 33 | done(); 34 | } 35 | }); 36 | 37 | sHello.send("h"); 38 | sUpper.send(true); 39 | sHello.send("e"); 40 | sHello.send("l"); 41 | sUpper.send(false); 42 | sHello.send("l"); 43 | sHello.send("o"); 44 | kill(); 45 | 46 | expect(out).toEqual(["", "h", "H", "HE", "HEL", "hel", "hell", "hello"]); 47 | }); 48 | 49 | enum FlushTarget { 50 | HELLO, 51 | WORLD, 52 | EMPTY 53 | } 54 | test('accum over multiple cells via snapshot/hold', (done) => { 55 | const sHello = new StreamSink(); 56 | const sWorld = new StreamSink(); 57 | const sFlush = new StreamSink(); 58 | 59 | const out = new Array(); 60 | 61 | const cFinal = Transaction.run(() => { 62 | const cLoop = new CellLoop(); 63 | 64 | cLoop.loop( 65 | sFlush.snapshot4(cLoop, sHello.hold(""), sWorld.hold(""), (evt, total, hello, world) => 66 | evt === FlushTarget.HELLO 67 | ? total += hello 68 | : evt === FlushTarget.WORLD 69 | ? total += world 70 | : total += " " 71 | ) 72 | .hold("") 73 | ) 74 | 75 | return cLoop; 76 | }) 77 | const kill = cFinal.listen(a => { 78 | out.push(a); 79 | if (out.length === 12) { 80 | done(); 81 | } 82 | }); 83 | 84 | sHello.send("h"); 85 | sFlush.send(FlushTarget.HELLO); 86 | sHello.send("e"); 87 | sFlush.send(FlushTarget.HELLO); 88 | sHello.send("l"); 89 | sFlush.send(FlushTarget.HELLO); 90 | sHello.send("l"); 91 | sFlush.send(FlushTarget.HELLO); 92 | sHello.send("o"); 93 | sFlush.send(FlushTarget.HELLO); 94 | 95 | sFlush.send(FlushTarget.EMPTY); 96 | 97 | sWorld.send("w"); 98 | sFlush.send(FlushTarget.WORLD); 99 | sWorld.send("o"); 100 | sFlush.send(FlushTarget.WORLD); 101 | sWorld.send("r"); 102 | sFlush.send(FlushTarget.WORLD); 103 | sWorld.send("l"); 104 | sFlush.send(FlushTarget.WORLD); 105 | sWorld.send("d"); 106 | sFlush.send(FlushTarget.WORLD); 107 | kill(); 108 | 109 | expect(out).toEqual(["", "h", "he", "hel", "hell", "hello", "hello ", "hello w", "hello wo", "hello wor", "hello worl", "hello world"]); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /src/tests/unit/Cell.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | lambda1, 3 | Cell, 4 | CellSink, 5 | Stream, 6 | StreamSink, 7 | Tuple2, 8 | getTotalRegistrations, 9 | Transaction 10 | } from '../../lib/Lib'; 11 | 12 | afterEach(() => { 13 | if (getTotalRegistrations() != 0) { 14 | throw new Error('listeners were not deregistered'); 15 | } 16 | }); 17 | 18 | test('should test constantCell', (done) => { 19 | 20 | const c = new Cell(12); 21 | const out: number[] = []; 22 | 23 | const kill = c.listen(a => { 24 | out.push(a); 25 | done(); 26 | }); 27 | 28 | 29 | expect([12]).toEqual(out); 30 | kill(); 31 | }); 32 | 33 | test('cellLiftArray', () => { 34 | const out: number[][] = []; 35 | const ss1 = new StreamSink(); 36 | const cs = new CellSink(1); 37 | const c1 = ss1.accum(0, (a,b) => a + b); 38 | const c2 = ss1.accum(1, (a,b) => a * b); 39 | const c3 = ss1.accum(0, (a,b) => a - b); 40 | const c = 41 | Cell.switchC( 42 | cs 43 | .map(lambda1(a => { 44 | switch (a) { 45 | default: 46 | case 1: 47 | return [c1,c2]; 48 | case 2: 49 | return [c2,c3]; 50 | case 3: 51 | return [c3,c1]; 52 | } 53 | }, [c1,c2,c3])) 54 | .map(cas => Cell.liftArray(cas)) 55 | ); 56 | let kill = c.listen(x => out.push(x)); 57 | ss1.send(1); 58 | ss1.send(2); 59 | ss1.send(3); 60 | ss1.send(4); 61 | cs.send(2); 62 | kill(); 63 | expect([[0,1],[1,1],[3,2],[6,6],[10,24],[24,2]]).toEqual(out); 64 | }); 65 | 66 | test('cellTracking', () => { 67 | const out: number[] = []; 68 | class A { 69 | c1: Cell; 70 | c2: Cell; 71 | constructor(c1: Cell, c2: Cell) { 72 | this.c1 = c1; 73 | this.c2 = c2; 74 | } 75 | } 76 | const ss = new StreamSink>(); 77 | const ss1 = new StreamSink(); 78 | const ss2 = new StreamSink(); 79 | let s1 = ss1.collect(0, (a,b) => new Tuple2(a + b, a + b)); 80 | let s2 = ss2.collect(1, (a,b) => new Tuple2(a * b, a * b)); 81 | let ca = 82 | ss 83 | .map(s => new A(s.accum(0, (a,b) => a + b), s.accum(1, (a,b) => a * b))) 84 | .hold(new A(new Cell(9), new Cell(9))) 85 | .tracking(a => [a.c1, a.c2]); 86 | let c1 = Cell.switchC(ca.map(a => a.c1)); 87 | let c2 = Cell.switchC(ca.map(a => a.c2)); 88 | let c3 = c1.lift(c2, (a, b) => a - b); 89 | let kill = c3.listen(a => out.push(a)); 90 | ss.send(s1); 91 | ss1.send(1); 92 | ss2.send(2); 93 | ss1.send(3); 94 | ss2.send(4); 95 | ss.send(s2); 96 | ss1.send(1); 97 | ss2.send(2); 98 | ss1.send(3); 99 | ss2.send(4); 100 | kill(); 101 | expect([0, -1, 0, 1, -1, 0, -6]).toEqual(out); 102 | }); 103 | 104 | test('cell lift work load', done => { 105 | let lines = [ 106 | "Work it harder", 107 | "Make it better", 108 | "Do it faster", 109 | "Makes us stronger" 110 | ]; 111 | let idx = 0; 112 | let c1 = new CellSink(0); 113 | let c2 = new CellSink(0); 114 | let c3 = new CellSink(0); 115 | let c4 = new CellSink(0); 116 | let out: string[] = []; 117 | let c = c1.lift4(c2, c3, c4, (x1, x2, x3, x4) => { 118 | out.push(lines[idx]); 119 | idx = (idx + 1) % lines.length; 120 | return x1 + x2 + x3 + x4; 121 | }); 122 | let kill = c.listen(() => {}); 123 | Transaction.run(() => { 124 | c1.send(1); 125 | c2.send(2); 126 | c3.send(3); 127 | c4.send(4); 128 | }); 129 | kill(); 130 | expect(out).toEqual(["Work it harder", "Make it better"]); 131 | done(); 132 | }); 133 | -------------------------------------------------------------------------------- /src/tests/unit/CellLoop.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | CellLoop, 4 | Cell, 5 | Operational, 6 | Transaction, 7 | StreamSink, 8 | Unit, 9 | CellSink, 10 | getTotalRegistrations 11 | } from '../../lib/Lib'; 12 | 13 | afterEach(() => { 14 | if (getTotalRegistrations() != 0) { 15 | throw new Error('listeners were not deregistered'); 16 | } 17 | }); 18 | 19 | test('should test loopValueSnapshot', (done) => { 20 | const out: string[] = []; 21 | const kill = Transaction.run(() => { 22 | const a = new Cell("lettuce"); 23 | const b = new CellLoop(); 24 | const eSnap = Operational.value(a).snapshot(b, (aa, bb) => aa + " " + bb); 25 | b.loop(new Cell("cheese")); 26 | return eSnap.listen(x => { 27 | out.push(x); 28 | done(); 29 | }); 30 | }); 31 | 32 | kill(); 33 | 34 | expect(["lettuce cheese"]).toEqual(out); 35 | }); 36 | 37 | test('should test loopValueHold', (done) => { 38 | const out: string[] = []; 39 | const value = Transaction.run(() => { 40 | const a = new CellLoop(); 41 | const value_ = Operational.value(a).hold("onion"); 42 | a.loop(new Cell("cheese")); 43 | return value_; 44 | }), 45 | sTick = new StreamSink(), 46 | kill = sTick.snapshot1(value).listen(x => { 47 | out.push(x); 48 | done(); 49 | }); 50 | 51 | sTick.send(Unit.UNIT); 52 | kill(); 53 | 54 | expect(["cheese"]).toEqual(out); 55 | }); 56 | 57 | test('should test liftLoop', (done) => { 58 | const out: string[] = []; 59 | const b = new CellSink("kettle"); 60 | const c = Transaction.run(() => { 61 | const a = new CellLoop(); 62 | const c_ = a.lift(b, (aa, bb) => aa + " " + bb); 63 | a.loop(new Cell("tea")); 64 | return c_; 65 | }); 66 | const kill = c.listen(x => { 67 | out.push(x); 68 | if (out.length === 2) { 69 | done(); 70 | } 71 | }); 72 | 73 | b.send("caddy"); 74 | kill(); 75 | 76 | expect(["tea kettle", "tea caddy"]).toEqual(out); 77 | }); 78 | -------------------------------------------------------------------------------- /src/tests/unit/CellSink.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Cell, 4 | StreamSink, 5 | CellSink, 6 | Transaction, 7 | Tuple2, 8 | getTotalRegistrations 9 | } from '../../lib/Lib'; 10 | 11 | afterEach(() => { 12 | if (getTotalRegistrations() != 0) { 13 | throw new Error('listeners were not deregistered'); 14 | } 15 | }); 16 | 17 | test('should test snapshot', (done) => { 18 | const c = new CellSink(0), 19 | s = new StreamSink(), 20 | out: string[] = [], 21 | kill = s.snapshot(c, (x, y) => x + " " + y) 22 | .listen(a => { 23 | out.push(a); 24 | if (out.length === 3) { 25 | done(); 26 | } 27 | }); 28 | 29 | s.send(100); 30 | c.send(2); 31 | s.send(200); 32 | c.send(9); 33 | c.send(1); 34 | s.send(300); 35 | kill(); 36 | 37 | expect(["100 0", "200 2", "300 1"]).toEqual(out); 38 | }); 39 | 40 | test('should test values', (done) => { 41 | const c = new CellSink(9), 42 | out: number[] = [], 43 | kill = c.listen(a => { 44 | out.push(a); 45 | if (out.length === 3) { 46 | done(); 47 | } 48 | }); 49 | 50 | c.send(2); 51 | c.send(7); 52 | kill(); 53 | 54 | expect([9, 2, 7]).toEqual(out); 55 | }); 56 | 57 | test("should test mapC", (done) => { 58 | const c = new CellSink(6), 59 | out: string[] = [], 60 | kill = c.map(a => "" + a) 61 | .listen(a => { 62 | out.push(a); 63 | if (out.length === 2) { 64 | done(); 65 | } 66 | }); 67 | c.send(8); 68 | kill(); 69 | 70 | expect(["6", "8"]).toEqual(out); 71 | }); 72 | 73 | test("should throw an error on mapCLateListen", () => { 74 | const c = new CellSink(6), 75 | out: string[] = [], 76 | cm = c.map(a => "" + a); 77 | 78 | try { 79 | c.send(2); 80 | const kill = cm.listen(a => out.push(a)); 81 | c.send(8); 82 | kill(); 83 | } catch(e) { 84 | 85 | expect(e.message).toBe('send() was invoked before listeners were registered'); 86 | } 87 | 88 | //assertEquals(["2", "8"], out); 89 | }); 90 | 91 | test("should test apply", (done) => { 92 | const cf = new CellSink<(a: number) => string>(a => "1 " + a), 93 | ca = new CellSink(5), 94 | out: string[] = [], 95 | kill = Cell.apply(cf, ca).listen(a => { 96 | out.push(a); 97 | if (out.length === 3) { 98 | done(); 99 | } 100 | }); 101 | 102 | cf.send(a => "12 " + a); 103 | ca.send(6); 104 | kill(); 105 | 106 | expect(["1 5", "12 5", "12 6"]).toEqual(out); 107 | }); 108 | 109 | test("should test lift", (done) => { 110 | const a = new CellSink(1), 111 | b = new CellSink(5), 112 | out: string[] = [], 113 | kill = a.lift(b, (aa, bb) => aa + " " + bb) 114 | .listen(a => { 115 | out.push(a); 116 | if (out.length === 3) { 117 | done(); 118 | } 119 | }); 120 | a.send(12); 121 | b.send(6); 122 | kill(); 123 | 124 | expect(["1 5", "12 5", "12 6"]).toEqual(out); 125 | }); 126 | 127 | test("should test liftGlitch", (done) => { 128 | const a = new CellSink(1), 129 | a3 = a.map(x => x * 3), 130 | a5 = a.map(x => x * 5), 131 | b = a3.lift(a5, (x, y) => x + " " + y), 132 | out: string[] = [], 133 | kill = b.listen(x => { 134 | out.push(x); 135 | if (out.length === 2) { 136 | done(); 137 | } 138 | 139 | }); 140 | a.send(2); 141 | kill(); 142 | 143 | expect(["3 5", "6 10"]).toEqual(out); 144 | }); 145 | 146 | test("should test liftFromSimultaneous", (done) => { 147 | const t = Transaction.run(() => { 148 | const b1 = new CellSink(3), 149 | b2 = new CellSink(5); 150 | b2.send(7); 151 | return new Tuple2(b1, b2); 152 | }); 153 | 154 | const b1 = t.a, 155 | b2 = t.b, 156 | out: number[] = [], 157 | kill = b1.lift(b2, (x, y) => x + y) 158 | .listen(a => { 159 | out.push(a); 160 | done(); 161 | }); 162 | kill(); 163 | 164 | expect([10]).toEqual(out); 165 | }); 166 | -------------------------------------------------------------------------------- /src/tests/unit/DoubleSnapshot.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { 4 | lambda1, 5 | StreamSink, 6 | CellSink, 7 | Transaction, 8 | Tuple2, 9 | Operational, 10 | Cell, 11 | CellLoop, 12 | getTotalRegistrations, 13 | lambda2 14 | } from '../../lib/Lib'; 15 | 16 | /* 17 | * Types 18 | */ 19 | 20 | interface Area { 21 | width: number; 22 | height: number; 23 | } 24 | 25 | interface Point { 26 | x: number; 27 | y: number; 28 | } 29 | 30 | interface State { 31 | info: string; 32 | } 33 | 34 | interface Unlisteners { 35 | display?: () => void; 36 | touch?: () => void; 37 | state?: () => void; 38 | } 39 | 40 | /* 41 | * Setup 42 | */ 43 | 44 | afterEach(() => { 45 | if (getTotalRegistrations() != 0) { 46 | throw new Error('listeners were not deregistered'); 47 | } 48 | }); 49 | 50 | /* 51 | * Tests 52 | */ 53 | test('Double Snapshot', done => { 54 | const unlisteners:Unlisteners = {}; 55 | const displayOut = new Array(); 56 | const stateOut = new Array(); 57 | 58 | 59 | const sDisplay_Sink = new StreamSink(); 60 | const cDisplay = sDisplay_Sink.hold({width: 1024, height: 768}); 61 | unlisteners.display = 62 | cDisplay.listen(area => { 63 | displayOut.push(area); 64 | checkEnd(); 65 | }); 66 | 67 | const sTouch_Sink = new StreamSink(); 68 | const sTouch = 69 | sTouch_Sink 70 | .snapshot(cDisplay, (touchPoint, display) => 71 | ({ 72 | x: display.width + touchPoint.x, 73 | y: display.height + touchPoint.y 74 | }) 75 | ); 76 | //unlisteners.touch = sTouch.listen(() => {}); 77 | 78 | const sState_Sink = new StreamSink(); 79 | const cState = sState_Sink.accum({info: "Current State"}, (f, s) => s); 80 | const sState = 81 | sTouch.snapshot(cState, (point, state) => 82 | ({ 83 | info: state.info + `: (${point.x}, ${point.y})` 84 | }) 85 | ); 86 | unlisteners.state = 87 | sState.listen(state => { 88 | stateOut.push(state); 89 | checkEnd(); 90 | }); 91 | 92 | sTouch_Sink.send({x: 176, y: 0}); 93 | sDisplay_Sink.send({width: 2048, height: 1536}); 94 | sState_Sink.send(null); 95 | sTouch_Sink.send({x: 176, y: 0}); 96 | 97 | function checkEnd() { 98 | if(displayOut.length === 2 && stateOut.length === 2) { 99 | //need to delay in case of only immediate listener 100 | setTimeout(() => { 101 | unlisten(unlisteners); 102 | 103 | expect(displayOut).toEqual([ 104 | {width: 1024, height: 768}, 105 | {width: 2048, height: 1536} 106 | ]); 107 | 108 | expect(stateOut).toEqual([ 109 | { info: "Current State: (1200, 768)" }, 110 | { info: "Current State: (2224, 1536)" } 111 | ]) 112 | done(); 113 | }, 0); 114 | } 115 | } 116 | }); 117 | 118 | function unlisten(unlisteners:Unlisteners) { 119 | Object.keys(unlisteners) 120 | .map(key => unlisteners[key]) 121 | .forEach(fn => fn()); 122 | } 123 | -------------------------------------------------------------------------------- /src/tests/unit/FantasyLand.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * Fantasy-land Algebraic Data Type Compatability. 5 | * Cell satisfies the Functor, Apply, and Applicative categories 6 | * @see {@link https://github.com/fantasyland/fantasy-land} for more info 7 | * @see {@link https://github.com/sanctuary-js/sanctuary/blob/master/test/Maybe/Maybe.js} for valid test examples (Sanctuary's Maybe) 8 | */ 9 | 10 | import * as jsc from 'jsverify'; 11 | import { S} from "../test-utils/Sanctuary"; 12 | import { Cell, StreamSink, Stream, Transaction} from '../../lib/Lib'; 13 | import * as laws from 'fantasy-laws'; 14 | import { testSequence } from '../test-utils/Sequence'; 15 | 16 | /* 17 | * Cell 18 | */ 19 | //would be nice if we could push all samples off to listeners... like in Fantasy-land Practical Tests below, but for unit testing it's okay 20 | 21 | function CellArb(arb: jsc.Arbitrary) { 22 | return arb.smap(x => new Cell(x), x => x.sample()); 23 | } 24 | 25 | function CellEq(a: Cell, b: Cell) { 26 | return a.sample() === b.sample(); 27 | } 28 | 29 | function CellHead(x: string): Cell { 30 | const head = S.head(x); 31 | 32 | return new Cell(head.isNothing ? "" : head.value); 33 | } 34 | 35 | function CellParseInt(radix: number): ((x: number) => Cell) { 36 | return function (x: number) { 37 | const m = S.parseInt(radix)(x); 38 | return new Cell(m.isNothing ? 0 : m.value); 39 | }; 40 | } 41 | 42 | test('Cell - Functor Laws', () => { 43 | const testLaws = laws.Functor(CellEq); 44 | 45 | testLaws.identity( 46 | CellArb(jsc.number) 47 | ); 48 | 49 | testLaws.composition( 50 | CellArb(jsc.number), 51 | jsc.constant(Math.sqrt), 52 | jsc.constant(Math.abs) 53 | ); 54 | }); 55 | 56 | test('Apply Laws', () => { 57 | const testLaws = laws.Apply(CellEq); 58 | 59 | testLaws.composition( 60 | CellArb(jsc.constant(Math.sqrt)), 61 | CellArb(jsc.constant(Math.abs)), 62 | CellArb(jsc.number) 63 | ); 64 | }); 65 | 66 | test('Appplicative Laws', () => { 67 | const testLaws = laws.Applicative(CellEq, Cell); 68 | 69 | testLaws.identity( 70 | CellArb(jsc.number) 71 | ); 72 | 73 | testLaws.homomorphism( 74 | jsc.constant(Math.abs), 75 | jsc.number 76 | ); 77 | 78 | testLaws.interchange( 79 | CellArb(jsc.constant(Math.abs)), 80 | jsc.number 81 | ); 82 | 83 | }); 84 | 85 | test('Lift', (done) => { 86 | const addFunctors = S.lift2(S.add); 87 | 88 | const cResult = addFunctors(new Cell(2)) (new Cell(3)); 89 | const kill = cResult.listen((n: number) => { 90 | expect(n).toBe(5); 91 | done(); 92 | }); 93 | 94 | kill(); 95 | }); 96 | 97 | test('Sequence', (done) => { 98 | testSequence (S.sequence(Cell)) (done) 99 | }); 100 | 101 | test('Concat', (done) => { 102 | const s1 = new StreamSink>(); 103 | const s2 = new StreamSink>(); 104 | const s3 = S.concat(s1) (s2); 105 | 106 | const kill = s3.listen((n: Array) => { 107 | expect(n).toEqual([5, 3, 42]); 108 | done(); 109 | }); 110 | 111 | Transaction.run(() => { 112 | s1.send([5]); 113 | s2.send([3, 42]); 114 | }) 115 | kill(); 116 | }); 117 | 118 | 119 | 120 | 121 | /* 122 | Stream 123 | describe('Fantasy-land Stream', () => { 124 | /* 125 | 126 | TODO: figure out right way to define arb and equality here 127 | If a solution is found that uses listen(), consider porting Cell to that approach as well. 128 | 129 | function StreamArb(arb: jsc.Arbitrary) { 130 | return arb.smap(x => { 131 | const sink = new StreamSink(); 132 | sink.listen(() => {}); 133 | sink.send(x); 134 | return sink; 135 | }, x => x.hold(undefined).sample()); 136 | } 137 | 138 | function StreamEq(a: Stream, b: Stream) { 139 | console.log(a.hold(undefined).sample()); 140 | return a.hold(undefined).sample() === b.hold(undefined).sample(); 141 | } 142 | 143 | describe('Functor Laws', () => { 144 | const testLaws = laws.Functor(StreamEq); 145 | 146 | it('Identity', testLaws.identity( 147 | StreamArb(jsc.number) 148 | )); 149 | 150 | it('Composition', testLaws.composition( 151 | StreamArb(jsc.number), 152 | jsc.constant(Math.sqrt), 153 | jsc.constant(Math.abs) 154 | )); 155 | }); 156 | */ 157 | -------------------------------------------------------------------------------- /src/tests/unit/IOAction.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | IOAction, 4 | StreamSink, 5 | getTotalRegistrations 6 | } from '../../lib/Lib'; 7 | 8 | afterEach(() => { 9 | if (getTotalRegistrations() != 0) { 10 | throw new Error('listeners were not deregistered'); 11 | } 12 | }); 13 | 14 | test('IOAction', (done) => { 15 | const name = "fromAsync", 16 | action = IOAction.fromAsync((a: number, result: (b: number) => void) => { 17 | setTimeout(() => { 18 | result(a + 1); 19 | }, 1); 20 | }), 21 | out: number[] = [], 22 | sa = new StreamSink(), 23 | kill = action(sa).listen(b => out.push(b)); 24 | 25 | let out0: number[] = [], 26 | out1: number[] = [], 27 | out2: number[] = []; 28 | 29 | sa.send(5); 30 | out0 = out.map((num) => num); 31 | 32 | setTimeout(() => { 33 | sa.send(9); 34 | out1 = out.map((num) => num); 35 | 36 | setTimeout(() => { 37 | out2 = out.map((num) => num); 38 | kill(); 39 | 40 | expect([]).toEqual(out0); 41 | expect([6]).toEqual(out1); 42 | expect([6, 10]).toEqual(out2); 43 | done(); 44 | }, 100); 45 | }, 100) 46 | 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /src/tests/unit/InnerLoop.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { 4 | lambda1, 5 | StreamSink, 6 | CellSink, 7 | Transaction, 8 | Tuple2, 9 | Operational, 10 | Cell, 11 | CellLoop, 12 | getTotalRegistrations, 13 | lambda2 14 | } from '../../lib/Lib'; 15 | 16 | afterEach(() => { 17 | if (getTotalRegistrations() != 0) { 18 | throw new Error('listeners were not deregistered'); 19 | } 20 | }); 21 | 22 | const runTest = (done: () => void) => { 23 | const results = [] 24 | const expected = [ 25 | "BAZ", 26 | "", 27 | "apple", 28 | "apple", 29 | "APPLE", 30 | "", 31 | "", 32 | "" 33 | ]; 34 | 35 | const unlisteners = []; 36 | const finish = () => setTimeout(() => { //postponed a frame for convenience 37 | unlisteners.forEach(fn => fn()); 38 | expect(results.length).toBe(expected.length); 39 | results.forEach((v, i) => expect(v).toEqual(expected[i])); 40 | done(); 41 | }, 0); 42 | 43 | const sWrite = new StreamSink(); 44 | 45 | //Modify items 46 | const sModify = new StreamSink(); 47 | const makeUppercase = (target: string, s: string) => target === s ? s.toUpperCase() : s; 48 | 49 | //Manage list of items 50 | const sAdd = new StreamSink(); 51 | const sRemoveAll = new StreamSink(); 52 | 53 | const cItems = Transaction.run(() => { 54 | const makeItem = (label: string, cCurr:Cell): Cell => { 55 | const cLoop = new CellLoop(); 56 | 57 | const cUpdate = sModify.snapshot(cLoop, makeUppercase).hold(label); 58 | 59 | cLoop.loop(cCurr); 60 | 61 | return cUpdate; 62 | }; 63 | 64 | const ccLoop = new CellLoop>(); 65 | 66 | const emptyCell = new Cell(""); 67 | 68 | const cCurr = Cell.switchC(ccLoop); 69 | 70 | const ccUpdate = 71 | sAdd.orElse(sRemoveAll) 72 | .map(lambda1(str => { 73 | if (str === "") { 74 | return emptyCell; 75 | } else { 76 | return makeItem(str, cCurr); 77 | } 78 | }, [emptyCell, sModify, cCurr])) 79 | .hold(emptyCell); 80 | 81 | ccLoop.loop(ccUpdate); 82 | const ccItems = ccLoop; 83 | 84 | const cResult = Cell.switchC(ccItems); //Then switchC on it to get Cell> 85 | 86 | return cResult; 87 | }); 88 | 89 | //Flush writes 90 | unlisteners.push( 91 | sWrite.snapshot(cItems, (evt, items) => { 92 | results.push(items); 93 | return evt; 94 | }) 95 | .listen(evt => { 96 | if (evt) { 97 | finish(); 98 | } 99 | }) 100 | ); 101 | 102 | //This is just for the sake of debugging 103 | unlisteners.push( 104 | cItems.listen(items => { 105 | //console.log(items); 106 | }) 107 | ); 108 | 109 | //expected state (after write): "BAZ" 110 | sAdd.send("foo"); 111 | sAdd.send("bar"); 112 | sAdd.send("baz"); 113 | sModify.send("baz"); 114 | sWrite.send(false); 115 | 116 | //expected state: "" 117 | sRemoveAll.send(""); 118 | sWrite.send(false); 119 | 120 | //expected state: "apple" 121 | sAdd.send("apple"); 122 | sWrite.send(false); 123 | 124 | //expected state: "apple" 125 | sModify.send("foo"); 126 | sWrite.send(false) 127 | 128 | //expected state: "APPLE" 129 | sModify.send("apple"); 130 | sWrite.send(false); 131 | 132 | //expected state: "" 133 | sRemoveAll.send(""); 134 | sWrite.send(false); 135 | 136 | //expected state: "" 137 | sModify.send("foo"); //Causes a "send() was invoked before listeners were registered" here: 138 | sWrite.send(false); 139 | 140 | //Last write - won't get checked 141 | sAdd.send("foo"); 142 | sRemoveAll.send(""); 143 | 144 | //This will pass fine with the listenOnInnerLoop set to true 145 | sModify.send("foo"); 146 | 147 | //----------DONE------------------------ 148 | //expected state : "" 149 | sRemoveAll.send(""); 150 | sWrite.send(true); 151 | } 152 | 153 | test('Inner Loop ', (done) => { 154 | runTest (done); 155 | }); 156 | -------------------------------------------------------------------------------- /src/tests/unit/MultipleLoop.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StreamSink, 3 | getTotalRegistrations, 4 | Transaction, 5 | CellLoop, 6 | lambda3 7 | } from '../../lib/Lib'; 8 | 9 | afterEach(() => { 10 | if (getTotalRegistrations() != 0) { 11 | throw new Error('listeners were not deregistered'); 12 | } 13 | }); 14 | 15 | //this passes 16 | test('multiple loop: stream in transaction', done => { 17 | const cResult = Transaction.run(() => { 18 | const s = new StreamSink(); 19 | 20 | const c1 = new CellLoop(); 21 | const c2 = new CellLoop(); 22 | c1.loop( 23 | s.snapshot3(c1, c2, (n1, n2, n3) => n1 * n2 * n3).hold(2) 24 | ); 25 | 26 | c2.loop( 27 | s.snapshot3(c1, c2, (n1, n2, n3) => n1 * n2 * n3).hold(2) 28 | ); 29 | 30 | 31 | s.send(4); 32 | 33 | return c1; 34 | }); 35 | 36 | 37 | //need to delay the handler so it can call kill 38 | const kill = cResult.listen(n => setTimeout(() => onValue(n), 0)); 39 | 40 | const onValue = n => { 41 | expect(16).toBe(n); 42 | kill(); 43 | done(); 44 | } 45 | }); 46 | 47 | 48 | //this also passes 49 | test('multiple loop: stream out of transaction', done => { 50 | const s = new StreamSink(); 51 | 52 | const cResult = Transaction.run(() => { 53 | 54 | const c1 = new CellLoop(); 55 | const c2 = new CellLoop(); 56 | c1.loop( 57 | s.snapshot3(c1, c2, (n1, n2, n3) => n1 * n2 * n3).hold(2) 58 | ); 59 | 60 | c2.loop( 61 | s.snapshot3(c1, c2, (n1, n2, n3) => n1 * n2 * n3).hold(2) 62 | ); 63 | 64 | 65 | s.send(4); 66 | 67 | return c1; 68 | }); 69 | 70 | 71 | //need to delay the handler so it can call kill 72 | const kill = cResult.listen(n => setTimeout(() => onValue(n), 0)); 73 | 74 | const onValue = n => { 75 | expect(16).toBe(n); 76 | kill(); 77 | done(); 78 | } 79 | }); 80 | 81 | //very interestingly - this also passes 82 | test('single loop: stream and send out of transaction', done => { 83 | const s = new StreamSink(); 84 | 85 | const cResult = Transaction.run(() => { 86 | 87 | const c = new CellLoop(); 88 | c.loop( 89 | s.snapshot(c, (n1, n2) => n1 * n2).hold(2) 90 | ); 91 | 92 | return c; 93 | }); 94 | 95 | 96 | //need to delay the handler so it can call kill 97 | const kill = cResult.listen(n => setTimeout(() => onValue(n), 0)); 98 | 99 | s.send(4); 100 | const out = []; 101 | 102 | const onValue = n => { 103 | out.push(n); 104 | if(out.length === 2) { 105 | expect([2, 8]).toEqual(out); 106 | kill(); 107 | done(); 108 | } 109 | } 110 | }); 111 | 112 | 113 | //this fails - even though all the above tests pass! 114 | //still fails with the lambda and delay attempts 115 | test('multiple loop w/ lambda: stream and send out of transaction', done => { 116 | const s = new StreamSink(); 117 | const cResult = Transaction.run(() => { 118 | 119 | const c1 = new CellLoop(); 120 | const c2 = new CellLoop(); 121 | 122 | //As an attempted fix, put everything in lambdas 123 | c1.loop( 124 | s.snapshot3(c1, c2, lambda3((n1, n2, n3) => n1 * n2 * n3, [s, c1, c2])).hold(2) 125 | ); 126 | 127 | c2.loop( 128 | s.snapshot3(c1, c2, lambda3((n1, n2, n3) => n1 * n2 * n3, [s, c1, c2])).hold(2) 129 | ); 130 | 131 | return c1; 132 | }); 133 | 134 | 135 | //need to delay the handler so it can call kill 136 | const kill = cResult.listen(n => setTimeout(() => onValue(n), 0)) 137 | 138 | //Uncomment this dummy listener for a "fix" (remember to kill it below too) 139 | //const killDummy = s.listen(() => {}); 140 | 141 | //As an attempted fix - put it in another transaction, and delay that 142 | setTimeout( 143 | () => Transaction.run(() => { 144 | s.send(4); 145 | }), 146 | 0 147 | ); 148 | 149 | 150 | //now we're getting the initial cell value too so we need to collect it 151 | const out = []; 152 | const onValue = n => { 153 | out.push(n); 154 | if(out.length == 2) { 155 | expect([2, 16]).toEqual(out); 156 | kill(); 157 | //killDummy(); 158 | done(); 159 | } 160 | } 161 | }); 162 | 163 | -------------------------------------------------------------------------------- /src/tests/unit/NestedLift.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | lambda1, 4 | StreamSink, 5 | CellSink, 6 | Transaction, 7 | Tuple2, 8 | Operational, 9 | Cell, 10 | CellLoop, 11 | getTotalRegistrations, 12 | lambda2 13 | } from '../../lib/Lib'; 14 | 15 | afterEach(() => { 16 | if (getTotalRegistrations() != 0) { 17 | throw new Error('listeners were not deregistered'); 18 | } 19 | }); 20 | 21 | test('map + nested lift', (done) => { 22 | const out = new Array(); 23 | const ccOriginal = new Cell>(new Cell(1)); 24 | const sOffset = new StreamSink(); 25 | const cOffset = sOffset.hold(0); 26 | 27 | const cTotal = ccOriginal.map(cOriginal => 28 | cOriginal.lift(cOffset, (value, offset) => value + offset) 29 | ); 30 | 31 | const kill = Cell 32 | .switchC(cTotal) 33 | .listen(value => { 34 | out.push(value); 35 | 36 | if (out.length === 2) { 37 | done(); 38 | } 39 | }); 40 | 41 | sOffset.send(2); 42 | sOffset.send(4); 43 | kill(); 44 | 45 | expect(out).toEqual([1, 3, 5]); 46 | }); 47 | 48 | 49 | test('lift + nested data/map', (done) => { 50 | interface Data { 51 | cValue: Cell; 52 | } 53 | const out = new Array(); 54 | const cOriginal = new Cell({ cValue: new Cell(1) }); 55 | const sOffset = new StreamSink(); 56 | const cOffset = sOffset.hold(0); 57 | 58 | const cTotal = cOriginal.lift(cOffset, (data, offset) => { 59 | return { 60 | cValue: data.cValue.map(value => value + offset) 61 | } 62 | }) 63 | 64 | 65 | const kill = Cell 66 | .switchC(cTotal.map(data => data.cValue)) 67 | .listen(value => { 68 | out.push(value); 69 | 70 | if (out.length === 2) { 71 | done(); 72 | } 73 | }); 74 | 75 | sOffset.send(2); 76 | sOffset.send(4); 77 | kill(); 78 | 79 | expect(out).toEqual([1, 3, 5]); 80 | }); 81 | 82 | test('map + nested data/lift (w/ Transaction)', (done) => { 83 | interface Data { 84 | cValue: Cell; 85 | } 86 | 87 | const out = new Array(); 88 | const cOriginal = new Cell({ cValue: new Cell(1) }); 89 | const sOffset = new StreamSink(); 90 | const cOffset = sOffset.hold(0); 91 | 92 | const cTotal = cOriginal.map(lambda1((data: Data) => { 93 | return { 94 | cValue: data.cValue.lift(cOffset, (value, offset) => value + offset) 95 | } 96 | }, [cOffset])); 97 | 98 | const kill = Transaction.run(() => 99 | Cell.switchC(cTotal.map(data => data.cValue)) 100 | .listen(value => { 101 | out.push(value); 102 | if (out.length === 2) { 103 | done(); 104 | } 105 | }) 106 | ); 107 | 108 | 109 | sOffset.send(2); 110 | sOffset.send(4); 111 | 112 | kill(); 113 | 114 | expect(out).toEqual([1, 3, 5]); 115 | 116 | }); 117 | 118 | test('map + nested data/lift (no Transaction)', (done) => { 119 | interface Data { 120 | cValue: Cell; 121 | } 122 | 123 | const out = new Array(); 124 | const cOriginal = new Cell({ cValue: new Cell(1) }); 125 | const sOffset = new StreamSink(); 126 | const cOffset = sOffset.hold(0); 127 | 128 | const cTotal = cOriginal.map(lambda1((data: Data) => { 129 | return { 130 | cValue: data.cValue.lift(cOffset, (value, offset) => value + offset) 131 | } 132 | }, [cOffset])); 133 | 134 | const kill = Cell 135 | .switchC(cTotal.map(data => data.cValue)) 136 | .listen(value => { 137 | out.push(value); 138 | if (out.length === 2) { 139 | done(); 140 | } 141 | }) 142 | 143 | 144 | sOffset.send(2); 145 | sOffset.send(4); 146 | 147 | kill(); 148 | 149 | expect(out).toEqual([1, 3, 5]); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /src/tests/unit/Rank.spec.ts: -------------------------------------------------------------------------------- 1 | import * as sodium from '../../lib/Lib'; 2 | 3 | afterEach(() => { 4 | if (sodium.getTotalRegistrations() != 0) { 5 | throw new Error('listeners were not deregistered'); 6 | } 7 | }); 8 | 9 | test("should test rank", done => { 10 | const ca = new sodium.CellSink(false); 11 | const cb = sodium.Cell.switchC(ca.map(a => { 12 | if (a) { 13 | let ca = new sodium.Cell(0); 14 | for (let i = 0; i < 50; ++i) { 15 | ca = ca.map(x => x); 16 | } 17 | return ca; 18 | } else { 19 | return new sodium.Cell(0); 20 | } 21 | })); 22 | let sa = new sodium.Stream(); 23 | let sb = sa.snapshot1(cb); 24 | const kill = sb.listen(() => {}); 25 | const rank1 = sb.getVertex__().rank; 26 | ca.send(true); 27 | const rank2 = sb.getVertex__().rank; 28 | kill(); 29 | expect(rank1).toEqual(rank2); 30 | done(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/tests/unit/Router.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cell, 3 | Router, 4 | StreamSink, 5 | getTotalRegistrations 6 | } from '../../lib/Lib'; 7 | 8 | afterEach(() => { 9 | if (getTotalRegistrations() != 0) { 10 | throw new Error('listeners were not deregistered'); 11 | } 12 | }); 13 | 14 | test('should test Router', (done) => { 15 | let out: string[] = []; 16 | let sa = new StreamSink(); 17 | let router = new Router(sa, x => x); 18 | let sb = router.filterMatches(1).mapTo("a"); 19 | let sc = router.filterMatches(2).mapTo("b"); 20 | let sd = router.filterMatches(3).mapTo("c"); 21 | let kill = sb.merge(sc, (x,y) => x+y).merge(sd, (x,y) => x+y).listen(x => out.push(x)); 22 | sa.send([1]); 23 | sa.send([2]); 24 | sa.send([3]); 25 | sa.send([1,2,3]); 26 | sa.send([1,2,3,1,2]) 27 | kill(); 28 | expect(out).toEqual(["a", "b", "c", "abc", "abc"]); 29 | done(); 30 | }); 31 | -------------------------------------------------------------------------------- /src/tests/unit/StreamSink.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | lambda1, 4 | StreamSink, 5 | StreamLoop, 6 | CellSink, 7 | Transaction, 8 | Tuple2, 9 | Operational, 10 | Cell, 11 | CellLoop, 12 | getTotalRegistrations 13 | } from '../../lib/Lib'; 14 | 15 | afterEach(() => { 16 | if (getTotalRegistrations() != 0) { 17 | throw new Error('listeners were not deregistered'); 18 | } 19 | }); 20 | 21 | test('should test map()', (done) => { 22 | const s = new StreamSink(); 23 | const out: number[] = []; 24 | const kill = s.map(a => a + 1) 25 | .listen(a => { 26 | out.push(a); 27 | done(); 28 | }); 29 | s.send(7); 30 | kill(); 31 | 32 | expect([8]).toEqual(out); 33 | }); 34 | 35 | test('should throw an error send_with_no_listener_1', () => { 36 | const s = new StreamSink(); 37 | 38 | try { 39 | s.send(7); 40 | } catch (e) { 41 | expect(e.message).toBe('send() was invoked before listeners were registered'); 42 | } 43 | 44 | }); 45 | 46 | test('should (not?) throw an error send_with_no_listener_2', () => { 47 | const s = new StreamSink(); 48 | const out: number[] = []; 49 | const kill = s.map(a => a + 1) 50 | .listen(a => out.push(a)); 51 | 52 | s.send(7); 53 | kill(); 54 | 55 | try { 56 | // TODO: the message below is bit misleading, need to verify with Stephen B. 57 | // - "this should not throw, because once() uses this mechanism" 58 | s.send(9); 59 | } catch (e) { 60 | expect(e.message).toBe('send() was invoked before listeners were registered'); 61 | } 62 | }); 63 | 64 | test('should map_tack', (done) => { 65 | const s = new StreamSink(), 66 | t = new StreamSink(), 67 | out: number[] = [], 68 | kill = s.map(lambda1((a: number) => a + 1, [t])) 69 | .listen(a => { 70 | out.push(a); 71 | done(); 72 | }); 73 | 74 | s.send(7); 75 | t.send("banana"); 76 | kill(); 77 | 78 | expect([8]).toEqual(out); 79 | }); 80 | 81 | test('should test mapTo()', (done) => { 82 | const s = new StreamSink(), 83 | out: string[] = [], 84 | kill = s.mapTo("fusebox") 85 | .listen(a => { 86 | out.push(a); 87 | if (out.length === 2) { 88 | done(); 89 | } 90 | }); 91 | 92 | s.send(7); 93 | s.send(9); 94 | kill(); 95 | 96 | expect(['fusebox', 'fusebox']).toEqual(out); 97 | }); 98 | 99 | test('should do mergeNonSimultaneous', (done) => { 100 | const s1 = new StreamSink(), 101 | s2 = new StreamSink(), 102 | out: number[] = []; 103 | 104 | const kill = s2.orElse(s1) 105 | .listen(a => { 106 | out.push(a); 107 | if (out.length === 3) { 108 | done(); 109 | } 110 | }); 111 | 112 | s1.send(7); 113 | s2.send(9); 114 | s1.send(8); 115 | kill(); 116 | 117 | expect([7, 9, 8]).toEqual(out); 118 | }); 119 | 120 | test('should do mergeSimultaneous', (done) => { 121 | const s1 = new StreamSink((l: number, r: number) => { return r; }), 122 | s2 = new StreamSink((l: number, r: number) => { return r; }), 123 | out: number[] = [], 124 | kill = s2.orElse(s1) 125 | .listen(a => { 126 | out.push(a); 127 | if (out.length === 5) { 128 | done(); 129 | } 130 | }); 131 | 132 | Transaction.run(() => { 133 | s1.send(7); 134 | s2.send(60); 135 | }); 136 | Transaction.run(() => { 137 | s1.send(9); 138 | }); 139 | Transaction.run(() => { 140 | s1.send(7); 141 | s1.send(60); 142 | s2.send(8); 143 | s2.send(90); 144 | }); 145 | Transaction.run(() => { 146 | s2.send(8); 147 | s2.send(90); 148 | s1.send(7); 149 | s1.send(60); 150 | }); 151 | Transaction.run(() => { 152 | s2.send(8); 153 | s1.send(7); 154 | s2.send(90); 155 | s1.send(60); 156 | }); 157 | kill(); 158 | 159 | expect([60, 9, 90, 90, 90]).toEqual(out); 160 | }); 161 | 162 | test('should do coalesce', (done) => { 163 | const s = new StreamSink((a, b) => a + b), 164 | out: number[] = [], 165 | kill = s.listen(a => { 166 | out.push(a); 167 | if (out.length === 2) { 168 | done(); 169 | } 170 | }); 171 | 172 | Transaction.run(() => { 173 | s.send(2); 174 | }); 175 | Transaction.run(() => { 176 | s.send(8); 177 | s.send(40); 178 | }); 179 | kill(); 180 | 181 | expect([2, 48]).toEqual(out); 182 | }); 183 | 184 | test('should test filter()', (done) => { 185 | const s = new StreamSink(), 186 | out: number[] = [], 187 | kill = s.filter(a => a < 10) 188 | .listen(a => { 189 | out.push(a); 190 | if (out.length === 2) { 191 | done(); 192 | } 193 | }); 194 | 195 | s.send(2); 196 | s.send(16); 197 | s.send(9); 198 | kill(); 199 | 200 | expect([2, 9]).toEqual(out); 201 | }); 202 | 203 | test('should test filterNotNull()', (done) => { 204 | const s = new StreamSink(), 205 | out: string[] = [], 206 | kill = s.filterNotNull() 207 | .listen(a => { 208 | out.push(a); 209 | if (out.length === 2) { 210 | done(); 211 | } 212 | }); 213 | 214 | s.send("tomato"); 215 | s.send(null); 216 | s.send("peach"); 217 | kill(); 218 | 219 | expect(["tomato", "peach"]).toEqual(out); 220 | }); 221 | 222 | test('should test merge()', (done) => { 223 | const sa = new StreamSink(), 224 | sb = sa.map(x => Math.floor(x / 10)) 225 | .filter(x => x != 0), 226 | sc = sa.map(x => x % 10) 227 | .merge(sb, (x, y) => x + y), 228 | out: number[] = [], 229 | kill = sc.listen(a => { 230 | out.push(a); 231 | if (out.length === 2) { 232 | done(); 233 | } 234 | }); 235 | 236 | sa.send(2); 237 | sa.send(52); 238 | kill(); 239 | 240 | expect([2, 7]).toEqual(out); 241 | }); 242 | 243 | test('should test loop()', (done) => { 244 | const sa = new StreamSink(), 245 | sc = Transaction.run(() => { 246 | const sb = new StreamLoop(), 247 | sc_ = sa.map(x => x % 10).merge(sb, 248 | (x, y) => x + y), 249 | sb_out = sa.map(x => Math.floor(x / 10)) 250 | .filter(x => x != 0); 251 | sb.loop(sb_out); 252 | return sc_; 253 | }), 254 | out: number[] = [], 255 | kill = sc.listen(a => { 256 | out.push(a); 257 | if (out.length === 2) { 258 | done(); 259 | } 260 | }); 261 | 262 | sa.send(2); 263 | sa.send(52); 264 | kill(); 265 | 266 | expect([2, 7]).toEqual(out); 267 | }); 268 | 269 | test('should test gate()', (done) => { 270 | const s = new StreamSink(), 271 | pred = new CellSink(true), 272 | out: string[] = [], 273 | kill = s.gate(pred).listen(a => { 274 | out.push(a); 275 | if (out.length === 2) { 276 | done(); 277 | } 278 | }); 279 | 280 | s.send("H"); 281 | pred.send(false); 282 | s.send('O'); 283 | pred.send(true); 284 | s.send('I'); 285 | kill(); 286 | 287 | expect(["H", "I"]).toEqual(out); 288 | }); 289 | 290 | test('should test collect()', (done) => { 291 | const ea = new StreamSink(), 292 | out: number[] = [], 293 | sum = ea.collect(0, (a, s) => new Tuple2(a + s + 100, a + s)), 294 | kill = sum.listen(a => { 295 | out.push(a); 296 | if (out.length === 5) { 297 | done(); 298 | } 299 | }); 300 | 301 | ea.send(5); 302 | ea.send(7); 303 | ea.send(1); 304 | ea.send(2); 305 | ea.send(3); 306 | kill(); 307 | 308 | expect([105, 112, 113, 115, 118]).toEqual(out); 309 | }); 310 | 311 | test('should test accum()', (done) => { 312 | const ea = new StreamSink(), 313 | out: number[] = [], 314 | sum = ea.accum(100, (a, s) => a + s), 315 | kill = sum.listen(a => { 316 | out.push(a); 317 | if (out.length === 6) { 318 | done(); 319 | } 320 | }); 321 | 322 | ea.send(5); 323 | ea.send(7); 324 | ea.send(1); 325 | ea.send(2); 326 | ea.send(3); 327 | kill(); 328 | 329 | expect([100, 105, 112, 113, 115, 118]).toEqual(out); 330 | }); 331 | 332 | test('should test once()', (done) => { 333 | const s = new StreamSink(), 334 | out: string[] = [], 335 | kill = s.once().listen(a => { 336 | out.push(a); 337 | done(); 338 | }); 339 | 340 | s.send("A"); 341 | s.send("B"); 342 | s.send("C"); 343 | kill(); 344 | 345 | expect(["A"]).toEqual(out); 346 | }); 347 | 348 | test('should test defer()', (done) => { 349 | const s = new StreamSink(), 350 | c = s.hold(" "), 351 | out: string[] = [], 352 | kill = Operational.defer(s).snapshot1(c) 353 | .listen(a => { 354 | out.push(a); 355 | if (out.length === 3) { 356 | done(); 357 | } 358 | }); 359 | 360 | s.send("C"); 361 | s.send("B"); 362 | s.send("A"); 363 | kill(); 364 | 365 | expect(["C", "B", "A"]).toEqual(out); 366 | }); 367 | 368 | test('should test hold()', (done) => { 369 | const s = new StreamSink(), 370 | c = s.hold(0), 371 | out: number[] = [], 372 | kill = Operational.updates(c) 373 | .listen(a => { 374 | out.push(a); 375 | if (out.length === 2) { 376 | done(); 377 | } 378 | }); 379 | 380 | s.send(2); 381 | s.send(9); 382 | kill(); 383 | 384 | expect([2, 9]).toEqual(out); 385 | }); 386 | 387 | test('should do holdIsDelayed', (done) => { 388 | const s = new StreamSink(), 389 | h = s.hold(0), 390 | sPair = s.snapshot(h, (a, b) => a + " " + b), 391 | out: string[] = [], 392 | kill = sPair.listen(a => { 393 | out.push(a); 394 | if (out.length === 2) { 395 | done(); 396 | } 397 | }); 398 | 399 | s.send(2); 400 | s.send(3); 401 | kill(); 402 | 403 | expect(["2 0", "3 2"]).toEqual(out); 404 | }); 405 | 406 | test('should test switchC()', (done) => { 407 | class SC { 408 | constructor(a: string, b: string, sw: string) { 409 | this.a = a; 410 | this.b = b; 411 | this.sw = sw; 412 | } 413 | 414 | a: string; 415 | b: string; 416 | sw: string; 417 | } 418 | 419 | const ssc = new StreamSink(), 420 | // Split each field out of SC so we can update multiple cells in a 421 | // single transaction. 422 | ca = ssc.map(s => s.a).filterNotNull().hold("A"), 423 | cb = ssc.map(s => s.b).filterNotNull().hold("a"), 424 | csw_str = ssc.map(s => s.sw).filterNotNull().hold("ca"), 425 | // **** 426 | // NOTE! Because this lambda contains references to Sodium objects, we 427 | // must declare them explicitly using lambda1() so that Sodium knows 428 | // about the dependency, otherwise it can't manage the memory. 429 | // **** 430 | csw = csw_str.map(lambda1(s => s == "ca" ? ca : cb, [ca, cb])), 431 | co = Cell.switchC(csw), 432 | out: string[] = [], 433 | kill = co.listen(c => { 434 | out.push(c); 435 | if (out.length === 11) { 436 | done(); 437 | } 438 | }); 439 | 440 | ssc.send(new SC("B", "b", null)); 441 | ssc.send(new SC("C", "c", "cb")); 442 | ssc.send(new SC("D", "d", null)); 443 | ssc.send(new SC("E", "e", "ca")); 444 | ssc.send(new SC("F", "f", null)); 445 | ssc.send(new SC(null, null, "cb")); 446 | ssc.send(new SC(null, null, "ca")); 447 | ssc.send(new SC("G", "g", "cb")); 448 | ssc.send(new SC("H", "h", "ca")); 449 | ssc.send(new SC("I", "i", "ca")); 450 | kill(); 451 | 452 | expect(["A", "B", "c", "d", "E", "F", "f", "F", "g", "H", "I"]).toEqual(out); 453 | 454 | }); 455 | 456 | test('should test switchS()', (done) => { 457 | class SS { 458 | constructor(a: string, b: string, sw: string) { 459 | this.a = a; 460 | this.b = b; 461 | this.sw = sw; 462 | } 463 | 464 | a: string; 465 | b: string; 466 | sw: string; 467 | } 468 | 469 | const sss = new StreamSink(), 470 | sa = sss.map(s => s.a), 471 | sb = sss.map(s => s.b), 472 | csw_str = sss.map(s => s.sw).filterNotNull().hold("sa"), 473 | // **** 474 | // NOTE! Because this lambda contains references to Sodium objects, we 475 | // must declare them explicitly using lambda1() so that Sodium knows 476 | // about the dependency, otherwise it can't manage the memory. 477 | // **** 478 | csw = csw_str.map(lambda1(sw => sw == "sa" ? sa : sb, [sa, sb])), 479 | so = Cell.switchS(csw), 480 | out: string[] = [], 481 | kill = so.listen(x => { 482 | out.push(x); 483 | if (out.length === 9) { 484 | done(); 485 | } 486 | }); 487 | 488 | sss.send(new SS("A", "a", null)); 489 | sss.send(new SS("B", "b", null)); 490 | sss.send(new SS("C", "c", "sb")); 491 | sss.send(new SS("D", "d", null)); 492 | sss.send(new SS("E", "e", "sa")); 493 | sss.send(new SS("F", "f", null)); 494 | sss.send(new SS("G", "g", "sb")); 495 | sss.send(new SS("H", "h", "sa")); 496 | sss.send(new SS("I", "i", "sa")); 497 | kill(); 498 | 499 | expect(["A", "B", "C", "d", "e", "F", "G", "h", "I"]).toEqual(out); 500 | }); 501 | 502 | test('should do switchSSimultaneous', (done) => { 503 | class SS2 { 504 | s: StreamSink = new StreamSink(); 505 | } 506 | 507 | const ss1 = new SS2(), 508 | ss2 = new SS2(), 509 | ss3 = new SS2(), 510 | ss4 = new SS2(), 511 | css = new CellSink(ss1), 512 | // **** 513 | // NOTE! Because this lambda contains references to Sodium objects, we 514 | // must declare them explicitly using lambda1() so that Sodium knows 515 | // about the dependency, otherwise it can't manage the memory. 516 | // **** 517 | so = Cell.switchS(css.map(lambda1((b: SS2) => b.s, [ss1.s, ss2.s, ss3.s, ss4.s]))), 518 | out: number[] = [], 519 | kill = so.listen(c => { 520 | out.push(c); 521 | if (out.length === 10) { 522 | done(); 523 | } 524 | }); 525 | 526 | ss1.s.send(0); 527 | ss1.s.send(1); 528 | ss1.s.send(2); 529 | css.send(ss2); 530 | ss1.s.send(7); 531 | ss2.s.send(3); 532 | ss2.s.send(4); 533 | ss3.s.send(2); 534 | css.send(ss3); 535 | ss3.s.send(5); 536 | ss3.s.send(6); 537 | ss3.s.send(7); 538 | Transaction.run(() => { 539 | ss3.s.send(8); 540 | css.send(ss4); 541 | ss4.s.send(2); 542 | }); 543 | ss4.s.send(9); 544 | kill(); 545 | 546 | expect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).toEqual(out); 547 | }); 548 | 549 | test('should test loopCell', (done) => { 550 | const sa = new StreamSink(), 551 | sum_out = Transaction.run(() => { 552 | const sum = new CellLoop(), 553 | sum_out_ = sa.snapshot(sum, (x, y) => x + y).hold(0); 554 | sum.loop(sum_out_); 555 | return sum_out_; 556 | }), 557 | out: number[] = [], 558 | kill = sum_out.listen(a => { 559 | out.push(a); 560 | if (out.length === 4) { 561 | done(); 562 | } 563 | }); 564 | 565 | sa.send(2); 566 | sa.send(3); 567 | sa.send(1); 568 | kill(); 569 | 570 | expect([0, 2, 5, 6]).toEqual(out); 571 | expect(6).toBe(sum_out.sample()); 572 | }); 573 | 574 | test('should test defer/split memory cycle', done => { 575 | // We do not fire through sl here, as it would cause an infinite loop. 576 | // This is just a memory management test. 577 | let sl : StreamLoop; 578 | Transaction.run(() => { 579 | sl = new StreamLoop(); 580 | sl.loop(Operational.defer(sl)); 581 | }); 582 | let kill = sl.listen(() => {}); 583 | kill(); 584 | done(); 585 | }); 586 | -------------------------------------------------------------------------------- /src/tests/unit/Timer.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Stream, 4 | StreamSink, 5 | CellLoop, 6 | TimerSystem, 7 | SecondsTimerSystem, 8 | Transaction, 9 | Unit, 10 | getTotalRegistrations 11 | } from "../../lib/Lib"; 12 | 13 | const timeout: number = 30000; 14 | 15 | test('should test Timer', (done) => { 16 | function periodic(sys: TimerSystem, period: number) { 17 | const time = sys.time, 18 | oAlarm = new CellLoop(), 19 | sAlarm = sys.at(oAlarm); 20 | oAlarm.loop( 21 | sAlarm.map(t => t + period) 22 | .hold(time.sample() + period)); 23 | return sAlarm; 24 | } 25 | 26 | function ticker(done: () => void) { 27 | let sTick: Stream = null; 28 | const sys = new SecondsTimerSystem(), 29 | time = sys.time, 30 | sMain = new StreamSink(), 31 | kill = Transaction.run(() => { 32 | const t0 = time.sample(), 33 | kill1 = periodic(sys, 1).listen(t => { 34 | //console.log((t - t0).toFixed(3) + " timer"); 35 | }), 36 | kill2 = sMain.snapshot1(time).listen(t => { 37 | //console.log((t - t0).toFixed(3) + " main"); 38 | }); 39 | return () => { kill1(); kill2(); }; 40 | }); 41 | 42 | const t0 = time.sample(); 43 | let tick: () => void = null; 44 | 45 | tick = () => { 46 | sMain.send(Unit.UNIT); 47 | if ((sys.time.sample() - t0) < 10.5) 48 | setTimeout(tick, 990); 49 | else { 50 | kill(); 51 | done(); 52 | } 53 | }; 54 | 55 | setTimeout(tick, 990); 56 | 57 | } 58 | 59 | setTimeout(() => { 60 | ticker(done); 61 | }, 1); 62 | 63 | expect(typeof ticker).toBe('function'); 64 | }, timeout); 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "noImplicitAny": false, //would be nice to enable this... 6 | "removeComments": false, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "declarationDir": "dist/typings", 11 | "moduleResolution": "node", 12 | "diagnostics": true, 13 | "lib": [ 14 | "es6", 15 | "es5", 16 | "dom" 17 | ] 18 | 19 | }, 20 | "files": [ 21 | "./src/lib/Lib.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "build", 27 | "coverage" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | var path = require('path'); 3 | 4 | return { 5 | files: [ 6 | 'src/**/*.ts?(x)', 7 | 'src/**/*.snap', 8 | '!src/**/*.spec.ts?(x)' 9 | ], 10 | tests: [ 11 | 'src/**/*.spec.ts?(x)' 12 | ], 13 | 14 | env: { 15 | type: 'node', 16 | runner: 'node' 17 | }, 18 | 19 | testFramework: 'jest', 20 | 21 | 22 | 23 | debug: true 24 | }; 25 | }; 26 | --------------------------------------------------------------------------------