├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── npm-publish.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── benchmarks ├── README.md ├── benchmark.js └── runBench.js ├── index.js ├── lib ├── AbstractEngine.js ├── RandomEngine.js ├── RoundRobinEngine.js ├── WeightedRandomEngine.js ├── WeightedRoundRobinEngine.js ├── debug.js └── prepareWeights.js ├── package.json ├── readme.md └── test ├── RandomEngine.test.js ├── RoundRobinEngine.test.js ├── WeightedRandomEngine.test.js ├── WeightedRoundRobinEngine.test.js └── index.test.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '42 23 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | 35 | publish-gpr: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: 12 43 | registry-url: https://npm.pkg.github.com/ 44 | - run: npm ci 45 | - run: npm publish 46 | env: 47 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | .yo-rc.json 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '11' 4 | - '10' 5 | - '8' 6 | - '6' 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 yaniv kessler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # benchmarks 2 | 3 | `benchmark.js` - runs the actual tests 4 | 5 | `runBench.js` - executes `benchmarks.js`, save the results (TBD: and compare with last N runs) 6 | -------------------------------------------------------------------------------- /benchmarks/benchmark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * this is the actual benchmark test, in it we'll test each engine's pick doing N iterations 3 | * should we run the same engine X times N iterations and save the average? 4 | */ 5 | const { RandomEngine, RoundRobinEngine, WeightedRandomEngine, WeightedRoundRobinEngine } = require('../index') 6 | 7 | const RANDOM_SEED = 999999 8 | const ITERATIONS = 1000000 9 | 10 | const simplePool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 11 | const weightedPool = [ 12 | { weight: 1, object: 1 }, 13 | { weight: 2, object: 2 }, 14 | { weight: 3, object: 3 }, 15 | { weight: 4, object: 4 }, 16 | { weight: 5, object: 5 }, 17 | { weight: 6, object: 6 }, 18 | { weight: 7, object: 7 }, 19 | { weight: 8, object: 8 }, 20 | { weight: 9, object: 9 }, 21 | { weight: 10, object: 10 } 22 | ] 23 | 24 | const engineInstances = { 25 | RandomEngine: new RandomEngine(simplePool, RANDOM_SEED), 26 | RoundRobinEngine: new RoundRobinEngine(simplePool), 27 | WeightedRandomEngine: new WeightedRandomEngine(weightedPool, RANDOM_SEED), 28 | WeightedRoundRobinEngine: new WeightedRoundRobinEngine(weightedPool) 29 | } 30 | 31 | const benchResults = {} 32 | 33 | // loop through engines 34 | for (let [name, engine] of Object.entries(engineInstances)) { 35 | let start = Date.now() 36 | 37 | // pick N times 38 | for (let i = 0; i < ITERATIONS; i++) { 39 | engine.pick() 40 | } 41 | 42 | let end = Date.now() 43 | benchResults[name] = end - start 44 | } 45 | 46 | console.log(JSON.stringify(benchResults)) 47 | -------------------------------------------------------------------------------- /benchmarks/runBench.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { promisify } = require('util') 3 | const cp = require('child_process') 4 | const readFile = promisify(fs.readFile) 5 | const writeFile = promisify(fs.writeFile) 6 | const exec = promisify(cp.exec) 7 | const path = require('path') 8 | const benchmarkFile = path.join(__dirname, 'benchmark.js') 9 | 10 | async function main() { 11 | const { stdout, stderr } = await exec(`node ${benchmarkFile}`) 12 | const benchResult = JSON.parse(stdout) 13 | const code = await readFile(benchmarkFile, 'utf8') 14 | 15 | const content = JSON.stringify({ 16 | benchResult, 17 | code 18 | }) 19 | 20 | const resultFile = path.join(__dirname, 'results', `${Date.now()}.benchmark.json`) 21 | await writeFile(resultFile, content) 22 | 23 | console.log(`results were written to ${resultFile}`) 24 | } 25 | 26 | main() -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const AbstractEngine = module.exports.AbstractEngine = require('./lib/AbstractEngine.js') 2 | const RandomEngine = module.exports.RandomEngine = require('./lib/RandomEngine.js') 3 | const WeightedRandomEngine = module.exports.WeightedRandomEngine = require('./lib/WeightedRandomEngine.js') 4 | const RoundRobinEngine = module.exports.RoundRobinEngine = require('./lib/RoundRobinEngine.js') 5 | const WeightedRoundRobinEngine = module.exports.WeightedRoundRobinEngine = require('./lib/WeightedRoundRobinEngine.js') 6 | 7 | module.exports.roundRobin = (pool) => { 8 | if (pool.length === 0) { 9 | throw new Error('pool length must be greater than zero') 10 | } 11 | 12 | const entry = pool[0] 13 | 14 | if (entry.weight) { 15 | return new WeightedRoundRobinEngine(pool) 16 | } else { 17 | return new RoundRobinEngine(pool) 18 | } 19 | } 20 | 21 | module.exports.random = (pool, seed) => { 22 | if (pool.length === 0) { 23 | throw new Error('pool length must be greater than zero') 24 | } 25 | 26 | const entry = pool[0] 27 | 28 | if (entry.weight) { 29 | return new WeightedRandomEngine(pool, seed) 30 | } else { 31 | return new RandomEngine(pool, seed) 32 | } 33 | } 34 | 35 | module.exports.isEngine = (engine) => { 36 | return engine instanceof AbstractEngine 37 | } 38 | -------------------------------------------------------------------------------- /lib/AbstractEngine.js: -------------------------------------------------------------------------------- 1 | const debug = require('./debug')('AbstractEngine') 2 | const { isNullOrUndefined } = require('util') 3 | 4 | class AbstractEngine { 5 | constructor (pool) { 6 | this._pool = pool 7 | debug('ctor') 8 | } 9 | 10 | get pool() { 11 | return this._pool 12 | } 13 | 14 | /** 15 | * pick a single member from the pool using the load balancing implementation 16 | * 17 | */ 18 | pick() { 19 | return this._pick(this._pool) 20 | } 21 | 22 | _pick(pool) { 23 | throw new Error('must implement') 24 | } 25 | } 26 | 27 | module.exports = AbstractEngine -------------------------------------------------------------------------------- /lib/RandomEngine.js: -------------------------------------------------------------------------------- 1 | const AbstractEngine = require('./AbstractEngine') 2 | const { Random, MersenneTwister19937 } = require( 'random-js') 3 | const debug = require('./debug')('RandomEngine') 4 | 5 | class RandomEngine extends AbstractEngine { 6 | 7 | /** 8 | * @param {Array} pool - objects to pick from 9 | * @param {int} seed - an optional seed that will be used to recreate a random sequence of selections 10 | */ 11 | constructor(pool, seed) { 12 | super(pool) 13 | debug('ctor') 14 | 15 | this._maxPick = pool.length - 1 16 | 17 | if (typeof(seed) === 'number') { 18 | debug('using Mersenne Twister engine with seed %d', seed) 19 | this._r = new Random(MersenneTwister19937.seed(seed)) 20 | } else { 21 | debug('using Mersenne Twister engine with autoSeed') 22 | this._r = new Random(MersenneTwister19937.autoSeed()) 23 | } 24 | } 25 | 26 | _pick(pool) { 27 | let index = this._lastPick = this._r.integer(0, this._maxPick) 28 | return pool[index] 29 | } 30 | } 31 | 32 | module.exports = RandomEngine -------------------------------------------------------------------------------- /lib/RoundRobinEngine.js: -------------------------------------------------------------------------------- 1 | const AbstractEngine = require('./AbstractEngine') 2 | const debug = require('./debug')('RoundRobinEngine') 3 | 4 | class RoundRobinEngine extends AbstractEngine { 5 | constructor(pool) { 6 | super(pool) 7 | debug('ctor') 8 | this._poolSize = pool.length 9 | this._currentIndex = 0 10 | } 11 | 12 | _pick(pool) { 13 | let pick = pool[this._currentIndex++] 14 | this._currentIndex = this._currentIndex % this._poolSize 15 | return pick 16 | } 17 | } 18 | 19 | module.exports = RoundRobinEngine -------------------------------------------------------------------------------- /lib/WeightedRandomEngine.js: -------------------------------------------------------------------------------- 1 | const RandomEngine = require('./RandomEngine') 2 | const prepareWeights = require('./prepareWeights') 3 | const debug = require('./debug')('WeightedRandomEngine') 4 | 5 | class WeightedRandomEngine extends RandomEngine { 6 | 7 | /** 8 | * @param {Array} pool - objects to pick from 9 | * @param {int} seed - an optional seed that will be used to recreate a random sequence of selections 10 | */ 11 | constructor(pool, seed) { 12 | super(prepareWeights(pool), seed) 13 | debug('ctor') 14 | } 15 | } 16 | 17 | module.exports = WeightedRandomEngine -------------------------------------------------------------------------------- /lib/WeightedRoundRobinEngine.js: -------------------------------------------------------------------------------- 1 | const RoundRobinEngine = require('./RoundRobinEngine') 2 | const prepareWeights = require('./prepareWeights') 3 | const debug = require('./debug')('WeightedRoundRobinEngine') 4 | 5 | class WeightedRoundRobinEngine extends RoundRobinEngine { 6 | constructor(pool) { 7 | super(prepareWeights(pool)) 8 | debug('ctor') 9 | } 10 | } 11 | 12 | module.exports = WeightedRoundRobinEngine -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug') 2 | 3 | module.exports = ns => debug('loadbalance:' + ns) -------------------------------------------------------------------------------- /lib/prepareWeights.js: -------------------------------------------------------------------------------- 1 | module.exports = (pool) => { 2 | if (pool.length === 0) { 3 | throw new Error('cannot prepare a zero length pool') 4 | } 5 | 6 | const preparedPool = [] 7 | 8 | pool.sort(function(a, b) { 9 | return b.weight - a.weight 10 | }) 11 | 12 | pool.forEach(function (entry, index) { 13 | let object 14 | 15 | // check this in a way that allows the use of zeros and "false" as object 16 | if (entry.object !== undefined && entry.object !== null) { 17 | object = entry.object 18 | } else if (entry.value !== undefined && entry.value !== null) { 19 | object = entry.value 20 | } 21 | 22 | if (object === undefined || object === null) { 23 | throw new Error('Please specify an object or a value (alias for object) property for entry in index ' + index) 24 | } 25 | 26 | if (entry.weight <= 0) { 27 | throw new Error('Weight in index ' + index + ' must be greater than zero') 28 | } 29 | 30 | if (entry.weight % 1 !== 0) { 31 | throw new Error('Weight in index ' + index + ' must be an integer') 32 | } 33 | 34 | for (var i = 0; i < entry.weight; i++) { 35 | preparedPool.push(object) 36 | } 37 | }) 38 | 39 | return preparedPool 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loadbalance", 3 | "version": "1.1.0", 4 | "description": "loadbalance", 5 | "license": "MIT", 6 | "author": { 7 | "name": "yaniv kessler", 8 | "email": "yanivk@gmail.com", 9 | "url": "blog.yanivkessler.com" 10 | }, 11 | "scripts": { 12 | "test": "mocha test/**/*.js", 13 | "bench": "node benchmarks/runBench.js" 14 | }, 15 | "dependencies": { 16 | "debug": "^4.1.1", 17 | "random-js": "^2.1.0" 18 | }, 19 | "devDependencies": { 20 | "chai": "^4.2.0", 21 | "mocha": "^6.1.4" 22 | }, 23 | "keywords": [ 24 | "load", 25 | "balance", 26 | "balancer", 27 | "loadbalance", 28 | "loadbalancer", 29 | "algorithms", 30 | "algorithm", 31 | "engine" 32 | ], 33 | "repository": "kessler/node-loadbalance", 34 | "bugs": "https://github.com/kessler/node-loadbalance/issues", 35 | "homepage": "https://github.com/kessler/node-loadbalance" 36 | } 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # loadbalance 2 | 3 | [![npm status](http://img.shields.io/npm/v/loadbalance.svg?style=flat-square)](https://www.npmjs.org/package/loadbalance) 4 | 5 | This is a collection of load balancing engines in (what is hopefully) their most distilled form. 6 | 7 | The goal was to create a highly reusable implementation that imposes as little as possible on the user. 8 | 9 | ## Install 10 | 11 | With [npm](https://npmjs.org) do: 12 | 13 | `npm i loadbalance` 14 | 15 | ```js 16 | const loadbalance = require('loadbalance') 17 | ``` 18 | 19 | ## Usage 20 | To use, instantiate an engine or call a factory method with a pool. Then call pick(), which will return the selected object, calling pick() repeatedly will yield the same or a different object from the pool, depending on the algorithm which powers that engine. 21 | 22 | ```javascript 23 | const loadbalance = require('loadbalance') 24 | const engine = loadbalance.random(['a', 'b', 'c']) 25 | const pick = engine.pick() 26 | ``` 27 | 28 | ### pick() 29 | `pick()` is called without any arguments and will always return an object which is a member of the pool according to the pool's selection strategy 30 | 31 | ## Engines 32 | 33 | ### Random Engine 34 | The random engine picks an object from the pool at random, each time pick() is called. 35 | 36 | ```javascript 37 | const loadbalance = require('loadbalance') 38 | const engine = loadbalance.random(['a', 'b', 'c']) 39 | const pick = engine.pick() 40 | ``` 41 | 42 | #### new RandomEngine(pool, seed) 43 | ```javascript 44 | const engine = new loadbalance.RandomEngine(pool) 45 | ``` 46 | Pool - an objects to pick from, eg ```[1,2,3]``` 47 | Seed - an optional seed that will be used to recreate a random sequence of selections 48 | 49 | ### Weighted Random Engine 50 | The random engine picks an object from the pool at random **but with bias** (probably should have called it BiasedRandomEngine), each time pick() is called. 51 | 52 | ```javascript 53 | const loadbalance = require('loadbalance') 54 | const engine = loadbalance.random([ 55 | { object: 'a', weight: 2 }, 56 | { object: 'b', weight: 3 }, 57 | { object: 'c', weight: 5 } 58 | ]) 59 | const pick = engine.pick() 60 | ``` 61 | With this engine, calling pick() repeatedly will roughly return `'a'` 20% of the time, `'b'` 30% of the time and `'c'` 50% of the time 62 | 63 | #### new WeightedRandomEngine(pool, seed) 64 | ```javascript 65 | const engine = new loadbalance.WeightedRandomEngine(pool) 66 | ``` 67 | 68 | Pool - objects to pick from. Each object is of the form: 69 | ```javascript 70 | const object1 = { 71 | object: 'something', 72 | weight: 2 73 | } 74 | ``` 75 | 76 | Seed - an optional seed that will be used to recreate a random sequence of selections 77 | 78 | ### RoundRobinEngine 79 | An engine that picks objects from its pool using Round Robin algorithm (doh!) 80 | 81 | ```javascript 82 | const loadbalance = require('loadbalance') 83 | const engine = loadbalance.roundRobin(['a', 'b', 'c']) 84 | const pick = engine.pick() 85 | ``` 86 | 87 | The roundRobin() factory method can be used to obtain both RoundRobinEngine and WeightedRoundRobinEngine. The decision is based on the contents of the pool. 88 | 89 | #### new RoundRobinEngine(pool) 90 | ```javascript 91 | const engine = new loadbalance.RoundRobinEngine(pool) 92 | ``` 93 | Pool - objects to pick from, eg ```[1,2,3]``` 94 | 95 | ### WeightedRoundRobinEngine 96 | Same as round robin engine, only members of the pool can have weights. 97 | 98 | ```javascript 99 | const loadbalance = require('loadbalance') 100 | const engine = loadbalance.roundRobin([{ object: 'a', weight: 2 }, {object: 'b', weight: 1 }]) 101 | const pick = engine.pick() 102 | ``` 103 | 104 | call pick six times using the above engine will yield: 'a', 'a', 'b', 'a', 'a', 'b' 105 | 106 | #### new WeightedRoundRobinEngine(pool) 107 | ```javascript 108 | const engine = new loadbalance.WeightedRoundRobinEngine(pool) 109 | ``` 110 | Pool - objects to pick from. Each object is of the form: 111 | ```javascript 112 | const object1 = { 113 | object: 'something', 114 | weight: 2 115 | } 116 | ``` 117 | 118 | Weight should always be an integer which is greater than zero. 119 | Object (you can also use value, its an alias property) can be anything you want, just like other pools. It cannot, however, be null or undefined at the time the pool is created. 120 | 121 | ### PriorityEngine 122 | Not yet implemented 123 | 124 | ### Extensibility 125 | Here is an example of a custom engine: 126 | ```javascript 127 | const AbstractEngine = require('loadbalance').AbstractEngine 128 | 129 | class MyEngine extends AbstractEngine { 130 | constructor(pool) { 131 | super(pool) 132 | } 133 | 134 | _pick(pool) { 135 | // pick something from the pool somehow and return it 136 | } 137 | } 138 | 139 | ``` 140 | 141 | ## misc 142 | 143 | This module shares some functinality with [pool](https://github.com/coopernurse/node-pool) module. It is worth taking a look at it if you are looking for something more high level. 144 | 145 | This module is heavily inspired by this [article about load balance algorithms](https://devcentral.f5.com/articles/intro-to-load-balancing-for-developers-ndash-the-algorithms) 146 | 147 | 148 | ## license 149 | 150 | [MIT](http://opensource.org/licenses/MIT) © [yaniv kessler](blog.yanivkessler.com) 151 | -------------------------------------------------------------------------------- /test/RandomEngine.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const RandomEngine = require('../lib/RandomEngine') 3 | 4 | describe('RandomEngine', function () { 5 | this.timeout(50000) 6 | 7 | const pool = ['a', 'b', 'c', 'd'] 8 | const TEST_SIZE = 100000 9 | 10 | it(`pick ${TEST_SIZE} random elements from a pool`, function () { 11 | const engine = new RandomEngine(pool) 12 | 13 | const results = {} 14 | 15 | for (let i = 0; i < pool.length; i++) { 16 | results[pool[i]] = 0 17 | } 18 | 19 | for (let i = 0; i < TEST_SIZE; i++) { 20 | let pick = engine.pick() 21 | expect(pool).to.include(pick) 22 | results[pick]++ 23 | } 24 | 25 | for (let r in results) { 26 | let p = results[r] / TEST_SIZE 27 | expect(p).to.be.above(0.24) 28 | expect(p).to.be.below(0.26) 29 | } 30 | }) 31 | 32 | it('accepts a seed number to be used for repeating random pick series', function () { 33 | const e1a = new RandomEngine(pool, 1) 34 | const e1b = new RandomEngine(pool, 1) 35 | 36 | const e2 = new RandomEngine(pool, 2) 37 | 38 | for (let i = 0; i < TEST_SIZE; i++) { 39 | const e1aPick = e1a.pick() 40 | const e1bPick = e1b.pick() 41 | const e2Pick = e2.pick() 42 | 43 | expect(e1aPick).to.equal(e1bPick) 44 | } 45 | }) 46 | 47 | it('benchmark', function () { 48 | 49 | }) 50 | }) -------------------------------------------------------------------------------- /test/RoundRobinEngine.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const RoundRobinEngine = require('../lib/RoundRobinEngine') 3 | 4 | describe('RoundRobinEngine', function () { 5 | this.timeout(50000) 6 | 7 | const pool = ['a', 'b', 'c', 'd'] 8 | 9 | const TEST_SIZE = 100000 10 | 11 | it(`pick ${TEST_SIZE} members from the pool using round robin`, function () { 12 | const engine = new RoundRobinEngine(pool) 13 | 14 | for (let i = 0; i < TEST_SIZE; i++) { 15 | const pick = engine.pick() 16 | 17 | const mod = i % pool.length 18 | 19 | expect(pool).to.include(pick) 20 | expect(pick).to.equal(pool[mod]) 21 | } 22 | }) 23 | }) -------------------------------------------------------------------------------- /test/WeightedRandomEngine.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const WeightedRandomEngine = require('../lib/WeightedRandomEngine') 3 | 4 | describe('WeightedRandomEngine', function() { 5 | this.timeout(50000) 6 | 7 | const pool = ['a', 'b', 'c', 'd'] 8 | 9 | const TEST_SIZE = 1000 10 | 11 | it(`picks ${TEST_SIZE} members from the pool randomly but with bias`, function() { 12 | const poolWithWeights = [{ 13 | object: 'a', 14 | weight: 2 15 | }, { 16 | object: 'b', 17 | weight: 3 18 | }, { 19 | object: 'c', 20 | weight: 5 21 | }] 22 | 23 | const engine = new WeightedRandomEngine(poolWithWeights) 24 | const results = { a: 0, b: 0, c: 0 } 25 | 26 | for (let i = 0; i < TEST_SIZE; i++) { 27 | const pick = engine.pick() 28 | expect(pool).to.include(pick) 29 | results[pick]++ 30 | } 31 | results.a = (results.a / TEST_SIZE) * 10 32 | results.b = (results.b / TEST_SIZE) * 10 33 | results.c = (results.c / TEST_SIZE) * 10 34 | 35 | expect(Math.round(results.a)).to.equal(2) 36 | expect(Math.round(results.b)).to.equal(3) 37 | expect(Math.round(results.c)).to.equal(5) 38 | }) 39 | 40 | it('entries must contain an "object" property', function() { 41 | const poolWithWeights = [{ 42 | weight: 1 43 | }] 44 | 45 | expect(function() { 46 | const engine = new WeightedRandomEngine(poolWithWeights) 47 | }).to.throw('Please specify an object or a value (alias for object) property for entry in index 0') 48 | }) 49 | 50 | it('entries "value" property is an alias to object property', function() { 51 | const poolWithWeights = [{ 52 | weight: 1, 53 | value: 'a' 54 | }] 55 | 56 | expect(function() { 57 | const engine = new WeightedRandomEngine(poolWithWeights) 58 | }).not.to.throw() 59 | }) 60 | 61 | it('weights must be integers', function() { 62 | const poolWithWeights = [{ 63 | weight: 0.2, 64 | object: 'a' 65 | }] 66 | 67 | expect(function() { 68 | const engine = new WeightedRandomEngine(poolWithWeights) 69 | }).to.throw('Weight in index 0 must be an integer') 70 | }) 71 | 72 | it('weights must greater than zero', function() { 73 | const poolWithWeights = [{ 74 | weight: 0, 75 | object: 'a' 76 | }] 77 | 78 | expect(function() { 79 | const engine = new WeightedRandomEngine(poolWithWeights) 80 | }).to.throw('Weight in index 0 must be greater than zero') 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/WeightedRoundRobinEngine.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const WeightedRoundRobinEngine = require('../lib/WeightedRoundRobinEngine') 3 | 4 | describe('WeightedRoundRobinEngine', function() { 5 | this.timeout(50000) 6 | 7 | const pool = ['a', 'b', 'c', 'd'] 8 | 9 | const TEST_SIZE = 1000 10 | 11 | it(`picks ${TEST_SIZE} members from the pool using a weighted round robin`, function() { 12 | const poolWithWeights = [] 13 | 14 | pool.forEach(function (v, i) { 15 | poolWithWeights.push({ 16 | object: v, 17 | weight: i + 1 18 | }) 19 | }) 20 | 21 | const expected = ['d','d','d','d','c','c','c','b','b','a'] 22 | 23 | const engine = new WeightedRoundRobinEngine(poolWithWeights) 24 | 25 | for (let i = 0, x = 0; i < TEST_SIZE; i++, x++) { 26 | if (x === expected.length) x = 0 27 | 28 | const pick = engine.pick() 29 | const mod = i % pool.length 30 | 31 | expect(pool).to.include(pick) 32 | expect(pick).to.equal(expected[x]) 33 | } 34 | }) 35 | 36 | it('entries must contain an "object" property', function () { 37 | const poolWithWeights = [{ 38 | weight: 1 39 | }] 40 | 41 | expect(function () { 42 | const engine = new WeightedRoundRobinEngine(poolWithWeights) 43 | }).to.throw('Please specify an object or a value (alias for object) property for entry in index 0') 44 | }) 45 | 46 | it('entries "value" property is an alias to object property', function () { 47 | const poolWithWeights = [{ 48 | weight: 1, 49 | value: 'a' 50 | }] 51 | 52 | expect(function () { 53 | const engine = new WeightedRoundRobinEngine(poolWithWeights) 54 | }).not.to.throw() 55 | }) 56 | 57 | it('weights must be integers', function () { 58 | const poolWithWeights = [{ 59 | weight: 0.2, 60 | object: 'a' 61 | }] 62 | 63 | expect(function () { 64 | const engine = new WeightedRoundRobinEngine(poolWithWeights) 65 | }).to.throw('Weight in index 0 must be an integer') 66 | }) 67 | 68 | it('weights must greater than zero', function () { 69 | const poolWithWeights = [{ 70 | weight: 0, 71 | object: 'a' 72 | }] 73 | 74 | expect(function () { 75 | const engine = new WeightedRoundRobinEngine(poolWithWeights) 76 | }).to.throw('Weight in index 0 must be greater than zero') 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const index = require('../index') 3 | const WeightedRoundRobinEngine = require('../lib/WeightedRoundRobinEngine') 4 | const RoundRobinEngine = require('../lib/RoundRobinEngine') 5 | 6 | describe('exports', function() { 7 | describe('a factory method that creates a round robin or weighted round robin engine, based on the contents of the pool.', function() { 8 | it('If the first entry in the pool contain a weight property a weighted round robin engine will be returned', function() { 9 | var engine = index.roundRobin([{ 10 | object: 'a', 11 | weight: 1 12 | }]) 13 | 14 | expect(engine).to.be.an.instanceOf(WeightedRoundRobinEngine) 15 | }) 16 | 17 | it('If the first entry in the pool DOES NOT contain a weight property a norma round robin engine will be returned', function() { 18 | var engine = index.roundRobin(['a']) 19 | 20 | expect(engine).to.be.an.instanceOf(RoundRobinEngine) 21 | }) 22 | }) 23 | 24 | describe('an isEngine method to identify instances of engines', function() { 25 | it('for example RoundRobinEngine will be true', function() { 26 | var engine = new RoundRobinEngine([1, 2, 3]) 27 | 28 | expect(index.isEngine(engine)).to.be.true 29 | }) 30 | 31 | it('while another object that does not inherit from AbstractEngine is false', function() { 32 | var engine = {} 33 | 34 | expect(index.isEngine(engine)).to.be.false 35 | }) 36 | }) 37 | }) --------------------------------------------------------------------------------