ShareJsDoc.coffee 5.7 KB

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