├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── index.js ├── lib ├── document-tree.js ├── document.js ├── point-helpers.js ├── serialization.js ├── splay-tree.js ├── split-tree.js └── teletype-crdt_pb.js ├── package-lock.json ├── package.json ├── script ├── electron-test-runner │ ├── index.html │ ├── main.js │ ├── package.json │ └── renderer.js └── test ├── teletype-crdt.proto └── test ├── document.test.js ├── helpers ├── local-document.js ├── peer.js ├── random.js └── words.js └── serialization.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "8" 5 | 6 | notifications: 7 | email: 8 | on_success: never 9 | on_failure: change 10 | slack: 11 | on_success: change 12 | rooms: 13 | - secure: mkxeBEUqnis57DyGzzH1gpbEsSDdmYH0wWvl/kdUemBd3nhUVdTnkceBCdHLsKixd5ZInt7yaxgqpIxS+CpNKOH97ZPwl7LAquPikVcJ+ol9ppds3WB/dX9oJaP/5gD/CdMZASB+guFhnj2CUuVZKS+HAEtYzTXi5iYUAS7Vn9kKWWnTrc1PZUNhu3wvXuDB+ODM3ZaZ4/prux1NBLK26RKTyQI25517MY8ZdcqF8tYfmqoD05SfL73yZphp5Dn8e6CL2FYRQLKgtbmgJf0zC9DWpmo/YSlNZeBuloVbQdRCyhrltbd+ftzWGuxe3HbxG8ZQ+RFjoS/w+XDc2LZP2z3tafPoSJ7kAAbM66NAqNzQB2cULlkimoPx2K1gc7aGXUh4X5cWSvvX98hSi68O45oOm0XlFBB4wNJUqQUUp48u83EvS1KmdYzTFaP+0tlYMsBkEfw5EdaFxkzMNm1WDcVGf9LA8PwtoOmuHXV10Wd81oCvy8utNoDbC5bCgRWTPVzUDFeFExUhle6Q7YC/fHnGMK3AMRQmplt/zkR+21EIS9X78DiYegaOAFMyAf2QyaX7Yon9354XvvG0eMQkADSMBW5DzlnPqD0/ebmtfpUf6oIjh6LIYpbD5Ff5Akca5BHzb19Q/2o6pUbDZlkmeuokNzXCnRYknhbGeNH91Yg= 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 GitHub 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # teletype-crdt 3 | 4 | The string-wise sequence CRDT powering peer-to-peer collaborative editing in [Teletype for Atom](https://github.com/atom/teletype). 5 | 6 | ## Hacking 7 | 8 | After cloning this repository, you can install its dependencies by running: 9 | 10 | ``` 11 | npm install 12 | ``` 13 | 14 | And then run tests via: 15 | 16 | ``` 17 | npm test 18 | ``` 19 | 20 | ## Background 21 | 22 | For more details on the techniques used for this data structure, we recommend reading the following papers: 23 | 24 | * [Data consistency for P2P Collaborative Editing](https://doi.org/10.1145/1180875.1180916) 25 | * [Supporting String-Wise Operations and Selective Undo for Peer-to-Peer Group Editing](https://doi.org/10.1145/2660398.2660401) 26 | * [High Responsiveness for Group Editing CRDTs](https://doi.org/10.1145/2957276.2957300) 27 | 28 | ## TODO 29 | 30 | * [ ] Document APIs 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Document = require('./lib/document') 2 | const { 3 | serializeOperation, deserializeOperation, 4 | serializeRemotePosition, deserializeRemotePosition 5 | } = require('./lib/serialization') 6 | 7 | module.exports = { 8 | Document, 9 | serializeOperation, deserializeOperation, 10 | serializeRemotePosition, deserializeRemotePosition 11 | } 12 | -------------------------------------------------------------------------------- /lib/document-tree.js: -------------------------------------------------------------------------------- 1 | const SplayTree = require('./splay-tree') 2 | const {ZERO_POINT, compare, traverse} = require('./point-helpers') 3 | 4 | module.exports = 5 | class DocumentTree extends SplayTree { 6 | constructor (firstSegment, lastSegment, isSegmentVisible) { 7 | super() 8 | this.firstSegment = firstSegment 9 | this.firstSegment.documentRight = lastSegment 10 | this.firstSegment.documentRight.documentParent = this.firstSegment 11 | this.firstSegment.documentLeft = null 12 | this.firstSegment.documentSubtreeExtent = ZERO_POINT 13 | lastSegment.documentSubtreeExtent = ZERO_POINT 14 | this.root = this.firstSegment 15 | this.isSegmentVisible = isSegmentVisible 16 | } 17 | 18 | getSegmentIndex (segment) { 19 | let index = segment.documentLeft ? segment.documentLeft.documentSubtreeSize : 0 20 | 21 | while (segment.documentParent) { 22 | if (segment.documentParent.documentRight === segment) { 23 | index++ 24 | if (segment.documentParent.documentLeft) { 25 | index += segment.documentParent.documentLeft.documentSubtreeSize 26 | } 27 | } 28 | segment = segment.documentParent 29 | } 30 | 31 | return index 32 | } 33 | 34 | getParent (node) { 35 | return node.documentParent 36 | } 37 | 38 | setParent (node, value) { 39 | node.documentParent = value 40 | } 41 | 42 | getLeft (node) { 43 | return node.documentLeft 44 | } 45 | 46 | setLeft (node, value) { 47 | node.documentLeft = value 48 | } 49 | 50 | getRight (node) { 51 | return node.documentRight 52 | } 53 | 54 | setRight (node, value) { 55 | node.documentRight = value 56 | } 57 | 58 | findSegmentContainingPosition (position) { 59 | let segment = this.root 60 | let leftAncestorEnd = ZERO_POINT 61 | while (segment) { 62 | let start = leftAncestorEnd 63 | if (segment.documentLeft) start = traverse(start, segment.documentLeft.documentSubtreeExtent) 64 | let end = start 65 | if (this.isSegmentVisible(segment)) end = traverse(end, segment.extent) 66 | 67 | if (compare(position, start) <= 0 && segment !== this.firstSegment) { 68 | segment = segment.documentLeft 69 | } else if (compare(position, end) > 0) { 70 | leftAncestorEnd = end 71 | segment = segment.documentRight 72 | } else { 73 | return {segment, start, end} 74 | } 75 | } 76 | 77 | throw new Error('No segment found') 78 | } 79 | 80 | insertBetween (prev, next, newSegment) { 81 | this.splayNode(prev) 82 | this.splayNode(next) 83 | this.root = newSegment 84 | newSegment.documentLeft = prev 85 | prev.documentParent = newSegment 86 | newSegment.documentRight = next 87 | next.documentParent = newSegment 88 | next.documentLeft = null 89 | this.updateSubtreeExtent(next) 90 | this.updateSubtreeExtent(newSegment) 91 | } 92 | 93 | splitSegment (prefix, suffix) { 94 | this.splayNode(prefix) 95 | 96 | this.root = suffix 97 | suffix.documentParent = null 98 | suffix.documentLeft = prefix 99 | prefix.documentParent = suffix 100 | suffix.documentRight = prefix.documentRight 101 | if (suffix.documentRight) suffix.documentRight.documentParent = suffix 102 | prefix.documentRight = null 103 | 104 | this.updateSubtreeExtent(prefix) 105 | this.updateSubtreeExtent(suffix) 106 | } 107 | 108 | updateSubtreeExtent (node, undoCountOverrides) { 109 | node.documentSubtreeExtent = ZERO_POINT 110 | node.documentSubtreeSize = 1 111 | if (node.documentLeft) { 112 | node.documentSubtreeExtent = traverse(node.documentSubtreeExtent, node.documentLeft.documentSubtreeExtent) 113 | node.documentSubtreeSize += node.documentLeft.documentSubtreeSize 114 | } 115 | if (this.isSegmentVisible(node, undoCountOverrides)) { 116 | node.documentSubtreeExtent = traverse(node.documentSubtreeExtent, node.extent) 117 | } 118 | if (node.documentRight) { 119 | node.documentSubtreeExtent = traverse(node.documentSubtreeExtent, node.documentRight.documentSubtreeExtent) 120 | node.documentSubtreeSize += node.documentRight.documentSubtreeSize 121 | } 122 | } 123 | 124 | getSegmentPosition (segment) { 125 | this.splayNode(segment) 126 | if (segment.documentLeft) { 127 | return segment.documentLeft.documentSubtreeExtent 128 | } else { 129 | return ZERO_POINT 130 | } 131 | } 132 | 133 | getSegments () { 134 | const treeSegments = [] 135 | function visitTreeInOrder (node) { 136 | if (node.documentLeft) visitTreeInOrder(node.documentLeft) 137 | treeSegments.push(node) 138 | if (node.documentRight) visitTreeInOrder(node.documentRight) 139 | } 140 | visitTreeInOrder(this.root) 141 | return treeSegments 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/document.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const DocumentTree = require('./document-tree') 3 | const SplitTree = require('./split-tree') 4 | const {ZERO_POINT, compare, traverse, traversal, extentForText} = require('./point-helpers') 5 | 6 | module.exports = 7 | class Document { 8 | constructor ({siteId, text, history}) { 9 | assert(siteId !== 0, 'siteId 0 is reserved') 10 | this.siteId = siteId 11 | this.nextSequenceNumber = 1 12 | this.splitTreesBySpliceId = new Map() 13 | this.deletionsBySpliceId = new Map() 14 | this.undoCountsBySpliceId = new Map() 15 | this.markerLayersBySiteId = new Map([[this.siteId, new Map()]]) 16 | this.deferredOperationsByDependencyId = new Map() 17 | this.deferredResolutionsByDependencyId = new Map() 18 | this.deferredMarkerUpdates = new Map() 19 | this.deferredMarkerUpdatesByDependencyId = new Map() 20 | this.maxSeqsBySite = {} 21 | this.operations = [] 22 | this.undoStack = [] 23 | this.redoStack = [] 24 | this.nextCheckpointId = 1 25 | 26 | const firstSegment = {spliceId: {site: 0, seq: 0}, offset: ZERO_POINT, text: '', extent: ZERO_POINT, nextSplit: null, deletions: new Set()} 27 | this.splitTreesBySpliceId.set(spliceIdToString(firstSegment.spliceId), new SplitTree(firstSegment)) 28 | 29 | const lastSegment = {spliceId: {site: 0, seq: 1}, offset: ZERO_POINT, text: '', extent: ZERO_POINT, nextSplit: null, deletions: new Set()} 30 | this.splitTreesBySpliceId.set(spliceIdToString(lastSegment.spliceId), new SplitTree(lastSegment)) 31 | 32 | this.documentTree = new DocumentTree( 33 | firstSegment, 34 | lastSegment, 35 | this.isSegmentVisible.bind(this) 36 | ) 37 | 38 | if (text) { 39 | this.setTextInRange(ZERO_POINT, ZERO_POINT, text) 40 | this.undoStack.length = 0 41 | } else if (history) { 42 | this.populateHistory(history) 43 | } 44 | } 45 | 46 | populateHistory ({baseText, nextCheckpointId, undoStack, redoStack}) { 47 | this.setTextInRange(ZERO_POINT, ZERO_POINT, baseText) 48 | this.nextCheckpointId = nextCheckpointId 49 | 50 | const newUndoStack = [] 51 | const allEntries = undoStack.concat(redoStack.slice().reverse()) 52 | for (let i = 0; i < allEntries.length; i++) { 53 | const {type, changes, markersBefore, markersAfter, id, markers} = allEntries[i] 54 | if (type === 'transaction') { 55 | const operations = [] 56 | const markersSnapshotBefore = this.snapshotFromMarkers(markersBefore) 57 | for (let j = changes.length - 1; j >= 0; j--) { 58 | const {oldStart, oldEnd, newText} = changes[j] 59 | operations.push(...this.setTextInRange(oldStart, oldEnd, newText)) 60 | } 61 | const markersSnapshotAfter = this.snapshotFromMarkers(markersAfter) 62 | newUndoStack.push(new Transaction(0, operations, markersSnapshotBefore, markersSnapshotAfter)) 63 | } else if (type === 'checkpoint') { 64 | newUndoStack.push(new Checkpoint(id, false, this.snapshotFromMarkers(markers))) 65 | } else { 66 | throw new Error(`Unknown entry type '${type}'`) 67 | } 68 | } 69 | 70 | this.undoStack = newUndoStack 71 | for (let i = 0; i < redoStack.length; i++) { 72 | if (redoStack[i].type === 'transaction') this.undo() 73 | } 74 | } 75 | 76 | replicate (siteId) { 77 | const replica = new Document({siteId}) 78 | replica.integrateOperations(this.getOperations()) 79 | return replica 80 | } 81 | 82 | /* 83 | Public: Gets all the operations that have been integrated in the document. 84 | 85 | Returns an {Array} of text operations and marker update operations. 86 | */ 87 | getOperations () { 88 | const markerOperations = [] 89 | this.markerLayersBySiteId.forEach((layersById, siteId) => { 90 | const siteMarkerLayers = {} 91 | layersById.forEach((markersById, layerId) => { 92 | const layer = {} 93 | markersById.forEach((marker, markerId) => { 94 | layer[markerId] = marker 95 | }) 96 | siteMarkerLayers[layerId] = layer 97 | }) 98 | 99 | markerOperations.push({ 100 | type: 'markers-update', 101 | updates: siteMarkerLayers, 102 | siteId 103 | }) 104 | }) 105 | 106 | return this.operations.concat(markerOperations) 107 | } 108 | 109 | /* 110 | Public: Replaces the (possibly empty) range identified by `start` and `end` 111 | with the specified `text`. 112 | 113 | * `start` Point {Object} 114 | * `end` Point {Object} 115 | * `text` {String} 116 | 117 | Returns an {Array} containing the integrated operation. 118 | */ 119 | setTextInRange (start, end, text) { 120 | const spliceId = {site: this.siteId, seq: this.nextSequenceNumber} 121 | const operation = {type: 'splice', spliceId} 122 | 123 | if (compare(end, start) > 0) { 124 | operation.deletion = this.delete(spliceId, start, end) 125 | } 126 | if (text && text.length > 0) { 127 | operation.insertion = this.insert(spliceId, start, text) 128 | } 129 | this.updateMaxSeqsBySite(spliceId) 130 | 131 | this.undoStack.push(new Transaction(this.getNow(), [operation])) 132 | this.clearRedoStack() 133 | 134 | this.operations.push(operation) 135 | return [operation] 136 | } 137 | 138 | /* 139 | Public: Gets all the markers currently available on this Document. 140 | 141 | Returns an {Object} of shape: 142 | * {Number} Site ID 143 | * {Number} Marker layer ID 144 | * {Number} Marker ID 145 | * Marker {Object} 146 | */ 147 | getMarkers () { 148 | const result = {} 149 | this.markerLayersBySiteId.forEach((layersById, siteId) => { 150 | if (layersById.size > 0) { 151 | result[siteId] = {} 152 | layersById.forEach((markersById, layerId) => { 153 | result[siteId][layerId] = {} 154 | markersById.forEach((marker, markerId) => { 155 | const resultMarker = Object.assign({}, marker) 156 | resultMarker.range = this.resolveLogicalRange(marker.range, marker.exclusive) 157 | 158 | result[siteId][layerId][markerId] = resultMarker 159 | }) 160 | }) 161 | } 162 | }) 163 | return result 164 | } 165 | 166 | /* 167 | Public: Updates markers in the Document. 168 | 169 | If a Marker exists with the specified ID, its value is updated. 170 | If no Marker exists with the specified ID, one is created. 171 | 172 | * `layerUpdatesById` {Object} of shape: 173 | * {Number} Marker layer ID 174 | * {Numer} Marker ID 175 | * Marker {Object} 176 | 177 | Returns an {Array} including the marker update operation. 178 | */ 179 | updateMarkers (layerUpdatesById) { 180 | const operation = { 181 | type: 'markers-update', 182 | siteId: this.siteId, 183 | updates: {} 184 | } 185 | 186 | const layers = this.markerLayersBySiteId.get(this.siteId) 187 | for (let layerId in layerUpdatesById) { 188 | const layerUpdate = layerUpdatesById[layerId] 189 | layerId = parseInt(layerId) 190 | let layer = layers.get(layerId) 191 | 192 | if (layerUpdate === null) { 193 | if (layer) { 194 | layers.delete(layerId) 195 | operation.updates[layerId] = null 196 | } 197 | } else { 198 | if (!layer) { 199 | layer = new Map() 200 | layers.set(layerId, layer) 201 | } 202 | 203 | operation.updates[layerId] = {} 204 | for (let markerId in layerUpdate) { 205 | const markerUpdate = layerUpdate[markerId] 206 | markerId = parseInt(markerId) 207 | let marker = layer.get(markerId) 208 | 209 | if (markerUpdate) { 210 | if (marker) { 211 | marker = Object.assign({}, marker) 212 | } else { 213 | marker = {exclusive: false, reversed: false, tailed: true} 214 | } 215 | 216 | const updatingExclusive = marker.exclusive !== markerUpdate.exclusive 217 | Object.assign(marker, markerUpdate) 218 | if (markerUpdate.range || updatingExclusive) { 219 | marker.range = this.getLogicalRange(markerUpdate.range || marker.range, marker.exclusive) 220 | } 221 | Object.freeze(marker) 222 | layer.set(markerId, marker) 223 | operation.updates[layerId][markerId] = marker 224 | } else { 225 | layer.delete(markerId) 226 | operation.updates[layerId][markerId] = null 227 | } 228 | } 229 | } 230 | } 231 | 232 | return [operation] 233 | } 234 | 235 | /* 236 | Public: Undoes the latest Transaction on the undo stack. 237 | 238 | If there's a barrier before the latest Transaction, nothing happens. 239 | 240 | Returns null or an {Object} of shape: 241 | * `markers` {Object} of shape: 242 | * {Number} Marker layer ID 243 | * {Number} Marker ID 244 | * Marker {Object} 245 | * `textUpdates` {Array} of {Object}s of shape: 246 | * `oldStart` Point {Object} 247 | * `oldEnd` Point {Object} 248 | * `oldText` {String} 249 | * `newStart` Point {Object} 250 | * `newEnd` Point {Object} 251 | * `newText` {String} 252 | * `operations` {Array} of operations 253 | */ 254 | undo () { 255 | let spliceIndex = null 256 | let operationsToUndo = [] 257 | let markersSnapshot 258 | for (let i = this.undoStack.length - 1; i >=0; i--) { 259 | const stackEntry = this.undoStack[i] 260 | if (stackEntry instanceof Transaction) { 261 | operationsToUndo = stackEntry.operations 262 | markersSnapshot = stackEntry.markersSnapshotBefore 263 | spliceIndex = i 264 | break 265 | } else if (stackEntry instanceof Checkpoint && stackEntry.isBarrier) { 266 | return null 267 | } 268 | } 269 | 270 | if (spliceIndex != null) { 271 | this.redoStack.push(...this.undoStack.splice(spliceIndex).reverse()) 272 | const {operations, textUpdates} = this.undoOrRedoOperations(operationsToUndo) 273 | let markers = this.markersFromSnapshot(markersSnapshot) 274 | return {operations, textUpdates, markers} 275 | } else { 276 | return null 277 | } 278 | } 279 | 280 | /* 281 | Public: Redoes the latest Transaction on the redo stack. 282 | 283 | Returns null or an {Object} of shape: 284 | * `markers` {Object} of shape: 285 | * {Number} Marker layer ID 286 | * {Number} Marker ID 287 | * Marker {Object} 288 | * `textUpdates` {Array} of {Object}s of shape: 289 | * `oldStart: Point {Object} 290 | * `oldEnd` Point {Object} 291 | * `oldText` {String} 292 | * `newStart` Point {Object} 293 | * `newEnd` Point {Object} 294 | * `newText` {String} 295 | * `operations` {Array} of operations 296 | */ 297 | redo () { 298 | let spliceIndex = null 299 | let operationsToRedo = [] 300 | let markersSnapshot 301 | for (let i = this.redoStack.length - 1; i >= 0; i--) { 302 | const stackEntry = this.redoStack[i] 303 | if (stackEntry instanceof Transaction) { 304 | operationsToRedo = stackEntry.operations 305 | markersSnapshot = stackEntry.markersSnapshotAfter 306 | spliceIndex = i 307 | break 308 | } 309 | } 310 | 311 | while (this.redoStack[spliceIndex - 1] instanceof Checkpoint) { 312 | spliceIndex-- 313 | } 314 | 315 | if (spliceIndex != null) { 316 | this.undoStack.push(...this.redoStack.splice(spliceIndex).reverse()) 317 | const {operations, textUpdates} = this.undoOrRedoOperations(operationsToRedo) 318 | const markers = markersSnapshot ? this.markersFromSnapshot(markersSnapshot) : null 319 | return {operations, textUpdates, markers} 320 | } else { 321 | return null 322 | } 323 | } 324 | 325 | clearUndoStack () { 326 | this.undoStack.length = 0 327 | } 328 | 329 | clearRedoStack () { 330 | this.redoStack.length = 0 331 | } 332 | 333 | /* 334 | Public: Groups together transactions that happened within the specified 335 | `groupingInterval`. 336 | 337 | * `groupingInterval` {Number} of milliseconds 338 | 339 | */ 340 | applyGroupingInterval (groupingInterval) { 341 | const topEntry = this.undoStack[this.undoStack.length - 1] 342 | const previousEntry = this.undoStack[this.undoStack.length - 2] 343 | 344 | if (topEntry instanceof Transaction) { 345 | topEntry.groupingInterval = groupingInterval 346 | } else { 347 | return 348 | } 349 | 350 | if (previousEntry instanceof Transaction) { 351 | const timeBetweenEntries = topEntry.timestamp - previousEntry.timestamp 352 | const minGroupingInterval = Math.min(groupingInterval, previousEntry.groupingInterval || Infinity) 353 | if (timeBetweenEntries < minGroupingInterval) { 354 | this.undoStack.pop() 355 | previousEntry.timestamp = topEntry.timestamp 356 | previousEntry.groupingInterval = groupingInterval 357 | previousEntry.operations.push(...topEntry.operations) 358 | previousEntry.markersSnapshotAfter = topEntry.markersSnapshotAfter 359 | } 360 | } 361 | } 362 | 363 | getNow () { 364 | return Date.now() 365 | } 366 | 367 | /* 368 | Public: Creates a Checkpoint in the undo stack. 369 | 370 | If a Checkpoint is a barrier, Transactions chronologically before it cannot be 371 | undone or grouped with Transactions before it. 372 | 373 | * `options` {Object} of shape: 374 | * `isBarrier` {Bool} 375 | * `markers` {Array} of Marker {Object}s 376 | 377 | Returns {Number} 378 | */ 379 | createCheckpoint (options) { 380 | const checkpoint = new Checkpoint( 381 | this.nextCheckpointId++, 382 | options && options.isBarrier, 383 | options && this.snapshotFromMarkers(options.markers) 384 | ) 385 | this.undoStack.push(checkpoint) 386 | return checkpoint.id 387 | } 388 | 389 | /* 390 | Private: Checks if a barrier Checkpoint is present chronologically 391 | before a given Checkpoint. 392 | 393 | This is used to prevent undo operations or Transaction grouping over barriers. 394 | 395 | * `checkpointId` {Number} 396 | 397 | Returns {Bool} 398 | */ 399 | isBarrierPresentBeforeCheckpoint (checkpointId) { 400 | for (let i = this.undoStack.length - 1; i >= 0; i--) { 401 | const stackEntry = this.undoStack[i] 402 | if (stackEntry instanceof Checkpoint) { 403 | if (stackEntry.id == checkpointId) return false 404 | if (stackEntry.isBarrier) return true 405 | } 406 | } 407 | 408 | return false 409 | } 410 | 411 | groupChangesSinceCheckpoint (checkpointId, options) { 412 | if (this.isBarrierPresentBeforeCheckpoint(checkpointId)) return false 413 | 414 | const result = this.collectOperationsSinceCheckpoint(checkpointId, true, options && options.deleteCheckpoint) 415 | if (result) { 416 | const {operations, markersSnapshot} = result 417 | if (operations.length > 0) { 418 | this.undoStack.push(new Transaction( 419 | this.getNow(), 420 | operations, 421 | markersSnapshot, 422 | options && this.snapshotFromMarkers(options.markers) 423 | )) 424 | return this.textUpdatesForOperations(operations) 425 | } else { 426 | return [] 427 | } 428 | } else { 429 | return false 430 | } 431 | } 432 | 433 | /* 434 | Public: Reverts the document to a checkpoint in the undo stack. 435 | 436 | If a barrier exists in the undo stack before the checkpoint matching 437 | `checkpointId`, the reversion fails. 438 | 439 | * `checkpointId` {Number} 440 | * `options` {Object} of shape: 441 | * `deleteCheckpoint` {Bool} 442 | 443 | Returns false if the revert couldn't be completed, else returns an {Object} of shape: 444 | * `markers` {Object} of shape: 445 | * {Number} Marker layer ID 446 | * {Number} Marker ID 447 | * Marker {Object} 448 | * `textUpdates` {Array} of {Object}s of shape: 449 | * `oldStart` Point {Object} 450 | * `oldEnd` Point {Object} 451 | * `oldText` {String} 452 | * `newStart` Point {Object} 453 | * `newEnd` Point {Object} 454 | * `newText` {String} 455 | * `operations` {Array} of operations 456 | */ 457 | revertToCheckpoint (checkpointId, options) { 458 | if (this.isBarrierPresentBeforeCheckpoint(checkpointId)) return false 459 | 460 | const collectResult = this.collectOperationsSinceCheckpoint(checkpointId, true, options && options.deleteCheckpoint) 461 | if (collectResult) { 462 | const {operations, textUpdates} = this.undoOrRedoOperations(collectResult.operations) 463 | const markers = this.markersFromSnapshot(collectResult.markersSnapshot) 464 | return {operations, textUpdates, markers} 465 | } else { 466 | return false 467 | } 468 | } 469 | 470 | /* 471 | Public: Gets changes performed since a checkpoint. 472 | 473 | * `checkpointId` {Number} 474 | 475 | Returns false or {Array} of {Object}s of shape: 476 | * `oldStart` Point {Object} 477 | * `oldEnd` Point {Object} 478 | * `oldText` {String} 479 | * `newStart` Point {Object} 480 | * `newEnd` Point {Object} 481 | * `newText` {String} 482 | */ 483 | getChangesSinceCheckpoint (checkpointId) { 484 | const result = this.collectOperationsSinceCheckpoint(checkpointId, false, false) 485 | if (result) { 486 | return this.textUpdatesForOperations(result.operations) 487 | } else { 488 | return false 489 | } 490 | } 491 | 492 | collectOperationsSinceCheckpoint (checkpointId, deleteOperations, deleteCheckpoint) { 493 | let checkpointIndex = -1 494 | const operations = [] 495 | for (let i = this.undoStack.length - 1; i >= 0; i--) { 496 | const stackEntry = this.undoStack[i] 497 | if (stackEntry instanceof Checkpoint) { 498 | if (stackEntry.id === checkpointId) { 499 | checkpointIndex = i 500 | break 501 | } 502 | } else if (stackEntry instanceof Transaction) { 503 | operations.push(...stackEntry.operations) 504 | } else { 505 | throw new Error('Unknown stack entry ' + stackEntry.constructor.name) 506 | } 507 | } 508 | 509 | if (checkpointIndex === -1) { 510 | return null 511 | } else { 512 | const {markersSnapshot} = this.undoStack[checkpointIndex] 513 | if (deleteOperations) { 514 | if (!deleteCheckpoint) checkpointIndex++ 515 | this.undoStack.splice(checkpointIndex) 516 | } 517 | return {operations, markersSnapshot} 518 | } 519 | } 520 | 521 | /* 522 | Public: Groups the last two changes on the undo stack. 523 | 524 | Returns true if a grouping was made, else false. 525 | */ 526 | groupLastChanges () { 527 | let lastTransaction 528 | 529 | for (let i = this.undoStack.length - 1; i >= 0; i--) { 530 | const stackEntry = this.undoStack[i] 531 | 532 | if (stackEntry instanceof Checkpoint) { 533 | if (stackEntry.isBarrier) return false 534 | } else { 535 | if (lastTransaction) { 536 | this.undoStack.splice(i) 537 | this.undoStack.push(new Transaction( 538 | this.getNow(), 539 | stackEntry.operations.concat(lastTransaction.operations), 540 | stackEntry.markersSnapshotBefore, 541 | lastTransaction.markersSnapshotAfter 542 | )) 543 | return true 544 | } else { 545 | lastTransaction = stackEntry 546 | } 547 | } 548 | } 549 | 550 | return false 551 | } 552 | 553 | /* 554 | Public: Gets a serializable representation of the history. 555 | 556 | * `maxEntries` Maximum {Number} of history entries to return 557 | 558 | Returns {Object} of shape: 559 | * `nextCheckpointId` {Number} 560 | * `undoStack` {Array} of either Checkpoint or Transaction {Object}s 561 | * `redoStack` {Array} of either Checkpoint or Transaction {Object}s 562 | */ 563 | getHistory (maxEntries) { 564 | const originalUndoCounts = new Map(this.undoCountsBySpliceId) 565 | 566 | const redoStack = [] 567 | for (let i = this.redoStack.length - 1; i >= 0; i--) { 568 | const entry = this.redoStack[i] 569 | if (entry instanceof Transaction) { 570 | const markersBefore = this.markersFromSnapshot(entry.markersSnapshotBefore) 571 | const changes = this.undoOrRedoOperations(entry.operations).textUpdates 572 | const markersAfter = this.markersFromSnapshot(entry.markersSnapshotAfter) 573 | redoStack.push({type: 'transaction', changes, markersBefore, markersAfter}) 574 | } else { 575 | redoStack.push({ 576 | type: 'checkpoint', 577 | id: entry.id, 578 | markers: this.markersFromSnapshot(entry.markersSnapshot) 579 | }) 580 | } 581 | if (redoStack.length === maxEntries) break 582 | } 583 | redoStack.reverse() 584 | 585 | // Undo operations we redid above while computing changes 586 | for (let i = this.redoStack.length - 1; i >= this.redoStack.length - redoStack.length; i--) { 587 | const entry = this.redoStack[i] 588 | if (entry instanceof Transaction) { 589 | this.undoOrRedoOperations(entry.operations) 590 | } 591 | } 592 | 593 | const undoStack = [] 594 | for (let i = this.undoStack.length - 1; i >= 0; i--) { 595 | const entry = this.undoStack[i] 596 | if (entry instanceof Transaction) { 597 | const markersAfter = this.markersFromSnapshot(entry.markersSnapshotAfter) 598 | const changes = invertTextUpdates(this.undoOrRedoOperations(entry.operations).textUpdates) 599 | const markersBefore = this.markersFromSnapshot(entry.markersSnapshotBefore) 600 | undoStack.push({type: 'transaction', changes, markersBefore, markersAfter}) 601 | } else { 602 | undoStack.push({ 603 | type: 'checkpoint', 604 | id: entry.id, 605 | markers: this.markersFromSnapshot(entry.markersSnapshot) 606 | }) 607 | } 608 | if (undoStack.length === maxEntries) break 609 | } 610 | undoStack.reverse() 611 | 612 | // Redo operations we undid above while computing changes 613 | for (let i = this.undoStack.length - 1; i >= this.undoStack.length - undoStack.length; i--) { 614 | const entry = this.undoStack[i] 615 | if (entry instanceof Transaction) { 616 | this.undoOrRedoOperations(entry.operations) 617 | } 618 | } 619 | 620 | this.undoCountsBySpliceId = originalUndoCounts 621 | 622 | return { 623 | nextCheckpointId: this.nextCheckpointId, 624 | undoStack, 625 | redoStack 626 | } 627 | } 628 | 629 | /* 630 | Private: Deletes text between `start` and `end`. 631 | 632 | * `spliceId` {Number} 633 | * `start` Point {Object} 634 | * `end` Point {Object} 635 | 636 | Returns deletion operation {Object} 637 | */ 638 | delete (spliceId, start, end) { 639 | const spliceIdString = spliceIdToString(spliceId) 640 | 641 | const left = this.findLocalSegmentBoundary(start)[1] 642 | const right = this.findLocalSegmentBoundary(end)[0] 643 | 644 | const maxSeqsBySite = {} 645 | let segment = left 646 | while (true) { 647 | const maxSeq = maxSeqsBySite[segment.spliceId.site] 648 | if (maxSeq == null || segment.spliceId.seq > maxSeq) { 649 | maxSeqsBySite[segment.spliceId.site] = segment.spliceId.seq 650 | } 651 | 652 | segment.deletions.add(spliceIdString) 653 | this.documentTree.splayNode(segment) 654 | this.documentTree.updateSubtreeExtent(segment) 655 | if (segment === right) break 656 | segment = this.documentTree.getSuccessor(segment) 657 | } 658 | 659 | const deletion = { 660 | spliceId, 661 | leftDependencyId: left.spliceId, 662 | offsetInLeftDependency: left.offset, 663 | rightDependencyId: right.spliceId, 664 | offsetInRightDependency: traverse(right.offset, right.extent), 665 | maxSeqsBySite 666 | } 667 | this.deletionsBySpliceId.set(spliceIdString, deletion) 668 | return deletion 669 | } 670 | 671 | /* 672 | Private: Inserts `text` at `position`. 673 | 674 | * `spliceId` {Number} 675 | * `position` Point {Object} 676 | * `text` {String} 677 | 678 | Returns insertion operation {Object} 679 | */ 680 | insert (spliceId, position, text) { 681 | const [left, right] = this.findLocalSegmentBoundary(position) 682 | const newSegment = { 683 | spliceId, 684 | text, 685 | extent: extentForText(text), 686 | offset: ZERO_POINT, 687 | leftDependency: left, 688 | rightDependency: right, 689 | nextSplit: null, 690 | deletions: new Set() 691 | } 692 | this.documentTree.insertBetween(left, right, newSegment) 693 | this.splitTreesBySpliceId.set(spliceIdToString(spliceId), new SplitTree(newSegment)) 694 | 695 | return { 696 | text, 697 | leftDependencyId: left.spliceId, 698 | offsetInLeftDependency: traverse(left.offset, left.extent), 699 | rightDependencyId: right.spliceId, 700 | offsetInRightDependency: right.offset 701 | } 702 | } 703 | 704 | undoOrRedoOperations (operationsToUndo) { 705 | const undoOperations = [] 706 | const oldUndoCounts = new Map() 707 | 708 | for (var i = 0; i < operationsToUndo.length; i++) { 709 | const {spliceId} = operationsToUndo[i] 710 | const newUndoCount = (this.undoCountsBySpliceId.get(spliceIdToString(spliceId)) || 0) + 1 711 | this.updateUndoCount(spliceId, newUndoCount, oldUndoCounts) 712 | const operation = {type: 'undo', spliceId, undoCount: newUndoCount} 713 | undoOperations.push(operation) 714 | this.operations.push(operation) 715 | } 716 | 717 | return { 718 | operations: undoOperations, 719 | textUpdates: this.textUpdatesForOperations(undoOperations, oldUndoCounts) 720 | } 721 | } 722 | 723 | isSpliceUndone ({spliceId}) { 724 | const undoCount = this.undoCountsBySpliceId.get(spliceIdToString(spliceId)) 725 | return undoCount != null && (undoCount & 1 === 1) 726 | } 727 | 728 | canIntegrateOperation (op) { 729 | switch (op.type) { 730 | case 'splice': { 731 | const {spliceId, deletion, insertion} = op 732 | 733 | if ((this.maxSeqsBySite[spliceId.site] || 0) !== spliceId.seq - 1) { 734 | return false 735 | } 736 | 737 | if (deletion) { 738 | const hasLeftAndRightDependencies = ( 739 | this.splitTreesBySpliceId.has(spliceIdToString(deletion.leftDependencyId)) && 740 | this.splitTreesBySpliceId.has(spliceIdToString(deletion.rightDependencyId)) 741 | ) 742 | if (!hasLeftAndRightDependencies) return false 743 | 744 | for (const site in deletion.maxSeqsBySite) { 745 | if (deletion.maxSeqsBySite[site] > (this.maxSeqsBySite[site] || 0)) { 746 | return false 747 | } 748 | } 749 | } 750 | 751 | if (insertion) { 752 | const hasLeftAndRightDependencies = ( 753 | this.splitTreesBySpliceId.has(spliceIdToString(insertion.leftDependencyId)) && 754 | this.splitTreesBySpliceId.has(spliceIdToString(insertion.rightDependencyId)) 755 | ) 756 | if (!hasLeftAndRightDependencies) return false 757 | } 758 | 759 | return true 760 | } 761 | case 'undo': { 762 | const spliceIdString = spliceIdToString(op.spliceId) 763 | return ( 764 | this.splitTreesBySpliceId.has(spliceIdString) || 765 | this.deletionsBySpliceId.has(spliceIdString) 766 | ) 767 | } 768 | case 'markers-update': 769 | return true 770 | default: 771 | throw new Error('Unknown operation type') 772 | } 773 | } 774 | 775 | /* 776 | Public: Integrates operations received from other documents into the current 777 | document. 778 | 779 | * `operations` {Array} of operations. 780 | 781 | Returns {Object} of shape: 782 | * `markerUpdates` {Object} of shape: 783 | * {Number} Site ID 784 | * {Number} Marker layer ID 785 | * {Number} Marker ID 786 | * Marker {Object} 787 | * `textUpdates` {Array} of {Object}s of shape: 788 | * `oldStart` Point {Object} 789 | * `oldEnd` Point {Object} 790 | * `oldText` {String} 791 | * `newStart` Point {Object} 792 | * `newEnd` Point {Object} 793 | * `newText` {String} 794 | */ 795 | integrateOperations (operations) { 796 | const integratedOperations = [] 797 | let oldUndoCounts 798 | let i = 0 799 | while (i < operations.length) { 800 | const operation = operations[i++] 801 | if (operation.type !== 'markers-update') this.operations.push(operation) 802 | 803 | if (this.canIntegrateOperation(operation)) { 804 | integratedOperations.push(operation) 805 | switch (operation.type) { 806 | case 'splice': 807 | if (operation.deletion) this.integrateDeletion(operation.spliceId, operation.deletion) 808 | if (operation.insertion) this.integrateInsertion(operation.spliceId, operation.insertion) 809 | this.updateMaxSeqsBySite(operation.spliceId) 810 | break 811 | case 'undo': 812 | if (!oldUndoCounts) oldUndoCounts = new Map() 813 | this.integrateUndo(operation, oldUndoCounts) 814 | break 815 | } 816 | this.collectDeferredOperations(operation, operations) 817 | } else { 818 | this.deferOperation(operation) 819 | } 820 | } 821 | 822 | const textUpdates = this.textUpdatesForOperations(integratedOperations, oldUndoCounts) 823 | const markerUpdates = this.updateMarkersForOperations(integratedOperations) 824 | 825 | return {textUpdates, markerUpdates} 826 | } 827 | 828 | collectDeferredOperations ({spliceId}, operations) { 829 | if (spliceId) { 830 | const spliceIdString = spliceIdToString(spliceId) 831 | const dependentOps = this.deferredOperationsByDependencyId.get(spliceIdString) 832 | if (dependentOps) { 833 | dependentOps.forEach((dependentOp) => { 834 | if (this.canIntegrateOperation(dependentOp)) { 835 | operations.push(dependentOp) 836 | } 837 | }) 838 | this.deferredOperationsByDependencyId.delete(spliceIdString) 839 | } 840 | } 841 | } 842 | 843 | deferOperation (op) { 844 | if (op.type === 'splice') { 845 | const {spliceId, deletion, insertion} = op 846 | this.addOperationDependency(this.deferredOperationsByDependencyId, {site: spliceId.site, seq: spliceId.seq - 1}, op) 847 | 848 | if (deletion) { 849 | this.addOperationDependency(this.deferredOperationsByDependencyId, deletion.leftDependencyId, op) 850 | this.addOperationDependency(this.deferredOperationsByDependencyId, deletion.rightDependencyId, op) 851 | for (const site in deletion.maxSeqsBySite) { 852 | const seq = deletion.maxSeqsBySite[site] 853 | this.addOperationDependency(this.deferredOperationsByDependencyId, {site, seq}, op) 854 | } 855 | } 856 | 857 | if (insertion) { 858 | this.addOperationDependency(this.deferredOperationsByDependencyId, insertion.leftDependencyId, op) 859 | this.addOperationDependency(this.deferredOperationsByDependencyId, insertion.rightDependencyId, op) 860 | } 861 | } else if (op.type === 'undo') { 862 | this.addOperationDependency(this.deferredOperationsByDependencyId, op.spliceId, op) 863 | } else { 864 | throw new Error('Unknown operation type: ' + op.type) 865 | } 866 | } 867 | 868 | addOperationDependency (map, dependencyId, op) { 869 | const dependencyIdString = spliceIdToString(dependencyId) 870 | if (!this.hasAppliedSplice(dependencyId)) { 871 | let deferredOps = map.get(dependencyIdString) 872 | if (!deferredOps) { 873 | deferredOps = new Set() 874 | map.set(dependencyIdString, deferredOps) 875 | } 876 | deferredOps.add(op) 877 | } 878 | } 879 | 880 | hasAppliedSplice (spliceId) { 881 | const spliceIdString = spliceIdToString(spliceId) 882 | return ( 883 | this.splitTreesBySpliceId.has(spliceIdString) || 884 | this.deletionsBySpliceId.has(spliceIdString) 885 | ) 886 | } 887 | 888 | integrateInsertion (spliceId, operation) { 889 | const {text, leftDependencyId, offsetInLeftDependency, rightDependencyId, offsetInRightDependency} = operation 890 | 891 | const originalRightDependency = this.findSegmentStart(rightDependencyId, offsetInRightDependency) 892 | const originalLeftDependency = this.findSegmentEnd(leftDependencyId, offsetInLeftDependency) 893 | 894 | this.documentTree.splayNode(originalLeftDependency) 895 | this.documentTree.splayNode(originalRightDependency) 896 | 897 | let currentSegment = this.documentTree.getSuccessor(originalLeftDependency) 898 | let leftDependency = originalLeftDependency 899 | let rightDependency = originalRightDependency 900 | while (currentSegment !== rightDependency) { 901 | const leftDependencyIndex = this.documentTree.getSegmentIndex(leftDependency) 902 | const rightDependencyIndex = this.documentTree.getSegmentIndex(rightDependency) 903 | const currentSegmentLeftDependencyIndex = this.documentTree.getSegmentIndex(currentSegment.leftDependency) 904 | const currentSegmentRightDependencyIndex = this.documentTree.getSegmentIndex(currentSegment.rightDependency) 905 | 906 | if (currentSegmentLeftDependencyIndex <= leftDependencyIndex && currentSegmentRightDependencyIndex >= rightDependencyIndex) { 907 | if (spliceId.site < currentSegment.spliceId.site) { 908 | rightDependency = currentSegment 909 | } else { 910 | leftDependency = currentSegment 911 | } 912 | 913 | currentSegment = this.documentTree.getSuccessor(leftDependency) 914 | } else { 915 | currentSegment = this.documentTree.getSuccessor(currentSegment) 916 | } 917 | } 918 | 919 | const newSegment = { 920 | spliceId, 921 | offset: ZERO_POINT, 922 | text, 923 | extent: extentForText(text), 924 | leftDependency: originalLeftDependency, 925 | rightDependency: originalRightDependency, 926 | nextSplit: null, 927 | deletions: new Set() 928 | } 929 | this.documentTree.insertBetween(leftDependency, rightDependency, newSegment) 930 | this.splitTreesBySpliceId.set(spliceIdToString(spliceId), new SplitTree(newSegment)) 931 | } 932 | 933 | integrateDeletion (spliceId, deletion) { 934 | const { 935 | leftDependencyId, offsetInLeftDependency, 936 | rightDependencyId, offsetInRightDependency, 937 | maxSeqsBySite 938 | } = deletion 939 | 940 | const spliceIdString = spliceIdToString(spliceId) 941 | this.deletionsBySpliceId.set(spliceIdString, deletion) 942 | 943 | const left = this.findSegmentStart(leftDependencyId, offsetInLeftDependency) 944 | const right = this.findSegmentEnd(rightDependencyId, offsetInRightDependency) 945 | let segment = left 946 | while (true) { 947 | const maxSeq = maxSeqsBySite[segment.spliceId.site] || 0 948 | if (segment.spliceId.seq <= maxSeq) { 949 | this.documentTree.splayNode(segment) 950 | segment.deletions.add(spliceIdString) 951 | this.documentTree.updateSubtreeExtent(segment) 952 | } 953 | 954 | if (segment === right) break 955 | segment = this.documentTree.getSuccessor(segment) 956 | } 957 | } 958 | 959 | integrateUndo ({spliceId, undoCount}, oldUndoCounts) { 960 | return this.updateUndoCount(spliceId, undoCount, oldUndoCounts) 961 | } 962 | 963 | getMarkerLayersForSiteId (siteId) { 964 | let layers = this.markerLayersBySiteId.get(siteId) 965 | if (!layers) { 966 | layers = new Map() 967 | this.markerLayersBySiteId.set(siteId, layers) 968 | } 969 | return layers 970 | } 971 | 972 | deferMarkerUpdate (siteId, layerId, markerId, markerUpdate) { 973 | const {range} = markerUpdate 974 | const deferredMarkerUpdate = {siteId, layerId, markerId} 975 | this.addOperationDependency(this.deferredMarkerUpdatesByDependencyId, range.startDependencyId, deferredMarkerUpdate) 976 | this.addOperationDependency(this.deferredMarkerUpdatesByDependencyId, range.endDependencyId, deferredMarkerUpdate) 977 | 978 | let deferredUpdatesByLayerId = this.deferredMarkerUpdates.get(siteId) 979 | if (!deferredUpdatesByLayerId) { 980 | deferredUpdatesByLayerId = new Map() 981 | this.deferredMarkerUpdates.set(siteId, deferredUpdatesByLayerId) 982 | } 983 | let deferredUpdatesByMarkerId = deferredUpdatesByLayerId.get(layerId) 984 | if (!deferredUpdatesByMarkerId) { 985 | deferredUpdatesByMarkerId = new Map() 986 | deferredUpdatesByLayerId.set(layerId, deferredUpdatesByMarkerId) 987 | } 988 | deferredUpdatesByMarkerId.set(markerId, markerUpdate) 989 | } 990 | 991 | updateMarkersForOperations (operations) { 992 | const markerUpdates = {} 993 | 994 | for (let i = 0; i < operations.length; i++) { 995 | const operation = operations[i] 996 | if (operation.type === 'markers-update') { 997 | this.integrateMarkerUpdates(markerUpdates, operation) 998 | } else if (operation.type === 'splice') { 999 | this.integrateDeferredMarkerUpdates(markerUpdates, operation) 1000 | } 1001 | } 1002 | 1003 | return markerUpdates 1004 | } 1005 | 1006 | integrateMarkerUpdates (markerUpdates, {siteId, updates}) { 1007 | const layers = this.getMarkerLayersForSiteId(siteId) 1008 | if (!markerUpdates[siteId]) markerUpdates[siteId] = {} 1009 | 1010 | for (let layerId in updates) { 1011 | const updatesByMarkerId = updates[layerId] 1012 | layerId = parseInt(layerId) 1013 | 1014 | let layer = layers.get(layerId) 1015 | if (updatesByMarkerId) { 1016 | if (!layer) { 1017 | layer = new Map() 1018 | layers.set(layerId, layer) 1019 | } 1020 | 1021 | if (!markerUpdates[siteId][layerId]) markerUpdates[siteId][layerId] = {} 1022 | 1023 | for (let markerId in updatesByMarkerId) { 1024 | const markerUpdate = updatesByMarkerId[markerId] 1025 | markerId = parseInt(markerId) 1026 | 1027 | if (markerUpdate) { 1028 | if (markerUpdate.range && !this.canResolveLogicalRange(markerUpdate.range)) { 1029 | this.deferMarkerUpdate(siteId, layerId, markerId, markerUpdate) 1030 | } else { 1031 | this.integrateMarkerUpdate(markerUpdates, siteId, layerId, markerId, markerUpdate) 1032 | } 1033 | } else { 1034 | if (layer.has(markerId)) { 1035 | layer.delete(markerId) 1036 | markerUpdates[siteId][layerId][markerId] = null 1037 | } 1038 | 1039 | const deferredUpdatesByLayerId = this.deferredMarkerUpdates.get(siteId) 1040 | if (deferredUpdatesByLayerId) { 1041 | const deferredUpdatesByMarkerId = deferredUpdatesByLayerId.get(layerId) 1042 | if (deferredUpdatesByMarkerId) { 1043 | deferredUpdatesByMarkerId.delete(markerId) 1044 | } 1045 | } 1046 | } 1047 | } 1048 | } else { 1049 | if (layer) { 1050 | markerUpdates[siteId][layerId] = null 1051 | layers.delete(layerId) 1052 | } 1053 | 1054 | const deferredUpdatesByLayerId = this.deferredMarkerUpdates.get(siteId) 1055 | if (deferredUpdatesByLayerId) { 1056 | deferredUpdatesByLayerId.delete(layerId) 1057 | } 1058 | } 1059 | } 1060 | } 1061 | 1062 | integrateDeferredMarkerUpdates (markerUpdates, {spliceId}) { 1063 | const spliceIdString = spliceIdToString(spliceId) 1064 | const dependentMarkerUpdates = this.deferredMarkerUpdatesByDependencyId.get(spliceIdString) 1065 | if (dependentMarkerUpdates) { 1066 | dependentMarkerUpdates.forEach(({siteId, layerId, markerId}) => { 1067 | const deferredUpdatesByLayerId = this.deferredMarkerUpdates.get(siteId) 1068 | if (deferredUpdatesByLayerId) { 1069 | const deferredUpdatesByMarkerId = deferredUpdatesByLayerId.get(layerId) 1070 | if (deferredUpdatesByMarkerId) { 1071 | const deferredUpdate = deferredUpdatesByMarkerId.get(markerId) 1072 | if (deferredUpdate && this.canResolveLogicalRange(deferredUpdate.range)) { 1073 | this.integrateMarkerUpdate(markerUpdates, siteId, layerId, markerId, deferredUpdate) 1074 | } 1075 | } 1076 | } 1077 | }) 1078 | this.deferredMarkerUpdatesByDependencyId.delete(spliceIdString) 1079 | } 1080 | } 1081 | 1082 | integrateMarkerUpdate (markerUpdates, siteId, layerId, markerId, update) { 1083 | let layer = this.markerLayersBySiteId.get(siteId).get(layerId) 1084 | if (!layer) { 1085 | layer = new Map() 1086 | this.markerLayersBySiteId.get(siteId).set(layerId, layer) 1087 | } 1088 | 1089 | let marker = layer.get(markerId) 1090 | marker = marker ? Object.assign({}, marker) : {} 1091 | Object.assign(marker, update) 1092 | Object.freeze(marker) 1093 | layer.set(markerId, marker) 1094 | 1095 | if (!markerUpdates[siteId]) markerUpdates[siteId] = {} 1096 | if (!markerUpdates[siteId][layerId]) markerUpdates[siteId][layerId] = {} 1097 | markerUpdates[siteId][layerId][markerId] = Object.assign({}, marker) 1098 | markerUpdates[siteId][layerId][markerId].range = this.resolveLogicalRange(marker.range, marker.exclusive) 1099 | 1100 | const deferredUpdatesByLayerId = this.deferredMarkerUpdates.get(siteId) 1101 | if (deferredUpdatesByLayerId) { 1102 | const deferredUpdatesByMarkerId = deferredUpdatesByLayerId.get(layerId) 1103 | if (deferredUpdatesByMarkerId) { 1104 | if (deferredUpdatesByMarkerId.has(markerId)) { 1105 | deferredUpdatesByMarkerId.delete(markerId) 1106 | if (deferredUpdatesByMarkerId.size === 0) { 1107 | deferredUpdatesByLayerId.delete(layerId) 1108 | if (deferredUpdatesByLayerId.size === 0) { 1109 | this.deferredMarkerUpdates.delete(siteId) 1110 | } 1111 | } 1112 | } 1113 | } 1114 | } 1115 | } 1116 | 1117 | snapshotFromMarkers (layersById) { 1118 | if (!layersById) return layersById 1119 | 1120 | const snapshot = {} 1121 | for (const layerId in layersById) { 1122 | const layerSnapshot = {} 1123 | const markersById = layersById[layerId] 1124 | for (const markerId in markersById) { 1125 | const markerSnapshot = Object.assign({}, markersById[markerId]) 1126 | markerSnapshot.range = this.getLogicalRange(markerSnapshot.range, markerSnapshot.exclusive) 1127 | layerSnapshot[markerId] = markerSnapshot 1128 | } 1129 | snapshot[layerId] = layerSnapshot 1130 | } 1131 | return snapshot 1132 | } 1133 | 1134 | markersFromSnapshot (snapshot) { 1135 | if (!snapshot) return snapshot 1136 | 1137 | const layersById = {} 1138 | for (const layerId in snapshot) { 1139 | const markersById = {} 1140 | const layerSnapshot = snapshot[layerId] 1141 | for (const markerId in layerSnapshot) { 1142 | const marker = Object.assign({}, layerSnapshot[markerId]) 1143 | marker.range = this.resolveLogicalRange(marker.range) 1144 | markersById[markerId] = marker 1145 | } 1146 | layersById[layerId] = markersById 1147 | } 1148 | return layersById 1149 | } 1150 | 1151 | updateUndoCount (spliceId, newUndoCount, oldUndoCounts) { 1152 | const spliceIdString = spliceIdToString(spliceId) 1153 | const previousUndoCount = this.undoCountsBySpliceId.get(spliceIdString) || 0 1154 | if (newUndoCount <= previousUndoCount) return 1155 | 1156 | oldUndoCounts.set(spliceIdString, previousUndoCount) 1157 | this.undoCountsBySpliceId.set(spliceIdString, newUndoCount) 1158 | 1159 | const segmentsToUpdate = new Set() 1160 | this.collectSegments(spliceIdString, segmentsToUpdate) 1161 | 1162 | segmentsToUpdate.forEach((segment) => { 1163 | const wasVisible = this.isSegmentVisible(segment, oldUndoCounts) 1164 | const isVisible = this.isSegmentVisible(segment) 1165 | if (isVisible !== wasVisible) { 1166 | this.documentTree.splayNode(segment, oldUndoCounts) 1167 | this.documentTree.updateSubtreeExtent(segment) 1168 | } 1169 | }) 1170 | } 1171 | 1172 | textUpdatesForOperations (operations, oldUndoCounts) { 1173 | const newSpliceIds = new Set() 1174 | const segmentStartPositions = new Map() 1175 | const segmentIndices = new Map() 1176 | 1177 | for (let i = 0; i < operations.length; i++) { 1178 | const operation = operations[i] 1179 | const {type, spliceId} = operation 1180 | if (spliceId) { 1181 | const spliceIdString = spliceIdToString(spliceId) 1182 | if (type === 'splice') newSpliceIds.add(spliceIdString) 1183 | this.collectSegments(spliceIdString, null, segmentIndices, segmentStartPositions) 1184 | } 1185 | } 1186 | 1187 | return this.computeChangesForSegments(segmentIndices, segmentStartPositions, oldUndoCounts, newSpliceIds) 1188 | } 1189 | 1190 | canResolveLogicalRange ({startDependencyId, endDependencyId}) { 1191 | return ( 1192 | this.hasAppliedSplice(startDependencyId) && 1193 | this.hasAppliedSplice(endDependencyId) 1194 | ) 1195 | } 1196 | 1197 | getLogicalRange ({start, end}, exclusive) { 1198 | const {segment: startDependency, offset: offsetInStartDependency} = this.findSegment(start, exclusive) 1199 | const {segment: endDependency, offset: offsetInEndDependency} = this.findSegment(end, !exclusive || compare(start, end) === 0) 1200 | 1201 | return { 1202 | startDependencyId: startDependency.spliceId, 1203 | offsetInStartDependency, 1204 | endDependencyId: endDependency.spliceId, 1205 | offsetInEndDependency 1206 | } 1207 | } 1208 | 1209 | resolveLogicalRange (logicalRange, exclusive) { 1210 | const { 1211 | startDependencyId, offsetInStartDependency, 1212 | endDependencyId, offsetInEndDependency 1213 | } = logicalRange 1214 | return { 1215 | start: this.resolveLogicalPosition(startDependencyId, offsetInStartDependency, exclusive), 1216 | end: this.resolveLogicalPosition(endDependencyId, offsetInEndDependency, !exclusive || isEmptyLogicalRange(logicalRange)) 1217 | } 1218 | } 1219 | 1220 | resolveLogicalPosition (spliceId, offset, preferStart) { 1221 | const splitTree = this.splitTreesBySpliceId.get(spliceIdToString(spliceId)) 1222 | let segment = splitTree.findSegmentContainingOffset(offset) 1223 | const nextSegmentOffset = traverse(segment.offset, segment.extent) 1224 | if (preferStart && compare(offset, nextSegmentOffset) === 0) { 1225 | segment = splitTree.getSuccessor(segment) || segment 1226 | } 1227 | const segmentStart = this.documentTree.getSegmentPosition(segment) 1228 | 1229 | if (this.isSegmentVisible(segment)) { 1230 | return traverse(segmentStart, traversal(offset, segment.offset)) 1231 | } else { 1232 | return segmentStart 1233 | } 1234 | } 1235 | 1236 | findLocalSegmentBoundary (position) { 1237 | const {segment, start, end} = this.documentTree.findSegmentContainingPosition(position) 1238 | if (compare(position, end) < 0) { 1239 | const splitTree = this.splitTreesBySpliceId.get(spliceIdToString(segment.spliceId)) 1240 | return this.splitSegment(splitTree, segment, traversal(position, start)) 1241 | } else { 1242 | return [segment, this.documentTree.getSuccessor(segment)] 1243 | } 1244 | } 1245 | 1246 | splitSegment (splitTree, segment, offset) { 1247 | const suffix = splitTree.splitSegment(segment, offset) 1248 | this.documentTree.splitSegment(segment, suffix) 1249 | return [segment, suffix] 1250 | } 1251 | 1252 | findSegment (position, preferStart) { 1253 | let {segment, start, end} = this.documentTree.findSegmentContainingPosition(position) 1254 | let offset = traverse(segment.offset, traversal(position, start)) 1255 | if (preferStart && compare(position, end) === 0) { 1256 | segment = this.documentTree.getSuccessor(segment) 1257 | offset = segment.offset 1258 | } 1259 | return {segment, offset} 1260 | } 1261 | 1262 | findSegmentStart (spliceId, offset) { 1263 | const splitTree = this.splitTreesBySpliceId.get(spliceIdToString(spliceId)) 1264 | const segment = splitTree.findSegmentContainingOffset(offset) 1265 | const segmentEndOffset = traverse(segment.offset, segment.extent) 1266 | if (compare(segment.offset, offset) === 0) { 1267 | return segment 1268 | } else if (compare(segmentEndOffset, offset) === 0) { 1269 | return segment.nextSplit 1270 | } else { 1271 | const [, suffix] = this.splitSegment(splitTree, segment, traversal(offset, segment.offset)) 1272 | return suffix 1273 | } 1274 | } 1275 | 1276 | findSegmentEnd (spliceId, offset) { 1277 | const splitTree = this.splitTreesBySpliceId.get(spliceIdToString(spliceId)) 1278 | const segment = splitTree.findSegmentContainingOffset(offset) 1279 | const segmentEndOffset = traverse(segment.offset, segment.extent) 1280 | if (compare(segmentEndOffset, offset) === 0) { 1281 | return segment 1282 | } else { 1283 | const [prefix] = this.splitSegment(splitTree, segment, traversal(offset, segment.offset)) 1284 | return prefix 1285 | } 1286 | } 1287 | 1288 | /* 1289 | Public: Gets the text of the Document. 1290 | 1291 | Returns {String} 1292 | */ 1293 | getText () { 1294 | let text = '' 1295 | const segments = this.documentTree.getSegments() 1296 | for (var i = 0; i < segments.length; i++) { 1297 | const segment = segments[i] 1298 | if (this.isSegmentVisible(segment)) text += segment.text 1299 | } 1300 | return text 1301 | } 1302 | 1303 | collectSegments (spliceIdString, segments, segmentIndices, segmentStartPositions) { 1304 | const insertionSplitTree = this.splitTreesBySpliceId.get(spliceIdString) 1305 | if (insertionSplitTree) { 1306 | let segment = insertionSplitTree.getStart() 1307 | while (segment) { 1308 | if (segments) { 1309 | segments.add(segment) 1310 | } else { 1311 | segmentStartPositions.set(segment, this.documentTree.getSegmentPosition(segment)) 1312 | segmentIndices.set(segment, this.documentTree.getSegmentIndex(segment)) 1313 | } 1314 | segment = insertionSplitTree.getSuccessor(segment) 1315 | } 1316 | } 1317 | 1318 | const deletion = this.deletionsBySpliceId.get(spliceIdString) 1319 | if (deletion) { 1320 | const { 1321 | leftDependencyId, offsetInLeftDependency, 1322 | rightDependencyId, offsetInRightDependency, 1323 | maxSeqsBySite 1324 | } = deletion 1325 | 1326 | const left = this.findSegmentStart(leftDependencyId, offsetInLeftDependency) 1327 | const right = this.findSegmentEnd(rightDependencyId, offsetInRightDependency) 1328 | let segment = left 1329 | while (true) { 1330 | const maxSeq = maxSeqsBySite[segment.spliceId.site] || 0 1331 | if (segment.spliceId.seq <= maxSeq) { 1332 | if (segments) { 1333 | segments.add(segment) 1334 | } else { 1335 | segmentStartPositions.set(segment, this.documentTree.getSegmentPosition(segment)) 1336 | segmentIndices.set(segment, this.documentTree.getSegmentIndex(segment)) 1337 | } 1338 | } 1339 | 1340 | if (segment === right) break 1341 | segment = this.documentTree.getSuccessor(segment) 1342 | } 1343 | } 1344 | } 1345 | 1346 | computeChangesForSegments (segmentIndices, segmentStartPositions, oldUndoCounts, newOperations) { 1347 | const orderedSegments = Array.from(segmentIndices.keys()).sort((s1, s2) => { 1348 | return segmentIndices.get(s1) - segmentIndices.get(s2) 1349 | }) 1350 | 1351 | const changes = [] 1352 | 1353 | let lastChange 1354 | for (let i = 0; i < orderedSegments.length; i++) { 1355 | const segment = orderedSegments[i] 1356 | const visibleBefore = this.isSegmentVisible(segment, oldUndoCounts, newOperations) 1357 | const visibleAfter = this.isSegmentVisible(segment) 1358 | 1359 | if (visibleBefore !== visibleAfter) { 1360 | const segmentNewStart = segmentStartPositions.get(segment) 1361 | const segmentOldStart = 1362 | lastChange 1363 | ? traverse(lastChange.oldEnd, traversal(segmentNewStart, lastChange.newEnd)) 1364 | : segmentNewStart 1365 | 1366 | if (visibleBefore) { 1367 | if (changes.length > 0 && compare(lastChange.newEnd, segmentNewStart) === 0) { 1368 | lastChange.oldEnd = traverse(lastChange.oldEnd, segment.extent) 1369 | lastChange.oldText += segment.text 1370 | } else { 1371 | lastChange = { 1372 | oldStart: segmentOldStart, 1373 | oldEnd: traverse(segmentOldStart, segment.extent), 1374 | oldText: segment.text, 1375 | newStart: segmentNewStart, 1376 | newEnd: segmentNewStart, 1377 | newText: '' 1378 | } 1379 | changes.push(lastChange) 1380 | } 1381 | } else { 1382 | if (lastChange && compare(lastChange.newEnd, segmentNewStart) === 0) { 1383 | lastChange.newEnd = traverse(lastChange.newEnd, segment.extent) 1384 | lastChange.newText += segment.text 1385 | } else { 1386 | lastChange = { 1387 | oldStart: segmentOldStart, 1388 | oldEnd: segmentOldStart, 1389 | oldText: '', 1390 | newStart: segmentNewStart, 1391 | newEnd: traverse(segmentNewStart, segment.extent), 1392 | newText: segment.text 1393 | } 1394 | changes.push(lastChange) 1395 | } 1396 | } 1397 | } 1398 | } 1399 | 1400 | return changes 1401 | } 1402 | 1403 | isSegmentVisible (segment, undoCountOverrides, operationsToIgnore) { 1404 | const spliceIdString = spliceIdToString(segment.spliceId) 1405 | 1406 | if (operationsToIgnore && operationsToIgnore.has(spliceIdString)) { 1407 | return false 1408 | } 1409 | 1410 | let undoCount 1411 | if (undoCountOverrides) { 1412 | undoCount = undoCountOverrides.get(spliceIdString) 1413 | } 1414 | if (undoCount == null) { 1415 | undoCount = this.undoCountsBySpliceId.get(spliceIdString) || 0 1416 | } 1417 | 1418 | return ( 1419 | (undoCount & 1) === 0 && 1420 | !this.isSegmentDeleted(segment, undoCountOverrides, operationsToIgnore) 1421 | ) 1422 | } 1423 | 1424 | isSegmentDeleted (segment, undoCountOverrides, operationsToIgnore) { 1425 | for (const deletionSpliceIdString of segment.deletions) { 1426 | if (operationsToIgnore && operationsToIgnore.has(deletionSpliceIdString)) { 1427 | continue 1428 | } 1429 | 1430 | let deletionUndoCount 1431 | if (undoCountOverrides) { 1432 | deletionUndoCount = undoCountOverrides.get(deletionSpliceIdString) 1433 | } 1434 | if (deletionUndoCount == null) { 1435 | deletionUndoCount = this.undoCountsBySpliceId.get(deletionSpliceIdString) || 0 1436 | } 1437 | 1438 | if ((deletionUndoCount & 1) === 0) return true 1439 | } 1440 | return false 1441 | } 1442 | 1443 | updateMaxSeqsBySite ({site, seq}) { 1444 | const previousSeq = this.maxSeqsBySite[site] || 0 1445 | assert.equal(previousSeq, seq - 1, 'Operations from a given site must be applied in order.') 1446 | this.maxSeqsBySite[site] = seq 1447 | if (this.siteId === site) this.nextSequenceNumber = seq + 1 1448 | } 1449 | } 1450 | 1451 | function spliceIdToString ({site, seq}) { 1452 | return site + '.' + seq 1453 | } 1454 | 1455 | function isEmptyLogicalRange ({startDependencyId, offsetInStartDependency, endDependencyId, offsetInEndDependency}) { 1456 | return ( 1457 | spliceIdsEqual(startDependencyId, endDependencyId) && 1458 | compare(offsetInStartDependency, offsetInEndDependency) === 0 1459 | ) 1460 | } 1461 | 1462 | function spliceIdsEqual (a, b) { 1463 | return a.site === b.site && a.seq === b.seq 1464 | } 1465 | 1466 | function invertTextUpdates (textUpdates) { 1467 | const invertedTextUpdates = [] 1468 | for (let i = 0; i < textUpdates.length; i++) { 1469 | const {oldStart, oldEnd, oldText, newStart, newEnd, newText} = textUpdates[i] 1470 | invertedTextUpdates.push({ 1471 | oldStart: newStart, 1472 | oldEnd: newEnd, 1473 | oldText: newText, 1474 | newStart: oldStart, 1475 | newEnd: oldEnd, 1476 | newText: oldText 1477 | }) 1478 | } 1479 | return invertedTextUpdates 1480 | } 1481 | 1482 | class Checkpoint { 1483 | constructor (id, isBarrier, markersSnapshot) { 1484 | this.id = id 1485 | this.isBarrier = isBarrier 1486 | this.markersSnapshot = markersSnapshot 1487 | } 1488 | } 1489 | 1490 | class Transaction { 1491 | constructor (timestamp, operations, markersSnapshotBefore, markersSnapshotAfter) { 1492 | this.timestamp = timestamp 1493 | this.operations = operations 1494 | this.markersSnapshotBefore = markersSnapshotBefore 1495 | this.markersSnapshotAfter = markersSnapshotAfter 1496 | } 1497 | } 1498 | -------------------------------------------------------------------------------- /lib/point-helpers.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | exports.ZERO_POINT = Object.freeze({row: 0, column: 0}) 4 | 5 | exports.compare = function (a, b) { 6 | return primitiveCompare(a.row, a.column, b.row, b.column) 7 | } 8 | 9 | function primitiveCompare (rowA, columnA, rowB, columnB) { 10 | if (rowA === rowB) { 11 | return columnA - columnB 12 | } else { 13 | return rowA - rowB 14 | } 15 | } 16 | 17 | exports.traverse = function (start, distance) { 18 | if (distance.row === 0) 19 | return {row: start.row, column: start.column + distance.column} 20 | else { 21 | return {row: start.row + distance.row, column: distance.column} 22 | } 23 | } 24 | 25 | exports.traversal = function (end, start) { 26 | if (end.row === start.row) { 27 | return {row: 0, column: end.column - start.column} 28 | } else { 29 | return {row: end.row - start.row, column: end.column} 30 | } 31 | } 32 | 33 | exports.extentForText = function (text) { 34 | let row = 0 35 | let column = 0 36 | let index = 0 37 | while (index < text.length) { 38 | const char = text[index] 39 | if (char === '\n') { 40 | column = 0 41 | row++ 42 | } else { 43 | column++ 44 | } 45 | index++ 46 | } 47 | 48 | return {row, column} 49 | } 50 | 51 | exports.characterIndexForPosition = function (text, target) { 52 | // Previously we instantiated a point object here and mutated its fields, so 53 | // that we could use the `compare` function we already export. However, this 54 | // seems to trigger a weird optimization bug on v8 5.6.326.50 which causes 55 | // this function to return unpredictable results, so we use primitive-valued 56 | // variables instead. 57 | let row = 0 58 | let column = 0 59 | let index = 0 60 | while (primitiveCompare(row, column, target.row, target.column) < 0 && index <= text.length) { 61 | if (text[index] === '\n') { 62 | row++ 63 | column = 0 64 | } else { 65 | column++ 66 | } 67 | 68 | index++ 69 | } 70 | 71 | assert(primitiveCompare(row, column, target.row, target.column) <= 0, 'Target position should not exceed the extent of the given text') 72 | 73 | return index 74 | } 75 | -------------------------------------------------------------------------------- /lib/serialization.js: -------------------------------------------------------------------------------- 1 | const {Operation} = require('./teletype-crdt_pb') 2 | 3 | function serializeOperation (op) { 4 | const operationMessage = new Operation() 5 | switch (op.type) { 6 | case 'splice': 7 | operationMessage.setSplice(serializeSplice(op)) 8 | break 9 | case 'undo': 10 | operationMessage.setUndo(serializeUndo(op)) 11 | break 12 | case 'markers-update': 13 | operationMessage.setMarkersUpdate(serializeMarkersUpdate(op)) 14 | break 15 | default: 16 | throw new Error('Unknown operation type: ' + op.type) 17 | } 18 | return operationMessage 19 | } 20 | 21 | function serializeOperationBinary (op) { 22 | return serializeOperation(op).serializeBinary() 23 | } 24 | 25 | function serializeSplice (splice) { 26 | const spliceMessage = new Operation.Splice() 27 | spliceMessage.setSpliceId(serializeSpliceId(splice.spliceId)) 28 | if (splice.insertion) { 29 | spliceMessage.setInsertion(serializeInsertion(splice.insertion)) 30 | } 31 | if (splice.deletion) { 32 | spliceMessage.setDeletion(serializeDeletion(splice.deletion)) 33 | } 34 | return spliceMessage 35 | } 36 | 37 | function serializeInsertion (insertion) { 38 | const insertionMessage = new Operation.Splice.Insertion() 39 | insertionMessage.setText(insertion.text) 40 | insertionMessage.setLeftDependencyId(serializeSpliceId(insertion.leftDependencyId)) 41 | insertionMessage.setOffsetInLeftDependency(serializePoint(insertion.offsetInLeftDependency)) 42 | insertionMessage.setRightDependencyId(serializeSpliceId(insertion.rightDependencyId)) 43 | insertionMessage.setOffsetInRightDependency(serializePoint(insertion.offsetInRightDependency)) 44 | return insertionMessage 45 | } 46 | 47 | function serializeDeletion (deletion) { 48 | const deletionMessage = new Operation.Splice.Deletion() 49 | deletionMessage.setLeftDependencyId(serializeSpliceId(deletion.leftDependencyId)) 50 | deletionMessage.setOffsetInLeftDependency(serializePoint(deletion.offsetInLeftDependency)) 51 | deletionMessage.setRightDependencyId(serializeSpliceId(deletion.rightDependencyId)) 52 | deletionMessage.setOffsetInRightDependency(serializePoint(deletion.offsetInRightDependency)) 53 | const maxSeqsBySiteMessage = deletionMessage.getMaxSeqsBySiteMap() 54 | for (const site in deletion.maxSeqsBySite) { 55 | maxSeqsBySiteMessage.set(site, deletion.maxSeqsBySite[site]) 56 | } 57 | return deletionMessage 58 | } 59 | 60 | function serializeUndo (undo) { 61 | const undoMessage = new Operation.Undo() 62 | undoMessage.setSpliceId(serializeSpliceId(undo.spliceId)) 63 | undoMessage.setUndoCount(undo.undoCount) 64 | return undoMessage 65 | } 66 | 67 | function serializeMarkersUpdate ({siteId, updates}) { 68 | const markersUpdateMessage = new Operation.MarkersUpdate() 69 | markersUpdateMessage.setSiteId(siteId) 70 | const layerOperationsMessage = markersUpdateMessage.getLayerOperationsMap() 71 | for (const layerId in updates) { 72 | const markerUpdates = updates[layerId] 73 | const layerOperationMessage = new Operation.MarkersUpdate.LayerOperation() 74 | if (markerUpdates) { 75 | layerOperationMessage.setIsDeletion(false) 76 | const markerOperationsMessage = layerOperationMessage.getMarkerOperationsMap() 77 | for (const markerId in markerUpdates) { 78 | const markerUpdate = markerUpdates[markerId] 79 | const markerOperationMessage = new Operation.MarkersUpdate.MarkerOperation() 80 | if (markerUpdate) { 81 | markerOperationMessage.setIsDeletion(false) 82 | const {range, exclusive, reversed, tailed} = markerUpdate 83 | const markerUpdateMessage = new Operation.MarkersUpdate.MarkerUpdate() 84 | const logicalRangeMessage = new Operation.MarkersUpdate.LogicalRange() 85 | logicalRangeMessage.setStartDependencyId(serializeSpliceId(range.startDependencyId)) 86 | logicalRangeMessage.setOffsetInStartDependency(serializePoint(range.offsetInStartDependency)) 87 | logicalRangeMessage.setEndDependencyId(serializeSpliceId(range.endDependencyId)) 88 | logicalRangeMessage.setOffsetInEndDependency(serializePoint(range.offsetInEndDependency)) 89 | markerUpdateMessage.setRange(logicalRangeMessage) 90 | markerUpdateMessage.setExclusive(exclusive) 91 | markerUpdateMessage.setReversed(reversed) 92 | markerUpdateMessage.setTailed(tailed) 93 | markerOperationMessage.setMarkerUpdate(markerUpdateMessage) 94 | } else { 95 | markerOperationMessage.setIsDeletion(true) 96 | } 97 | markerOperationsMessage.set(markerId, markerOperationMessage) 98 | } 99 | } else { 100 | layerOperationMessage.setIsDeletion(true) 101 | } 102 | layerOperationsMessage.set(layerId, layerOperationMessage) 103 | } 104 | return markersUpdateMessage 105 | } 106 | 107 | function serializeSpliceId ({site, seq}) { 108 | const spliceIdMessage = new Operation.SpliceId() 109 | spliceIdMessage.setSite(site) 110 | spliceIdMessage.setSeq(seq) 111 | return spliceIdMessage 112 | } 113 | 114 | function serializePoint ({row, column}) { 115 | const pointMessage = new Operation.Point() 116 | pointMessage.setRow(row) 117 | pointMessage.setColumn(column) 118 | return pointMessage 119 | } 120 | 121 | function deserializeOperation (operationMessage) { 122 | if (operationMessage.hasSplice()) { 123 | return deserializeSplice(operationMessage.getSplice()) 124 | } else if (operationMessage.hasUndo()) { 125 | return deserializeUndo(operationMessage.getUndo()) 126 | } else if (operationMessage.hasMarkersUpdate()) { 127 | return deserializeMarkersUpdate(operationMessage.getMarkersUpdate()) 128 | } else { 129 | throw new Error('Unknown operation type') 130 | } 131 | } 132 | 133 | function deserializeOperationBinary (data) { 134 | return deserializeOperation(Operation.deserializeBinary(data)) 135 | } 136 | 137 | function deserializeSplice (spliceMessage) { 138 | const insertionMessage = spliceMessage.getInsertion() 139 | const deletionMessage = spliceMessage.getDeletion() 140 | return { 141 | type: 'splice', 142 | spliceId: deserializeSpliceId(spliceMessage.getSpliceId()), 143 | insertion: insertionMessage ? deserializeInsertion(insertionMessage) : null, 144 | deletion: deletionMessage ? deserializeDeletion(deletionMessage) : null 145 | } 146 | } 147 | 148 | function deserializeInsertion (insertionMessage) { 149 | return { 150 | text: insertionMessage.getText(), 151 | leftDependencyId: deserializeSpliceId(insertionMessage.getLeftDependencyId()), 152 | offsetInLeftDependency: deserializePoint(insertionMessage.getOffsetInLeftDependency()), 153 | rightDependencyId: deserializeSpliceId(insertionMessage.getRightDependencyId()), 154 | offsetInRightDependency: deserializePoint(insertionMessage.getOffsetInRightDependency()) 155 | } 156 | } 157 | 158 | function deserializeDeletion (deletionMessage) { 159 | const maxSeqsBySite = {} 160 | deletionMessage.getMaxSeqsBySiteMap().forEach((seq, site) => { 161 | maxSeqsBySite[site] = seq 162 | }) 163 | return { 164 | leftDependencyId: deserializeSpliceId(deletionMessage.getLeftDependencyId()), 165 | offsetInLeftDependency: deserializePoint(deletionMessage.getOffsetInLeftDependency()), 166 | rightDependencyId: deserializeSpliceId(deletionMessage.getRightDependencyId()), 167 | offsetInRightDependency: deserializePoint(deletionMessage.getOffsetInRightDependency()), 168 | maxSeqsBySite 169 | } 170 | } 171 | 172 | function deserializeUndo (undoMessage) { 173 | return { 174 | type: 'undo', 175 | spliceId: deserializeSpliceId(undoMessage.getSpliceId()), 176 | undoCount: undoMessage.getUndoCount() 177 | } 178 | } 179 | 180 | function deserializeMarkersUpdate (markersUpdateMessage) { 181 | const updates = {} 182 | 183 | markersUpdateMessage.getLayerOperationsMap().forEach((layerOperation, layerId) => { 184 | if (layerOperation.getIsDeletion()) { 185 | updates[layerId] = null 186 | } else { 187 | const markerUpdates = {} 188 | 189 | layerOperation.getMarkerOperationsMap().forEach((markerOperation, markerId) => { 190 | if (markerOperation.getIsDeletion()) { 191 | markerUpdates[markerId] = null 192 | } else { 193 | const markerUpdateMessage = markerOperation.getMarkerUpdate() 194 | const rangeMessage = markerUpdateMessage.getRange() 195 | const range = { 196 | startDependencyId: deserializeSpliceId(rangeMessage.getStartDependencyId()), 197 | offsetInStartDependency: deserializePoint(rangeMessage.getOffsetInStartDependency()), 198 | endDependencyId: deserializeSpliceId(rangeMessage.getEndDependencyId()), 199 | offsetInEndDependency: deserializePoint(rangeMessage.getOffsetInEndDependency()) 200 | } 201 | 202 | markerUpdates[markerId] = { 203 | range, 204 | exclusive: markerUpdateMessage.getExclusive(), 205 | reversed: markerUpdateMessage.getReversed(), 206 | tailed: markerUpdateMessage.getTailed() 207 | } 208 | } 209 | }) 210 | 211 | updates[layerId] = markerUpdates 212 | } 213 | }) 214 | 215 | return { 216 | type: 'markers-update', 217 | siteId: markersUpdateMessage.getSiteId(), 218 | updates 219 | } 220 | } 221 | 222 | function deserializeSpliceId (spliceIdMessage) { 223 | return { 224 | site: spliceIdMessage.getSite(), 225 | seq: spliceIdMessage.getSeq() 226 | } 227 | } 228 | 229 | function deserializePoint (pointMessage) { 230 | return { 231 | row: pointMessage.getRow(), 232 | column: pointMessage.getColumn() 233 | } 234 | } 235 | 236 | module.exports = { 237 | serializeOperation, deserializeOperation, 238 | serializeOperationBinary, deserializeOperationBinary, 239 | } 240 | -------------------------------------------------------------------------------- /lib/splay-tree.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class SplayTree { 3 | splayNode (node) { 4 | if (!node) return 5 | 6 | while (true) { 7 | if (this.isNodeLeftChild(this.getParent(node)) && this.isNodeRightChild(node)) { // zig-zag 8 | this.rotateNodeLeft(node) 9 | this.rotateNodeRight(node) 10 | } else if (this.isNodeRightChild(this.getParent(node)) && this.isNodeLeftChild(node)) { // zig-zag 11 | this.rotateNodeRight(node) 12 | this.rotateNodeLeft(node) 13 | } else if (this.isNodeLeftChild(this.getParent(node)) && this.isNodeLeftChild(node)) { // zig-zig 14 | this.rotateNodeRight(this.getParent(node)) 15 | this.rotateNodeRight(node) 16 | } else if (this.isNodeRightChild(this.getParent(node)) && this.isNodeRightChild(node)) { // zig-zig 17 | this.rotateNodeLeft(this.getParent(node)) 18 | this.rotateNodeLeft(node) 19 | } else { // zig 20 | if (this.isNodeLeftChild(node)) { 21 | this.rotateNodeRight(node) 22 | } else if (this.isNodeRightChild(node)) { 23 | this.rotateNodeLeft(node) 24 | } 25 | 26 | return 27 | } 28 | } 29 | } 30 | 31 | rotateNodeLeft (pivot) { 32 | const root = this.getParent(pivot) 33 | if (this.getParent(root)) { 34 | if (root === this.getLeft(this.getParent(root))) { 35 | this.setLeft(this.getParent(root), pivot) 36 | } else { 37 | this.setRight(this.getParent(root), pivot) 38 | } 39 | } else { 40 | this.root = pivot 41 | } 42 | this.setParent(pivot, this.getParent(root)) 43 | 44 | this.setRight(root, this.getLeft(pivot)) 45 | if (this.getRight(root)) this.setParent(this.getRight(root), root) 46 | 47 | this.setLeft(pivot, root) 48 | this.setParent(this.getLeft(pivot), pivot) 49 | 50 | this.updateSubtreeExtent(root) 51 | this.updateSubtreeExtent(pivot) 52 | } 53 | 54 | rotateNodeRight (pivot) { 55 | const root = this.getParent(pivot) 56 | if (this.getParent(root)) { 57 | if (root === this.getLeft(this.getParent(root))) { 58 | this.setLeft(this.getParent(root), pivot) 59 | } else { 60 | this.setRight(this.getParent(root), pivot) 61 | } 62 | } else { 63 | this.root = pivot 64 | } 65 | this.setParent(pivot, this.getParent(root)) 66 | 67 | this.setLeft(root, this.getRight(pivot)) 68 | if (this.getLeft(root)) this.setParent(this.getLeft(root), root) 69 | 70 | this.setRight(pivot, root) 71 | this.setParent(this.getRight(pivot), pivot) 72 | 73 | this.updateSubtreeExtent(root) 74 | this.updateSubtreeExtent(pivot) 75 | } 76 | 77 | isNodeLeftChild (node) { 78 | return node != null && this.getParent(node) != null && this.getLeft(this.getParent(node)) === node 79 | } 80 | 81 | isNodeRightChild (node) { 82 | return node != null && this.getParent(node) != null && this.getRight(this.getParent(node)) === node 83 | } 84 | 85 | getSuccessor (node) { 86 | if (this.getRight(node)) { 87 | node = this.getRight(node) 88 | while (this.getLeft(node)) { 89 | node = this.getLeft(node) 90 | } 91 | } else { 92 | while (this.getParent(node) && this.getRight(this.getParent(node)) === node) { 93 | node = this.getParent(node) 94 | } 95 | node = this.getParent(node) 96 | } 97 | return node 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/split-tree.js: -------------------------------------------------------------------------------- 1 | const SplayTree = require('./splay-tree') 2 | const {ZERO_POINT, compare, traverse, traversal, characterIndexForPosition} = require('./point-helpers') 3 | 4 | module.exports = 5 | class SplitTree extends SplayTree { 6 | constructor (segment) { 7 | super() 8 | this.startSegment = segment 9 | this.startSegment.splitLeft = null 10 | this.startSegment.splitRight = null 11 | this.startSegment.splitParent = null 12 | this.startSegment.splitSubtreeExtent = this.startSegment.extent 13 | this.root = this.startSegment 14 | } 15 | 16 | getStart () { 17 | return this.startSegment 18 | } 19 | 20 | getParent (node) { 21 | return node.splitParent 22 | } 23 | 24 | setParent (node, value) { 25 | node.splitParent = value 26 | } 27 | 28 | getLeft (node) { 29 | return node.splitLeft 30 | } 31 | 32 | setLeft (node, value) { 33 | node.splitLeft = value 34 | } 35 | 36 | getRight (node) { 37 | return node.splitRight 38 | } 39 | 40 | setRight (node, value) { 41 | node.splitRight = value 42 | } 43 | 44 | updateSubtreeExtent (node) { 45 | node.splitSubtreeExtent = ZERO_POINT 46 | if (node.splitLeft) node.splitSubtreeExtent = traverse(node.splitSubtreeExtent, node.splitLeft.splitSubtreeExtent) 47 | node.splitSubtreeExtent = traverse(node.splitSubtreeExtent, node.extent) 48 | if (node.splitRight) node.splitSubtreeExtent = traverse(node.splitSubtreeExtent, node.splitRight.splitSubtreeExtent) 49 | } 50 | 51 | findSegmentContainingOffset (offset) { 52 | let segment = this.root 53 | let leftAncestorEnd = ZERO_POINT 54 | while (segment) { 55 | let start = leftAncestorEnd 56 | if (segment.splitLeft) start = traverse(start, segment.splitLeft.splitSubtreeExtent) 57 | const end = traverse(start, segment.extent) 58 | 59 | if (compare(offset, start) <= 0 && segment.splitLeft) { 60 | segment = segment.splitLeft 61 | } else if (compare(offset, end) > 0) { 62 | leftAncestorEnd = end 63 | segment = segment.splitRight 64 | } else { 65 | this.splayNode(segment) 66 | return segment 67 | } 68 | } 69 | 70 | throw new Error('No segment found') 71 | } 72 | 73 | splitSegment (segment, offset) { 74 | const splitIndex = characterIndexForPosition(segment.text, offset) 75 | 76 | this.splayNode(segment) 77 | const suffix = Object.assign({}, segment) 78 | suffix.text = segment.text.slice(splitIndex) 79 | suffix.extent = traversal(segment.extent, offset) 80 | 81 | suffix.spliceId = Object.assign({}, segment.spliceId) 82 | suffix.offset = traverse(suffix.offset, offset) 83 | suffix.deletions = new Set(suffix.deletions) 84 | segment.text = segment.text.slice(0, splitIndex) 85 | segment.extent = offset 86 | segment.nextSplit = suffix 87 | 88 | this.root = suffix 89 | suffix.splitParent = null 90 | suffix.splitLeft = segment 91 | segment.splitParent = suffix 92 | suffix.splitRight = segment.splitRight 93 | if (suffix.splitRight) suffix.splitRight.splitParent = suffix 94 | segment.splitRight = null 95 | 96 | this.updateSubtreeExtent(segment) 97 | this.updateSubtreeExtent(suffix) 98 | 99 | return suffix 100 | } 101 | 102 | getSuccessor (segment) { 103 | return segment.nextSplit 104 | } 105 | 106 | getSegments () { 107 | const segments = [] 108 | let segment = this.getStart() 109 | while (segment) { 110 | segments.push(segment) 111 | segment = segment.nextSplit 112 | } 113 | return segments 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@atom/teletype-crdt", 3 | "version": "0.9.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "10.12.19", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz", 10 | "integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA==", 11 | "dev": true 12 | }, 13 | "ajv": { 14 | "version": "6.7.0", 15 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", 16 | "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", 17 | "dev": true, 18 | "requires": { 19 | "fast-deep-equal": "^2.0.1", 20 | "fast-json-stable-stringify": "^2.0.0", 21 | "json-schema-traverse": "^0.4.1", 22 | "uri-js": "^4.2.2" 23 | } 24 | }, 25 | "ansi-regex": { 26 | "version": "2.1.1", 27 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 28 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 29 | "dev": true 30 | }, 31 | "array-find-index": { 32 | "version": "1.0.2", 33 | "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", 34 | "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", 35 | "dev": true 36 | }, 37 | "asn1": { 38 | "version": "0.2.4", 39 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 40 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 41 | "dev": true, 42 | "requires": { 43 | "safer-buffer": "~2.1.0" 44 | } 45 | }, 46 | "assert-plus": { 47 | "version": "1.0.0", 48 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 49 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 50 | "dev": true 51 | }, 52 | "asynckit": { 53 | "version": "0.4.0", 54 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 55 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 56 | "dev": true 57 | }, 58 | "aws-sign2": { 59 | "version": "0.7.0", 60 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 61 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 62 | "dev": true 63 | }, 64 | "aws4": { 65 | "version": "1.8.0", 66 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 67 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", 68 | "dev": true 69 | }, 70 | "balanced-match": { 71 | "version": "1.0.0", 72 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 73 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 74 | "dev": true 75 | }, 76 | "bcrypt-pbkdf": { 77 | "version": "1.0.2", 78 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 79 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 80 | "dev": true, 81 | "requires": { 82 | "tweetnacl": "^0.14.3" 83 | } 84 | }, 85 | "brace-expansion": { 86 | "version": "1.1.11", 87 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 88 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 89 | "dev": true, 90 | "requires": { 91 | "balanced-match": "^1.0.0", 92 | "concat-map": "0.0.1" 93 | } 94 | }, 95 | "browser-stdout": { 96 | "version": "1.3.1", 97 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 98 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 99 | "dev": true 100 | }, 101 | "buffer-from": { 102 | "version": "1.1.1", 103 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 104 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 105 | "dev": true 106 | }, 107 | "builtin-modules": { 108 | "version": "1.1.1", 109 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 110 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 111 | "dev": true 112 | }, 113 | "camelcase": { 114 | "version": "2.1.1", 115 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", 116 | "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", 117 | "dev": true 118 | }, 119 | "camelcase-keys": { 120 | "version": "2.1.0", 121 | "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", 122 | "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", 123 | "dev": true, 124 | "requires": { 125 | "camelcase": "^2.0.0", 126 | "map-obj": "^1.0.0" 127 | } 128 | }, 129 | "caseless": { 130 | "version": "0.12.0", 131 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 132 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 133 | "dev": true 134 | }, 135 | "cliui": { 136 | "version": "3.2.0", 137 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", 138 | "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", 139 | "dev": true, 140 | "requires": { 141 | "string-width": "^1.0.1", 142 | "strip-ansi": "^3.0.1", 143 | "wrap-ansi": "^2.0.0" 144 | } 145 | }, 146 | "code-point-at": { 147 | "version": "1.1.0", 148 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 149 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", 150 | "dev": true 151 | }, 152 | "combined-stream": { 153 | "version": "1.0.7", 154 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", 155 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", 156 | "dev": true, 157 | "requires": { 158 | "delayed-stream": "~1.0.0" 159 | } 160 | }, 161 | "commander": { 162 | "version": "2.15.1", 163 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 164 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 165 | "dev": true 166 | }, 167 | "concat-map": { 168 | "version": "0.0.1", 169 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 170 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 171 | "dev": true 172 | }, 173 | "concat-stream": { 174 | "version": "1.6.2", 175 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 176 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 177 | "dev": true, 178 | "requires": { 179 | "buffer-from": "^1.0.0", 180 | "inherits": "^2.0.3", 181 | "readable-stream": "^2.2.2", 182 | "typedarray": "^0.0.6" 183 | }, 184 | "dependencies": { 185 | "isarray": { 186 | "version": "1.0.0", 187 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 188 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 189 | "dev": true 190 | }, 191 | "readable-stream": { 192 | "version": "2.3.6", 193 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 194 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 195 | "dev": true, 196 | "requires": { 197 | "core-util-is": "~1.0.0", 198 | "inherits": "~2.0.3", 199 | "isarray": "~1.0.0", 200 | "process-nextick-args": "~2.0.0", 201 | "safe-buffer": "~5.1.1", 202 | "string_decoder": "~1.1.1", 203 | "util-deprecate": "~1.0.1" 204 | } 205 | }, 206 | "string_decoder": { 207 | "version": "1.1.1", 208 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 209 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 210 | "dev": true, 211 | "requires": { 212 | "safe-buffer": "~5.1.0" 213 | } 214 | } 215 | } 216 | }, 217 | "core-util-is": { 218 | "version": "1.0.2", 219 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 220 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 221 | "dev": true 222 | }, 223 | "cross-spawn": { 224 | "version": "5.1.0", 225 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", 226 | "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", 227 | "dev": true, 228 | "requires": { 229 | "lru-cache": "^4.0.1", 230 | "shebang-command": "^1.2.0", 231 | "which": "^1.2.9" 232 | } 233 | }, 234 | "currently-unhandled": { 235 | "version": "0.4.1", 236 | "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", 237 | "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", 238 | "dev": true, 239 | "requires": { 240 | "array-find-index": "^1.0.1" 241 | } 242 | }, 243 | "dashdash": { 244 | "version": "1.14.1", 245 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 246 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 247 | "dev": true, 248 | "requires": { 249 | "assert-plus": "^1.0.0" 250 | } 251 | }, 252 | "debug": { 253 | "version": "2.6.9", 254 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 255 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 256 | "requires": { 257 | "ms": "2.0.0" 258 | } 259 | }, 260 | "decamelize": { 261 | "version": "1.2.0", 262 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 263 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 264 | "dev": true 265 | }, 266 | "deep-extend": { 267 | "version": "0.6.0", 268 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 269 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 270 | "dev": true 271 | }, 272 | "delayed-stream": { 273 | "version": "1.0.0", 274 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 275 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 276 | "dev": true 277 | }, 278 | "diff": { 279 | "version": "3.5.0", 280 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 281 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 282 | "dev": true 283 | }, 284 | "ecc-jsbn": { 285 | "version": "0.1.2", 286 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 287 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 288 | "dev": true, 289 | "requires": { 290 | "jsbn": "~0.1.0", 291 | "safer-buffer": "^2.1.0" 292 | } 293 | }, 294 | "electron": { 295 | "version": "4.0.3", 296 | "resolved": "https://registry.npmjs.org/electron/-/electron-4.0.3.tgz", 297 | "integrity": "sha512-wOBYnlv3Xgbwh9DAHBktP3sQcbCBuXMQi1NzPH3EMnbdYNqj+FnTzVLq0RYp0uSzdjR3fhAZ/E6wFSMLaAc5iw==", 298 | "dev": true, 299 | "requires": { 300 | "@types/node": "^10.12.18", 301 | "electron-download": "^4.1.0", 302 | "extract-zip": "^1.0.3" 303 | } 304 | }, 305 | "electron-download": { 306 | "version": "4.1.1", 307 | "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.1.tgz", 308 | "integrity": "sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==", 309 | "dev": true, 310 | "requires": { 311 | "debug": "^3.0.0", 312 | "env-paths": "^1.0.0", 313 | "fs-extra": "^4.0.1", 314 | "minimist": "^1.2.0", 315 | "nugget": "^2.0.1", 316 | "path-exists": "^3.0.0", 317 | "rc": "^1.2.1", 318 | "semver": "^5.4.1", 319 | "sumchecker": "^2.0.2" 320 | }, 321 | "dependencies": { 322 | "debug": { 323 | "version": "3.2.6", 324 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 325 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 326 | "dev": true, 327 | "requires": { 328 | "ms": "^2.1.1" 329 | } 330 | }, 331 | "ms": { 332 | "version": "2.1.1", 333 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 334 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", 335 | "dev": true 336 | } 337 | } 338 | }, 339 | "env-paths": { 340 | "version": "1.0.0", 341 | "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", 342 | "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", 343 | "dev": true 344 | }, 345 | "error-ex": { 346 | "version": "1.3.1", 347 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", 348 | "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", 349 | "dev": true, 350 | "requires": { 351 | "is-arrayish": "^0.2.1" 352 | } 353 | }, 354 | "escape-string-regexp": { 355 | "version": "1.0.5", 356 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 357 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 358 | "dev": true 359 | }, 360 | "execa": { 361 | "version": "0.7.0", 362 | "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", 363 | "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", 364 | "dev": true, 365 | "requires": { 366 | "cross-spawn": "^5.0.1", 367 | "get-stream": "^3.0.0", 368 | "is-stream": "^1.1.0", 369 | "npm-run-path": "^2.0.0", 370 | "p-finally": "^1.0.0", 371 | "signal-exit": "^3.0.0", 372 | "strip-eof": "^1.0.0" 373 | } 374 | }, 375 | "extend": { 376 | "version": "3.0.2", 377 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 378 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 379 | "dev": true 380 | }, 381 | "extract-zip": { 382 | "version": "1.6.7", 383 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", 384 | "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", 385 | "dev": true, 386 | "requires": { 387 | "concat-stream": "1.6.2", 388 | "debug": "2.6.9", 389 | "mkdirp": "0.5.1", 390 | "yauzl": "2.4.1" 391 | }, 392 | "dependencies": { 393 | "debug": { 394 | "version": "2.6.9", 395 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 396 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 397 | "dev": true, 398 | "requires": { 399 | "ms": "2.0.0" 400 | } 401 | } 402 | } 403 | }, 404 | "extsprintf": { 405 | "version": "1.3.0", 406 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 407 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 408 | "dev": true 409 | }, 410 | "fast-deep-equal": { 411 | "version": "2.0.1", 412 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 413 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", 414 | "dev": true 415 | }, 416 | "fast-json-stable-stringify": { 417 | "version": "2.0.0", 418 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 419 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 420 | "dev": true 421 | }, 422 | "fd-slicer": { 423 | "version": "1.0.1", 424 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", 425 | "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", 426 | "dev": true, 427 | "requires": { 428 | "pend": "~1.2.0" 429 | } 430 | }, 431 | "find-up": { 432 | "version": "1.1.2", 433 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", 434 | "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", 435 | "dev": true, 436 | "requires": { 437 | "path-exists": "^2.0.0", 438 | "pinkie-promise": "^2.0.0" 439 | }, 440 | "dependencies": { 441 | "path-exists": { 442 | "version": "2.1.0", 443 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", 444 | "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", 445 | "dev": true, 446 | "requires": { 447 | "pinkie-promise": "^2.0.0" 448 | } 449 | } 450 | } 451 | }, 452 | "forever-agent": { 453 | "version": "0.6.1", 454 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 455 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 456 | "dev": true 457 | }, 458 | "form-data": { 459 | "version": "2.3.3", 460 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 461 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 462 | "dev": true, 463 | "requires": { 464 | "asynckit": "^0.4.0", 465 | "combined-stream": "^1.0.6", 466 | "mime-types": "^2.1.12" 467 | } 468 | }, 469 | "fs-extra": { 470 | "version": "4.0.3", 471 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", 472 | "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", 473 | "dev": true, 474 | "requires": { 475 | "graceful-fs": "^4.1.2", 476 | "jsonfile": "^4.0.0", 477 | "universalify": "^0.1.0" 478 | } 479 | }, 480 | "fs.realpath": { 481 | "version": "1.0.0", 482 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 483 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 484 | "dev": true 485 | }, 486 | "get-caller-file": { 487 | "version": "1.0.2", 488 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", 489 | "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", 490 | "dev": true 491 | }, 492 | "get-stdin": { 493 | "version": "4.0.1", 494 | "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", 495 | "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", 496 | "dev": true 497 | }, 498 | "get-stream": { 499 | "version": "3.0.0", 500 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", 501 | "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", 502 | "dev": true 503 | }, 504 | "getpass": { 505 | "version": "0.1.7", 506 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 507 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 508 | "dev": true, 509 | "requires": { 510 | "assert-plus": "^1.0.0" 511 | } 512 | }, 513 | "glob": { 514 | "version": "7.1.2", 515 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 516 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 517 | "dev": true, 518 | "requires": { 519 | "fs.realpath": "^1.0.0", 520 | "inflight": "^1.0.4", 521 | "inherits": "2", 522 | "minimatch": "^3.0.4", 523 | "once": "^1.3.0", 524 | "path-is-absolute": "^1.0.0" 525 | } 526 | }, 527 | "google-protobuf": { 528 | "version": "3.4.0", 529 | "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.4.0.tgz", 530 | "integrity": "sha512-8gq3CyAZFyHJ5dZ0Du/OztbwxEHhHTNOtfr1cSUQYuW6lsrKjMU2DETvOlxPpYNKZ7zAW4xEE7UWqrTxH7bG6Q==" 531 | }, 532 | "graceful-fs": { 533 | "version": "4.1.11", 534 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 535 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", 536 | "dev": true 537 | }, 538 | "growl": { 539 | "version": "1.10.5", 540 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 541 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 542 | "dev": true 543 | }, 544 | "har-schema": { 545 | "version": "2.0.0", 546 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 547 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 548 | "dev": true 549 | }, 550 | "har-validator": { 551 | "version": "5.1.3", 552 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 553 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 554 | "dev": true, 555 | "requires": { 556 | "ajv": "^6.5.5", 557 | "har-schema": "^2.0.0" 558 | } 559 | }, 560 | "has-flag": { 561 | "version": "3.0.0", 562 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 563 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 564 | "dev": true 565 | }, 566 | "he": { 567 | "version": "1.1.1", 568 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 569 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 570 | "dev": true 571 | }, 572 | "hosted-git-info": { 573 | "version": "2.5.0", 574 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", 575 | "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", 576 | "dev": true 577 | }, 578 | "http-signature": { 579 | "version": "1.2.0", 580 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 581 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 582 | "dev": true, 583 | "requires": { 584 | "assert-plus": "^1.0.0", 585 | "jsprim": "^1.2.2", 586 | "sshpk": "^1.7.0" 587 | } 588 | }, 589 | "indent-string": { 590 | "version": "2.1.0", 591 | "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", 592 | "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", 593 | "dev": true, 594 | "requires": { 595 | "repeating": "^2.0.0" 596 | } 597 | }, 598 | "inflight": { 599 | "version": "1.0.6", 600 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 601 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 602 | "dev": true, 603 | "requires": { 604 | "once": "^1.3.0", 605 | "wrappy": "1" 606 | } 607 | }, 608 | "inherits": { 609 | "version": "2.0.3", 610 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 611 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 612 | "dev": true 613 | }, 614 | "ini": { 615 | "version": "1.3.5", 616 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 617 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", 618 | "dev": true 619 | }, 620 | "invert-kv": { 621 | "version": "1.0.0", 622 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", 623 | "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", 624 | "dev": true 625 | }, 626 | "is-arrayish": { 627 | "version": "0.2.1", 628 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 629 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 630 | "dev": true 631 | }, 632 | "is-builtin-module": { 633 | "version": "1.0.0", 634 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", 635 | "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", 636 | "dev": true, 637 | "requires": { 638 | "builtin-modules": "^1.0.0" 639 | } 640 | }, 641 | "is-finite": { 642 | "version": "1.0.2", 643 | "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", 644 | "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", 645 | "dev": true, 646 | "requires": { 647 | "number-is-nan": "^1.0.0" 648 | } 649 | }, 650 | "is-fullwidth-code-point": { 651 | "version": "1.0.0", 652 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 653 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 654 | "dev": true, 655 | "requires": { 656 | "number-is-nan": "^1.0.0" 657 | } 658 | }, 659 | "is-stream": { 660 | "version": "1.1.0", 661 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 662 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", 663 | "dev": true 664 | }, 665 | "is-typedarray": { 666 | "version": "1.0.0", 667 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 668 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 669 | "dev": true 670 | }, 671 | "is-utf8": { 672 | "version": "0.2.1", 673 | "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", 674 | "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", 675 | "dev": true 676 | }, 677 | "isarray": { 678 | "version": "0.0.1", 679 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 680 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 681 | "dev": true 682 | }, 683 | "isexe": { 684 | "version": "2.0.0", 685 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 686 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 687 | "dev": true 688 | }, 689 | "isstream": { 690 | "version": "0.1.2", 691 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 692 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", 693 | "dev": true 694 | }, 695 | "jsbn": { 696 | "version": "0.1.1", 697 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 698 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 699 | "dev": true 700 | }, 701 | "json-schema": { 702 | "version": "0.2.3", 703 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 704 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 705 | "dev": true 706 | }, 707 | "json-schema-traverse": { 708 | "version": "0.4.1", 709 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 710 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 711 | "dev": true 712 | }, 713 | "json-stringify-safe": { 714 | "version": "5.0.1", 715 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 716 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 717 | "dev": true 718 | }, 719 | "jsonfile": { 720 | "version": "4.0.0", 721 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 722 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 723 | "dev": true, 724 | "requires": { 725 | "graceful-fs": "^4.1.6" 726 | } 727 | }, 728 | "jsprim": { 729 | "version": "1.4.1", 730 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 731 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 732 | "dev": true, 733 | "requires": { 734 | "assert-plus": "1.0.0", 735 | "extsprintf": "1.3.0", 736 | "json-schema": "0.2.3", 737 | "verror": "1.10.0" 738 | } 739 | }, 740 | "lcid": { 741 | "version": "1.0.0", 742 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", 743 | "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", 744 | "dev": true, 745 | "requires": { 746 | "invert-kv": "^1.0.0" 747 | } 748 | }, 749 | "load-json-file": { 750 | "version": "1.1.0", 751 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", 752 | "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", 753 | "dev": true, 754 | "requires": { 755 | "graceful-fs": "^4.1.2", 756 | "parse-json": "^2.2.0", 757 | "pify": "^2.0.0", 758 | "pinkie-promise": "^2.0.0", 759 | "strip-bom": "^2.0.0" 760 | } 761 | }, 762 | "locate-path": { 763 | "version": "2.0.0", 764 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", 765 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", 766 | "dev": true, 767 | "requires": { 768 | "p-locate": "^2.0.0", 769 | "path-exists": "^3.0.0" 770 | }, 771 | "dependencies": { 772 | "path-exists": { 773 | "version": "3.0.0", 774 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 775 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 776 | "dev": true 777 | } 778 | } 779 | }, 780 | "loud-rejection": { 781 | "version": "1.6.0", 782 | "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", 783 | "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", 784 | "dev": true, 785 | "requires": { 786 | "currently-unhandled": "^0.4.1", 787 | "signal-exit": "^3.0.0" 788 | } 789 | }, 790 | "lru-cache": { 791 | "version": "4.1.1", 792 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", 793 | "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", 794 | "dev": true, 795 | "requires": { 796 | "pseudomap": "^1.0.2", 797 | "yallist": "^2.1.2" 798 | } 799 | }, 800 | "map-obj": { 801 | "version": "1.0.1", 802 | "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", 803 | "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", 804 | "dev": true 805 | }, 806 | "mem": { 807 | "version": "1.1.0", 808 | "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", 809 | "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", 810 | "dev": true, 811 | "requires": { 812 | "mimic-fn": "^1.0.0" 813 | } 814 | }, 815 | "meow": { 816 | "version": "3.7.0", 817 | "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", 818 | "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", 819 | "dev": true, 820 | "requires": { 821 | "camelcase-keys": "^2.0.0", 822 | "decamelize": "^1.1.2", 823 | "loud-rejection": "^1.0.0", 824 | "map-obj": "^1.0.1", 825 | "minimist": "^1.1.3", 826 | "normalize-package-data": "^2.3.4", 827 | "object-assign": "^4.0.1", 828 | "read-pkg-up": "^1.0.1", 829 | "redent": "^1.0.0", 830 | "trim-newlines": "^1.0.0" 831 | } 832 | }, 833 | "mime-db": { 834 | "version": "1.37.0", 835 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", 836 | "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", 837 | "dev": true 838 | }, 839 | "mime-types": { 840 | "version": "2.1.21", 841 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", 842 | "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", 843 | "dev": true, 844 | "requires": { 845 | "mime-db": "~1.37.0" 846 | } 847 | }, 848 | "mimic-fn": { 849 | "version": "1.1.0", 850 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", 851 | "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", 852 | "dev": true 853 | }, 854 | "minimatch": { 855 | "version": "3.0.4", 856 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 857 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 858 | "dev": true, 859 | "requires": { 860 | "brace-expansion": "^1.1.7" 861 | } 862 | }, 863 | "minimist": { 864 | "version": "1.2.0", 865 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 866 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 867 | "dev": true 868 | }, 869 | "mkdirp": { 870 | "version": "0.5.1", 871 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 872 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 873 | "dev": true, 874 | "requires": { 875 | "minimist": "0.0.8" 876 | }, 877 | "dependencies": { 878 | "minimist": { 879 | "version": "0.0.8", 880 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 881 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 882 | "dev": true 883 | } 884 | } 885 | }, 886 | "mocha": { 887 | "version": "5.2.0", 888 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 889 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 890 | "dev": true, 891 | "requires": { 892 | "browser-stdout": "1.3.1", 893 | "commander": "2.15.1", 894 | "debug": "3.1.0", 895 | "diff": "3.5.0", 896 | "escape-string-regexp": "1.0.5", 897 | "glob": "7.1.2", 898 | "growl": "1.10.5", 899 | "he": "1.1.1", 900 | "minimatch": "3.0.4", 901 | "mkdirp": "0.5.1", 902 | "supports-color": "5.4.0" 903 | }, 904 | "dependencies": { 905 | "debug": { 906 | "version": "3.1.0", 907 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 908 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 909 | "dev": true, 910 | "requires": { 911 | "ms": "2.0.0" 912 | } 913 | } 914 | } 915 | }, 916 | "ms": { 917 | "version": "2.0.0", 918 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 919 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 920 | }, 921 | "normalize-package-data": { 922 | "version": "2.4.0", 923 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", 924 | "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", 925 | "dev": true, 926 | "requires": { 927 | "hosted-git-info": "^2.1.4", 928 | "is-builtin-module": "^1.0.0", 929 | "semver": "2 || 3 || 4 || 5", 930 | "validate-npm-package-license": "^3.0.1" 931 | } 932 | }, 933 | "npm-run-path": { 934 | "version": "2.0.2", 935 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", 936 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", 937 | "dev": true, 938 | "requires": { 939 | "path-key": "^2.0.0" 940 | } 941 | }, 942 | "nugget": { 943 | "version": "2.0.1", 944 | "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", 945 | "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", 946 | "dev": true, 947 | "requires": { 948 | "debug": "^2.1.3", 949 | "minimist": "^1.1.0", 950 | "pretty-bytes": "^1.0.2", 951 | "progress-stream": "^1.1.0", 952 | "request": "^2.45.0", 953 | "single-line-log": "^1.1.2", 954 | "throttleit": "0.0.2" 955 | } 956 | }, 957 | "number-is-nan": { 958 | "version": "1.0.1", 959 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 960 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", 961 | "dev": true 962 | }, 963 | "oauth-sign": { 964 | "version": "0.9.0", 965 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 966 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 967 | "dev": true 968 | }, 969 | "object-assign": { 970 | "version": "4.1.1", 971 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 972 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 973 | "dev": true 974 | }, 975 | "object-keys": { 976 | "version": "0.4.0", 977 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", 978 | "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", 979 | "dev": true 980 | }, 981 | "once": { 982 | "version": "1.4.0", 983 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 984 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 985 | "dev": true, 986 | "requires": { 987 | "wrappy": "1" 988 | } 989 | }, 990 | "os-locale": { 991 | "version": "2.1.0", 992 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", 993 | "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", 994 | "dev": true, 995 | "requires": { 996 | "execa": "^0.7.0", 997 | "lcid": "^1.0.0", 998 | "mem": "^1.1.0" 999 | } 1000 | }, 1001 | "p-finally": { 1002 | "version": "1.0.0", 1003 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 1004 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", 1005 | "dev": true 1006 | }, 1007 | "p-limit": { 1008 | "version": "1.1.0", 1009 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", 1010 | "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", 1011 | "dev": true 1012 | }, 1013 | "p-locate": { 1014 | "version": "2.0.0", 1015 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", 1016 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", 1017 | "dev": true, 1018 | "requires": { 1019 | "p-limit": "^1.1.0" 1020 | } 1021 | }, 1022 | "parse-json": { 1023 | "version": "2.2.0", 1024 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 1025 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 1026 | "dev": true, 1027 | "requires": { 1028 | "error-ex": "^1.2.0" 1029 | } 1030 | }, 1031 | "path-exists": { 1032 | "version": "3.0.0", 1033 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 1034 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 1035 | "dev": true 1036 | }, 1037 | "path-is-absolute": { 1038 | "version": "1.0.1", 1039 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1040 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1041 | "dev": true 1042 | }, 1043 | "path-key": { 1044 | "version": "2.0.1", 1045 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 1046 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 1047 | "dev": true 1048 | }, 1049 | "path-type": { 1050 | "version": "1.1.0", 1051 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", 1052 | "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", 1053 | "dev": true, 1054 | "requires": { 1055 | "graceful-fs": "^4.1.2", 1056 | "pify": "^2.0.0", 1057 | "pinkie-promise": "^2.0.0" 1058 | } 1059 | }, 1060 | "pend": { 1061 | "version": "1.2.0", 1062 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 1063 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", 1064 | "dev": true 1065 | }, 1066 | "performance-now": { 1067 | "version": "2.1.0", 1068 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 1069 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 1070 | "dev": true 1071 | }, 1072 | "pify": { 1073 | "version": "2.3.0", 1074 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1075 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 1076 | "dev": true 1077 | }, 1078 | "pinkie": { 1079 | "version": "2.0.4", 1080 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 1081 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 1082 | "dev": true 1083 | }, 1084 | "pinkie-promise": { 1085 | "version": "2.0.1", 1086 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 1087 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 1088 | "dev": true, 1089 | "requires": { 1090 | "pinkie": "^2.0.0" 1091 | } 1092 | }, 1093 | "pretty-bytes": { 1094 | "version": "1.0.4", 1095 | "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", 1096 | "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", 1097 | "dev": true, 1098 | "requires": { 1099 | "get-stdin": "^4.0.1", 1100 | "meow": "^3.1.0" 1101 | } 1102 | }, 1103 | "process-nextick-args": { 1104 | "version": "2.0.0", 1105 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 1106 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", 1107 | "dev": true 1108 | }, 1109 | "progress-stream": { 1110 | "version": "1.2.0", 1111 | "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", 1112 | "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", 1113 | "dev": true, 1114 | "requires": { 1115 | "speedometer": "~0.1.2", 1116 | "through2": "~0.2.3" 1117 | } 1118 | }, 1119 | "pseudomap": { 1120 | "version": "1.0.2", 1121 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 1122 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", 1123 | "dev": true 1124 | }, 1125 | "psl": { 1126 | "version": "1.1.31", 1127 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", 1128 | "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", 1129 | "dev": true 1130 | }, 1131 | "punycode": { 1132 | "version": "2.1.1", 1133 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1134 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1135 | "dev": true 1136 | }, 1137 | "qs": { 1138 | "version": "6.5.2", 1139 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 1140 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", 1141 | "dev": true 1142 | }, 1143 | "random-seed": { 1144 | "version": "0.3.0", 1145 | "resolved": "https://registry.npmjs.org/random-seed/-/random-seed-0.3.0.tgz", 1146 | "integrity": "sha1-2UXy4fOPSejViRNDG4v2u5N1Vs0=", 1147 | "dev": true, 1148 | "requires": { 1149 | "json-stringify-safe": "^5.0.1" 1150 | } 1151 | }, 1152 | "rc": { 1153 | "version": "1.2.8", 1154 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 1155 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 1156 | "dev": true, 1157 | "requires": { 1158 | "deep-extend": "^0.6.0", 1159 | "ini": "~1.3.0", 1160 | "minimist": "^1.2.0", 1161 | "strip-json-comments": "~2.0.1" 1162 | } 1163 | }, 1164 | "read-pkg": { 1165 | "version": "1.1.0", 1166 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", 1167 | "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", 1168 | "dev": true, 1169 | "requires": { 1170 | "load-json-file": "^1.0.0", 1171 | "normalize-package-data": "^2.3.2", 1172 | "path-type": "^1.0.0" 1173 | } 1174 | }, 1175 | "read-pkg-up": { 1176 | "version": "1.0.1", 1177 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", 1178 | "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", 1179 | "dev": true, 1180 | "requires": { 1181 | "find-up": "^1.0.0", 1182 | "read-pkg": "^1.0.0" 1183 | } 1184 | }, 1185 | "readable-stream": { 1186 | "version": "1.1.14", 1187 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 1188 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 1189 | "dev": true, 1190 | "requires": { 1191 | "core-util-is": "~1.0.0", 1192 | "inherits": "~2.0.1", 1193 | "isarray": "0.0.1", 1194 | "string_decoder": "~0.10.x" 1195 | } 1196 | }, 1197 | "redent": { 1198 | "version": "1.0.0", 1199 | "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", 1200 | "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", 1201 | "dev": true, 1202 | "requires": { 1203 | "indent-string": "^2.1.0", 1204 | "strip-indent": "^1.0.1" 1205 | } 1206 | }, 1207 | "repeating": { 1208 | "version": "2.0.1", 1209 | "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", 1210 | "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", 1211 | "dev": true, 1212 | "requires": { 1213 | "is-finite": "^1.0.0" 1214 | } 1215 | }, 1216 | "request": { 1217 | "version": "2.88.0", 1218 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 1219 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 1220 | "dev": true, 1221 | "requires": { 1222 | "aws-sign2": "~0.7.0", 1223 | "aws4": "^1.8.0", 1224 | "caseless": "~0.12.0", 1225 | "combined-stream": "~1.0.6", 1226 | "extend": "~3.0.2", 1227 | "forever-agent": "~0.6.1", 1228 | "form-data": "~2.3.2", 1229 | "har-validator": "~5.1.0", 1230 | "http-signature": "~1.2.0", 1231 | "is-typedarray": "~1.0.0", 1232 | "isstream": "~0.1.2", 1233 | "json-stringify-safe": "~5.0.1", 1234 | "mime-types": "~2.1.19", 1235 | "oauth-sign": "~0.9.0", 1236 | "performance-now": "^2.1.0", 1237 | "qs": "~6.5.2", 1238 | "safe-buffer": "^5.1.2", 1239 | "tough-cookie": "~2.4.3", 1240 | "tunnel-agent": "^0.6.0", 1241 | "uuid": "^3.3.2" 1242 | } 1243 | }, 1244 | "require-directory": { 1245 | "version": "2.1.1", 1246 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1247 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 1248 | "dev": true 1249 | }, 1250 | "require-main-filename": { 1251 | "version": "1.0.1", 1252 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", 1253 | "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", 1254 | "dev": true 1255 | }, 1256 | "safe-buffer": { 1257 | "version": "5.1.2", 1258 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1259 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 1260 | "dev": true 1261 | }, 1262 | "safer-buffer": { 1263 | "version": "2.1.2", 1264 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1265 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1266 | "dev": true 1267 | }, 1268 | "semver": { 1269 | "version": "5.4.1", 1270 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 1271 | "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=", 1272 | "dev": true 1273 | }, 1274 | "set-blocking": { 1275 | "version": "2.0.0", 1276 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1277 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", 1278 | "dev": true 1279 | }, 1280 | "shebang-command": { 1281 | "version": "1.2.0", 1282 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1283 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1284 | "dev": true, 1285 | "requires": { 1286 | "shebang-regex": "^1.0.0" 1287 | } 1288 | }, 1289 | "shebang-regex": { 1290 | "version": "1.0.0", 1291 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1292 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1293 | "dev": true 1294 | }, 1295 | "signal-exit": { 1296 | "version": "3.0.2", 1297 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1298 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 1299 | "dev": true 1300 | }, 1301 | "single-line-log": { 1302 | "version": "1.1.2", 1303 | "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", 1304 | "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", 1305 | "dev": true, 1306 | "requires": { 1307 | "string-width": "^1.0.1" 1308 | } 1309 | }, 1310 | "spdx-correct": { 1311 | "version": "1.0.2", 1312 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", 1313 | "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", 1314 | "dev": true, 1315 | "requires": { 1316 | "spdx-license-ids": "^1.0.2" 1317 | } 1318 | }, 1319 | "spdx-expression-parse": { 1320 | "version": "1.0.4", 1321 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", 1322 | "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", 1323 | "dev": true 1324 | }, 1325 | "spdx-license-ids": { 1326 | "version": "1.2.2", 1327 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", 1328 | "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", 1329 | "dev": true 1330 | }, 1331 | "speedometer": { 1332 | "version": "0.1.4", 1333 | "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", 1334 | "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", 1335 | "dev": true 1336 | }, 1337 | "sshpk": { 1338 | "version": "1.16.1", 1339 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1340 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1341 | "dev": true, 1342 | "requires": { 1343 | "asn1": "~0.2.3", 1344 | "assert-plus": "^1.0.0", 1345 | "bcrypt-pbkdf": "^1.0.0", 1346 | "dashdash": "^1.12.0", 1347 | "ecc-jsbn": "~0.1.1", 1348 | "getpass": "^0.1.1", 1349 | "jsbn": "~0.1.0", 1350 | "safer-buffer": "^2.0.2", 1351 | "tweetnacl": "~0.14.0" 1352 | } 1353 | }, 1354 | "string-width": { 1355 | "version": "1.0.2", 1356 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 1357 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 1358 | "dev": true, 1359 | "requires": { 1360 | "code-point-at": "^1.0.0", 1361 | "is-fullwidth-code-point": "^1.0.0", 1362 | "strip-ansi": "^3.0.0" 1363 | } 1364 | }, 1365 | "string_decoder": { 1366 | "version": "0.10.31", 1367 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 1368 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", 1369 | "dev": true 1370 | }, 1371 | "strip-ansi": { 1372 | "version": "3.0.1", 1373 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1374 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1375 | "dev": true, 1376 | "requires": { 1377 | "ansi-regex": "^2.0.0" 1378 | } 1379 | }, 1380 | "strip-bom": { 1381 | "version": "2.0.0", 1382 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", 1383 | "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", 1384 | "dev": true, 1385 | "requires": { 1386 | "is-utf8": "^0.2.0" 1387 | } 1388 | }, 1389 | "strip-eof": { 1390 | "version": "1.0.0", 1391 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", 1392 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", 1393 | "dev": true 1394 | }, 1395 | "strip-indent": { 1396 | "version": "1.0.1", 1397 | "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", 1398 | "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", 1399 | "dev": true, 1400 | "requires": { 1401 | "get-stdin": "^4.0.1" 1402 | } 1403 | }, 1404 | "strip-json-comments": { 1405 | "version": "2.0.1", 1406 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1407 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 1408 | "dev": true 1409 | }, 1410 | "sumchecker": { 1411 | "version": "2.0.2", 1412 | "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", 1413 | "integrity": "sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=", 1414 | "dev": true, 1415 | "requires": { 1416 | "debug": "^2.2.0" 1417 | } 1418 | }, 1419 | "supports-color": { 1420 | "version": "5.4.0", 1421 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 1422 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 1423 | "dev": true, 1424 | "requires": { 1425 | "has-flag": "^3.0.0" 1426 | } 1427 | }, 1428 | "throttleit": { 1429 | "version": "0.0.2", 1430 | "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", 1431 | "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", 1432 | "dev": true 1433 | }, 1434 | "through2": { 1435 | "version": "0.2.3", 1436 | "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", 1437 | "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", 1438 | "dev": true, 1439 | "requires": { 1440 | "readable-stream": "~1.1.9", 1441 | "xtend": "~2.1.1" 1442 | } 1443 | }, 1444 | "tough-cookie": { 1445 | "version": "2.4.3", 1446 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 1447 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 1448 | "dev": true, 1449 | "requires": { 1450 | "psl": "^1.1.24", 1451 | "punycode": "^1.4.1" 1452 | }, 1453 | "dependencies": { 1454 | "punycode": { 1455 | "version": "1.4.1", 1456 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 1457 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", 1458 | "dev": true 1459 | } 1460 | } 1461 | }, 1462 | "trim-newlines": { 1463 | "version": "1.0.0", 1464 | "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", 1465 | "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", 1466 | "dev": true 1467 | }, 1468 | "tunnel-agent": { 1469 | "version": "0.6.0", 1470 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1471 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1472 | "dev": true, 1473 | "requires": { 1474 | "safe-buffer": "^5.0.1" 1475 | } 1476 | }, 1477 | "tweetnacl": { 1478 | "version": "0.14.5", 1479 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1480 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 1481 | "dev": true 1482 | }, 1483 | "typedarray": { 1484 | "version": "0.0.6", 1485 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1486 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 1487 | "dev": true 1488 | }, 1489 | "universalify": { 1490 | "version": "0.1.2", 1491 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 1492 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", 1493 | "dev": true 1494 | }, 1495 | "uri-js": { 1496 | "version": "4.2.2", 1497 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1498 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1499 | "dev": true, 1500 | "requires": { 1501 | "punycode": "^2.1.0" 1502 | } 1503 | }, 1504 | "util-deprecate": { 1505 | "version": "1.0.2", 1506 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1507 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1508 | "dev": true 1509 | }, 1510 | "uuid": { 1511 | "version": "3.3.2", 1512 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1513 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", 1514 | "dev": true 1515 | }, 1516 | "validate-npm-package-license": { 1517 | "version": "3.0.1", 1518 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", 1519 | "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", 1520 | "dev": true, 1521 | "requires": { 1522 | "spdx-correct": "~1.0.0", 1523 | "spdx-expression-parse": "~1.0.0" 1524 | } 1525 | }, 1526 | "verror": { 1527 | "version": "1.10.0", 1528 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1529 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1530 | "dev": true, 1531 | "requires": { 1532 | "assert-plus": "^1.0.0", 1533 | "core-util-is": "1.0.2", 1534 | "extsprintf": "^1.2.0" 1535 | } 1536 | }, 1537 | "which": { 1538 | "version": "1.3.0", 1539 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", 1540 | "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", 1541 | "dev": true, 1542 | "requires": { 1543 | "isexe": "^2.0.0" 1544 | } 1545 | }, 1546 | "which-module": { 1547 | "version": "2.0.0", 1548 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 1549 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", 1550 | "dev": true 1551 | }, 1552 | "wrap-ansi": { 1553 | "version": "2.1.0", 1554 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", 1555 | "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", 1556 | "dev": true, 1557 | "requires": { 1558 | "string-width": "^1.0.1", 1559 | "strip-ansi": "^3.0.1" 1560 | } 1561 | }, 1562 | "wrappy": { 1563 | "version": "1.0.2", 1564 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1565 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1566 | "dev": true 1567 | }, 1568 | "xtend": { 1569 | "version": "2.1.2", 1570 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", 1571 | "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", 1572 | "dev": true, 1573 | "requires": { 1574 | "object-keys": "~0.4.0" 1575 | } 1576 | }, 1577 | "y18n": { 1578 | "version": "3.2.1", 1579 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", 1580 | "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", 1581 | "dev": true 1582 | }, 1583 | "yallist": { 1584 | "version": "2.1.2", 1585 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 1586 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", 1587 | "dev": true 1588 | }, 1589 | "yargs": { 1590 | "version": "8.0.2", 1591 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", 1592 | "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", 1593 | "dev": true, 1594 | "requires": { 1595 | "camelcase": "^4.1.0", 1596 | "cliui": "^3.2.0", 1597 | "decamelize": "^1.1.1", 1598 | "get-caller-file": "^1.0.1", 1599 | "os-locale": "^2.0.0", 1600 | "read-pkg-up": "^2.0.0", 1601 | "require-directory": "^2.1.1", 1602 | "require-main-filename": "^1.0.1", 1603 | "set-blocking": "^2.0.0", 1604 | "string-width": "^2.0.0", 1605 | "which-module": "^2.0.0", 1606 | "y18n": "^3.2.1", 1607 | "yargs-parser": "^7.0.0" 1608 | }, 1609 | "dependencies": { 1610 | "ansi-regex": { 1611 | "version": "3.0.0", 1612 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 1613 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 1614 | "dev": true 1615 | }, 1616 | "camelcase": { 1617 | "version": "4.1.0", 1618 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", 1619 | "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", 1620 | "dev": true 1621 | }, 1622 | "find-up": { 1623 | "version": "2.1.0", 1624 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", 1625 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", 1626 | "dev": true, 1627 | "requires": { 1628 | "locate-path": "^2.0.0" 1629 | } 1630 | }, 1631 | "is-fullwidth-code-point": { 1632 | "version": "2.0.0", 1633 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 1634 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 1635 | "dev": true 1636 | }, 1637 | "load-json-file": { 1638 | "version": "2.0.0", 1639 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", 1640 | "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", 1641 | "dev": true, 1642 | "requires": { 1643 | "graceful-fs": "^4.1.2", 1644 | "parse-json": "^2.2.0", 1645 | "pify": "^2.0.0", 1646 | "strip-bom": "^3.0.0" 1647 | } 1648 | }, 1649 | "path-type": { 1650 | "version": "2.0.0", 1651 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", 1652 | "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", 1653 | "dev": true, 1654 | "requires": { 1655 | "pify": "^2.0.0" 1656 | } 1657 | }, 1658 | "read-pkg": { 1659 | "version": "2.0.0", 1660 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", 1661 | "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", 1662 | "dev": true, 1663 | "requires": { 1664 | "load-json-file": "^2.0.0", 1665 | "normalize-package-data": "^2.3.2", 1666 | "path-type": "^2.0.0" 1667 | } 1668 | }, 1669 | "read-pkg-up": { 1670 | "version": "2.0.0", 1671 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", 1672 | "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", 1673 | "dev": true, 1674 | "requires": { 1675 | "find-up": "^2.0.0", 1676 | "read-pkg": "^2.0.0" 1677 | } 1678 | }, 1679 | "string-width": { 1680 | "version": "2.1.1", 1681 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1682 | "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", 1683 | "dev": true, 1684 | "requires": { 1685 | "is-fullwidth-code-point": "^2.0.0", 1686 | "strip-ansi": "^4.0.0" 1687 | } 1688 | }, 1689 | "strip-ansi": { 1690 | "version": "4.0.0", 1691 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1692 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1693 | "dev": true, 1694 | "requires": { 1695 | "ansi-regex": "^3.0.0" 1696 | } 1697 | }, 1698 | "strip-bom": { 1699 | "version": "3.0.0", 1700 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1701 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 1702 | "dev": true 1703 | } 1704 | } 1705 | }, 1706 | "yargs-parser": { 1707 | "version": "7.0.0", 1708 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", 1709 | "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", 1710 | "dev": true, 1711 | "requires": { 1712 | "camelcase": "^4.1.0" 1713 | }, 1714 | "dependencies": { 1715 | "camelcase": { 1716 | "version": "4.1.0", 1717 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", 1718 | "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", 1719 | "dev": true 1720 | } 1721 | } 1722 | }, 1723 | "yauzl": { 1724 | "version": "2.4.1", 1725 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", 1726 | "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", 1727 | "dev": true, 1728 | "requires": { 1729 | "fd-slicer": "~1.0.1" 1730 | } 1731 | } 1732 | } 1733 | } 1734 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@atom/teletype-crdt", 3 | "version": "0.9.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node script/test", 8 | "compile-protobuf": "protoc --js_out=import_style=commonjs,binary:lib teletype-crdt.proto" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/atom/teletype.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/atom/teletype/issues" 16 | }, 17 | "license": "MIT", 18 | "devDependencies": { 19 | "electron": "^4.0.3", 20 | "mocha": "^5.2.0", 21 | "random-seed": "^0.3.0", 22 | "yargs": "^8.0.1" 23 | }, 24 | "dependencies": { 25 | "google-protobuf": "^3.3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /script/electron-test-runner/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /script/electron-test-runner/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | // Module to control application life. 3 | const app = electron.app 4 | // Module to create native browser window. 5 | const BrowserWindow = electron.BrowserWindow 6 | 7 | // Keep a global reference of the window object, if you don't, the window will 8 | // be closed automatically when the JavaScript object is garbage collected. 9 | let mainWindow 10 | 11 | function createWindow () { 12 | // Create the browser window. 13 | mainWindow = new BrowserWindow({ 14 | width: 800, 15 | height: 600, 16 | webPreferences: {nodeIntegration: true} 17 | }) 18 | 19 | // and load the index.html of the app. 20 | mainWindow.loadURL(`file://${__dirname}/index.html`) 21 | 22 | // Open the DevTools. 23 | mainWindow.webContents.openDevTools() 24 | 25 | // Emitted when the window is closed. 26 | mainWindow.on('closed', function () { 27 | // Dereference the window object, usually you would store windows 28 | // in an array if your app supports multi windows, this is the time 29 | // when you should delete the corresponding element. 30 | mainWindow = null 31 | }) 32 | } 33 | 34 | // This method will be called when Electron has finished 35 | // initialization and is ready to create browser windows. 36 | // Some APIs can only be used after this event occurs. 37 | app.on('ready', createWindow) 38 | 39 | // Quit when all windows are closed. 40 | app.on('window-all-closed', function () { 41 | // On OS X it is common for applications and their menu bar 42 | // to stay active until the user quits explicitly with Cmd + Q 43 | if (process.platform !== 'darwin') { 44 | app.quit() 45 | } 46 | }) 47 | 48 | app.on('activate', function () { 49 | // On OS X it's common to re-create a window in the app when the 50 | // dock icon is clicked and there are no other windows open. 51 | if (mainWindow === null) { 52 | createWindow() 53 | } 54 | }) 55 | 56 | // In this file you can include the rest of your app's specific main process 57 | // code. You can also put them in separate files and require them here. 58 | -------------------------------------------------------------------------------- /script/electron-test-runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runner", 3 | "main": "main.js", 4 | "scripts": { 5 | "start": "electron ." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /script/electron-test-runner/renderer.js: -------------------------------------------------------------------------------- 1 | const remote = require('electron').remote 2 | const Mocha = require('mocha') 3 | const mocha = new Mocha() 4 | 5 | for (let path of remote.process.argv.slice(2)) { 6 | let resolvedFiles = Mocha.utils.lookupFiles(path, ['js'], true); 7 | if (typeof resolvedFiles === "string") { 8 | resolvedFiles = [resolvedFiles]; 9 | } 10 | for (let resolvedFile of resolvedFiles) { 11 | mocha.addFile(resolvedFile) 12 | } 13 | } 14 | mocha.ui('tdd') 15 | mocha.useColors(false) 16 | mocha.run() 17 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const childProcess = require('child_process') 4 | const path = require('path') 5 | 6 | const argv = 7 | require('yargs') 8 | .usage('Run tests') 9 | .boolean('interactive') 10 | .describe('interactive', 'Run tests in an Electron window') 11 | .alias('i', 'interactive') 12 | .boolean('rebuild') 13 | .describe('rebuild', 'Rebuild against the correct Node headers before running tests') 14 | .alias('r', 'rebuild') 15 | .help() 16 | .argv 17 | 18 | // Rebuild module against correct Node headers if requested 19 | if (argv.rebuild) { 20 | let env 21 | if (argv.interactive) { 22 | const [major, minor] = Array.from(require('../package.json').devDependencies.electron.match(/(\d+)\.(\d+)/)).slice(1) 23 | const electronVersion = `${major}.${minor}.0` 24 | env = Object.assign({}, process.env, { 25 | npm_config_runtime: 'electron', 26 | npm_config_target: electronVersion, 27 | npm_config_disturl: 'https://atom.io/download/atom-shell' 28 | }) 29 | console.log(`Rebuilding native modules against Electron ${electronVersion}`) 30 | } else { 31 | env = process.env 32 | } 33 | childProcess.spawnSync('npm', ['rebuild'], {env, stdio: 'inherit'}) 34 | } 35 | 36 | normalizeCommand = (command) => { 37 | return process.platform === 'win32' ? path.normalize(command + '.cmd') : command 38 | } 39 | 40 | // Run tests 41 | if (argv.interactive) { 42 | childProcess.spawnSync(normalizeCommand('./node_modules/.bin/electron'), ['script/electron-test-runner', 'test/**/*test.js'], {stdio: 'inherit', cwd: path.join(__dirname, '..')}) 43 | } else { 44 | process.exit(childProcess.spawnSync(normalizeCommand('./node_modules/.bin/mocha'), ['--ui=tdd'], {stdio: 'inherit'}).status) 45 | } 46 | -------------------------------------------------------------------------------- /teletype-crdt.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Operation { 4 | oneof variant { 5 | Splice splice = 1; 6 | Undo undo = 2; 7 | MarkersUpdate markers_update = 3; 8 | } 9 | 10 | message Splice { 11 | SpliceId splice_id = 1; 12 | Insertion insertion = 2; 13 | Deletion deletion = 3; 14 | 15 | message Insertion { 16 | string text = 2; 17 | SpliceId left_dependency_id = 3; 18 | Point offset_in_left_dependency = 4; 19 | SpliceId right_dependency_id = 5; 20 | Point offset_in_right_dependency = 6; 21 | } 22 | 23 | message Deletion { 24 | SpliceId left_dependency_id = 2; 25 | Point offset_in_left_dependency = 3; 26 | SpliceId right_dependency_id = 4; 27 | Point offset_in_right_dependency = 5; 28 | map max_seqs_by_site = 6; 29 | } 30 | } 31 | 32 | message Undo { 33 | SpliceId splice_id = 1; 34 | uint32 undo_count = 2; 35 | } 36 | 37 | message MarkersUpdate { 38 | uint32 site_id = 1; 39 | map layer_operations = 2; 40 | 41 | message LayerOperation { 42 | bool is_deletion = 1; 43 | map marker_operations = 2; 44 | } 45 | 46 | message MarkerOperation { 47 | bool is_deletion = 1; 48 | MarkerUpdate marker_update = 2; 49 | } 50 | 51 | message MarkerUpdate { 52 | LogicalRange range = 1; 53 | bool exclusive = 2; 54 | bool reversed = 3; 55 | bool tailed = 4; 56 | } 57 | 58 | message LogicalRange { 59 | SpliceId start_dependency_id = 1; 60 | Point offset_in_start_dependency = 2; 61 | SpliceId end_dependency_id = 3; 62 | Point offset_in_end_dependency = 4; 63 | } 64 | } 65 | 66 | message SpliceId { 67 | uint32 site = 1; 68 | uint32 seq = 2; 69 | } 70 | 71 | message Point { 72 | uint32 row = 1; 73 | uint32 column = 2; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/document.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const Random = require('random-seed') 3 | const LocalDocument = require('./helpers/local-document') 4 | const Document = require('../lib/document') 5 | const Peer = require('./helpers/peer') 6 | const {ZERO_POINT} = require('../lib/point-helpers') 7 | 8 | suite('Document', () => { 9 | suite('operations', () => { 10 | test('concurrent inserts at 0', () => { 11 | const replica1 = buildDocument(1) 12 | const replica2 = replicateDocument(2, replica1) 13 | 14 | const ops1 = performInsert(replica1, {row: 0, column: 0}, 'a') 15 | const ops2 = performInsert(replica2, {row: 0, column: 0}, 'b') 16 | integrateOperations(replica1, ops2) 17 | integrateOperations(replica2, ops1) 18 | 19 | assert.equal(replica1.testLocalDocument.text, 'ab') 20 | assert.equal(replica2.testLocalDocument.text, 'ab') 21 | }) 22 | 23 | test('concurrent inserts at the same position inside a previous insertion', () => { 24 | const replica1 = buildDocument(1, 'ABCDEFG') 25 | const replica2 = replicateDocument(2, replica1) 26 | 27 | const ops1 = performInsert(replica1, {row: 0, column: 2}, '+++') 28 | const ops2 = performInsert(replica2, {row: 0, column: 2}, '***') 29 | integrateOperations(replica1, ops2) 30 | integrateOperations(replica2, ops1) 31 | 32 | assert.equal(replica1.testLocalDocument.text, 'AB+++***CDEFG') 33 | assert.equal(replica2.testLocalDocument.text, 'AB+++***CDEFG') 34 | }) 35 | 36 | test('concurrent inserts at different positions inside a previous insertion', () => { 37 | const replica1 = buildDocument(1, 'ABCDEFG') 38 | const replica2 = replicateDocument(2, replica1) 39 | 40 | const ops1 = performInsert(replica1, {row: 0, column: 6}, '+++') 41 | const ops2 = performInsert(replica2, {row: 0, column: 2}, '***') 42 | integrateOperations(replica1, ops2) 43 | integrateOperations(replica2, ops1) 44 | 45 | assert.equal(replica1.testLocalDocument.text, 'AB***CDEF+++G') 46 | assert.equal(replica2.testLocalDocument.text, 'AB***CDEF+++G') 47 | }) 48 | 49 | test('concurrent overlapping deletions', () => { 50 | const replica1 = buildDocument(1, 'ABCDEFG') 51 | const replica2 = replicateDocument(2, replica1) 52 | 53 | const ops1 = performDelete(replica1, {row: 0, column: 2}, {row: 0, column: 5}) 54 | const ops2 = performDelete(replica2, {row: 0, column: 4}, {row: 0, column: 6}) 55 | integrateOperations(replica1, ops2) 56 | integrateOperations(replica2, ops1) 57 | 58 | assert.equal(replica1.testLocalDocument.text, 'ABG') 59 | assert.equal(replica2.testLocalDocument.text, 'ABG') 60 | }) 61 | 62 | test('undoing an insertion containing other insertions', () => { 63 | const replica1 = buildDocument(1) 64 | const replica2 = replicateDocument(2, replica1) 65 | 66 | const ops1 = performInsert(replica1, {row: 0, column: 0}, 'ABCDEFG') 67 | integrateOperations(replica2, ops1) 68 | 69 | const ops2 = performInsert(replica1, {row: 0, column: 3}, '***') 70 | integrateOperations(replica2, ops2) 71 | 72 | const ops1Undo = performUndoOrRedoOperations(replica1, ops1) 73 | integrateOperations(replica2, ops1Undo) 74 | 75 | assert.equal(replica1.testLocalDocument.text, '***') 76 | assert.equal(replica2.testLocalDocument.text, '***') 77 | }) 78 | 79 | test('undoing an insertion containing a deletion', () => { 80 | const replica1 = buildDocument(1) 81 | const replica2 = replicateDocument(2, replica1) 82 | 83 | const ops1 = performInsert(replica1, {row: 0, column: 0}, 'ABCDEFG') 84 | integrateOperations(replica2, ops1) 85 | 86 | const ops2 = performDelete(replica1, {row: 0, column: 3}, {row: 0, column: 6}) 87 | integrateOperations(replica2, ops2) 88 | 89 | const ops1Undo = performUndoOrRedoOperations(replica1, ops1) 90 | integrateOperations(replica2, ops1Undo) 91 | 92 | assert.equal(replica1.testLocalDocument.text, '') 93 | assert.equal(replica2.testLocalDocument.text, '') 94 | }) 95 | 96 | test('undoing a deletion that overlaps another concurrent deletion', () => { 97 | const replica1 = buildDocument(1, 'ABCDEFG') 98 | const replica2 = replicateDocument(2, replica1) 99 | 100 | const ops1 = performDelete(replica1, {row: 0, column: 1}, {row: 0, column: 4}) 101 | const ops2 = performDelete(replica2, {row: 0, column: 3}, {row: 0, column: 6}) 102 | integrateOperations(replica1, ops2) 103 | integrateOperations(replica2, ops1) 104 | const ops2Undo = performUndoOrRedoOperations(replica1, ops2) 105 | integrateOperations(replica2, ops2Undo) 106 | 107 | assert.equal(replica1.testLocalDocument.text, 'AEFG') 108 | assert.equal(replica2.testLocalDocument.text, 'AEFG') 109 | }) 110 | 111 | test('inserting in the middle of an undone deletion and then redoing the deletion', () => { 112 | const document = buildDocument(1, 'ABCDEFG') 113 | 114 | const deleteOps = performDelete(document, {row: 0, column: 1}, {row: 0, column: 6}) 115 | performUndoOrRedoOperations(document, deleteOps) 116 | performInsert(document, {row: 0, column: 3}, '***') 117 | performUndoOrRedoOperations(document, deleteOps) // Redo 118 | 119 | assert.equal(document.testLocalDocument.text, 'A***G') 120 | }) 121 | 122 | test('applying remote operations generated by a copy of the local replica', () => { 123 | const localReplica = buildDocument(1) 124 | const remoteReplica = buildDocument(1) 125 | 126 | integrateOperations(localReplica, performInsert(remoteReplica, {row: 0, column: 0}, 'ABCDEFG')) 127 | integrateOperations(localReplica, performInsert(remoteReplica, {row: 0, column: 3}, '+++')) 128 | performInsert(localReplica, {row: 0, column: 1}, '***') 129 | 130 | assert.equal(localReplica.testLocalDocument.text, 'A***BC+++DEFG') 131 | }) 132 | 133 | test('updating marker layers', () => { 134 | const replica1 = buildDocument(1, 'ABCDEFG') 135 | const replica2 = replicateDocument(2, replica1) 136 | 137 | const insert1 = performInsert(replica1, {row: 0, column: 6}, '+++') 138 | performInsert(replica2, {row: 0, column: 2}, '**') 139 | integrateOperations(replica2, insert1) 140 | 141 | integrateOperations(replica2, performUpdateMarkers(replica1, { 142 | 1: { // Create a marker layer with 1 marker 143 | 1: { 144 | range: { 145 | start: {row: 0, column: 1}, 146 | end: {row: 0, column: 9} 147 | }, 148 | exclusive: false, 149 | reversed: false, 150 | tailed: true 151 | } 152 | } 153 | })) 154 | assert.deepEqual(replica1.getMarkers(), { 155 | 1: { // Site 1 156 | 1: { // Marker layer 1 157 | 1: { // Marker 1 158 | range: { 159 | start: {row: 0, column: 1}, 160 | end: {row: 0, column: 9} 161 | }, 162 | exclusive: false, 163 | reversed: false, 164 | tailed: true 165 | } 166 | } 167 | } 168 | }) 169 | assert.deepEqual(replica2.getMarkers(), { 170 | 1: { // Site 1 171 | 1: { // Marker layer 1 172 | 1: { // Marker 1 173 | range: { 174 | start: {row: 0, column: 1}, 175 | end: {row: 0, column: 11} 176 | }, 177 | exclusive: false, 178 | reversed: false, 179 | tailed: true 180 | } 181 | } 182 | } 183 | }) 184 | assert.deepEqual(replica2.testLocalDocument.markers, replica2.getMarkers()) 185 | 186 | integrateOperations(replica2, performUpdateMarkers(replica1, { 187 | 1: { 188 | 1: { // Update marker 189 | range: { 190 | start: {row: 0, column: 2}, 191 | end: {row: 0, column: 10} 192 | }, 193 | exclusive: true, 194 | reversed: true 195 | }, 196 | 2: { // Create marker (with default values for exclusive, reversed, and tailed) 197 | range: { 198 | start: {row: 0, column: 0}, 199 | end: {row: 0, column: 1} 200 | } 201 | } 202 | }, 203 | 2: { // Create marker layer with 1 marker 204 | 1: { 205 | range: { 206 | start: {row: 0, column: 1}, 207 | end: {row: 0, column: 2} 208 | } 209 | } 210 | } 211 | })) 212 | assert.deepEqual(replica1.getMarkers(), { 213 | 1: { 214 | 1: { 215 | 1: { 216 | range: { 217 | start: {row: 0, column: 2}, 218 | end: {row: 0, column: 10} 219 | }, 220 | exclusive: true, 221 | reversed: true, 222 | tailed: true 223 | }, 224 | 2: { 225 | range: { 226 | start: {row: 0, column: 0}, 227 | end: {row: 0, column: 1} 228 | }, 229 | exclusive: false, 230 | reversed: false, 231 | tailed: true 232 | } 233 | }, 234 | 2: { 235 | 1: { 236 | range: { 237 | start: {row: 0, column: 1}, 238 | end: {row: 0, column: 2} 239 | }, 240 | exclusive: false, 241 | reversed: false, 242 | tailed: true 243 | } 244 | } 245 | } 246 | }) 247 | assert.deepEqual(replica2.getMarkers(), { 248 | 1: { 249 | 1: { 250 | 1: { 251 | range: { 252 | start: {row: 0, column: 4}, 253 | end: {row: 0, column: 12} 254 | }, 255 | exclusive: true, 256 | reversed: true, 257 | tailed: true 258 | }, 259 | 2: { 260 | range: { 261 | start: {row: 0, column: 0}, 262 | end: {row: 0, column: 1} 263 | }, 264 | exclusive: false, 265 | reversed: false, 266 | tailed: true 267 | } 268 | }, 269 | 2: { 270 | 1: { 271 | range: { 272 | start: {row: 0, column: 1}, 273 | end: {row: 0, column: 4} 274 | }, 275 | exclusive: false, 276 | reversed: false, 277 | tailed: true 278 | } 279 | } 280 | } 281 | }) 282 | assert.deepEqual(replica2.testLocalDocument.markers, replica2.getMarkers()) 283 | 284 | integrateOperations(replica2, performUpdateMarkers(replica1, { 285 | 1: { 286 | 2: null // Delete marker 287 | }, 288 | 2: null // Delete marker layer 289 | })) 290 | assert.deepEqual(replica1.getMarkers(), { 291 | 1: { 292 | 1: { 293 | 1: { 294 | range: { 295 | start: {row: 0, column: 2}, 296 | end: {row: 0, column: 10} 297 | }, 298 | exclusive: true, 299 | reversed: true, 300 | tailed: true 301 | }, 302 | } 303 | } 304 | }) 305 | assert.deepEqual(replica2.getMarkers(), { 306 | 1: { 307 | 1: { 308 | 1: { 309 | range: { 310 | start: {row: 0, column: 4}, 311 | end: {row: 0, column: 12} 312 | }, 313 | exclusive: true, 314 | reversed: true, 315 | tailed: true 316 | }, 317 | } 318 | } 319 | }) 320 | assert.deepEqual(replica2.testLocalDocument.markers, replica2.getMarkers()) 321 | }) 322 | 323 | test('deferring marker updates until the dependencies of their logical ranges arrive', () => { 324 | const replica1 = buildDocument(1) 325 | const replica2 = replicateDocument(2, replica1) 326 | 327 | const insertion1 = performInsert(replica1, {row: 0, column: 0}, 'ABCDEFG') 328 | const insertion2 = performInsert(replica1, {row: 0, column: 4}, 'WXYZ') 329 | 330 | const layerUpdate1 = replica1.updateMarkers({ 331 | 1: { 332 | // This only depends on insertion 1 333 | 1: { 334 | range: { 335 | start: {row: 0, column: 1}, 336 | end: {row: 0, column: 3} 337 | } 338 | }, 339 | // This depends on insertion 2 340 | 2: { 341 | range: { 342 | start: {row: 0, column: 5}, 343 | end: {row: 0, column: 7} 344 | } 345 | }, 346 | // This depends on insertion 2 but will be overwritten before 347 | // insertion 2 arrives at site 2 348 | 3: { 349 | range: { 350 | start: {row: 0, column: 5}, 351 | end: {row: 0, column: 7} 352 | } 353 | } 354 | } 355 | }) 356 | 357 | const layerUpdate2 = replica1.updateMarkers({ 358 | 1: { 359 | 3: { 360 | range: { 361 | start: {row: 0, column: 1}, 362 | end: {row: 0, column: 3} 363 | } 364 | } 365 | } 366 | }) 367 | 368 | replica2.integrateOperations(insertion1) 369 | { 370 | const {markerUpdates} = replica2.integrateOperations(layerUpdate1.concat(layerUpdate2)) 371 | assert.deepEqual(markerUpdates, { 372 | 1: { 373 | 1: { 374 | 1: { 375 | range: { 376 | start: {row: 0, column: 1}, 377 | end: {row: 0, column: 3} 378 | }, 379 | exclusive: false, 380 | reversed: false, 381 | tailed: true 382 | }, 383 | 3: { 384 | range: { 385 | start: {row: 0, column: 1}, 386 | end: {row: 0, column: 3} 387 | }, 388 | exclusive: false, 389 | reversed: false, 390 | tailed: true 391 | } 392 | } 393 | } 394 | }) 395 | } 396 | 397 | { 398 | const {markerUpdates} = replica2.integrateOperations(insertion2) 399 | assert.deepEqual(markerUpdates, { 400 | 1: { 401 | 1: { 402 | 2: { 403 | range: { 404 | start: {row: 0, column: 5}, 405 | end: {row: 0, column: 7} 406 | }, 407 | exclusive: false, 408 | reversed: false, 409 | tailed: true 410 | } 411 | } 412 | } 413 | }) 414 | } 415 | }) 416 | }) 417 | 418 | suite('history', () => { 419 | test('basic undo and redo', () => { 420 | const replicaA = buildDocument(1) 421 | const replicaB = replicateDocument(2, replicaA) 422 | 423 | integrateOperations(replicaB, performInsert(replicaA, {row: 0, column: 0}, 'a1 ')) 424 | integrateOperations(replicaA, performInsert(replicaB, {row: 0, column: 3}, 'b1 ')) 425 | integrateOperations(replicaB, performInsert(replicaA, {row: 0, column: 6}, 'a2 ')) 426 | integrateOperations(replicaA, performInsert(replicaB, {row: 0, column: 9}, 'b2')) 427 | integrateOperations(replicaA, performSetTextInRange(replicaB, {row: 0, column: 3}, {row: 0, column: 5}, 'b3')) 428 | assert.equal(replicaA.testLocalDocument.text, 'a1 b3 a2 b2') 429 | assert.equal(replicaB.testLocalDocument.text, 'a1 b3 a2 b2') 430 | 431 | { 432 | integrateOperations(replicaA, performUndo(replicaB).operations) 433 | assert.equal(replicaA.testLocalDocument.text, 'a1 b1 a2 b2') 434 | assert.equal(replicaB.testLocalDocument.text, 'a1 b1 a2 b2') 435 | } 436 | 437 | { 438 | integrateOperations(replicaB, performUndo(replicaA).operations) 439 | assert.equal(replicaA.testLocalDocument.text, 'a1 b1 b2') 440 | assert.equal(replicaB.testLocalDocument.text, 'a1 b1 b2') 441 | } 442 | 443 | { 444 | integrateOperations(replicaB, performRedo(replicaA).operations) 445 | assert.equal(replicaA.testLocalDocument.text, 'a1 b1 a2 b2') 446 | assert.equal(replicaB.testLocalDocument.text, 'a1 b1 a2 b2') 447 | } 448 | 449 | { 450 | integrateOperations(replicaA, performRedo(replicaB).operations) 451 | assert.equal(replicaA.testLocalDocument.text, 'a1 b3 a2 b2') 452 | assert.equal(replicaB.testLocalDocument.text, 'a1 b3 a2 b2') 453 | } 454 | 455 | { 456 | integrateOperations(replicaA, performUndo(replicaB).operations) 457 | assert.equal(replicaA.testLocalDocument.text, 'a1 b1 a2 b2') 458 | assert.equal(replicaB.testLocalDocument.text, 'a1 b1 a2 b2') 459 | } 460 | }) 461 | 462 | test('does not allow the initial text to be undone', () => { 463 | const document = buildDocument(1, 'hello') 464 | performInsert(document, {row: 0, column: 5}, ' world') 465 | assert.notEqual(document.undo(), null) 466 | assert.equal(document.getText(), 'hello') 467 | assert.equal(document.undo(), null) 468 | assert.equal(document.getText(), 'hello') 469 | }) 470 | 471 | test('constructing the document with an initial history state', () => { 472 | const document = new Document({ 473 | siteId: 1, 474 | history: { 475 | nextCheckpointId: 4, 476 | baseText: 'a ', 477 | undoStack: [ 478 | { 479 | type: 'transaction', 480 | changes: [ 481 | {oldStart: point(0, 2), oldEnd: point(0, 2), newStart: point(0, 2), newEnd: point(0, 4), oldText: '', newText: 'b '} 482 | ], 483 | markersBefore: {1: {1: {range: range(point(0, 0), point(0, 2))}}}, 484 | markersAfter: {1: {1: {range: range(point(0, 2), point(0, 4))}}} 485 | }, 486 | { 487 | type: 'checkpoint', 488 | id: 2, 489 | markers: {1: {1: {range: range(point(0, 2), point(0, 4))}}} 490 | }, 491 | { 492 | type: 'transaction', 493 | changes: [ 494 | {oldStart: point(0, 4), oldEnd: point(0, 4), newStart: point(0, 4), newEnd: point(0, 6), oldText: '', newText: 'c '} 495 | ], 496 | markersBefore: {1: {1: {range: range(point(0, 2), point(0, 4))}}}, 497 | markersAfter: {1: {1: {range: range(point(0, 4), point(0, 6))}}} 498 | } 499 | ], 500 | redoStack: [ 501 | { 502 | type: 'transaction', 503 | changes: [ 504 | {oldStart: point(0, 0), oldEnd: point(0, 0), newStart: point(0, 0), newEnd: point(0, 2), oldText: '', newText: 'z '}, 505 | {oldStart: point(0, 8), oldEnd: point(0, 8), newStart: point(0, 10), newEnd: point(0, 11), oldText: '', newText: 'e'} 506 | ], 507 | markersBefore: {1: {1: {range: range(point(0, 6), point(0, 8))}}}, 508 | markersAfter: {1: {1: {range: range(point(0, 0), point(0, 2))}}} 509 | }, 510 | { 511 | type: 'transaction', 512 | changes: [ 513 | {oldStart: point(0, 6), oldEnd: point(0, 6), newStart: point(0, 6), newEnd: point(0, 8), oldText: '', newText: 'd '} 514 | ], 515 | markersBefore: {1: {1: {range: range(point(0, 4), point(0, 6))}}}, 516 | markersAfter: {1: {1: {range: range(point(0, 6), point(0, 8))}}} 517 | }, 518 | { 519 | type: 'checkpoint', 520 | id: 3, 521 | markers: {1: {1: {range: range(point(0, 4), point(0, 6))}}} 522 | } 523 | ] 524 | } 525 | }) 526 | 527 | assert.equal(document.getText(), 'a b c ') 528 | { 529 | const {markers} = document.redo() 530 | assert.equal(document.getText(), 'a b c d ') 531 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 6), point(0, 8))}}}) 532 | } 533 | { 534 | const {markers} = document.redo() 535 | assert.equal(document.getText(), 'z a b c d e') 536 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 0), point(0, 2))}}}) 537 | } 538 | { 539 | const {markers} = document.undo() 540 | assert.equal(document.getText(), 'a b c d ') 541 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 6), point(0, 8))}}}) 542 | } 543 | { 544 | const {markers} = document.undo() 545 | assert.equal(document.getText(), 'a b c ') 546 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 4), point(0, 6))}}}) 547 | } 548 | { 549 | const {markers} = document.undo() 550 | assert.equal(document.getText(), 'a b ') 551 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 2), point(0, 4))}}}) 552 | } 553 | { 554 | const {markers} = document.undo() 555 | assert.equal(document.getText(), 'a ') 556 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 0), point(0, 2))}}}) 557 | } 558 | assert(document.undo() == null) 559 | assert.equal(document.getText(), 'a ') 560 | 561 | // Redo everything 562 | while (document.redo()) {} 563 | 564 | // Ensure we set the next checkpoint id appropriately 565 | const checkpoint = document.createCheckpoint() 566 | assert.equal(checkpoint, 4) 567 | 568 | { 569 | const {markers} = document.revertToCheckpoint(3) 570 | assert.equal(document.getText(), 'a b c ') 571 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 4), point(0, 6))}}}) 572 | } 573 | { 574 | const {markers} = document.revertToCheckpoint(2) 575 | assert.equal(document.getText(), 'a b ') 576 | assert.deepEqual(markers, {1: {1: {range: range(point(0, 2), point(0, 4))}}}) 577 | } 578 | }) 579 | 580 | test('clearing undo and redo stacks', () => { 581 | const document = buildDocument(1) 582 | performInsert(document, {row: 0, column: 0}, 'a') 583 | document.clearUndoStack() 584 | performInsert(document, {row: 0, column: 1}, 'b') 585 | performInsert(document, {row: 0, column: 2}, 'c') 586 | document.undo() 587 | document.undo() 588 | assert.equal(document.getText(), 'a') 589 | document.redo() 590 | assert.equal(document.getText(), 'ab') 591 | document.clearRedoStack() 592 | document.redo() 593 | assert.equal(document.getText(), 'ab') 594 | 595 | // Clears the redo stack on changes 596 | document.undo() 597 | performInsert(document, {row: 0, column: 1}, 'd') 598 | assert.equal(document.getText(), 'ad') 599 | document.redo() 600 | assert.equal(document.getText(), 'ad') 601 | }) 602 | 603 | test('grouping changes since a checkpoint', () => { 604 | const replicaA = buildDocument(1) 605 | const replicaB = replicateDocument(2, replicaA) 606 | 607 | integrateOperations(replicaB, performInsert(replicaA, {row: 0, column: 0}, 'a1 ')) 608 | const checkpoint = replicaA.createCheckpoint({markers: { 609 | 1: { 610 | 1: {range: buildRange(0, 1), exclusive: true, a: 1}, 611 | }, 612 | 2: { 613 | 1: {range: buildRange(1, 2), b: 2} 614 | } 615 | }}) 616 | integrateOperations(replicaB, performSetTextInRange(replicaA, {row: 0, column: 1}, {row: 0, column: 3}, '2 a3 ')) 617 | integrateOperations(replicaB, performDelete(replicaA, {row: 0, column: 5}, {row: 0, column: 6})) 618 | integrateOperations(replicaA, performInsert(replicaB, {row: 0, column: 0}, 'b1 ')) 619 | assert.equal(replicaA.testLocalDocument.text, 'b1 a2 a3') 620 | assert.equal(replicaB.testLocalDocument.text, replicaA.testLocalDocument.text) 621 | assert.deepEqual(replicaB.testLocalDocument.markers, replicaA.testLocalDocument.markers) 622 | 623 | const changes = replicaA.groupChangesSinceCheckpoint(checkpoint, { 624 | markers: { 625 | 1: { 626 | 1: {range: buildRange(3, 5), c: 3}, 627 | } 628 | } 629 | }) 630 | 631 | assert.deepEqual(changes, [ 632 | { 633 | oldStart: {row: 0, column: 4}, 634 | oldEnd: {row: 0, column: 6}, 635 | oldText: "1 ", 636 | newStart: {row: 0, column: 4}, 637 | newEnd: {row: 0, column: 8}, 638 | newText: "2 a3" 639 | } 640 | ]) 641 | assert.equal(replicaA.testLocalDocument.text, 'b1 a2 a3') 642 | assert.equal(replicaB.testLocalDocument.text, 'b1 a2 a3') 643 | 644 | { 645 | const {operations, markers} = performUndo(replicaA) 646 | integrateOperations(replicaB, operations) 647 | assert.equal(replicaA.testLocalDocument.text, 'b1 a1 ') 648 | assert.equal(replicaB.testLocalDocument.text, replicaA.testLocalDocument.text) 649 | assert.deepEqual(markers, { 650 | 1: { 651 | 1: {range: buildRange(3, 4), exclusive: true, a: 1}, 652 | }, 653 | 2: { 654 | 1: {range: buildRange(4, 5), b: 2} 655 | } 656 | }) 657 | } 658 | 659 | { 660 | const {operations, markers} = performRedo(replicaA) 661 | integrateOperations(replicaB, operations) 662 | assert.equal(replicaA.testLocalDocument.text, 'b1 a2 a3') 663 | assert.equal(replicaB.testLocalDocument.text, replicaA.testLocalDocument.text) 664 | assert.deepEqual(markers, { 665 | 1: { 666 | 1: {range: buildRange(3, 5), c: 3}, 667 | } 668 | }) 669 | } 670 | 671 | integrateOperations(replicaA, performUndo(replicaB).operations) 672 | 673 | { 674 | const {operations, markers} = performUndo(replicaA) 675 | integrateOperations(replicaB, operations) 676 | assert.equal(replicaA.testLocalDocument.text, 'a1 ') 677 | assert.equal(replicaB.testLocalDocument.text, replicaA.testLocalDocument.text) 678 | assert.deepEqual(markers, { 679 | 1: { 680 | 1: {range: buildRange(0, 1), exclusive: true, a: 1}, 681 | }, 682 | 2: { 683 | 1: {range: buildRange(1, 2), b: 2} 684 | } 685 | }) 686 | } 687 | 688 | // Delete checkpoint 689 | assert.deepEqual(replicaA.groupChangesSinceCheckpoint(checkpoint, {deleteCheckpoint: true}), []) 690 | assert.equal(replicaA.groupChangesSinceCheckpoint(checkpoint), false) 691 | }) 692 | 693 | test('does not allow grouping changes past a barrier checkpoint', () => { 694 | const document = buildDocument(1) 695 | 696 | const checkpointBeforeBarrier = document.createCheckpoint({isBarrier: false}) 697 | performInsert(document, {row: 0, column: 0}, 'a') 698 | const barrierCheckpoint = document.createCheckpoint({isBarrier: true}) 699 | performInsert(document, {row: 0, column: 1}, 'b') 700 | assert.equal(document.groupChangesSinceCheckpoint(checkpointBeforeBarrier), false) 701 | 702 | performInsert(document, {row: 0, column: 2}, 'c') 703 | const checkpointAfterBarrier = document.createCheckpoint({isBarrier: false}) 704 | const changes = document.groupChangesSinceCheckpoint(barrierCheckpoint) 705 | assert.deepEqual(changes, [ 706 | { 707 | oldStart: {row: 0, column: 1}, 708 | oldEnd: {row: 0, column: 1}, 709 | oldText: '', 710 | newStart: {row: 0, column: 1}, 711 | newEnd: {row: 0, column: 3}, 712 | newText: 'bc' 713 | } 714 | ]) 715 | }) 716 | 717 | test('reverting to a checkpoint', () => { 718 | const replicaA = buildDocument(1) 719 | const replicaB = replicateDocument(2, replicaA) 720 | 721 | integrateOperations(replicaB, performInsert(replicaA, {row: 0, column: 0}, 'a1 ')) 722 | const checkpoint = replicaA.createCheckpoint({markers: { 723 | 1: { 724 | 1: {range: buildRange(0, 1), exclusive: true, a: 1}, 725 | }, 726 | 2: { 727 | 1: {range: buildRange(1, 2), b: 2} 728 | } 729 | }}) 730 | integrateOperations(replicaB, performSetTextInRange(replicaA, {row: 0, column: 1}, {row: 0, column: 3}, '2 a3 ')) 731 | integrateOperations(replicaB, performDelete(replicaA, {row: 0, column: 5}, {row: 0, column: 6})) 732 | integrateOperations(replicaA, performInsert(replicaB, {row: 0, column: 0}, 'b1 ')) 733 | 734 | assert.equal(replicaA.testLocalDocument.text, 'b1 a2 a3') 735 | assert.equal(replicaB.testLocalDocument.text, replicaA.testLocalDocument.text) 736 | 737 | const {operations, markers} = performRevertToCheckpoint(replicaA, checkpoint) 738 | integrateOperations(replicaB, operations) 739 | assert.equal(replicaA.testLocalDocument.text, 'b1 a1 ') 740 | assert.equal(replicaB.testLocalDocument.text, replicaA.testLocalDocument.text) 741 | assert.deepEqual(markers, { 742 | 1: { 743 | 1: {range: buildRange(3, 4), exclusive: true, a: 1}, 744 | }, 745 | 2: { 746 | 1: {range: buildRange(4, 5), b: 2} 747 | } 748 | }) 749 | 750 | // Delete checkpoint 751 | replicaA.revertToCheckpoint(checkpoint, {deleteCheckpoint: true}) 752 | assert.equal(replicaA.revertToCheckpoint(checkpoint), false) 753 | }) 754 | 755 | test('does not allow reverting past a barrier checkpoint', () => { 756 | const document = buildDocument(1) 757 | const checkpointBeforeBarrier = document.createCheckpoint({isBarrier: false}) 758 | performInsert(document, {row: 0, column: 0}, 'a') 759 | document.createCheckpoint({isBarrier: true}) 760 | 761 | assert.equal(document.revertToCheckpoint(checkpointBeforeBarrier), false) 762 | assert.equal(document.getText(), 'a') 763 | 764 | performInsert(document, {row: 0, column: 1}, 'b') 765 | assert.equal(document.revertToCheckpoint(checkpointBeforeBarrier), false) 766 | assert.equal(document.getText(), 'ab') 767 | }) 768 | 769 | test('getting changes since a checkpoint', () => { 770 | const replicaA = buildDocument(1) 771 | const replicaB = replicateDocument(2, replicaA) 772 | 773 | integrateOperations(replicaB, performInsert(replicaA, {row: 0, column: 0}, 'a1 ')) 774 | const checkpoint = replicaA.createCheckpoint() 775 | integrateOperations(replicaB, performSetTextInRange(replicaA, {row: 0, column: 1}, {row: 0, column: 3}, '2 a3 ')) 776 | integrateOperations(replicaB, performDelete(replicaA, {row: 0, column: 5}, {row: 0, column: 6})) 777 | integrateOperations(replicaA, performInsert(replicaB, {row: 0, column: 0}, 'b1 ')) 778 | assert.equal(replicaA.testLocalDocument.text, 'b1 a2 a3') 779 | 780 | const changesSinceCheckpoint = replicaA.getChangesSinceCheckpoint(checkpoint) 781 | for (const change of changesSinceCheckpoint.reverse()) { 782 | replicaA.testLocalDocument.setTextInRange(change.newStart, change.newEnd, change.oldText) 783 | } 784 | assert.equal(replicaA.testLocalDocument.text, 'b1 a1 ') 785 | 786 | // Ensure we don't modify the undo stack when getting changes since checkpoint (regression). 787 | assert.deepEqual(replicaA.getChangesSinceCheckpoint(checkpoint), changesSinceCheckpoint) 788 | }) 789 | 790 | test('undoing and redoing an operation that occurred adjacent to a checkpoint', () => { 791 | const document = buildDocument(1) 792 | performInsert(document, {row: 0, column: 0}, 'a') 793 | performInsert(document, {row: 0, column: 1}, 'b') 794 | document.createCheckpoint() 795 | performInsert(document, {row: 0, column: 2}, 'c') 796 | 797 | document.undo() 798 | assert.equal(document.getText(), 'ab') 799 | document.undo() 800 | assert.equal(document.getText(), 'a') 801 | document.redo() 802 | assert.equal(document.getText(), 'ab') 803 | document.redo() 804 | assert.equal(document.getText(), 'abc') 805 | }) 806 | 807 | test('reverting to a checkpoint after undoing and redoing an operation', () => { 808 | const document = buildDocument(1) 809 | 810 | performInsert(document, {row: 0, column: 0}, 'a') 811 | const checkpoint1 = document.createCheckpoint() 812 | performInsert(document, {row: 0, column: 1}, 'b') 813 | const checkpoint2 = document.createCheckpoint() 814 | 815 | document.undo() 816 | assert.equal(document.getText(), 'a') 817 | document.redo() 818 | assert.equal(document.getText(), 'ab') 819 | 820 | performInsert(document, {row: 0, column: 2}, 'c') 821 | 822 | document.revertToCheckpoint(checkpoint2) 823 | assert.equal(document.getText(), 'ab') 824 | 825 | document.revertToCheckpoint(checkpoint1) 826 | assert.equal(document.getText(), 'a') 827 | }) 828 | 829 | test('undoing preserves checkpoint created prior to any operations', () => { 830 | const document = buildDocument(1) 831 | const checkpoint = document.createCheckpoint() 832 | document.undo() 833 | performInsert(document, {row: 0, column: 0}, 'a') 834 | 835 | document.revertToCheckpoint(checkpoint) 836 | assert.equal(document.getText(), '') 837 | }) 838 | 839 | test('does not allow undoing past a barrier checkpoint', () => { 840 | const document = buildDocument(1) 841 | performInsert(document, {row: 0, column: 0}, 'a') 842 | performInsert(document, {row: 0, column: 1}, 'b') 843 | document.createCheckpoint({isBarrier: true}) 844 | performInsert(document, {row: 0, column: 2}, 'c') 845 | document.createCheckpoint({isBarrier: false}) 846 | 847 | assert.equal(document.getText(), 'abc') 848 | document.undo() 849 | assert.equal(document.getText(), 'ab') 850 | assert.equal(document.undo(), null) 851 | assert.equal(document.getText(), 'ab') 852 | }) 853 | 854 | test('does not add empty transactions to the undo stack', () => { 855 | const replicaA = buildDocument(1) 856 | const replicaB = replicateDocument(2, replicaA) 857 | integrateOperations(replicaB, performInsert(replicaA, {row: 0, column: 0}, 'a')) 858 | integrateOperations(replicaB, performInsert(replicaA, {row: 0, column: 1}, 'b')) 859 | const checkpoint = replicaA.createCheckpoint() 860 | integrateOperations(replicaA, performInsert(replicaB, {row: 0, column: 2}, 'c')) 861 | replicaA.groupChangesSinceCheckpoint(checkpoint) 862 | integrateOperations(replicaB, performUndo(replicaA).operations) 863 | 864 | assert.equal(replicaA.testLocalDocument.text, 'ac') 865 | assert.equal(replicaB.testLocalDocument.text, 'ac') 866 | }) 867 | 868 | test('grouping the last 2 transactions', () => { 869 | const document = buildDocument(1) 870 | performInsert(document, {row: 0, column: 0}, 'a') 871 | performInsert(document, {row: 0, column: 1}, 'b') 872 | const checkpoint1 = document.createCheckpoint() 873 | performInsert(document, {row: 0, column: 2}, 'c') 874 | const checkpoint2 = document.createCheckpoint() 875 | 876 | assert(document.groupLastChanges()) 877 | assert.equal(document.getText(), 'abc') 878 | document.undo() 879 | assert.equal(document.getText(), 'a') 880 | document.redo() 881 | assert.equal(document.getText(), 'abc') 882 | assert(!document.revertToCheckpoint(checkpoint1)) 883 | performInsert(document, {row: 0, column: 3}, 'd') 884 | assert(!document.revertToCheckpoint(checkpoint2)) 885 | 886 | // Can't group past barrier checkpoints 887 | const checkpoint3 = document.createCheckpoint({isBarrier: true}) 888 | performInsert(document, {row: 0, column: 4}, 'e') 889 | assert(!document.groupLastChanges()) 890 | assert(document.revertToCheckpoint(checkpoint3)) 891 | assert.equal(document.getText(), 'abcd') 892 | }) 893 | 894 | test('applying a grouping interval', () => { 895 | const document = buildDocument(1) 896 | document.getNow = () => now 897 | 898 | let now = 0 899 | const initialMarkers = { 900 | 1: { 901 | 1: {range: range(point(0, 0), point(0, 0))} 902 | } 903 | } 904 | const checkpoint1 = document.createCheckpoint({markers: initialMarkers}) 905 | performInsert(document, {row: 0, column: 0}, 'a') 906 | const markersAfterInsertion1 = { 907 | 1: { 908 | 1: {range: range(point(0, 1), point(0, 1))} 909 | } 910 | } 911 | document.groupChangesSinceCheckpoint(checkpoint1, {markers: markersAfterInsertion1, deleteCheckpoint: true}) 912 | document.applyGroupingInterval(101) 913 | 914 | now += 100 915 | const checkpoint2 = document.createCheckpoint({markers: markersAfterInsertion1}) 916 | performInsert(document, {row: 0, column: 1}, 'b') 917 | const markersAfterInsertion2 = { 918 | 1: { 919 | 1: {range: range(point(0, 2), point(0, 2))} 920 | } 921 | } 922 | document.groupChangesSinceCheckpoint(checkpoint2, {markers: markersAfterInsertion2, deleteCheckpoint: true}) 923 | document.applyGroupingInterval(201) 924 | 925 | now += 200 926 | const checkpoint3 = document.createCheckpoint({markers: markersAfterInsertion2}) 927 | performInsert(document, {row: 0, column: 2}, 'c') 928 | const markersAfterInsertion3 = { 929 | 1: { 930 | 1: {range: range(point(0, 3), point(0, 3))} 931 | } 932 | } 933 | document.groupChangesSinceCheckpoint(checkpoint3, {markers: markersAfterInsertion3, deleteCheckpoint: true}) 934 | document.applyGroupingInterval(201) 935 | 936 | // Not grouped with previous transaction because its associated grouping 937 | // interval is 201 and we always respect the minimum associated interval 938 | // between the last and penultimate transaction. 939 | now += 300 940 | const checkpoint4 = document.createCheckpoint({markers: markersAfterInsertion3}) 941 | performInsert(document, {row: 0, column: 3}, 'd') 942 | const markersAfterInsertion4 = { 943 | 1: { 944 | 1: {range: range(point(0, 4), point(0, 4))} 945 | } 946 | } 947 | document.groupChangesSinceCheckpoint(checkpoint4, {markers: markersAfterInsertion4, deleteCheckpoint: true}) 948 | document.applyGroupingInterval(301) 949 | 950 | assert.equal(document.testLocalDocument.text, 'abcd') 951 | 952 | { 953 | const {markers} = performUndo(document) 954 | assert.equal(document.testLocalDocument.text, 'abc') 955 | assert.deepEqual(markers, markersAfterInsertion3) 956 | } 957 | 958 | { 959 | const {markers} = performUndo(document) 960 | assert.equal(document.testLocalDocument.text, '') 961 | assert.deepEqual(markers, initialMarkers) 962 | } 963 | 964 | { 965 | const {markers} = performRedo(document) 966 | assert.equal(document.testLocalDocument.text, 'abc') 967 | assert.deepEqual(markers, markersAfterInsertion3) 968 | } 969 | 970 | { 971 | const {markers} = performRedo(document) 972 | assert.equal(document.testLocalDocument.text, 'abcd') 973 | assert.deepEqual(markers, markersAfterInsertion4) 974 | } 975 | }) 976 | 977 | test('getting the state of the history', () => { 978 | const document = buildDocument(1, 'a ') 979 | performInsert(document, point(0, 2), 'b ') 980 | performInsert(document, point(0, 4), 'c ') 981 | const checkpoint1 = document.createCheckpoint({ 982 | markers: {1: {1: {range: range(point(0, 2), point(0, 5))}}} 983 | }) 984 | performInsert(document, point(0, 0), 'd ') 985 | performInsert(document, point(0, 8), 'e ') 986 | document.groupChangesSinceCheckpoint(checkpoint1, { 987 | markers: {1: {1: {range: range(point(0, 1), point(0, 3))}}} 988 | }) 989 | performInsert(document, point(0, 10), 'f ') 990 | const checkpoint2 = document.createCheckpoint({ 991 | markers: {1: {1: {range: range(point(0, 0), point(0, 1))}}} 992 | }) 993 | performInsert(document, point(0, 10), 'g ') 994 | document.undo() 995 | document.undo() 996 | const replica = replicateDocument(2, document) 997 | 998 | const history = document.getHistory(3) 999 | 1000 | // The current implementation of getHistory temporarily mutates the replica. 1001 | // Here we make sure we restore the state of the document and its undo counts. 1002 | assert.equal(document.getText(), replica.getText()) 1003 | integrateOperations(document, replica.undoOrRedoOperations(replica.operations.slice()).operations) 1004 | assert.equal(document.getText(), replica.getText()) 1005 | 1006 | assert.equal(history.nextCheckpointId, 3) 1007 | assert.deepEqual(history.undoStack, [ 1008 | { 1009 | type: "transaction", 1010 | changes: [ 1011 | {oldStart: point(0, 4), oldEnd: point(0, 4), oldText: "", newStart: point(0, 4), newEnd: point(0, 6), newText: "c "} 1012 | ], 1013 | markersBefore: null, 1014 | markersAfter: null 1015 | }, 1016 | { 1017 | type: "checkpoint", 1018 | id: 1, 1019 | markers: {1: {1: {range: range(point(0, 2), point(0, 5))}}} 1020 | }, 1021 | { 1022 | type: "transaction", 1023 | changes: [ 1024 | {oldStart: point(0, 0), oldEnd: point(0, 0), oldText: "", newStart: point(0, 0), newEnd: point(0, 2), newText: "d "}, 1025 | {oldStart: point(0, 6), oldEnd: point(0, 6), oldText: "", newStart: point(0, 8), newEnd: point(0, 10), newText: "e "} 1026 | ], 1027 | markersBefore: {1: {1: {range: range(point(0, 2), point(0, 5))}}}, 1028 | markersAfter: {1: {1: {range: range(point(0, 1), point(0, 3))}}} 1029 | } 1030 | ]) 1031 | assert.deepEqual(history.redoStack, [ 1032 | { 1033 | type: "transaction", 1034 | changes: [ 1035 | {oldStart: point(0, 10), oldEnd: point(0, 10), oldText: "", newStart: point(0, 10), newEnd: point(0, 12), newText: "g "} 1036 | ], 1037 | markersBefore: null, 1038 | markersAfter: null 1039 | }, 1040 | { 1041 | type: "checkpoint", 1042 | id: 2, 1043 | markers: {1: {1: {range: range(point(0, 0), point(0, 1))}}} 1044 | }, 1045 | { 1046 | type: "transaction", 1047 | changes: [ 1048 | {oldStart: point(0, 10), oldEnd: point(0, 10), oldText: "", newStart: point(0, 10), newEnd: point(0, 12), newText: "f "} 1049 | ], 1050 | markersBefore: null, 1051 | markersAfter: null 1052 | } 1053 | ]) 1054 | }) 1055 | }) 1056 | 1057 | test('replica convergence with random operations', function () { 1058 | this.timeout(Infinity) 1059 | const initialSeed = Date.now() 1060 | const peerCount = 5 1061 | for (var i = 0; i < 1000; i++) { 1062 | const peers = Peer.buildNetwork(peerCount, '') 1063 | let seed = initialSeed + i 1064 | // seed = 1504270975436 1065 | // global.enableLog = true 1066 | const failureMessage = `Random seed: ${seed}` 1067 | try { 1068 | const random = Random(seed) 1069 | let operationCount = 0 1070 | while (operationCount < 10) { 1071 | const peersWithOutboundOperations = peers.filter(p => !p.isOutboxEmpty()) 1072 | if (peersWithOutboundOperations.length === 0 || random(2)) { 1073 | const peer = peers[random(peerCount)] 1074 | const k = random(10) 1075 | if (k < 2 && peer.editOperations.length > 0) { 1076 | peer.undoRandomOperation(random) 1077 | } else if (k < 4) { 1078 | peer.updateRandomMarkers(random) 1079 | } else { 1080 | peer.performRandomEdit(random) 1081 | } 1082 | 1083 | if (random(10) < 3) { 1084 | peer.verifyTextUpdatesForRandomOperations(random) 1085 | } 1086 | 1087 | if (random(10) < 3) { 1088 | peer.verifyDocumentReplication() 1089 | } 1090 | 1091 | assert.equal(peer.document.getText(), peer.localDocument.text) 1092 | 1093 | operationCount++ 1094 | } else { 1095 | const peer = peersWithOutboundOperations[random(peersWithOutboundOperations.length)] 1096 | peer.deliverRandomOperation(random) 1097 | 1098 | assert.equal(peer.document.getText(), peer.localDocument.text) 1099 | } 1100 | } 1101 | 1102 | while (true) { 1103 | const peersWithOutboundOperations = peers.filter(p => !p.isOutboxEmpty()) 1104 | if (peersWithOutboundOperations.length === 0) break 1105 | 1106 | const peer = peersWithOutboundOperations[random(peersWithOutboundOperations.length)] 1107 | peer.deliverRandomOperation(random) 1108 | } 1109 | 1110 | for (let j = 0; j < peers.length; j++) { 1111 | const peer = peers[j] 1112 | peer.log(JSON.stringify(peer.localDocument.text)) 1113 | } 1114 | 1115 | for (let j = 0; j < peers.length; j++) { 1116 | assert.equal(peers[j].localDocument.text, peers[j].document.getText()) 1117 | } 1118 | 1119 | for (let j = 0; j < peers.length - 1; j++) { 1120 | assert.equal(peers[j].localDocument.text, peers[j + 1].localDocument.text, failureMessage) 1121 | } 1122 | 1123 | // TODO: Get markers to converge. This isn't critical since markers 1124 | // are current just used for decorations and an occasional divergence 1125 | // won't be fatal. 1126 | // 1127 | // for (let j = 0; j < peers.length - 1; j++) { 1128 | // assert.deepEqual(peers[j].localDocument.markers, peers[j + 1].localDocument.markers, failureMessage) 1129 | // } 1130 | } catch (e) { 1131 | console.log(failureMessage); 1132 | throw e 1133 | } 1134 | } 1135 | }) 1136 | }) 1137 | 1138 | function buildDocument (siteId, text) { 1139 | const document = new Document({siteId, text}) 1140 | document.testLocalDocument = new LocalDocument(document.getText()) 1141 | return document 1142 | } 1143 | 1144 | function replicateDocument (siteId, document) { 1145 | const replica = document.replicate(siteId) 1146 | replica.testLocalDocument = new LocalDocument(replica.getText()) 1147 | return replica 1148 | } 1149 | 1150 | function performInsert (replica, position, text) { 1151 | return performSetTextInRange(replica, position, position, text) 1152 | } 1153 | 1154 | function performDelete (replica, start, end) { 1155 | return performSetTextInRange(replica, start, end, '') 1156 | } 1157 | 1158 | function performSetTextInRange (replica, start, end, text, options) { 1159 | replica.testLocalDocument.setTextInRange(start, end, text) 1160 | return replica.setTextInRange(start, end, text, options) 1161 | } 1162 | 1163 | function performUndo (replica) { 1164 | const {operations, textUpdates, markers} = replica.undo() 1165 | replica.testLocalDocument.updateText(textUpdates) 1166 | return {operations, markers} 1167 | } 1168 | 1169 | function performRedo (replica) { 1170 | const {operations, textUpdates, markers} = replica.redo() 1171 | replica.testLocalDocument.updateText(textUpdates) 1172 | return {operations, markers} 1173 | } 1174 | 1175 | function performUndoOrRedoOperations (replica, operationToUndo) { 1176 | const {operations, textUpdates} = replica.undoOrRedoOperations(operationToUndo) 1177 | replica.testLocalDocument.updateText(textUpdates) 1178 | return operations 1179 | } 1180 | 1181 | function performRevertToCheckpoint (replica, checkpoint, options) { 1182 | const {operations, textUpdates, markers} = replica.revertToCheckpoint(checkpoint, options) 1183 | replica.testLocalDocument.updateText(textUpdates) 1184 | return {operations, markers} 1185 | } 1186 | 1187 | function performUpdateMarkers (replica, markerUpdates) { 1188 | replica.testLocalDocument.updateMarkers({[replica.siteId]: markerUpdates}) 1189 | return replica.updateMarkers(markerUpdates) 1190 | } 1191 | 1192 | function integrateOperations (replica, ops) { 1193 | const {textUpdates, markerUpdates} = replica.integrateOperations(ops) 1194 | replica.testLocalDocument.updateText(textUpdates) 1195 | replica.testLocalDocument.updateMarkers(markerUpdates) 1196 | } 1197 | 1198 | function buildRange (startColumn, endColumn) { 1199 | return { 1200 | start: {row: 0, column: startColumn}, 1201 | end: {row: 0, column: endColumn} 1202 | } 1203 | } 1204 | 1205 | function range (start, end) { 1206 | return {start, end} 1207 | } 1208 | 1209 | function point(row, column) { 1210 | return {row, column} 1211 | } 1212 | -------------------------------------------------------------------------------- /test/helpers/local-document.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { 3 | ZERO_POINT, characterIndexForPosition, extentForText, compare, traverse, traversal 4 | } = require('../../lib/point-helpers') 5 | 6 | module.exports = 7 | class LocalDocument { 8 | constructor (text) { 9 | this.text = text 10 | this.markers = {} 11 | } 12 | 13 | updateText (changes) { 14 | for (let i = changes.length - 1; i >= 0; i--) { 15 | const {oldStart, oldEnd, newText} = changes[i] 16 | this.setTextInRange(oldStart, oldEnd, newText) 17 | } 18 | } 19 | 20 | updateMarkers (updatesBySiteId) { 21 | for (const siteId in updatesBySiteId) { 22 | let layersById = this.markers[siteId] 23 | if (!layersById) { 24 | layersById = {} 25 | this.markers[siteId] = layersById 26 | } 27 | 28 | const updatesByLayerId = updatesBySiteId[siteId] 29 | for (const layerId in updatesByLayerId) { 30 | const updatesByMarkerId = updatesByLayerId[layerId] 31 | 32 | if (updatesByMarkerId === null) { 33 | assert(layersById[layerId], 'Layer should exist') 34 | delete layersById[layerId] 35 | } else { 36 | let markersById = layersById[layerId] 37 | if (!markersById) { 38 | markersById = {} 39 | layersById[layerId] = markersById 40 | } 41 | 42 | for (const markerId in updatesByMarkerId) { 43 | const markerUpdate = updatesByMarkerId[markerId] 44 | if (markerUpdate === null) { 45 | assert(markersById[markerId], 'Marker should exist') 46 | delete markersById[markerId] 47 | } else { 48 | const marker = Object.assign({}, markerUpdate) 49 | marker.range = Object.assign({}, marker.range) 50 | markersById[markerId] = marker 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | setTextInRange (oldStart, oldEnd, text) { 59 | if (compare(oldEnd, oldStart) > 0) { 60 | this.delete(oldStart, oldEnd) 61 | } 62 | 63 | if (text.length > 0) { 64 | this.insert(oldStart, text) 65 | } 66 | 67 | this.spliceMarkers(oldStart, oldEnd, traverse(oldStart, extentForText(text))) 68 | } 69 | 70 | spliceMarkers (oldStart, oldEnd, newEnd) { 71 | const isInsertion = compare(oldStart, oldEnd) === 0 72 | 73 | 74 | for (const siteId in this.markers) { 75 | const layersById = this.markers[siteId] 76 | for (const layerId in layersById) { 77 | const markersById = layersById[layerId] 78 | for (const markerId in markersById) { 79 | const {range, exclusive} = markersById[markerId] 80 | const rangeIsEmpty = compare(range.start, range.end) === 0 81 | 82 | const moveMarkerStart = ( 83 | compare(oldStart, range.start) < 0 || 84 | ( 85 | exclusive && 86 | (!rangeIsEmpty || isInsertion) && 87 | compare(oldStart, range.start) === 0 88 | ) 89 | ) 90 | 91 | const moveMarkerEnd = ( 92 | moveMarkerStart || 93 | (compare(oldStart, range.end) < 0) || 94 | (!exclusive && compare(oldEnd, range.end) === 0) 95 | ) 96 | 97 | if (moveMarkerStart) { 98 | if (compare(oldEnd, range.start) <= 0) { // splice precedes marker start 99 | range.start = traverse(newEnd, traversal(range.start, oldEnd)) 100 | } else { // splice surrounds marker start 101 | range.start = newEnd 102 | } 103 | } 104 | 105 | if (moveMarkerEnd) { 106 | if (compare(oldEnd, range.end) <= 0) { // splice precedes marker end 107 | range.end = traverse(newEnd, traversal(range.end, oldEnd)) 108 | } else { // splice surrounds marker end 109 | range.end = newEnd 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | insert (position, text) { 118 | const index = characterIndexForPosition(this.text, position) 119 | this.text = this.text.slice(0, index) + text + this.text.slice(index) 120 | } 121 | 122 | delete (startPosition, endPosition) { 123 | const textExtent = extentForText(this.text) 124 | assert(compare(startPosition, textExtent) < 0) 125 | assert(compare(endPosition, textExtent) <= 0) 126 | const startIndex = characterIndexForPosition(this.text, startPosition) 127 | const endIndex = characterIndexForPosition(this.text, endPosition) 128 | this.text = this.text.slice(0, startIndex) + this.text.slice(endIndex) 129 | } 130 | 131 | lineForRow (row) { 132 | const startIndex = characterIndexForPosition(this.text, {row, column: 0}) 133 | const endIndex = characterIndexForPosition(this.text, {row: row + 1, column: 0}) - 1 134 | return this.text.slice(startIndex, endIndex) 135 | } 136 | 137 | getLineCount () { 138 | return extentForText(this.text).row + 1 139 | } 140 | 141 | getTextInRange (start, end) { 142 | const startIndex = characterIndexForPosition(this.text, start) 143 | const endIndex = characterIndexForPosition(this.text, end) 144 | return this.text.slice(startIndex, endIndex) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /test/helpers/peer.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const {getRandomDocumentRange, buildRandomLines} = require('./random') 3 | const {ZERO_POINT, compare, traverse, extentForText} = require('../../lib/point-helpers') 4 | const {serializeOperation, deserializeOperation} = require('../../lib/serialization') 5 | const LocalDocument = require('./local-document') 6 | const Document = require('../../lib/document') 7 | 8 | module.exports = 9 | class Peer { 10 | static buildNetwork (n, text) { 11 | const peers = [] 12 | for (var i = 0; i < n; i++) { 13 | peers.push(new Peer(i + 1, text)) 14 | } 15 | 16 | for (var i = 0; i < n; i++) { 17 | for (var j = 0; j < n; j++) { 18 | if (i !== j) peers[i].connect(peers[j]) 19 | } 20 | } 21 | 22 | return peers 23 | } 24 | 25 | constructor (siteId, text) { 26 | this.siteId = siteId 27 | this.outboxes = new Map() 28 | this.localDocument = new LocalDocument(text) 29 | this.document = new Document({siteId}) 30 | this.deferredOperations = [] 31 | this.editOperations = [] 32 | this.nonUndoEditOperations = [] 33 | } 34 | 35 | connect (peer) { 36 | this.outboxes.set(peer, []) 37 | } 38 | 39 | send (operation) { 40 | operation = serializeOperation(operation) 41 | this.outboxes.forEach((outbox) => outbox.push(operation)) 42 | } 43 | 44 | receive (operation) { 45 | operation = deserializeOperation(operation) 46 | this.log('Received', operation) 47 | const {textUpdates, markerUpdates} = this.document.integrateOperations([operation]) 48 | // this.log('Applying delta', changes) 49 | this.localDocument.updateText(textUpdates) 50 | this.localDocument.updateMarkers(markerUpdates) 51 | this.log('Text', JSON.stringify(this.localDocument.text)) 52 | 53 | if (operation.type !== 'markers-update') { 54 | this.editOperations.push(operation) 55 | if (operation.type !== 'undo') this.nonUndoEditOperations.push(operation) 56 | } 57 | } 58 | 59 | isOutboxEmpty () { 60 | return Array.from(this.outboxes.values()).every((o) => o.length === 0) 61 | } 62 | 63 | performRandomEdit (random) { 64 | let operations 65 | while (true) { 66 | let {start, end} = getRandomDocumentRange(random, this.localDocument) 67 | const text = buildRandomLines(random, 1) 68 | if (compare(end, ZERO_POINT) > 0 || text.length > 0) { 69 | this.log('setTextInRange', start, end, JSON.stringify(text)) 70 | this.localDocument.setTextInRange(start, end, text) 71 | operations = this.document.setTextInRange(start, end, text) 72 | break 73 | } 74 | } 75 | this.log('Text', JSON.stringify(this.localDocument.text)) 76 | 77 | for (const operation of operations) { 78 | this.send(operation) 79 | this.editOperations.push(operation) 80 | this.nonUndoEditOperations.push(operation) 81 | } 82 | } 83 | 84 | undoRandomOperation (random) { 85 | const opToUndo = this.editOperations[random(this.editOperations.length)] 86 | const {spliceId} = opToUndo 87 | 88 | if (this.document.hasAppliedSplice(spliceId)) { 89 | this.log('Undoing', opToUndo) 90 | const {operations, textUpdates} = this.document.undoOrRedoOperations([opToUndo]) 91 | this.log('Applying delta', textUpdates) 92 | this.localDocument.updateText(textUpdates) 93 | this.log('Text', JSON.stringify(this.localDocument.text)) 94 | this.editOperations.push(operations[0]) 95 | this.send(operations[0]) 96 | } 97 | } 98 | 99 | updateRandomMarkers (random) { 100 | const markerUpdates = {} 101 | const siteMarkerLayers = this.localDocument.markers[this.siteId] || {} 102 | 103 | const n = random.intBetween(1, 1) 104 | for (let i = 0; i < n; i++) { 105 | const layerId = random(10) 106 | 107 | if (random(10) < 1 && siteMarkerLayers[layerId]) { 108 | markerUpdates[layerId] = null 109 | } else { 110 | if (!markerUpdates[layerId]) markerUpdates[layerId] = {} 111 | const layer = siteMarkerLayers[layerId] || {} 112 | const markerIds = Object.keys(layer) 113 | if (random(10) < 1 && markerIds.length > 0) { 114 | const markerId = markerIds[random(markerIds.length)] 115 | markerUpdates[layerId][markerId] = null 116 | } else { 117 | const markerId = random(10) 118 | const range = getRandomDocumentRange(random, this.localDocument) 119 | const exclusive = Boolean(random(2)) 120 | const reversed = Boolean(random(2)) 121 | const tailed = Boolean(random(2)) 122 | markerUpdates[layerId][markerId] = {range, exclusive, reversed, tailed} 123 | } 124 | } 125 | } 126 | 127 | this.log('Update markers', markerUpdates) 128 | this.localDocument.updateMarkers({[this.siteId]: markerUpdates}) 129 | const operations = this.document.updateMarkers(markerUpdates) 130 | for (const operation of operations) { 131 | this.send(operation) 132 | } 133 | } 134 | 135 | verifyTextUpdatesForRandomOperations (random) { 136 | const n = random(Math.min(10, this.nonUndoEditOperations.length)) 137 | const operationsSet = new Set() 138 | for (let i = 0; i < n; i++) { 139 | const index = random(this.nonUndoEditOperations.length) 140 | const operation = this.nonUndoEditOperations[index] 141 | if (this.document.hasAppliedSplice(operation.spliceId)) operationsSet.add(operation) 142 | } 143 | const operations = Array.from(operationsSet) 144 | const delta = this.document.textUpdatesForOperations(operations) 145 | 146 | const documentCopy = new LocalDocument(this.localDocument.text) 147 | for (const change of delta.slice().reverse()) { 148 | documentCopy.setTextInRange(change.newStart, change.newEnd, change.oldText) 149 | } 150 | 151 | const replicaCopy = this.document.replicate(this.document.siteId) 152 | const notUndoneOperations = operations.filter((operation) => 153 | !this.document.isSpliceUndone(operation) 154 | ) 155 | replicaCopy.undoOrRedoOperations(notUndoneOperations) 156 | 157 | assert.equal(documentCopy.text, replicaCopy.getText()) 158 | } 159 | 160 | verifyDocumentReplication () { 161 | const replica = this.document.replicate(this.document.siteId) 162 | assert.equal(replica.getText(), this.document.getText()) 163 | assert.deepEqual(replica.getMarkers(), this.document.getMarkers()) 164 | } 165 | 166 | deliverRandomOperation (random) { 167 | const outboxes = Array.from(this.outboxes).filter(([peer, operations]) => operations.length > 0) 168 | const [peer, operations] = outboxes[random(outboxes.length)] 169 | peer.receive(operations.shift()) 170 | } 171 | 172 | log (...message) { 173 | if (global.enableLog) { 174 | console.log(`Site ${this.siteId}`, ...message) 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /test/helpers/random.js: -------------------------------------------------------------------------------- 1 | const WORDS = require('./words') 2 | const {compare, traversal} = require('../../lib/point-helpers') 3 | 4 | exports.getRandomDocumentRange = function getRandomDocumentPositionAndExtent (random, document) { 5 | const endRow = random(document.getLineCount()) 6 | const endColumn = random(document.lineForRow(endRow).length) 7 | 8 | let startRow, startColumn 9 | if (random(10) < 1) { 10 | startRow = endRow 11 | startColumn = endColumn 12 | } else { 13 | startRow = random.intBetween(0, endRow) 14 | startColumn = random(document.lineForRow(startRow).length) 15 | } 16 | 17 | let start = {row: startRow, column: startColumn} 18 | let end = {row: endRow, column: endColumn} 19 | if (compare(start, end) > 0) { 20 | const temp = end 21 | end = start 22 | start = temp 23 | } 24 | return {start, end} 25 | } 26 | 27 | exports.buildRandomLines = function buildRandomLines (random, maxLines) { 28 | const lineCount = random.intBetween(0, maxLines) 29 | const lines = [] 30 | for (let i = 0; i < lineCount; i++) { 31 | const wordCount = lineCount === 1 ? random.intBetween(1, 5) : random(5) 32 | lines.push(buildRandomLine(random, wordCount)) 33 | } 34 | return lines.join('\n') 35 | } 36 | 37 | function buildRandomLine (random, wordCount) { 38 | const line = [] 39 | for (let i = 0; i < wordCount; i++) { 40 | line.push(WORDS[random(WORDS.length)]) 41 | } 42 | return line.join('') 43 | } 44 | -------------------------------------------------------------------------------- /test/serialization.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { 3 | serializeOperationBinary, deserializeOperationBinary, 4 | serializeRemotePositionBinary, deserializeRemotePositionBinary 5 | } = require('../lib/serialization') 6 | 7 | suite('serialization/deserialization', () => { 8 | test('inserts', () => { 9 | const op = { 10 | type: 'splice', 11 | spliceId: {site: 1, seq: 2}, 12 | insertion: { 13 | text: 'hello', 14 | leftDependencyId: {site: 1, seq: 1}, 15 | offsetInLeftDependency: {row: 0, column: 5}, 16 | rightDependencyId: {site: 1, seq: 1}, 17 | offsetInRightDependency: {row: 0, column: 5}, 18 | }, 19 | deletion: { 20 | leftDependencyId: {site: 1, seq: 1}, 21 | offsetInLeftDependency: {row: 0, column: 5}, 22 | rightDependencyId: {site: 1, seq: 1}, 23 | offsetInRightDependency: {row: 0, column: 5}, 24 | maxSeqsBySite: { 25 | '1': 3, 26 | '2': 5 27 | } 28 | } 29 | } 30 | 31 | assert.deepEqual(deserializeOperationBinary(serializeOperationBinary(op)), op) 32 | }) 33 | 34 | test('undo', () => { 35 | const op = { 36 | type: 'undo', 37 | spliceId: {site: 1, seq: 3}, 38 | undoCount: 3 39 | } 40 | 41 | assert.deepEqual(deserializeOperationBinary(serializeOperationBinary(op)), op) 42 | }) 43 | 44 | test('marker updates', () => { 45 | const op = { 46 | type: 'markers-update', 47 | siteId: 1, 48 | updates: { 49 | 1: { 50 | 1: { 51 | range: { 52 | startDependencyId: {site: 1, seq: 1}, 53 | offsetInStartDependency: {row: 0, column: 1}, 54 | endDependencyId: {site: 1, seq: 1}, 55 | offsetInEndDependency: {row: 0, column: 6} 56 | }, 57 | exclusive: false, 58 | reversed: false, 59 | tailed: true 60 | }, 61 | 2: null 62 | }, 63 | 2: null 64 | } 65 | } 66 | 67 | assert.deepEqual(deserializeOperationBinary(serializeOperationBinary(op)), op) 68 | }) 69 | }) 70 | --------------------------------------------------------------------------------