Document.coffee 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. define [
  2. "editor/ShareJsDoc"
  3. "libs/backbone"
  4. "libs/underscore"
  5. ], (ShareJsDoc) ->
  6. class Document
  7. @getDocument: (ide, doc_id) ->
  8. @openDocs ||= {}
  9. if !@openDocs[doc_id]?
  10. @openDocs[doc_id] = new Document(ide, doc_id)
  11. return @openDocs[doc_id]
  12. @hasUnsavedChanges: () ->
  13. for doc_id, doc of (@openDocs or {})
  14. return true if doc.hasBufferedOps()
  15. return false
  16. constructor: (@ide, @doc_id) ->
  17. @connected = @ide.socket.socket.connected
  18. @joined = false
  19. @wantToBeJoined = false
  20. @_checkConsistency = _.bind(@_checkConsistency, @)
  21. @inconsistentCount = 0
  22. @_bindToEditorEvents()
  23. @_bindToSocketEvents()
  24. attachToAce: (@ace) ->
  25. @doc?.attachToAce(@ace)
  26. editorDoc = @ace.getSession().getDocument()
  27. editorDoc.on "change", @_checkConsistency
  28. detachFromAce: () ->
  29. @doc?.detachFromAce()
  30. editorDoc = @ace?.getSession().getDocument()
  31. editorDoc?.off "change", @_checkConsistency
  32. _checkConsistency: () ->
  33. # We've been seeing a lot of errors when I think there shouldn't be
  34. # any, which may be related to this check happening before the change is
  35. # applied. If we use a timeout, hopefully we can reduce this.
  36. setTimeout () =>
  37. editorValue = @ace?.getValue()
  38. sharejsValue = @doc?.getSnapshot()
  39. if editorValue != sharejsValue
  40. @inconsistentCount++
  41. else
  42. @inconsistentCount = 0
  43. if @inconsistentCount >= 3
  44. @_onError new Error("Editor text does not match server text")
  45. , 0
  46. getSnapshot: () ->
  47. @doc?.getSnapshot()
  48. getType: () ->
  49. @doc?.getType()
  50. getInflightOp: () ->
  51. @doc?.getInflightOp()
  52. getPendingOp: () ->
  53. @doc?.getPendingOp()
  54. hasBufferedOps: () ->
  55. @doc?.hasBufferedOps()
  56. _bindToSocketEvents: () ->
  57. @_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update)
  58. @ide.socket.on "otUpdateApplied", @_onUpdateAppliedHandler
  59. @_onErrorHandler = (error, update) => @_onError(error, update)
  60. @ide.socket.on "otUpdateError", @_onErrorHandler
  61. @_onDisconnectHandler = (error) => @_onDisconnect(error)
  62. @ide.socket.on "disconnect", @_onDisconnectHandler
  63. _bindToEditorEvents: () ->
  64. @_onReconnectHandler = (update) => @_onReconnect(update)
  65. @ide.on "afterJoinProject", @_onReconnectHandler
  66. unBindFromSocketEvents: () ->
  67. @ide.socket.removeListener "otUpdateApplied", @_onUpdateAppliedHandler
  68. @ide.socket.removeListener "otUpdateError", @_onUpdateErrorHandler
  69. @ide.socket.removeListener "disconnect", @_onDisconnectHandler
  70. unBindFromEditorEvents: () ->
  71. @ide.off "afterJoinProject", @_onReconnectHandler
  72. leaveAndCleanUp: () ->
  73. @leave (error) =>
  74. @_cleanUp()
  75. join: (callback = (error) ->) ->
  76. @wantToBeJoined = true
  77. @_cancelLeave()
  78. if @connected
  79. return @_joinDoc callback
  80. else
  81. @_joinCallbacks ||= []
  82. @_joinCallbacks.push callback
  83. leave: (callback = (error) ->) ->
  84. @wantToBeJoined = false
  85. @_cancelJoin()
  86. if (@doc? and @doc.hasBufferedOps())
  87. @_leaveCallbacks ||= []
  88. @_leaveCallbacks.push callback
  89. else if !@connected
  90. callback()
  91. else
  92. @_leaveDoc(callback)
  93. pollSavedStatus: () ->
  94. # returns false if doc has ops waiting to be acknowledged or
  95. # sent that haven't changed since the last time we checked.
  96. # Otherwise returns true.
  97. inflightOp = @getInflightOp()
  98. pendingOp = @getPendingOp()
  99. if !inflightOp? and !pendingOp?
  100. # there's nothing going on
  101. saved = true
  102. else if inflightOp == @oldInflightOp
  103. saved = false
  104. else if pendingOp?
  105. saved = false
  106. else
  107. saved = true
  108. @oldInflightOp = inflightOp
  109. return saved
  110. _cancelLeave: () ->
  111. if @_leaveCallbacks?
  112. delete @_leaveCallbacks
  113. _cancelJoin: () ->
  114. if @_joinCallbacks?
  115. delete @_joinCallbacks
  116. _onUpdateApplied: (update) ->
  117. @ide.pushEvent "received-update",
  118. doc_id: @doc_id
  119. remote_doc_id: update?.doc
  120. wantToBeJoined: @wantToBeJoined
  121. update: update
  122. if Math.random() < (@ide.disconnectRate or 0)
  123. console.log "Simulating disconnect"
  124. @ide.connectionManager.disconnect()
  125. return
  126. if update?.doc == @doc_id and @doc?
  127. @doc.processUpdateFromServer update
  128. if !@wantToBeJoined
  129. @leave()
  130. _onDisconnect: () ->
  131. @connected = false
  132. @joined = false
  133. @doc?.updateConnectionState "disconnected"
  134. _onReconnect: () ->
  135. @ide.pushEvent "reconnected:afterJoinProject"
  136. @connected = true
  137. if @wantToBeJoined or @doc?.hasBufferedOps()
  138. @_joinDoc (error) =>
  139. return @_onError(error) if error?
  140. @doc.updateConnectionState "ok"
  141. @doc.flushPendingOps()
  142. @_callJoinCallbacks()
  143. _callJoinCallbacks: () ->
  144. for callback in @_joinCallbacks or []
  145. callback()
  146. delete @_joinCallbacks
  147. _joinDoc: (callback = (error) ->) ->
  148. if @doc?
  149. @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates) =>
  150. return callback(error) if error?
  151. @joined = true
  152. @doc.catchUp( updates )
  153. callback()
  154. else
  155. @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version) =>
  156. return callback(error) if error?
  157. @joined = true
  158. @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
  159. @_bindToShareJsDocEvents()
  160. callback()
  161. _leaveDoc: (callback = (error) ->) ->
  162. @ide.socket.emit 'leaveDoc', @doc_id, (error) =>
  163. return callback(error) if error?
  164. @joined = false
  165. for callback in @_leaveCallbacks or []
  166. callback(error)
  167. delete @_leaveCallbacks
  168. callback(error)
  169. _cleanUp: () ->
  170. delete Document.openDocs[@doc_id]
  171. @unBindFromEditorEvents()
  172. @unBindFromSocketEvents()
  173. _bindToShareJsDocEvents: () ->
  174. @doc.on "error", (error, meta) => @_onError error, meta
  175. @doc.on "externalUpdate", () =>
  176. @ide.pushEvent "externalUpdate",
  177. doc_id: @doc_id
  178. @trigger "externalUpdate"
  179. @doc.on "remoteop", () =>
  180. @ide.pushEvent "remoteop",
  181. doc_id: @doc_id
  182. @trigger "remoteop"
  183. @doc.on "op:sent", (op) =>
  184. @ide.pushEvent "op:sent",
  185. doc_id: @doc_id
  186. op: op
  187. @trigger "op:sent"
  188. @doc.on "op:acknowledged", (op) =>
  189. @ide.pushEvent "op:acknowledged",
  190. doc_id: @doc_id
  191. op: op
  192. @trigger "op:acknowledged"
  193. @doc.on "op:timeout", (op) =>
  194. @ide.pushEvent "op:timeout",
  195. doc_id: @doc_id
  196. op: op
  197. @trigger "op:timeout"
  198. ga?('send', 'event', 'error', "op timeout", "Op was now acknowledged - #{ide.socket.socket.transport.name}" )
  199. @ide.connectionManager.reconnectImmediately()
  200. @doc.on "flush", (inflightOp, pendingOp, version) =>
  201. @ide.pushEvent "flush",
  202. doc_id: @doc_id,
  203. inflightOp: inflightOp,
  204. pendingOp: pendingOp
  205. v: version
  206. _onError: (error, meta = {}) ->
  207. console.error "ShareJS error", error, meta
  208. ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
  209. @ide.socket.disconnect()
  210. meta.doc_id = @doc_id
  211. @ide.reportError(error, meta)
  212. @doc?.clearInflightAndPendingOps()
  213. @_cleanUp()
  214. @trigger "error", error
  215. _.extend(Document::, Backbone.Events)
  216. return Document