├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .huskyrc.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── logs ├── logs-20170117-test1 │ ├── log-20170117-test1-village-august-immune-1.json │ └── log-20170117-test1-village-august-immune-2.json ├── logs-20170117-test2 │ ├── log-20170117-test2-galileo-camel-motor-1.json │ └── log-20170117-test2-galileo-camel-motor-2.json ├── logs-20170117-test3 │ ├── log-20170117-test3-galileo-camel-motor.json │ └── log-20170117-test3-village-august-immune.json ├── logs-20170117-test4 │ ├── log-20170117-test4-nobel-letter-neutral.json │ └── log-20170117-test4-telex-lobby-sweet.json ├── logs-20170117-test5 │ ├── log-20170117-test5-banana-oxygen-pilgrim.json │ ├── log-20170117-test5-epoxy-tango-engine.json │ └── log-20170117-test5-jasmine-bravo-vital.json ├── logs-4u0HUYhI3a │ ├── log-4u0HUYhI3a-amigo-pilot-verbal-1.json │ ├── log-4u0HUYhI3a-amigo-pilot-verbal-2.json │ └── log-4u0HUYhI3a-amigo-pilot-verbal-3.json ├── logs-AKjlI6j4yD │ ├── log-AKjlI6j4yD-combat-salary-clara-1.json │ ├── log-AKjlI6j4yD-combat-salary-clara-2.json │ └── log-AKjlI6j4yD-combat-salary-clara-3.json ├── logs-ct20171019 │ ├── log-ct20171019-join-iris-berlin.json │ └── log-ct20171019-ocean-button-contour.json ├── logs-iWYksvoqJo │ ├── log-iWYksvoqJo-holiday-field-summer-1.json │ ├── log-iWYksvoqJo-holiday-field-summer-2.json │ └── log-iWYksvoqJo-holiday-field-summer-3.json ├── logs-quTF5eAc │ ├── log-quTF5eAc-harbor-royal-explain.json │ ├── log-quTF5eAc-number-speed-metal.json │ └── log-quTF5eAc-prelude-chris-tourist.json └── logs-rXFbhTf8Ct │ ├── log-rXFbhTf8Ct-additional-operations.json │ ├── log-rXFbhTf8Ct-incorrect-operations-order.json │ └── log-rXFbhTf8Ct-original-log.json ├── package-lock.json ├── package.json ├── src ├── data-validation.ts ├── dot.ts ├── epoch │ ├── epoch.ts │ ├── epochid.ts │ └── epochstore.ts ├── helpers.ts ├── identifier.ts ├── identifierinterval.ts ├── identifiertuple.ts ├── idfactory.ts ├── index.ts ├── int32.ts ├── iteratorhelperidentifier.ts ├── logootsblock.ts ├── logootsropes.ts ├── operations │ ├── delete │ │ ├── logootsdel.ts │ │ ├── renamablelogootsdel.ts │ │ └── textdelete.ts │ ├── insert │ │ ├── logootsadd.ts │ │ ├── renamablelogootsadd.ts │ │ └── textinsert.ts │ ├── logootsoperation.ts │ ├── renamablelistoperation.ts │ ├── renamablelogootsoperation.ts │ ├── rename │ │ └── logootsrename.ts │ └── textoperation.ts ├── ordering.ts ├── renamablereplicablelist.ts ├── renamingmap │ ├── renamingmap.ts │ └── renamingmapstore.ts ├── responseintnode.ts ├── ropesnodes.ts ├── stats.ts └── textutils.ts ├── test ├── .editorconfig ├── .eslintrc.json ├── epochstore.test.ts ├── helpers.ts ├── identifier.test.ts ├── identifierinterval.test.ts ├── identifiertuple.test.ts ├── idfactory.test.ts ├── int32.test.ts ├── iteratorhelperidentifier.test.ts ├── logootsropes.test.ts ├── logs.test.ts ├── renamablereplicablelist.test.ts ├── renamingmap.test.ts ├── renamingmapstore.test.ts ├── ropesnodes.test.ts ├── tree.test.ts └── tslint.json ├── trees └── trees-nct │ ├── tree-nct-nikita-button-shirt-1.json │ └── tree-nct-nikita-button-shirt-2.json ├── tsconfig.es2015.json ├── tsconfig.json ├── tsconfig.main.json ├── tsconfig.module.json ├── tsconfig.types.json └── tslint.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "conaclos/base" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary 2 | yarn.lock binary 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | # Commenting this out is preferred by some people, see 3 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 4 | node_modules 5 | 6 | # Generated code 7 | .tested 8 | dist 9 | 10 | # Generated pack 11 | mute-structs-*.tgz 12 | 13 | # Document files 14 | logs/**/doc-* 15 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npm test", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: xenial 3 | language: node_js 4 | cache: npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - 'node' 9 | - '12' 10 | - '11' 11 | script: 12 | - npm test 13 | jobs: 14 | include: 15 | - stage: npm release 16 | node_js: 'node' 17 | script: npm run build 18 | deploy: 19 | provider: npm 20 | api_token: 21 | secure: jbPUO4ggV+A8USN3aqhZ59bjav1UUiCHvLT+B/aj+XnjMv8fAH6XgdF/eb4k/nQOdg9S3VfwHnVv5FKG+6SWufDuwAoGVNiZPcsKFtlPta97YfxgICRmk5DcK4jpqfG0/jmDc1K/W710zKIjxgYHCb1ZukWF4v20DOIz7NL0diM= 22 | edge: true 23 | on: 24 | tags: true 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.0.5](https://github.com/coast-team/mute-structs/compare/v2.0.4...v2.0.5) (2021-10-07) 6 | 7 | ### [2.0.4](https://github.com/coast-team/mute-structs/compare/v2.0.3...v2.0.4) (2021-10-07) 8 | 9 | ### [2.0.3](https://github.com/coast-team/mute-structs/compare/v2.0.2...v2.0.3) (2021-10-07) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * **reverserenameid:** handle missing cases when lastId < newLastId ([34f8cea](https://github.com/coast-team/mute-structs/commit/34f8cea167cc7a20d04499abfded64ade1ce303e)) 15 | 16 | ### [2.0.2](https://github.com/coast-team/mute-structs/compare/v2.0.1...v2.0.2) (2020-08-17) 17 | 18 | ### [2.0.1](https://github.com/coast-team/mute-structs/compare/v2.0.0...v2.0.1) (2020-08-11) 19 | 20 | 21 | ### Features 22 | 23 | * **epochstore:** add compareEpochFullIds() ([1d2de9e](https://github.com/coast-team/mute-structs/commit/1d2de9e1bdf9aa435caecacaf4269b08d3100ae0)) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **renamablereplicablelist:** fix priority order between epochs ([d59c8d1](https://github.com/coast-team/mute-structs/commit/d59c8d12622e38c98d1e6aeea39343112f59f526)) 29 | 30 | ## [2.0.0](https://github.com/coast-team/mute-structs/compare/v2.0.0-11...v2.0.0) (2020-07-31) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * **logootsropes:** merge fix on append/prepend from master branch ([fee69ff](https://github.com/coast-team/mute-structs/commit/fee69ff6728ca64b546825f1242c82141e5831f6)) 36 | 37 | ## [1.1.0](https://github.com/coast-team/mute-structs/compare/v2.0.0-1...v1.1.0) (2019-03-04) 38 | 39 | 40 | ### Features 41 | 42 | * **ropesnodes:** add max getter to retrieve max id from a subtree ([ee779a7](https://github.com/coast-team/mute-structs/commit/ee779a7d3662cbe2952381c7b2a7fee9ce1ac52b)) 43 | * **ropesnodes:** add min getter to retrieve min id from a subtree ([ee2da24](https://github.com/coast-team/mute-structs/commit/ee2da24fccdc80a8c4d447ca4412627f8254fd94)) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **logootsropes:** fix checks before appending/prepending ([91f24b5](https://github.com/coast-team/mute-structs/commit/91f24b52a2a5a7fb695b92baf087cd5e4b8ce8b9)) 49 | 50 | ## [2.0.0-11](https://github.com/coast-team/mute-structs/compare/v2.0.0-10...v2.0.0-11) (2020-07-10) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * **idfactory:** edit createBetweenPosition() to prevent overflow ([a328b00](https://github.com/coast-team/mute-structs/commit/a328b0081402ae1a02c90861e2cc4d68a497c105)) 56 | 57 | ## [2.0.0-10](https://github.com/coast-team/mute-structs/compare/v2.0.0-9...v2.0.0-10) (2020-07-10) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * **idfactory:** fix createBetweenPosition() to pass added test case ([439ad1c](https://github.com/coast-team/mute-structs/commit/439ad1ca773cec1008c6d366b445c059ceb65863)) 63 | 64 | ## [2.0.0-9](https://github.com/coast-team/mute-structs/compare/v2.0.0-8...v2.0.0-9) (2020-07-09) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * **renamable-list:** use renameId() instead of renameIdInterval() ([22ba09a](https://github.com/coast-team/mute-structs/commit/22ba09ace073d2d9e4244cb7dc462a42c169f1f4)) 70 | 71 | ## [2.0.0-8](https://github.com/coast-team/mute-structs/compare/v2.0.0-7...v2.0.0-8) (2020-06-16) 72 | 73 | 74 | ### Features 75 | 76 | * **renameid:** handle properly some ids causally inserted to rename op ([3f019a3](https://github.com/coast-team/mute-structs/commit/3f019a3b5bcceb0576c22bb6d7566edb00f34ed1)) 77 | * **renameid:** handle properly some ids causally inserted to rename op ([d59c1d5](https://github.com/coast-team/mute-structs/commit/d59c1d58f1ff58933a8e7e33f271a2a04e746a0b)) 78 | * **renameid:** handle properly some ids causally inserted to rename op ([141a0e0](https://github.com/coast-team/mute-structs/commit/141a0e0e7027fca4dc3ee80551bb289e04c5f780)) 79 | * **renameid:** handle properly some ids causally inserted to rename op ([c1058cc](https://github.com/coast-team/mute-structs/commit/c1058ccd60618e833ffe449227c1899aab53b2bc)) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * **idfactory:** fix createBetweenPosition() ([5b0eaa0](https://github.com/coast-team/mute-structs/commit/5b0eaa0938239e689e589753e9bea2ea3c11b20f)) 85 | * **renamingmap:** edit createBetweenPosition() to fix failing test case ([a8ca694](https://github.com/coast-team/mute-structs/commit/a8ca6943dd2362a65cfb7449611f4cf40afe0293)) 86 | * **renamingmap:** fix reverseRenameId() ([3fe88fe](https://github.com/coast-team/mute-structs/commit/3fe88fe42ea3608b4fcc43ada6a250c9916d475a)) 87 | * **test:** fix initial settings of a test case ([2a18504](https://github.com/coast-team/mute-structs/commit/2a1850429870af90863554513463d0ef82754b47)) 88 | 89 | ## [2.0.0-7](https://github.com/coast-team/mute-structs/compare/v2.0.0-6...v2.0.0-7) (2019-11-21) 90 | 91 | 92 | # [2.0.0-6](https://github.com/coast-team/mute-structs/compare/v2.0.0-5...v2.0.0-6) (2019-11-21) 93 | 94 | 95 | 96 | 97 | # [2.0.0-5](https://github.com/coast-team/mute-structs/compare/v2.0.0-4...v2.0.0-5) (2019-11-21) 98 | 99 | 100 | ### Performance Improvements 101 | 102 | * **flat:** use flatMap(...) instead of map(...) + reduce(flatten) ([87b8108](https://github.com/coast-team/mute-structs/commit/87b8108)) 103 | 104 | 105 | 106 | 107 | # [2.0.0-1](https://github.com/coast-team/mute-structs/compare/v2.0.0-0...v2.0.0-1) (2019-01-28) 108 | 109 | 110 | ### Code Refactoring 111 | 112 | * **renamablereplicablelist:** impose usage of a factory ([04ab821](https://github.com/coast-team/mute-structs/commit/04ab821)) 113 | 114 | 115 | ### Features 116 | 117 | * export several new types ([593c96e](https://github.com/coast-team/mute-structs/commit/593c96e)) 118 | * **data-validation:** add isArrayFromMap() ([50b5345](https://github.com/coast-team/mute-structs/commit/50b5345)) 119 | * **epoch:** add fromPlain() ([1afa238](https://github.com/coast-team/mute-structs/commit/1afa238)) 120 | * **epochid:** add fromPlain() ([f038ff9](https://github.com/coast-team/mute-structs/commit/f038ff9)) 121 | * **epochstore:** add fromPlain() ([00aa21c](https://github.com/coast-team/mute-structs/commit/00aa21c)) 122 | * **logootsropes:** change signature of fromPlain() ([391becf](https://github.com/coast-team/mute-structs/commit/391becf)) 123 | * **renamablereplicablelist:** add fromPlain() ([ce47db0](https://github.com/coast-team/mute-structs/commit/ce47db0)) 124 | * **renamingmap:** add fromPlain() ([4b7b961](https://github.com/coast-team/mute-structs/commit/4b7b961)) 125 | * **renamingmapstore:** add fromPlain() ([b3ca2df](https://github.com/coast-team/mute-structs/commit/b3ca2df)) 126 | 127 | 128 | ### BREAKING CHANGES 129 | 130 | * **renamablereplicablelist:** the constructor is now private 131 | * **logootsropes:** remove parameters replicaNumber and clock 132 | 133 | 134 | 135 | 136 | # [2.0.0-0](https://github.com/coast-team/mute-structs/compare/v1.0.0...v2.0.0-0) (2019-01-22) 137 | 138 | 139 | ### Bug Fixes 140 | 141 | * **epoch:** Fix assertion on parentId ([d5fa2d0](https://github.com/coast-team/mute-structs/commit/d5fa2d0)) 142 | * **epoch:** Remove import of SafeAny ([ae59b6b](https://github.com/coast-team/mute-structs/commit/ae59b6b)) 143 | * **epochstore:** Use equals() instead of === to compare epochs ([3099e11](https://github.com/coast-team/mute-structs/commit/3099e11)) 144 | * **extendedrenamingmap:** Support new cases and add corresponding tests ([3025e06](https://github.com/coast-team/mute-structs/commit/3025e06)) 145 | * **helpers:** Fix findPredecessor() ([14bd97d](https://github.com/coast-team/mute-structs/commit/14bd97d)) 146 | * **renamingmap:** Fix reverseRenameId() in the scenario highlighted in 461e0eb ([fa859b7](https://github.com/coast-team/mute-structs/commit/fa859b7)) 147 | * **renamingmap:** Fix reverseRenameId() of concurrently inserted id such as newFirstId < id < firstId ([c734de0](https://github.com/coast-team/mute-structs/commit/c734de0)) 148 | 149 | 150 | ### Features 151 | 152 | * **epoch:** Add equals() ([cd9f8c6](https://github.com/coast-team/mute-structs/commit/cd9f8c6)) 153 | * **epochstore:** Add getEpochPath() to retrieve the full path of an epoch ([e722283](https://github.com/coast-team/mute-structs/commit/e722283)) 154 | * **epochstore:** Add getPathBetweenEpochs() to retrieve the path between two given epochs ([e74c589](https://github.com/coast-team/mute-structs/commit/e74c589)) 155 | * **extendedrenamingmap:** Handle additional tricky cases in renameId() and reverseRenameId() ([1a7b53d](https://github.com/coast-team/mute-structs/commit/1a7b53d)) 156 | * **extendedrenamingmap:** Implement reverseRenameId() ([20888cc](https://github.com/coast-team/mute-structs/commit/20888cc)) 157 | * **helpers:** Add findPredecessor() ([445ead5](https://github.com/coast-team/mute-structs/commit/445ead5)) 158 | * **helpers:** Add flatten() ([ef11bad](https://github.com/coast-team/mute-structs/commit/ef11bad)) 159 | * **helpers:** Add isSorted() ([04f4e94](https://github.com/coast-team/mute-structs/commit/04f4e94)) 160 | * **identifier:** Add concat() ([12bb1e0](https://github.com/coast-team/mute-structs/commit/12bb1e0)) 161 | * **identifierinterval:** Add mergeIdsIntoIntervals() ([90e3138](https://github.com/coast-team/mute-structs/commit/90e3138)) 162 | * **identifierinterval:** Add toIds() ([5ae27dd](https://github.com/coast-team/mute-structs/commit/5ae27dd)) 163 | * **idfactory:** Add createAtPosition() ([9c4a61a](https://github.com/coast-team/mute-structs/commit/9c4a61a)) 164 | * **idfactory:** Retain INT32_BOTTOM/TOP for renaming purpose, introduce INT32_BOTTOM/TOP_USER ([492328c](https://github.com/coast-team/mute-structs/commit/492328c)) 165 | * **logootsadd:** Add insertedIds() ([31cc63f](https://github.com/coast-team/mute-structs/commit/31cc63f)) 166 | * **logootsropes:** Remove ability to prepend using insertLocal() ([979170b](https://github.com/coast-team/mute-structs/commit/979170b)) 167 | * **renamablereplicablelist:** Add delLocal() and corresponding RenamableLogootSDel ([7584deb](https://github.com/coast-team/mute-structs/commit/7584deb)) 168 | * **renamablereplicablelist:** Add insertLocal() and corresponding RenamableLogootSAdd ([e52521c](https://github.com/coast-team/mute-structs/commit/e52521c)) 169 | * **renamablereplicablelist:** Add insertRemote() ([fc6b9d8](https://github.com/coast-team/mute-structs/commit/fc6b9d8)) 170 | * **renamablereplicablelist:** Implement delRemote() ([a7acacc](https://github.com/coast-team/mute-structs/commit/a7acacc)) 171 | * **renamablereplicablelist:** Implement renameRemote() ([0d0c537](https://github.com/coast-team/mute-structs/commit/0d0c537)) 172 | * **renamablereplicablelist:** Implement renameRemote() to revert and apply correct renaming operations ([6ddf0ae](https://github.com/coast-team/mute-structs/commit/6ddf0ae)) 173 | * **renamablereplicablelist:** Store observed renamingMaps ([ed217d6](https://github.com/coast-team/mute-structs/commit/ed217d6)) 174 | * **renamablereplicablelist:** Use renameIdsFromEpochToCurrent() to properly rename deleted ids if needed ([73ce5b3](https://github.com/coast-team/mute-structs/commit/73ce5b3)) 175 | * **renamablereplicablelist:** Use renameIdsFromEpochToCurrent() to properly rename new ids if needed ([747ffef](https://github.com/coast-team/mute-structs/commit/747ffef)) 176 | * Add Epoch ([9523bbc](https://github.com/coast-team/mute-structs/commit/9523bbc)) 177 | * Add EpochId ([3a4757d](https://github.com/coast-team/mute-structs/commit/3a4757d)) 178 | * Add EpochStore ([63dd210](https://github.com/coast-team/mute-structs/commit/63dd210)) 179 | * Add RenamableListOperation ([58e78fd](https://github.com/coast-team/mute-structs/commit/58e78fd)) 180 | * Add RenamableLogootSOperation ([44c8b7e](https://github.com/coast-team/mute-structs/commit/44c8b7e)) 181 | * Add RenamableReplicableList ([33c71cb](https://github.com/coast-team/mute-structs/commit/33c71cb)) 182 | * Add renaming operation LogootSRename ([30eb882](https://github.com/coast-team/mute-structs/commit/30eb882)) 183 | * Add RenamingMap ([14ef3c8](https://github.com/coast-team/mute-structs/commit/14ef3c8)) 184 | * Add RenamingMap ([8ea767d](https://github.com/coast-team/mute-structs/commit/8ea767d)) 185 | * Add RenamingMapStore ([6b72596](https://github.com/coast-team/mute-structs/commit/6b72596)) 186 | * **ropesnodes:** Add mkNodeAt() ([fb203a4](https://github.com/coast-team/mute-structs/commit/fb203a4)) 187 | 188 | 189 | 190 | # [1.1.0](https://github.com/coast-team/mute-structs/compare/v1.0.0...v1.1.0) (2019-03-04) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * **logootsropes:** fix checks before appending/prepending ([91f24b5](https://github.com/coast-team/mute-structs/commit/91f24b5)) 196 | 197 | 198 | ### Features 199 | 200 | * **ropesnodes:** add max getter to retrieve max id from a subtree ([ee779a7](https://github.com/coast-team/mute-structs/commit/ee779a7)) 201 | * **ropesnodes:** add min getter to retrieve min id from a subtree ([ee2da24](https://github.com/coast-team/mute-structs/commit/ee2da24)) 202 | 203 | 204 | 205 | # [1.0.0](https://github.com/coast-team/mute-structs/compare/v0.5.8...v1.0.0) (2019-01-21) 206 | 207 | 208 | 209 | 210 | ## [0.5.8](https://github.com/coast-team/mute-structs/compare/v0.5.7...v0.5.8) (2019-01-21) 211 | 212 | 213 | 214 | 215 | ## [0.5.7](https://github.com/coast-team/mute-structs/compare/v0.5.6...v0.5.7) (2019-01-21) 216 | 217 | 218 | 219 | 220 | ## [0.5.6](https://github.com/coast-team/mute-structs/compare/v0.5.5...v0.5.6) (2019-01-21) 221 | 222 | 223 | 224 | 225 | ## [0.5.5](https://github.com/coast-team/mute-structs/compare/v0.5.4...v0.5.5) (2019-01-21) 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MUTE-structs 2 | 3 | [![Build Status](https://travis-ci.org/coast-team/mute-structs.svg?branch=master)](https://travis-ci.org/coast-team/mute-structs) 4 | 5 | MUTE-structs is a Typescript library that provides an implementation of the 6 | LogootSplit CRDT algorithm [[André et al., 2013]](#ref-1). It is an optimistic replication 7 | algorithm that ensures eventual consistency on replicated text sequences. 8 | It is used in a *real-time collaborative text editor* based on CRDT named MUTE. 9 | 10 | 11 | #### References 12 | 13 | [André et al., 2013] Luc André, Stéphane Martin, Gérald Oster et Claudia-Lavinia Ignat. **Supporting Adaptable Granularity of Changes for Massive-scale Collaborative Editing**. In *Proceedings of the international conference on collaborative computing: networking, applications and worksharing - CollaborateCom 2013*. IEEE Computer Society, Austin, Texas, USA, october 2013, pages 50–59. doi: [10.4108/icst.collaboratecom.2013.254123](https://dx.doi.org/10.4108/icst.collaboratecom.2013.254123). url: https://hal.inria.fr/hal-00903813/. 14 | 15 | 16 | ## Installation 17 | 18 | ``` 19 | npm install mute-structs 20 | ``` 21 | 22 | ## See also 23 | 24 | * [**mute**](https://github.com/coast-team/mute) 25 | * [**mute-core**](https://github.com/coast-team/mute-core) 26 | 27 | ## License 28 | 29 | **MUTE-structs** is licensed under the GNU Affero General Public License 3. 30 | 31 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 32 | 33 | This program is free software: you can redistribute it and/or modify 34 | it under the terms of the GNU Affero General Public License as 35 | published by the Free Software Foundation, either version 3 of the 36 | License, or (at your option) any later version. 37 | 38 | This program is distributed in the hope that it will be useful, 39 | but WITHOUT ANY WARRANTY; without even the implied warranty of 40 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 41 | GNU Affero General Public License for more details. 42 | 43 | You should have received a copy of the GNU Affero General Public License 44 | along with this program. If not, see . 45 | 46 | The documentation, tutorial and source code are intended as a community 47 | resource and you can basically use, copy and improve them however you want. 48 | Included works are subject to their respective licenses. 49 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /logs/logs-rXFbhTf8Ct/log-rXFbhTf8Ct-additional-operations.json: -------------------------------------------------------------------------------- 1 | {"vector":{},"richLogootSOps":[{"id":691992486,"clock":0,"logootSOp":{"id":{"tuples":[{"random":-1703428533,"replicaNumber":691992486,"clock":0,"offset":0}]},"content":"A"}},{"id":691992486,"clock":1,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1703428533,"replicaNumber":691992486,"clock":0,"offset":0}]},"end":0}]}},{"id":691992486,"clock":2,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":0}]},"content":"B"}},{"id":691992486,"clock":3,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":1}]},"content":"r"}},{"id":691992486,"clock":4,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":2}]},"content":"a"}},{"id":691992486,"clock":5,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":3}]},"content":"n"}},{"id":691992486,"clock":6,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":4}]},"content":"d"}},{"id":691992486,"clock":7,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":5}]},"content":" "}},{"id":691992486,"clock":8,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":6}]},"content":"n"}},{"id":691992486,"clock":9,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":7}]},"content":"e"}},{"id":691992486,"clock":10,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":8}]},"content":"w"}},{"id":691992486,"clock":11,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9}]},"content":" "}},{"id":691992486,"clock":12,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":10}]},"content":"d"}},{"id":691992486,"clock":13,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":11}]},"content":"o"}},{"id":691992486,"clock":14,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":12}]},"content":"c"}},{"id":691992486,"clock":15,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":13}]},"content":"u"}},{"id":691992486,"clock":16,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":14}]},"content":"m"}},{"id":691992486,"clock":17,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":15}]},"content":"e"}},{"id":691992486,"clock":18,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":16}]},"content":"n"}},{"id":691992486,"clock":19,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":17}]},"content":"t"}},{"id":691992486,"clock":20,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":18}]},"content":" "}},{"id":691992486,"clock":21,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":19}]},"content":"!"}},{"id":1232192738,"clock":0,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":0}]},"content":"\n"}},{"id":1232192738,"clock":1,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":1}]},"content":"A"}},{"id":1232192738,"clock":2,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":2}]},"content":"n"}},{"id":1232192738,"clock":3,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":3}]},"content":"d"}},{"id":1232192738,"clock":4,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":4}]},"content":" "}},{"id":1232192738,"clock":5,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":5}]},"content":"i"}},{"id":1232192738,"clock":6,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":6}]},"content":"t"}},{"id":1232192738,"clock":7,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":7}]},"content":"'"}},{"id":1232192738,"clock":8,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":8}]},"content":"s"}},{"id":1232192738,"clock":9,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":9}]},"content":" "}},{"id":1232192738,"clock":10,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":10}]},"content":"w"}},{"id":1232192738,"clock":11,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":11}]},"content":"o"}},{"id":1232192738,"clock":12,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":12}]},"content":"r"}},{"id":1232192738,"clock":13,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":13}]},"content":"k"}},{"id":1232192738,"clock":14,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":14}]},"content":"i"}},{"id":1232192738,"clock":15,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":15}]},"content":"n"}},{"id":1232192738,"clock":16,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16}]},"content":"g"}},{"id":1232192738,"clock":17,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":17}]},"content":" "}},{"id":1232192738,"clock":18,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":18}]},"content":"="}},{"id":1232192738,"clock":19,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":18}]},"end":18}]}},{"id":1232192738,"clock":20,"logootSOp":{"id":{"tuples":[{"random":1384979433,"replicaNumber":1232192738,"clock":1,"offset":0}]},"content":"!"}},{"id":1232192738,"clock":21,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":10}]},"end":17}]}},{"id":1232192738,"clock":22,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":2063810427,"replicaNumber":1232192738,"clock":2,"offset":0}]},"content":"c"}},{"id":1232192738,"clock":23,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":2063810427,"replicaNumber":1232192738,"clock":2,"offset":0}]},"end":0}]}},{"id":1232192738,"clock":24,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":0}]},"content":"v"}},{"id":1232192738,"clock":25,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":1}]},"content":"e"}},{"id":1232192738,"clock":26,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":2}]},"content":"r"}},{"id":1232192738,"clock":27,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":3}]},"content":"s"}},{"id":1232192738,"clock":28,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":4}]},"content":"i"}},{"id":1232192738,"clock":29,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":5}]},"content":"o"}},{"id":1232192738,"clock":30,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":6}]},"content":"n"}},{"id":691992486,"clock":22,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":0}]},"content":","}},{"id":691992486,"clock":23,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":1}]},"content":" "}},{"id":691992486,"clock":24,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":2}]},"content":"i"}},{"id":691992486,"clock":25,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":3}]},"content":"n"}},{"id":691992486,"clock":26,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":4}]},"content":"s"}},{"id":691992486,"clock":27,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":5}]},"content":"n"}},{"id":691992486,"clock":28,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":5}]},"end":5}]}},{"id":691992486,"clock":29,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":4}]},"end":4}]}},{"id":691992486,"clock":30,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":3}]},"end":3}]}},{"id":691992486,"clock":31,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":0}]},"content":"s"}},{"id":691992486,"clock":32,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":1}]},"content":"n"}},{"id":691992486,"clock":33,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":2}]},"content":"'"}},{"id":691992486,"clock":34,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":3}]},"content":"t"}},{"id":691992486,"clock":35,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":4}]},"content":" "}},{"id":691992486,"clock":36,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":5}]},"content":"i"}},{"id":691992486,"clock":37,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":6}]},"content":"t"}},{"id":691992486,"clock":38,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":0}]},"content":"?"}},{"id":691992486,"clock":39,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":1}]},"content":"\n"}},{"id":691992486,"clock":40,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":2}]},"content":"M"}},{"id":691992486,"clock":41,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":3}]},"content":"w"}},{"id":691992486,"clock":42,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":4}]},"content":"a"}},{"id":691992486,"clock":43,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":5}]},"content":"h"}},{"id":691992486,"clock":44,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":6}]},"content":"a"}},{"id":691992486,"clock":45,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":7}]},"content":"h"}},{"id":691992486,"clock":46,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":8}]},"content":"a"}},{"id":691992486,"clock":47,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":9}]},"content":"h"}},{"id":691992486,"clock":48,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":10}]},"content":"a"}},{"id":691992486,"clock":49,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":11}]},"content":"h"}},{"id":691992486,"clock":50,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":12}]},"content":"a"}}]} -------------------------------------------------------------------------------- /logs/logs-rXFbhTf8Ct/log-rXFbhTf8Ct-incorrect-operations-order.json: -------------------------------------------------------------------------------- 1 | {"vector":{},"richLogootSOps":[{"id":691992486,"clock":1,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1703428533,"replicaNumber":691992486,"clock":0,"offset":0}]},"end":0}]}},{"id":691992486,"clock":0,"logootSOp":{"id":{"tuples":[{"random":-1703428533,"replicaNumber":691992486,"clock":0,"offset":0}]},"content":"A"}}, {"id":691992486,"clock":2,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":0}]},"content":"B"}},{"id":691992486,"clock":3,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":1}]},"content":"r"}},{"id":691992486,"clock":4,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":2}]},"content":"a"}},{"id":691992486,"clock":5,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":3}]},"content":"n"}},{"id":691992486,"clock":6,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":4}]},"content":"d"}},{"id":691992486,"clock":7,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":5}]},"content":" "}},{"id":691992486,"clock":8,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":6}]},"content":"n"}},{"id":691992486,"clock":9,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":7}]},"content":"e"}},{"id":691992486,"clock":10,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":8}]},"content":"w"}},{"id":691992486,"clock":11,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9}]},"content":" "}},{"id":691992486,"clock":12,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":10}]},"content":"d"}},{"id":691992486,"clock":13,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":11}]},"content":"o"}},{"id":691992486,"clock":14,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":12}]},"content":"c"}},{"id":691992486,"clock":15,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":13}]},"content":"u"}},{"id":691992486,"clock":16,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":14}]},"content":"m"}},{"id":691992486,"clock":17,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":15}]},"content":"e"}},{"id":691992486,"clock":18,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":16}]},"content":"n"}},{"id":691992486,"clock":19,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":17}]},"content":"t"}},{"id":691992486,"clock":20,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":18}]},"content":" "}},{"id":691992486,"clock":21,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":19}]},"content":"!"}},{"id":1232192738,"clock":0,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":0}]},"content":"\n"}},{"id":1232192738,"clock":1,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":1}]},"content":"A"}},{"id":1232192738,"clock":2,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":2}]},"content":"n"}},{"id":1232192738,"clock":3,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":3}]},"content":"d"}},{"id":1232192738,"clock":4,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":4}]},"content":" "}},{"id":1232192738,"clock":5,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":5}]},"content":"i"}},{"id":1232192738,"clock":6,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":6}]},"content":"t"}},{"id":1232192738,"clock":7,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":7}]},"content":"'"}},{"id":1232192738,"clock":8,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":8}]},"content":"s"}},{"id":1232192738,"clock":9,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":9}]},"content":" "}},{"id":1232192738,"clock":10,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":10}]},"content":"w"}},{"id":1232192738,"clock":11,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":11}]},"content":"o"}},{"id":1232192738,"clock":12,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":12}]},"content":"r"}},{"id":1232192738,"clock":13,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":13}]},"content":"k"}},{"id":1232192738,"clock":14,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":14}]},"content":"i"}},{"id":1232192738,"clock":15,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":15}]},"content":"n"}},{"id":1232192738,"clock":16,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16}]},"content":"g"}},{"id":1232192738,"clock":17,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":17}]},"content":" "}},{"id":1232192738,"clock":18,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":18}]},"content":"="}},{"id":1232192738,"clock":19,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":18}]},"end":18}]}},{"id":1232192738,"clock":20,"logootSOp":{"id":{"tuples":[{"random":1384979433,"replicaNumber":1232192738,"clock":1,"offset":0}]},"content":"!"}},{"id":1232192738,"clock":21,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":10}]},"end":17}]}},{"id":1232192738,"clock":22,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":2063810427,"replicaNumber":1232192738,"clock":2,"offset":0}]},"content":"c"}},{"id":1232192738,"clock":23,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":2063810427,"replicaNumber":1232192738,"clock":2,"offset":0}]},"end":0}]}},{"id":1232192738,"clock":24,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":0}]},"content":"v"}},{"id":1232192738,"clock":25,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":1}]},"content":"e"}},{"id":1232192738,"clock":26,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":2}]},"content":"r"}},{"id":1232192738,"clock":27,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":3}]},"content":"s"}},{"id":1232192738,"clock":28,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":4}]},"content":"i"}},{"id":1232192738,"clock":29,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":5}]},"content":"o"}},{"id":1232192738,"clock":30,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":6}]},"content":"n"}},{"id":691992486,"clock":22,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":0}]},"content":","}},{"id":691992486,"clock":23,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":1}]},"content":" "}},{"id":691992486,"clock":24,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":2}]},"content":"i"}},{"id":691992486,"clock":25,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":3}]},"content":"n"}},{"id":691992486,"clock":26,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":4}]},"content":"s"}},{"id":691992486,"clock":27,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":5}]},"content":"n"}},{"id":691992486,"clock":28,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":5}]},"end":5}]}},{"id":691992486,"clock":29,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":4}]},"end":4}]}},{"id":691992486,"clock":30,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":3}]},"end":3}]}},{"id":691992486,"clock":31,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":0}]},"content":"s"}},{"id":691992486,"clock":32,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":1}]},"content":"n"}},{"id":691992486,"clock":33,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":2}]},"content":"'"}},{"id":691992486,"clock":34,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":3}]},"content":"t"}},{"id":691992486,"clock":35,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":4}]},"content":" "}},{"id":691992486,"clock":36,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":5}]},"content":"i"}},{"id":691992486,"clock":37,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":6}]},"content":"t"}},{"id":691992486,"clock":38,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":0}]},"content":"?"}}]} 2 | -------------------------------------------------------------------------------- /logs/logs-rXFbhTf8Ct/log-rXFbhTf8Ct-original-log.json: -------------------------------------------------------------------------------- 1 | {"vector":{},"richLogootSOps":[{"id":691992486,"clock":0,"logootSOp":{"id":{"tuples":[{"random":-1703428533,"replicaNumber":691992486,"clock":0,"offset":0}]},"content":"A"}},{"id":691992486,"clock":1,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1703428533,"replicaNumber":691992486,"clock":0,"offset":0}]},"end":0}]}},{"id":691992486,"clock":2,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":0}]},"content":"B"}},{"id":691992486,"clock":3,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":1}]},"content":"r"}},{"id":691992486,"clock":4,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":2}]},"content":"a"}},{"id":691992486,"clock":5,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":3}]},"content":"n"}},{"id":691992486,"clock":6,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":4}]},"content":"d"}},{"id":691992486,"clock":7,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":5}]},"content":" "}},{"id":691992486,"clock":8,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":6}]},"content":"n"}},{"id":691992486,"clock":9,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":7}]},"content":"e"}},{"id":691992486,"clock":10,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":8}]},"content":"w"}},{"id":691992486,"clock":11,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9}]},"content":" "}},{"id":691992486,"clock":12,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":10}]},"content":"d"}},{"id":691992486,"clock":13,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":11}]},"content":"o"}},{"id":691992486,"clock":14,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":12}]},"content":"c"}},{"id":691992486,"clock":15,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":13}]},"content":"u"}},{"id":691992486,"clock":16,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":14}]},"content":"m"}},{"id":691992486,"clock":17,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":15}]},"content":"e"}},{"id":691992486,"clock":18,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":16}]},"content":"n"}},{"id":691992486,"clock":19,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":17}]},"content":"t"}},{"id":691992486,"clock":20,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":18}]},"content":" "}},{"id":691992486,"clock":21,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":19}]},"content":"!"}},{"id":1232192738,"clock":0,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":0}]},"content":"\n"}},{"id":1232192738,"clock":1,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":1}]},"content":"A"}},{"id":1232192738,"clock":2,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":2}]},"content":"n"}},{"id":1232192738,"clock":3,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":3}]},"content":"d"}},{"id":1232192738,"clock":4,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":4}]},"content":" "}},{"id":1232192738,"clock":5,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":5}]},"content":"i"}},{"id":1232192738,"clock":6,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":6}]},"content":"t"}},{"id":1232192738,"clock":7,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":7}]},"content":"'"}},{"id":1232192738,"clock":8,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":8}]},"content":"s"}},{"id":1232192738,"clock":9,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":9}]},"content":" "}},{"id":1232192738,"clock":10,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":10}]},"content":"w"}},{"id":1232192738,"clock":11,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":11}]},"content":"o"}},{"id":1232192738,"clock":12,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":12}]},"content":"r"}},{"id":1232192738,"clock":13,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":13}]},"content":"k"}},{"id":1232192738,"clock":14,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":14}]},"content":"i"}},{"id":1232192738,"clock":15,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":15}]},"content":"n"}},{"id":1232192738,"clock":16,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16}]},"content":"g"}},{"id":1232192738,"clock":17,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":17}]},"content":" "}},{"id":1232192738,"clock":18,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":18}]},"content":"="}},{"id":1232192738,"clock":19,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":18}]},"end":18}]}},{"id":1232192738,"clock":20,"logootSOp":{"id":{"tuples":[{"random":1384979433,"replicaNumber":1232192738,"clock":1,"offset":0}]},"content":"!"}},{"id":1232192738,"clock":21,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":10}]},"end":17}]}},{"id":1232192738,"clock":22,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":2063810427,"replicaNumber":1232192738,"clock":2,"offset":0}]},"content":"c"}},{"id":1232192738,"clock":23,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":2063810427,"replicaNumber":1232192738,"clock":2,"offset":0}]},"end":0}]}},{"id":1232192738,"clock":24,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":0}]},"content":"v"}},{"id":1232192738,"clock":25,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":1}]},"content":"e"}},{"id":1232192738,"clock":26,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":2}]},"content":"r"}},{"id":1232192738,"clock":27,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":3}]},"content":"s"}},{"id":1232192738,"clock":28,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":4}]},"content":"i"}},{"id":1232192738,"clock":29,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":5}]},"content":"o"}},{"id":1232192738,"clock":30,"logootSOp":{"id":{"tuples":[{"random":-1335142058,"replicaNumber":691992486,"clock":1,"offset":9},{"random":751425177,"replicaNumber":1232192738,"clock":3,"offset":6}]},"content":"n"}},{"id":691992486,"clock":22,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":0}]},"content":","}},{"id":691992486,"clock":23,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":1}]},"content":" "}},{"id":691992486,"clock":24,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":2}]},"content":"i"}},{"id":691992486,"clock":25,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":3}]},"content":"n"}},{"id":691992486,"clock":26,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":4}]},"content":"s"}},{"id":691992486,"clock":27,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":5}]},"content":"n"}},{"id":691992486,"clock":28,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":5}]},"end":5}]}},{"id":691992486,"clock":29,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":4}]},"end":4}]}},{"id":691992486,"clock":30,"logootSOp":{"lid":[{"idBegin":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":-923420830,"replicaNumber":691992486,"clock":2,"offset":3}]},"end":3}]}},{"id":691992486,"clock":31,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":0}]},"content":"s"}},{"id":691992486,"clock":32,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":1}]},"content":"n"}},{"id":691992486,"clock":33,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":2}]},"content":"'"}},{"id":691992486,"clock":34,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":3}]},"content":"t"}},{"id":691992486,"clock":35,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":4}]},"content":" "}},{"id":691992486,"clock":36,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":5}]},"content":"i"}},{"id":691992486,"clock":37,"logootSOp":{"id":{"tuples":[{"random":1010207176,"replicaNumber":1232192738,"clock":0,"offset":16},{"random":398010652,"replicaNumber":691992486,"clock":3,"offset":6}]},"content":"t"}},{"id":691992486,"clock":38,"logootSOp":{"id":{"tuples":[{"random":1836512351,"replicaNumber":691992486,"clock":4,"offset":0}]},"content":"?"}}]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mute-structs", 3 | "version": "2.0.5", 4 | "description": "NodeJS module providing an implementation of the LogootSplit CRDT algorithm", 5 | "main": "dist/main/index.js", 6 | "module": "dist/module/index.js", 7 | "es2015": "dist/es2015/index.js", 8 | "types": "dist/types/index.d.ts", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/coast-team/mute-structs" 16 | }, 17 | "devDependencies": { 18 | "@commitlint/cli": "^8.2.0", 19 | "@commitlint/config-conventional": "^8.2.0", 20 | "@types/node": "^11.15.2", 21 | "ava": "^2.4.0", 22 | "husky": "^3.1.0", 23 | "standard-version": "^7.0.1", 24 | "tslint": "^5.11", 25 | "tslint-eslint-rules": "^5.4", 26 | "typescript": "^3.6.4" 27 | }, 28 | "scripts": { 29 | "prebuild": "npm run lint && npm run clean", 30 | "build": "tsc -b tsconfig.main.json tsconfig.module.json tsconfig.es2015.json tsconfig.types.json", 31 | "build:test": "tsc", 32 | "clean": "rm -rf dist .tested", 33 | "check": "tsc --noEmit", 34 | "lint": "tslint --project tsconfig.json", 35 | "release": "standard-version", 36 | "pretest": "npm run lint && npm run build:test", 37 | "test": "ava" 38 | }, 39 | "ava": { 40 | "files": [ 41 | ".tested/test/**/*.test.js" 42 | ], 43 | "source": [ 44 | "test" 45 | ] 46 | }, 47 | "keywords": [ 48 | "crdt", 49 | "ropes", 50 | "data-structures" 51 | ], 52 | "author": "Matthieu Nicolas ", 53 | "contributors": [ 54 | "Victorien Elvinger ", 55 | "Gerald Oster " 56 | ], 57 | "license": "AGPL-3.0" 58 | } 59 | -------------------------------------------------------------------------------- /src/data-validation.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Victorien Elvinger 2 | // 3 | // Licensed under the zlib license (https://opensource.org/licenses/zlib). 4 | // 5 | // This file is part of replayable-random 6 | // (https://github.com/Conaclos/replayable-random) 7 | 8 | /* tslint:disable */ 9 | 10 | export type NonFunctionNames = 11 | { [k in keyof T]: T[k] extends Function ? never : k }[keyof T] 12 | 13 | export type Unknown = { [k in NonFunctionNames]?: unknown } 14 | 15 | /** 16 | * Example: 17 | * Given `x: unknown` 18 | * `isObject<{ p: number }>(x) && typeof x.p === "number"` 19 | * enables to test if x is conforms to `{ p: number }`. 20 | * 21 | * @param x 22 | * @param Is `x' a non-null object? 23 | */ 24 | export const isObject = (x: unknown): x is Unknown => 25 | typeof x === "object" && x !== null 26 | 27 | export function isArrayFromMap (o: unknown): o is [unknown, unknown] { 28 | return Array.isArray(o) && o.length === 2 29 | } 30 | -------------------------------------------------------------------------------- /src/dot.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "./data-validation" 21 | import {isInt32} from "./int32" 22 | 23 | export interface Dot { 24 | readonly replicaNumber: number 25 | readonly clock: number 26 | } 27 | 28 | export function isDot (dot: unknown): dot is Dot { 29 | return isObject(dot) && 30 | isInt32(dot.replicaNumber) && isInt32(dot.clock) 31 | } 32 | -------------------------------------------------------------------------------- /src/epoch/epoch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import { isObject } from "../data-validation" 21 | import { EpochId } from "./epochid" 22 | 23 | export class Epoch { 24 | 25 | static fromPlain (o: unknown): Epoch | null { 26 | if (isObject(o)) { 27 | const id = EpochId.fromPlain(o.id) 28 | const parentId = EpochId.fromPlain(o.parentId) 29 | 30 | if (id !== null && id.epochNumber === 0 && parentId === null) { 31 | return new Epoch(id) 32 | } else if (id !== null && id.epochNumber !== 0 && parentId !== null) { 33 | return new Epoch(id, parentId) 34 | } 35 | } 36 | return null 37 | } 38 | 39 | readonly id: EpochId 40 | readonly parentId?: EpochId 41 | 42 | constructor (id: EpochId, parentId?: EpochId) { 43 | console.assert((id.epochNumber !== 0 && parentId !== undefined) 44 | || (id.epochNumber === 0 && parentId === undefined), 45 | "Every epoch, except the 0 one, should have a parentId") 46 | 47 | this.id = id 48 | this.parentId = parentId 49 | } 50 | 51 | /** 52 | * Check if two instances of epochs are equal 53 | * 54 | * @param {Epoch} other The other epoch to which to compare 55 | * @return {boolean} Are the two epochs equal 56 | */ 57 | equals (other: Epoch): boolean { 58 | const areParentsEqual = 59 | (this.parentId === undefined && other.parentId === undefined) || 60 | (this.parentId !== undefined && other.parentId !== undefined && 61 | this.parentId.replicaNumber === other.parentId.replicaNumber && 62 | this.parentId.epochNumber === other.parentId.epochNumber) 63 | 64 | return areParentsEqual && 65 | this.id.replicaNumber === other.id.replicaNumber && 66 | this.id.epochNumber === other.id.epochNumber 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/epoch/epochid.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import { isObject } from "../data-validation" 21 | import { isInt32 } from "../int32" 22 | 23 | export class EpochId { 24 | 25 | static fromPlain (o: unknown): EpochId | null { 26 | if (isObject(o) && isInt32(o.replicaNumber) && 27 | isInt32(o.epochNumber)) { 28 | 29 | return new EpochId(o.replicaNumber, o.epochNumber) 30 | } 31 | return null 32 | } 33 | 34 | readonly replicaNumber: number 35 | readonly epochNumber: number 36 | 37 | constructor (replicaNumber: number, epochNumber: number) { 38 | this.replicaNumber = replicaNumber 39 | this.epochNumber = epochNumber 40 | } 41 | 42 | get asStr (): string { 43 | return `${this.replicaNumber},${this.epochNumber}` 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/epoch/epochstore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import { isArrayFromMap, isObject } from "../data-validation" 21 | import { Ordering } from "../ordering" 22 | import { Epoch } from "./epoch" 23 | import { EpochId } from "./epochid" 24 | 25 | export interface EpochStoreJSON { 26 | readonly epochs: Array<[string, Epoch]> 27 | } 28 | 29 | export function compareEpochFullIds (id1: number[], id2: number[]): Ordering { 30 | const minLength = id1.length < id2.length ? id1.length : id2.length 31 | for (let i = 0; i < minLength; i++) { 32 | const value1 = id1[i] 33 | const value2 = id2[i] 34 | 35 | if (value1 < value2) { 36 | return Ordering.Less 37 | } else if (value1 > value2) { 38 | return Ordering.Greater 39 | } 40 | } 41 | if (id1.length < id2.length) { 42 | return Ordering.Less 43 | } else if (id1.length > id2.length) { 44 | return Ordering.Greater 45 | } else { 46 | return Ordering.Equal 47 | } 48 | } 49 | 50 | export class EpochStore { 51 | 52 | static fromPlain (o: unknown): EpochStore | null { 53 | if (isObject(o) && Array.isArray(o.epochs) && o.epochs.length > 0) { 54 | 55 | const epochs = o.epochs 56 | .filter(isArrayFromMap) 57 | .map(([_, v]) => { 58 | return Epoch.fromPlain(v) 59 | }) 60 | .filter((epoch): epoch is Epoch => epoch !== null) 61 | 62 | if (o.epochs.length === epochs.length) { 63 | const [origin, ...rest] = epochs 64 | const epochStore = new EpochStore(origin) 65 | rest.forEach((epoch) => { 66 | epochStore.addEpoch(epoch) 67 | }) 68 | return epochStore 69 | } 70 | } 71 | return null 72 | } 73 | 74 | private epochs: Map 75 | 76 | constructor (origin: Epoch) { 77 | this.epochs = new Map() 78 | this.addEpoch(origin) 79 | } 80 | 81 | addEpoch (epoch: Epoch) { 82 | this.epochs.set(epoch.id.asStr, epoch) 83 | } 84 | 85 | hasEpoch (epoch: Epoch): boolean { 86 | return this.epochs.has(epoch.id.asStr) 87 | } 88 | 89 | getEpoch (epochId: EpochId): Epoch | undefined { 90 | return this.epochs.get(epochId.asStr) 91 | } 92 | 93 | getEpochFullId (epoch: Epoch): number[] { 94 | console.assert(this.hasEpoch(epoch), 95 | "The epoch should have been added to the store previously") 96 | 97 | let parentEpochFullId: number[] = [] 98 | if (epoch.parentId !== undefined) { 99 | const parentEpoch = this.getEpoch(epoch.parentId) 100 | if (parentEpoch !== undefined) { 101 | parentEpochFullId = this.getEpochFullId(parentEpoch) 102 | } 103 | } 104 | return parentEpochFullId.concat(epoch.id.replicaNumber, epoch.id.epochNumber) 105 | } 106 | 107 | getEpochPath (epoch: Epoch): Epoch[] { 108 | const pathEpoch: Epoch[] = [] 109 | let currentEpoch: Epoch | undefined = epoch 110 | while (currentEpoch !== undefined) { 111 | pathEpoch.push(currentEpoch) 112 | currentEpoch = currentEpoch.parentId !== undefined ? this.getEpoch(currentEpoch.parentId) : undefined 113 | } 114 | return pathEpoch.reverse() 115 | } 116 | 117 | getPathBetweenEpochs (from: Epoch, to: Epoch): [Epoch[], Epoch[]] { 118 | const fromPath = this.getEpochPath(from) 119 | const toPath = this.getEpochPath(to) 120 | 121 | let i = 0 122 | while (i < fromPath.length && i < toPath.length && fromPath[i].equals(toPath[i])) { 123 | i++ 124 | } 125 | return [fromPath.slice(i).reverse(), toPath.slice(i)] 126 | } 127 | 128 | toJSON (): EpochStoreJSON { 129 | return { epochs: Array.from(this.epochs) } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import {Ordering} from "./ordering" 2 | 3 | export function findPredecessor (list: T[], element: T, compareFn: (a: T, b: T) => Ordering): T | undefined { 4 | let l = 0 5 | let r = list.length 6 | while (l < r) { 7 | const m = Math.floor((l + r) / 2) 8 | const other = list[m] 9 | if (compareFn(other, element) === Ordering.Less) { 10 | l = m + 1 11 | } else { 12 | r = m 13 | } 14 | } 15 | return list[l - 1] 16 | } 17 | 18 | /** 19 | * Check if an array is sorted 20 | * 21 | * @param {T[]} array The array to browse 22 | * @param {(a: T, b: T) => Ordering} compareFn The comparison function used to determine the order between two elements 23 | * @return {boolean} Is the array sorted 24 | */ 25 | export function isSorted (array: T[], compareFn: (a: T, b: T) => Ordering): boolean { 26 | return array.every((value: T, index: number) => { 27 | if (index === 0) { 28 | return true 29 | } 30 | const other = array[index - 1] 31 | return compareFn(other, value) === Ordering.Less 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/identifierinterval.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "./data-validation" 21 | import {Dot} from "./dot" 22 | import {isSorted} from "./helpers" 23 | import {Identifier} from "./identifier" 24 | import {isInt32} from "./int32" 25 | import {Ordering} from "./ordering" 26 | 27 | /** 28 | * Define an interval between two identifiers sharing the same base 29 | */ 30 | export class IdentifierInterval { 31 | 32 | static fromPlain (o: unknown): IdentifierInterval | null { 33 | if (isObject(o) && isInt32(o.end)) { 34 | const idBegin = Identifier.fromPlain(o.idBegin) 35 | if (idBegin !== null && idBegin.lastOffset <= o.end) { 36 | return new IdentifierInterval(idBegin, o.end) 37 | } 38 | } 39 | return null 40 | } 41 | 42 | /** 43 | * Merge as much as possible Identifiers contained into an array into IdentifierIntervals 44 | * 45 | * @param {Identifier[]} ids The array of Identifiers 46 | * @return {IdentifierInterval[]} The corresponding array of IdentifierIntervals 47 | */ 48 | static mergeIdsIntoIntervals (ids: Identifier[]): IdentifierInterval[] { 49 | const compareIdsFn = (id: Identifier, other: Identifier): Ordering => id.compareTo(other) 50 | console.assert(isSorted(ids, compareIdsFn), "The array should be sorted") 51 | 52 | const res = [] 53 | if (ids.length > 0) { 54 | let idBegin: Identifier = ids[0] 55 | for (let i = 1; i < ids.length; i++) { 56 | const prevId = ids[i - 1] 57 | const id = ids[i] 58 | if (!prevId.equalsBase(id) || prevId.lastOffset + 1 !== id.lastOffset) { 59 | const idInterval = new IdentifierInterval(idBegin, prevId.lastOffset) 60 | res.push(idInterval) 61 | idBegin = id 62 | } 63 | } 64 | const lastId = ids[ids.length - 1] 65 | const lastIdInterval = new IdentifierInterval(idBegin, lastId.lastOffset) 66 | res.push(lastIdInterval) 67 | } 68 | return res 69 | } 70 | 71 | // Access 72 | readonly idBegin: Identifier 73 | readonly end: number 74 | 75 | // Creation 76 | constructor (idBegin: Identifier, end: number) { 77 | console.assert(isInt32(end), "end ∈ int32") 78 | console.assert(idBegin.lastOffset <= end, "idBegin must be less than or equal to idEnd") 79 | 80 | this.idBegin = idBegin 81 | this.end = end 82 | } 83 | 84 | /** 85 | * Shortcut to retrieve the offset of the last tuple of idBegin 86 | * This offset also corresponds to the beginning of the interval 87 | * 88 | * @return {number} The offset 89 | */ 90 | get begin (): number { 91 | return this.idBegin.lastOffset 92 | } 93 | 94 | /** 95 | * Shortcut to retrieve the last identifier of the interval 96 | * 97 | * @return {Identifier} The last identifier of the interval 98 | */ 99 | get idEnd (): Identifier { 100 | return this.getBaseId(this.end) 101 | } 102 | 103 | /** 104 | * Shortcut to compute the length of the interval 105 | * 106 | * @return {number} The length 107 | */ 108 | get length (): number { 109 | return this.end - this.begin + 1 110 | } 111 | 112 | get base (): number[] { 113 | return this.idBegin.base 114 | } 115 | 116 | get dot (): Dot { 117 | return this.idBegin.dot 118 | } 119 | 120 | equals (aOther: IdentifierInterval): boolean { 121 | return this.idBegin.equals(aOther.idBegin) && 122 | this.begin === aOther.begin && this.end === aOther.end 123 | } 124 | 125 | /** 126 | * Compute the union between this interval and [aBegin, aEnd] 127 | * 128 | * @param {number} aBegin 129 | * @param {number} aEnd 130 | * @return {IdentifierInterval} this U [aBegin, aEnd] 131 | */ 132 | union (aBegin: number, aEnd: number): IdentifierInterval { 133 | console.assert(isInt32(aBegin), "aBegin ∈ int32") 134 | console.assert(isInt32(aEnd), "aEnd ∈ int32") 135 | 136 | const minBegin = Math.min(this.begin, aBegin) 137 | const maxEnd = Math.max(this.end, aEnd) 138 | 139 | const newIdBegin: Identifier = Identifier.fromBase(this.idBegin, minBegin) 140 | 141 | return new IdentifierInterval(newIdBegin, maxEnd) 142 | } 143 | 144 | /** 145 | * Check if the provided identifier belongs to this interval 146 | * 147 | * @param {Identifier} id 148 | * @return {boolean} Does the identifier belongs to this interval 149 | */ 150 | containsId (id: Identifier): boolean { 151 | return this.idBegin.compareTo(id) === Ordering.Less && 152 | this.idEnd.compareTo(id) === Ordering.Greater 153 | } 154 | 155 | /** 156 | * Retrieve a identifier from the interval from its offset 157 | * 158 | * @param {number} offset The offset of the identifier 159 | * @return {Identifier} The identifier 160 | */ 161 | getBaseId (offset: number): Identifier { 162 | console.assert(isInt32(offset), "offset ∈ int32") 163 | console.assert(this.begin <= offset && offset <= this.end, 164 | "offset must be included in the interval") 165 | 166 | return Identifier.fromBase(this.idBegin, offset) 167 | } 168 | 169 | digest (): number { 170 | // '| 0' converts to 32bits integer 171 | return (this.idBegin.digest() * 17 + this.end) | 0 172 | } 173 | 174 | toIds (): Identifier[] { 175 | const res = [] 176 | for (let i = this.begin; i <= this.end; i++) { 177 | res.push(Identifier.fromBase(this.idBegin, i)) 178 | } 179 | return res 180 | } 181 | 182 | toString (): string { 183 | return "IdInterval[" + this.idBegin.tuples.join(",") + " .. " + this.end + "]" 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/identifiertuple.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "./data-validation" 21 | import {isInt32} from "./int32" 22 | import {Ordering} from "./ordering" 23 | 24 | export class IdentifierTuple { 25 | 26 | static fromPlain (o: unknown): IdentifierTuple | null { 27 | if (isObject(o) && 28 | isInt32(o.random) && isInt32(o.replicaNumber) && 29 | isInt32(o.clock) && isInt32(o.offset)) { 30 | 31 | return new IdentifierTuple(o.random, o.replicaNumber, o.clock, o.offset) 32 | } 33 | return null 34 | } 35 | 36 | /** 37 | * Generate a new IdentifierTuple with the same base as the provided one but with a different offset 38 | * 39 | * @param {tuple} IdentifierTuple The tuple to partly copy 40 | * @param {number} offset The offset of the new IdentifierTuple 41 | * @return {IdentifierTuple} The generated IdentifierTuple 42 | */ 43 | static fromBase (tuple: IdentifierTuple, offset: number): IdentifierTuple { 44 | console.assert(isInt32(offset), "offset ∈ int32") 45 | 46 | return new IdentifierTuple(tuple.random, tuple.replicaNumber, tuple.clock, offset) 47 | } 48 | 49 | readonly random: number 50 | readonly replicaNumber: number 51 | readonly clock: number 52 | readonly offset: number 53 | 54 | constructor (random: number, replicaNumber: number, clock: number, offset: number) { 55 | console.assert([random, replicaNumber, clock, offset].every(isInt32), 56 | "each value ∈ int32") 57 | 58 | this.random = random 59 | this.replicaNumber = replicaNumber 60 | this.clock = clock 61 | this.offset = offset 62 | } 63 | 64 | /** 65 | * Compare this tuple to another one to order them 66 | * Ordering.Less means that this is less than other 67 | * Ordering.Greater means that this is greater than other 68 | * Ordering.Equal means that this is equals to other 69 | * 70 | * @param {IdentifierTuple} other The tuple to compare 71 | * @return {Ordering} The order of the two tuples 72 | */ 73 | compareTo (other: IdentifierTuple): Ordering { 74 | const array: number[] = this.asArray() 75 | const otherArray: number[] = other.asArray() 76 | let i = 0 77 | 78 | while (i < array.length && array[i] === otherArray[i]) { 79 | i++ 80 | } 81 | 82 | if (i === array.length) { 83 | return Ordering.Equal 84 | } else if (array[i] < otherArray[i]) { 85 | return Ordering.Less 86 | } else { 87 | return Ordering.Greater 88 | } 89 | } 90 | 91 | equals (other: IdentifierTuple): boolean { 92 | return this.equalsBase(other) 93 | && this.offset === other.offset 94 | } 95 | 96 | /** 97 | * Check if this tuple and another one share the same base 98 | * The base is composed of a random number, a replicaNumber and a clock 99 | * 100 | * @param {IdentifierTuple} other The tuple to compare 101 | * @return {boolean} Are the two tuple sharing the same base 102 | */ 103 | equalsBase (other: IdentifierTuple): boolean { 104 | return this.random === other.random 105 | && this.replicaNumber === other.replicaNumber 106 | && this.clock === other.clock 107 | } 108 | 109 | /** 110 | * Map the tuple to an array, making it easier to browse 111 | * 112 | * @return {number[]} The tuple as an array 113 | */ 114 | asArray (): number[] { 115 | return [this.random, this.replicaNumber, this.clock, this.offset] 116 | } 117 | 118 | digest (): number { 119 | return this.asArray().reduce((prev, v) => (prev * 17 + v) | 0, 0) 120 | } 121 | 122 | toString (): string { 123 | return `${this.random},${this.replicaNumber},${this.clock},${this.offset}` 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/idfactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {Identifier} from "./identifier" 21 | import {IdentifierTuple} from "./identifiertuple" 22 | import { 23 | INT32_BOTTOM, 24 | INT32_TOP, 25 | isInt32, 26 | randomInt32, 27 | } from "./int32" 28 | import {Ordering} from "./ordering" 29 | 30 | export const INT32_BOTTOM_USER = INT32_BOTTOM + 1 31 | export const INT32_TOP_USER = INT32_TOP - 1 32 | 33 | export const MIN_TUPLE: IdentifierTuple = new IdentifierTuple(INT32_BOTTOM, 0, 0, 0) 34 | export const MIN_TUPLE_USER: IdentifierTuple = new IdentifierTuple(INT32_BOTTOM_USER, 0, 0, 0) 35 | export const MAX_TUPLE_USER: IdentifierTuple = new IdentifierTuple(INT32_TOP_USER, 0, 0, 0) 36 | export const MAX_TUPLE: IdentifierTuple = new IdentifierTuple(INT32_TOP, 0, 0, 0) 37 | 38 | export function createBetweenPosition ( 39 | id1: Identifier | null, id2: Identifier | null, 40 | replicaNumber: number, clock: number): Identifier { 41 | 42 | console.assert(id1 === null || id2 === null || 43 | id1.compareTo(id2) === Ordering.Less, "id1 < id2") 44 | console.assert(isInt32(replicaNumber), "replicaNumber is an int32") 45 | console.assert(isInt32(clock), "clock is an int32") 46 | 47 | const seq1 = infiniteSequence(tuplesOf(id1), MIN_TUPLE_USER) 48 | const seq2 = infiniteSequence(tuplesOf(id2), MAX_TUPLE_USER) 49 | const tuples: IdentifierTuple[] = [] 50 | 51 | let tuple1 = seq1.next().value 52 | let tuple2 = seq2.next().value 53 | while (tuple1.compareTo(tuple2) === Ordering.Equal) { 54 | // Cannot insert a new tuple between tuple1 and tuple2 55 | tuples.push(tuple1) 56 | tuple1 = seq1.next().value 57 | tuple2 = seq2.next().value 58 | } 59 | 60 | if (tuple1.random === INT32_BOTTOM && tuple2.random === INT32_BOTTOM_USER) { 61 | // Special case to avoid problematic scenarios with renaming mechanism 62 | tuples.push(tuple2) 63 | while (tuple1.compareTo(MIN_TUPLE_USER) !== Ordering.Equal) { 64 | tuple1 = seq1.next().value 65 | } 66 | tuple2 = seq2.next().value 67 | } else if (tuple2.random - tuple1.random <= 1) { 68 | tuples.push(tuple1) 69 | tuple1 = seq1.next().value 70 | while (tuple2.compareTo(MAX_TUPLE_USER) !== Ordering.Equal) { 71 | tuple2 = seq2.next().value 72 | } 73 | } 74 | 75 | if (tuple1.random + 1 === INT32_TOP_USER) { 76 | tuples.push(tuple1) 77 | tuple1 = seq1.next().value 78 | } 79 | 80 | const random = randomInt32(tuple1.random + 1, tuple2.random) 81 | // random ∈ ]tuple1.random, tuple2.random[ 82 | // tuple1.random exclusion ensures a dense set 83 | // tuple2.random exclusion ensures that newTuple < tuple2 84 | // and thus that newId < id2 85 | tuples.push(new IdentifierTuple(random, replicaNumber, clock, 0)) 86 | 87 | return new Identifier(tuples) 88 | } 89 | 90 | export function createAtPosition ( 91 | replicaNumber: number, clock: number, 92 | position: number, offset: number): Identifier { 93 | 94 | console.assert([position, replicaNumber, clock, offset].every(isInt32), 95 | "each value ∈ int32") 96 | 97 | const tuple = new IdentifierTuple(position, replicaNumber, clock, offset) 98 | return new Identifier([tuple]) 99 | } 100 | 101 | /** 102 | * Generate an infinite sequence of tuples 103 | * 104 | * @param values 105 | * @param defaultValue 106 | */ 107 | function *infiniteSequence 108 | (values: T[], defaultValue: T): IterableIterator { 109 | 110 | for (const v of values) { 111 | yield v 112 | } 113 | while (true) { 114 | yield defaultValue 115 | } 116 | } 117 | 118 | /** 119 | * @param id 120 | * @return Tuples of `a' or an empty array if none. 121 | */ 122 | function tuplesOf (id: Identifier | null): IdentifierTuple[] { 123 | return (id !== null) ? id.tuples : [] 124 | } 125 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | export { Dot, isDot } from "./dot" 20 | export { IdentifierTuple } from "./identifiertuple" 21 | export { Identifier } from "./identifier" 22 | export { IdentifierInterval } from "./identifierinterval" 23 | 24 | export { LogootSBlock } from "./logootsblock" 25 | export { LogootSRopes } from "./logootsropes" 26 | export { RopesNodes } from "./ropesnodes" 27 | 28 | export { LogootSDel } from "./operations/delete/logootsdel" 29 | export { TextDelete } from "./operations/delete/textdelete" 30 | export { LogootSAdd } from "./operations/insert/logootsadd" 31 | export { TextInsert } from "./operations/insert/textinsert" 32 | export { LogootSOperation } from "./operations/logootsoperation" 33 | export { TextOperation } from "./operations/textoperation" 34 | 35 | export { Epoch } from "./epoch/epoch" 36 | export { EpochId } from "./epoch/epochid" 37 | export { RenamableReplicableList } from "./renamablereplicablelist" 38 | export { RenamingMap } from "./renamingmap/renamingmap" 39 | 40 | export { RenamableLogootSDel } from "./operations/delete/renamablelogootsdel" 41 | export { RenamableLogootSAdd } from "./operations/insert/renamablelogootsadd" 42 | export { LogootSRename } from "./operations/rename/logootsrename" 43 | export { RenamableListOperation } from "./operations/renamablelistoperation" 44 | export { RenamableLogootSOperation } from "./operations/renamablelogootsoperation" 45 | 46 | export { BasicStats, Stats } from "./stats" 47 | 48 | export { insert, del, occurrences } from "./textutils" 49 | -------------------------------------------------------------------------------- /src/int32.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of Mute-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | export const INT32_BOTTOM = - 0x7fffffff - 1 21 | export const INT32_TOP = 0x7fffffff 22 | 23 | /** 24 | * @param n 25 | * @return Is `n' an int32? 26 | */ 27 | export function isInt32 (n: unknown): n is number { 28 | return typeof n === "number" && 29 | Number.isSafeInteger(n) && INT32_BOTTOM <= n && n <= INT32_TOP 30 | } 31 | 32 | /** 33 | * @param l lower bound 34 | * @param u upper bound 35 | * @return random integer 32 in [l, u[ 36 | */ 37 | export function randomInt32 (l: number, u: number): number { 38 | console.assert(isInt32(l), "l must be an int32") 39 | console.assert(isInt32(u), "u must be an int32") 40 | console.assert(l < u, "u is greater than l") 41 | 42 | const randomFloat = (Math.random() * (u - l)) + l 43 | // Generate a random float number in [b1, b2[ 44 | const result = Math.floor(randomFloat) 45 | 46 | console.assert(isInt32(result) && l <= result && result < u, 47 | "result is an integer 32 in [l, u[") 48 | return result 49 | } 50 | -------------------------------------------------------------------------------- /src/iteratorhelperidentifier.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {Identifier} from "./identifier" 21 | import {IdentifierInterval} from "./identifierinterval" 22 | import {Ordering} from "./ordering" 23 | 24 | export const enum IdentifierIteratorResults { 25 | B1_AFTER_B2, 26 | B1_BEFORE_B2, 27 | B1_INSIDE_B2, 28 | B2_INSIDE_B1, 29 | B1_CONCAT_B2, 30 | B2_CONCAT_B1, 31 | B1_EQUALS_B2, 32 | } 33 | 34 | export function compareBase ( 35 | idInterval1: IdentifierInterval, 36 | idInterval2: IdentifierInterval): IdentifierIteratorResults { 37 | 38 | const id1: Identifier = idInterval1.idBegin 39 | const begin1 = idInterval1.begin 40 | const end1 = idInterval1.end 41 | 42 | const id2: Identifier = idInterval2.idBegin 43 | const begin2 = idInterval2.begin 44 | const end2 = idInterval2.end 45 | 46 | if (id1.equalsBase(id2)) { 47 | if (begin1 === begin2 && end1 === end2) { 48 | return IdentifierIteratorResults.B1_EQUALS_B2 49 | } else if ((end1 + 1) === begin2) { 50 | return IdentifierIteratorResults.B1_CONCAT_B2 51 | } else if (begin1 === (end2 + 1)) { 52 | return IdentifierIteratorResults.B2_CONCAT_B1 53 | } else if (end1 < begin2) { 54 | return IdentifierIteratorResults.B1_BEFORE_B2 55 | } else if (end2 < begin1 ) { 56 | return IdentifierIteratorResults.B1_AFTER_B2 57 | } else { 58 | /* 59 | (B2 ⊂ B1) || (B1 ⊂ B2) || (B1 ∩ B2 !== {}) 60 | It happens only in the following cases: 61 | - An already applied operation is delivered again, 62 | but the interval has since then been updated 63 | (append, prepend, deletion at the bounds) 64 | - It is a malicious operation which try to insert 65 | again some identifiers 66 | For now, do not do anything in both cases. 67 | */ 68 | console.warn("Trying to duplicate existing identifiers: ", 69 | idInterval1, idInterval2) 70 | return IdentifierIteratorResults.B1_EQUALS_B2 71 | } 72 | } 73 | return compareIntervalsDifferentBases(idInterval1, idInterval2) 74 | } 75 | 76 | function compareIntervalsDifferentBases ( 77 | idInterval1: IdentifierInterval, 78 | idInterval2: IdentifierInterval): IdentifierIteratorResults { 79 | 80 | const id1: Identifier = idInterval1.idBegin 81 | const id2: Identifier = idInterval2.idBegin 82 | console.assert(!id1.equalsBase(id2), "the bases of the ids must be different") 83 | 84 | if (idInterval1.containsId(id2)) { 85 | return IdentifierIteratorResults.B2_INSIDE_B1 86 | } 87 | if (idInterval2.containsId(id1)) { 88 | return IdentifierIteratorResults.B1_INSIDE_B2 89 | } 90 | if (id1.compareTo(id2) === Ordering.Less) { 91 | return IdentifierIteratorResults.B1_BEFORE_B2 92 | } 93 | return IdentifierIteratorResults.B1_AFTER_B2 94 | } 95 | -------------------------------------------------------------------------------- /src/logootsblock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "./data-validation" 21 | import {IdentifierInterval} from "./identifierinterval" 22 | import {isInt32} from "./int32" 23 | 24 | export class LogootSBlock { 25 | 26 | static fromPlain (o: unknown): LogootSBlock | null { 27 | if (isObject(o) && 28 | isInt32(o.nbElement) && o.nbElement >= 0) { 29 | 30 | const id = IdentifierInterval.fromPlain(o.idInterval) 31 | if (id !== null) { 32 | return new LogootSBlock(id, o.nbElement) 33 | // FIXME: Always not mine? 34 | } 35 | } 36 | return null 37 | } 38 | 39 | // Access 40 | idInterval: IdentifierInterval 41 | nbElement: number 42 | 43 | // Creation 44 | constructor (idInterval: IdentifierInterval, nbElt: number) { 45 | console.assert(isInt32(nbElt) && nbElt >= 0, 46 | "nbElt must be a non-negative integer") 47 | 48 | this.idInterval = idInterval 49 | this.nbElement = nbElt 50 | } 51 | 52 | isMine (replicaNumber: number): boolean { 53 | return this.idInterval.idBegin.generator === replicaNumber 54 | } 55 | 56 | addBlock (pos: number, length: number): void { 57 | console.assert(isInt32(length) && length > 0, "length must be a positive int32") 58 | 59 | this.nbElement += length 60 | this.idInterval = this.idInterval.union(pos, pos + length - 1) 61 | } 62 | 63 | delBlock (nbElt: number): void { 64 | console.assert(isInt32(nbElt) && nbElt > 0, "nbElt must be a positive int32") 65 | 66 | this.nbElement -= nbElt 67 | } 68 | 69 | toString (): string { 70 | return "{" + this.nbElement + "," + this.idInterval.toString() + "}" 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/operations/delete/logootsdel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "../../data-validation" 21 | import { IdentifierInterval } from "../../identifierinterval" 22 | import { isInt32 } from "../../int32" 23 | import { LogootSRopes } from "../../logootsropes" 24 | import { LogootSOperation } from "../logootsoperation" 25 | import { TextDelete } from "./textdelete" 26 | 27 | const arrayConcat = Array.prototype.concat 28 | 29 | class LogootSDelV1 { 30 | static fromPlain (o: unknown): LogootSDel | null { 31 | if (isObject(o) && 32 | Array.isArray(o.lid) && o.lid.length > 0) { 33 | 34 | let isOk = true 35 | let i = 0 36 | const lid: IdentifierInterval[] = [] 37 | while (isOk && i < o.lid.length) { 38 | const idi = IdentifierInterval.fromPlain( o.lid[i]) 39 | if (idi !== null) { 40 | lid.push(idi) 41 | } else { 42 | isOk = false 43 | } 44 | i++ 45 | } 46 | if (isOk) { 47 | return new LogootSDel(lid, -1) 48 | } 49 | } 50 | return null 51 | } 52 | 53 | readonly lid?: IdentifierInterval[] 54 | } 55 | 56 | /** 57 | * Represents a LogootSplit delete operation. 58 | */ 59 | export class LogootSDel extends LogootSOperation { 60 | static fromPlain (o: unknown): LogootSDel | null { 61 | if (isObject(o) && 62 | Array.isArray(o.lid) && o.lid.length > 0 && isInt32(o.author)) { 63 | 64 | let isOk = true 65 | let i = 0 66 | const lid: IdentifierInterval[] = [] 67 | while (isOk && i < o.lid.length) { 68 | const idi = IdentifierInterval.fromPlain(o.lid[i]) 69 | if (idi !== null) { 70 | lid.push(idi) 71 | } else { 72 | isOk = false 73 | } 74 | i++ 75 | } 76 | if (isOk) { 77 | return new LogootSDel(lid, o.author) 78 | } 79 | } 80 | // For backward compatibility 81 | // Allow to replay and update previous log of operations 82 | return LogootSDelV1.fromPlain(o) 83 | } 84 | 85 | readonly lid: IdentifierInterval[] 86 | readonly author: number 87 | 88 | /** 89 | * @constructor 90 | * @param {IdentifierInterval[]} lid - the list of identifier that localise the deletion in the logoot sequence. 91 | * @param {number} author - the author of the operation. 92 | */ 93 | constructor (lid: IdentifierInterval[], author: number) { 94 | console.assert(lid.length > 0, "lid must not be empty") 95 | console.assert(isInt32(author), "author ∈ int32") 96 | 97 | super() 98 | this.lid = lid 99 | this.author = author 100 | } 101 | 102 | equals (aOther: LogootSDel): boolean { 103 | return ( 104 | this.lid.length === aOther.lid.length && 105 | this.lid.every( 106 | (idInterval: IdentifierInterval, index: number): boolean => { 107 | const otherIdInterval: IdentifierInterval = aOther.lid[index] 108 | return idInterval.equals(otherIdInterval) 109 | }, 110 | ) 111 | ) 112 | } 113 | 114 | /** 115 | * Apply the current delete operation to a LogootSplit document. 116 | * @param {LogootSRopes} doc - the LogootSplit document on which the deletions wil be performed. 117 | * @return {TextDelete[]} the list of deletions to be applied on the sequence representing the document content. 118 | */ 119 | execute (doc: LogootSRopes): TextDelete[] { 120 | return arrayConcat.apply( 121 | [], 122 | this.lid.map( 123 | (aId: IdentifierInterval): TextDelete[] => 124 | doc.delBlock(aId, this.author), 125 | ), 126 | ) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/operations/delete/renamablelogootsdel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "../../data-validation" 21 | import {Epoch} from "../../epoch/epoch" 22 | import {RenamableReplicableList} from "../../renamablereplicablelist" 23 | import {RenamableLogootSOperation} from "../renamablelogootsoperation" 24 | import {TextOperation} from "../textoperation" 25 | import {LogootSDel} from "./logootsdel" 26 | 27 | export class RenamableLogootSDel extends RenamableLogootSOperation { 28 | 29 | static fromPlain (o: unknown): RenamableLogootSDel | null { 30 | if (isObject(o)) { 31 | const op = LogootSDel.fromPlain(o.op) 32 | const epoch = Epoch.fromPlain(o.epoch) 33 | 34 | if (op !== null && epoch !== null) { 35 | return new RenamableLogootSDel(op, epoch) 36 | } 37 | } 38 | return null 39 | } 40 | 41 | constructor (op: LogootSDel, epoch: Epoch) { 42 | super(op, epoch) 43 | } 44 | 45 | execute (renamableList: RenamableReplicableList): TextOperation[] { 46 | return renamableList.delRemote(this.epoch, this.op) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/operations/delete/textdelete.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isInt32} from "../../int32" 21 | import {LogootSRopes} from "../../logootsropes" 22 | import {TextOperation} from "../textoperation" 23 | import {LogootSDel} from "./logootsdel" 24 | 25 | /** 26 | * Represents a sequence operation (deletion). 27 | */ 28 | export class TextDelete extends TextOperation { 29 | 30 | readonly length: number 31 | 32 | /** 33 | * @constructor 34 | * @param {number} index - the position of the first element to be deleted in the sequence. 35 | * @param {number} length - the length of the range to be deleted in the sequence. 36 | * @param {number} author - the author of the operation. 37 | */ 38 | constructor (index: number, length: number, author: number) { 39 | console.assert(isInt32(index), "index ∈ int32") 40 | console.assert(isInt32(length), "length ∈ int32") 41 | console.assert(length > 0, "length > 0") 42 | console.assert(isInt32(author), "author ∈ int32") 43 | 44 | super(index, author) 45 | this.length = length 46 | } 47 | 48 | equals (other: TextDelete): boolean { 49 | return this.index === other.index && 50 | this.length === other.length 51 | } 52 | 53 | /** 54 | * Apply the current delete operation to a LogootSplit document. 55 | * @param {LogootSDocument} doc - the LogootSplit document on which the deletion wil be performed. 56 | * @return {LogootSDel} the logootsplit deletion that is related to the deletion that has been performed. 57 | */ 58 | applyTo (doc: LogootSRopes): LogootSDel { 59 | return doc.delLocal(this.index, this.index + this.length - 1) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/operations/insert/logootsadd.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "../../data-validation" 21 | import {Identifier} from "../../identifier" 22 | import {IdentifierInterval} from "../../identifierinterval" 23 | import {LogootSRopes} from "../../logootsropes" 24 | import {LogootSOperation} from "../logootsoperation" 25 | import {TextInsert} from "./textinsert" 26 | 27 | class LogootSAddV1 { 28 | static fromPlain (o: unknown): LogootSAdd | null { 29 | if (isObject(o) && 30 | typeof o.l === "string" && o.l.length > 0) { 31 | 32 | const id = Identifier.fromPlain(o.id) 33 | if (id !== null) { 34 | return new LogootSAdd(id, o.l) 35 | } 36 | } 37 | return null 38 | } 39 | 40 | readonly id?: Identifier 41 | readonly l?: string 42 | } 43 | 44 | /** 45 | * Represents a LogootSplit insert operation. 46 | */ 47 | export class LogootSAdd extends LogootSOperation { 48 | 49 | static fromPlain (o: unknown): LogootSAdd | null { 50 | if (isObject(o) && 51 | typeof o.content === "string" && o.content.length > 0) { 52 | 53 | const id = Identifier.fromPlain(o.id) 54 | if (id !== null) { 55 | return new LogootSAdd(id, o.content) 56 | } 57 | } 58 | // For backward compatibility 59 | // Allow to replay and update previous log of operations 60 | return LogootSAddV1.fromPlain(o) 61 | } 62 | 63 | readonly id: Identifier 64 | readonly content: string 65 | 66 | get author (): number { 67 | return this.id.replicaNumber 68 | } 69 | 70 | /** 71 | * @constructor 72 | * @param {Identifier} id - the identifier that localise the insertion in the logoot sequence. 73 | * @param {string} content - the content of the block to be inserted. 74 | */ 75 | constructor (id: Identifier, content: string) { 76 | console.assert(content.length > 0, "content must not be empty") 77 | 78 | super() 79 | this.id = id 80 | this.content = content 81 | } 82 | 83 | get insertedIds (): Identifier[] { 84 | const insertedIdInterval = new IdentifierInterval(this.id, this.id.lastOffset + this.content.length - 1) 85 | return insertedIdInterval.toIds() 86 | } 87 | 88 | equals (aOther: LogootSAdd): boolean { 89 | return this.id.equals(aOther.id) && 90 | this.content === aOther.content 91 | } 92 | 93 | /** 94 | * Apply the current insert operation to a LogootSplit document. 95 | * @param {LogootSRopes} doc - the LogootSplit document on which the operation wil be applied. 96 | * @return {TextInsert[]} the insertion to be applied on the sequence representing the document content. 97 | */ 98 | execute (doc: LogootSRopes): TextInsert[] { 99 | return doc.addBlock(this.content, this.id) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/operations/insert/renamablelogootsadd.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "../../data-validation" 21 | import {Epoch} from "../../epoch/epoch" 22 | import {RenamableReplicableList} from "../../renamablereplicablelist" 23 | import {RenamableLogootSOperation} from "../renamablelogootsoperation" 24 | import {TextOperation} from "../textoperation" 25 | import {LogootSAdd} from "./logootsadd" 26 | 27 | export class RenamableLogootSAdd extends RenamableLogootSOperation { 28 | 29 | static fromPlain (o: unknown): RenamableLogootSAdd | null { 30 | if (isObject(o)) { 31 | const op = LogootSAdd.fromPlain(o.op) 32 | const epoch = Epoch.fromPlain(o.epoch) 33 | 34 | if (op !== null && epoch !== null) { 35 | return new RenamableLogootSAdd(op, epoch) 36 | } 37 | } 38 | return null 39 | } 40 | 41 | constructor (op: LogootSAdd, epoch: Epoch) { 42 | super(op, epoch) 43 | } 44 | 45 | execute (renamableList: RenamableReplicableList): TextOperation[] { 46 | return renamableList.insertRemote(this.epoch, this.op) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/operations/insert/textinsert.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isInt32} from "../../int32" 21 | import {LogootSRopes} from "../../logootsropes" 22 | import {TextOperation} from "../textoperation" 23 | import {LogootSAdd} from "./logootsadd" 24 | 25 | /** 26 | * Represents a sequence operation (insert). 27 | */ 28 | export class TextInsert extends TextOperation { 29 | 30 | readonly content: string 31 | 32 | /** 33 | * @constructor 34 | * @param {number} offset - the insertion position in the sequence. 35 | * @param {string} content - the content to be inserted in the sequence. 36 | * @param {number} author - the author of the operation. 37 | */ 38 | constructor (index: number, content: string, author: number) { 39 | console.assert(isInt32(index), "index ∈ int32") 40 | console.assert(isInt32(author), "author ∈ int32") 41 | 42 | super(index, author) 43 | this.content = content 44 | } 45 | 46 | equals (other: TextInsert): boolean { 47 | return this.index === other.index && 48 | this.content === other.content 49 | } 50 | 51 | /** 52 | * Apply the current insert operation to a LogootSplit document. 53 | * @param {LogootSDocument} doc - the LogootSplit document on which the insertion wil be performed. 54 | * @return {LogootSAdd} the logootsplit insertion that is related to the insertion that has been performed. 55 | */ 56 | applyTo (doc: LogootSRopes): LogootSAdd { 57 | return doc.insertLocal(this.index, this.content) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/operations/logootsoperation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {LogootSRopes} from "../logootsropes" 21 | import {TextOperation} from "./textoperation" 22 | 23 | export abstract class LogootSOperation { 24 | abstract readonly author: number 25 | 26 | abstract execute (doc: LogootSRopes): TextOperation[] 27 | } 28 | -------------------------------------------------------------------------------- /src/operations/renamablelistoperation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {Epoch} from "../epoch/epoch" 21 | import {RenamableReplicableList} from "../renamablereplicablelist" 22 | import {TextOperation} from "./textoperation" 23 | 24 | export abstract class RenamableListOperation { 25 | 26 | abstract readonly author: number 27 | 28 | readonly epoch: Epoch 29 | 30 | constructor (epoch: Epoch) { 31 | this.epoch = epoch 32 | } 33 | 34 | abstract execute (doc: RenamableReplicableList): TextOperation[] 35 | } 36 | -------------------------------------------------------------------------------- /src/operations/renamablelogootsoperation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {Epoch} from "../epoch/epoch" 21 | import {RenamableReplicableList} from "../renamablereplicablelist" 22 | import {LogootSOperation} from "./logootsoperation" 23 | import {RenamableListOperation} from "./renamablelistoperation" 24 | import {TextOperation} from "./textoperation" 25 | 26 | export abstract class RenamableLogootSOperation extends RenamableListOperation { 27 | 28 | readonly op: T 29 | 30 | constructor (op: T, epoch: Epoch) { 31 | super(epoch) 32 | 33 | this.op = op 34 | } 35 | 36 | get author (): number { 37 | return this.op.author 38 | } 39 | 40 | abstract execute (renamableList: RenamableReplicableList): TextOperation[] 41 | } 42 | -------------------------------------------------------------------------------- /src/operations/rename/logootsrename.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "../../data-validation" 21 | import {Epoch} from "../../epoch/epoch" 22 | import {IdentifierInterval} from "../../identifierinterval" 23 | import {isInt32} from "../../int32" 24 | import {RenamableReplicableList} from "../../renamablereplicablelist" 25 | import {RenamableListOperation} from "../renamablelistoperation" 26 | import {TextOperation} from "../textoperation" 27 | 28 | /** 29 | * Represents a LogootSplit rename operation. 30 | */ 31 | export class LogootSRename extends RenamableListOperation { 32 | 33 | static fromPlain (o: unknown) { 34 | if (isObject(o) && 35 | isInt32(o.replicaNumber) && isInt32(o.clock) && 36 | Array.isArray(o.renamedIdIntervals) && o.renamedIdIntervals.length > 0) { 37 | 38 | const renamedIdIntervals = o.renamedIdIntervals.map(IdentifierInterval.fromPlain) 39 | .filter((v): v is IdentifierInterval => v !== null) 40 | const epoch = Epoch.fromPlain(o.epoch) 41 | 42 | if (epoch !== null && 43 | o.renamedIdIntervals.length === renamedIdIntervals.length) { 44 | return new LogootSRename(o.replicaNumber, o.clock, epoch, renamedIdIntervals) 45 | } 46 | } 47 | return null 48 | } 49 | 50 | readonly replicaNumber: number 51 | readonly clock: number 52 | readonly renamedIdIntervals: IdentifierInterval[] 53 | 54 | /** 55 | * @constructor 56 | */ 57 | constructor (replicaNumber: number, clock: number, epoch: Epoch, 58 | renamedIdIntervals: IdentifierInterval[]) { 59 | 60 | super(epoch) 61 | this.replicaNumber = replicaNumber 62 | this.clock = clock 63 | this.renamedIdIntervals = renamedIdIntervals 64 | } 65 | 66 | get author (): number { 67 | return this.replicaNumber 68 | } 69 | 70 | execute (renamableList: RenamableReplicableList): TextOperation[] { 71 | renamableList.renameRemote(this.replicaNumber, this.clock, this.epoch, this.renamedIdIntervals) 72 | return [] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/operations/textoperation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {LogootSRopes} from "../logootsropes" 21 | import {LogootSOperation} from "./logootsoperation" 22 | 23 | export abstract class TextOperation { 24 | 25 | readonly index: number 26 | readonly author: number 27 | 28 | constructor (index: number, author: number) { 29 | this.index = index 30 | this.author = author 31 | } 32 | 33 | abstract applyTo (doc: LogootSRopes): LogootSOperation 34 | } 35 | -------------------------------------------------------------------------------- /src/ordering.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | /** 21 | * Possible relation between two elements in a totally ordered set. 22 | */ 23 | export const enum Ordering { 24 | Less = -1, 25 | Equal = 0, 26 | Greater = 1, 27 | } 28 | -------------------------------------------------------------------------------- /src/renamablereplicablelist.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import { isObject } from "./data-validation" 21 | import { Epoch} from "./epoch/epoch" 22 | import { EpochId } from "./epoch/epochid" 23 | import { compareEpochFullIds, EpochStore } from "./epoch/epochstore" 24 | import { Identifier } from "./identifier" 25 | import { IdentifierInterval } from "./identifierinterval" 26 | import { createAtPosition } from "./idfactory" 27 | import { LogootSRopes } from "./logootsropes" 28 | import { LogootSDel } from "./operations/delete/logootsdel" 29 | import { RenamableLogootSDel } from "./operations/delete/renamablelogootsdel" 30 | import { TextDelete } from "./operations/delete/textdelete" 31 | import { LogootSAdd } from "./operations/insert/logootsadd" 32 | import { RenamableLogootSAdd } from "./operations/insert/renamablelogootsadd" 33 | import { TextInsert } from "./operations/insert/textinsert" 34 | import { LogootSRename } from "./operations/rename/logootsrename" 35 | import { Ordering } from "./ordering" 36 | import { RenamingMap } from "./renamingmap/renamingmap" 37 | import { RenamingMapStore } from "./renamingmap/renamingmapstore" 38 | import { mkNodeAt } from "./ropesnodes" 39 | 40 | function generateInsertOps (idIntervals: IdentifierInterval[], str: string): LogootSAdd[] { 41 | let currentOffset = 0 42 | return idIntervals 43 | .map((idInterval: IdentifierInterval): LogootSAdd => { 44 | const nextOffset = currentOffset + idInterval.length 45 | const content = str.slice(currentOffset, nextOffset) 46 | currentOffset = nextOffset 47 | return new LogootSAdd(idInterval.idBegin, content) 48 | }) 49 | } 50 | 51 | export interface RenamableReplicableListJSON { 52 | readonly epochsStore: EpochStore 53 | readonly renamingMapStore: RenamingMapStore 54 | readonly list: LogootSRopes 55 | readonly currentEpoch: Epoch 56 | } 57 | 58 | export class RenamableReplicableList { 59 | 60 | static create (replicaNumber = 0, clock = 0): RenamableReplicableList { 61 | const list = new LogootSRopes(replicaNumber, clock) 62 | 63 | const currentEpoch = new Epoch(new EpochId(0, 0)) 64 | const epochsStore = new EpochStore(currentEpoch) 65 | const renamingMapStore = new RenamingMapStore() 66 | 67 | return new RenamableReplicableList(list, currentEpoch, epochsStore, renamingMapStore) 68 | } 69 | 70 | static fromPlain (o: unknown): RenamableReplicableList | null { 71 | if (isObject(o)) { 72 | 73 | const list = LogootSRopes.fromPlain(o.list) 74 | const epochsStore = EpochStore.fromPlain(o.epochsStore) 75 | const renamingMapStore = RenamingMapStore.fromPlain(o.renamingMapStore) 76 | const currentEpoch = Epoch.fromPlain(o.currentEpoch) 77 | 78 | if (list !== null && epochsStore !== null && 79 | renamingMapStore !== null && currentEpoch !== null) { 80 | 81 | return new RenamableReplicableList(list, currentEpoch, epochsStore, renamingMapStore) 82 | } 83 | } 84 | return null 85 | } 86 | 87 | static fromPlainLogootSRopes (o: unknown): RenamableReplicableList | null { 88 | const list = LogootSRopes.fromPlain(o) 89 | if (list !== null) { 90 | const currentEpoch = new Epoch(new EpochId(0, 0)) 91 | const epochsStore = new EpochStore(currentEpoch) 92 | const renamingMapStore = new RenamingMapStore() 93 | 94 | return new RenamableReplicableList(list, currentEpoch, epochsStore, renamingMapStore) 95 | } 96 | return null 97 | } 98 | 99 | readonly epochsStore: EpochStore 100 | readonly renamingMapStore: RenamingMapStore 101 | private list: LogootSRopes 102 | private currentEpoch: Epoch 103 | 104 | private constructor ( 105 | list: LogootSRopes, currentEpoch: Epoch, 106 | epochsStore: EpochStore, renamingMapStore: RenamingMapStore) { 107 | 108 | this.list = list 109 | this.currentEpoch = currentEpoch 110 | this.epochsStore = epochsStore 111 | this.renamingMapStore = renamingMapStore 112 | } 113 | 114 | get replicaNumber (): number { 115 | return this.list.replicaNumber 116 | } 117 | 118 | get clock (): number { 119 | return this.list.clock 120 | } 121 | 122 | get currentRenamingMap (): RenamingMap { 123 | return this.renamingMapStore.getRenamingMap(this.currentEpoch.id) as RenamingMap 124 | } 125 | 126 | getList (): LogootSRopes { 127 | return this.list 128 | } 129 | 130 | getCurrentEpoch (): Epoch { 131 | return this.currentEpoch 132 | } 133 | 134 | get str (): string { 135 | return this.list.str 136 | } 137 | 138 | insertLocal (pos: number, l: string): RenamableLogootSAdd { 139 | return new RenamableLogootSAdd(this.list.insertLocal(pos, l), this.currentEpoch) 140 | } 141 | 142 | insertRemote (epoch: Epoch, op: LogootSAdd): TextInsert[] { 143 | if (!epoch.equals(this.currentEpoch)) { 144 | const strat = (rmap: RenamingMap, ids: Identifier[]) => rmap.initRenameIds(ids) 145 | const newIds = this.renameFromEpochToCurrent(op.insertedIds, epoch, strat) 146 | const newIdIntervals = IdentifierInterval.mergeIdsIntoIntervals(newIds) 147 | 148 | const insertOps = generateInsertOps(newIdIntervals, op.content) 149 | return insertOps 150 | .flatMap((insertOp: LogootSAdd): TextInsert[] => insertOp.execute(this.list)) 151 | } 152 | return op.execute(this.list) 153 | } 154 | 155 | delLocal (begin: number, end: number): RenamableLogootSDel { 156 | return new RenamableLogootSDel(this.list.delLocal(begin, end), this.currentEpoch) 157 | } 158 | 159 | delRemote (epoch: Epoch, op: LogootSDel): TextDelete[] { 160 | if (!epoch.equals(this.currentEpoch)) { 161 | const idsToRename = op.lid 162 | .flatMap((idInterval: IdentifierInterval): Identifier[] => idInterval.toIds()) 163 | 164 | const strat = (rmap: RenamingMap, ids: Identifier[]) => rmap.initRenameIds(ids) 165 | const newIds = this.renameFromEpochToCurrent(idsToRename, epoch, strat) 166 | const newIdIntervals = IdentifierInterval.mergeIdsIntoIntervals(newIds) 167 | 168 | const newOp = new LogootSDel(newIdIntervals, op.author) 169 | return newOp.execute(this.list) 170 | } 171 | return op.execute(this.list) 172 | } 173 | 174 | renameLocal (): LogootSRename { 175 | const renamedIdIntervals = this.list.toList() 176 | const clock = this.clock 177 | 178 | const newEpochNumber = this.currentEpoch.id.epochNumber + 1 179 | const newEpochId = new EpochId(this.replicaNumber, newEpochNumber) 180 | this.currentEpoch = new Epoch(newEpochId, this.currentEpoch.id) 181 | 182 | this.epochsStore.addEpoch(this.currentEpoch) 183 | 184 | const newRandom = renamedIdIntervals[0].idBegin.tuples[0].random 185 | const renamingMap = new RenamingMap(this.replicaNumber, clock, renamedIdIntervals) 186 | this.renamingMapStore.add(this.currentEpoch, renamingMap) 187 | 188 | const baseId = createAtPosition(this.replicaNumber, clock, newRandom, 0) 189 | const newRoot = mkNodeAt(baseId, this.str.length) 190 | 191 | this.list = new LogootSRopes(this.replicaNumber, clock + 1, newRoot, this.str) 192 | 193 | return new LogootSRename(this.replicaNumber, clock, this.currentEpoch, renamedIdIntervals) 194 | } 195 | 196 | renameRemote (replicaNumber: number, clock: number, newEpoch: Epoch, 197 | renamedIdIntervals: IdentifierInterval[]) { 198 | 199 | const renamingMap = new RenamingMap(replicaNumber, clock, renamedIdIntervals) 200 | this.epochsStore.addEpoch(newEpoch) 201 | this.renamingMapStore.add(newEpoch, renamingMap) 202 | 203 | const newEpochFullId = this.epochsStore.getEpochFullId(newEpoch) 204 | const currentEpochFullId = this.epochsStore.getEpochFullId(this.currentEpoch) 205 | 206 | if (compareEpochFullIds(currentEpochFullId, newEpochFullId) === Ordering.Less) { 207 | const previousEpoch = this.currentEpoch 208 | this.currentEpoch = newEpoch 209 | 210 | const idsToRename = this.list.toList().flatMap((idInterval) => idInterval.toIds()) 211 | const strat = (rmap: RenamingMap, ids: Identifier[]) => rmap.initRenameSeq(ids) 212 | const newIds = this.renameFromEpochToCurrent(idsToRename, previousEpoch, strat) 213 | const newIdIntervals = IdentifierInterval.mergeIdsIntoIntervals(newIds) 214 | 215 | const newList = new LogootSRopes(this.replicaNumber, this.clock) 216 | const insertOps = generateInsertOps(newIdIntervals, this.str) 217 | insertOps.forEach((insertOp: LogootSAdd) => { 218 | insertOp.execute(newList) 219 | }) 220 | this.list = newList 221 | } 222 | } 223 | 224 | renameFromEpochToCurrent ( 225 | idsToRename: Identifier[], 226 | fromEpoch: Epoch, 227 | strat: (rmap: RenamingMap, ids: Identifier[]) => Identifier[], 228 | ): Identifier[] { 229 | 230 | const [epochsToRevert, epochsToApply] = 231 | this.epochsStore.getPathBetweenEpochs(fromEpoch, this.currentEpoch) 232 | 233 | let ids = idsToRename 234 | epochsToRevert.forEach((epoch) => { 235 | const rmap = this.renamingMapStore.getRenamingMap(epoch.id) as RenamingMap 236 | ids = ids.map((id) => rmap.reverseRenameId(id)) 237 | }) 238 | 239 | epochsToApply.forEach((epoch) => { 240 | const rmap = this.renamingMapStore.getRenamingMap(epoch.id) as RenamingMap 241 | ids = strat(rmap, ids) 242 | }) 243 | 244 | return ids 245 | } 246 | 247 | getNbBlocks (): number { 248 | return this.list.toList().length 249 | } 250 | 251 | digest (): number { 252 | return this.list.digest() 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/renamingmap/renamingmapstore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import { isArrayFromMap, isObject } from "../data-validation" 21 | import { Epoch } from "../epoch/epoch" 22 | import { EpochId } from "../epoch/epochid" 23 | import { RenamingMap } from "./renamingmap" 24 | 25 | export interface RenamingMapStoreJSON { 26 | readonly renamingMaps: Array<[string, RenamingMap]> 27 | } 28 | 29 | export class RenamingMapStore { 30 | 31 | static fromPlain (o: unknown): RenamingMapStore | null { 32 | if (isObject(o) && 33 | Array.isArray(o.renamingMaps)) { 34 | 35 | const renamingMaps = o.renamingMaps 36 | .filter(isArrayFromMap) 37 | .map(([k, v]): [unknown, RenamingMap | null] => { 38 | return [k, RenamingMap.fromPlain(v)] 39 | }) 40 | .filter((arg): arg is [string, RenamingMap] => typeof arg[0] === "string" && arg [1] !== null) 41 | 42 | if (o.renamingMaps.length === renamingMaps.length) { 43 | const renamingMapStore = new RenamingMapStore() 44 | renamingMaps.forEach(([epochId, renamingMap]) => { 45 | renamingMapStore.internalAdd(epochId, renamingMap) 46 | }) 47 | return renamingMapStore 48 | } 49 | } 50 | return null 51 | } 52 | 53 | private renamingMaps: Map 54 | 55 | constructor () { 56 | this.renamingMaps = new Map() 57 | } 58 | 59 | add (epoch: Epoch, renamingMap: RenamingMap) { 60 | this.internalAdd(epoch.id.asStr, renamingMap) 61 | } 62 | 63 | getRenamingMap (epochId: EpochId): RenamingMap | undefined { 64 | return this.renamingMaps.get(epochId.asStr) 65 | } 66 | 67 | toJSON (): RenamingMapStoreJSON { 68 | return { renamingMaps: Array.from(this.renamingMaps) } 69 | } 70 | 71 | purge (): void { 72 | this.renamingMaps.clear() 73 | } 74 | 75 | private internalAdd (epochId: string, renamingMap: RenamingMap) { 76 | this.renamingMaps.set(epochId, renamingMap) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/responseintnode.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {RopesNodes} from "./ropesnodes" 21 | 22 | export interface ResponseIntNode { 23 | 24 | readonly i: number 25 | 26 | readonly node: RopesNodes 27 | 28 | readonly path: RopesNodes[] 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/ropesnodes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import {isObject} from "./data-validation" 21 | import {Identifier} from "./identifier" 22 | import {IdentifierInterval} from "./identifierinterval" 23 | import {isInt32} from "./int32" 24 | import {LogootSBlock} from "./logootsblock" 25 | 26 | /** 27 | * @param aNode may be null 28 | * @returns Height of aNode or 0 if aNode is null 29 | */ 30 | function heightOf (aNode: RopesNodes | null): number { 31 | if (aNode !== null) { 32 | return aNode.height 33 | } else { 34 | return 0 35 | } 36 | } 37 | 38 | /** 39 | * @param aNode may be null 40 | * @returns size of aNode (including children sizes) or 0 if aNode is null 41 | */ 42 | function subtreeSizeOf (aNode: RopesNodes | null): number { 43 | if (aNode !== null) { 44 | return aNode.sizeNodeAndChildren 45 | } else { 46 | return 0 47 | } 48 | } 49 | 50 | export function mkNodeAt (id: Identifier, length: number): RopesNodes { 51 | console.assert(isInt32(length), "length ∈ int32") 52 | console.assert(length > 0, "length > 0") 53 | 54 | const idi = new IdentifierInterval(id, length - 1) 55 | const newBlock = new LogootSBlock(idi, 0) 56 | return RopesNodes.leaf(newBlock, 0, length) 57 | } 58 | 59 | export class RopesNodes { 60 | 61 | static fromPlain (o: unknown): RopesNodes | null { 62 | if (isObject(o) && 63 | isInt32(o.actualBegin) && isInt32(o.length) && o.length >= 0) { 64 | 65 | const block = LogootSBlock.fromPlain(o.block) 66 | if (block !== null && 67 | block.idInterval.begin <= o.actualBegin && 68 | (block.idInterval.end - block.idInterval.begin) >= o.length - 1) { 69 | 70 | const right = RopesNodes.fromPlain(o.right) 71 | const left = RopesNodes.fromPlain(o.left) 72 | return new RopesNodes(block, o.actualBegin, o.length, left, right) 73 | } 74 | } 75 | return null 76 | } 77 | 78 | static leaf (block: LogootSBlock, offset: number, lenth: number): RopesNodes { 79 | console.assert(isInt32(offset), "aOffset ∈ int32") 80 | console.assert(isInt32(lenth), "lenth ∈ int32") 81 | console.assert(lenth > 0, "lenth > 0") 82 | 83 | block.addBlock(offset, lenth) // Mutation 84 | return new RopesNodes(block, offset, lenth, null, null) 85 | } 86 | 87 | // Access 88 | left: RopesNodes | null 89 | 90 | right: RopesNodes | null 91 | 92 | height: number 93 | 94 | block: LogootSBlock 95 | 96 | /** 97 | * The current position of the beginning of the block 98 | * 99 | * Should always ensure that block.idInterval.begin <= actualBegin <= block.idInterval.end 100 | */ 101 | actualBegin: number 102 | 103 | /** 104 | * The current length of the block 105 | * 106 | * Should always ensure that length <= to block.idInterval.end - block.idInterval.begin + 1 107 | */ 108 | length: number 109 | 110 | sizeNodeAndChildren: number 111 | 112 | // Creation 113 | constructor ( 114 | block: LogootSBlock, actualBegin: number, length: number, 115 | left: RopesNodes | null, right: RopesNodes | null) { 116 | 117 | console.assert(isInt32(actualBegin), "actualBegin ∈ int32") 118 | console.assert(block.idInterval.begin <= actualBegin, 119 | "actualBegin must be greater than or equal to idInterval.begin") 120 | 121 | this.block = block 122 | this.actualBegin = actualBegin 123 | this.length = length 124 | this.left = left 125 | this.right = right 126 | this.height = Math.max(heightOf(left), heightOf(right)) + 1 127 | this.sizeNodeAndChildren = length + 128 | subtreeSizeOf(left) + subtreeSizeOf(right) 129 | } 130 | 131 | get actualEnd (): number { 132 | return this.actualBegin + this.length - 1 133 | } 134 | 135 | getIdBegin (): Identifier { 136 | return this.block.idInterval.getBaseId(this.actualBegin) 137 | } 138 | 139 | getIdEnd (): Identifier { 140 | return this.block.idInterval.getBaseId(this.actualEnd) 141 | } 142 | 143 | get max (): Identifier { 144 | if (this.right !== null) { 145 | return this.right.max 146 | } 147 | return this.getIdEnd() 148 | } 149 | 150 | get min (): Identifier { 151 | if (this.left !== null) { 152 | return this.left.min 153 | } 154 | return this.getIdBegin() 155 | } 156 | 157 | addString (length: number): void { 158 | console.assert(isInt32(length), "length ∈ int32") 159 | // `length" may be negative 160 | 161 | this.sizeNodeAndChildren += length 162 | } 163 | 164 | appendEnd (length: number): Identifier { 165 | console.assert(isInt32(length), "length ∈ int32") 166 | console.assert(length > 0, "" + length, " > 0") 167 | 168 | const b = this.actualEnd + 1 169 | this.length += length 170 | this.block.addBlock(b, length) 171 | return this.block.idInterval.getBaseId(b) 172 | } 173 | 174 | appendBegin (length: number): Identifier { 175 | console.assert(isInt32(length), "length ∈ int32") 176 | console.assert(length > 0, "" + length, " > 0") 177 | 178 | this.actualBegin -= length 179 | this.length += length 180 | this.block.addBlock(this.actualBegin, length) 181 | return this.getIdBegin() 182 | } 183 | 184 | /** 185 | * Delete a interval of identifiers belonging to this node 186 | * Reduces the node"s {@link RopesNodes#length} and/or shifts its {@link RopesNodes#offset} 187 | * May also trigger a split of the current node if the deletion cuts it in two parts 188 | * 189 | * @param {number} begin The start of the interval to delete 190 | * @param {number} end The end of the interval to delete 191 | * @returns {RopesNodes | null} The resulting block if a split occured, null otherwise 192 | */ 193 | deleteOffsets (begin: number, end: number): RopesNodes | null { 194 | console.assert(isInt32(begin), "begin ∈ int32") 195 | console.assert(isInt32(end), "end ∈ int32") 196 | console.assert(begin <= end, "begin <= end: " + begin, " <= " + end) 197 | console.assert(this.block.idInterval.begin <= begin, 198 | "this.block.idInterval.begin <= to begin: " + this.block.idInterval.begin, " <= " + begin) 199 | console.assert(end <= this.block.idInterval.end, 200 | "end <= this.block.idInterval.end: " + end, " <= " + this.block.idInterval.end) 201 | 202 | let ret: RopesNodes | null = null 203 | 204 | // Some identifiers may have already been deleted by a previous operation 205 | // Need to update the range of the deletion accordingly 206 | // NOTE: actualEnd can be < to actualBegin if all the range has previously been deleted 207 | const actualBegin: number = Math.max(this.actualBegin, begin) 208 | const actualEnd: number = Math.min(this.actualEnd, end) 209 | 210 | if (actualBegin <= actualEnd) { 211 | const sizeToDelete = actualEnd - actualBegin + 1 212 | this.block.delBlock(sizeToDelete) 213 | 214 | if (sizeToDelete !== this.length) { 215 | if (actualBegin === this.actualBegin) { 216 | // Deleting the beginning of the block 217 | this.actualBegin = actualEnd + 1 218 | } else if (actualEnd !== this.actualEnd) { 219 | // Deleting the middle of the block 220 | ret = this.split(actualEnd - this.actualBegin + 1, null) 221 | } 222 | } 223 | this.length = this.length - sizeToDelete 224 | } 225 | 226 | return ret 227 | } 228 | 229 | split (size: number, node: RopesNodes | null): RopesNodes { 230 | const newRight = new RopesNodes(this.block, 231 | this.actualBegin + size, this.length - size, node, this.right) 232 | this.length = size 233 | this.right = newRight 234 | this.height = Math.max(this.height, newRight.height) 235 | return newRight 236 | } 237 | 238 | leftSubtreeSize (): number { 239 | return subtreeSizeOf (this.left) 240 | } 241 | 242 | rightSubtreeSize (): number { 243 | return subtreeSizeOf (this.right) 244 | } 245 | 246 | sumDirectChildren (): void { 247 | this.height = Math.max(heightOf(this.left), heightOf(this.right)) + 1 248 | this.sizeNodeAndChildren = this.leftSubtreeSize() + this.rightSubtreeSize() + this.length 249 | } 250 | 251 | replaceChildren (node: RopesNodes, by: RopesNodes | null): void { 252 | if (this.left === node) { 253 | this.left = by 254 | } else if (this.right === node) { 255 | this.right = by 256 | } 257 | } 258 | 259 | balanceScore (): number { 260 | return heightOf(this.right) - heightOf(this.left) 261 | } 262 | 263 | become (node: RopesNodes): void { 264 | this.sizeNodeAndChildren = -this.length + node.length 265 | this.length = node.length 266 | this.actualBegin = node.actualBegin 267 | this.block = node.block 268 | } 269 | 270 | isAppendableAfter (replicaNumber: number, length: number): boolean { 271 | return this.block.isMine(replicaNumber) && 272 | this.block.idInterval.end === this.actualEnd && 273 | this.block.idInterval.idEnd.hasPlaceAfter(length) 274 | } 275 | 276 | isAppendableBefore (replicaNumber: number, length: number): boolean { 277 | return this.block.isMine(replicaNumber) && 278 | this.block.idInterval.begin === this.actualBegin && 279 | this.block.idInterval.idBegin.hasPlaceBefore(length) 280 | } 281 | 282 | toString (): string { 283 | const current = this.getIdentifierInterval().toString() 284 | const leftToString = (this.left !== null) ? this.left.toString() : "\t#" 285 | const rightToString = (this.right !== null) ? this.right.toString() : "\t#" 286 | return rightToString.replace(/(\t+)/g, "\t$1") + "\n" + 287 | "\t" + current + "\n" + 288 | leftToString.replace(/(\t+)/g, "\t$1") 289 | } 290 | 291 | /** 292 | * @return linear representation 293 | */ 294 | toList (): IdentifierInterval[] { 295 | const idInterval = this.getIdentifierInterval() 296 | const leftList = (this.left !== null) ? this.left.toList() : [] 297 | const rightList = (this.right !== null) ? this.right.toList() : [] 298 | return leftList.concat(idInterval, rightList) 299 | } 300 | 301 | getIdentifierInterval (): IdentifierInterval { 302 | return new IdentifierInterval(this.getIdBegin(), this.actualEnd) 303 | } 304 | 305 | /** 306 | * @return list of blocks (potentially with occurrences) 307 | */ 308 | getBlocks (): LogootSBlock[] { 309 | let result = [this.block] 310 | 311 | const left = this.left 312 | if (left !== null) { 313 | result = result.concat(left.getBlocks()) 314 | } 315 | 316 | const right = this.right 317 | if (right !== null) { 318 | result = result.concat(right.getBlocks()) 319 | } 320 | 321 | return result 322 | } 323 | 324 | } 325 | -------------------------------------------------------------------------------- /src/stats.ts: -------------------------------------------------------------------------------- 1 | import { LogootSRopes } from "./logootsropes" 2 | import { RopesNodes } from "./ropesnodes" 3 | 4 | export interface BasicStats { 5 | min: number 6 | max: number 7 | mean: number 8 | median: number 9 | lengthRepartition: Map 10 | } 11 | 12 | export class Stats { 13 | readonly documentLength: number 14 | readonly treeHeight: number 15 | private nodeNumber: number 16 | 17 | private nodesStats: BasicStats 18 | private identifiersStats: BasicStats 19 | 20 | private nodeLengths: number[] 21 | private identifierLengths: number[] 22 | 23 | constructor (rope: LogootSRopes) { 24 | this.documentLength = rope.str.length 25 | this.treeHeight = rope.height 26 | this.nodeNumber = 0 27 | this.nodeLengths = [] 28 | this.identifierLengths = [] 29 | this.nodesStats = { 30 | max: 0, 31 | min: -1, 32 | mean: 0, 33 | median: 0, 34 | lengthRepartition: new Map(), 35 | } 36 | this.identifiersStats = { 37 | max: 0, 38 | min: -1, 39 | mean: 0, 40 | median: 0, 41 | lengthRepartition: new Map(), 42 | } 43 | this.compute(rope) 44 | } 45 | 46 | get numberOfNodes (): number { 47 | return this.nodeNumber 48 | } 49 | 50 | get maxNodeLength (): number { 51 | return this.nodesStats.max 52 | } 53 | 54 | get minNodeLength (): number { 55 | return this.nodesStats.min 56 | } 57 | 58 | get meanNodeLength (): number { 59 | return this.nodesStats.mean 60 | } 61 | 62 | get medianNodeLength (): number { 63 | return this.nodesStats.median 64 | } 65 | 66 | get repartitionNodeLength (): Map { 67 | return this.nodesStats.lengthRepartition 68 | } 69 | 70 | get repartitionNodeLengthString (): string { 71 | const arr = Array.from(this.nodesStats.lengthRepartition) 72 | arr.sort((a, b) => { 73 | return a[0] - b[0] 74 | }) 75 | let str = "" 76 | arr.forEach((entry) => { 77 | str += "(" + entry[0] + ", " + entry[1] + "), " 78 | }) 79 | return str 80 | } 81 | 82 | get maxIdentifierLength (): number { 83 | return this.identifiersStats.max 84 | } 85 | 86 | get minIdentifierLength (): number { 87 | return this.identifiersStats.min 88 | } 89 | 90 | get meanIdentifierLength (): number { 91 | return this.identifiersStats.mean 92 | } 93 | 94 | get medianIdentifierLength (): number { 95 | return this.identifiersStats.median 96 | } 97 | 98 | get repartitionIdentifierLength (): Map { 99 | return this.identifiersStats.lengthRepartition 100 | } 101 | 102 | get repartitionIdentifierLengthString (): string { 103 | const arr = Array.from(this.identifiersStats.lengthRepartition) 104 | arr.sort((a, b) => { 105 | return a[0] - b[0] 106 | }) 107 | let str = "" 108 | arr.forEach((entry) => { 109 | str += "(" + entry[0] + ", " + entry[1] + "), " 110 | }) 111 | return str 112 | } 113 | 114 | public toString (): string { 115 | let str = "" 116 | str += "Document stats : \n" 117 | str += "\t Document length : " + this.documentLength + "\n" 118 | str += "\t Number of nodes : " + this.numberOfNodes + "\n" 119 | str += "\t Height of the tree : " + this.treeHeight + "\n" 120 | str += "\t Nodes : " + "\n" 121 | str += "\t\tMax length : " + this.maxNodeLength + "\n" 122 | str += "\t\tMin length : " + this.minNodeLength + "\n" 123 | str += "\t\tMean length : " + this.meanNodeLength + "\n" 124 | str += "\t\tMedian length : " + this.medianNodeLength + "\n" 125 | str += 126 | "\t\tLength repartition : " + this.repartitionNodeLengthString + "\n" 127 | str += "\t Identifier : " + "\n" 128 | str += "\t\tMax length : " + this.maxIdentifierLength + "\n" 129 | str += "\t\tMin length : " + this.minIdentifierLength + "\n" 130 | str += "\t\tMean length : " + this.meanIdentifierLength + "\n" 131 | str += "\t\tMedian length : " + this.medianIdentifierLength + "\n" 132 | str += 133 | "\t\tLength repartition : " + 134 | this.repartitionIdentifierLengthString + 135 | "\n" 136 | return str 137 | } 138 | 139 | private compute (rope: LogootSRopes) { 140 | this.nodeNumber = this.recCompute(rope.root) 141 | 142 | this.nodeLengths = this.nodeLengths.sort((a, b) => { 143 | return a - b 144 | }) 145 | this.identifierLengths = this.identifierLengths.sort((a, b) => { 146 | return a - b 147 | }) 148 | 149 | this.nodesStats.mean /= this.nodeNumber 150 | const N = this.nodeLengths.length 151 | this.nodesStats.median = 152 | N % 2 === 0 153 | ? (this.nodeLengths[N / 2] + this.nodeLengths[N / 2 + 1]) / 2 154 | : this.nodeLengths[Math.ceil(N / 2)] 155 | 156 | this.identifiersStats.mean /= this.nodeNumber 157 | const M = this.identifierLengths.length 158 | this.identifiersStats.median = 159 | M % 2 === 0 160 | ? (this.identifierLengths[M / 2] + this.identifierLengths[M / 2 + 1]) / 2 161 | : this.identifierLengths[Math.ceil(M / 2)] 162 | } 163 | 164 | private recCompute (rope: RopesNodes | null): number { 165 | if (!rope) { 166 | return 0 167 | } 168 | 169 | // node stats 170 | 171 | const nLength = rope.length 172 | this.nodeLengths.push(nLength) 173 | this.nodesStats.max = 174 | this.nodesStats.max < nLength ? nLength : this.nodesStats.max 175 | this.nodesStats.min = 176 | this.nodesStats.min === -1 177 | ? nLength 178 | : this.nodesStats.min > nLength 179 | ? nLength 180 | : this.nodesStats.min 181 | this.nodesStats.mean += nLength 182 | if (this.nodesStats.lengthRepartition.has(nLength)) { 183 | const n = this.nodesStats.lengthRepartition.get(nLength) 184 | if (n) { 185 | this.nodesStats.lengthRepartition.set(nLength, n + 1) 186 | } 187 | } else { 188 | this.nodesStats.lengthRepartition.set(nLength, 1) 189 | } 190 | 191 | // identifier stats 192 | 193 | const iLength = rope.getIdBegin().length 194 | this.identifierLengths.push(iLength) 195 | this.identifiersStats.max = 196 | this.identifiersStats.max < iLength ? iLength : this.identifiersStats.max 197 | this.identifiersStats.min = 198 | this.identifiersStats.min === -1 199 | ? iLength 200 | : this.identifiersStats.min > iLength 201 | ? iLength 202 | : this.identifiersStats.min 203 | this.identifiersStats.mean += iLength 204 | if ( 205 | this.identifiersStats.lengthRepartition.has(iLength) && 206 | this.identifiersStats.lengthRepartition 207 | ) { 208 | const n = this.identifiersStats.lengthRepartition.get(iLength) 209 | if (n) { 210 | this.identifiersStats.lengthRepartition.set(iLength, n + 1) 211 | } 212 | } else { 213 | this.identifiersStats.lengthRepartition.set(iLength, 1) 214 | } 215 | 216 | return this.recCompute(rope.left) + 1 + this.recCompute(rope.right) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/textutils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | /** 21 | * Insert a string at a specific position in an existing string. 22 | * @param {string} aOriginal - the string in which to insert the new content. 23 | * @param {number} index - the position where to perform the insertion. 24 | * @param {string} string - the string to be inserted. 25 | * @return {string} the resulting string aOriginal[0:index]+string+aOriginal[index+1:aOriginal.length-1]. 26 | */ 27 | function insert (aOriginal: string, index: number, str: string): string { 28 | console.assert(Number.isSafeInteger(index), "index ∈ safe integer") 29 | 30 | const positiveIndex = Math.max(0, index) 31 | return aOriginal.slice(0, positiveIndex) + 32 | str + 33 | aOriginal.slice(positiveIndex) 34 | } 35 | 36 | /** 37 | * Remove a range of characters from a string. 38 | * @param {string} aOriginal - the string in which to insert the new content. 39 | * @param {number} begin - the beginning index of the range to be removed. 40 | * @param {number} end - the end index of the range to be removed. 41 | * @return {string} the resulting string aOriginal[0:begin]+aOriginal[end:aOriginal.length-1]. 42 | */ 43 | function del (aOriginal: string, begin: number, end: number): string { 44 | console.assert(Number.isSafeInteger(begin), "begin ∈ safe integer") 45 | console.assert(Number.isSafeInteger(end), "end ∈ safe integer") 46 | 47 | return aOriginal.slice(0, begin) + 48 | aOriginal.slice(end + 1) 49 | } 50 | 51 | /** 52 | * Compute the number of disjoint-occurence of a string within a string. 53 | * @param {string} string - the string in which to count occurences. 54 | * @param {string} substring - the substring to look for. 55 | * @return {number} the occurence count. 56 | */ 57 | function occurrences (str: string, substring: string): number { 58 | let result = 0 59 | const substringLength = substring.length 60 | 61 | let pos = str.indexOf(substring) 62 | while (pos !== -1) { 63 | result++ 64 | pos = str.indexOf(substring, pos + substringLength) 65 | } 66 | 67 | return result 68 | } 69 | 70 | export {insert, del, occurrences} 71 | -------------------------------------------------------------------------------- /test/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [package.json] 12 | indent_size = 2 13 | insert_final_newline = false 14 | 15 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "conaclos/esnext-style" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /test/epochstore.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import { ExecutionContext } from "ava" 22 | 23 | import { Epoch } from "../src/epoch/epoch" 24 | import { EpochId } from "../src/epoch/epochid" 25 | import { compareEpochFullIds, EpochStore } from "../src/epoch/epochstore" 26 | import { Ordering } from "../src/ordering" 27 | 28 | function generateEpoch (replicaNumber: number, epochNumber: number, parentEpoch: Epoch): Epoch { 29 | return new Epoch(new EpochId(replicaNumber, epochNumber), parentEpoch.id) 30 | } 31 | 32 | function generateEpochStore (): EpochStore { 33 | const origin = new Epoch(new EpochId(0, 0)) 34 | const epochA1 = generateEpoch(1, 1, origin) 35 | const epochA2 = generateEpoch(1, 2, epochA1) 36 | const epochB1 = generateEpoch(2, 1, origin) 37 | const epochB2 = generateEpoch(2, 2, epochB1) 38 | const epochA3 = generateEpoch(1, 3, epochB2) 39 | 40 | const epochs = [ 41 | epochA1, 42 | epochA2, 43 | epochB1, 44 | epochB2, 45 | epochA3, 46 | ] 47 | 48 | const expectedEpochStore = new EpochStore(origin) 49 | epochs.forEach((epoch) => expectedEpochStore.addEpoch(epoch)) 50 | return expectedEpochStore 51 | } 52 | 53 | test("epochStore-from-plain-factory", (t: ExecutionContext) => { 54 | const expectedEpochStore = generateEpochStore() 55 | 56 | const actualEpochStore = 57 | EpochStore.fromPlain(expectedEpochStore.toJSON()) 58 | 59 | if (actualEpochStore === null) { 60 | t.fail("The EpochStore should have been correctly instantiated") 61 | } else { 62 | t.deepEqual(actualEpochStore, expectedEpochStore) 63 | } 64 | }) 65 | 66 | test("compare-epoch-full-ids", (t) => { 67 | t.is(compareEpochFullIds([0, 0], [0, 0]), Ordering.Equal) 68 | 69 | t.is(compareEpochFullIds([0, 0], [0, 0, 1, 1]), Ordering.Less) 70 | t.is(compareEpochFullIds([0, 0, 1, 1], [0, 0]), Ordering.Greater) 71 | 72 | t.is(compareEpochFullIds([0, 0, 1, 1], [0, 0, 2, 1]), Ordering.Less) 73 | t.is(compareEpochFullIds([0, 0, 2, 1], [0, 0, 1, 1]), Ordering.Greater) 74 | 75 | t.is(compareEpochFullIds([0, 0], [0, 0, -1, 1]), Ordering.Less) 76 | t.is(compareEpochFullIds([0, 0, -1, 1], [0, 0]), Ordering.Greater) 77 | 78 | t.is(compareEpochFullIds([0, 0, -2, 1], [0, 0, -1, 1]), Ordering.Less) 79 | t.is(compareEpochFullIds([0, 0, -1, 1], [0, 0, -2, 1]), Ordering.Greater) 80 | }) 81 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import { Identifier } from "../src/identifier" 21 | import { IdentifierInterval } from "../src/identifierinterval" 22 | import { IdentifierTuple } from "../src/identifiertuple" 23 | 24 | export function idFactory (...values: number[]): Identifier { 25 | console.assert(values.length % 4 === 0, "values.length must be a multiple of 4") 26 | 27 | const groupedValues: Array<[number, number, number, number]> = [] 28 | for (let i = 0; i < values.length; i = i + 4) { 29 | groupedValues.push([ 30 | values[i], 31 | values[i + 1], 32 | values[i + 2], 33 | values[i + 3], 34 | ]) 35 | } 36 | const tuples: IdentifierTuple[] = 37 | groupedValues.map(([random, replicaNumber, clock, offset]: [number, number, number, number]) => { 38 | return new IdentifierTuple(random, replicaNumber, clock, offset) 39 | }) 40 | return new Identifier(tuples) 41 | } 42 | 43 | export function generateIdIntervalFactory (...values: number[]): (end: number) => IdentifierInterval { 44 | const id = idFactory(...values) 45 | return (end: number) => new IdentifierInterval(id, end) 46 | } 47 | 48 | export function generateStr (length: number): string { 49 | let text = "" 50 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 51 | 52 | for (let i = 0; i < length; i++) { 53 | text += possible.charAt(Math.floor(Math.random() * possible.length)) 54 | } 55 | 56 | return text 57 | } 58 | -------------------------------------------------------------------------------- /test/identifier.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {ExecutionContext} from "ava" 22 | 23 | import {Identifier} from "../src/identifier" 24 | import {IdentifierTuple} from "../src/identifiertuple" 25 | import { 26 | INT32_BOTTOM, 27 | INT32_TOP, 28 | } from "../src/int32" 29 | import {Ordering} from "../src/ordering" 30 | 31 | function equalsMacro ( 32 | t: ExecutionContext, 33 | id1: Identifier, id2: Identifier, expected: boolean): void { 34 | 35 | const actual = id1.equals(id2) 36 | t.is(actual, expected) 37 | } 38 | 39 | function equalsBaseMacro ( 40 | t: ExecutionContext, 41 | id1: Identifier, id2: Identifier, expected: boolean): void { 42 | 43 | const actual = id1.equalsBase(id2) 44 | t.is(actual, expected) 45 | } 46 | 47 | test("from-plain-factory", (t) => { 48 | const plainTuples = [{ 49 | random: 42, 50 | replicaNumber: 1, 51 | clock: 10, 52 | offset: -5, 53 | }, { 54 | random: 53, 55 | replicaNumber: 2, 56 | clock: 0, 57 | offset: 0, 58 | }] 59 | const plain = { 60 | tuples: plainTuples, 61 | } 62 | const id: Identifier | null = Identifier.fromPlain(plain) 63 | 64 | if (id === null) { 65 | t.fail("The identifier should have been correctly instantiated") 66 | } else { 67 | t.is(id.length, plain.tuples.length) 68 | 69 | id.tuples.forEach((actualTuple: IdentifierTuple, i: number) => { 70 | const expectedTuple = plain.tuples[i] 71 | 72 | t.is(actualTuple.random, expectedTuple.random) 73 | t.is(actualTuple.replicaNumber, expectedTuple.replicaNumber) 74 | t.is(actualTuple.clock, expectedTuple.clock) 75 | t.is(actualTuple.offset, expectedTuple.offset) 76 | }) 77 | } 78 | }) 79 | 80 | test("from-plain-factory-missing-property", (t) => { 81 | const plain = {} 82 | const id: Identifier | null = Identifier.fromPlain(plain) 83 | 84 | t.is(id, null) 85 | }) 86 | 87 | test("from-plain-factory-wrong-type", (t) => { 88 | const plain = { 89 | tuples: [1, 2, 3, 4], 90 | } 91 | const id: Identifier | null = Identifier.fromPlain(plain) 92 | 93 | t.is(id, null) 94 | }) 95 | 96 | const testTuple = (): void => { 97 | const tuple00 = new IdentifierTuple(0, 0, 0, 0) 98 | const tuple01 = new IdentifierTuple(0, 0, 0, 1) 99 | const tuple11 = new IdentifierTuple(0, 0, 1, 1) 100 | 101 | const id00 = new Identifier([tuple00]) 102 | const id00Twin = new Identifier([tuple00]) 103 | const id01 = new Identifier([tuple01]) 104 | const id11 = new Identifier([tuple11]) 105 | const id0001 = new Identifier([tuple00, tuple01]) 106 | const id0100 = new Identifier([tuple01, tuple00]) 107 | 108 | test("equals-twin", equalsMacro, id00, id00Twin, true) 109 | test("equals-same-length", equalsMacro, id00, id11, false) 110 | test("equals-different-length", equalsMacro, id00, id0001, false) 111 | 112 | test("equalsBase-twin", equalsBaseMacro, id00, id00Twin, true) 113 | test("equalsBase-same-base-same-length", equalsBaseMacro, id00, id01, true) 114 | test("equalsBase-different-base-same-length", equalsBaseMacro, id00, id11, false) 115 | test("equalsBase-is-prefix", equalsBaseMacro, id00, id0001, false) 116 | test("equalsBase-different-base-different-length", equalsBaseMacro, 117 | id0001, id0100, false) 118 | } 119 | testTuple() 120 | 121 | test("compare-to-last", (t) => { 122 | const id1 = new Identifier([new IdentifierTuple(0, 0, 0, 4)]) 123 | const id1Twin = new Identifier([new IdentifierTuple(0, 0, 0, 4)]) 124 | const id2 = new Identifier([new IdentifierTuple(0, 0, 0, 1)]) 125 | const id3 = new Identifier([new IdentifierTuple(0, 0, 0, 9)]) 126 | 127 | t.is(id1.compareTo(id1Twin), Ordering.Equal) 128 | t.not(id1.compareTo(id2), Ordering.Less) 129 | t.not(id1.compareTo(id3), Ordering.Greater) 130 | }) 131 | 132 | test("compare-to-base", (t) => { 133 | const tuple0: IdentifierTuple = new IdentifierTuple(0, 0, 0, 0) 134 | const tuple1: IdentifierTuple = new IdentifierTuple(1, 0, 0, 0) 135 | const tuple2: IdentifierTuple = new IdentifierTuple(2, 0, 0, 0) 136 | const id01 = new Identifier([tuple0, tuple1]) 137 | const id01Twin = new Identifier([tuple0, tuple1]) 138 | const id012 = new Identifier([tuple0, tuple1, tuple2]) 139 | const id0 = new Identifier([tuple0]) 140 | const id02 = new Identifier([tuple0, tuple2]) 141 | const id00 = new Identifier([tuple0, tuple0]) 142 | 143 | t.is(id01.compareTo(id01Twin), Ordering.Equal) 144 | t.is(id01.compareTo(id012), Ordering.Less) 145 | t.is(id01.compareTo(id0), Ordering.Greater) 146 | t.is(id01.compareTo(id02), Ordering.Less) 147 | t.is(id01.compareTo(id00), Ordering.Greater) 148 | }) 149 | 150 | test("hasPlaceAfter-max-last", (t) => { 151 | const tuple: IdentifierTuple = new IdentifierTuple(0, 0, 0, INT32_TOP - 1) 152 | const id = new Identifier([tuple]) 153 | 154 | t.true(id.hasPlaceAfter(1)) 155 | t.false(id.hasPlaceAfter(2)) 156 | }) 157 | 158 | test("hasPlaceBefore-min-last", (t) => { 159 | const tuple: IdentifierTuple = new IdentifierTuple(1, 0, 0, INT32_BOTTOM + 1) 160 | const id = new Identifier([tuple]) 161 | 162 | t.true(id.hasPlaceBefore(1)) 163 | t.false(id.hasPlaceBefore(2)) 164 | }) 165 | 166 | test("maxOffsetBeforeNext-same-base", (t) => { 167 | const tuple3: IdentifierTuple = new IdentifierTuple(0, 0, 0, 3) 168 | const tuple5: IdentifierTuple = new IdentifierTuple(0, 0, 0, 5) 169 | const id3 = new Identifier([tuple3]) 170 | const id5 = new Identifier([tuple5]) 171 | 172 | const expected = 4 173 | const actual = id3.maxOffsetBeforeNext(id5, 4) 174 | 175 | t.is(actual, expected) 176 | }) 177 | 178 | test("maxOffsetBeforeNext-base-is-prefix", (t) => { 179 | const tuple03: IdentifierTuple = new IdentifierTuple(0, 0, 0, 3) 180 | const tuple05: IdentifierTuple = new IdentifierTuple(0, 0, 0, 5) 181 | const tuple10: IdentifierTuple = new IdentifierTuple(0, 0, 1, 0) 182 | const id03 = new Identifier([tuple03]) 183 | const id0510 = new Identifier([tuple05, tuple10]) 184 | 185 | const expected = 5 186 | const actual = id03.maxOffsetBeforeNext(id0510, 10) 187 | 188 | t.is(actual, expected) 189 | }) 190 | 191 | test("minOffsetAfterPrev-same-base", (t) => { 192 | const tuple3: IdentifierTuple = new IdentifierTuple(0, 0, 0, 3) 193 | const tuple5: IdentifierTuple = new IdentifierTuple(0, 0, 0, 5) 194 | const id3 = new Identifier([tuple3]) 195 | const id5 = new Identifier([tuple5]) 196 | 197 | const expected = 4 198 | const actual = id5.minOffsetAfterPrev(id3, 4) 199 | 200 | t.is(actual, expected) 201 | }) 202 | 203 | test("minOffsetAfterPrev-base-is-prefix", (t) => { 204 | const tuple03: IdentifierTuple = new IdentifierTuple(0, 0, 0, 3) 205 | const tuple05: IdentifierTuple = new IdentifierTuple(0, 0, 0, 5) 206 | const tuple10: IdentifierTuple = new IdentifierTuple(0, 0, 1, 0) 207 | const id0310 = new Identifier([tuple03, tuple10]) 208 | const id05 = new Identifier([tuple05]) 209 | 210 | const expected = 4 211 | const actual = id05.minOffsetAfterPrev(id0310, 0) 212 | 213 | t.is(actual, expected) 214 | }) 215 | -------------------------------------------------------------------------------- /test/identifierinterval.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {ExecutionContext} from "ava" 22 | import {Identifier} from "../src/identifier.js" 23 | import {IdentifierInterval} from "../src/identifierinterval.js" 24 | import {IdentifierTuple} from "../src/identifiertuple.js" 25 | 26 | function generateIdInterval (begin: number, end: number): IdentifierInterval { 27 | const idBegin: Identifier = 28 | new Identifier([new IdentifierTuple(0, 0, 0, begin)]) 29 | return new IdentifierInterval(idBegin, 5) 30 | } 31 | 32 | test("from-plain-factory", (t) => { 33 | const begin = 0 34 | const end = 5 35 | const tuples: IdentifierTuple[] = 36 | [new IdentifierTuple(42, 1, 10, -5), new IdentifierTuple(53, 2, 0, begin)] 37 | const idBegin: Identifier = new Identifier(tuples) 38 | const plain = {idBegin, end} 39 | const idInterval: IdentifierInterval | null = IdentifierInterval.fromPlain(plain) 40 | 41 | if (idInterval === null) { 42 | t.fail("The identifier interval should have been correctly instantiated") 43 | } else { 44 | t.true(idInterval.idBegin.equals(plain.idBegin)) 45 | t.is(idInterval.begin, begin) 46 | t.is(idInterval.end, end) 47 | t.is(idInterval.length, end - begin + 1) 48 | } 49 | }) 50 | 51 | test("getBaseId", (t) => { 52 | const begin = 0 53 | const end = 5 54 | const idInterval: IdentifierInterval = generateIdInterval(begin, end) 55 | 56 | const offset = 2 57 | const expectedId: Identifier = 58 | Identifier.fromBase(idInterval.idBegin, offset) 59 | const actualId = idInterval.getBaseId(offset) 60 | 61 | t.true(actualId.equals(expectedId)) 62 | }) 63 | 64 | test("union-prepend-only", (t: ExecutionContext) => { 65 | const begin = 0 66 | const end = 5 67 | const idInterval = generateIdInterval(begin, end) 68 | 69 | const aBegin = -5 70 | const aEnd = 2 71 | const unionInterval = idInterval.union(aBegin, aEnd) 72 | 73 | const expectedBegin = aBegin 74 | const expectedEnd = end 75 | 76 | t.is(unionInterval.begin, expectedBegin) 77 | t.is(unionInterval.end, expectedEnd) 78 | }) 79 | 80 | test("union-append-only", (t: ExecutionContext) => { 81 | const begin = 0 82 | const end = 5 83 | const idInterval = generateIdInterval(begin, end) 84 | 85 | const aBegin = 2 86 | const aEnd = 10 87 | const unionInterval = idInterval.union(aBegin, aEnd) 88 | 89 | const expectedBegin = begin 90 | const expectedEnd = aEnd 91 | 92 | t.is(unionInterval.begin, expectedBegin) 93 | t.is(unionInterval.end, expectedEnd) 94 | }) 95 | 96 | test("union-prepend-append", (t: ExecutionContext) => { 97 | const begin = 0 98 | const end = 5 99 | const idInterval = generateIdInterval(begin, end) 100 | 101 | const aBegin = -5 102 | const aEnd = 10 103 | const unionInterval = idInterval.union(aBegin, aEnd) 104 | 105 | const expectedBegin = aBegin 106 | const expectedEnd = aEnd 107 | 108 | t.is(unionInterval.begin, expectedBegin) 109 | t.is(unionInterval.end, expectedEnd) 110 | }) 111 | 112 | test("union-no-changes", (t: ExecutionContext) => { 113 | const begin = 0 114 | const end = 5 115 | const idInterval = generateIdInterval(begin, end) 116 | 117 | const aBegin = 2 118 | const aEnd = 2 119 | const unionInterval = idInterval.union(aBegin, aEnd) 120 | 121 | const expectedBegin = begin 122 | const expectedEnd = end 123 | 124 | t.is(unionInterval.begin, expectedBegin) 125 | t.is(unionInterval.end, expectedEnd) 126 | }) 127 | -------------------------------------------------------------------------------- /test/identifiertuple.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {ExecutionContext} from "ava" 22 | import {IdentifierTuple} from "../src/identifiertuple" 23 | import {Ordering} from "../src/ordering" 24 | 25 | /** 26 | * Macro to check if compareTo() returns the expected result 27 | */ 28 | function compareTuplesMacro ( 29 | t: ExecutionContext, 30 | tuple: IdentifierTuple, other: IdentifierTuple, 31 | expected: Ordering): void { 32 | 33 | const actual: Ordering = tuple.compareTo(other) 34 | t.is(actual, expected) 35 | } 36 | 37 | /** 38 | * Macro to check if equalsBase() returns the expected result 39 | */ 40 | function equalsBaseMacro ( 41 | t: ExecutionContext, 42 | tuple: IdentifierTuple, other: IdentifierTuple, 43 | expected: boolean): void { 44 | 45 | const actual: boolean = tuple.equalsBase(other) 46 | t.is(actual, expected) 47 | } 48 | 49 | test("fromPlain", (t: ExecutionContext) => { 50 | const plain = { 51 | random: 42, 52 | replicaNumber: 1, 53 | clock: 10, 54 | offset: -5, 55 | } 56 | 57 | const tuple: IdentifierTuple | null = IdentifierTuple.fromPlain(plain) 58 | 59 | if (tuple === null) { 60 | t.fail("The identifier tuple should have been correctly instantiated") 61 | } else { 62 | t.is(tuple.random, plain.random) 63 | t.is(tuple.replicaNumber, plain.replicaNumber) 64 | t.is(tuple.clock, plain.clock) 65 | t.is(tuple.offset, plain.offset) 66 | } 67 | }) 68 | 69 | test("fromPlain-missing-property", (t: ExecutionContext) => { 70 | const plain = { 71 | replicaNumber: 1, 72 | clock: 10, 73 | offset: -5, 74 | } 75 | 76 | const tuple: IdentifierTuple | null = IdentifierTuple.fromPlain(plain) 77 | 78 | t.is(tuple, null) 79 | }) 80 | 81 | test("fromPlain-wrong-type", (t: ExecutionContext) => { 82 | const plain = { 83 | random: 42.7, 84 | replicaNumber: 1, 85 | clock: 10, 86 | offset: -5, 87 | } 88 | 89 | const tuple: IdentifierTuple | null = IdentifierTuple.fromPlain(plain) 90 | 91 | t.is(tuple, null) 92 | }) 93 | 94 | test("generateWithSameBase", (t: ExecutionContext) => { 95 | const expected = 5 96 | const tuple1: IdentifierTuple = new IdentifierTuple(42, 7, 8, 26) 97 | const tuple2: IdentifierTuple = IdentifierTuple.fromBase(tuple1, expected) 98 | 99 | t.true(tuple1.equalsBase(tuple2)) 100 | t.is(tuple2.offset, expected) 101 | }) 102 | 103 | const tuple0000: IdentifierTuple = new IdentifierTuple(0, 0, 0, 0) 104 | const tuple0001: IdentifierTuple = new IdentifierTuple(0, 0, 0, 1) 105 | const tuple0010: IdentifierTuple = new IdentifierTuple(0, 0, 1, 0) 106 | const tuple0100: IdentifierTuple = new IdentifierTuple(0, 1, 0, 0) 107 | const tuple1000: IdentifierTuple = new IdentifierTuple(1, 0, 0, 0) 108 | 109 | test("compareTo-tuple-less-other-1", compareTuplesMacro, tuple0000, tuple1000, Ordering.Less) 110 | test("compareTo-tuple-less-other-2", compareTuplesMacro, tuple0000, tuple0100, Ordering.Less) 111 | test("compareTo-tuple-less-other-3", compareTuplesMacro, tuple0000, tuple0010, Ordering.Less) 112 | test("compareTo-tuple-less-other-4", compareTuplesMacro, tuple0000, tuple0001, Ordering.Less) 113 | 114 | test("compareTo-tuple-equal-other", compareTuplesMacro, tuple0000, new IdentifierTuple(0, 0, 0, 0), Ordering.Equal) 115 | 116 | test("compareTo-tuple-greater-other-1", compareTuplesMacro, tuple1000, tuple0000, Ordering.Greater) 117 | test("compareTo-tuple-greater-other-2", compareTuplesMacro, tuple0100, tuple0000, Ordering.Greater) 118 | test("compareTo-tuple-greater-other-3", compareTuplesMacro, tuple0010, tuple0000, Ordering.Greater) 119 | test("compareTo-tuple-greater-other-4", compareTuplesMacro, tuple0001, tuple0000, Ordering.Greater) 120 | 121 | test("equalsBase-tuple-equal-other-1", equalsBaseMacro, tuple0000, new IdentifierTuple(0, 0, 0, 0), true) 122 | test("equalsBase-tuple-equal-other-2", equalsBaseMacro, tuple0000, tuple0001, true) 123 | 124 | test("equalsBase-tuple-different-other-1", equalsBaseMacro, tuple0000, tuple1000, false) 125 | test("equalsBase-tuple-different-other-2", equalsBaseMacro, tuple0000, tuple0100, false) 126 | test("equalsBase-tuple-different-other-3", equalsBaseMacro, tuple0000, tuple0010, false) 127 | 128 | test("asArray-properties-order", (t: ExecutionContext) => { 129 | const random = 42 130 | const replicaNumber = 1 131 | const clock = 10 132 | const offset = -5 133 | 134 | const tuple: IdentifierTuple = new IdentifierTuple(random, replicaNumber, clock, offset) 135 | 136 | const expected: number[] = [random, replicaNumber, clock, offset] 137 | const actual = tuple["asArray"]() // Hack to test this private function 138 | 139 | t.deepEqual(actual, expected) 140 | }) 141 | -------------------------------------------------------------------------------- /test/idfactory.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {ExecutionContext} from "ava" 22 | import {Identifier} from "../src/identifier.js" 23 | import {IdentifierTuple} from "../src/identifiertuple.js" 24 | import {Ordering} from "../src/ordering.js" 25 | 26 | import {createBetweenPosition, INT32_TOP_USER} from "../src/idfactory.js" 27 | import {idFactory} from "./helpers" 28 | 29 | test("two-noncontiguous-bases", (t: ExecutionContext) => { 30 | const replicaNumber = 0 31 | const clock = 1 32 | const id1: Identifier = new Identifier([new IdentifierTuple(0, replicaNumber, clock - 1, 0)]) 33 | const id2: Identifier = new Identifier([new IdentifierTuple(42, 0, 0, 0)]) 34 | 35 | const newId: Identifier = createBetweenPosition(id1, id2, replicaNumber, clock) 36 | 37 | t.is(id1.compareTo(newId), Ordering.Less) 38 | t.is(newId.compareTo(id2), Ordering.Less) 39 | }) 40 | 41 | test("two-contiguous-bases", (t: ExecutionContext) => { 42 | const replicaNumber = 0 43 | const clock = 1 44 | const tuple: IdentifierTuple = new IdentifierTuple(0, replicaNumber, clock - 1, 0) 45 | const id1: Identifier = new Identifier([tuple]) 46 | const id2: Identifier = new Identifier([new IdentifierTuple(1, 0, 0, 0)]) 47 | 48 | const newId: Identifier = createBetweenPosition(id1, id2, replicaNumber, clock) 49 | 50 | t.is(id1.compareTo(newId), Ordering.Less) 51 | t.is(newId.compareTo(id2), Ordering.Less) 52 | t.is(newId.length, id1.length + 1) 53 | t.true(id1.isPrefix(newId)) 54 | }) 55 | 56 | test("is-mine", (t: ExecutionContext) => { 57 | const replicaNumber = 0 58 | const clock = 1 59 | const newId: Identifier = createBetweenPosition(null, null, replicaNumber, clock) 60 | 61 | t.is(newId.generator, replicaNumber) 62 | }) 63 | 64 | test(`createBetweenPosition(id1, id2) generates id3 with smallest size when 65 | id1 = prefix + tail, 66 | id2 = prefix' + tail' 67 | and prefix'.random - prefix.random = 1`, (t) => { 68 | 69 | const id1 = idFactory(42, 42, 0, 0, 77, 77, 0, 0) 70 | const id2 = idFactory(43, 43, 0, 0, 23, 23, 0, 0) 71 | 72 | const id3 = createBetweenPosition(id1, id2, 100, 0) 73 | 74 | const expectedLength = 2 75 | const actualLength = id3.length 76 | t.is(actualLength, expectedLength) 77 | 78 | const [expectedFirstTuple, tuple2OfId1] = id1.tuples 79 | const [actualFirstTuple, tuple2OfId3] = id3.tuples 80 | t.is(actualFirstTuple, expectedFirstTuple) 81 | 82 | const expectedOrder = Ordering.Less 83 | const actualOrder = tuple2OfId1.compareTo(tuple2OfId3) 84 | t.is(actualOrder, expectedOrder) 85 | }) 86 | 87 | test(`createBetweenPosition(id1, id2) generates valid id3 when 88 | id1 = tuple11 + tuple12, 89 | id2 = successor(tuple11) + tuple22 and 90 | tuple22.random < tuple12.random`, (t) => { 91 | 92 | const id1 = idFactory(42, 42, 0, 0, 77, 77, 0, 0) 93 | const id2 = idFactory(42, 42, 0, 1, 23, 23, 0, 0) 94 | 95 | const id3 = createBetweenPosition(id1, id2, 100, 0) 96 | 97 | const expectedLength = 2 98 | const actualLength = id3.length 99 | t.is(actualLength, expectedLength) 100 | 101 | const [expectedFirstTuple, tuple2OfId1] = id1.tuples 102 | const [actualFirstTuple, tuple2OfId3] = id3.tuples 103 | t.is(actualFirstTuple, expectedFirstTuple) 104 | 105 | const expectedOrder = Ordering.Less 106 | const actualOrder = tuple2OfId1.compareTo(tuple2OfId3) 107 | t.is(actualOrder, expectedOrder) 108 | }) 109 | 110 | test(`createBetweenPosition(id1, id2) generates valid id3 when 111 | id1 = tuple11 + tuple12, 112 | id2 = successor(tuple11) and 113 | tuple12.random = INT32_TOP_USER - 1`, (t) => { 114 | 115 | const id1 = idFactory(42, 42, 0, 0, INT32_TOP_USER - 1, 77, 0, 0) 116 | const id2 = idFactory(42, 42, 0, 1) 117 | 118 | const id3 = createBetweenPosition(id1, id2, 100, 0) 119 | 120 | const expectedLength = 3 121 | const actualLength = id3.length 122 | t.is(actualLength, expectedLength) 123 | 124 | const [expectedFirstTuple, expectedSecondTuple] = id1.tuples 125 | const [actualFirstTuple, actualSecondTuple, tuple3OfId1] = id3.tuples 126 | t.is(actualFirstTuple, expectedFirstTuple) 127 | t.is(actualSecondTuple, expectedSecondTuple) 128 | }) 129 | -------------------------------------------------------------------------------- /test/int32.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {ExecutionContext} from "ava" 22 | 23 | import { 24 | INT32_BOTTOM, 25 | INT32_TOP, 26 | isInt32, 27 | randomInt32, 28 | } from "../src/int32.js" 29 | 30 | test("safe-integers-are-not-int32", (t: ExecutionContext) => { 31 | t.false(isInt32(Number.MIN_SAFE_INTEGER)) 32 | t.false(isInt32(INT32_BOTTOM - 1)) 33 | t.false(isInt32(INT32_TOP + 1)) 34 | t.false(isInt32(Number.MAX_SAFE_INTEGER)) 35 | }) 36 | 37 | test("int32-are-int32", (t: ExecutionContext) => { 38 | t.true(isInt32(INT32_BOTTOM)) 39 | t.true(isInt32(0)) 40 | t.true(isInt32(INT32_TOP)) 41 | }) 42 | 43 | test("float-are-not-int32", (t: ExecutionContext) => { 44 | t.false(isInt32(-1.2)) 45 | t.false(isInt32(0.1)) 46 | t.false(isInt32(1.2)) 47 | }) 48 | 49 | test("randomInt32-upper-bound-is-excluded", (t: ExecutionContext) => { 50 | // WARNING: No deterministric test (no seeded radom function) 51 | for (let i = 0; i < 100; i++) { 52 | t.is(randomInt32(0, 1), 0) 53 | t.is(randomInt32(-1, 0), -1) 54 | } 55 | }) 56 | 57 | test("randomInt32-in-interval", (t: ExecutionContext) => { 58 | // WARNING: No deterministric test (no seeded radom function) 59 | for (let i = 0; i < 100; i++) { 60 | const r = randomInt32(INT32_BOTTOM, INT32_TOP) 61 | t.true(isInt32(r) && r !== INT32_TOP) 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /test/iteratorhelperidentifier.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {ExecutionContext} from "ava" 22 | import {Identifier} from "../src/identifier" 23 | import {IdentifierInterval} from "../src/identifierinterval" 24 | import {IdentifierTuple} from "../src/identifiertuple" 25 | import { 26 | compareBase, 27 | IdentifierIteratorResults, 28 | } from "../src/iteratorhelperidentifier" 29 | 30 | function compareBaseMacro ( 31 | t: ExecutionContext, 32 | idInterval1: IdentifierInterval, idInterval2: IdentifierInterval, 33 | expected: IdentifierIteratorResults): void { 34 | 35 | const actual: IdentifierIteratorResults = 36 | compareBase(idInterval1, idInterval2) 37 | t.is(actual, expected) 38 | } 39 | 40 | const tuple00: IdentifierTuple = new IdentifierTuple(0, 0, 0, 0) 41 | const tuple04: IdentifierTuple = new IdentifierTuple(0, 0, 0, 4) 42 | const tuple90: IdentifierTuple = new IdentifierTuple(9, 0, 0, 0) 43 | 44 | const id00 = new Identifier([tuple00]) 45 | const id03 = Identifier.fromBase(id00, 3) 46 | const id0400 = new Identifier([tuple04, tuple00]) 47 | const id06 = Identifier.fromBase(id00, 6) 48 | const id07 = Identifier.fromBase(id00, 7) 49 | const id90 = new Identifier([tuple90]) 50 | const id96 = Identifier.fromBase(id90, 6) 51 | 52 | const id00To4: IdentifierInterval = new IdentifierInterval(id00, 4) 53 | const id00To5: IdentifierInterval = new IdentifierInterval(id00, 5) 54 | const id03To7: IdentifierInterval = new IdentifierInterval(id03, 7) 55 | const id06To10: IdentifierInterval = new IdentifierInterval(id06, 10) 56 | const id07To11: IdentifierInterval = new IdentifierInterval(id07, 11) 57 | 58 | const id90To5: IdentifierInterval = new IdentifierInterval(id90, 5) 59 | const id96To10: IdentifierInterval = new IdentifierInterval(id96, 10) 60 | 61 | const id0400To5: IdentifierInterval = 62 | new IdentifierInterval(id0400, 5) 63 | 64 | test("b1-before-b2-different-base", compareBaseMacro, id00To5, id90To5, 65 | IdentifierIteratorResults.B1_BEFORE_B2) 66 | test("b1-before-b2-same-base", compareBaseMacro, id00To5, id07To11, 67 | IdentifierIteratorResults.B1_BEFORE_B2) 68 | test("b1-before-b2-prefix", compareBaseMacro, id00To4, id0400To5, 69 | IdentifierIteratorResults.B1_BEFORE_B2) 70 | 71 | test("b1-after-b2-different-base", compareBaseMacro, id90To5, id00To5, 72 | IdentifierIteratorResults.B1_AFTER_B2) 73 | test("b1-after-b2-same-base", compareBaseMacro, id07To11, id00To5, 74 | IdentifierIteratorResults.B1_AFTER_B2) 75 | test("b1-after-b2-suffix", compareBaseMacro, id0400To5, id00To4, 76 | IdentifierIteratorResults.B1_AFTER_B2) 77 | 78 | test("b1-concat-b2", compareBaseMacro, id00To5, id06To10, 79 | IdentifierIteratorResults.B1_CONCAT_B2) 80 | test("b2-concat-b1", compareBaseMacro, id06To10, id00To5, 81 | IdentifierIteratorResults.B2_CONCAT_B1) 82 | 83 | test("b1-inside-b2", compareBaseMacro, id0400To5, id00To5, 84 | IdentifierIteratorResults.B1_INSIDE_B2) 85 | test("b2-inside-b1", compareBaseMacro, id00To5, id0400To5, 86 | IdentifierIteratorResults.B2_INSIDE_B1) 87 | 88 | test("b1-equals-b2", compareBaseMacro, id00To5, id00To5, 89 | IdentifierIteratorResults.B1_EQUALS_B2) 90 | 91 | test("b1-overlap-b2", compareBaseMacro, id00To5, id03To7, 92 | IdentifierIteratorResults.B1_EQUALS_B2) 93 | test("b1-included-in-b2", compareBaseMacro, id00To4, id00To5, 94 | IdentifierIteratorResults.B1_EQUALS_B2) 95 | test("b2-included-in-b1", compareBaseMacro, id00To5, id00To4, 96 | IdentifierIteratorResults.B1_EQUALS_B2) 97 | -------------------------------------------------------------------------------- /test/logs.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {ExecutionContext} from "ava" 22 | import * as fs from "fs" 23 | 24 | import {LogootSRopes} from "../src/logootsropes.js" 25 | import {LogootSDel} from "../src/operations/delete/logootsdel.js" 26 | import {LogootSAdd} from "../src/operations/insert/logootsadd.js" 27 | import {LogootSOperation} from "../src/operations/logootsoperation.js" 28 | 29 | /** 30 | * Check if every element of the array provided is equals to the expected value 31 | * 32 | * @param {T[]} inputs - An array 33 | * @param {any} expected - The expected value of each element 34 | */ 35 | function everyEqualsTo (inputs: T[], expected: T): boolean { 36 | return inputs.every((input) => input === expected) 37 | } 38 | 39 | /** 40 | * Macro to check if all logs from a provided set converge 41 | * 42 | * @param {ExecutionContext} t - The test tool, provided by Ava 43 | * @param {string[]} logFiles - The set of files containing the logs to compare 44 | * @param {boolean} expected - Should the logs converge or not 45 | */ 46 | function everyLogsConvergeMacro (t: ExecutionContext, logFiles: string[], expected: boolean): void { 47 | const logs: LogootSOperation[][] = [] 48 | 49 | // Retrieve operation logs from files 50 | logFiles.forEach((file) => { 51 | const data = fs.readFileSync(file, "utf8") 52 | const log = JSON.parse(data) 53 | 54 | // Has to set explicitly the type of richLogootSOps 55 | // so that TypeScript does its job and infers the return type of richLogootSOps.map(...) 56 | const richLogootSOps: unknown = log.richLogootSOps 57 | 58 | if (richLogootSOps instanceof Array && richLogootSOps.length > 0) { 59 | let isOk = true 60 | let i = 0 61 | 62 | const logootSOps: LogootSOperation[] = [] 63 | 64 | while (isOk && i < richLogootSOps.length) { 65 | const plainLogootSOp: unknown = { 66 | ...richLogootSOps[i].logootSOp, 67 | author: richLogootSOps[i].id, 68 | } 69 | let logootSOp: LogootSOperation | null = LogootSDel.fromPlain(plainLogootSOp) 70 | if (logootSOp === null) { 71 | logootSOp = LogootSAdd.fromPlain(plainLogootSOp) 72 | } 73 | if (logootSOp === null) { 74 | isOk = false 75 | } else { 76 | logootSOps.push(logootSOp) 77 | } 78 | i++ 79 | } 80 | 81 | if (isOk) { 82 | logs.push(logootSOps) 83 | } else { 84 | t.fail("the log must contains only valid logoots operations") 85 | } 86 | } else { 87 | t.fail("the log must not be empty") 88 | } 89 | }) 90 | const docs: LogootSRopes[] = [] 91 | let j = 0 92 | 93 | // Replay operation logs 94 | logs.forEach((log) => { 95 | const doc = new LogootSRopes(j) 96 | 97 | log.forEach((logootSOp) => { 98 | logootSOp.execute(doc) 99 | }) 100 | 101 | docs.push(doc) 102 | j++ 103 | }) 104 | 105 | const actualDigests = docs.map((doc) => doc.digest()) 106 | const expectedDigest = actualDigests[0] 107 | 108 | const actualStrings = docs.map((doc) => doc.str) 109 | const expectedString = actualStrings[0] 110 | 111 | t.is(everyEqualsTo(actualDigests, expectedDigest), expected) 112 | t.is(everyEqualsTo(actualStrings, expectedString), expected) 113 | } 114 | 115 | function testConvergentLogs ( 116 | testName: string, 117 | logsPath: string, logName: string, users: string[]): void { 118 | 119 | const logsSet = users.map((user) => `${logsPath}/${logName}-${user}.json`) 120 | test(testName, everyLogsConvergeMacro, logsSet, true) 121 | } 122 | 123 | function testDivergentLogs ( 124 | testName: string, 125 | logsPath: string, logName: string, users: string[]): void { 126 | 127 | const logsSet = users.map((user) => `${logsPath}/${logName}-${user}.json`) 128 | test(testName, everyLogsConvergeMacro, logsSet, false) 129 | } 130 | 131 | testConvergentLogs( 132 | "convergent-logs-AKj-set", 133 | "logs/logs-AKjlI6j4yD", 134 | "log-AKjlI6j4yD-combat-salary-clara", 135 | ["1", "2", "3"]) 136 | 137 | testConvergentLogs( 138 | "convergent-logs-4u0-set", 139 | "logs/logs-4u0HUYhI3a", 140 | "log-4u0HUYhI3a-amigo-pilot-verbal", 141 | ["1", "2", "3"]) 142 | 143 | testConvergentLogs( 144 | "convergent-logs-iWY-set", 145 | "logs/logs-iWYksvoqJo", 146 | "log-iWYksvoqJo-holiday-field-summer", 147 | ["1", "2", "3"]) 148 | 149 | testConvergentLogs( 150 | "convergent-logs-20170117-test1-set", 151 | "logs/logs-20170117-test1", 152 | "log-20170117-test1-village-august-immune", 153 | ["1", "2"]) 154 | 155 | testConvergentLogs( 156 | "convergent-logs-20170117-test2-set", 157 | "logs/logs-20170117-test2", 158 | "log-20170117-test2-galileo-camel-motor", 159 | ["1", "2"]) 160 | 161 | testDivergentLogs( 162 | "convergent-logs-20170117-test3-set", 163 | "logs/logs-20170117-test3", 164 | "log-20170117-test3", 165 | ["galileo-camel-motor", "village-august-immune"]) 166 | 167 | testDivergentLogs( 168 | "convergent-logs-20170117-test4-set", 169 | "logs/logs-20170117-test4", 170 | "log-20170117-test4", 171 | ["nobel-letter-neutral", "telex-lobby-sweet"]) 172 | 173 | testDivergentLogs( 174 | "convergent-logs-20170117-test5-set", 175 | "logs/logs-20170117-test5", 176 | "log-20170117-test5", 177 | ["banana-oxygen-pilgrim", "epoxy-tango-engine", "jasmine-bravo-vital"]) 178 | 179 | testDivergentLogs( 180 | "divergent-logs-ct20171019-set", 181 | "logs/logs-ct20171019", 182 | "log-ct20171019", 183 | ["join-iris-berlin", "ocean-button-contour"]) 184 | 185 | testDivergentLogs( 186 | "divergent-logs-quT-set", 187 | "logs/logs-quTF5eAc", 188 | "log-quTF5eAc", 189 | ["harbor-royal-explain", "number-speed-metal", "prelude-chris-tourist"]) 190 | 191 | testDivergentLogs( 192 | "divergent-logs-rXF-set-1", 193 | "logs/logs-rXFbhTf8Ct", 194 | "log-rXFbhTf8Ct", 195 | ["original-log", "incorrect-operations-order"]) 196 | 197 | testDivergentLogs( 198 | "divergent-logs-rXF-set-2", 199 | "logs/logs-rXFbhTf8Ct", 200 | "log-rXFbhTf8Ct", 201 | ["original-log", "additional-operations"]) 202 | -------------------------------------------------------------------------------- /test/renamingmapstore.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import { ExecutionContext } from "ava" 22 | 23 | import { RenamingMapStore } from "../src/renamingmap/renamingmapstore" 24 | import { generateRenamableReplicableList } from "./renamablereplicablelist.test" 25 | 26 | function generateRenamingMapStore (): RenamingMapStore { 27 | return generateRenamableReplicableList().renamingMapStore 28 | } 29 | 30 | test("renamingMapStore-from-plain-factory", (t: ExecutionContext) => { 31 | const expectedRenamingMapStore = generateRenamingMapStore() 32 | 33 | const actualRenamingMapStore = 34 | RenamingMapStore.fromPlain(expectedRenamingMapStore.toJSON()) 35 | 36 | if (actualRenamingMapStore === null) { 37 | t.fail("The RenamingMapStore should have been correctly instantiated") 38 | } else { 39 | t.deepEqual(actualRenamingMapStore, expectedRenamingMapStore) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /test/ropesnodes.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import {Identifier} from "../src/identifier.js" 22 | import {IdentifierInterval} from "../src/identifierinterval.js" 23 | import {IdentifierTuple} from "../src/identifiertuple.js" 24 | import {LogootSBlock} from "../src/logootsblock.js" 25 | import {RopesNodes} from "../src/ropesnodes.js" 26 | 27 | test("matching-linear-representation", (t) => { 28 | const tuple03 = new IdentifierTuple(0, 0, 0, 3) 29 | const tuple50 = new IdentifierTuple(5, 0, 0, 0) 30 | const tuple80 = new IdentifierTuple(8, 0, 0, 0) 31 | 32 | const id03 = new Identifier([tuple03]) 33 | const id50 = new Identifier([tuple50]) 34 | const id5080 = new Identifier([tuple50, tuple80]) 35 | 36 | const idi1 = new IdentifierInterval(id03, 5) 37 | const idi2 = new IdentifierInterval(id50, 5) 38 | const idi3 = new IdentifierInterval(id5080, 5) 39 | 40 | const block1 = new LogootSBlock(idi1, 5) 41 | const block2 = new LogootSBlock(idi2, 5) 42 | const block3 = new LogootSBlock(idi3, 5) 43 | const tree1 = new RopesNodes(block2, 0, 5, 44 | RopesNodes.leaf(block1, 0, 5), 45 | RopesNodes.leaf(block3, 0, 5)) 46 | const tree2 = new RopesNodes(block1, 0, 5, null, 47 | new RopesNodes(block2, 0, 5, null, 48 | RopesNodes.leaf(block3, 0, 5))) 49 | const tree3 = new RopesNodes(block3, 0, 5, 50 | new RopesNodes(block2, 0, 5, 51 | RopesNodes.leaf(block1, 0, 5), null), null) 52 | const tree4 = new RopesNodes(block3, 0, 5, 53 | new RopesNodes(block1, 0, 5, null, 54 | RopesNodes.leaf(block2, 0, 5)), null) 55 | const tree5 = new RopesNodes(block1, 0, 5, null, 56 | new RopesNodes(block3, 0, 5, 57 | RopesNodes.leaf(block2, 0, 5), null)) 58 | const list1 = tree1.toList() 59 | const list2 = tree2.toList() 60 | const list3 = tree3.toList() 61 | const list4 = tree4.toList() 62 | const list5 = tree5.toList() 63 | 64 | t.deepEqual(list2, list1) 65 | t.deepEqual(list3, list1) 66 | t.deepEqual(list4, list1) 67 | t.deepEqual(list5, list1) 68 | }) 69 | -------------------------------------------------------------------------------- /test/tree.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of MUTE-structs. 3 | 4 | Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | import test from "ava" 21 | import * as fs from "fs" 22 | 23 | import {LogootSRopes} from "../src/logootsropes.js" 24 | 25 | function getLogootSRopesFromTree (file: string): LogootSRopes | null { 26 | const data = fs.readFileSync(file, "utf8") 27 | const tree = JSON.parse(data) 28 | 29 | if (tree !== null && typeof tree === "object") { 30 | return LogootSRopes.fromPlain(tree) 31 | } 32 | return null 33 | } 34 | 35 | test.failing("non-convergent-balanced-trees-different-digests", (t) => { 36 | const docs: LogootSRopes[] = [] 37 | 38 | const files = [ 39 | "trees/trees-nct/tree-nct-nikita-button-shirt-1.json", 40 | "trees/trees-nct/tree-nct-nikita-button-shirt-2.json", 41 | ] 42 | 43 | files.forEach((file) => { 44 | const doc: LogootSRopes | null = getLogootSRopesFromTree(file) 45 | 46 | if (doc !== null) { 47 | docs.push(doc) 48 | } else { 49 | t.fail("the file must contains a valid serialization of a LogootSRopes") 50 | } 51 | }) 52 | 53 | const digests: number [] = docs.map((doc: LogootSRopes) => doc.digest()) 54 | const allDifferents: boolean = digests.every((digest, index) => { 55 | return digests.every((otherDigest, otherIndex) => { 56 | return index === otherIndex || digest !== otherDigest 57 | }) 58 | }) 59 | 60 | t.true(allDifferents) 61 | }) 62 | -------------------------------------------------------------------------------- /test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../tslint.json" 4 | ], 5 | "rules": { 6 | "no-string-literal": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /trees/trees-nct/tree-nct-nikita-button-shirt-1.json: -------------------------------------------------------------------------------- 1 | {"replicaNumber":-35094020,"clock":0,"root":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0}]},"end":1},"nbElement":2,"mine":false},"actualBegin":1,"length":1,"left":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0}]},"end":1},"nbElement":2,"mine":false},"actualBegin":0,"length":1,"left":null,"right":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0},{"random":420701825,"replicaNumber":1275885185,"clock":1,"offset":0}]},"end":0},"nbElement":1,"mine":false},"actualBegin":0,"length":1,"left":null,"right":null,"height":1,"sizeNodeAndChildren":1},"height":2,"sizeNodeAndChildren":2},"right":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":2095866607,"replicaNumber":1275885185,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":false},"actualBegin":0,"length":1,"left":null,"right":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":2101046630,"replicaNumber":1370625671,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":false},"actualBegin":0,"length":1,"left":null,"right":null,"height":1,"sizeNodeAndChildren":1},"height":2,"sizeNodeAndChildren":2},"height":3,"sizeNodeAndChildren":5},"str":"ab\ncd","mapBaseToBlock":{"1071687779,-1113885304,0":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0}]},"end":1},"nbElement":2,"mine":false},"2095866607,1275885185,0":{"idInterval":{"idBegin":{"tuples":[{"random":2095866607,"replicaNumber":1275885185,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":false},"1071687779,-1113885304,0,0,420701825,1275885185,1":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0},{"random":420701825,"replicaNumber":1275885185,"clock":1,"offset":0}]},"end":0},"nbElement":1,"mine":false},"2101046630,1370625671,0":{"idInterval":{"idBegin":{"tuples":[{"random":2101046630,"replicaNumber":1370625671,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":false}}} -------------------------------------------------------------------------------- /trees/trees-nct/tree-nct-nikita-button-shirt-2.json: -------------------------------------------------------------------------------- 1 | {"replicaNumber":1370625671,"clock":1,"root":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":2095866607,"replicaNumber":1275885185,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":false},"actualBegin":0,"length":1,"left":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0},{"random":420701825,"replicaNumber":1275885185,"clock":1,"offset":0}]},"end":0},"nbElement":1,"mine":false},"actualBegin":0,"length":1,"left":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0}]},"end":1},"nbElement":2,"mine":false},"actualBegin":0,"length":1,"left":null,"right":null,"height":1,"sizeNodeAndChildren":1},"right":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0}]},"end":1},"nbElement":2,"mine":false},"actualBegin":1,"length":1,"left":null,"right":null,"height":1,"sizeNodeAndChildren":1},"height":2,"sizeNodeAndChildren":3},"right":{"block":{"idInterval":{"idBegin":{"tuples":[{"random":2101046630,"replicaNumber":1370625671,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":true},"actualBegin":0,"length":1,"left":null,"right":null,"height":1,"sizeNodeAndChildren":1},"height":3,"sizeNodeAndChildren":5},"str":"ab\ncd","mapBaseToBlock":{"1071687779,-1113885304,0":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0}]},"end":1},"nbElement":2,"mine":false},"2095866607,1275885185,0":{"idInterval":{"idBegin":{"tuples":[{"random":2095866607,"replicaNumber":1275885185,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":false},"2101046630,1370625671,0":{"idInterval":{"idBegin":{"tuples":[{"random":2101046630,"replicaNumber":1370625671,"clock":0,"offset":0}]},"end":0},"nbElement":1,"mine":true},"1071687779,-1113885304,0,0,420701825,1275885185,1":{"idInterval":{"idBegin":{"tuples":[{"random":1071687779,"replicaNumber":-1113885304,"clock":0,"offset":0},{"random":420701825,"replicaNumber":1275885185,"clock":1,"offset":0}]},"end":0},"nbElement":1,"mine":false}}} -------------------------------------------------------------------------------- /tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.module.json", 3 | "compilerOptions": { 4 | "target": "ES6", 5 | 6 | "outDir": "./dist/es2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2019"], 4 | "downlevelIteration": true, 5 | "target": "ES5", 6 | "sourceMap": true, 7 | "stripInternal": true, 8 | "resolveJsonModule": true, 9 | 10 | "outDir": "./.tested", 11 | 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "strict": true, 16 | 17 | "pretty": true 18 | }, 19 | "include": ["src/**/*.ts", "test/**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/main" 5 | }, 6 | "include": ["src/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES6", 5 | "moduleResolution": "Node", 6 | 7 | "outDir": "./dist/module" 8 | }, 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | 7 | "outDir": "./dist/types" 8 | }, 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-eslint-rules", 4 | "tslint:recommended" 5 | ], 6 | "rules": { 7 | "no-console": false, 8 | "no-var-requires": false, 9 | "max-classes-per-file": false, 10 | "member-access": false, 11 | "no-default-export": true, 12 | "no-empty": false, 13 | "no-switch-case-fall-through": true, 14 | "object-literal-sort-keys": false, 15 | "quotemark": [ 16 | true, 17 | "double" 18 | ], 19 | "semicolon": [ 20 | true, 21 | "never" 22 | ], 23 | "space-before-function-paren": true, 24 | "ter-indent": [true, 4], 25 | "interface-name": false, 26 | "no-shadowed-variable": true, 27 | "no-bitwise": false 28 | } 29 | } 30 | --------------------------------------------------------------------------------