├── .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 |
--------------------------------------------------------------------------------