ShareJsDoc.coffee 6.4 KB

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