UpdateCompressor.js 9.6 KB

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