mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-31 21:21:03 -04:00
cde5144c42
If we do not get a reply from the server acknowledging our update after 5 seconds, send it again. If it never got to the server, this is like normal. If the update got to the server, but we never received the ack then we need to rely on ShareJs's duplicate handling. We set the dupIfSource parameter on any retried updates which let ShareJs know that it's a dup if we already have an op with this version number and client id. The doc-updater and real-time services need changes to correctly send another ack only to the submitting client in the case of a duplicate update.
140 lines
4 KiB
CoffeeScript
140 lines
4 KiB
CoffeeScript
define [
|
|
"utils/EventEmitter"
|
|
"libs/sharejs"
|
|
], (EventEmitter, ShareJs) ->
|
|
SINGLE_USER_FLUSH_DELAY = 1000 #ms
|
|
|
|
class ShareJsDoc extends EventEmitter
|
|
constructor: (@doc_id, docLines, version, @socket) ->
|
|
# Dencode any binary bits of data
|
|
# See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
|
|
@type = "text"
|
|
docLines = for line in docLines
|
|
if line.text?
|
|
@type = "json"
|
|
line.text = decodeURIComponent(escape(line.text))
|
|
else
|
|
@type = "text"
|
|
line = decodeURIComponent(escape(line))
|
|
line
|
|
|
|
if @type == "text"
|
|
snapshot = docLines.join("\n")
|
|
else if @type == "json"
|
|
snapshot = { lines: docLines }
|
|
else
|
|
throw new Error("Unknown type: #{@type}")
|
|
|
|
@connection = {
|
|
send: (update) =>
|
|
@_startInflightOpTimeout(update)
|
|
if window.dropUpdates? and Math.random() < window.dropAcks
|
|
console.log "Simulating a lost update", update
|
|
return
|
|
@socket.emit "applyOtUpdate", @doc_id, update
|
|
state: "ok"
|
|
# Unlike ShareJs, our connection.id never changes, even when we disconnect/reconnect.
|
|
# This gives this client a unique id used for detecting duplicates ops.
|
|
id: @socket.socket.sessionid
|
|
}
|
|
|
|
@_doc = new ShareJs.Doc @connection, @doc_id,
|
|
type: @type
|
|
@_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
|
|
@_doc.on "change", () =>
|
|
@trigger "change"
|
|
@_doc.on "acknowledge", () =>
|
|
@trigger "acknowledge"
|
|
@_doc.on "remoteop", () =>
|
|
# As soon as we're working with a collaborator, start sending
|
|
# ops as quickly as possible for low latency.
|
|
@_doc.setFlushDelay(0)
|
|
@trigger "remoteop"
|
|
@_doc.on "error", (e) =>
|
|
@_handleError(e)
|
|
|
|
@_bindToDocChanges(@_doc)
|
|
|
|
@processUpdateFromServer
|
|
open: true
|
|
v: version
|
|
snapshot: snapshot
|
|
|
|
submitOp: (args...) -> @_doc.submitOp(args...)
|
|
|
|
processUpdateFromServer: (message) ->
|
|
try
|
|
@_doc._onMessage message
|
|
catch error
|
|
# Version mismatches are thrown as errors
|
|
@_handleError(error)
|
|
|
|
if message?.meta?.type == "external"
|
|
@trigger "externalUpdate", message
|
|
|
|
catchUp: (updates) ->
|
|
for update, i in updates
|
|
update.v = @_doc.version
|
|
update.doc = @doc_id
|
|
@processUpdateFromServer(update)
|
|
|
|
getSnapshot: () -> @_doc.snapshot
|
|
getVersion: () -> @_doc.version
|
|
getType: () -> @type
|
|
|
|
clearInflightAndPendingOps: () ->
|
|
@_doc.inflightOp = null
|
|
@_doc.inflightCallbacks = []
|
|
@_doc.pendingOp = null
|
|
@_doc.pendingCallbacks = []
|
|
|
|
flushPendingOps: () ->
|
|
# This will flush any ops that are pending.
|
|
# If there is an inflight op it will do nothing.
|
|
@_doc.flush()
|
|
|
|
updateConnectionState: (state) ->
|
|
@connection.state = state
|
|
@_doc.autoOpen = false
|
|
@_doc._connectionStateChanged(state)
|
|
|
|
hasBufferedOps: () ->
|
|
@_doc.inflightOp? or @_doc.pendingOp?
|
|
|
|
getInflightOp: () -> @_doc.inflightOp
|
|
getPendingOp: () -> @_doc.pendingOp
|
|
|
|
attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength)
|
|
detachFromAce: () -> @_doc.detach_ace?()
|
|
|
|
INFLIGHT_OP_TIMEOUT: 5000
|
|
_startInflightOpTimeout: (update) ->
|
|
timer = setTimeout () =>
|
|
# Only send the update again if inflightOp is still populated
|
|
# This can be cleared when hard reloading the document in which
|
|
# case we don't want to keep trying to send it.
|
|
if @_doc.inflightOp?
|
|
update.dupIfSource = [@connection.id]
|
|
@connection.send(update)
|
|
# TODO: Trigger op:timeout only when some max retries have been hit
|
|
# and we need to do a full reload.
|
|
# @trigger "op:timeout", update
|
|
, @INFLIGHT_OP_TIMEOUT
|
|
@_doc.inflightCallbacks.push () =>
|
|
clearTimeout timer
|
|
|
|
_handleError: (error, meta = {}) ->
|
|
@trigger "error", error, meta
|
|
|
|
_bindToDocChanges: (doc) ->
|
|
submitOp = doc.submitOp
|
|
doc.submitOp = (args...) =>
|
|
@trigger "op:sent", args...
|
|
doc.pendingCallbacks.push () =>
|
|
@trigger "op:acknowledged", args...
|
|
submitOp.apply(doc, args)
|
|
|
|
flush = doc.flush
|
|
doc.flush = (args...) =>
|
|
@trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
|
|
flush.apply(doc, args)
|