ShareJsDoc.coffee 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. define [
  2. "utils/EventEmitter"
  3. "libs/sharejs"
  4. ], (EventEmitter, ShareJs) ->
  5. SINGLE_USER_FLUSH_DELAY = 1000 #ms
  6. class ShareJsDoc extends EventEmitter
  7. constructor: (@doc_id, docLines, version, @socket) ->
  8. # Dencode any binary bits of data
  9. # See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
  10. @type = "text"
  11. docLines = for line in docLines
  12. if line.text?
  13. @type = "json"
  14. line.text = decodeURIComponent(escape(line.text))
  15. else
  16. @type = "text"
  17. line = decodeURIComponent(escape(line))
  18. line
  19. if @type == "text"
  20. snapshot = docLines.join("\n")
  21. else if @type == "json"
  22. snapshot = { lines: docLines }
  23. else
  24. throw new Error("Unknown type: #{@type}")
  25. @connection = {
  26. send: (update) =>
  27. @_startInflightOpTimeout(update)
  28. if window.disconnectOnUpdate? and Math.random() < window.disconnectOnUpdate
  29. sl_console.log "Disconnecting on update", update
  30. window._ide.socket.socket.disconnect()
  31. if window.dropUpdates? and Math.random() < window.dropUpdates
  32. sl_console.log "Simulating a lost update", update
  33. return
  34. @socket.emit "applyOtUpdate", @doc_id, update, (error) =>
  35. return @_handleError(error) if error?
  36. state: "ok"
  37. id: @socket.socket.sessionid
  38. }
  39. @_doc = new ShareJs.Doc @connection, @doc_id,
  40. type: @type
  41. @_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
  42. @_doc.on "change", () =>
  43. @trigger "change"
  44. @_doc.on "acknowledge", () =>
  45. @lastAcked = new Date() # note time of last ack from server for an op we sent
  46. @trigger "acknowledge"
  47. @_doc.on "remoteop", (args...) =>
  48. # As soon as we're working with a collaborator, start sending
  49. # ops as quickly as possible for low latency.
  50. @_doc.setFlushDelay(0)
  51. @trigger "remoteop", args...
  52. @_doc.on "error", (e) =>
  53. @_handleError(e)
  54. @_bindToDocChanges(@_doc)
  55. @processUpdateFromServer
  56. open: true
  57. v: version
  58. snapshot: snapshot
  59. submitOp: (args...) -> @_doc.submitOp(args...)
  60. processUpdateFromServer: (message) ->
  61. try
  62. @_doc._onMessage message
  63. catch error
  64. # Version mismatches are thrown as errors
  65. @_handleError(error)
  66. if message?.meta?.type == "external"
  67. @trigger "externalUpdate", message
  68. catchUp: (updates) ->
  69. for update, i in updates
  70. update.v = @_doc.version
  71. update.doc = @doc_id
  72. @processUpdateFromServer(update)
  73. getSnapshot: () -> @_doc.snapshot
  74. getVersion: () -> @_doc.version
  75. getType: () -> @type
  76. clearInflightAndPendingOps: () ->
  77. @_doc.inflightOp = null
  78. @_doc.inflightCallbacks = []
  79. @_doc.pendingOp = null
  80. @_doc.pendingCallbacks = []
  81. flushPendingOps: () ->
  82. # This will flush any ops that are pending.
  83. # If there is an inflight op it will do nothing.
  84. @_doc.flush()
  85. updateConnectionState: (state) ->
  86. sl_console.log "[updateConnectionState] Setting state to #{state}"
  87. @connection.state = state
  88. @connection.id = @socket.socket.sessionid
  89. @_doc.autoOpen = false
  90. @_doc._connectionStateChanged(state)
  91. @lastAcked = null # reset the last ack time when connection changes
  92. hasBufferedOps: () ->
  93. @_doc.inflightOp? or @_doc.pendingOp?
  94. getInflightOp: () -> @_doc.inflightOp
  95. getPendingOp: () -> @_doc.pendingOp
  96. getRecentAck: () ->
  97. # check if we have received an ack recently (within the flush delay)
  98. @lastAcked? and new Date() - @lastAcked < @_doc._flushDelay
  99. getOpSize: (op) ->
  100. # compute size of an op from its components
  101. # (total number of characters inserted and deleted)
  102. size = 0
  103. for component in op or []
  104. if component?.i?
  105. size += component.i.length
  106. if component?.d?
  107. size += component.d.length
  108. return size
  109. attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength)
  110. detachFromAce: () -> @_doc.detach_ace?()
  111. INFLIGHT_OP_TIMEOUT: 5000 # Retry sending ops after 5 seconds without an ack
  112. WAIT_FOR_CONNECTION_TIMEOUT: 500 # If we're waiting for the project to join, try again in 0.5 seconds
  113. _startInflightOpTimeout: (update) ->
  114. @_startFatalTimeoutTimer(update)
  115. retryOp = () =>
  116. # Only send the update again if inflightOp is still populated
  117. # This can be cleared when hard reloading the document in which
  118. # case we don't want to keep trying to send it.
  119. sl_console.log "[inflightOpTimeout] Trying op again"
  120. if @_doc.inflightOp?
  121. # When there is a socket.io disconnect, @_doc.inflightSubmittedIds
  122. # is updated with the socket.io client id of the current op in flight
  123. # (meta.source of the op).
  124. # @connection.id is the client id of the current socket.io session.
  125. # So we need both depending on whether the op was submitted before
  126. # one or more disconnects, or if it was submitted during the current session.
  127. update.dupIfSource = [@connection.id, @_doc.inflightSubmittedIds...]
  128. # We must be joined to a project for applyOtUpdate to work on the real-time
  129. # service, so don't send an op if we're not. Connection state is set to 'ok'
  130. # when we've joined the project
  131. if @connection.state != "ok"
  132. sl_console.log "[inflightOpTimeout] Not connected, retrying in 0.5s"
  133. timer = setTimeout retryOp, @WAIT_FOR_CONNECTION_TIMEOUT
  134. else
  135. sl_console.log "[inflightOpTimeout] Sending"
  136. @connection.send(update)
  137. timer = setTimeout retryOp, @INFLIGHT_OP_TIMEOUT
  138. @_doc.inflightCallbacks.push () =>
  139. @_clearFatalTimeoutTimer()
  140. clearTimeout timer
  141. FATAL_OP_TIMEOUT: 30000 # 30 seconds
  142. _startFatalTimeoutTimer: (update) ->
  143. # If an op doesn't get acked within FATAL_OP_TIMEOUT, something has
  144. # gone unrecoverably wrong (the op will have been retried multiple times)
  145. return if @_timeoutTimer?
  146. @_timeoutTimer = setTimeout () =>
  147. @_clearFatalTimeoutTimer()
  148. @trigger "op:timeout", update
  149. , @FATAL_OP_TIMEOUT
  150. _clearFatalTimeoutTimer: () ->
  151. return if !@_timeoutTimer?
  152. clearTimeout @_timeoutTimer
  153. @_timeoutTimer = null
  154. _handleError: (error, meta = {}) ->
  155. @trigger "error", error, meta
  156. _bindToDocChanges: (doc) ->
  157. submitOp = doc.submitOp
  158. doc.submitOp = (args...) =>
  159. @trigger "op:sent", args...
  160. doc.pendingCallbacks.push () =>
  161. @trigger "op:acknowledged", args...
  162. submitOp.apply(doc, args)
  163. flush = doc.flush
  164. doc.flush = (args...) =>
  165. @trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
  166. flush.apply(doc, args)