UpdateCompressor.coffee 6.7 KB

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