UpdateCompressor.coffee 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. strInject = (s1, pos, s2) -> s1[...pos] + s2 + s1[pos..]
  2. strRemove = (s1, pos, length) -> s1[...pos] + s1[(pos + length)..]
  3. module.exports = UpdateCompressor =
  4. NOOP: "noop"
  5. # Updates come from the doc updater in format
  6. # {
  7. # op: [ { ... op1 ... }, { ... op2 ... } ]
  8. # meta: { ts: ..., user_id: ... }
  9. # }
  10. # but it's easier to work with on op per update, so convert these updates to
  11. # our compressed format
  12. # [{
  13. # op: op1
  14. # meta: { start_ts: ... , end_ts: ..., user_id: ... }
  15. # }, {
  16. # op: op2
  17. # meta: { start_ts: ... , end_ts: ..., user_id: ... }
  18. # }]
  19. convertToSingleOpUpdates: (updates) ->
  20. splitUpdates = []
  21. for update in updates
  22. if update.op.length == 0
  23. splitUpdates.push
  24. op: UpdateCompressor.NOOP
  25. meta:
  26. start_ts: update.meta.start_ts or update.meta.ts
  27. end_ts: update.meta.end_ts or update.meta.ts
  28. user_id: update.meta.user_id
  29. v: update.v
  30. else
  31. for op in update.op
  32. splitUpdates.push
  33. op: op
  34. meta:
  35. start_ts: update.meta.start_ts or update.meta.ts
  36. end_ts: update.meta.end_ts or update.meta.ts
  37. user_id: update.meta.user_id
  38. v: update.v
  39. return splitUpdates
  40. concatUpdatesWithSameVersion: (updates) ->
  41. concattedUpdates = []
  42. for update in updates
  43. lastUpdate = concattedUpdates[concattedUpdates.length - 1]
  44. if lastUpdate? and lastUpdate.v == update.v
  45. lastUpdate.op.push update.op unless update.op == UpdateCompressor.NOOP
  46. else
  47. nextUpdate =
  48. op: []
  49. meta: update.meta
  50. v: update.v
  51. nextUpdate.op.push update.op unless update.op == UpdateCompressor.NOOP
  52. concattedUpdates.push nextUpdate
  53. return concattedUpdates
  54. compressRawUpdates: (lastPreviousUpdate, rawUpdates) ->
  55. if lastPreviousUpdate?.op?.length > 1
  56. # if the last previous update was an array op, don't compress onto it.
  57. # The avoids cases where array length changes but version number doesn't
  58. return [lastPreviousUpdate].concat UpdateCompressor.compressRawUpdates(null,rawUpdates)
  59. if lastPreviousUpdate?
  60. rawUpdates = [lastPreviousUpdate].concat(rawUpdates)
  61. updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates)
  62. updates = UpdateCompressor.compressUpdates(updates)
  63. return UpdateCompressor.concatUpdatesWithSameVersion(updates)
  64. compressUpdates: (updates) ->
  65. return [] if updates.length == 0
  66. compressedUpdates = [updates.shift()]
  67. for update in updates
  68. lastCompressedUpdate = compressedUpdates.pop()
  69. if lastCompressedUpdate?
  70. compressedUpdates = compressedUpdates.concat UpdateCompressor._concatTwoUpdates lastCompressedUpdate, update
  71. else
  72. compressedUpdates.push update
  73. return compressedUpdates
  74. MAX_TIME_BETWEEN_UPDATES: oneMinute = 60 * 1000
  75. MAX_UPDATE_SIZE: twoMegabytes = 2* 1024 * 1024
  76. _concatTwoUpdates: (firstUpdate, secondUpdate) ->
  77. firstUpdate =
  78. op: firstUpdate.op
  79. meta:
  80. user_id: firstUpdate.meta.user_id or null
  81. start_ts: firstUpdate.meta.start_ts or firstUpdate.meta.ts
  82. end_ts: firstUpdate.meta.end_ts or firstUpdate.meta.ts
  83. v: firstUpdate.v
  84. secondUpdate =
  85. op: secondUpdate.op
  86. meta:
  87. user_id: secondUpdate.meta.user_id or null
  88. start_ts: secondUpdate.meta.start_ts or secondUpdate.meta.ts
  89. end_ts: secondUpdate.meta.end_ts or secondUpdate.meta.ts
  90. v: secondUpdate.v
  91. if firstUpdate.meta.user_id != secondUpdate.meta.user_id
  92. return [firstUpdate, secondUpdate]
  93. if secondUpdate.meta.start_ts - firstUpdate.meta.end_ts > UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
  94. return [firstUpdate, secondUpdate]
  95. firstOp = firstUpdate.op
  96. secondOp = secondUpdate.op
  97. firstSize = firstOp.i?.length or firstOp.d?.length
  98. secondSize = secondOp.i?.length or secondOp.d?.length
  99. # Two inserts
  100. if firstOp.i? and secondOp.i? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length) and firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
  101. return [
  102. meta:
  103. start_ts: firstUpdate.meta.start_ts
  104. end_ts: secondUpdate.meta.end_ts
  105. user_id: firstUpdate.meta.user_id
  106. op:
  107. p: firstOp.p
  108. i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
  109. v: secondUpdate.v
  110. ]
  111. # Two deletes
  112. else if firstOp.d? and secondOp.d? and secondOp.p <= firstOp.p <= (secondOp.p + secondOp.d.length) and firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
  113. return [
  114. meta:
  115. start_ts: firstUpdate.meta.start_ts
  116. end_ts: secondUpdate.meta.end_ts
  117. user_id: firstUpdate.meta.user_id
  118. op:
  119. p: secondOp.p
  120. d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
  121. v: secondUpdate.v
  122. ]
  123. # An insert and then a delete
  124. else if firstOp.i? and secondOp.d? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length)
  125. offset = secondOp.p - firstOp.p
  126. insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
  127. # Only trim the insert when the delete is fully contained within in it
  128. if insertedText == secondOp.d
  129. insert = strRemove(firstOp.i, offset, secondOp.d.length)
  130. return [
  131. meta:
  132. start_ts: firstUpdate.meta.start_ts
  133. end_ts: secondUpdate.meta.end_ts
  134. user_id: firstUpdate.meta.user_id
  135. op:
  136. p: firstOp.p
  137. i: insert
  138. v: secondUpdate.v
  139. ]
  140. else
  141. # This will only happen if the delete extends outside the insert
  142. return [firstUpdate, secondUpdate]
  143. else
  144. return [firstUpdate, secondUpdate]