UpdateCompressor.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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) { lastUpdate.op.push(update.op); }
  75. } else {
  76. const nextUpdate = {
  77. op: [],
  78. meta: update.meta,
  79. v: update.v
  80. };
  81. if (update.op !== UpdateCompressor.NOOP) { nextUpdate.op.push(update.op); }
  82. concattedUpdates.push(nextUpdate);
  83. }
  84. }
  85. return concattedUpdates;
  86. },
  87. compressRawUpdates(lastPreviousUpdate, rawUpdates) {
  88. if (__guard__(lastPreviousUpdate != null ? lastPreviousUpdate.op : undefined, x => x.length) > 1) {
  89. // if the last previous update was an array op, don't compress onto it.
  90. // The avoids cases where array length changes but version number doesn't
  91. return [lastPreviousUpdate].concat(UpdateCompressor.compressRawUpdates(null,rawUpdates));
  92. }
  93. if (lastPreviousUpdate != null) {
  94. rawUpdates = [lastPreviousUpdate].concat(rawUpdates);
  95. }
  96. let updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates);
  97. updates = UpdateCompressor.compressUpdates(updates);
  98. return UpdateCompressor.concatUpdatesWithSameVersion(updates);
  99. },
  100. compressUpdates(updates) {
  101. if (updates.length === 0) { return []; }
  102. let compressedUpdates = [updates.shift()];
  103. for (const update of Array.from(updates)) {
  104. const lastCompressedUpdate = compressedUpdates.pop();
  105. if (lastCompressedUpdate != null) {
  106. compressedUpdates = compressedUpdates.concat(UpdateCompressor._concatTwoUpdates(lastCompressedUpdate, update));
  107. } else {
  108. compressedUpdates.push(update);
  109. }
  110. }
  111. return compressedUpdates;
  112. },
  113. MAX_TIME_BETWEEN_UPDATES: (oneMinute = 60 * 1000),
  114. MAX_UPDATE_SIZE: (twoMegabytes = 2* 1024 * 1024),
  115. _concatTwoUpdates(firstUpdate, secondUpdate) {
  116. let offset;
  117. firstUpdate = {
  118. op: firstUpdate.op,
  119. meta: {
  120. user_id: firstUpdate.meta.user_id || null,
  121. start_ts: firstUpdate.meta.start_ts || firstUpdate.meta.ts,
  122. end_ts: firstUpdate.meta.end_ts || firstUpdate.meta.ts
  123. },
  124. v: firstUpdate.v
  125. };
  126. secondUpdate = {
  127. op: secondUpdate.op,
  128. meta: {
  129. user_id: secondUpdate.meta.user_id || null,
  130. start_ts: secondUpdate.meta.start_ts || secondUpdate.meta.ts,
  131. end_ts: secondUpdate.meta.end_ts || secondUpdate.meta.ts
  132. },
  133. v: secondUpdate.v
  134. };
  135. if (firstUpdate.meta.user_id !== secondUpdate.meta.user_id) {
  136. return [firstUpdate, secondUpdate];
  137. }
  138. if ((secondUpdate.meta.start_ts - firstUpdate.meta.end_ts) > UpdateCompressor.MAX_TIME_BETWEEN_UPDATES) {
  139. return [firstUpdate, secondUpdate];
  140. }
  141. const firstOp = firstUpdate.op;
  142. const secondOp = secondUpdate.op;
  143. const firstSize = (firstOp.i != null ? firstOp.i.length : undefined) || (firstOp.d != null ? firstOp.d.length : undefined);
  144. const secondSize = (secondOp.i != null ? secondOp.i.length : undefined) || (secondOp.d != null ? secondOp.d.length : undefined);
  145. // Two inserts
  146. if ((firstOp.i != null) && (secondOp.i != null) && (firstOp.p <= secondOp.p && secondOp.p <= (firstOp.p + firstOp.i.length)) && ((firstSize + secondSize) < UpdateCompressor.MAX_UPDATE_SIZE)) {
  147. return [{
  148. meta: {
  149. start_ts: firstUpdate.meta.start_ts,
  150. end_ts: secondUpdate.meta.end_ts,
  151. user_id: firstUpdate.meta.user_id
  152. },
  153. op: {
  154. p: firstOp.p,
  155. i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
  156. },
  157. v: secondUpdate.v
  158. }
  159. ];
  160. // Two deletes
  161. } 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)) {
  162. return [{
  163. meta: {
  164. start_ts: firstUpdate.meta.start_ts,
  165. end_ts: secondUpdate.meta.end_ts,
  166. user_id: firstUpdate.meta.user_id
  167. },
  168. op: {
  169. p: secondOp.p,
  170. d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
  171. },
  172. v: secondUpdate.v
  173. }
  174. ];
  175. // An insert and then a delete
  176. } else if ((firstOp.i != null) && (secondOp.d != null) && (firstOp.p <= secondOp.p && secondOp.p <= (firstOp.p + firstOp.i.length))) {
  177. offset = secondOp.p - firstOp.p;
  178. const insertedText = firstOp.i.slice(offset, offset + secondOp.d.length);
  179. // Only trim the insert when the delete is fully contained within in it
  180. if (insertedText === secondOp.d) {
  181. const insert = strRemove(firstOp.i, offset, secondOp.d.length);
  182. return [{
  183. meta: {
  184. start_ts: firstUpdate.meta.start_ts,
  185. end_ts: secondUpdate.meta.end_ts,
  186. user_id: firstUpdate.meta.user_id
  187. },
  188. op: {
  189. p: firstOp.p,
  190. i: insert
  191. },
  192. v: secondUpdate.v
  193. }
  194. ];
  195. } else {
  196. // This will only happen if the delete extends outside the insert
  197. return [firstUpdate, secondUpdate];
  198. }
  199. // A delete then an insert at the same place, likely a copy-paste of a chunk of content
  200. } else if ((firstOp.d != null) && (secondOp.i != null) && (firstOp.p === secondOp.p)) {
  201. offset = firstOp.p;
  202. const diff_ops = this.diffAsShareJsOps(firstOp.d, secondOp.i);
  203. if (diff_ops.length === 0) {
  204. return [{ // Noop
  205. meta: {
  206. start_ts: firstUpdate.meta.start_ts,
  207. end_ts: secondUpdate.meta.end_ts,
  208. user_id: firstUpdate.meta.user_id
  209. },
  210. op: {
  211. p: firstOp.p,
  212. i: ""
  213. },
  214. v: secondUpdate.v
  215. }];
  216. } else {
  217. return diff_ops.map(function(op) {
  218. op.p += offset;
  219. return {
  220. meta: {
  221. start_ts: firstUpdate.meta.start_ts,
  222. end_ts: secondUpdate.meta.end_ts,
  223. user_id: firstUpdate.meta.user_id
  224. },
  225. op,
  226. v: secondUpdate.v
  227. };});
  228. }
  229. } else {
  230. return [firstUpdate, secondUpdate];
  231. }
  232. },
  233. ADDED: 1,
  234. REMOVED: -1,
  235. UNCHANGED: 0,
  236. diffAsShareJsOps(before, after, callback) {
  237. if (callback == null) { callback = function(error, ops) {}; }
  238. const diffs = dmp.diff_main(before, after);
  239. dmp.diff_cleanupSemantic(diffs);
  240. const ops = [];
  241. let position = 0;
  242. for (const diff of Array.from(diffs)) {
  243. const type = diff[0];
  244. const content = diff[1];
  245. if (type === this.ADDED) {
  246. ops.push({
  247. i: content,
  248. p: position
  249. });
  250. position += content.length;
  251. } else if (type === this.REMOVED) {
  252. ops.push({
  253. d: content,
  254. p: position
  255. });
  256. } else if (type === this.UNCHANGED) {
  257. position += content.length;
  258. } else {
  259. throw "Unknown type";
  260. }
  261. }
  262. return ops;
  263. }
  264. });
  265. function __guard__(value, transform) {
  266. return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
  267. }