/** * @module utils */ import { Tree } from '../lib/Tree.js' import * as ID from './ID.js' import * as encoding from '../lib/encoding.js' import * as decoding from '../lib/decoding.js' import { deleteItemRange } from '../utils/structManipulation.js' class DSNode { constructor (id, len, gc) { this._id = id this.len = len this.gc = gc } clone () { return new DSNode(this._id, this.len, this.gc) } } export class DeleteStore extends Tree { logTable () { const deletes = [] this.iterate(null, null, n => { deletes.push({ user: n._id.user, clock: n._id.clock, len: n.len, gc: n.gc }) }) console.table(deletes) } isDeleted (id) { var n = this.findWithUpperBound(id) return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len } mark (id, length, gc) { if (length === 0) return // Step 1. Unmark range const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1)) // Resize left DSNode if necessary if (leftD !== null && leftD._id.user === id.user) { if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) { // node is overlapping. need to resize if (id.clock + length < leftD._id.clock + leftD.len) { // overlaps new mark range and some more // create another DSNode to the right of new mark this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc)) } // resize left DSNode leftD.len = id.clock - leftD._id.clock } // Otherwise there is no overlapping } // Resize right DSNode if necessary const upper = ID.createID(id.user, id.clock + length - 1) const rightD = this.findWithUpperBound(upper) if (rightD !== null && rightD._id.user === id.user) { if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node const d = id.clock + length - rightD._id.clock rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d) rightD.len -= d } } // Now we only have to delete all inner marks const deleteNodeIds = [] this.iterate(id, upper, m => { deleteNodeIds.push(m._id) }) for (let i = deleteNodeIds.length - 1; i >= 0; i--) { this.delete(deleteNodeIds[i]) } let newMark = new DSNode(id, length, gc) // Step 2. Check if we can extend left or right if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) { // We can extend left leftD.len += length newMark = leftD } const rightNext = this.find(ID.createID(id.user, id.clock + length)) if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) { // We can merge newMark and rightNext newMark.len += rightNext.len this.delete(rightNext._id) } if (leftD !== newMark) { // only put if we didn't extend left this.put(newMark) } } } /** * Stringifies a message-encoded Delete Set. * * @param {decoding.Decoder} decoder * @return {string} */ export const stringifyDeleteStore = (decoder) => { let str = '' const dsLength = decoding.readUint32(decoder) for (let i = 0; i < dsLength; i++) { str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user const dvLength = decoding.readUint32(decoder) for (let j = 0; j < dvLength; j++) { str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n` } } return str } /** * Write the DeleteSet of a shared document to an Encoder. * * @param {encoding.Encoder} encoder * @param {DeleteStore} ds */ export const writeDeleteStore = (encoder, ds) => { let currentUser = null let currentLength let lastLenPos let numberOfUsers = 0 const laterDSLenPus = encoding.length(encoder) encoding.writeUint32(encoder, 0) ds.iterate(null, null, n => { const user = n._id.user const clock = n._id.clock const len = n.len const gc = n.gc if (currentUser !== user) { numberOfUsers++ // a new user was found if (currentUser !== null) { // happens on first iteration encoding.setUint32(encoder, lastLenPos, currentLength) } currentUser = user encoding.writeVarUint(encoder, user) // pseudo-fill pos lastLenPos = encoding.length(encoder) encoding.writeUint32(encoder, 0) currentLength = 0 } encoding.writeVarUint(encoder, clock) encoding.writeVarUint(encoder, len) encoding.writeUint8(encoder, gc ? 1 : 0) currentLength++ }) if (currentUser !== null) { // happens on first iteration encoding.setUint32(encoder, lastLenPos, currentLength) } encoding.setUint32(encoder, laterDSLenPus, numberOfUsers) } /** * Read delete store from Decoder and create a fresh DeleteStore * * @param {decoding.Decoder} decoder * @return {DeleteStore} */ export const readFreshDeleteStore = decoder => { const ds = new DeleteStore() const dsLength = decoding.readUint32(decoder) for (let i = 0; i < dsLength; i++) { const user = decoding.readVarUint(decoder) const dvLength = decoding.readUint32(decoder) for (let j = 0; j < dvLength; j++) { const from = decoding.readVarUint(decoder) const len = decoding.readVarUint(decoder) const gc = decoding.readUint8(decoder) ds.put(new DSNode(ID.createID(user, from), len, gc)) } } return ds } /** * Read delete set from Decoder and apply it to a shared document. * * @param {decoding.Decoder} decoder * @param {Y} y */ export const readDeleteStore = (decoder, y) => { const dsLength = decoding.readUint32(decoder) for (let i = 0; i < dsLength; i++) { const user = decoding.readVarUint(decoder) const dv = [] const dvLength = decoding.readUint32(decoder) for (let j = 0; j < dvLength; j++) { const from = decoding.readVarUint(decoder) const len = decoding.readVarUint(decoder) const gc = decoding.readUint8(decoder) === 1 dv.push({from, len, gc}) } if (dvLength > 0) { const deletions = [] let pos = 0 let d = dv[pos] y.ds.iterate(ID.createID(user, 0), ID.createID(user, Number.MAX_VALUE), n => { // cases: // 1. d deletes something to the right of n // => go to next n (break) // 2. d deletes something to the left of n // => create deletions // => reset d accordingly // *)=> if d doesn't delete anything anymore, go to next d (continue) // 3. not 2) and d deletes something that also n deletes // => reset d so that it doesn't contain n's deletion // *)=> if d does not delete anything anymore, go to next d (continue) while (d != null) { var diff = 0 // describe the diff of length in 1) and 2) if (n._id.clock + n.len <= d.from) { // 1) break } else if (d.from < n._id.clock) { // 2) // delete maximum the len of d // else delete as much as possible diff = Math.min(n._id.clock - d.from, d.len) // deleteItemRange(y, user, d.from, diff, true) deletions.push([user, d.from, diff]) } else { // 3) diff = n._id.clock + n.len - d.from // never null (see 1) if (d.gc && !n.gc) { // d marks as gc'd but n does not // then delete either way // deleteItemRange(y, user, d.from, Math.min(diff, d.len), true) deletions.push([user, d.from, Math.min(diff, d.len)]) } } if (d.len <= diff) { // d doesn't delete anything anymore d = dv[++pos] } else { d.from = d.from + diff // reset pos d.len = d.len - diff // reset length } } }) // TODO: It would be more performant to apply the deletes in the above loop // Adapt the Tree implementation to support delete while iterating for (let i = deletions.length - 1; i >= 0; i--) { const del = deletions[i] deleteItemRange(y, del[0], del[1], del[2], true) } // for the rest.. just apply it for (; pos < dv.length; pos++) { d = dv[pos] deleteItemRange(y, user, d.from, d.len, true) // deletions.push([user, d.from, d.len, d.gc) } } } }