mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
185 lines
6.3 KiB
CoffeeScript
185 lines
6.3 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 = (decodeURIComponent(escape(line)) for line in docLines)
|
|
snapshot = docLines.join("\n")
|
|
@track_changes = false
|
|
|
|
@connection = {
|
|
send: (update) =>
|
|
@_startInflightOpTimeout(update)
|
|
if window.disconnectOnUpdate? and Math.random() < window.disconnectOnUpdate
|
|
sl_console.log "Disconnecting on update", update
|
|
window._ide.socket.socket.disconnect()
|
|
if window.dropUpdates? and Math.random() < window.dropUpdates
|
|
sl_console.log "Simulating a lost update", update
|
|
return
|
|
if @track_changes
|
|
update.meta ?= {}
|
|
update.meta.tc = @track_changes_id_seeds.inflight
|
|
@socket.emit "applyOtUpdate", @doc_id, update, (error) =>
|
|
return @_handleError(error) if error?
|
|
state: "ok"
|
|
id: @socket.socket.sessionid
|
|
}
|
|
|
|
@_doc = new ShareJs.Doc @connection, @doc_id,
|
|
type: @type
|
|
@_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
|
|
@_doc.on "change", (args...) =>
|
|
@trigger "change", args...
|
|
@_doc.on "acknowledge", () =>
|
|
@lastAcked = new Date() # note time of last ack from server for an op we sent
|
|
@trigger "acknowledge"
|
|
@_doc.on "remoteop", (args...) =>
|
|
# As soon as we're working with a collaborator, start sending
|
|
# ops as quickly as possible for low latency.
|
|
@_doc.setFlushDelay(0)
|
|
@trigger "remoteop", args...
|
|
@_doc.on "flipped_pending_to_inflight", () =>
|
|
@trigger "flipped_pending_to_inflight"
|
|
@_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
|
|
console.log error
|
|
@_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) ->
|
|
sl_console.log "[updateConnectionState] Setting state to #{state}"
|
|
@connection.state = state
|
|
@connection.id = @socket.socket.sessionid
|
|
@_doc.autoOpen = false
|
|
@_doc._connectionStateChanged(state)
|
|
@lastAcked = null # reset the last ack time when connection changes
|
|
|
|
hasBufferedOps: () ->
|
|
@_doc.inflightOp? or @_doc.pendingOp?
|
|
|
|
getInflightOp: () -> @_doc.inflightOp
|
|
getPendingOp: () -> @_doc.pendingOp
|
|
getRecentAck: () ->
|
|
# check if we have received an ack recently (within a factor of two of the single user flush delay)
|
|
@lastAcked? and new Date() - @lastAcked < 2 * SINGLE_USER_FLUSH_DELAY
|
|
getOpSize: (op) ->
|
|
# compute size of an op from its components
|
|
# (total number of characters inserted and deleted)
|
|
size = 0
|
|
for component in op or []
|
|
if component?.i?
|
|
size += component.i.length
|
|
if component?.d?
|
|
size += component.d.length
|
|
return size
|
|
|
|
attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength)
|
|
detachFromAce: () -> @_doc.detach_ace?()
|
|
|
|
INFLIGHT_OP_TIMEOUT: 5000 # Retry sending ops after 5 seconds without an ack
|
|
WAIT_FOR_CONNECTION_TIMEOUT: 500 # If we're waiting for the project to join, try again in 0.5 seconds
|
|
_startInflightOpTimeout: (update) ->
|
|
@_startFatalTimeoutTimer(update)
|
|
retryOp = () =>
|
|
# 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.
|
|
sl_console.log "[inflightOpTimeout] Trying op again"
|
|
if @_doc.inflightOp?
|
|
# When there is a socket.io disconnect, @_doc.inflightSubmittedIds
|
|
# is updated with the socket.io client id of the current op in flight
|
|
# (meta.source of the op).
|
|
# @connection.id is the client id of the current socket.io session.
|
|
# So we need both depending on whether the op was submitted before
|
|
# one or more disconnects, or if it was submitted during the current session.
|
|
update.dupIfSource = [@connection.id, @_doc.inflightSubmittedIds...]
|
|
|
|
# We must be joined to a project for applyOtUpdate to work on the real-time
|
|
# service, so don't send an op if we're not. Connection state is set to 'ok'
|
|
# when we've joined the project
|
|
if @connection.state != "ok"
|
|
sl_console.log "[inflightOpTimeout] Not connected, retrying in 0.5s"
|
|
timer = setTimeout retryOp, @WAIT_FOR_CONNECTION_TIMEOUT
|
|
else
|
|
sl_console.log "[inflightOpTimeout] Sending"
|
|
@connection.send(update)
|
|
|
|
timer = setTimeout retryOp, @INFLIGHT_OP_TIMEOUT
|
|
@_doc.inflightCallbacks.push () =>
|
|
@_clearFatalTimeoutTimer()
|
|
clearTimeout timer
|
|
|
|
FATAL_OP_TIMEOUT: 30000 # 30 seconds
|
|
_startFatalTimeoutTimer: (update) ->
|
|
# If an op doesn't get acked within FATAL_OP_TIMEOUT, something has
|
|
# gone unrecoverably wrong (the op will have been retried multiple times)
|
|
return if @_timeoutTimer?
|
|
@_timeoutTimer = setTimeout () =>
|
|
@_clearFatalTimeoutTimer()
|
|
@trigger "op:timeout", update
|
|
, @FATAL_OP_TIMEOUT
|
|
|
|
_clearFatalTimeoutTimer: () ->
|
|
return if !@_timeoutTimer?
|
|
clearTimeout @_timeoutTimer
|
|
@_timeoutTimer = null
|
|
|
|
_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)
|