UpdateCompressor.js 9.4 KB

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