| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- strInject = (s1, pos, s2) -> s1[...pos] + s2 + s1[pos..]
- strRemove = (s1, pos, length) -> s1[...pos] + s1[(pos + length)..]
- diff_match_patch = require("../lib/diff_match_patch").diff_match_patch
- dmp = new diff_match_patch()
- module.exports = UpdateCompressor =
- NOOP: "noop"
- # Updates come from the doc updater in format
- # {
- # op: [ { ... op1 ... }, { ... op2 ... } ]
- # meta: { ts: ..., user_id: ... }
- # }
- # but it's easier to work with on op per update, so convert these updates to
- # our compressed format
- # [{
- # op: op1
- # meta: { start_ts: ... , end_ts: ..., user_id: ... }
- # }, {
- # op: op2
- # meta: { start_ts: ... , end_ts: ..., user_id: ... }
- # }]
- convertToSingleOpUpdates: (updates) ->
- splitUpdates = []
- for update in updates
- # Reject any non-insert or delete ops, i.e. comments
- ops = update.op.filter (o) -> o.i? or o.d?
- if ops.length == 0
- splitUpdates.push
- op: UpdateCompressor.NOOP
- meta:
- start_ts: update.meta.start_ts or update.meta.ts
- end_ts: update.meta.end_ts or update.meta.ts
- user_id: update.meta.user_id
- v: update.v
- else
- for op in ops
- splitUpdates.push
- op: op
- meta:
- start_ts: update.meta.start_ts or update.meta.ts
- end_ts: update.meta.end_ts or update.meta.ts
- user_id: update.meta.user_id
- v: update.v
- return splitUpdates
- concatUpdatesWithSameVersion: (updates) ->
- concattedUpdates = []
- for update in updates
- lastUpdate = concattedUpdates[concattedUpdates.length - 1]
- if lastUpdate? and lastUpdate.v == update.v
- lastUpdate.op.push update.op unless update.op == UpdateCompressor.NOOP
- else
- nextUpdate =
- op: []
- meta: update.meta
- v: update.v
- nextUpdate.op.push update.op unless update.op == UpdateCompressor.NOOP
- concattedUpdates.push nextUpdate
- return concattedUpdates
- compressRawUpdates: (lastPreviousUpdate, rawUpdates) ->
- if lastPreviousUpdate?.op?.length > 1
- # if the last previous update was an array op, don't compress onto it.
- # The avoids cases where array length changes but version number doesn't
- return [lastPreviousUpdate].concat UpdateCompressor.compressRawUpdates(null,rawUpdates)
- if lastPreviousUpdate?
- rawUpdates = [lastPreviousUpdate].concat(rawUpdates)
- updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates)
- updates = UpdateCompressor.compressUpdates(updates)
- return UpdateCompressor.concatUpdatesWithSameVersion(updates)
- compressUpdates: (updates) ->
- return [] if updates.length == 0
- compressedUpdates = [updates.shift()]
- for update in updates
- lastCompressedUpdate = compressedUpdates.pop()
- if lastCompressedUpdate?
- compressedUpdates = compressedUpdates.concat UpdateCompressor._concatTwoUpdates lastCompressedUpdate, update
- else
- compressedUpdates.push update
- return compressedUpdates
- MAX_TIME_BETWEEN_UPDATES: oneMinute = 60 * 1000
- MAX_UPDATE_SIZE: twoMegabytes = 2* 1024 * 1024
- _concatTwoUpdates: (firstUpdate, secondUpdate) ->
- firstUpdate =
- op: firstUpdate.op
- meta:
- user_id: firstUpdate.meta.user_id or null
- start_ts: firstUpdate.meta.start_ts or firstUpdate.meta.ts
- end_ts: firstUpdate.meta.end_ts or firstUpdate.meta.ts
- v: firstUpdate.v
- secondUpdate =
- op: secondUpdate.op
- meta:
- user_id: secondUpdate.meta.user_id or null
- start_ts: secondUpdate.meta.start_ts or secondUpdate.meta.ts
- end_ts: secondUpdate.meta.end_ts or secondUpdate.meta.ts
- v: secondUpdate.v
- if firstUpdate.meta.user_id != secondUpdate.meta.user_id
- return [firstUpdate, secondUpdate]
- if secondUpdate.meta.start_ts - firstUpdate.meta.end_ts > UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
- return [firstUpdate, secondUpdate]
- firstOp = firstUpdate.op
- secondOp = secondUpdate.op
- firstSize = firstOp.i?.length or firstOp.d?.length
- secondSize = secondOp.i?.length or secondOp.d?.length
- # Two inserts
- if firstOp.i? and secondOp.i? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length) and firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
- return [
- meta:
- start_ts: firstUpdate.meta.start_ts
- end_ts: secondUpdate.meta.end_ts
- user_id: firstUpdate.meta.user_id
- op:
- p: firstOp.p
- i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
- v: secondUpdate.v
- ]
- # Two deletes
- else if firstOp.d? and secondOp.d? and secondOp.p <= firstOp.p <= (secondOp.p + secondOp.d.length) and firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
- return [
- meta:
- start_ts: firstUpdate.meta.start_ts
- end_ts: secondUpdate.meta.end_ts
- user_id: firstUpdate.meta.user_id
- op:
- p: secondOp.p
- d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
- v: secondUpdate.v
- ]
- # An insert and then a delete
- else if firstOp.i? and secondOp.d? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length)
- offset = secondOp.p - firstOp.p
- insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
- # Only trim the insert when the delete is fully contained within in it
- if insertedText == secondOp.d
- insert = strRemove(firstOp.i, offset, secondOp.d.length)
- return [
- meta:
- start_ts: firstUpdate.meta.start_ts
- end_ts: secondUpdate.meta.end_ts
- user_id: firstUpdate.meta.user_id
- op:
- p: firstOp.p
- i: insert
- v: secondUpdate.v
- ]
- else
- # This will only happen if the delete extends outside the insert
- return [firstUpdate, secondUpdate]
- # A delete then an insert at the same place, likely a copy-paste of a chunk of content
- else if firstOp.d? and secondOp.i? and firstOp.p == secondOp.p
- offset = firstOp.p
- diff_ops = @diffAsShareJsOps(firstOp.d, secondOp.i)
- if diff_ops.length == 0
- return [{ # Noop
- meta:
- start_ts: firstUpdate.meta.start_ts
- end_ts: secondUpdate.meta.end_ts
- user_id: firstUpdate.meta.user_id
- op:
- p: firstOp.p
- i: ""
- v: secondUpdate.v
- }]
- else
- return diff_ops.map (op) ->
- op.p += offset
- return {
- meta:
- start_ts: firstUpdate.meta.start_ts
- end_ts: secondUpdate.meta.end_ts
- user_id: firstUpdate.meta.user_id
- op: op
- v: secondUpdate.v
- }
- else
- return [firstUpdate, secondUpdate]
- ADDED: 1
- REMOVED: -1
- UNCHANGED: 0
- diffAsShareJsOps: (before, after, callback = (error, ops) ->) ->
- diffs = dmp.diff_main(before, after)
- dmp.diff_cleanupSemantic(diffs)
- ops = []
- position = 0
- for diff in diffs
- type = diff[0]
- content = diff[1]
- if type == @ADDED
- ops.push
- i: content
- p: position
- position += content.length
- else if type == @REMOVED
- ops.push
- d: content
- p: position
- else if type == @UNCHANGED
- position += content.length
- else
- throw "Unknown type"
- return ops
|