├── .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 | [](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 |
--------------------------------------------------------------------------------