Document.coffee 7.1 KB

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