UpdateCompressor.coffee 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. strInject = (s1, pos, s2) -> s1[...pos] + s2 + s1[pos..]
  2. strRemove = (s1, pos, length) -> s1[...pos] + s1[(pos + length)..]
  3. diff_match_patch = require("../lib/diff_match_patch").diff_match_patch
  4. dmp = new diff_match_patch()
  5. module.exports = UpdateCompressor =
  6. NOOP: "noop"
  7. # Updates come from the doc updater in format
  8. # {
  9. # op: [ { ... op1 ... }, { ... op2 ... } ]
  10. # meta: { ts: ..., user_id: ... }
  11. # }
  12. # but it's easier to work with on op per update, so convert these updates to
  13. # our compressed format
  14. # [{
  15. # op: op1
  16. # meta: { start_ts: ... , end_ts: ..., user_id: ... }
  17. # }, {
  18. # op: op2
  19. # meta: { start_ts: ... , end_ts: ..., user_id: ... }
  20. # }]
  21. convertToSingleOpUpdates: (updates) ->
  22. splitUpdates = []
  23. for update in updates
  24. if update.op.length == 0
  25. splitUpdates.push
  26. op: UpdateCompressor.NOOP
  27. meta:
  28. start_ts: update.meta.start_ts or update.meta.ts
  29. end_ts: update.meta.end_ts or update.meta.ts
  30. user_id: update.meta.user_id
  31. v: update.v
  32. else
  33. for op in update.op
  34. splitUpdates.push
  35. op: op
  36. meta:
  37. start_ts: update.meta.start_ts or update.meta.ts
  38. end_ts: update.meta.end_ts or update.meta.ts
  39. user_id: update.meta.user_id
  40. v: update.v
  41. return splitUpdates
  42. concatUpdatesWithSameVersion: (updates) ->
  43. concattedUpdates = []
  44. for update in updates
  45. lastUpdate = concattedUpdates[concattedUpdates.length - 1]
  46. if lastUpdate? and lastUpdate.v == update.v
  47. lastUpdate.op.push update.op unless update.op == UpdateCompressor.NOOP
  48. else
  49. nextUpdate =
  50. op: []
  51. meta: update.meta
  52. v: update.v
  53. nextUpdate.op.push update.op unless update.op == UpdateCompressor.NOOP
  54. concattedUpdates.push nextUpdate
  55. return concattedUpdates
  56. compressRawUpdates: (lastPreviousUpdate, rawUpdates) ->
  57. if lastPreviousUpdate?.op?.length > 1
  58. # if the last previous update was an array op, don't compress onto it.
  59. # The avoids cases where array length changes but version number doesn't
  60. return [lastPreviousUpdate].concat UpdateCompressor.compressRawUpdates(null,rawUpdates)
  61. if lastPreviousUpdate?
  62. rawUpdates = [lastPreviousUpdate].concat(rawUpdates)
  63. updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates)
  64. updates = UpdateCompressor.compressUpdates(updates)
  65. return UpdateCompressor.concatUpdatesWithSameVersion(updates)
  66. compressUpdates: (updates) ->
  67. return [] if updates.length == 0
  68. compressedUpdates = [updates.shift()]
  69. for update in updates
  70. lastCompressedUpdate = compressedUpdates.pop()
  71. if lastCompressedUpdate?
  72. compressedUpdates = compressedUpdates.concat UpdateCompressor._concatTwoUpdates lastCompressedUpdate, update
  73. else
  74. compressedUpdates.push update
  75. return compressedUpdates
  76. MAX_TIME_BETWEEN_UPDATES: oneMinute = 60 * 1000
  77. MAX_UPDATE_SIZE: twoMegabytes = 2* 1024 * 1024
  78. _concatTwoUpdates: (firstUpdate, secondUpdate) ->
  79. firstUpdate =
  80. op: firstUpdate.op
  81. meta:
  82. user_id: firstUpdate.meta.user_id or null
  83. start_ts: firstUpdate.meta.start_ts or firstUpdate.meta.ts
  84. end_ts: firstUpdate.meta.end_ts or firstUpdate.meta.ts
  85. v: firstUpdate.v
  86. secondUpdate =
  87. op: secondUpdate.op
  88. meta:
  89. user_id: secondUpdate.meta.user_id or null
  90. start_ts: secondUpdate.meta.start_ts or secondUpdate.meta.ts
  91. end_ts: secondUpdate.meta.end_ts or secondUpdate.meta.ts
  92. v: secondUpdate.v
  93. if firstUpdate.meta.user_id != secondUpdate.meta.user_id
  94. return [firstUpdate, secondUpdate]
  95. if secondUpdate.meta.start_ts - firstUpdate.meta.end_ts > UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
  96. return [firstUpdate, secondUpdate]
  97. firstOp = firstUpdate.op
  98. secondOp = secondUpdate.op
  99. firstSize = firstOp.i?.length or firstOp.d?.length
  100. secondSize = secondOp.i?.length or secondOp.d?.length
  101. # Two inserts
  102. if firstOp.i? and secondOp.i? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length) and firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
  103. return [
  104. meta:
  105. start_ts: firstUpdate.meta.start_ts
  106. end_ts: secondUpdate.meta.end_ts
  107. user_id: firstUpdate.meta.user_id
  108. op:
  109. p: firstOp.p
  110. i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
  111. v: secondUpdate.v
  112. ]
  113. # Two deletes
  114. else if firstOp.d? and secondOp.d? and secondOp.p <= firstOp.p <= (secondOp.p + secondOp.d.length) and firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
  115. return [
  116. meta:
  117. start_ts: firstUpdate.meta.start_ts
  118. end_ts: secondUpdate.meta.end_ts
  119. user_id: firstUpdate.meta.user_id
  120. op:
  121. p: secondOp.p
  122. d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
  123. v: secondUpdate.v
  124. ]
  125. # An insert and then a delete
  126. else if firstOp.i? and secondOp.d? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length)
  127. offset = secondOp.p - firstOp.p
  128. insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
  129. # Only trim the insert when the delete is fully contained within in it
  130. if insertedText == secondOp.d
  131. insert = strRemove(firstOp.i, offset, secondOp.d.length)
  132. return [
  133. meta:
  134. start_ts: firstUpdate.meta.start_ts
  135. end_ts: secondUpdate.meta.end_ts
  136. user_id: firstUpdate.meta.user_id
  137. op:
  138. p: firstOp.p
  139. i: insert
  140. v: secondUpdate.v
  141. ]
  142. else
  143. # This will only happen if the delete extends outside the insert
  144. return [firstUpdate, secondUpdate]
  145. # A delete then an insert at the same place, likely a copy-paste of a chunk of content
  146. else if firstOp.d? and secondOp.i? and firstOp.p == secondOp.p
  147. offset = firstOp.p
  148. diff_ops = @diffAsShareJsOps(firstOp.d, secondOp.i)
  149. if diff_ops.length == 0
  150. return [{ # Noop
  151. meta:
  152. start_ts: firstUpdate.meta.start_ts
  153. end_ts: secondUpdate.meta.end_ts
  154. user_id: firstUpdate.meta.user_id
  155. op:
  156. p: firstOp.p
  157. i: ""
  158. v: secondUpdate.v
  159. }]
  160. else
  161. return diff_ops.map (op) ->
  162. op.p += offset
  163. return {
  164. meta:
  165. start_ts: firstUpdate.meta.start_ts
  166. end_ts: secondUpdate.meta.end_ts
  167. user_id: firstUpdate.meta.user_id
  168. op: op
  169. v: secondUpdate.v
  170. }
  171. else
  172. return [firstUpdate, secondUpdate]
  173. ADDED: 1
  174. REMOVED: -1
  175. UNCHANGED: 0
  176. diffAsShareJsOps: (before, after, callback = (error, ops) ->) ->
  177. diffs = dmp.diff_main(before, after)
  178. dmp.diff_cleanupSemantic(diffs)
  179. ops = []
  180. position = 0
  181. for diff in diffs
  182. type = diff[0]
  183. content = diff[1]
  184. if type == @ADDED
  185. ops.push
  186. i: content
  187. p: position
  188. position += content.length
  189. else if type == @REMOVED
  190. ops.push
  191. d: content
  192. p: position
  193. else if type == @UNCHANGED
  194. position += content.length
  195. else
  196. throw "Unknown type"
  197. return ops