/* eslint-disable camelcase, new-cap, no-throw-literal, no-unused-vars, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS103: Rewrite code to no longer use __guard__ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let oneMinute, twoMegabytes, UpdateCompressor const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos) const strRemove = (s1, pos, length) => s1.slice(0, pos) + s1.slice(pos + length) const { diff_match_patch } = require('../lib/diff_match_patch') const dmp = new diff_match_patch() module.exports = UpdateCompressor = { NOOP: 'noop', // Updates come from the doc updater in format // { // op: [ { ... op1 ... }, { ... op2 ... } ] // meta: { ts: ..., user_id: ... } // } // but it's easier to work with on op per update, so convert these updates to // our compressed format // [{ // op: op1 // meta: { start_ts: ... , end_ts: ..., user_id: ... } // }, { // op: op2 // meta: { start_ts: ... , end_ts: ..., user_id: ... } // }] convertToSingleOpUpdates(updates) { const splitUpdates = [] for (const update of Array.from(updates)) { // Reject any non-insert or delete ops, i.e. comments const ops = update.op.filter(o => o.i != null || o.d != null) if (ops.length === 0) { splitUpdates.push({ op: UpdateCompressor.NOOP, meta: { start_ts: update.meta.start_ts || update.meta.ts, end_ts: update.meta.end_ts || update.meta.ts, user_id: update.meta.user_id, }, v: update.v, }) } else { for (const op of Array.from(ops)) { splitUpdates.push({ op, meta: { start_ts: update.meta.start_ts || update.meta.ts, end_ts: update.meta.end_ts || update.meta.ts, user_id: update.meta.user_id, }, v: update.v, }) } } } return splitUpdates }, concatUpdatesWithSameVersion(updates) { const concattedUpdates = [] for (const update of Array.from(updates)) { const lastUpdate = concattedUpdates[concattedUpdates.length - 1] if (lastUpdate != null && lastUpdate.v === update.v) { if (update.op !== UpdateCompressor.NOOP) { lastUpdate.op.push(update.op) } } else { const nextUpdate = { op: [], meta: update.meta, v: update.v, } if (update.op !== UpdateCompressor.NOOP) { nextUpdate.op.push(update.op) } concattedUpdates.push(nextUpdate) } } return concattedUpdates }, compressRawUpdates(lastPreviousUpdate, rawUpdates) { if ( __guard__( lastPreviousUpdate != null ? lastPreviousUpdate.op : undefined, x => x.length ) > 1 ) { // if the last previous update was an array op, don't compress onto it. // The avoids cases where array length changes but version number doesn't return [lastPreviousUpdate].concat( UpdateCompressor.compressRawUpdates(null, rawUpdates) ) } if (lastPreviousUpdate != null) { rawUpdates = [lastPreviousUpdate].concat(rawUpdates) } let updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates) updates = UpdateCompressor.compressUpdates(updates) return UpdateCompressor.concatUpdatesWithSameVersion(updates) }, compressUpdates(updates) { if (updates.length === 0) { return [] } let compressedUpdates = [updates.shift()] for (const update of Array.from(updates)) { const lastCompressedUpdate = compressedUpdates.pop() if (lastCompressedUpdate != null) { compressedUpdates = compressedUpdates.concat( UpdateCompressor._concatTwoUpdates(lastCompressedUpdate, update) ) } else { compressedUpdates.push(update) } } return compressedUpdates }, MAX_TIME_BETWEEN_UPDATES: (oneMinute = 60 * 1000), MAX_UPDATE_SIZE: (twoMegabytes = 2 * 1024 * 1024), _concatTwoUpdates(firstUpdate, secondUpdate) { let offset firstUpdate = { op: firstUpdate.op, meta: { user_id: firstUpdate.meta.user_id || null, start_ts: firstUpdate.meta.start_ts || firstUpdate.meta.ts, end_ts: firstUpdate.meta.end_ts || firstUpdate.meta.ts, }, v: firstUpdate.v, } secondUpdate = { op: secondUpdate.op, meta: { user_id: secondUpdate.meta.user_id || null, start_ts: secondUpdate.meta.start_ts || secondUpdate.meta.ts, end_ts: secondUpdate.meta.end_ts || secondUpdate.meta.ts, }, v: secondUpdate.v, } if (firstUpdate.meta.user_id !== secondUpdate.meta.user_id) { return [firstUpdate, secondUpdate] } if ( secondUpdate.meta.start_ts - firstUpdate.meta.end_ts > UpdateCompressor.MAX_TIME_BETWEEN_UPDATES ) { return [firstUpdate, secondUpdate] } const firstOp = firstUpdate.op const secondOp = secondUpdate.op const firstSize = (firstOp.i != null ? firstOp.i.length : undefined) || (firstOp.d != null ? firstOp.d.length : undefined) const secondSize = (secondOp.i != null ? secondOp.i.length : undefined) || (secondOp.d != null ? secondOp.d.length : undefined) // Two inserts if ( firstOp.i != null && secondOp.i != null && firstOp.p <= secondOp.p && secondOp.p <= firstOp.p + firstOp.i.length && firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE ) { return [ { meta: { start_ts: firstUpdate.meta.start_ts, end_ts: secondUpdate.meta.end_ts, user_id: firstUpdate.meta.user_id, }, op: { p: firstOp.p, i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i), }, v: secondUpdate.v, }, ] // Two deletes } else if ( firstOp.d != null && secondOp.d != null && secondOp.p <= firstOp.p && firstOp.p <= secondOp.p + secondOp.d.length && firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE ) { return [ { meta: { start_ts: firstUpdate.meta.start_ts, end_ts: secondUpdate.meta.end_ts, user_id: firstUpdate.meta.user_id, }, op: { p: secondOp.p, d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d), }, v: secondUpdate.v, }, ] // An insert and then a delete } else if ( firstOp.i != null && secondOp.d != null && firstOp.p <= secondOp.p && secondOp.p <= firstOp.p + firstOp.i.length ) { offset = secondOp.p - firstOp.p const insertedText = firstOp.i.slice(offset, offset + secondOp.d.length) // Only trim the insert when the delete is fully contained within in it if (insertedText === secondOp.d) { const insert = strRemove(firstOp.i, offset, secondOp.d.length) return [ { meta: { start_ts: firstUpdate.meta.start_ts, end_ts: secondUpdate.meta.end_ts, user_id: firstUpdate.meta.user_id, }, op: { p: firstOp.p, i: insert, }, v: secondUpdate.v, }, ] } else { // This will only happen if the delete extends outside the insert return [firstUpdate, secondUpdate] } // A delete then an insert at the same place, likely a copy-paste of a chunk of content } else if ( firstOp.d != null && secondOp.i != null && firstOp.p === secondOp.p ) { offset = firstOp.p const diff_ops = this.diffAsShareJsOps(firstOp.d, secondOp.i) if (diff_ops.length === 0) { return [ { // Noop meta: { start_ts: firstUpdate.meta.start_ts, end_ts: secondUpdate.meta.end_ts, user_id: firstUpdate.meta.user_id, }, op: { p: firstOp.p, i: '', }, v: secondUpdate.v, }, ] } else { return diff_ops.map(function (op) { op.p += offset return { meta: { start_ts: firstUpdate.meta.start_ts, end_ts: secondUpdate.meta.end_ts, user_id: firstUpdate.meta.user_id, }, op, v: secondUpdate.v, } }) } } else { return [firstUpdate, secondUpdate] } }, ADDED: 1, REMOVED: -1, UNCHANGED: 0, diffAsShareJsOps(before, after, callback) { if (callback == null) { callback = function () {} } const diffs = dmp.diff_main(before, after) dmp.diff_cleanupSemantic(diffs) const ops = [] let position = 0 for (const diff of Array.from(diffs)) { const type = diff[0] const content = diff[1] if (type === this.ADDED) { ops.push({ i: content, p: position, }) position += content.length } else if (type === this.REMOVED) { ops.push({ d: content, p: position, }) } else if (type === this.UNCHANGED) { position += content.length } else { throw 'Unknown type' } } return ops }, } function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null ? transform(value) : undefined }