UpdateCompressor.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. new-cap,
  5. no-throw-literal,
  6. no-unused-vars,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS103: Rewrite code to no longer use __guard__
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. let oneMinute, twoMegabytes, UpdateCompressor
  18. const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos)
  19. const strRemove = (s1, pos, length) => s1.slice(0, pos) + s1.slice(pos + length)
  20. const { diff_match_patch } = require('../lib/diff_match_patch')
  21. const dmp = new diff_match_patch()
  22. module.exports = UpdateCompressor = {
  23. NOOP: 'noop',
  24. // Updates come from the doc updater in format
  25. // {
  26. // op: [ { ... op1 ... }, { ... op2 ... } ]
  27. // meta: { ts: ..., user_id: ... }
  28. // }
  29. // but it's easier to work with on op per update, so convert these updates to
  30. // our compressed format
  31. // [{
  32. // op: op1
  33. // meta: { start_ts: ... , end_ts: ..., user_id: ... }
  34. // }, {
  35. // op: op2
  36. // meta: { start_ts: ... , end_ts: ..., user_id: ... }
  37. // }]
  38. convertToSingleOpUpdates(updates) {
  39. const splitUpdates = []
  40. for (const update of Array.from(updates)) {
  41. // Reject any non-insert or delete ops, i.e. comments
  42. const ops = update.op.filter((o) => o.i != null || o.d != null)
  43. if (ops.length === 0) {
  44. splitUpdates.push({
  45. op: UpdateCompressor.NOOP,
  46. meta: {
  47. start_ts: update.meta.start_ts || update.meta.ts,
  48. end_ts: update.meta.end_ts || update.meta.ts,
  49. user_id: update.meta.user_id
  50. },
  51. v: update.v
  52. })
  53. } else {
  54. for (const op of Array.from(ops)) {
  55. splitUpdates.push({
  56. op,
  57. meta: {
  58. start_ts: update.meta.start_ts || update.meta.ts,
  59. end_ts: update.meta.end_ts || update.meta.ts,
  60. user_id: update.meta.user_id
  61. },
  62. v: update.v
  63. })
  64. }
  65. }
  66. }
  67. return splitUpdates
  68. },
  69. concatUpdatesWithSameVersion(updates) {
  70. const concattedUpdates = []
  71. for (const update of Array.from(updates)) {
  72. const lastUpdate = concattedUpdates[concattedUpdates.length - 1]
  73. if (lastUpdate != null && lastUpdate.v === update.v) {
  74. if (update.op !== UpdateCompressor.NOOP) {
  75. lastUpdate.op.push(update.op)
  76. }
  77. } else {
  78. const nextUpdate = {
  79. op: [],
  80. meta: update.meta,
  81. v: update.v
  82. }
  83. if (update.op !== UpdateCompressor.NOOP) {
  84. nextUpdate.op.push(update.op)
  85. }
  86. concattedUpdates.push(nextUpdate)
  87. }
  88. }
  89. return concattedUpdates
  90. },
  91. compressRawUpdates(lastPreviousUpdate, rawUpdates) {
  92. if (
  93. __guard__(
  94. lastPreviousUpdate != null ? lastPreviousUpdate.op : undefined,
  95. (x) => x.length
  96. ) > 1
  97. ) {
  98. // if the last previous update was an array op, don't compress onto it.
  99. // The avoids cases where array length changes but version number doesn't
  100. return [lastPreviousUpdate].concat(
  101. UpdateCompressor.compressRawUpdates(null, rawUpdates)
  102. )
  103. }
  104. if (lastPreviousUpdate != null) {
  105. rawUpdates = [lastPreviousUpdate].concat(rawUpdates)
  106. }
  107. let updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates)
  108. updates = UpdateCompressor.compressUpdates(updates)
  109. return UpdateCompressor.concatUpdatesWithSameVersion(updates)
  110. },
  111. compressUpdates(updates) {
  112. if (updates.length === 0) {
  113. return []
  114. }
  115. let compressedUpdates = [updates.shift()]
  116. for (const update of Array.from(updates)) {
  117. const lastCompressedUpdate = compressedUpdates.pop()
  118. if (lastCompressedUpdate != null) {
  119. compressedUpdates = compressedUpdates.concat(
  120. UpdateCompressor._concatTwoUpdates(lastCompressedUpdate, update)
  121. )
  122. } else {
  123. compressedUpdates.push(update)
  124. }
  125. }
  126. return compressedUpdates
  127. },
  128. MAX_TIME_BETWEEN_UPDATES: (oneMinute = 60 * 1000),
  129. MAX_UPDATE_SIZE: (twoMegabytes = 2 * 1024 * 1024),
  130. _concatTwoUpdates(firstUpdate, secondUpdate) {
  131. let offset
  132. firstUpdate = {
  133. op: firstUpdate.op,
  134. meta: {
  135. user_id: firstUpdate.meta.user_id || null,
  136. start_ts: firstUpdate.meta.start_ts || firstUpdate.meta.ts,
  137. end_ts: firstUpdate.meta.end_ts || firstUpdate.meta.ts
  138. },
  139. v: firstUpdate.v
  140. }
  141. secondUpdate = {
  142. op: secondUpdate.op,
  143. meta: {
  144. user_id: secondUpdate.meta.user_id || null,
  145. start_ts: secondUpdate.meta.start_ts || secondUpdate.meta.ts,
  146. end_ts: secondUpdate.meta.end_ts || secondUpdate.meta.ts
  147. },
  148. v: secondUpdate.v
  149. }
  150. if (firstUpdate.meta.user_id !== secondUpdate.meta.user_id) {
  151. return [firstUpdate, secondUpdate]
  152. }
  153. if (
  154. secondUpdate.meta.start_ts - firstUpdate.meta.end_ts >
  155. UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
  156. ) {
  157. return [firstUpdate, secondUpdate]
  158. }
  159. const firstOp = firstUpdate.op
  160. const secondOp = secondUpdate.op
  161. const firstSize =
  162. (firstOp.i != null ? firstOp.i.length : undefined) ||
  163. (firstOp.d != null ? firstOp.d.length : undefined)
  164. const secondSize =
  165. (secondOp.i != null ? secondOp.i.length : undefined) ||
  166. (secondOp.d != null ? secondOp.d.length : undefined)
  167. // Two inserts
  168. if (
  169. firstOp.i != null &&
  170. secondOp.i != null &&
  171. firstOp.p <= secondOp.p &&
  172. secondOp.p <= firstOp.p + firstOp.i.length &&
  173. firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
  174. ) {
  175. return [
  176. {
  177. meta: {
  178. start_ts: firstUpdate.meta.start_ts,
  179. end_ts: secondUpdate.meta.end_ts,
  180. user_id: firstUpdate.meta.user_id
  181. },
  182. op: {
  183. p: firstOp.p,
  184. i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
  185. },
  186. v: secondUpdate.v
  187. }
  188. ]
  189. // Two deletes
  190. } else if (
  191. firstOp.d != null &&
  192. secondOp.d != null &&
  193. secondOp.p <= firstOp.p &&
  194. firstOp.p <= secondOp.p + secondOp.d.length &&
  195. firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
  196. ) {
  197. return [
  198. {
  199. meta: {
  200. start_ts: firstUpdate.meta.start_ts,
  201. end_ts: secondUpdate.meta.end_ts,
  202. user_id: firstUpdate.meta.user_id
  203. },
  204. op: {
  205. p: secondOp.p,
  206. d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
  207. },
  208. v: secondUpdate.v
  209. }
  210. ]
  211. // An insert and then a delete
  212. } else if (
  213. firstOp.i != null &&
  214. secondOp.d != null &&
  215. firstOp.p <= secondOp.p &&
  216. secondOp.p <= firstOp.p + firstOp.i.length
  217. ) {
  218. offset = secondOp.p - firstOp.p
  219. const insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
  220. // Only trim the insert when the delete is fully contained within in it
  221. if (insertedText === secondOp.d) {
  222. const insert = strRemove(firstOp.i, offset, secondOp.d.length)
  223. return [
  224. {
  225. meta: {
  226. start_ts: firstUpdate.meta.start_ts,
  227. end_ts: secondUpdate.meta.end_ts,
  228. user_id: firstUpdate.meta.user_id
  229. },
  230. op: {
  231. p: firstOp.p,
  232. i: insert
  233. },
  234. v: secondUpdate.v
  235. }
  236. ]
  237. } else {
  238. // This will only happen if the delete extends outside the insert
  239. return [firstUpdate, secondUpdate]
  240. }
  241. // A delete then an insert at the same place, likely a copy-paste of a chunk of content
  242. } else if (
  243. firstOp.d != null &&
  244. secondOp.i != null &&
  245. firstOp.p === secondOp.p
  246. ) {
  247. offset = firstOp.p
  248. const diff_ops = this.diffAsShareJsOps(firstOp.d, secondOp.i)
  249. if (diff_ops.length === 0) {
  250. return [
  251. {
  252. // Noop
  253. meta: {
  254. start_ts: firstUpdate.meta.start_ts,
  255. end_ts: secondUpdate.meta.end_ts,
  256. user_id: firstUpdate.meta.user_id
  257. },
  258. op: {
  259. p: firstOp.p,
  260. i: ''
  261. },
  262. v: secondUpdate.v
  263. }
  264. ]
  265. } else {
  266. return diff_ops.map(function (op) {
  267. op.p += offset
  268. return {
  269. meta: {
  270. start_ts: firstUpdate.meta.start_ts,
  271. end_ts: secondUpdate.meta.end_ts,
  272. user_id: firstUpdate.meta.user_id
  273. },
  274. op,
  275. v: secondUpdate.v
  276. }
  277. })
  278. }
  279. } else {
  280. return [firstUpdate, secondUpdate]
  281. }
  282. },
  283. ADDED: 1,
  284. REMOVED: -1,
  285. UNCHANGED: 0,
  286. diffAsShareJsOps(before, after, callback) {
  287. if (callback == null) {
  288. callback = function (error, ops) {}
  289. }
  290. const diffs = dmp.diff_main(before, after)
  291. dmp.diff_cleanupSemantic(diffs)
  292. const ops = []
  293. let position = 0
  294. for (const diff of Array.from(diffs)) {
  295. const type = diff[0]
  296. const content = diff[1]
  297. if (type === this.ADDED) {
  298. ops.push({
  299. i: content,
  300. p: position
  301. })
  302. position += content.length
  303. } else if (type === this.REMOVED) {
  304. ops.push({
  305. d: content,
  306. p: position
  307. })
  308. } else if (type === this.UNCHANGED) {
  309. position += content.length
  310. } else {
  311. throw 'Unknown type'
  312. }
  313. }
  314. return ops
  315. }
  316. }
  317. function __guard__(value, transform) {
  318. return typeof value !== 'undefined' && value !== null
  319. ? transform(value)
  320. : undefined
  321. }