UpdateCompressor.js 9.4 KB

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