ShareJsDoc.coffee 6.3 KB

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