├── .github └── workflows │ ├── npm-publish-next.yml │ ├── npm-publish.yml │ └── run-tests.yml ├── .gitignore ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchmark └── benchmark-sets.js ├── circle.yml ├── package-lock.json ├── package.json ├── src ├── 2P-Set.js ├── CmRDT-Set.js ├── G-Counter.js ├── G-Set.js ├── LWW-Set.js ├── OR-Set.js ├── PN-Counter.js ├── index.js └── utils.js └── test ├── 2P-Set.test.js ├── CRDT.test.js ├── Common-Set.js ├── G-Counter.test.js ├── G-Set.test.js ├── LWW-Set.test.js ├── OR-Set.test.js ├── PN-Counter.test.js └── lamport-clock.js /.github/workflows/npm-publish-next.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Node.js Package (next tag) 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | publish-npm: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 'lts/*' 17 | registry-url: https://registry.npmjs.org/ 18 | - run: npm ci 19 | - run: npm test 20 | - run: | 21 | npm version prerelease --no-git-tag-version \ 22 | --preid=`git rev-parse --short HEAD` 23 | npm publish --tag next 24 | env: 25 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 26 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Node.js Package 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | publish-npm: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 'lts/*' 17 | registry-url: https://registry.npmjs.org/ 18 | - run: npm ci 19 | - run: npm test 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 23 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Tests 3 | 4 | on: push 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 'lts/*' 14 | registry-url: https://registry.npmjs.org/ 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Run tests 18 | run: npm test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *sublime* 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | haad 2 | Haad 3 | Richard Littauer 4 | shamb0t 5 | adam-palazzo 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [community@orbitdb.org](mailto:community@orbitdb.org), which goes to all members of the @OrbitDB community team, or to [richardlitt@orbitdb.org](mailto:richardlitt@orbitdb.org), which goes only to [@RichardLitt](https://github.com/RichardLitt) or to [haadcode@orbitdb.org](mailto:haadcode@orbitdb.org), which goes only to [@haadcode](https://github.com/haadcode). 59 | 60 | All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 61 | 62 | Project maintainers who do not follow or enforce the Code of Conduct in good 63 | faith may face temporary or permanent repercussions as determined by other 64 | members of the project's leadership. 65 | 66 | ## Attribution 67 | 68 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 69 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 70 | 71 | [homepage]: https://www.contributor-covenant.org 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Please contribute! Here are some things that would be great: 4 | 5 | - [Open an issue!](https://github.com/orbitdb/orbit-db-access-controllers/issues/new) 6 | - Open a pull request! 7 | - Say hi! :wave: 8 | 9 | Please note that we have a [Code of Conduct](CODE_OF_CONDUCT.md), and that all activity in the [@OrbitDB](https://github.com/orbitdb) organization falls under it. Read it when you get the chance, as being part of this community means that you agree to abide by it. Thanks. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2019 Haja Networks Oy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CRDTs 2 | 3 | [![npm version](https://badge.fury.io/js/crdts.svg)](https://www.npmjs.com/package/crdts) 4 | [![CircleCI](https://circleci.com/gh/orbitdb/crdts.svg?style=shield)](https://circleci.com/gh/orbitdb/crdts) 5 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/orbitdb/Lobby) [![Matrix](https://img.shields.io/badge/matrix-%23orbitdb%3Apermaweb.io-blue.svg)](https://riot.permaweb.io/#/room/#orbitdb:permaweb.io) 6 | 7 | > A library of Conflict-Free Replicated Data Types for JavaScript. 8 | 9 | ***Work In Progress*** 10 | 11 | This module provides a set of Conflict-Free Replicated Data Types for your JavaScript programs. All CRDTs in this library, except G-Counter, are currently operation-based. 12 | 13 | CRDTs implemented in this module: 14 | 15 | - [G-Counter](https://github.com/orbitdb/crdts/blob/master/src/G-Counter.js) 16 | - [PN-Counter](https://github.com/orbitdb/crdts/blob/master/src/PN-Counter.js) 17 | - [G-Set](https://github.com/orbitdb/crdts/blob/master/src/G-Set.js) 18 | - [2P-Set](https://github.com/orbitdb/crdts/blob/master/src/2P-Set.js) 19 | - [OR-Set](https://github.com/orbitdb/crdts/blob/master/src/OR-Set.js) 20 | - [LWW-Set](https://github.com/orbitdb/crdts/blob/master/src/LWW-Set.js) 21 | 22 | ## Install 23 | 24 | This module uses [npm](https://www.npmjs.com/) and [node](https://nodejs.org/en/). 25 | 26 | To install, run: 27 | 28 | ```sh 29 | $ npm install crdts 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```javascript 35 | import { GCounter, PNCounter, GSet, TwoPSet, ORSet, LWWSet, GSet, ORSet, LWWSet } from 'crdts' 36 | ``` 37 | 38 | See the [source code for each CRDT](https://github.com/orbitdb/crdts/blob/master/src) for the APIs and [tests](https://github.com/orbitdb/crdts/blob/master/test/) for usage examples. 39 | 40 | ## Inheritance 41 | 42 | ``` 43 | +-----------++-----------++----------++---------++------------++------------+ 44 | Data Type | OR-Set || LWW-Set || 2P-Set || G-Set || G-Counter || PN-Counter | 45 | +-----------++-----------++----------++---------++------------++------------+ 46 | Base Class | CmRDT-Set | -- | 47 | |-----------------------------------------------+---------------------------+ 48 | CRDT Type | Operation-Based | State-based | 49 | +-----------------------------------------------+---------------------------+ 50 | ``` 51 | 52 | ## CRDTs Research 53 | 54 | To learn more about CRDTs, check out this research: 55 | 56 | - ["A comprehensive study of Convergent and Commutative Replicated Data Types"](http://hal.upmc.fr/inria-00555588/document) paper 57 | - [CRDTs on Wikipedia](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#Known_CRDTs) 58 | - [IPFS's CRDT research group](https://github.com/ipfs/research-CRDT) 59 | 60 | ## Contribute 61 | 62 | If you think this could be better, please [open an issue](https://github.com/orbitdb/crdts/issues/new)! 63 | 64 | Please note that all interactions in [@OrbitDB](https://github.com/OrbitDB) fall under our [Code of Conduct](CODE_OF_CONDUCT.md). 65 | 66 | ## License 67 | 68 | [MIT](LICENSE) © 2017-2019 Haja Networks Oy 69 | -------------------------------------------------------------------------------- /benchmark/benchmark-sets.js: -------------------------------------------------------------------------------- 1 | // All supported Set CRDTs 2 | import GSet from '../src/G-Set.js' 3 | import TwoPSet from '../src/2P-Set.js' 4 | import ORSet from '../src/OR-Set.js' 5 | import LWWSet from '../src/LWW-Set.js' 6 | 7 | // Choose your weapon from ^ 8 | const SetCRDT = GSet 9 | 10 | // State 11 | let crdt = new SetCRDT() 12 | 13 | // Metrics 14 | let totalQueries = 0 15 | let seconds = 0 16 | let queriesPerSecond = 0 17 | let lastTenSeconds = 0 18 | 19 | const queryLoop = () => { 20 | crdt.add(totalQueries) 21 | totalQueries++ 22 | lastTenSeconds++ 23 | queriesPerSecond++ 24 | setImmediate(queryLoop) 25 | } 26 | 27 | export default (() => { 28 | console.log('Starting benchmark....js') 29 | // Output metrics at 1 second interval 30 | setInterval(() => { 31 | seconds++ 32 | if (seconds % 10 === 0) { 33 | console.log(`--> Average of ${lastTenSeconds / 10} q/s in the last 10 seconds`) 34 | if (lastTenSeconds === 0) throw new Error('Problems!.js') 35 | lastTenSeconds = 0 36 | } 37 | console.log(`${queriesPerSecond} queries per second, ${totalQueries} queries in ${seconds} seconds`) 38 | queriesPerSecond = 0 39 | }, 1000) 40 | 41 | setImmediate(queryLoop) 42 | })() 43 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 8.2.0 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crdts", 3 | "version": "0.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "crdts", 9 | "version": "0.2.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "istanbul": "^0.4.5", 13 | "mocha": "^10.2.0" 14 | } 15 | }, 16 | "node_modules/abbrev": { 17 | "version": "1.0.9", 18 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", 19 | "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", 20 | "dev": true 21 | }, 22 | "node_modules/amdefine": { 23 | "version": "1.0.1", 24 | "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", 25 | "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", 26 | "dev": true, 27 | "optional": true, 28 | "engines": { 29 | "node": ">=0.4.2" 30 | } 31 | }, 32 | "node_modules/ansi-colors": { 33 | "version": "4.1.1", 34 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 35 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 36 | "dev": true, 37 | "engines": { 38 | "node": ">=6" 39 | } 40 | }, 41 | "node_modules/ansi-regex": { 42 | "version": "5.0.1", 43 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 44 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 45 | "dev": true, 46 | "engines": { 47 | "node": ">=8" 48 | } 49 | }, 50 | "node_modules/ansi-styles": { 51 | "version": "4.3.0", 52 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 53 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 54 | "dev": true, 55 | "dependencies": { 56 | "color-convert": "^2.0.1" 57 | }, 58 | "engines": { 59 | "node": ">=8" 60 | }, 61 | "funding": { 62 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 63 | } 64 | }, 65 | "node_modules/anymatch": { 66 | "version": "3.1.3", 67 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 68 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 69 | "dev": true, 70 | "dependencies": { 71 | "normalize-path": "^3.0.0", 72 | "picomatch": "^2.0.4" 73 | }, 74 | "engines": { 75 | "node": ">= 8" 76 | } 77 | }, 78 | "node_modules/argparse": { 79 | "version": "1.0.10", 80 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 81 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 82 | "dev": true, 83 | "dependencies": { 84 | "sprintf-js": "~1.0.2" 85 | } 86 | }, 87 | "node_modules/async": { 88 | "version": "1.5.2", 89 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 90 | "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", 91 | "dev": true 92 | }, 93 | "node_modules/balanced-match": { 94 | "version": "1.0.2", 95 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 96 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 97 | "dev": true 98 | }, 99 | "node_modules/binary-extensions": { 100 | "version": "2.2.0", 101 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 102 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 103 | "dev": true, 104 | "engines": { 105 | "node": ">=8" 106 | } 107 | }, 108 | "node_modules/brace-expansion": { 109 | "version": "1.1.11", 110 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 111 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 112 | "dev": true, 113 | "dependencies": { 114 | "balanced-match": "^1.0.0", 115 | "concat-map": "0.0.1" 116 | } 117 | }, 118 | "node_modules/braces": { 119 | "version": "3.0.2", 120 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 121 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 122 | "dev": true, 123 | "dependencies": { 124 | "fill-range": "^7.0.1" 125 | }, 126 | "engines": { 127 | "node": ">=8" 128 | } 129 | }, 130 | "node_modules/browser-stdout": { 131 | "version": "1.3.1", 132 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 133 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 134 | "dev": true 135 | }, 136 | "node_modules/camelcase": { 137 | "version": "6.3.0", 138 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 139 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 140 | "dev": true, 141 | "engines": { 142 | "node": ">=10" 143 | }, 144 | "funding": { 145 | "url": "https://github.com/sponsors/sindresorhus" 146 | } 147 | }, 148 | "node_modules/chalk": { 149 | "version": "4.1.2", 150 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 151 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 152 | "dev": true, 153 | "dependencies": { 154 | "ansi-styles": "^4.1.0", 155 | "supports-color": "^7.1.0" 156 | }, 157 | "engines": { 158 | "node": ">=10" 159 | }, 160 | "funding": { 161 | "url": "https://github.com/chalk/chalk?sponsor=1" 162 | } 163 | }, 164 | "node_modules/chalk/node_modules/has-flag": { 165 | "version": "4.0.0", 166 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 167 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 168 | "dev": true, 169 | "engines": { 170 | "node": ">=8" 171 | } 172 | }, 173 | "node_modules/chalk/node_modules/supports-color": { 174 | "version": "7.2.0", 175 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 176 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 177 | "dev": true, 178 | "dependencies": { 179 | "has-flag": "^4.0.0" 180 | }, 181 | "engines": { 182 | "node": ">=8" 183 | } 184 | }, 185 | "node_modules/chokidar": { 186 | "version": "3.5.3", 187 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 188 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 189 | "dev": true, 190 | "funding": [ 191 | { 192 | "type": "individual", 193 | "url": "https://paulmillr.com/funding/" 194 | } 195 | ], 196 | "dependencies": { 197 | "anymatch": "~3.1.2", 198 | "braces": "~3.0.2", 199 | "glob-parent": "~5.1.2", 200 | "is-binary-path": "~2.1.0", 201 | "is-glob": "~4.0.1", 202 | "normalize-path": "~3.0.0", 203 | "readdirp": "~3.6.0" 204 | }, 205 | "engines": { 206 | "node": ">= 8.10.0" 207 | }, 208 | "optionalDependencies": { 209 | "fsevents": "~2.3.2" 210 | } 211 | }, 212 | "node_modules/cliui": { 213 | "version": "7.0.4", 214 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 215 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 216 | "dev": true, 217 | "dependencies": { 218 | "string-width": "^4.2.0", 219 | "strip-ansi": "^6.0.0", 220 | "wrap-ansi": "^7.0.0" 221 | } 222 | }, 223 | "node_modules/color-convert": { 224 | "version": "2.0.1", 225 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 226 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 227 | "dev": true, 228 | "dependencies": { 229 | "color-name": "~1.1.4" 230 | }, 231 | "engines": { 232 | "node": ">=7.0.0" 233 | } 234 | }, 235 | "node_modules/color-name": { 236 | "version": "1.1.4", 237 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 238 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 239 | "dev": true 240 | }, 241 | "node_modules/concat-map": { 242 | "version": "0.0.1", 243 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 244 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 245 | "dev": true 246 | }, 247 | "node_modules/debug": { 248 | "version": "4.3.4", 249 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 250 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 251 | "dev": true, 252 | "dependencies": { 253 | "ms": "2.1.2" 254 | }, 255 | "engines": { 256 | "node": ">=6.0" 257 | }, 258 | "peerDependenciesMeta": { 259 | "supports-color": { 260 | "optional": true 261 | } 262 | } 263 | }, 264 | "node_modules/debug/node_modules/ms": { 265 | "version": "2.1.2", 266 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 267 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 268 | "dev": true 269 | }, 270 | "node_modules/decamelize": { 271 | "version": "4.0.0", 272 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 273 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 274 | "dev": true, 275 | "engines": { 276 | "node": ">=10" 277 | }, 278 | "funding": { 279 | "url": "https://github.com/sponsors/sindresorhus" 280 | } 281 | }, 282 | "node_modules/deep-is": { 283 | "version": "0.1.4", 284 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 285 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 286 | "dev": true 287 | }, 288 | "node_modules/diff": { 289 | "version": "5.0.0", 290 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 291 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 292 | "dev": true, 293 | "engines": { 294 | "node": ">=0.3.1" 295 | } 296 | }, 297 | "node_modules/emoji-regex": { 298 | "version": "8.0.0", 299 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 300 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 301 | "dev": true 302 | }, 303 | "node_modules/escalade": { 304 | "version": "3.1.1", 305 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 306 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 307 | "dev": true, 308 | "engines": { 309 | "node": ">=6" 310 | } 311 | }, 312 | "node_modules/escape-string-regexp": { 313 | "version": "4.0.0", 314 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 315 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 316 | "dev": true, 317 | "engines": { 318 | "node": ">=10" 319 | }, 320 | "funding": { 321 | "url": "https://github.com/sponsors/sindresorhus" 322 | } 323 | }, 324 | "node_modules/escodegen": { 325 | "version": "1.8.1", 326 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", 327 | "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", 328 | "dev": true, 329 | "dependencies": { 330 | "esprima": "^2.7.1", 331 | "estraverse": "^1.9.1", 332 | "esutils": "^2.0.2", 333 | "optionator": "^0.8.1" 334 | }, 335 | "bin": { 336 | "escodegen": "bin/escodegen.js", 337 | "esgenerate": "bin/esgenerate.js" 338 | }, 339 | "engines": { 340 | "node": ">=0.12.0" 341 | }, 342 | "optionalDependencies": { 343 | "source-map": "~0.2.0" 344 | } 345 | }, 346 | "node_modules/esprima": { 347 | "version": "2.7.3", 348 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", 349 | "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", 350 | "dev": true, 351 | "bin": { 352 | "esparse": "bin/esparse.js", 353 | "esvalidate": "bin/esvalidate.js" 354 | }, 355 | "engines": { 356 | "node": ">=0.10.0" 357 | } 358 | }, 359 | "node_modules/estraverse": { 360 | "version": "1.9.3", 361 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", 362 | "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", 363 | "dev": true, 364 | "engines": { 365 | "node": ">=0.10.0" 366 | } 367 | }, 368 | "node_modules/esutils": { 369 | "version": "2.0.3", 370 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 371 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 372 | "dev": true, 373 | "engines": { 374 | "node": ">=0.10.0" 375 | } 376 | }, 377 | "node_modules/fast-levenshtein": { 378 | "version": "2.0.6", 379 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 380 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 381 | "dev": true 382 | }, 383 | "node_modules/fill-range": { 384 | "version": "7.0.1", 385 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 386 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 387 | "dev": true, 388 | "dependencies": { 389 | "to-regex-range": "^5.0.1" 390 | }, 391 | "engines": { 392 | "node": ">=8" 393 | } 394 | }, 395 | "node_modules/find-up": { 396 | "version": "5.0.0", 397 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 398 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 399 | "dev": true, 400 | "dependencies": { 401 | "locate-path": "^6.0.0", 402 | "path-exists": "^4.0.0" 403 | }, 404 | "engines": { 405 | "node": ">=10" 406 | }, 407 | "funding": { 408 | "url": "https://github.com/sponsors/sindresorhus" 409 | } 410 | }, 411 | "node_modules/flat": { 412 | "version": "5.0.2", 413 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 414 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 415 | "dev": true, 416 | "bin": { 417 | "flat": "cli.js" 418 | } 419 | }, 420 | "node_modules/fs.realpath": { 421 | "version": "1.0.0", 422 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 423 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 424 | "dev": true 425 | }, 426 | "node_modules/fsevents": { 427 | "version": "2.3.2", 428 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 429 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 430 | "dev": true, 431 | "hasInstallScript": true, 432 | "optional": true, 433 | "os": [ 434 | "darwin" 435 | ], 436 | "engines": { 437 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 438 | } 439 | }, 440 | "node_modules/get-caller-file": { 441 | "version": "2.0.5", 442 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 443 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 444 | "dev": true, 445 | "engines": { 446 | "node": "6.* || 8.* || >= 10.*" 447 | } 448 | }, 449 | "node_modules/glob": { 450 | "version": "5.0.15", 451 | "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", 452 | "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", 453 | "dev": true, 454 | "dependencies": { 455 | "inflight": "^1.0.4", 456 | "inherits": "2", 457 | "minimatch": "2 || 3", 458 | "once": "^1.3.0", 459 | "path-is-absolute": "^1.0.0" 460 | }, 461 | "engines": { 462 | "node": "*" 463 | } 464 | }, 465 | "node_modules/glob-parent": { 466 | "version": "5.1.2", 467 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 468 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 469 | "dev": true, 470 | "dependencies": { 471 | "is-glob": "^4.0.1" 472 | }, 473 | "engines": { 474 | "node": ">= 6" 475 | } 476 | }, 477 | "node_modules/handlebars": { 478 | "version": "4.7.7", 479 | "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", 480 | "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", 481 | "dev": true, 482 | "dependencies": { 483 | "minimist": "^1.2.5", 484 | "neo-async": "^2.6.0", 485 | "source-map": "^0.6.1", 486 | "wordwrap": "^1.0.0" 487 | }, 488 | "bin": { 489 | "handlebars": "bin/handlebars" 490 | }, 491 | "engines": { 492 | "node": ">=0.4.7" 493 | }, 494 | "optionalDependencies": { 495 | "uglify-js": "^3.1.4" 496 | } 497 | }, 498 | "node_modules/handlebars/node_modules/source-map": { 499 | "version": "0.6.1", 500 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 501 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 502 | "dev": true, 503 | "engines": { 504 | "node": ">=0.10.0" 505 | } 506 | }, 507 | "node_modules/has-flag": { 508 | "version": "1.0.0", 509 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", 510 | "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", 511 | "dev": true, 512 | "engines": { 513 | "node": ">=0.10.0" 514 | } 515 | }, 516 | "node_modules/he": { 517 | "version": "1.2.0", 518 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 519 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 520 | "dev": true, 521 | "bin": { 522 | "he": "bin/he" 523 | } 524 | }, 525 | "node_modules/inflight": { 526 | "version": "1.0.6", 527 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 528 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 529 | "dev": true, 530 | "dependencies": { 531 | "once": "^1.3.0", 532 | "wrappy": "1" 533 | } 534 | }, 535 | "node_modules/inherits": { 536 | "version": "2.0.4", 537 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 538 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 539 | "dev": true 540 | }, 541 | "node_modules/is-binary-path": { 542 | "version": "2.1.0", 543 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 544 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 545 | "dev": true, 546 | "dependencies": { 547 | "binary-extensions": "^2.0.0" 548 | }, 549 | "engines": { 550 | "node": ">=8" 551 | } 552 | }, 553 | "node_modules/is-extglob": { 554 | "version": "2.1.1", 555 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 556 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 557 | "dev": true, 558 | "engines": { 559 | "node": ">=0.10.0" 560 | } 561 | }, 562 | "node_modules/is-fullwidth-code-point": { 563 | "version": "3.0.0", 564 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 565 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 566 | "dev": true, 567 | "engines": { 568 | "node": ">=8" 569 | } 570 | }, 571 | "node_modules/is-glob": { 572 | "version": "4.0.3", 573 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 574 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 575 | "dev": true, 576 | "dependencies": { 577 | "is-extglob": "^2.1.1" 578 | }, 579 | "engines": { 580 | "node": ">=0.10.0" 581 | } 582 | }, 583 | "node_modules/is-number": { 584 | "version": "7.0.0", 585 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 586 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 587 | "dev": true, 588 | "engines": { 589 | "node": ">=0.12.0" 590 | } 591 | }, 592 | "node_modules/is-plain-obj": { 593 | "version": "2.1.0", 594 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 595 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 596 | "dev": true, 597 | "engines": { 598 | "node": ">=8" 599 | } 600 | }, 601 | "node_modules/is-unicode-supported": { 602 | "version": "0.1.0", 603 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 604 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 605 | "dev": true, 606 | "engines": { 607 | "node": ">=10" 608 | }, 609 | "funding": { 610 | "url": "https://github.com/sponsors/sindresorhus" 611 | } 612 | }, 613 | "node_modules/isexe": { 614 | "version": "2.0.0", 615 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 616 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 617 | "dev": true 618 | }, 619 | "node_modules/istanbul": { 620 | "version": "0.4.5", 621 | "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", 622 | "integrity": "sha512-nMtdn4hvK0HjUlzr1DrKSUY8ychprt8dzHOgY2KXsIhHu5PuQQEOTM27gV9Xblyon7aUH/TSFIjRHEODF/FRPg==", 623 | "deprecated": "This module is no longer maintained, try this instead:\n npm i nyc\nVisit https://istanbul.js.org/integrations for other alternatives.", 624 | "dev": true, 625 | "dependencies": { 626 | "abbrev": "1.0.x", 627 | "async": "1.x", 628 | "escodegen": "1.8.x", 629 | "esprima": "2.7.x", 630 | "glob": "^5.0.15", 631 | "handlebars": "^4.0.1", 632 | "js-yaml": "3.x", 633 | "mkdirp": "0.5.x", 634 | "nopt": "3.x", 635 | "once": "1.x", 636 | "resolve": "1.1.x", 637 | "supports-color": "^3.1.0", 638 | "which": "^1.1.1", 639 | "wordwrap": "^1.0.0" 640 | }, 641 | "bin": { 642 | "istanbul": "lib/cli.js" 643 | } 644 | }, 645 | "node_modules/js-yaml": { 646 | "version": "3.14.1", 647 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 648 | "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 649 | "dev": true, 650 | "dependencies": { 651 | "argparse": "^1.0.7", 652 | "esprima": "^4.0.0" 653 | }, 654 | "bin": { 655 | "js-yaml": "bin/js-yaml.js" 656 | } 657 | }, 658 | "node_modules/js-yaml/node_modules/esprima": { 659 | "version": "4.0.1", 660 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 661 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 662 | "dev": true, 663 | "bin": { 664 | "esparse": "bin/esparse.js", 665 | "esvalidate": "bin/esvalidate.js" 666 | }, 667 | "engines": { 668 | "node": ">=4" 669 | } 670 | }, 671 | "node_modules/levn": { 672 | "version": "0.3.0", 673 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 674 | "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", 675 | "dev": true, 676 | "dependencies": { 677 | "prelude-ls": "~1.1.2", 678 | "type-check": "~0.3.2" 679 | }, 680 | "engines": { 681 | "node": ">= 0.8.0" 682 | } 683 | }, 684 | "node_modules/locate-path": { 685 | "version": "6.0.0", 686 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 687 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 688 | "dev": true, 689 | "dependencies": { 690 | "p-locate": "^5.0.0" 691 | }, 692 | "engines": { 693 | "node": ">=10" 694 | }, 695 | "funding": { 696 | "url": "https://github.com/sponsors/sindresorhus" 697 | } 698 | }, 699 | "node_modules/log-symbols": { 700 | "version": "4.1.0", 701 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 702 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 703 | "dev": true, 704 | "dependencies": { 705 | "chalk": "^4.1.0", 706 | "is-unicode-supported": "^0.1.0" 707 | }, 708 | "engines": { 709 | "node": ">=10" 710 | }, 711 | "funding": { 712 | "url": "https://github.com/sponsors/sindresorhus" 713 | } 714 | }, 715 | "node_modules/minimatch": { 716 | "version": "3.1.2", 717 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 718 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 719 | "dev": true, 720 | "dependencies": { 721 | "brace-expansion": "^1.1.7" 722 | }, 723 | "engines": { 724 | "node": "*" 725 | } 726 | }, 727 | "node_modules/minimist": { 728 | "version": "1.2.7", 729 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", 730 | "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", 731 | "dev": true, 732 | "funding": { 733 | "url": "https://github.com/sponsors/ljharb" 734 | } 735 | }, 736 | "node_modules/mkdirp": { 737 | "version": "0.5.6", 738 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 739 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 740 | "dev": true, 741 | "dependencies": { 742 | "minimist": "^1.2.6" 743 | }, 744 | "bin": { 745 | "mkdirp": "bin/cmd.js" 746 | } 747 | }, 748 | "node_modules/mocha": { 749 | "version": "10.2.0", 750 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", 751 | "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", 752 | "dev": true, 753 | "dependencies": { 754 | "ansi-colors": "4.1.1", 755 | "browser-stdout": "1.3.1", 756 | "chokidar": "3.5.3", 757 | "debug": "4.3.4", 758 | "diff": "5.0.0", 759 | "escape-string-regexp": "4.0.0", 760 | "find-up": "5.0.0", 761 | "glob": "7.2.0", 762 | "he": "1.2.0", 763 | "js-yaml": "4.1.0", 764 | "log-symbols": "4.1.0", 765 | "minimatch": "5.0.1", 766 | "ms": "2.1.3", 767 | "nanoid": "3.3.3", 768 | "serialize-javascript": "6.0.0", 769 | "strip-json-comments": "3.1.1", 770 | "supports-color": "8.1.1", 771 | "workerpool": "6.2.1", 772 | "yargs": "16.2.0", 773 | "yargs-parser": "20.2.4", 774 | "yargs-unparser": "2.0.0" 775 | }, 776 | "bin": { 777 | "_mocha": "bin/_mocha", 778 | "mocha": "bin/mocha.js" 779 | }, 780 | "engines": { 781 | "node": ">= 14.0.0" 782 | }, 783 | "funding": { 784 | "type": "opencollective", 785 | "url": "https://opencollective.com/mochajs" 786 | } 787 | }, 788 | "node_modules/mocha/node_modules/argparse": { 789 | "version": "2.0.1", 790 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 791 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 792 | "dev": true 793 | }, 794 | "node_modules/mocha/node_modules/glob": { 795 | "version": "7.2.0", 796 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 797 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 798 | "dev": true, 799 | "dependencies": { 800 | "fs.realpath": "^1.0.0", 801 | "inflight": "^1.0.4", 802 | "inherits": "2", 803 | "minimatch": "^3.0.4", 804 | "once": "^1.3.0", 805 | "path-is-absolute": "^1.0.0" 806 | }, 807 | "engines": { 808 | "node": "*" 809 | }, 810 | "funding": { 811 | "url": "https://github.com/sponsors/isaacs" 812 | } 813 | }, 814 | "node_modules/mocha/node_modules/glob/node_modules/minimatch": { 815 | "version": "3.1.2", 816 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 817 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 818 | "dev": true, 819 | "dependencies": { 820 | "brace-expansion": "^1.1.7" 821 | }, 822 | "engines": { 823 | "node": "*" 824 | } 825 | }, 826 | "node_modules/mocha/node_modules/has-flag": { 827 | "version": "4.0.0", 828 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 829 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 830 | "dev": true, 831 | "engines": { 832 | "node": ">=8" 833 | } 834 | }, 835 | "node_modules/mocha/node_modules/js-yaml": { 836 | "version": "4.1.0", 837 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 838 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 839 | "dev": true, 840 | "dependencies": { 841 | "argparse": "^2.0.1" 842 | }, 843 | "bin": { 844 | "js-yaml": "bin/js-yaml.js" 845 | } 846 | }, 847 | "node_modules/mocha/node_modules/minimatch": { 848 | "version": "5.0.1", 849 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", 850 | "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", 851 | "dev": true, 852 | "dependencies": { 853 | "brace-expansion": "^2.0.1" 854 | }, 855 | "engines": { 856 | "node": ">=10" 857 | } 858 | }, 859 | "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { 860 | "version": "2.0.1", 861 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 862 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 863 | "dev": true, 864 | "dependencies": { 865 | "balanced-match": "^1.0.0" 866 | } 867 | }, 868 | "node_modules/mocha/node_modules/supports-color": { 869 | "version": "8.1.1", 870 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 871 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 872 | "dev": true, 873 | "dependencies": { 874 | "has-flag": "^4.0.0" 875 | }, 876 | "engines": { 877 | "node": ">=10" 878 | }, 879 | "funding": { 880 | "url": "https://github.com/chalk/supports-color?sponsor=1" 881 | } 882 | }, 883 | "node_modules/ms": { 884 | "version": "2.1.3", 885 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 886 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 887 | "dev": true 888 | }, 889 | "node_modules/nanoid": { 890 | "version": "3.3.3", 891 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", 892 | "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", 893 | "dev": true, 894 | "bin": { 895 | "nanoid": "bin/nanoid.cjs" 896 | }, 897 | "engines": { 898 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 899 | } 900 | }, 901 | "node_modules/neo-async": { 902 | "version": "2.6.2", 903 | "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", 904 | "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", 905 | "dev": true 906 | }, 907 | "node_modules/nopt": { 908 | "version": "3.0.6", 909 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", 910 | "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", 911 | "dev": true, 912 | "dependencies": { 913 | "abbrev": "1" 914 | }, 915 | "bin": { 916 | "nopt": "bin/nopt.js" 917 | } 918 | }, 919 | "node_modules/normalize-path": { 920 | "version": "3.0.0", 921 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 922 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 923 | "dev": true, 924 | "engines": { 925 | "node": ">=0.10.0" 926 | } 927 | }, 928 | "node_modules/once": { 929 | "version": "1.4.0", 930 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 931 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 932 | "dev": true, 933 | "dependencies": { 934 | "wrappy": "1" 935 | } 936 | }, 937 | "node_modules/optionator": { 938 | "version": "0.8.3", 939 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 940 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 941 | "dev": true, 942 | "dependencies": { 943 | "deep-is": "~0.1.3", 944 | "fast-levenshtein": "~2.0.6", 945 | "levn": "~0.3.0", 946 | "prelude-ls": "~1.1.2", 947 | "type-check": "~0.3.2", 948 | "word-wrap": "~1.2.3" 949 | }, 950 | "engines": { 951 | "node": ">= 0.8.0" 952 | } 953 | }, 954 | "node_modules/p-limit": { 955 | "version": "3.1.0", 956 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 957 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 958 | "dev": true, 959 | "dependencies": { 960 | "yocto-queue": "^0.1.0" 961 | }, 962 | "engines": { 963 | "node": ">=10" 964 | }, 965 | "funding": { 966 | "url": "https://github.com/sponsors/sindresorhus" 967 | } 968 | }, 969 | "node_modules/p-locate": { 970 | "version": "5.0.0", 971 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 972 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 973 | "dev": true, 974 | "dependencies": { 975 | "p-limit": "^3.0.2" 976 | }, 977 | "engines": { 978 | "node": ">=10" 979 | }, 980 | "funding": { 981 | "url": "https://github.com/sponsors/sindresorhus" 982 | } 983 | }, 984 | "node_modules/path-exists": { 985 | "version": "4.0.0", 986 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 987 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 988 | "dev": true, 989 | "engines": { 990 | "node": ">=8" 991 | } 992 | }, 993 | "node_modules/path-is-absolute": { 994 | "version": "1.0.1", 995 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 996 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 997 | "dev": true, 998 | "engines": { 999 | "node": ">=0.10.0" 1000 | } 1001 | }, 1002 | "node_modules/picomatch": { 1003 | "version": "2.3.1", 1004 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1005 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1006 | "dev": true, 1007 | "engines": { 1008 | "node": ">=8.6" 1009 | }, 1010 | "funding": { 1011 | "url": "https://github.com/sponsors/jonschlinkert" 1012 | } 1013 | }, 1014 | "node_modules/prelude-ls": { 1015 | "version": "1.1.2", 1016 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 1017 | "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", 1018 | "dev": true, 1019 | "engines": { 1020 | "node": ">= 0.8.0" 1021 | } 1022 | }, 1023 | "node_modules/randombytes": { 1024 | "version": "2.1.0", 1025 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1026 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1027 | "dev": true, 1028 | "dependencies": { 1029 | "safe-buffer": "^5.1.0" 1030 | } 1031 | }, 1032 | "node_modules/readdirp": { 1033 | "version": "3.6.0", 1034 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1035 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1036 | "dev": true, 1037 | "dependencies": { 1038 | "picomatch": "^2.2.1" 1039 | }, 1040 | "engines": { 1041 | "node": ">=8.10.0" 1042 | } 1043 | }, 1044 | "node_modules/require-directory": { 1045 | "version": "2.1.1", 1046 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1047 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1048 | "dev": true, 1049 | "engines": { 1050 | "node": ">=0.10.0" 1051 | } 1052 | }, 1053 | "node_modules/resolve": { 1054 | "version": "1.1.7", 1055 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", 1056 | "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", 1057 | "dev": true 1058 | }, 1059 | "node_modules/safe-buffer": { 1060 | "version": "5.2.1", 1061 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1062 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1063 | "dev": true, 1064 | "funding": [ 1065 | { 1066 | "type": "github", 1067 | "url": "https://github.com/sponsors/feross" 1068 | }, 1069 | { 1070 | "type": "patreon", 1071 | "url": "https://www.patreon.com/feross" 1072 | }, 1073 | { 1074 | "type": "consulting", 1075 | "url": "https://feross.org/support" 1076 | } 1077 | ] 1078 | }, 1079 | "node_modules/serialize-javascript": { 1080 | "version": "6.0.0", 1081 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 1082 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 1083 | "dev": true, 1084 | "dependencies": { 1085 | "randombytes": "^2.1.0" 1086 | } 1087 | }, 1088 | "node_modules/source-map": { 1089 | "version": "0.2.0", 1090 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", 1091 | "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", 1092 | "dev": true, 1093 | "optional": true, 1094 | "dependencies": { 1095 | "amdefine": ">=0.0.4" 1096 | }, 1097 | "engines": { 1098 | "node": ">=0.8.0" 1099 | } 1100 | }, 1101 | "node_modules/sprintf-js": { 1102 | "version": "1.0.3", 1103 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1104 | "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", 1105 | "dev": true 1106 | }, 1107 | "node_modules/string-width": { 1108 | "version": "4.2.3", 1109 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1110 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1111 | "dev": true, 1112 | "dependencies": { 1113 | "emoji-regex": "^8.0.0", 1114 | "is-fullwidth-code-point": "^3.0.0", 1115 | "strip-ansi": "^6.0.1" 1116 | }, 1117 | "engines": { 1118 | "node": ">=8" 1119 | } 1120 | }, 1121 | "node_modules/strip-ansi": { 1122 | "version": "6.0.1", 1123 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1124 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1125 | "dev": true, 1126 | "dependencies": { 1127 | "ansi-regex": "^5.0.1" 1128 | }, 1129 | "engines": { 1130 | "node": ">=8" 1131 | } 1132 | }, 1133 | "node_modules/strip-json-comments": { 1134 | "version": "3.1.1", 1135 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1136 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1137 | "dev": true, 1138 | "engines": { 1139 | "node": ">=8" 1140 | }, 1141 | "funding": { 1142 | "url": "https://github.com/sponsors/sindresorhus" 1143 | } 1144 | }, 1145 | "node_modules/supports-color": { 1146 | "version": "3.2.3", 1147 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", 1148 | "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", 1149 | "dev": true, 1150 | "dependencies": { 1151 | "has-flag": "^1.0.0" 1152 | }, 1153 | "engines": { 1154 | "node": ">=0.8.0" 1155 | } 1156 | }, 1157 | "node_modules/to-regex-range": { 1158 | "version": "5.0.1", 1159 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1160 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1161 | "dev": true, 1162 | "dependencies": { 1163 | "is-number": "^7.0.0" 1164 | }, 1165 | "engines": { 1166 | "node": ">=8.0" 1167 | } 1168 | }, 1169 | "node_modules/type-check": { 1170 | "version": "0.3.2", 1171 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1172 | "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", 1173 | "dev": true, 1174 | "dependencies": { 1175 | "prelude-ls": "~1.1.2" 1176 | }, 1177 | "engines": { 1178 | "node": ">= 0.8.0" 1179 | } 1180 | }, 1181 | "node_modules/uglify-js": { 1182 | "version": "3.17.4", 1183 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", 1184 | "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", 1185 | "dev": true, 1186 | "optional": true, 1187 | "bin": { 1188 | "uglifyjs": "bin/uglifyjs" 1189 | }, 1190 | "engines": { 1191 | "node": ">=0.8.0" 1192 | } 1193 | }, 1194 | "node_modules/which": { 1195 | "version": "1.3.1", 1196 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1197 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1198 | "dev": true, 1199 | "dependencies": { 1200 | "isexe": "^2.0.0" 1201 | }, 1202 | "bin": { 1203 | "which": "bin/which" 1204 | } 1205 | }, 1206 | "node_modules/word-wrap": { 1207 | "version": "1.2.3", 1208 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 1209 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 1210 | "dev": true, 1211 | "engines": { 1212 | "node": ">=0.10.0" 1213 | } 1214 | }, 1215 | "node_modules/wordwrap": { 1216 | "version": "1.0.0", 1217 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 1218 | "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", 1219 | "dev": true 1220 | }, 1221 | "node_modules/workerpool": { 1222 | "version": "6.2.1", 1223 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", 1224 | "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", 1225 | "dev": true 1226 | }, 1227 | "node_modules/wrap-ansi": { 1228 | "version": "7.0.0", 1229 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1230 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1231 | "dev": true, 1232 | "dependencies": { 1233 | "ansi-styles": "^4.0.0", 1234 | "string-width": "^4.1.0", 1235 | "strip-ansi": "^6.0.0" 1236 | }, 1237 | "engines": { 1238 | "node": ">=10" 1239 | }, 1240 | "funding": { 1241 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1242 | } 1243 | }, 1244 | "node_modules/wrappy": { 1245 | "version": "1.0.2", 1246 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1247 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1248 | "dev": true 1249 | }, 1250 | "node_modules/y18n": { 1251 | "version": "5.0.8", 1252 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1253 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1254 | "dev": true, 1255 | "engines": { 1256 | "node": ">=10" 1257 | } 1258 | }, 1259 | "node_modules/yargs": { 1260 | "version": "16.2.0", 1261 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1262 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1263 | "dev": true, 1264 | "dependencies": { 1265 | "cliui": "^7.0.2", 1266 | "escalade": "^3.1.1", 1267 | "get-caller-file": "^2.0.5", 1268 | "require-directory": "^2.1.1", 1269 | "string-width": "^4.2.0", 1270 | "y18n": "^5.0.5", 1271 | "yargs-parser": "^20.2.2" 1272 | }, 1273 | "engines": { 1274 | "node": ">=10" 1275 | } 1276 | }, 1277 | "node_modules/yargs-parser": { 1278 | "version": "20.2.4", 1279 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1280 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1281 | "dev": true, 1282 | "engines": { 1283 | "node": ">=10" 1284 | } 1285 | }, 1286 | "node_modules/yargs-unparser": { 1287 | "version": "2.0.0", 1288 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1289 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1290 | "dev": true, 1291 | "dependencies": { 1292 | "camelcase": "^6.0.0", 1293 | "decamelize": "^4.0.0", 1294 | "flat": "^5.0.2", 1295 | "is-plain-obj": "^2.1.0" 1296 | }, 1297 | "engines": { 1298 | "node": ">=10" 1299 | } 1300 | }, 1301 | "node_modules/yocto-queue": { 1302 | "version": "0.1.0", 1303 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1304 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1305 | "dev": true, 1306 | "engines": { 1307 | "node": ">=10" 1308 | }, 1309 | "funding": { 1310 | "url": "https://github.com/sponsors/sindresorhus" 1311 | } 1312 | } 1313 | } 1314 | } 1315 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crdts", 3 | "version": "0.2.0", 4 | "description": "A library of Conflict-Free Replicated Data Types for JavaScript.", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/orbitdb/crdts" 10 | }, 11 | "bugs": "https://github.com/orbitdb/crdts/issues", 12 | "author": "Haad", 13 | "homepage": "https://github.com/orbitdb/crdts", 14 | "license": "MIT", 15 | "keywords": [ 16 | "crdt", 17 | "crdts", 18 | "g-counter", 19 | "g-set", 20 | "or-set", 21 | "lww-set", 22 | "2p-set" 23 | ], 24 | "directories": { 25 | "test": "test" 26 | }, 27 | "scripts": { 28 | "test": "mocha", 29 | "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha" 30 | }, 31 | "localMaintainers": [ 32 | "haad ", 33 | "shamb0t ", 34 | "hajamark " 35 | ], 36 | "devDependencies": { 37 | "istanbul": "^0.4.5", 38 | "mocha": "^10.2.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/2P-Set.js: -------------------------------------------------------------------------------- 1 | import CRDTSet from './CmRDT-Set.js' 2 | import GSet from './G-Set.js' 3 | 4 | /** 5 | * 2P-Set 6 | * 7 | * Operation-based Two-Phase Set CRDT 8 | * 9 | * See base class CmRDT-Set.js for the rest of the API 10 | * https://github.com/orbitdb/crdts/blob/master/src/CmRDT-Set.js 11 | * 12 | * Sources: 13 | * "A comprehensive study of Convergent and Commutative Replicated Data Types" 14 | * http://hal.upmc.fr/inria-00555588/document, "3.3.2 2P-Set" 15 | */ 16 | export default class TwoPSet extends CRDTSet { 17 | /** 18 | * Create a new TwoPSet instance 19 | * @param {[Iterable]} added [Added values] 20 | * @param {[Iterable]} removed [Removed values] 21 | */ 22 | constructor (added, removed) { 23 | super() 24 | // We track the operations and state differently 25 | // than the base class: use two GSets for operations 26 | this._added = new GSet(added) 27 | this._removed = new GSet(removed) 28 | } 29 | 30 | /** 31 | * Return all values added to the Set 32 | * @override 33 | * @return {[Iterator]} [Iterator for values in the Set] 34 | */ 35 | values () { 36 | // A value is included in the set if it's present in 37 | // the add set and not present in the remove set. We can 38 | // determine this by calculating the difference between 39 | // adds and removes. 40 | const difference = GSet.difference(this._added, this._removed) 41 | return difference.values() 42 | } 43 | 44 | /** 45 | * Add a value to the Set 46 | * @param {[Any]} value [Value to add to the Set] 47 | */ 48 | add (element) { 49 | this._added.add(element) 50 | } 51 | 52 | /** 53 | * Remove a value from the Set 54 | * @override 55 | * @param {[Any]} element [Value to remove from the Set] 56 | */ 57 | remove (element) { 58 | // Only add the value to the remove set if it exists in the add set 59 | if (this._added.has(element)) { 60 | this._removed.add(element) 61 | } 62 | } 63 | 64 | /** 65 | * Merge the Set with another Set 66 | * @override 67 | * @param {[TwoPSet]} other [Set to merge with] 68 | */ 69 | merge (other) { 70 | this._added = new GSet(this._added.toArray().concat(other._added.toArray())) 71 | this._removed = new GSet(this._removed.toArray().concat(other._removed.toArray())) 72 | } 73 | 74 | /** 75 | * TwoPSet as an Object that can be JSON.stringified 76 | * @return {[Object]} [Object in the shape of `{ values: { added: [], removed: [] } }`] 77 | */ 78 | toJSON () { 79 | return { 80 | values: { 81 | added: this._added.toArray(), 82 | removed: this._removed.toArray(), 83 | }, 84 | } 85 | } 86 | 87 | /** 88 | * Create TwoPSet from a json object 89 | * @param {[Object]} json [Input object to create the GSet from. Needs to be: '{ values: { added: [...], removed: [...] } }'] 90 | * @return {[TwoPSet]} [new TwoPSet instance] 91 | */ 92 | static from (json) { 93 | return new TwoPSet(json.values.added, json.values.removed) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/CmRDT-Set.js: -------------------------------------------------------------------------------- 1 | import { OperationTuple3 } from './utils.js' 2 | 3 | /** 4 | * CmRDT-Set 5 | * 6 | * Base Class for Operation-Based Set CRDT. Provides a Set interface. 7 | * 8 | * Operations are described as: 9 | * 10 | * Operation = Tuple3(value : Any, added : Set, removed : Set) 11 | * 12 | * This class is meant to be used as a base class for 13 | * Operation-Based CRDTs that can be derived from Set 14 | * semantics and which calculate the state (values) 15 | * based on a set of operations. 16 | * 17 | * Used by: 18 | * G-Set - https://github.com/orbitdb/crdts/blob/master/src/G-Set.js 19 | * OR-Set - https://github.com/orbitdb/crdts/blob/master/src/OR-Set.js 20 | * 2P-Set - https://github.com/orbitdb/crdts/blob/master/src/2P-Set.js 21 | * LWW-Set - https://github.com/orbitdb/crdts/blob/master/LWW-Set.js 22 | * 23 | * Sources: 24 | * "A comprehensive study of Convergent and Commutative Replicated Data Types" 25 | * http://hal.upmc.fr/inria-00555588/document 26 | */ 27 | export default class CmRDTSet extends Set { 28 | /** 29 | * Create a new CmRDTSet instance 30 | * @override 31 | * 32 | * The constructor should never be used directly 33 | * but rather via `super()` call in the constructor of 34 | * the class that inherits from CmRDTSet 35 | * 36 | * @param {[Iterable]} iterable [Opetional Iterable object (eg. Array, Set) to create the Set from] 37 | * @param {[Object]} options [Options to pass to the Set. Currently supported: `{ compareFunc: (a, b) => true|false }`] 38 | */ 39 | constructor (iterable, options) { 40 | super() 41 | // Internal cache for tracking which values have been added to the set 42 | this._values = new Set() 43 | // List of operations (adds or removes of a value) for this set as 44 | // Operation : Tuple3(value : Any, added : Set, removed : Set) 45 | // added and removed can be any value, eg. it can be used to store 46 | // timestamps/clocks for each operation in order to determine if 47 | // a value is in the set 48 | this._operations = iterable ? iterable.map(OperationTuple3.from) : [] 49 | // Internal options 50 | this._options = options || {} 51 | } 52 | 53 | /** 54 | * Return the values in the Set 55 | * @override 56 | * @return {[Set]} [Values in this set] 57 | */ 58 | values () { 59 | const shouldIncludeValue = e => this._resolveValueState(e.added, e.removed, this._options.compareFunc) 60 | const getValue = e => e.value 61 | // Filter out values that should not be in this set 62 | // by using the _resolveValueState() function to determine 63 | // if the value should be present 64 | const state = this._operations 65 | .filter(shouldIncludeValue) 66 | .map(getValue) 67 | return new Set(state).values() 68 | } 69 | 70 | /** 71 | * Check if this Set has a value 72 | * @param {[Any]} value [Value to look for] 73 | * @return {Boolean} [True if value is in the Set, false if not] 74 | */ 75 | has (value) { 76 | return new Set(this.values()).has(value) 77 | } 78 | 79 | /** 80 | * Check if this Set has all values of an input array 81 | * @param {[Array]} values [Values that should be in the Set] 82 | * @return {Boolean} [True if all values are in the Set, false if not] 83 | */ 84 | hasAll (values) { 85 | const contains = e => this.has(e) 86 | return values.every(contains) 87 | } 88 | 89 | /** 90 | * Add a value to the Set 91 | * @override 92 | * 93 | * Optionally, a "tag" can be given for the add operation, 94 | * for example the tag can be a clock or other identifier 95 | * that can be used to determine together with remove operations, 96 | * whether a value is included in the Set 97 | * 98 | * @param {[Any]} value [Value to add to the Set] 99 | * @param {[Any]} tag [Optional tag for this add operation, eg. a clock] 100 | */ 101 | add (value, tag) { 102 | // If the value is not in the set yet 103 | if (!this._values.has(value)) { 104 | // Create an operation for the value and apply it to this set 105 | const addOperation = OperationTuple3.create(value, [tag], null) 106 | this._applyOperation(addOperation) 107 | } else { 108 | // If the value is in the set, add a tag to its added set 109 | this._findOperationsFor(value).map(val => val.added.add(tag)) 110 | } 111 | } 112 | 113 | /** 114 | * Remove a value from the Set 115 | * @override 116 | * 117 | * Optionally, a "tag" can be given for the remove operation, 118 | * for example the tag can be a clock or other identifier 119 | * that can be used to determine together with add operations, 120 | * whether a value is included in the Set 121 | * 122 | * @param {[Any]} value [Value to remove from the Set] 123 | * @param {[Any]} tag [Optional tag for this remove operation, eg. a clock] 124 | */ 125 | remove (value, tag) { 126 | // Add a remove tag to the value's removed set, and only 127 | // apply the remove operation if the value was added previously 128 | this._findOperationsFor(value).map(e => e.removed.add(tag)) 129 | } 130 | 131 | /** 132 | * Merge the Set with another Set 133 | * @override 134 | * @param {[CRDTSet]} other [Set to merge with] 135 | */ 136 | merge (other) { 137 | other._operations.forEach(operation => { 138 | const value = operation.value 139 | if (!this._values.has(value)) { 140 | // If we don't have the value yet, add it with all tags from other's operation 141 | const op = OperationTuple3.create(value, operation.added, operation.removed) 142 | this._applyOperation(op) 143 | } else { 144 | // If this set has the value 145 | this._findOperationsFor(value).map(op => { 146 | // Add all add and remove tags from other's operations to value in this set 147 | operation.added.forEach(e => op.added.add(e)) 148 | operation.removed.forEach(e => op.removed.add(e)) 149 | }) 150 | } 151 | }) 152 | } 153 | 154 | /** 155 | * CmRDT-Set as an Object that can be JSON.stringified 156 | * @return {[Object]} [Object in the shape of `{ values: [ { value: , added: [], removed: [] } ] }`] 157 | */ 158 | toJSON () { 159 | const values = this._operations.map(e => { 160 | return { 161 | value: e.value, 162 | added: Array.from(e.added), 163 | removed: Array.from(e.removed), 164 | } 165 | }) 166 | return { values: values } 167 | } 168 | 169 | /** 170 | * Create an Array of the values of this Set 171 | * @return {[Array]} [Values of this Set as an Array] 172 | */ 173 | toArray () { 174 | return Array.from(this.values()) 175 | } 176 | 177 | /** 178 | * Check if this Set equal another Set 179 | * @param {[type]} other [Set to compare] 180 | * @return {Boolean} [True if this Set is the same as the other Set] 181 | */ 182 | isEqual (other) { 183 | return CmRDTSet.isEqual(this, other) 184 | } 185 | 186 | /** 187 | * _resolveValueState function is used to determine if an element is present in a Set. 188 | * 189 | * It receives a Set of add tags and a Set of remove tags for an element as arguments. 190 | * It returns true if an element should be included in the state and false if not. 191 | * 192 | * Overwriting this function gives us the ability to compare add/remove operations 193 | * of a particular element (value) in the set and determine if the value should be 194 | * included in the set or not. The function gets called once per element and returning 195 | * true will include the value in the set and returning false will exclude it from the set. 196 | * 197 | * @param {[type]} added [Set of added elements] 198 | * @param {[type]} removed [Set of removed elements] 199 | * @param {[type]} compareFunc [Comparison function to compare elements with] 200 | * @return {[type]} [true if element should be included in the current state] 201 | */ 202 | _resolveValueState (added, removed, compareFunc) { 203 | // By default, if there's an add operation present, 204 | // and there are no remove operations, we include 205 | // the value in the set 206 | return added.size > 0 && removed.size === 0 207 | } 208 | 209 | /** 210 | * Add a value to the internal cache 211 | * @param {[OperationTuple3]} operationTuple3 [Tuple3(value, addedTags, removedTags)] 212 | */ 213 | _applyOperation (operationTuple3) { 214 | this._operations.push(operationTuple3) 215 | this._values.add(operationTuple3.value) 216 | } 217 | 218 | /** 219 | * Find a value and its operations from this set's internal cache 220 | * @private 221 | * 222 | * Returns a value and all its operations as a OperationTuple3: 223 | * Operation : Tuple3(value : Any, added : Set, removed : Set) 224 | * 225 | * Where 'value' is the value in the set, 'added' is all add operations 226 | * and 'removed' are all remove operations for that value. 227 | * 228 | * @param {[Any]} value [Value to find] 229 | * @return {[Any]} [Value if found, undefined if value was not found] 230 | */ 231 | _findOperationsFor (value) { 232 | let operations = [] 233 | if (this._values.has(value)) { 234 | const isForValue = e => e.value === value 235 | const notNull = e => e !== undefined 236 | operations = [this._operations.find(isForValue)].filter(notNull) 237 | } 238 | return operations 239 | } 240 | 241 | /** 242 | * Create Set from a json object 243 | * @param {[Object]} json [Input object to create the Set from. Needs to be: '{ values: [] }'] 244 | * @return {[Set]} [new Set instance] 245 | */ 246 | static from (json) { 247 | return new CmRDTSet(json.values) 248 | } 249 | 250 | /** 251 | * Check if two Set are equal 252 | * 253 | * Two Set are equal if they both contain exactly 254 | * the same values. 255 | * 256 | * @param {[Set]} a [Set to compare] 257 | * @param {[Set]} b [Set to compare] 258 | * @return {Boolean} [True input Set are the same] 259 | */ 260 | static isEqual (a, b) { 261 | return (a.toArray().length === b.toArray().length) 262 | && a.hasAll(b.toArray()) 263 | } 264 | 265 | /** 266 | * Return the difference between the values of two Sets 267 | * 268 | * @param {[Set]} a [First Set] 269 | * @param {[Set]} b [Second Set] 270 | * @return {[Set]} [Set of values that are in Set A but not in Set B] 271 | */ 272 | static difference (a, b) { 273 | const otherDoesntInclude = x => !b.has(x) 274 | const difference = new Set([...a.values()].filter(otherDoesntInclude)) 275 | return difference 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/G-Counter.js: -------------------------------------------------------------------------------- 1 | import { deepEqual } from './utils.js' 2 | const sum = (acc, val) => acc + val 3 | 4 | /** 5 | * G-Counter 6 | * 7 | * Operation-based Increment-Only Counter CRDT 8 | * 9 | * Sources: 10 | * "A comprehensive study of Convergent and Commutative Replicated Data Types" 11 | * http://hal.upmc.fr/inria-00555588/document, "3.1.1 Op-based counter and 3.1.2 State-based increment-only Counter (G-Counter)" 12 | */ 13 | 14 | export default class GCounter { 15 | constructor (id, counter) { 16 | this.id = id 17 | this._counters = counter ? counter : {} 18 | this._counters[this.id] = this._counters[this.id] ? this._counters[this.id] : 0 19 | } 20 | 21 | get value () { 22 | return Object.values(this._counters).reduce(sum, 0) 23 | } 24 | 25 | increment (amount) { 26 | if (amount && amount < 1) 27 | return 28 | 29 | if (amount === undefined || amount === null) 30 | amount = 1 31 | 32 | this._counters[this.id] = this._counters[this.id] + amount 33 | } 34 | 35 | merge (other) { 36 | // Go through each counter in the other counter 37 | Object.entries(other._counters).forEach(([id, value]) => { 38 | // Take the maximum of the counter value we have or the counter value they have 39 | this._counters[id] = Math.max(this._counters[id] || 0, value) 40 | }) 41 | } 42 | 43 | toJSON () { 44 | return { 45 | id: this.id, 46 | counters: this._counters 47 | } 48 | } 49 | 50 | isEqual (other) { 51 | return GCounter.isEqual(this, other) 52 | } 53 | 54 | static from (json) { 55 | return new GCounter(json.id, json.counters) 56 | } 57 | 58 | static isEqual (a, b) { 59 | if(a.id !== b.id) 60 | return false 61 | 62 | return deepEqual(a._counters, b._counters) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/G-Set.js: -------------------------------------------------------------------------------- 1 | import CRDTSet from './CmRDT-Set.js' 2 | 3 | /** 4 | * G-Set 5 | * 6 | * Operation-based Grow-Only Set CRDT 7 | * 8 | * G stands for "Grow-Only" which means that values can only 9 | * ever be added to the set, they can never be removed. 10 | * 11 | * See base class CmRDT-Set.js for the rest of the API 12 | * https://github.com/orbitdb/crdts/blob/master/src/CmRDT-Set.js 13 | * 14 | * Used by: 15 | * 2P-Set - https://github.com/orbitdb/crdts/blob/master/src/2P-Set.js 16 | * 17 | * Sources: 18 | * "A comprehensive study of Convergent and Commutative Replicated Data Types" 19 | * http://hal.upmc.fr/inria-00555588/document, "3.3.1 Grow-Only Set (G-Set)" 20 | */ 21 | export default class GSet extends CRDTSet { 22 | /** 23 | * Create a G-Set CRDT instance 24 | * @param {[Iterable]} iterable [Opetional Iterable object (eg. Array, Set) to create the GSet from] 25 | */ 26 | constructor (iterable) { 27 | super() 28 | this._values = new Set(iterable) 29 | } 30 | 31 | /** 32 | * Return all values added to the Set 33 | * @return {[Iterator]} [Iterator for values in the Set] 34 | */ 35 | values () { 36 | return this._values.values() 37 | } 38 | 39 | /** 40 | * Add a value to the Set 41 | * 42 | * Values can only be ever added to a G-Set, 43 | * removing values is not possible (Grow-Only) 44 | * 45 | * @param {[Any]} value [Value to add to the Set] 46 | */ 47 | add (value) { 48 | this._values.add(value) 49 | } 50 | 51 | // G-Set doesn't allow removal of values, throw an error 52 | // Including this to satisfy normal Set API in case the user 53 | // accidentally calls remove on GSet 54 | remove (value) { 55 | throw new Error(`G-Set doesn't allow removing values`) 56 | } 57 | 58 | /** 59 | * Merge another GSet to this GSet 60 | * @param {[GSet]} other [GSet to merge with] 61 | */ 62 | merge (other) { 63 | // Merge values of other set with this set 64 | this._values = new Set([...this._values, ...other._values]) 65 | } 66 | 67 | /** 68 | * GSet as an Object that can be JSON.stringified 69 | * @return {[Object]} [Object in the shape of `{ values: [] }`] 70 | */ 71 | toJSON () { 72 | return { 73 | values: this.toArray(), 74 | } 75 | } 76 | 77 | /** 78 | * Create GSet from a json object 79 | * @param {[Object]} json [Input object to create the GSet from. Needs to be: '{ values: [] }'] 80 | * @return {[GSet]} [new GSet instance] 81 | */ 82 | static from (json) { 83 | return new GSet(json.values) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/LWW-Set.js: -------------------------------------------------------------------------------- 1 | import CRDTSet from './CmRDT-Set.js' 2 | 3 | /** 4 | * LWWSet-Set 5 | * 6 | * Operation-based Last-Write-Wins Set CRDT 7 | * 8 | * See base class CmRDT-Set.js for the rest of the API 9 | * https://github.com/orbitdb/crdts/blob/master/src/CmRDT-Set.js 10 | * 11 | * Sources: 12 | * "A comprehensive study of Convergent and Commutative Replicated Data Types" 13 | * http://hal.upmc.fr/inria-00555588/document, "Figure 8: LWW-Set (state-based)" 14 | */ 15 | export default class LWWSet extends CRDTSet { 16 | /** 17 | * @override 18 | * 19 | * _resolveValueState function is used to determine if an element is present in a Set. 20 | * 21 | * It receives a Set of add tags and a Set of remove tags for an element as arguments. 22 | * It returns true if an element should be included in the state and false if not. 23 | * 24 | * Overwriting this function gives us the ability to compare add/remove operations 25 | * of a particular element (value) in the set and determine if the value should be 26 | * included in the set or not. The function gets called once per element and returning 27 | * true will include the value in the set and returning false will exclude it from the set. 28 | * 29 | * @param {[type]} added [Set of added elements] 30 | * @param {[type]} removed [Set of removed elements] 31 | * @param {[type]} compareFunc [Comparison function to compare elements with] 32 | * @return {[type]} [true if element should be included in the current state] 33 | */ 34 | _resolveValueState (added, removed, compareFunc) { 35 | // Sort both sets with the given comparison function 36 | // or use "distance" sort by default 37 | compareFunc = compareFunc ? compareFunc : (a, b) => (a || 0) - (b || 0) 38 | const sortedAdded = Array.from(added).sort(compareFunc).reverse() 39 | const sortedRemoved = Array.from(removed).sort(compareFunc).reverse() 40 | // If the latest add operation is greater or equal than latest remove operation, 41 | // we include it in the state 42 | return compareFunc(sortedAdded[0], sortedRemoved[0]) > -1 43 | } 44 | 45 | /** 46 | * Create LWWSet from a json object 47 | * @param {[Object]} json [Input object to create the LWWSet from. Needs to be: '{ values: [] }'] 48 | * @return {[LWWSet]} [new LWWSet instance] 49 | */ 50 | static from (json) { 51 | return new LWWSet(json.values) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/OR-Set.js: -------------------------------------------------------------------------------- 1 | import CRDTSet from './CmRDT-Set.js' 2 | 3 | /** 4 | * OR-Set 5 | * 6 | * Operation-based Observed-Remove Set CRDT 7 | * 8 | * See base class CmRDT-Set.js for the rest of the API 9 | * https://github.com/orbitdb/crdts/blob/master/src/CmRDT-Set.js 10 | * 11 | * Sources: 12 | * "A comprehensive study of Convergent and Commutative Replicated Data Types" 13 | * http://hal.upmc.fr/inria-00555588/document, "3.3.5 Observed-Remove Set (OR-Set)" 14 | */ 15 | export default class ORSet extends CRDTSet { 16 | /** 17 | * @override 18 | * 19 | * Remove a value from the Set 20 | * 21 | * Overriding the remove functionality for OR-Set, so that we 22 | * have the Observed-remove mechanics: when a remove operation 23 | * is executed, we add all the known add operation tags to the 24 | * removed tags allowing us to exclude the value from the set 25 | * in _resolveState() if all given add tags are present in 26 | * remove tags. 27 | * 28 | * @param {[Any]} value [Value to remove from the Set] 29 | * @param {[Any]} tag [Optional tag for this remove operation, eg. a clock] 30 | */ 31 | remove (value) { 32 | // Add all observed (known) add tags to the removed tags 33 | const removeObserved = e => e.removed = new Set([...e.added, ...e.removed]) 34 | // Create a remove operation for the value if it exists 35 | this._findOperationsFor(value).map(removeObserved) 36 | } 37 | 38 | /** 39 | * @override 40 | * 41 | * _resolveValueState function is used to determine if an element is present in a Set. 42 | * 43 | * It receives a Set of add tags and a Set of remove tags for an element as arguments. 44 | * It returns true if an element should be included in the state and false if not. 45 | * 46 | * Overwriting this function gives us the ability to compare add/remove operations 47 | * of a particular element (value) in the set and determine if the value should be 48 | * included in the set or not. The function gets called once per element and returning 49 | * true will include the value in the set and returning false will exclude it from the set. 50 | * 51 | * @param {[type]} added [Set of added elements] 52 | * @param {[type]} removed [Set of removed elements] 53 | * @param {[type]} compareFunc [Comparison function to compare elements with] 54 | * @return {[type]} [true if element should be included in the current state] 55 | */ 56 | _resolveValueState (added, removed, compareFunc) { 57 | // Check if a tag is included in the remove set 58 | const hasMatchingRemoveOperation = addTag => { 59 | // Check if remove tags includes the add tag, ie. check for 60 | // equality for the tags using a provided comparison function 61 | if (compareFunc) { 62 | return !Array.from(removed).some(removeTag => compareFunc(removeTag, addTag)) 63 | } 64 | 65 | // If remove set doesn't have the tag, 66 | // return true to include the value in the state 67 | return !removed.has(addTag) 68 | } 69 | // If the remove set doesn't include the add tag, 70 | // return true to include the value in the state 71 | return Array.from(added).filter(hasMatchingRemoveOperation).length > 0 72 | } 73 | 74 | /** 75 | * Create ORSet from a json object 76 | * @param {[Object]} json [Input object to create the ORSet from. Needs to be: '{ values: [] }'] 77 | * @return {[ORSet]} [new ORSet instance] 78 | */ 79 | static from (json) { 80 | return new ORSet(json.values) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/PN-Counter.js: -------------------------------------------------------------------------------- 1 | import GCounter from '../src/G-Counter.js' 2 | 3 | const isGCounter = (obj) => obj && obj instanceof GCounter 4 | 5 | export default class PNCounter { 6 | constructor (id, pCounters, nCounters) { 7 | this.id = id 8 | this.p = isGCounter(pCounters) ? pCounters : new GCounter(id, pCounters) 9 | this.n = isGCounter(nCounters) ? nCounters : new GCounter(id, nCounters) 10 | } 11 | 12 | get value() { 13 | return this.p.value - this.n.value 14 | } 15 | 16 | increment (amount) { 17 | this.p.increment(amount) 18 | } 19 | 20 | decrement (amount) { 21 | this.n.increment(amount) 22 | } 23 | 24 | merge (other) { 25 | this.p.merge(other.p) 26 | this.n.merge(other.n) 27 | } 28 | 29 | toJSON () { 30 | return { 31 | id: this.id, 32 | p: this.p._counters, 33 | n: this.n._counters 34 | } 35 | } 36 | 37 | isEqual (other) { 38 | return PNCounter.isEqual(this, other) 39 | } 40 | 41 | static from (json) { 42 | return new PNCounter(json.id, json.p, json.n) 43 | } 44 | 45 | static isEqual (a, b) { 46 | return GCounter.isEqual(a.p, b.p) && GCounter.isEqual(a.n, b.n) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Export all supported CRDTs 2 | export { default as GCounter } from './G-Counter.js' 3 | export { default as CmRDTSet } from './CmRDT-Set.js' 4 | export { default as GSet } from './G-Set.js' 5 | export { default as TwoPSet } from './2P-Set.js' 6 | export { default as ORSet } from './OR-Set.js' 7 | export { default as LWWSet } from './LWW-Set.js' 8 | export { default as PNCounter } from './PN-Counter.js' 9 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const deepEqual = (a, b) => { 2 | const propsA = Object.getOwnPropertyNames(a) 3 | const propsB = Object.getOwnPropertyNames(b) 4 | 5 | if(propsA.length !== propsB.length) 6 | return false 7 | 8 | for(let i = 0; i < propsA.length; i ++) { 9 | const prop = propsA[i] 10 | if(a[prop] !== b[prop]) 11 | return false 12 | } 13 | 14 | return true 15 | } 16 | 17 | class OperationTuple3 { 18 | constructor (value, added, removed) { 19 | this.value = value 20 | this.added = new Set(added) 21 | this.removed = new Set(removed) 22 | } 23 | 24 | static create (value, added, removed) { 25 | return new OperationTuple3(value, added, removed) 26 | } 27 | 28 | static from (json) { 29 | return OperationTuple3.create(json.value, json.added, json.removed) 30 | } 31 | } 32 | 33 | export { 34 | deepEqual, 35 | OperationTuple3 36 | } 37 | -------------------------------------------------------------------------------- /test/2P-Set.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { TwoPSet, GSet, CmRDTSet } from '../src/index.js' 3 | 4 | describe('2P-Set', () => { 5 | describe('Instance', () => { 6 | describe('constructor', () => { 7 | it('creates a set', () => { 8 | const crdt = new TwoPSet() 9 | assert.notEqual(crdt, null) 10 | }) 11 | 12 | it('has two GSets', () => { 13 | const crdt = new TwoPSet() 14 | assert.notEqual(crdt._added, null) 15 | assert.equal(crdt._added instanceof GSet, true) 16 | assert.notEqual(crdt._removed, null) 17 | assert.equal(crdt._removed instanceof GSet, true) 18 | }) 19 | 20 | it('is a CmRDT Set', () => { 21 | const crdt = new TwoPSet() 22 | assert.equal(crdt instanceof CmRDTSet, true) 23 | }) 24 | 25 | it('creates a set from values', () => { 26 | const crdt = new TwoPSet(['A', 'B']) 27 | assert.notEqual(crdt, null) 28 | assert.equal(crdt._added instanceof GSet, true) 29 | assert.deepEqual(new Set(crdt._added.values()), new Set(['B', 'A'])) 30 | }) 31 | }) 32 | 33 | describe('values', () => { 34 | it('is an Iterator', () => { 35 | const crdt = new TwoPSet() 36 | assert.equal(crdt.values().toString(), '[object Set Iterator]') 37 | }) 38 | 39 | it('returns an Iterator', () => { 40 | const crdt = new TwoPSet() 41 | crdt.add('A') 42 | crdt.add('B') 43 | const iterator = crdt.values() 44 | assert.equal(iterator.next().value, 'A') 45 | assert.equal(iterator.next().value, 'B') 46 | }) 47 | }) 48 | 49 | describe('add', () => { 50 | it('adds an element to the set', () => { 51 | const crdt = new TwoPSet() 52 | crdt.add('A') 53 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 54 | }) 55 | 56 | it('adds three elements to the set', () => { 57 | const crdt = new TwoPSet() 58 | crdt.add('A') 59 | crdt.add('B') 60 | crdt.add('C') 61 | assert.deepEqual(new Set(crdt.values()), new Set(['A', 'B', 'C'])) 62 | }) 63 | 64 | it('contains only unique values', () => { 65 | const crdt = new TwoPSet() 66 | crdt.add(1) 67 | crdt.add('A') 68 | crdt.add('A') 69 | crdt.add(1) 70 | crdt.add('A') 71 | const obj = { hello: 'ABC' } 72 | crdt.add(obj) 73 | crdt.add(obj) 74 | crdt.add({ hello: 'ABCD' }) 75 | 76 | const expectedResult = [ 77 | 'A', 78 | 1, 79 | { hello: 'ABC' }, 80 | { hello: 'ABCD' }, 81 | ] 82 | 83 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 84 | }) 85 | 86 | it('has internally a set for added elements', () => { 87 | const addedValues = ['A', 'B', 'C', 0, 1, 2, 3, 4] 88 | const removedValues = ['A', 1, 2, 3] 89 | const expectedResult = ['B', 'C', 0, 4] 90 | const crdt = new TwoPSet(addedValues, removedValues) 91 | assert.equal(crdt._added instanceof GSet, true) 92 | assert.deepEqual(new Set(crdt._added.values()), new Set(addedValues)) 93 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 94 | }) 95 | }) 96 | 97 | describe('remove', () => { 98 | it('removes an element from the set', () => { 99 | const crdt = new TwoPSet() 100 | crdt.add('A') 101 | crdt.remove('A') 102 | assert.deepEqual(new Set(crdt.values()), new Set([])) 103 | }) 104 | 105 | it('element can only be removed if it is present in the set', () => { 106 | const crdt = new TwoPSet() 107 | crdt.remove('A') 108 | crdt.add('A') 109 | assert.deepEqual(new Set(crdt._removed.values()), new Set([])) 110 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 111 | }) 112 | 113 | it('has internally a set for removed elements', () => { 114 | const addedValues = ['A', 'B', 'C', 0, 1, 2, 3, 4] 115 | const removedValues = ['A', 1, 2, 3] 116 | const expectedResult = ['B', 'C', 0, 4] 117 | const crdt = new TwoPSet(addedValues, removedValues) 118 | assert.equal(crdt._removed instanceof GSet, true) 119 | assert.deepEqual(new Set(crdt._removed.values()), new Set(removedValues)) 120 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 121 | }) 122 | 123 | it('adds removed elements to the internal removed set', () => { 124 | const crdt = new TwoPSet() 125 | crdt.add('A') 126 | crdt.add('B') 127 | crdt.remove('A') 128 | crdt.remove('B') 129 | crdt.remove('A') 130 | const expectedResult = ['A', 'B'] 131 | assert.deepEqual(new Set(crdt._removed.values()), new Set(expectedResult)) 132 | }) 133 | }) 134 | 135 | describe('merge', () => { 136 | it('merges two sets with same id', () => { 137 | const crdt1 = new TwoPSet() 138 | const crdt2 = new TwoPSet() 139 | crdt1.add('A') 140 | crdt2.add('B') 141 | crdt2.add('C') 142 | crdt2.add('D') 143 | crdt2.remove('D') 144 | crdt1.merge(crdt2) 145 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'C']) 146 | }) 147 | 148 | it('merges two sets with same values', () => { 149 | const crdt1 = new TwoPSet() 150 | const crdt2 = new TwoPSet() 151 | crdt1.add('A') 152 | crdt2.add('B') 153 | crdt2.add('A') 154 | crdt2.remove('B') 155 | crdt1.merge(crdt2) 156 | assert.deepEqual(crdt1.toArray(), ['A']) 157 | }) 158 | 159 | it('merges two sets with removed values', () => { 160 | const crdt1 = new TwoPSet() 161 | const crdt2 = new TwoPSet() 162 | crdt1.add('A') 163 | crdt1.remove('A') 164 | crdt2.remove('A') 165 | crdt2.add('A') 166 | crdt1.add('AAA') 167 | crdt2.add('AAA') 168 | crdt1.add('A') 169 | crdt1.merge(crdt2) 170 | assert.deepEqual(crdt1.toArray(), ['AAA']) 171 | }) 172 | 173 | it('merge four different sets', () => { 174 | const crdt1 = new TwoPSet() 175 | const crdt2 = new TwoPSet() 176 | const crdt3 = new TwoPSet() 177 | const crdt4 = new TwoPSet() 178 | crdt1.add('A') 179 | crdt2.add('B') 180 | crdt3.add('C') 181 | crdt4.add('D') 182 | crdt2.add('BB') 183 | crdt2.remove('BB') 184 | 185 | crdt1.merge(crdt2) 186 | crdt1.merge(crdt3) 187 | crdt1.remove('C') 188 | crdt1.merge(crdt4) 189 | 190 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'D']) 191 | }) 192 | 193 | it('doesn\'t overwrite other\'s values on merge', () => { 194 | const crdt1 = new TwoPSet() 195 | const crdt2 = new TwoPSet() 196 | crdt1.add('A') 197 | crdt2.add('C') 198 | crdt2.add('B') 199 | crdt2.remove('C') 200 | crdt1.merge(crdt2) 201 | crdt1.add('AA') 202 | crdt2.add('CC') 203 | crdt2.add('BB') 204 | crdt2.remove('CC') 205 | crdt1.merge(crdt2) 206 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'AA', 'BB']) 207 | assert.deepEqual(crdt2.toArray(), ['B', 'BB']) 208 | }) 209 | }) 210 | 211 | describe('has', () => { 212 | it('returns true if given element is in the set', () => { 213 | const crdt = new TwoPSet() 214 | crdt.add('A') 215 | crdt.add('B') 216 | crdt.add(1) 217 | crdt.add(13) 218 | crdt.remove(13) 219 | const obj = { hello: 'world' } 220 | crdt.add(obj) 221 | assert.equal(crdt.has('A'), true) 222 | assert.equal(crdt.has('B'), true) 223 | assert.equal(crdt.has(1), true) 224 | assert.equal(crdt.has(13), false) 225 | assert.equal(crdt.has(obj), true) 226 | }) 227 | 228 | it('returns false if given element is not in the set', () => { 229 | const crdt = new TwoPSet() 230 | crdt.add('A') 231 | crdt.add('B') 232 | crdt.add('nothere') 233 | crdt.remove('nothere') 234 | assert.equal(crdt.has('nothere'), false) 235 | }) 236 | }) 237 | 238 | describe('hasAll', () => { 239 | it('returns true if all given elements are in the set', () => { 240 | const crdt = new TwoPSet() 241 | crdt.add('A') 242 | crdt.add('B') 243 | crdt.add('C') 244 | crdt.remove('C') 245 | crdt.add('D') 246 | assert.equal(crdt.hasAll(['D', 'A', 'B']), true) 247 | }) 248 | 249 | it('returns false if any of the given elements are not in the set', () => { 250 | const crdt = new TwoPSet() 251 | crdt.add('A') 252 | crdt.add('B') 253 | crdt.add('C') 254 | crdt.remove('C') 255 | crdt.add('D') 256 | assert.equal(crdt.hasAll(['D', 'A', 'C', 'B']), false) 257 | }) 258 | }) 259 | 260 | describe('toArray', () => { 261 | it('returns the values of the set as an Array', () => { 262 | const crdt = new TwoPSet() 263 | const array = crdt.toArray() 264 | assert.equal(Array.isArray(array), true) 265 | }) 266 | 267 | it('returns values', () => { 268 | const crdt = new TwoPSet() 269 | crdt.add('A') 270 | crdt.add('B') 271 | crdt.add('C') 272 | crdt.remove('C') 273 | const array = crdt.toArray() 274 | assert.equal(array.length, 2) 275 | assert.equal(array[0], 'A') 276 | assert.equal(array[1], 'B') 277 | }) 278 | }) 279 | 280 | describe('toJSON', () => { 281 | it('returns the set as JSON object', () => { 282 | const crdt = new TwoPSet() 283 | crdt.add('A') 284 | assert.equal(crdt.toJSON().values.added.length, 1) 285 | assert.equal(crdt.toJSON().values.added[0], 'A') 286 | assert.equal(crdt.toJSON().values.removed.length, 0) 287 | crdt.remove('A') 288 | assert.equal(crdt.toJSON().values.removed.length, 1) 289 | assert.equal(crdt.toJSON().values.removed[0], 'A') 290 | }) 291 | 292 | it('returns a JSON object after a merge', () => { 293 | const crdt1 = new TwoPSet() 294 | const crdt2 = new TwoPSet() 295 | crdt1.add('A') 296 | crdt2.add('B') 297 | crdt2.remove('B') 298 | crdt1.merge(crdt2) 299 | crdt2.merge(crdt1) 300 | assert.equal(crdt1.toJSON().values.added.length, 2) 301 | assert.equal(crdt1.toJSON().values.added[0], 'A') 302 | assert.equal(crdt1.toJSON().values.added[1], 'B') 303 | assert.equal(crdt1.toJSON().values.removed.length, 1) 304 | assert.equal(crdt1.toJSON().values.removed[0], 'B') 305 | }) 306 | }) 307 | 308 | describe('isEqual', () => { 309 | it('returns true for sets with same values', () => { 310 | const crdt1 = new TwoPSet() 311 | const crdt2 = new TwoPSet() 312 | crdt1.add('A') 313 | crdt2.add('A') 314 | assert.equal(crdt1.isEqual(crdt2), true) 315 | assert.equal(crdt2.isEqual(crdt1), true) 316 | }) 317 | 318 | it('returns true for empty sets', () => { 319 | const crdt1 = new TwoPSet() 320 | const crdt2 = new TwoPSet() 321 | assert.equal(crdt1.isEqual(crdt2), true) 322 | assert.equal(crdt2.isEqual(crdt1), true) 323 | }) 324 | 325 | it('returns false for sets with different values', () => { 326 | const crdt1 = new TwoPSet() 327 | const crdt2 = new TwoPSet() 328 | crdt1.add('A') 329 | crdt2.add('B') 330 | assert.equal(crdt1.isEqual(crdt2), false) 331 | assert.equal(crdt2.isEqual(crdt1), false) 332 | }) 333 | }) 334 | }) 335 | }) 336 | -------------------------------------------------------------------------------- /test/CRDT.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { GCounter, PNCounter, GSet, TwoPSet, ORSet, LWWSet } from '../src/index.js' 3 | import CmRDTSet from '../src/CmRDT-Set.js' 4 | 5 | const crdts = [ 6 | { 7 | type: 'G-Counter', 8 | class: GCounter, 9 | create: (id) => new GCounter(id), 10 | update: (crdt, value) => crdt.increment(value), 11 | merge: (crdt, other) => crdt.merge(other), 12 | query: (crdt) => crdt.value, 13 | getExpectedMergedValue: (values) => values.reduce((acc, val) => acc + val, 0), 14 | }, 15 | { 16 | type: 'PN-Counter', 17 | class: PNCounter, 18 | create: (id) => new PNCounter(id), 19 | update: (crdt, value) => { 20 | crdt.increment(value + 1) 21 | crdt.decrement(value) 22 | }, 23 | merge: (crdt, other) => crdt.merge(other), 24 | query: (crdt) => crdt.value, 25 | getExpectedMergedValue: (values) => -values.reduce((acc, val) => acc + val, 0) + values.reduce((acc, val) => (acc + 1) + val, 0), 26 | }, 27 | { 28 | type: 'G-Set', 29 | class: GSet, 30 | create: () => new GSet(), 31 | update: (crdt, value) => crdt.add(value), 32 | merge: (crdt, other) => crdt.merge(other), 33 | query: (crdt) => new Set(crdt.values()), 34 | getExpectedMergedValue: (values) => new Set(values), 35 | }, 36 | { 37 | type: '2P-Set', 38 | class: TwoPSet, 39 | create: () => new GSet(), 40 | update: (crdt, value) => crdt.add(value), 41 | merge: (crdt, other) => crdt.merge(other), 42 | query: (crdt) => new Set(crdt.values()), 43 | getExpectedMergedValue: (values) => new Set(values), 44 | }, 45 | { 46 | type: 'OR-Set', 47 | class: ORSet, 48 | create: () => new ORSet(), 49 | update: (crdt, value) => crdt.add(value), 50 | merge: (crdt, other) => crdt.merge(other), 51 | query: (crdt) => new Set(crdt.values()), 52 | getExpectedMergedValue: (values) => new Set(values), 53 | }, 54 | { 55 | type: 'LWW-Set', 56 | class: LWWSet, 57 | create: () => new LWWSet(), 58 | update: (crdt, value) => crdt.add(value, 0), 59 | merge: (crdt, other) => crdt.merge(other), 60 | query: (crdt) => new Set(crdt.values()), 61 | getExpectedMergedValue: (values) => new Set(values), 62 | }, 63 | ] 64 | 65 | describe('CRDT', () => { 66 | crdts.forEach(async (CRDT) => { 67 | describe(CRDT.type, () => { 68 | // Associativity: 69 | // a + (b + c) == (a + b) + c 70 | it('is associative', () => { 71 | // a + (b + c) 72 | const crdt1 = CRDT.create('A') 73 | const crdt2 = CRDT.create('B') 74 | const crdt3 = CRDT.create('C') 75 | CRDT.update(crdt1, 42) 76 | CRDT.update(crdt2, 2) 77 | CRDT.update(crdt3, 1) 78 | CRDT.merge(crdt2, crdt3) 79 | CRDT.merge(crdt1, crdt2) 80 | const expectedValue1 = CRDT.getExpectedMergedValue([2, 42, 1]) 81 | const res1 = CRDT.query(crdt1) 82 | assert.deepEqual(res1, expectedValue1) 83 | 84 | // (a + b) + c 85 | const crdt4 = CRDT.create('A') 86 | const crdt5 = CRDT.create('B') 87 | const crdt6 = CRDT.create('C') 88 | CRDT.update(crdt4, 42) 89 | CRDT.update(crdt5, 2) 90 | CRDT.update(crdt6, 1) 91 | CRDT.merge(crdt4, crdt5) 92 | CRDT.merge(crdt6, crdt4) 93 | const expectedValue2 = CRDT.getExpectedMergedValue([1, 2, 42]) 94 | const res2 = CRDT.query(crdt6) 95 | assert.deepEqual(res2, expectedValue2) 96 | 97 | // a + (b + c) == (a + b) + c 98 | assert.deepEqual(res1, res2) 99 | }) 100 | 101 | // Commutativity: 102 | // a + b == b + a 103 | it('is commutative', () => { 104 | // a + b 105 | const crdt1 = CRDT.create('A') 106 | const crdt2 = CRDT.create('B') 107 | CRDT.update(crdt1, 12) 108 | CRDT.update(crdt2, 43) 109 | CRDT.merge(crdt1, crdt2) 110 | const expectedValue1 = CRDT.getExpectedMergedValue([43, 12]) 111 | const res1 = CRDT.query(crdt1) 112 | assert.deepEqual(res1, expectedValue1) 113 | 114 | // b + a 115 | const crdt3 = CRDT.create('A') 116 | const crdt4 = CRDT.create('B') 117 | CRDT.update(crdt3, 12) 118 | CRDT.update(crdt4, 43) 119 | CRDT.merge(crdt3, crdt4) 120 | const expectedValue2 = CRDT.getExpectedMergedValue([12, 43]) 121 | const res2 = CRDT.query(crdt3) 122 | assert.deepEqual(res2, expectedValue2) 123 | 124 | // a + b == b + a 125 | assert.deepEqual(res1, res2) 126 | }) 127 | 128 | // Idempotence: 129 | // a + a = a 130 | it('is idempotent', () => { 131 | const crdt = CRDT.create('A') 132 | CRDT.update(crdt, 3) 133 | CRDT.update(crdt, 42) 134 | CRDT.update(crdt, 7) 135 | CRDT.merge(crdt, crdt) 136 | const res = CRDT.query(crdt) 137 | const expectedValue = CRDT.getExpectedMergedValue([7, 3, 42]) 138 | 139 | // a + a = a 140 | assert.deepEqual(res, expectedValue) 141 | }) 142 | }) 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /test/Common-Set.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { GCounter, GSet, TwoPSet, ORSet, LWWSet } from '../src/index.js' 3 | import CmRDTSet from '../src/CmRDT-Set.js' 4 | 5 | const added = [1, 2, 3] 6 | const removed = [1] 7 | const diff = ['one', 77] 8 | 9 | const crdts = [ 10 | { 11 | type: 'G-Set', 12 | class: GSet, 13 | added: added, 14 | removed: [], 15 | diff: diff, 16 | inputData: { 17 | values: added, 18 | }, 19 | expectedValues: added, 20 | expectedValuesWithDiff: added.concat(diff), 21 | create: (input) => new GSet(input && input.values ? input.values : []), 22 | from: (json) => GSet.from(json), 23 | remove: (crdt, tag) => 0, 24 | isEqual: (a, b) => GSet.isEqual(a, b), 25 | difference: (a, b) => GSet.difference(a, b), 26 | }, 27 | { 28 | type: '2P-Set', 29 | class: TwoPSet, 30 | added: added, 31 | removed: removed, 32 | diff: diff, 33 | inputData: { 34 | values: { 35 | added: added, 36 | removed: removed, 37 | } 38 | }, 39 | expectedValues: [2, 3], 40 | expectedValuesWithDiff: added.slice(1, added.length).concat(diff), 41 | create: (input) => new TwoPSet(input && input.values && input.values.added ? input.values.added : [], input && input.values && input.values.removed ? input.values.removed : []), 42 | from: (json) => TwoPSet.from(json), 43 | remove: (crdt, tag) => crdt.remove(tag), 44 | isEqual: (a, b) => TwoPSet.isEqual(a, b), 45 | difference: (a, b) => TwoPSet.difference(a, b), 46 | }, 47 | { 48 | type: 'CmRDT-Set', 49 | class: CmRDTSet, 50 | added: added, 51 | removed: removed, 52 | diff: diff, 53 | inputData: { 54 | values: [ 55 | { 56 | value: 'A', 57 | added: [1], 58 | removed: [], 59 | }, 60 | { 61 | value: 'B', 62 | added: [1], 63 | removed: [1], 64 | }, 65 | { 66 | value: 'C', 67 | added: [1, 2], 68 | removed: [2, 3], 69 | }, 70 | ], 71 | }, 72 | expectedValues: ['A'], 73 | expectedValuesWithDiff: added.slice(1, added.length).concat(diff), 74 | create: (input) => new CmRDTSet(input && input.values ? input.values : []), 75 | from: (json) => new CmRDTSet(json && json.values ? json.values : []), 76 | remove: (crdt, tag) => crdt.remove(tag), 77 | isEqual: (a, b) => CmRDTSet.isEqual(a, b), 78 | difference: (a, b) => CmRDTSet.difference(a, b), 79 | }, 80 | { 81 | type: 'OR-Set', 82 | class: ORSet, 83 | added: added, 84 | removed: removed, 85 | diff: diff, 86 | inputData: { 87 | values: [ 88 | { 89 | value: 'A', 90 | added: [1], 91 | removed: [], 92 | }, 93 | { 94 | value: 'B', 95 | added: [1], 96 | removed: [1], 97 | }, 98 | { 99 | value: 'C', 100 | added: [1, 2], 101 | removed: [2, 3], 102 | }, 103 | ], 104 | }, 105 | expectedValues: ['A', 'C'], 106 | expectedValuesWithDiff: added.slice(1, added.length).concat(diff), 107 | create: (input) => new ORSet(input && input.values ? input.values : []), 108 | from: (json) => ORSet.from(json), 109 | remove: (crdt, tag) => crdt.remove(tag), 110 | isEqual: (a, b) => ORSet.isEqual(a, b), 111 | difference: (a, b) => ORSet.difference(a, b), 112 | }, 113 | { 114 | type: 'LWW-Set', 115 | class: LWWSet, 116 | added: added, 117 | removed: removed, 118 | diff: diff, 119 | inputData: { 120 | values: [ 121 | { 122 | value: 'A', 123 | added: [1], 124 | removed: [], 125 | }, 126 | { 127 | value: 'B', 128 | added: [1], 129 | removed: [1], 130 | }, 131 | { 132 | value: 'C', 133 | added: [1, 2], 134 | removed: [2, 3], 135 | }, 136 | ], 137 | }, 138 | expectedValues: ['A', 'B'], 139 | expectedValuesWithDiff: added.slice(1, added.length).concat(diff), 140 | create: (input) => new LWWSet(input && input.values ? input.values : []), 141 | from: (json) => LWWSet.from(json), 142 | remove: (crdt, value, tag) => crdt.remove(value, tag + 1), 143 | isEqual: (a, b) => LWWSet.isEqual(a, b), 144 | difference: (a, b) => LWWSet.difference(a, b), 145 | }, 146 | ] 147 | 148 | describe('Sets - Common', () => { 149 | crdts.forEach(async (CRDT) => { 150 | describe(CRDT.type, () => { 151 | it('creates a new ' + CRDT.type + ' from a JSON object', () => { 152 | const crdt1 = CRDT.create(CRDT.inputData) 153 | const crdt2 = CRDT.from(CRDT.inputData) 154 | assert.deepEqual(new Set(crdt2.values()), new Set(CRDT.expectedValues)) 155 | }) 156 | 157 | it('is a Set', () => { 158 | const crdt = CRDT.create() 159 | assert.equal(crdt instanceof Set, true) 160 | }) 161 | 162 | it('provides a JSON object and creates itself from it', () => { 163 | const crdt1 = CRDT.create() 164 | crdt1.add('A') 165 | crdt1.add('B') 166 | crdt1.add('C') 167 | const crdt2 = CRDT.class.from(crdt1.toJSON()) 168 | assert.deepEqual(new Set(crdt2.values()), new Set(crdt1.values())) 169 | }) 170 | 171 | it('toArray() returns the values in the set as an array', () => { 172 | const crdt1 = CRDT.create() 173 | crdt1.add('A') 174 | crdt1.add('B') 175 | crdt1.add('C') 176 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'C']) 177 | }) 178 | 179 | it('returns true if two Sets are equal', () => { 180 | const crdt1 = CRDT.create(CRDT.inputData) 181 | const crdt2 = CRDT.create(CRDT.inputData) 182 | const crdt3 = CRDT.create({}) 183 | assert.equal(CRDT.isEqual(crdt1, crdt2), true) 184 | const isEqual = CRDT.isEqual(crdt1, crdt3) 185 | assert.equal(isEqual, false) 186 | }) 187 | 188 | it('returns true if set has all values', () => { 189 | const crdt1 = CRDT.create(CRDT.inputData) 190 | assert.equal(crdt1.hasAll(CRDT.expectedValues), true) 191 | }) 192 | 193 | it('returns false if set doesn\'t have all values', () => { 194 | const crdt1 = CRDT.create(CRDT.inputData) 195 | assert.equal(crdt1.hasAll(CRDT.expectedValues.concat(['extra', 1])), false) 196 | }) 197 | 198 | it('returns a Set of values from Set A that are not in Set B', () => { 199 | const addedValues = added 200 | const removedValues = removed 201 | const expectedDiff = CRDT.diff 202 | const expectedValues = CRDT.expectedValuesWithDiff 203 | 204 | const crdt1 = CRDT.create() 205 | const crdt2 = CRDT.create() 206 | const crdt3 = CRDT.create() 207 | 208 | addedValues.concat(expectedDiff).forEach((e, idx) => crdt1.add(e, idx)) 209 | removedValues.forEach(e => CRDT.remove(crdt1, e, 10)) 210 | addedValues.forEach(e => crdt2.add(e, 100)) 211 | 212 | assert.deepEqual(CRDT.difference(crdt1, crdt2), new Set(expectedDiff)) 213 | assert.deepEqual(CRDT.difference(crdt1, crdt3), new Set(expectedValues)) 214 | }) 215 | }) 216 | }) 217 | }) 218 | -------------------------------------------------------------------------------- /test/G-Counter.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { GCounter } from '../src/index.js' 3 | 4 | describe('G-GCounter', () => { 5 | describe('Instance', () => { 6 | describe('constructor', () => { 7 | it('creates a counter', () => { 8 | const counter = new GCounter('A') 9 | assert(counter, null) 10 | assert.notEqual(counter.id, null) 11 | assert.notEqual(counter._counters, null) 12 | assert.equal(counter._counters['A'], 0) 13 | }) 14 | }) 15 | 16 | describe('value', () => { 17 | it('returns the count', () => { 18 | const counter = new GCounter('A') 19 | assert.equal(counter.value, 0) 20 | }) 21 | 22 | it('returns the count after increment', () => { 23 | const counter = new GCounter('A') 24 | counter.increment(5) 25 | assert.equal(counter.value, 5) 26 | }) 27 | }) 28 | 29 | describe('increment', () => { 30 | it('increments the count by 1', () => { 31 | const counter = new GCounter('A') 32 | counter.increment() 33 | assert.equal(counter.value, 1) 34 | }) 35 | 36 | it('increments the count by 2', () => { 37 | const counter = new GCounter('A') 38 | counter.increment() 39 | counter.increment() 40 | assert.equal(counter.value, 2) 41 | }) 42 | 43 | it('increments the count by 3', () => { 44 | const counter = new GCounter('A') 45 | counter.increment(3) 46 | assert.equal(counter.value, 3) 47 | }) 48 | 49 | it('increments the count by 42', () => { 50 | const counter = new GCounter('A') 51 | counter.increment() 52 | counter.increment(42) 53 | assert.equal(counter.value, 43) 54 | }) 55 | 56 | it('can\'t decrease the counter', () => { 57 | const counter = new GCounter('A') 58 | counter.increment(-1) 59 | assert.equal(counter.value, 0) 60 | }) 61 | 62 | it('can\'t decrease the counter', () => { 63 | const counter = new GCounter('A') 64 | counter.increment(0) 65 | assert.equal(counter.value, 0) 66 | }) 67 | }) 68 | 69 | describe('merge', () => { 70 | it('merges two counters with same id', () => { 71 | const counter1 = new GCounter('A') 72 | const counter2 = new GCounter('A') 73 | counter1.increment() 74 | counter2.increment() 75 | counter1.merge(counter2) 76 | assert.equal(counter1.value, 1) 77 | }) 78 | 79 | it('merges two counters with same values', () => { 80 | const counter1 = new GCounter('A') 81 | const counter2 = new GCounter('B') 82 | counter1.increment() 83 | counter2.increment() 84 | counter1.merge(counter2) 85 | counter2.merge(counter1) 86 | assert.equal(counter1.value, 2) 87 | assert.equal(counter2.value, 2) 88 | }) 89 | 90 | it('merges four different counters', () => { 91 | const counter1 = new GCounter('A') 92 | const counter2 = new GCounter('B') 93 | const counter3 = new GCounter('C') 94 | const counter4 = new GCounter('D') 95 | counter1.increment() 96 | counter2.increment() 97 | counter3.increment() 98 | counter4.increment() 99 | counter1.merge(counter2) 100 | counter1.merge(counter3) 101 | counter1.merge(counter4) 102 | assert.equal(counter1.value, 4) 103 | }) 104 | 105 | it('doesn\'t overwrite its own value on merge', () => { 106 | const counter1 = new GCounter('A') 107 | const counter2 = new GCounter('B') 108 | counter1.increment() 109 | counter2.increment() 110 | counter1.merge(counter2) 111 | counter2.merge(counter1) 112 | counter1.increment() 113 | counter1.merge(counter2) 114 | assert.equal(counter1.value, 3) 115 | }) 116 | 117 | it('doesn\'t overwrite others\' values on merge', () => { 118 | const counter1 = new GCounter('A') 119 | const counter2 = new GCounter('B') 120 | counter1.increment() 121 | counter2.increment() 122 | counter1.merge(counter2) 123 | counter2.merge(counter1) 124 | counter1.increment() 125 | counter2.increment() 126 | counter1.merge(counter2) 127 | assert.equal(counter1.value, 4) 128 | }) 129 | }) 130 | 131 | describe('toJSON', () => { 132 | it('returns the counter as JSON object', () => { 133 | const counter = new GCounter('A') 134 | assert.equal(counter.toJSON().id, 'A') 135 | assert.equal(counter.toJSON().counters.A, 0) 136 | }) 137 | 138 | it('returns a JSON object after a merge', () => { 139 | const counter1 = new GCounter('A') 140 | const counter2 = new GCounter('B') 141 | counter1.increment() 142 | counter2.increment() 143 | counter1.merge(counter2) 144 | counter2.merge(counter1) 145 | assert.equal(Object.keys(counter1.toJSON().counters).length, 2) 146 | assert.equal(counter1.toJSON().counters.A, 1) 147 | assert.equal(counter1.toJSON().counters.B, 1) 148 | assert.equal(counter2.toJSON().counters.A, 1) 149 | assert.equal(counter2.toJSON().counters.B, 1) 150 | }) 151 | }) 152 | 153 | describe('isEqual', () => { 154 | it('returns true for equal counters', () => { 155 | const counter1 = new GCounter('A') 156 | const counter2 = new GCounter('A') 157 | counter1.increment() 158 | counter2.increment() 159 | assert.equal(counter1.isEqual(counter2), true) 160 | }) 161 | 162 | it('returns false for unequal counters - different id', () => { 163 | const counter1 = new GCounter('A') 164 | const counter2 = new GCounter('B') 165 | assert.equal(counter1.isEqual(counter2), false) 166 | }) 167 | 168 | it('returns false for unequal counters - same id, different counts', () => { 169 | const counter1 = new GCounter('A') 170 | const counter2 = new GCounter('A') 171 | counter1.increment() 172 | counter2.increment() 173 | counter2.increment() 174 | assert.equal(counter1.isEqual(counter2), false) 175 | }) 176 | 177 | it('returns false for unequal counters - different counters', () => { 178 | const counter1 = new GCounter('A') 179 | const counter2 = new GCounter('A') 180 | counter2._counters['extra'] = 'world' 181 | assert.equal(counter1.isEqual(counter2), false) 182 | }) 183 | }) 184 | }) 185 | 186 | describe('GGCounter.from', () => { 187 | it('creates a new counter from JSON object', () => { 188 | const counter1 = new GCounter('A') 189 | counter1.increment() 190 | 191 | const input = { 192 | id: 'A', 193 | counters: { 194 | A: 1, 195 | } 196 | } 197 | 198 | const counter2 = GCounter.from(input) 199 | assert.equal(GCounter.isEqual(counter1, counter2), true) 200 | assert.equal(counter2.id, 'A') 201 | assert.equal(counter2.value, 1) 202 | }) 203 | }) 204 | 205 | describe('GGCounter.isEqual', () => { 206 | it('returns true if to GSets are equal', () => { 207 | const values = ['A', 'B', 'C'] 208 | const counter1 = new GCounter('A') 209 | const counter2 = new GCounter('A') 210 | const counter3 = new GCounter('B') 211 | counter1.increment(2) 212 | counter2.increment(2) 213 | assert.equal(GCounter.isEqual(counter1, counter2), true) 214 | assert.equal(GCounter.isEqual(counter1, counter3), false) 215 | }) 216 | }) 217 | }) 218 | -------------------------------------------------------------------------------- /test/G-Set.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { GSet, CmRDTSet } from '../src/index.js' 3 | 4 | describe('G-Set', () => { 5 | describe('Instance', () => { 6 | describe('constructor', () => { 7 | it('creates a set', () => { 8 | const crdt = new GSet() 9 | assert.notEqual(crdt, null) 10 | assert.notEqual(crdt._values, null) 11 | assert.equal(crdt._values instanceof Set, true) 12 | }) 13 | 14 | it('is a CmRDT Set', () => { 15 | const crdt = new GSet() 16 | assert.equal(crdt instanceof CmRDTSet, true) 17 | }) 18 | 19 | it('creates a set from values', () => { 20 | const crdt = new GSet(['A', 'B']) 21 | assert.notEqual(crdt, null) 22 | assert.equal(crdt._values instanceof Set, true) 23 | assert.deepEqual(crdt._values, new Set(['B', 'A'])) 24 | }) 25 | }) 26 | 27 | describe('values', () => { 28 | it('is an Iterator', () => { 29 | const crdt = new GSet() 30 | assert.equal(crdt.values().toString(), '[object Set Iterator]') 31 | }) 32 | 33 | it('returns an Iterator', () => { 34 | const crdt = new GSet() 35 | crdt.add('A') 36 | crdt.add('B') 37 | const iterator = crdt.values() 38 | assert.equal(iterator.next().value, 'A') 39 | assert.equal(iterator.next().value, 'B') 40 | }) 41 | }) 42 | 43 | describe('add', () => { 44 | it('adds an element to the set', () => { 45 | const crdt = new GSet() 46 | crdt.add('A') 47 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 48 | }) 49 | 50 | it('adds three elements to the set', () => { 51 | const crdt = new GSet() 52 | crdt.add('A') 53 | crdt.add('B') 54 | crdt.add('C') 55 | assert.deepEqual(new Set(crdt.values()), new Set(['A', 'B', 'C'])) 56 | }) 57 | 58 | it('contains only unique values', () => { 59 | const crdt = new GSet() 60 | crdt.add(1) 61 | crdt.add('A') 62 | crdt.add('A') 63 | crdt.add(1) 64 | crdt.add('A') 65 | const obj = { hello: 'ABC' } 66 | crdt.add(obj) 67 | crdt.add(obj) 68 | crdt.add({ hello: 'ABCD' }) 69 | 70 | const expectedResult = [ 71 | 'A', 72 | 1, 73 | { hello: 'ABC' }, 74 | { hello: 'ABCD' }, 75 | ] 76 | 77 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 78 | }) 79 | }) 80 | 81 | describe('remove', () => { 82 | it('doesn\'t allow removing values', () => { 83 | let crdt, err 84 | try { 85 | crdt = new GSet() 86 | crdt.add('A') 87 | crdt.remove('A') 88 | } catch (e) { 89 | err = e.toString() 90 | } 91 | assert.equal(err, `Error: G-Set doesn't allow removing values`) 92 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 93 | }) 94 | }) 95 | 96 | describe('merge', () => { 97 | it('merges two sets with same id', () => { 98 | const crdt1 = new GSet() 99 | const crdt2 = new GSet() 100 | crdt1.add('A') 101 | crdt2.add('B') 102 | crdt2.add('C') 103 | crdt1.merge(crdt2) 104 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'C']) 105 | }) 106 | 107 | it('merges two sets with same values', () => { 108 | const crdt1 = new GSet() 109 | const crdt2 = new GSet() 110 | crdt1.add('A') 111 | crdt2.add('A') 112 | crdt1.merge(crdt2) 113 | assert.deepEqual(crdt1.toArray(), ['A']) 114 | }) 115 | 116 | it('merge four different sets', () => { 117 | const crdt1 = new GSet() 118 | const crdt2 = new GSet() 119 | const crdt3 = new GSet() 120 | const crdt4 = new GSet() 121 | crdt1.add('A') 122 | crdt2.add('B') 123 | crdt3.add('C') 124 | crdt4.add('D') 125 | 126 | crdt1.merge(crdt2) 127 | crdt1.merge(crdt3) 128 | crdt1.merge(crdt4) 129 | 130 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'C', 'D']) 131 | }) 132 | 133 | it('doesn\'t overwrite other\'s values on merge', () => { 134 | const crdt1 = new GSet() 135 | const crdt2 = new GSet() 136 | crdt1.add('A') 137 | crdt2.add('B') 138 | crdt1.merge(crdt2) 139 | crdt1.add('AA') 140 | crdt2.add('BB') 141 | crdt1.merge(crdt2) 142 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'AA', 'BB']) 143 | assert.deepEqual(crdt2.toArray(), ['B', 'BB']) 144 | }) 145 | }) 146 | 147 | describe('has', () => { 148 | it('returns true if given element is in the set', () => { 149 | const crdt = new GSet() 150 | crdt.add('A') 151 | crdt.add('B') 152 | crdt.add(1) 153 | const obj = { hello: 'world' } 154 | crdt.add(obj) 155 | assert.equal(crdt.has('A'), true) 156 | assert.equal(crdt.has('B'), true) 157 | assert.equal(crdt.has(1), true) 158 | assert.equal(crdt.has(obj), true) 159 | }) 160 | 161 | it('returns false if given element is not in the set', () => { 162 | const crdt = new GSet() 163 | crdt.add('A') 164 | crdt.add('B') 165 | assert.equal(crdt.has('nothere'), false) 166 | }) 167 | }) 168 | 169 | describe('hasAll', () => { 170 | it('returns true if all given elements are in the set', () => { 171 | const crdt = new GSet() 172 | crdt.add('A') 173 | crdt.add('B') 174 | crdt.add('C') 175 | crdt.add('D') 176 | assert.equal(crdt.hasAll(['D', 'A', 'C', 'B']), true) 177 | }) 178 | 179 | it('returns false if any of the given elements are not in the set', () => { 180 | const crdt = new GSet() 181 | crdt.add('A') 182 | crdt.add('B') 183 | crdt.add('C') 184 | crdt.add('D') 185 | assert.equal(crdt.hasAll(['D', 'A', 'C', 'B', 'nothere']), false) 186 | }) 187 | }) 188 | 189 | describe('toArray', () => { 190 | it('returns the values of the set as an Array', () => { 191 | const crdt = new GSet() 192 | const array = crdt.toArray() 193 | assert.equal(Array.isArray(array), true) 194 | }) 195 | 196 | it('returns values', () => { 197 | const crdt = new GSet() 198 | crdt.add('A') 199 | crdt.add('B') 200 | const array = crdt.toArray() 201 | assert.equal(array[0], 'A') 202 | assert.equal(array[1], 'B') 203 | }) 204 | }) 205 | 206 | describe('toJSON', () => { 207 | it('returns the set as JSON object', () => { 208 | const crdt = new GSet() 209 | crdt.add('A') 210 | assert.equal(crdt.toJSON().values.length, 1) 211 | assert.equal(crdt.toJSON().values[0], 'A') 212 | }) 213 | 214 | it('returns a JSON object after a merge', () => { 215 | const crdt1 = new GSet() 216 | const crdt2 = new GSet() 217 | crdt1.add('A') 218 | crdt2.add('B') 219 | crdt1.merge(crdt2) 220 | crdt2.merge(crdt1) 221 | assert.equal(crdt1.toJSON().values.length, 2) 222 | assert.equal(crdt1.toJSON().values[0], 'A') 223 | assert.equal(crdt1.toJSON().values[1], 'B') 224 | }) 225 | }) 226 | 227 | describe('isEqual', () => { 228 | it('returns true for sets with same values', () => { 229 | const crdt1 = new GSet() 230 | const crdt2 = new GSet() 231 | crdt1.add('A') 232 | crdt2.add('A') 233 | assert.equal(crdt1.isEqual(crdt2), true) 234 | assert.equal(crdt2.isEqual(crdt1), true) 235 | }) 236 | 237 | it('returns true for empty sets', () => { 238 | const crdt1 = new GSet() 239 | const crdt2 = new GSet() 240 | assert.equal(crdt1.isEqual(crdt2), true) 241 | assert.equal(crdt2.isEqual(crdt1), true) 242 | }) 243 | 244 | it('returns false for sets with different values', () => { 245 | const crdt1 = new GSet() 246 | const crdt2 = new GSet() 247 | crdt1.add('A') 248 | crdt2.add('B') 249 | assert.equal(crdt1.isEqual(crdt2), false) 250 | assert.equal(crdt2.isEqual(crdt1), false) 251 | }) 252 | }) 253 | }) 254 | }) 255 | 256 | -------------------------------------------------------------------------------- /test/LWW-Set.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { LWWSet, CmRDTSet } from '../src/index.js' 3 | import LamportClock from './lamport-clock.js' 4 | 5 | describe('LWW-Set', () => { 6 | describe('Instance', () => { 7 | describe('constructor', () => { 8 | it('creates a set', () => { 9 | const crdt = new LWWSet() 10 | assert.notEqual(crdt, null) 11 | }) 12 | 13 | it('is a CmRDT Set', () => { 14 | const gset = new LWWSet() 15 | assert.equal(gset instanceof CmRDTSet, true) 16 | }) 17 | }) 18 | 19 | describe('values', () => { 20 | it('is an Iterator', () => { 21 | const crdt = new LWWSet() 22 | assert.equal(crdt.values().toString(), '[object Set Iterator]') 23 | }) 24 | 25 | it('returns an Iterator', () => { 26 | const crdt = new LWWSet() 27 | crdt.add('A') 28 | crdt.add('B') 29 | const iterator = crdt.values() 30 | assert.equal(iterator.next().value, 'A') 31 | assert.equal(iterator.next().value, 'B') 32 | }) 33 | }) 34 | 35 | describe('add', () => { 36 | it('adds an element to the set', () => { 37 | const crdt = new LWWSet() 38 | const uid = new Date().getTime() 39 | crdt.add('A', uid) 40 | crdt.add('A', uid + 1) 41 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 42 | }) 43 | 44 | it('adds three elements to the set', () => { 45 | const crdt = new LWWSet() 46 | crdt.add('A') 47 | crdt.add('B') 48 | crdt.add('C') 49 | assert.deepEqual(new Set(crdt.values()), new Set(['A', 'B', 'C'])) 50 | }) 51 | 52 | it('contains only unique values', () => { 53 | const crdt = new LWWSet() 54 | crdt.add(1) 55 | crdt.add('A') 56 | crdt.add('A') 57 | crdt.add(1) 58 | crdt.add('A') 59 | const obj = { hello: 'ABC' } 60 | crdt.add(obj) 61 | crdt.add(obj) 62 | crdt.add({ hello: 'ABCD' }) 63 | 64 | crdt.add(9, 11111) 65 | 66 | const expectedResult = [ 67 | 'A', 68 | 1, 69 | { hello: 'ABC' }, 70 | { hello: 'ABCD' }, 71 | 9 72 | ] 73 | 74 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 75 | }) 76 | }) 77 | 78 | describe('add with Lamport Clocks', () => { 79 | it('adds an element to the set', () => { 80 | let uid = new LamportClock('A') 81 | const crdt = new LWWSet(null, { compareFunc: LamportClock.compare }) 82 | crdt.add('A', uid) 83 | crdt.add('A', uid.tick()) 84 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 85 | }) 86 | 87 | it('adds three elements to the set', () => { 88 | let uid = new LamportClock('A') 89 | const crdt = new LWWSet(null, { compareFunc: LamportClock.compare }) 90 | crdt.add('A', uid) 91 | crdt.add('B', uid) 92 | crdt.add('C', uid) 93 | assert.deepEqual(new Set(crdt.values()), new Set(['A', 'B', 'C'])) 94 | }) 95 | 96 | it('contains only unique values', () => { 97 | let uid = new LamportClock('A') 98 | const crdt = new LWWSet(null, { compareFunc: LamportClock.compare }) 99 | crdt.add(1, uid.tick()) 100 | crdt.add('A', uid.tick()) 101 | crdt.add('A', uid.tick()) 102 | crdt.add(1, uid.tick()) 103 | crdt.add('A', uid.tick()) 104 | const obj = { hello: 'ABC' } 105 | crdt.add(obj, uid.tick()) 106 | crdt.add(obj, uid) 107 | crdt.add({ hello: 'ABCD' }, uid.tick()) 108 | 109 | crdt.add(9, uid) 110 | 111 | const expectedResult = [ 112 | 'A', 113 | 1, 114 | { hello: 'ABC' }, 115 | { hello: 'ABCD' }, 116 | 9 117 | ] 118 | 119 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 120 | }) 121 | }) 122 | 123 | describe('remove', () => { 124 | it('removes an element from the set', () => { 125 | let tag = 1 126 | const crdt = new LWWSet() 127 | crdt.add('A', tag) 128 | crdt.remove('A', tag + 1) 129 | assert.deepEqual(new Set(crdt.values()), new Set([])) 130 | }) 131 | 132 | it('removes an element from the set when element has multiple tags', () => { 133 | const crdt = new LWWSet() 134 | crdt.add('A', 1) 135 | crdt.add('A', 2) 136 | crdt.add('A', 3) 137 | crdt.remove('A', 4) 138 | assert.deepEqual(new Set(crdt.values()), new Set([])) 139 | }) 140 | 141 | it('doesn\'t remove an element from the set if uid was later than add', () => { 142 | const compareFunc = (a, b) => b - a 143 | const crdt = new LWWSet() 144 | crdt.add('A', 2) 145 | crdt.remove('A', 1) 146 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 147 | }) 148 | 149 | it('doesn\'t remove an element from the set if uid was later than add - custom compare function', () => { 150 | const compareFunc = (a, b) => a.time === 'bigger' ? 1 : -1 151 | const crdt = new LWWSet(null, { compareFunc: compareFunc }) 152 | crdt.add('A', { time: 'bigger' }) 153 | crdt.remove('A', { time: 'smaller' }) 154 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 155 | }) 156 | 157 | it('removes an element from the set if all add tags are in removed tags', () => { 158 | const crdt = new LWWSet() 159 | const uid = new Date().getTime() 160 | crdt.add('A', uid) 161 | crdt.remove('A', uid) 162 | crdt.add('A', uid + 1) 163 | crdt.add('A', uid + 2) 164 | crdt.remove('A', uid + 3) 165 | assert.deepEqual(new Set(crdt.values()), new Set([])) 166 | }) 167 | 168 | it('doesn\'t remove an element from the set if add and remove were concurrent', () => { 169 | const crdt = new LWWSet() 170 | const uid = new Date().getTime() 171 | crdt.remove('A', uid) 172 | crdt.add('A', uid) 173 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 174 | }) 175 | 176 | it('doesn\'t remove an element from the set if element wasn\'t in the set', () => { 177 | const crdt = new LWWSet() 178 | const uid = new Date().getTime() 179 | crdt.remove('A', uid - 1) 180 | crdt.add('A', uid) 181 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 182 | }) 183 | }) 184 | 185 | describe('remove with Lamport Clocks', () => { 186 | it('removes an element from the set', () => { 187 | let tag = new LamportClock('A') 188 | const crdt = new LWWSet(null, { compareFunc: LamportClock.compare }) 189 | crdt.add('A', tag) 190 | crdt.remove('A', tag.tick()) 191 | assert.deepEqual(new Set(crdt.values()), new Set([])) 192 | }) 193 | 194 | it('removes an element from the set with the same tag', () => { 195 | let clock1 = new LamportClock('A') 196 | let clock2 = new LamportClock('A') 197 | const crdt = new LWWSet(null, { compareFunc: LamportClock.compare }) 198 | clock1 = clock1.tick() 199 | crdt.add('A', clock1) 200 | clock2 = clock2.tick() 201 | crdt.add('A', clock1) 202 | clock2 = clock2.tick() 203 | clock2 = clock2.tick() 204 | crdt.remove('A', clock2) 205 | assert.deepEqual(new Set(crdt.values()), new Set([])) 206 | }) 207 | }) 208 | 209 | describe('merge', () => { 210 | it('merges two sets with same id', () => { 211 | const lwwset1 = new LWWSet() 212 | const lwwset2 = new LWWSet() 213 | lwwset1.add('A') 214 | lwwset2.add('B') 215 | lwwset2.add('C') 216 | lwwset2.add('D', 33) 217 | lwwset2.remove('D', 34) 218 | lwwset1.merge(lwwset2) 219 | assert.deepEqual(lwwset1.toArray(), ['A', 'B', 'C']) 220 | }) 221 | 222 | it('merges two sets with same values', () => { 223 | const lwwset1 = new LWWSet() 224 | const lwwset2 = new LWWSet() 225 | lwwset1.add('A') 226 | lwwset2.add('B', 13) 227 | lwwset2.add('A') 228 | lwwset2.remove('B', 14) 229 | lwwset1.merge(lwwset2) 230 | assert.deepEqual(lwwset1.toArray(), ['A']) 231 | }) 232 | 233 | it('merges two sets with removed values', () => { 234 | const lwwset1 = new LWWSet() 235 | const lwwset2 = new LWWSet() 236 | lwwset1.add('A', 1) 237 | lwwset1.remove('A', 2) 238 | lwwset2.remove('A', 3) 239 | lwwset2.add('A', 4) 240 | lwwset1.add('AAA', 5) 241 | lwwset2.add('AAA', 6) 242 | lwwset1.add('A', 7) 243 | lwwset1.remove('A', 8) 244 | lwwset1.merge(lwwset2) 245 | assert.deepEqual(lwwset1.toArray(), ['AAA']) 246 | }) 247 | 248 | it('merge four different sets', () => { 249 | const lwwset1 = new LWWSet() 250 | const lwwset2 = new LWWSet() 251 | const lwwset3 = new LWWSet() 252 | const lwwset4 = new LWWSet() 253 | lwwset1.add('A', 1) 254 | lwwset2.add('B', 1) 255 | lwwset3.add('C', 1) 256 | lwwset4.add('D', 1) 257 | lwwset2.add('BB', 2) 258 | lwwset2.remove('BB', 3) 259 | 260 | lwwset1.merge(lwwset2) 261 | lwwset1.merge(lwwset3) 262 | lwwset1.remove('C', 2) 263 | lwwset1.merge(lwwset4) 264 | 265 | assert.deepEqual(lwwset1.toArray(), ['A', 'B', 'D']) 266 | }) 267 | 268 | it('doesn\'t overwrite other\'s values on merge', () => { 269 | const lwwset1 = new LWWSet() 270 | const lwwset2 = new LWWSet() 271 | lwwset1.add('A', 1) 272 | lwwset2.add('C', 1) 273 | lwwset2.add('B', 1) 274 | lwwset2.remove('C', 2) 275 | lwwset1.merge(lwwset2) 276 | lwwset1.add('AA', 3) 277 | lwwset2.add('CC', 3) 278 | lwwset2.add('BB', 3) 279 | lwwset2.remove('CC', 4) 280 | lwwset1.merge(lwwset2) 281 | assert.deepEqual(lwwset1.toArray(), ['A', 'B', 'AA', 'BB']) 282 | assert.deepEqual(lwwset2.toArray(), ['B', 'BB']) 283 | }) 284 | }) 285 | 286 | describe('has', () => { 287 | it('returns true if given element is in the set', () => { 288 | const crdt = new LWWSet() 289 | crdt.add('A') 290 | crdt.add('B') 291 | crdt.add(1) 292 | crdt.add(13, 'A') 293 | crdt.remove(13, 'B') 294 | const obj = { hello: 'world' } 295 | crdt.add(obj) 296 | assert.equal(crdt.has('A'), true) 297 | assert.equal(crdt.has('B'), true) 298 | assert.equal(crdt.has(1), true) 299 | assert.equal(crdt.has(13), false) 300 | assert.equal(crdt.has(obj), true) 301 | }) 302 | 303 | it('returns false if given element is not in the set', () => { 304 | const crdt = new LWWSet() 305 | crdt.add('A') 306 | crdt.add('B') 307 | crdt.add('nothere', 666) 308 | crdt.remove('nothere', 777) 309 | assert.equal(crdt.has('nothere'), false) 310 | }) 311 | }) 312 | 313 | describe('hasAll', () => { 314 | it('returns true if all given elements are in the set', () => { 315 | const crdt = new LWWSet() 316 | crdt.add('A') 317 | crdt.add('B') 318 | crdt.add('C') 319 | crdt.remove('C') 320 | crdt.add('D') 321 | assert.equal(crdt.hasAll(['D', 'A', 'B']), true) 322 | }) 323 | 324 | it('returns false if any of the given elements are not in the set', () => { 325 | const crdt = new LWWSet() 326 | crdt.add('A') 327 | crdt.add('B') 328 | crdt.add('C', 1) 329 | crdt.remove('C', 2) 330 | crdt.add('D') 331 | assert.equal(crdt.hasAll(['D', 'A', 'C', 'B']), false) 332 | }) 333 | }) 334 | 335 | describe('toArray', () => { 336 | it('returns the values of the set as an Array', () => { 337 | const crdt = new LWWSet() 338 | const array = crdt.toArray() 339 | assert.equal(Array.isArray(array), true) 340 | }) 341 | 342 | it('returns values', () => { 343 | const crdt = new LWWSet() 344 | crdt.add('A') 345 | crdt.add('B') 346 | crdt.add('C', 1) 347 | crdt.remove('C', 2) 348 | const array = crdt.toArray() 349 | assert.equal(array.length, 2) 350 | assert.equal(array[0], 'A') 351 | assert.equal(array[1], 'B') 352 | }) 353 | }) 354 | 355 | describe('toJSON', () => { 356 | it('returns the set as JSON object', () => { 357 | const crdt = new LWWSet() 358 | crdt.add('A', 1) 359 | assert.equal(crdt.toJSON().values.length, 1) 360 | assert.equal(crdt.toJSON().values[0].value, 'A') 361 | assert.equal(crdt.toJSON().values[0].added[0], 1) 362 | crdt.remove('A', 2) 363 | assert.equal(crdt.toJSON().values.length, 1) 364 | assert.equal(crdt.toJSON().values[0].value, 'A') 365 | assert.equal(crdt.toJSON().values[0].removed[0], 2) 366 | }) 367 | 368 | it('returns a JSON object after a merge', () => { 369 | const lwwset1 = new LWWSet() 370 | const lwwset2 = new LWWSet() 371 | lwwset1.add('A', 1) 372 | lwwset2.add('B', 1) 373 | lwwset2.remove('B', 2) 374 | lwwset1.add('C', 3) 375 | lwwset1.merge(lwwset2) 376 | assert.equal(lwwset1.toJSON().values.length, 3) 377 | assert.equal(lwwset1.toJSON().values[0].value, 'A') 378 | assert.equal(lwwset1.toJSON().values[1].value, 'C') 379 | assert.equal(lwwset1.toJSON().values[2].value, 'B') 380 | }) 381 | }) 382 | 383 | describe('isEqual', () => { 384 | it('returns true for sets with same values', () => { 385 | const lwwset1 = new LWWSet() 386 | const lwwset2 = new LWWSet() 387 | lwwset1.add('A', 1) 388 | lwwset2.add('A', 1) 389 | assert.equal(lwwset1.isEqual(lwwset2), true) 390 | assert.equal(lwwset2.isEqual(lwwset1), true) 391 | }) 392 | 393 | it('returns true for empty sets', () => { 394 | const lwwset1 = new LWWSet() 395 | const lwwset2 = new LWWSet() 396 | assert.equal(lwwset1.isEqual(lwwset2), true) 397 | assert.equal(lwwset2.isEqual(lwwset1), true) 398 | }) 399 | 400 | it('returns false for sets with different values', () => { 401 | const lwwset1 = new LWWSet() 402 | const lwwset2 = new LWWSet() 403 | lwwset1.add('A') 404 | lwwset2.add('B') 405 | assert.equal(lwwset1.isEqual(lwwset2), false) 406 | assert.equal(lwwset2.isEqual(lwwset1), false) 407 | }) 408 | }) 409 | }) 410 | }) 411 | -------------------------------------------------------------------------------- /test/OR-Set.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { ORSet, CmRDTSet } from '../src/index.js' 3 | import LamportClock from './lamport-clock.js' 4 | 5 | describe('OR-Set', () => { 6 | describe('Instance', () => { 7 | describe('constructor', () => { 8 | it('creates a set', () => { 9 | const crdt = new ORSet() 10 | assert.notEqual(crdt, null) 11 | }) 12 | 13 | it('is a CmRDT Set', () => { 14 | const crdt = new ORSet() 15 | assert.equal(crdt instanceof CmRDTSet, true) 16 | }) 17 | }) 18 | 19 | describe('values', () => { 20 | it('is an Iterator', () => { 21 | const crdt = new ORSet() 22 | assert.equal(crdt.values().toString(), '[object Set Iterator]') 23 | }) 24 | 25 | it('returns an Iterator', () => { 26 | const crdt = new ORSet() 27 | crdt.add('A') 28 | crdt.add('B') 29 | const iterator = crdt.values() 30 | assert.equal(iterator.next().value, 'A') 31 | assert.equal(iterator.next().value, 'B') 32 | }) 33 | }) 34 | 35 | describe('add', () => { 36 | it('adds an element to the set', () => { 37 | const crdt = new ORSet() 38 | const uid = new Date().getTime() 39 | crdt.add('A', uid) 40 | crdt.add('A', uid + 1) 41 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 42 | }) 43 | 44 | it('adds three elements to the set', () => { 45 | const crdt = new ORSet() 46 | crdt.add('A') 47 | crdt.add('B') 48 | crdt.add('C') 49 | assert.deepEqual(new Set(crdt.values()), new Set(['A', 'B', 'C'])) 50 | }) 51 | 52 | it('contains only unique values', () => { 53 | const crdt = new ORSet() 54 | crdt.add(1) 55 | crdt.add('A') 56 | crdt.add('A') 57 | crdt.add(1) 58 | crdt.add('A') 59 | const obj = { hello: 'ABC' } 60 | crdt.add(obj) 61 | crdt.add(obj) 62 | crdt.add({ hello: 'ABCD' }) 63 | 64 | crdt.add(9, 'ok') 65 | 66 | const expectedResult = [ 67 | 'A', 68 | 1, 69 | { hello: 'ABC' }, 70 | { hello: 'ABCD' }, 71 | 9 72 | ] 73 | 74 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 75 | }) 76 | }) 77 | 78 | describe('add with Lamport Clocks', () => { 79 | it('adds an element to the set', () => { 80 | let clock = new LamportClock('A') 81 | const crdt = new ORSet(null, { compareFunc: LamportClock.isEqual }) 82 | crdt.add('A', clock) 83 | crdt.add('A', clock.tick()) 84 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 85 | }) 86 | 87 | it('adds three elements to the set', () => { 88 | let uid = new LamportClock('A') 89 | const crdt = new ORSet(null, { compareFunc: LamportClock.isEqual }) 90 | crdt.add('A', uid) 91 | crdt.add('B', uid) 92 | crdt.add('C', uid) 93 | assert.deepEqual(new Set(crdt.values()), new Set(['A', 'B', 'C'])) 94 | }) 95 | 96 | it('contains only unique values', () => { 97 | let uid = new LamportClock('A') 98 | const crdt = new ORSet(null, { compareFunc: LamportClock.isEqual }) 99 | crdt.add(1, uid.tick()) 100 | crdt.add('A', uid.tick()) 101 | crdt.add('A', uid.tick()) 102 | crdt.add(1, uid.tick()) 103 | crdt.add('A', uid.tick()) 104 | const obj = { hello: 'ABC' } 105 | crdt.add(obj, uid.tick()) 106 | crdt.add(obj, uid) 107 | crdt.add({ hello: 'ABCD' }, uid.tick()) 108 | 109 | crdt.add(9, 'ok') 110 | 111 | const expectedResult = [ 112 | 'A', 113 | 1, 114 | { hello: 'ABC' }, 115 | { hello: 'ABCD' }, 116 | 9 117 | ] 118 | 119 | assert.deepEqual(new Set(crdt.values()), new Set(expectedResult)) 120 | }) 121 | }) 122 | 123 | describe('remove', () => { 124 | it('removes an element from the set', () => { 125 | let tag = 1 126 | const crdt = new ORSet() 127 | crdt.add('A', tag) 128 | crdt.remove('A', tag) 129 | assert.deepEqual(new Set(crdt.values()), new Set([])) 130 | }) 131 | 132 | it('removes an element from the set when element has multiple tags', () => { 133 | const crdt = new ORSet() 134 | crdt.add('A', 1) 135 | crdt.add('A', 2) 136 | crdt.add('A', 3) 137 | crdt.remove('A') 138 | assert.deepEqual(new Set(crdt.values()), new Set([])) 139 | }) 140 | 141 | it('removes an element from the set if all add tags are in removed tags', () => { 142 | const crdt = new ORSet() 143 | const uid = new Date().getTime() 144 | crdt.add('A', uid) 145 | crdt.remove('A') 146 | crdt.add('A', uid + 1) 147 | crdt.add('A', uid + 2) 148 | crdt.remove('A') 149 | assert.deepEqual(new Set(crdt.values()), new Set([])) 150 | }) 151 | 152 | it('doesn\'t remove an element from the set if element wasn\'t in the set', () => { 153 | const crdt = new ORSet() 154 | const uid = new Date().getTime() 155 | crdt.remove('A') 156 | crdt.add('A', uid) 157 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 158 | }) 159 | }) 160 | 161 | describe('remove with Lamport Clocks', () => { 162 | it('removes an element from the set', () => { 163 | let clock = new LamportClock('A') 164 | const crdt = new ORSet(null, { compareFunc: LamportClock.isEqual }) 165 | crdt.add('A', clock) 166 | clock = clock.tick() 167 | crdt.remove('A') 168 | assert.deepEqual(new Set(crdt.values()), new Set([])) 169 | }) 170 | 171 | it('doesn\'t remove an element from the set if element was added with a new tag', () => { 172 | const crdt = new ORSet() 173 | const clock1 = new LamportClock('A') 174 | const clock2 = new LamportClock('B') 175 | crdt.add('A', clock1) 176 | crdt.remove('A') 177 | crdt.add('A', clock1.tick()) 178 | assert.deepEqual(new Set(crdt.values()), new Set(['A'])) 179 | }) 180 | 181 | it('removes an element from the set with the same tag', () => { 182 | let clock1 = new LamportClock('A') 183 | let clock2 = new LamportClock('A') 184 | const crdt = new ORSet(null, { compareFunc: LamportClock.isEqual }) 185 | clock1 = clock1.tick() 186 | crdt.add('A', clock1) 187 | clock2 = clock2.tick() 188 | crdt.add('A', clock1) 189 | crdt.remove('A') 190 | assert.deepEqual(new Set(crdt.values()), new Set([])) 191 | }) 192 | 193 | it('doesn\'t remove an element from the set if element was added with different tags', () => { 194 | const crdt1 = new ORSet() 195 | const crdt2 = new ORSet() 196 | const clock1 = new LamportClock('A') 197 | const clock2 = new LamportClock('B') 198 | crdt1.add('A', clock1) 199 | crdt1.remove('A') 200 | crdt2.add('A', clock2) 201 | crdt2.remove('A') 202 | assert.deepEqual(new Set(crdt1.values()), new Set([])) 203 | assert.deepEqual(new Set(crdt2.values()), new Set([])) 204 | 205 | crdt1.merge(crdt2) 206 | assert.deepEqual(new Set(crdt1.values()), new Set([])) 207 | 208 | crdt1.add('A', clock1.tick()) 209 | assert.deepEqual(new Set(crdt1.values()), new Set(['A'])) 210 | 211 | crdt2.merge(crdt1) 212 | assert.deepEqual(new Set(crdt2.values()), new Set(['A'])) 213 | }) 214 | 215 | it('doesn\'t remove an element from the set if not all add tags are observed', () => { 216 | const crdt1 = new ORSet() 217 | const crdt2 = new ORSet() 218 | const clock1 = new LamportClock('A') 219 | const clock2 = new LamportClock('B') 220 | crdt1.add('A', clock1) 221 | crdt2.add('A', clock2) 222 | crdt2.remove('A') 223 | assert.deepEqual(new Set(crdt1.values()), new Set(['A'])) 224 | assert.deepEqual(new Set(crdt2.values()), new Set([])) 225 | 226 | crdt2.merge(crdt1) 227 | assert.deepEqual(new Set(crdt1.values()), new Set(['A'])) 228 | }) 229 | }) 230 | 231 | describe('merge', () => { 232 | it('merges two sets with same id', () => { 233 | const crdt1 = new ORSet() 234 | const crdt2 = new ORSet() 235 | crdt1.add('A') 236 | crdt2.add('B') 237 | crdt2.add('C') 238 | crdt2.add('D') 239 | crdt2.remove('D') 240 | crdt1.merge(crdt2) 241 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'C']) 242 | }) 243 | 244 | it('merges two sets with same values', () => { 245 | const crdt1 = new ORSet() 246 | const crdt2 = new ORSet() 247 | crdt1.add('A') 248 | crdt2.add('B', 13) 249 | crdt2.add('A') 250 | crdt2.remove('B', 13) 251 | crdt1.merge(crdt2) 252 | assert.deepEqual(crdt1.toArray(), ['A']) 253 | }) 254 | 255 | it('merges two sets with removed values', () => { 256 | const crdt1 = new ORSet() 257 | const crdt2 = new ORSet() 258 | crdt1.add('A', 1) 259 | crdt1.remove('A', 1) 260 | crdt2.add('A', 1) 261 | crdt2.remove('A', 2) 262 | crdt1.add('AAA') 263 | crdt2.add('AAA') 264 | crdt1.add('A', 1) 265 | crdt1.merge(crdt2) 266 | assert.deepEqual(crdt1.toArray(), ['AAA']) 267 | }) 268 | 269 | it('merge four different sets', () => { 270 | const crdt1 = new ORSet() 271 | const crdt2 = new ORSet() 272 | const orset3 = new ORSet() 273 | const crdt4 = new ORSet() 274 | crdt1.add('A') 275 | crdt2.add('B') 276 | orset3.add('C', 7) 277 | crdt4.add('D') 278 | crdt2.add('BB', 1) 279 | crdt2.remove('BB', 1) 280 | 281 | crdt1.merge(crdt2) 282 | crdt1.merge(orset3) 283 | crdt1.remove('C', 8) 284 | crdt1.merge(crdt4) 285 | 286 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'D']) 287 | }) 288 | 289 | it('doesn\'t overwrite other\'s values on merge', () => { 290 | const crdt1 = new ORSet() 291 | const crdt2 = new ORSet() 292 | crdt1.add('A') 293 | crdt2.add('C') 294 | crdt2.add('B') 295 | crdt2.remove('C') 296 | crdt1.merge(crdt2) 297 | crdt1.add('AA') 298 | crdt2.add('CC', 1) 299 | crdt2.add('BB') 300 | crdt2.remove('CC', 1) 301 | crdt1.merge(crdt2) 302 | assert.deepEqual(crdt1.toArray(), ['A', 'B', 'AA', 'BB']) 303 | assert.deepEqual(crdt2.toArray(), ['B', 'BB']) 304 | }) 305 | }) 306 | 307 | describe('has', () => { 308 | it('returns true if given element is in the set', () => { 309 | const crdt = new ORSet() 310 | crdt.add('A') 311 | crdt.add('B') 312 | crdt.add(1) 313 | crdt.add(13) 314 | crdt.remove(13) 315 | const obj = { hello: 'world' } 316 | crdt.add(obj) 317 | assert.equal(crdt.has('A'), true) 318 | assert.equal(crdt.has('B'), true) 319 | assert.equal(crdt.has(1), true) 320 | assert.equal(crdt.has(13), false) 321 | assert.equal(crdt.has(obj), true) 322 | }) 323 | 324 | it('returns false if given element is not in the set', () => { 325 | const crdt = new ORSet() 326 | crdt.add('A') 327 | crdt.add('B') 328 | crdt.add('nothere') 329 | crdt.remove('nothere') 330 | assert.equal(crdt.has('nothere'), false) 331 | }) 332 | }) 333 | 334 | describe('hasAll', () => { 335 | it('returns true if all given elements are in the set', () => { 336 | const crdt = new ORSet() 337 | crdt.add('A') 338 | crdt.add('B') 339 | crdt.add('C') 340 | crdt.remove('C') 341 | crdt.add('D') 342 | assert.equal(crdt.hasAll(['D', 'A', 'B']), true) 343 | }) 344 | 345 | it('returns false if any of the given elements are not in the set', () => { 346 | const crdt = new ORSet() 347 | crdt.add('A') 348 | crdt.add('B') 349 | crdt.add('C') 350 | crdt.remove('C') 351 | crdt.add('D') 352 | assert.equal(crdt.hasAll(['D', 'A', 'C', 'B']), false) 353 | }) 354 | }) 355 | 356 | describe('toArray', () => { 357 | it('returns the values of the set as an Array', () => { 358 | const crdt = new ORSet() 359 | const array = crdt.toArray() 360 | assert.equal(Array.isArray(array), true) 361 | }) 362 | 363 | it('returns values', () => { 364 | const crdt = new ORSet() 365 | crdt.add('A') 366 | crdt.add('B') 367 | crdt.add('C', 1) 368 | crdt.remove('C', 1) 369 | const array = crdt.toArray() 370 | assert.equal(array.length, 2) 371 | assert.equal(array[0], 'A') 372 | assert.equal(array[1], 'B') 373 | }) 374 | }) 375 | 376 | describe('toJSON', () => { 377 | it('returns the set as JSON object', () => { 378 | const crdt = new ORSet() 379 | crdt.add('A', 1) 380 | assert.equal(crdt.toJSON().values.length, 1) 381 | assert.equal(crdt.toJSON().values[0].added.length, 1) 382 | assert.equal(crdt.toJSON().values[0].value, 'A') 383 | crdt.remove('A', 1) 384 | assert.equal(crdt.toJSON().values.length, 1) 385 | assert.equal(crdt.toJSON().values[0].removed.length, 1) 386 | assert.equal(crdt.toJSON().values[0].value, 'A') 387 | }) 388 | 389 | it('returns a JSON object after a merge', () => { 390 | const crdt1 = new ORSet() 391 | const crdt2 = new ORSet() 392 | crdt1.add('A') 393 | crdt2.add('B', 1) 394 | crdt2.remove('B', 1) 395 | crdt1.add('C') 396 | crdt1.merge(crdt2) 397 | crdt2.merge(crdt1) 398 | assert.equal(crdt1.toJSON().values.length, 3) 399 | assert.equal(crdt1.toJSON().values[0].value, 'A') 400 | assert.equal(crdt1.toJSON().values[1].value, 'C') 401 | assert.equal(crdt1.toJSON().values[2].value, 'B') 402 | assert.equal(crdt1.toJSON().values[2].added.length, 1) 403 | assert.equal(crdt1.toJSON().values[2].removed.length, 1) 404 | }) 405 | }) 406 | 407 | describe('isEqual', () => { 408 | it('returns true for sets with same values', () => { 409 | const crdt1 = new ORSet() 410 | const crdt2 = new ORSet() 411 | crdt1.add('A') 412 | crdt2.add('A') 413 | assert.equal(crdt1.isEqual(crdt2), true) 414 | assert.equal(crdt2.isEqual(crdt1), true) 415 | }) 416 | 417 | it('returns true for empty sets', () => { 418 | const crdt1 = new ORSet() 419 | const crdt2 = new ORSet() 420 | assert.equal(crdt1.isEqual(crdt2), true) 421 | assert.equal(crdt2.isEqual(crdt1), true) 422 | }) 423 | 424 | it('returns false for sets with different values', () => { 425 | const crdt1 = new ORSet() 426 | const crdt2 = new ORSet() 427 | crdt1.add('A') 428 | crdt2.add('B') 429 | assert.equal(crdt1.isEqual(crdt2), false) 430 | assert.equal(crdt2.isEqual(crdt1), false) 431 | }) 432 | }) 433 | }) 434 | }) 435 | -------------------------------------------------------------------------------- /test/PN-Counter.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { GCounter, PNCounter } from '../src/index.js' 3 | 4 | describe('PN-Counter', () => { 5 | describe('Instance', () => { 6 | describe('constructor', () => { 7 | it('creates a counter', () => { 8 | const counter = new PNCounter('A') 9 | assert(counter, null) 10 | assert.notEqual(counter.id, null) 11 | assert.notEqual(counter.p, null) 12 | assert.notEqual(counter.n, null) 13 | }) 14 | 15 | it('creates two GCounters', () => { 16 | const gcounters = new PNCounter() 17 | assert.notEqual(gcounters.p, null) 18 | assert.notEqual(gcounters.n, null) 19 | assert.equal(gcounters.p instanceof GCounter, true) 20 | assert.equal(gcounters.n instanceof GCounter, true) 21 | }) 22 | }) 23 | 24 | describe('value', () => { 25 | it('returns the count', () => { 26 | const counter = new PNCounter('A') 27 | assert.equal(counter.value, 0) 28 | }) 29 | 30 | it('returns the count after increment', () => { 31 | const counter = new PNCounter('A') 32 | counter.increment(5) 33 | assert.equal(counter.value, 5) 34 | }) 35 | 36 | it('returns the count after increment and decrement', () => { 37 | const counter = new PNCounter('A') 38 | counter.increment(5) 39 | counter.decrement(3) 40 | assert.equal(counter.value, 2) 41 | }) 42 | }) 43 | 44 | describe('increment', () => { 45 | it('increments the count by 1', () => { 46 | const counter = new PNCounter('A') 47 | counter.increment() 48 | assert.equal(counter.value, 1) 49 | }) 50 | 51 | it('increments the count by 2', () => { 52 | const counter = new PNCounter('A') 53 | counter.increment() 54 | counter.increment() 55 | assert.equal(counter.value, 2) 56 | }) 57 | 58 | it('increments the count by 3', () => { 59 | const counter = new PNCounter('A') 60 | counter.increment(3) 61 | assert.equal(counter.value, 3) 62 | }) 63 | 64 | it('increments the count by 42', () => { 65 | const counter = new PNCounter('A') 66 | counter.increment() 67 | counter.increment(42) 68 | assert.equal(counter.value, 43) 69 | }) 70 | 71 | it('can\'t increment the counter by negative number', () => { 72 | const counter = new PNCounter('A') 73 | counter.increment(-1) 74 | assert.equal(counter.value, 0) 75 | }) 76 | }) 77 | 78 | describe('decrement', () => { 79 | it('decrements the count by 1', () => { 80 | const counter = new PNCounter('A') 81 | counter.decrement() 82 | assert.equal(counter.value, -1) 83 | }) 84 | 85 | it('increments the count by 2 then decrements by 1', () => { 86 | const counter = new PNCounter('A') 87 | counter.increment() 88 | counter.increment() 89 | counter.decrement() 90 | assert.equal(counter.value, 1) 91 | }) 92 | 93 | it('increments the count by 3 then decrement by 2', () => { 94 | const counter = new PNCounter('A') 95 | counter.increment(3) 96 | counter.decrement(2) 97 | assert.equal(counter.value, 1) 98 | }) 99 | 100 | it('increment the count by 100 then decrement by 58', () => { 101 | const counter = new PNCounter('A') 102 | counter.increment() 103 | counter.increment(99) 104 | counter.decrement(58) 105 | assert.equal(counter.value, 42) 106 | }) 107 | 108 | it('can\'t decrement by negative number', () => { 109 | const counter = new PNCounter('A') 110 | counter.decrement(-1) 111 | assert.equal(counter.value, 0) 112 | }) 113 | }) 114 | 115 | describe('merge', () => { 116 | it('merges two counters with same id', () => { 117 | const counter1 = new PNCounter('A') 118 | const counter2 = new PNCounter('A') 119 | counter1.increment() 120 | counter2.increment() 121 | counter1.merge(counter2) 122 | assert.equal(counter1.value, 1) 123 | }) 124 | 125 | it('merges two counters with same values', () => { 126 | const counter1 = new PNCounter('A') 127 | const counter2 = new PNCounter('B') 128 | counter1.increment() 129 | counter2.increment() 130 | counter1.merge(counter2) 131 | counter2.merge(counter1) 132 | assert.equal(counter1.value, 2) 133 | assert.equal(counter2.value, 2) 134 | }) 135 | 136 | it('merges four different counters', () => { 137 | const counter1 = new PNCounter('A') 138 | const counter2 = new PNCounter('B') 139 | const counter3 = new PNCounter('C') 140 | const counter4 = new PNCounter('D') 141 | counter1.increment() 142 | counter2.increment(5) 143 | counter3.increment(5) 144 | counter3.decrement(3) 145 | counter4.increment() 146 | counter4.increment() 147 | counter4.decrement() 148 | counter1.merge(counter2) 149 | counter1.merge(counter3) 150 | counter1.merge(counter4) 151 | assert.equal(counter1.value, 9) 152 | }) 153 | 154 | it('doesn\'t overwrite its own value on merge', () => { 155 | const counter1 = new PNCounter('A') 156 | const counter2 = new PNCounter('B') 157 | counter1.increment(3) 158 | counter2.increment() 159 | counter1.merge(counter2) 160 | counter2.merge(counter1) 161 | counter1.decrement() 162 | counter1.merge(counter2) 163 | assert.equal(counter1.value, 3) 164 | }) 165 | 166 | it('doesn\'t overwrite others\' values on merge', () => { 167 | const counter1 = new PNCounter('A') 168 | const counter2 = new PNCounter('B') 169 | counter1.increment() 170 | counter2.increment() 171 | counter1.merge(counter2) 172 | counter2.merge(counter1) 173 | counter1.increment(2) 174 | counter2.increment() 175 | counter2.decrement(2) 176 | counter1.merge(counter2) 177 | assert.equal(counter1.value, 3) 178 | }) 179 | }) 180 | 181 | describe('toJSON', () => { 182 | it('returns the counter as JSON object', () => { 183 | const counter = new PNCounter('A') 184 | assert.equal(counter.toJSON().id, 'A') 185 | assert.equal(counter.toJSON().p.A, 0) 186 | assert.equal(counter.toJSON().n.A, 0) 187 | }) 188 | 189 | it('returns a JSON object after a merge', () => { 190 | const counter1 = new PNCounter('A') 191 | const counter2 = new PNCounter('B') 192 | counter1.increment() 193 | counter2.increment() 194 | counter1.decrement() 195 | counter2.decrement() 196 | counter1.merge(counter2) 197 | counter2.merge(counter1) 198 | assert.equal(Object.keys(counter1.toJSON().p).length, 2) 199 | assert.equal(Object.keys(counter1.toJSON().n).length, 2) 200 | assert.equal(counter1.toJSON().p.A, 1) 201 | assert.equal(counter1.toJSON().p.B, 1) 202 | assert.equal(counter2.toJSON().p.A, 1) 203 | assert.equal(counter2.toJSON().p.B, 1) 204 | assert.equal(counter1.toJSON().n.A, 1) 205 | assert.equal(counter1.toJSON().n.B, 1) 206 | assert.equal(counter2.toJSON().n.A, 1) 207 | assert.equal(counter2.toJSON().n.B, 1) 208 | }) 209 | }) 210 | 211 | describe('isEqual', () => { 212 | it('returns true for equal counters', () => { 213 | const counter1 = new PNCounter('A') 214 | const counter2 = new PNCounter('A') 215 | counter1.increment() 216 | counter2.increment() 217 | counter1.decrement() 218 | counter2.decrement() 219 | assert.equal(counter1.isEqual(counter2), true) 220 | }) 221 | 222 | it('returns false for unequal counters - different id', () => { 223 | const counter1 = new PNCounter('A') 224 | const counter2 = new PNCounter('B') 225 | assert.equal(counter1.isEqual(counter2), false) 226 | }) 227 | 228 | it('returns false for unequal counters - same id, different counts', () => { 229 | const counter1 = new PNCounter('A') 230 | const counter2 = new PNCounter('A') 231 | counter1.increment() 232 | counter2.increment() 233 | counter2.decrement() 234 | assert.equal(counter1.isEqual(counter2), false) 235 | }) 236 | }) 237 | }) 238 | 239 | describe('PNCounter.from', () => { 240 | it('creates a new counter from JSON object', () => { 241 | const counter1 = new PNCounter('A') 242 | counter1.increment() 243 | counter1.decrement() 244 | const gCounter1 = new GCounter('A') 245 | gCounter1.increment() 246 | const gCounter2 = new GCounter('A') 247 | gCounter2.increment() 248 | const input = { 249 | id: 'A', 250 | p: gCounter1, 251 | n: gCounter2 252 | } 253 | 254 | const counter2 = PNCounter.from(input) 255 | assert.equal(PNCounter.isEqual(counter1, counter2), true) 256 | assert.equal(counter2.id, 'A') 257 | assert.equal(counter2.value, 0) 258 | }) 259 | }) 260 | 261 | describe('PNCounter.isEqual', () => { 262 | it('returns true if two PNCounters are equal', () => { 263 | const values = ['A', 'B', 'C'] 264 | const counter1 = new PNCounter('A') 265 | const counter2 = new PNCounter('A') 266 | const counter3 = new PNCounter('B') 267 | counter1.increment(2) 268 | counter1.decrement() 269 | counter2.increment(2) 270 | counter2.decrement() 271 | assert.equal(PNCounter.isEqual(counter1, counter2), true) 272 | assert.equal(PNCounter.isEqual(counter1, counter3), false) 273 | }) 274 | }) 275 | }) 276 | -------------------------------------------------------------------------------- /test/lamport-clock.js: -------------------------------------------------------------------------------- 1 | export default class LamportClock { 2 | constructor (id, time) { 3 | this.id = id 4 | this.time = time || 0 5 | } 6 | 7 | tick () { 8 | return new LamportClock(this.id, this.time + 1) 9 | } 10 | 11 | merge (clock) { 12 | this.time = Math.max(this.time, clock.time) 13 | return new LamportClock(this.id, this.time) 14 | } 15 | 16 | clone () { 17 | return new LamportClock(this.id, this.time) 18 | } 19 | 20 | static isEqual (a, b) { 21 | return a.id == b.id && a.time == b.time 22 | } 23 | 24 | static compare (a, b) { 25 | if (!a || !a.time) a = { time: 0 } 26 | if (!b || !b.time) b = { time: 0 } 27 | 28 | // Calculate the "distance" based on the clock, ie. lower or greater 29 | var dist = a.time - b.time 30 | 31 | // If the sequence number is the same (concurrent events), 32 | // and the IDs are different, take the one with a "lower" id 33 | if (dist === 0 && a.id !== b.id) return a.id < b.id ? -1 : 1 34 | 35 | return dist 36 | } 37 | } 38 | --------------------------------------------------------------------------------