ShareJsDoc.coffee 4.9 KB

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