Document.coffee 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. define [
  2. "utils/EventEmitter"
  3. "ide/editor/ShareJsDoc"
  4. "ide/review-panel/RangesTracker"
  5. ], (EventEmitter, ShareJsDoc, RangesTracker) ->
  6. class Document extends EventEmitter
  7. @getDocument: (ide, doc_id) ->
  8. @openDocs ||= {}
  9. if !@openDocs[doc_id]?
  10. sl_console.log "[getDocument] Creating new document instance for #{doc_id}"
  11. @openDocs[doc_id] = new Document(ide, doc_id)
  12. else
  13. sl_console.log "[getDocument] Returning existing document instance for #{doc_id}"
  14. return @openDocs[doc_id]
  15. @hasUnsavedChanges: () ->
  16. for doc_id, doc of (@openDocs or {})
  17. return true if doc.hasBufferedOps()
  18. return false
  19. @flushAll: () ->
  20. for doc_id, doc of @openDocs
  21. doc.flush()
  22. constructor: (@ide, @doc_id) ->
  23. @connected = @ide.socket.socket.connected
  24. @joined = false
  25. @wantToBeJoined = false
  26. @_checkAceConsistency = _.bind(@_checkConsistency, @, @ace)
  27. @_checkCMConsistency = _.bind(@_checkConsistency, @, @cm)
  28. @inconsistentCount = 0
  29. @_bindToEditorEvents()
  30. @_bindToSocketEvents()
  31. attachToAce: (@ace) ->
  32. @doc?.attachToAce(@ace)
  33. editorDoc = @ace.getSession().getDocument()
  34. editorDoc.on "change", @_checkAceConsistency
  35. @ide.$scope.$emit 'document:opened', @doc
  36. detachFromAce: () ->
  37. @doc?.detachFromAce()
  38. editorDoc = @ace?.getSession().getDocument()
  39. editorDoc?.off "change", @_checkAceConsistency
  40. @ide.$scope.$emit 'document:closed', @doc
  41. attachToCM: (@cm) ->
  42. @doc?.attachToCM(@cm)
  43. @cm?.on "change", @_checkCMConsistency
  44. @ide.$scope.$emit 'document:opened', @doc
  45. detachFromCM: () ->
  46. @doc?.detachFromCM()
  47. @cm?.off "change", @_checkCMConsistency
  48. @ide.$scope.$emit 'document:closed', @doc
  49. submitOp: (args...) -> @doc?.submitOp(args...)
  50. _checkConsistency: (editor) ->
  51. return () =>
  52. # We've been seeing a lot of errors when I think there shouldn't be
  53. # any, which may be related to this check happening before the change is
  54. # applied. If we use a timeout, hopefully we can reduce this.
  55. setTimeout () =>
  56. editorValue = editor?.getValue()
  57. sharejsValue = @doc?.getSnapshot()
  58. if editorValue != sharejsValue
  59. @inconsistentCount++
  60. else
  61. @inconsistentCount = 0
  62. if @inconsistentCount >= 3
  63. @_onError new Error("Editor text does not match server text")
  64. , 0
  65. getSnapshot: () ->
  66. @doc?.getSnapshot()
  67. getType: () ->
  68. @doc?.getType()
  69. getInflightOp: () ->
  70. @doc?.getInflightOp()
  71. getPendingOp: () ->
  72. @doc?.getPendingOp()
  73. getRecentAck: () ->
  74. @doc?.getRecentAck()
  75. getOpSize: (op) ->
  76. @doc?.getOpSize(op)
  77. hasBufferedOps: () ->
  78. @doc?.hasBufferedOps()
  79. setTrackingChanges: (track_changes) ->
  80. @doc.track_changes = track_changes
  81. getTrackingChanges: () ->
  82. !!@doc.track_changes
  83. setTrackChangesIdSeeds: (id_seeds) ->
  84. @doc.track_changes_id_seeds = id_seeds
  85. _bindToSocketEvents: () ->
  86. @_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update)
  87. @ide.socket.on "otUpdateApplied", @_onUpdateAppliedHandler
  88. @_onErrorHandler = (error, update) => @_onError(error, update)
  89. @ide.socket.on "otUpdateError", @_onErrorHandler
  90. @_onDisconnectHandler = (error) => @_onDisconnect(error)
  91. @ide.socket.on "disconnect", @_onDisconnectHandler
  92. _bindToEditorEvents: () ->
  93. onReconnectHandler = (update) =>
  94. @_onReconnect(update)
  95. @_unsubscribeReconnectHandler = @ide.$scope.$on "project:joined", onReconnectHandler
  96. _unBindFromEditorEvents: () ->
  97. @_unsubscribeReconnectHandler()
  98. _unBindFromSocketEvents: () ->
  99. @ide.socket.removeListener "otUpdateApplied", @_onUpdateAppliedHandler
  100. @ide.socket.removeListener "otUpdateError", @_onErrorHandler
  101. @ide.socket.removeListener "disconnect", @_onDisconnectHandler
  102. leaveAndCleanUp: () ->
  103. @leave (error) =>
  104. @_cleanUp()
  105. join: (callback = (error) ->) ->
  106. @wantToBeJoined = true
  107. @_cancelLeave()
  108. if @connected
  109. return @_joinDoc callback
  110. else
  111. @_joinCallbacks ||= []
  112. @_joinCallbacks.push callback
  113. leave: (callback = (error) ->) ->
  114. @wantToBeJoined = false
  115. @_cancelJoin()
  116. if (@doc? and @doc.hasBufferedOps())
  117. sl_console.log "[leave] Doc has buffered ops, pushing callback for later"
  118. @_leaveCallbacks ||= []
  119. @_leaveCallbacks.push callback
  120. else if !@connected
  121. sl_console.log "[leave] Not connected, returning now"
  122. callback()
  123. else
  124. sl_console.log "[leave] Leaving now"
  125. @_leaveDoc(callback)
  126. flush: () ->
  127. @doc?.flushPendingOps()
  128. chaosMonkey: (line = 0, char = "a") ->
  129. orig = char
  130. copy = null
  131. pos = 0
  132. timer = () =>
  133. unless copy? and copy.length
  134. copy = orig.slice() + ' ' + new Date() + '\n'
  135. line += if Math.random() > 0.1 then 1 else -2
  136. line = 0 if line < 0
  137. pos = 0
  138. char = copy[0]
  139. copy = copy.slice(1)
  140. @ace.session.insert({row: line, column: pos}, char)
  141. pos += 1
  142. @_cm = setTimeout timer, 100 + if Math.random() < 0.1 then 1000 else 0
  143. @_cm = timer()
  144. clearChaosMonkey: () ->
  145. clearTimeout @_cm
  146. MAX_PENDING_OP_SIZE: 64 # pending ops bigger than this are always considered unsaved
  147. pollSavedStatus: () ->
  148. # returns false if doc has ops waiting to be acknowledged or
  149. # sent that haven't changed since the last time we checked.
  150. # Otherwise returns true.
  151. inflightOp = @getInflightOp()
  152. pendingOp = @getPendingOp()
  153. recentAck = @getRecentAck()
  154. pendingOpSize = pendingOp? && @getOpSize(pendingOp)
  155. if !inflightOp? and !pendingOp?
  156. # there's nothing going on, this is ok.
  157. saved = true
  158. sl_console.log "[pollSavedStatus] no inflight or pending ops"
  159. else if inflightOp? and inflightOp == @oldInflightOp
  160. # The same inflight op has been sitting unacked since we
  161. # last checked, this is bad.
  162. saved = false
  163. sl_console.log "[pollSavedStatus] inflight op is same as before"
  164. else if pendingOp? and recentAck && pendingOpSize < @MAX_PENDING_OP_SIZE
  165. # There is an op waiting to go to server but it is small and
  166. # within the flushDelay, this is ok for now.
  167. saved = true
  168. sl_console.log "[pollSavedStatus] pending op (small with recent ack) assume ok", pendingOp, pendingOpSize
  169. else
  170. # In any other situation, assume the document is unsaved.
  171. saved = false
  172. sl_console.log "[pollSavedStatus] assuming not saved (inflightOp?: #{inflightOp?}, pendingOp?: #{pendingOp?})"
  173. @oldInflightOp = inflightOp
  174. return saved
  175. _cancelLeave: () ->
  176. if @_leaveCallbacks?
  177. delete @_leaveCallbacks
  178. _cancelJoin: () ->
  179. if @_joinCallbacks?
  180. delete @_joinCallbacks
  181. _onUpdateApplied: (update) ->
  182. @ide.pushEvent "received-update",
  183. doc_id: @doc_id
  184. remote_doc_id: update?.doc
  185. wantToBeJoined: @wantToBeJoined
  186. update: update
  187. if window.disconnectOnAck? and Math.random() < window.disconnectOnAck
  188. sl_console.log "Disconnecting on ack", update
  189. window._ide.socket.socket.disconnect()
  190. # Pretend we never received the ack
  191. return
  192. if window.dropAcks? and Math.random() < window.dropAcks
  193. if !update.op? # Only drop our own acks, not collaborator updates
  194. sl_console.log "Simulating a lost ack", update
  195. return
  196. if update?.doc == @doc_id and @doc?
  197. @doc.processUpdateFromServer update
  198. if !@wantToBeJoined
  199. @leave()
  200. _onDisconnect: () ->
  201. sl_console.log '[onDisconnect] disconnecting'
  202. @connected = false
  203. @joined = false
  204. @doc?.updateConnectionState "disconnected"
  205. _onReconnect: () ->
  206. sl_console.log "[onReconnect] reconnected (joined project)"
  207. @ide.pushEvent "reconnected:afterJoinProject"
  208. @connected = true
  209. if @wantToBeJoined or @doc?.hasBufferedOps()
  210. sl_console.log "[onReconnect] Rejoining (wantToBeJoined: #{@wantToBeJoined} OR hasBufferedOps: #{@doc?.hasBufferedOps()})"
  211. @_joinDoc (error) =>
  212. return @_onError(error) if error?
  213. @doc.updateConnectionState "ok"
  214. @doc.flushPendingOps()
  215. @_callJoinCallbacks()
  216. _callJoinCallbacks: () ->
  217. for callback in @_joinCallbacks or []
  218. callback()
  219. delete @_joinCallbacks
  220. _joinDoc: (callback = (error) ->) ->
  221. if @doc?
  222. @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), { encodeRanges: true }, (error, docLines, version, updates, ranges) =>
  223. return callback(error) if error?
  224. @joined = true
  225. @doc.catchUp( updates )
  226. @_decodeRanges(ranges)
  227. @_catchUpRanges(ranges?.changes, ranges?.comments)
  228. callback()
  229. else
  230. @ide.socket.emit 'joinDoc', @doc_id, { encodeRanges: true }, (error, docLines, version, updates, ranges) =>
  231. return callback(error) if error?
  232. @joined = true
  233. @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
  234. @_decodeRanges(ranges)
  235. @ranges = new RangesTracker(ranges?.changes, ranges?.comments)
  236. @_bindToShareJsDocEvents()
  237. callback()
  238. _decodeRanges: (ranges) ->
  239. decodeFromWebsockets = (text) -> decodeURIComponent(escape(text))
  240. try
  241. for change in ranges.changes or []
  242. change.op.i = decodeFromWebsockets(change.op.i) if change.op.i?
  243. change.op.d = decodeFromWebsockets(change.op.d) if change.op.d?
  244. for comment in ranges.comments or []
  245. comment.op.c = decodeFromWebsockets(comment.op.c) if comment.op.c?
  246. catch err
  247. console.log(err)
  248. _leaveDoc: (callback = (error) ->) ->
  249. sl_console.log '[_leaveDoc] Sending leaveDoc request'
  250. @ide.socket.emit 'leaveDoc', @doc_id, (error) =>
  251. return callback(error) if error?
  252. @joined = false
  253. for callback in @_leaveCallbacks or []
  254. sl_console.log '[_leaveDoc] Calling buffered callback', callback
  255. callback(error)
  256. delete @_leaveCallbacks
  257. callback(error)
  258. _cleanUp: () ->
  259. if Document.openDocs[@doc_id] == @
  260. sl_console.log "[_cleanUp] Removing self (#{@doc_id}) from in openDocs"
  261. delete Document.openDocs[@doc_id]
  262. else
  263. # It's possible that this instance has error, and the doc has been reloaded.
  264. # This creates a new instance in Document.openDoc with the same id. We shouldn't
  265. # clear it because it's not this instance.
  266. sl_console.log "[_cleanUp] New instance of (#{@doc_id}) created. Not removing"
  267. @_unBindFromEditorEvents()
  268. @_unBindFromSocketEvents()
  269. _bindToShareJsDocEvents: () ->
  270. @doc.on "error", (error, meta) => @_onError error, meta
  271. @doc.on "externalUpdate", (update) =>
  272. @ide.pushEvent "externalUpdate",
  273. doc_id: @doc_id
  274. @trigger "externalUpdate", update
  275. @doc.on "remoteop", (args...) =>
  276. @ide.pushEvent "remoteop",
  277. doc_id: @doc_id
  278. @trigger "remoteop", args...
  279. @doc.on "op:sent", (op) =>
  280. @ide.pushEvent "op:sent",
  281. doc_id: @doc_id
  282. op: op
  283. @trigger "op:sent"
  284. @doc.on "op:acknowledged", (op) =>
  285. @ide.pushEvent "op:acknowledged",
  286. doc_id: @doc_id
  287. op: op
  288. @ide.$scope.$emit "ide:opAcknowledged",
  289. doc_id: @doc_id
  290. op: op
  291. @trigger "op:acknowledged"
  292. @doc.on "op:timeout", (op) =>
  293. @ide.pushEvent "op:timeout",
  294. doc_id: @doc_id
  295. op: op
  296. @trigger "op:timeout"
  297. @_onError new Error("op timed out"), {op: op}
  298. @doc.on "flush", (inflightOp, pendingOp, version) =>
  299. @ide.pushEvent "flush",
  300. doc_id: @doc_id,
  301. inflightOp: inflightOp,
  302. pendingOp: pendingOp
  303. v: version
  304. @doc.on "change", (ops, oldSnapshot, msg) =>
  305. @_applyOpsToRanges(ops, oldSnapshot, msg)
  306. @ide.$scope.$emit "doc:changed",
  307. doc_id: @doc_id
  308. @doc.on "flipped_pending_to_inflight", () =>
  309. @trigger "flipped_pending_to_inflight"
  310. @doc.on "saved", () =>
  311. @ide.$scope.$emit "doc:saved",
  312. doc_id: @doc_id
  313. _onError: (error, meta = {}) ->
  314. meta.doc_id = @doc_id
  315. sl_console.log "ShareJS error", error, meta
  316. ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{@ide.socket.socket.transport.name}" )
  317. @doc?.clearInflightAndPendingOps()
  318. @trigger "error", error, meta
  319. # The clean up should run after the error is triggered because the error triggers a
  320. # disconnect. If we run the clean up first, we remove our event handlers and miss
  321. # the disconnect event, which means we try to leaveDoc when the connection comes back.
  322. # This could intefere with the new connection of a new instance of this document.
  323. @_cleanUp()
  324. _applyOpsToRanges: (ops = [], oldSnapshot, msg) ->
  325. track_changes_as = null
  326. remote_op = msg?
  327. if msg?.meta?.tc?
  328. old_id_seed = @ranges.getIdSeed()
  329. @ranges.setIdSeed(msg.meta.tc)
  330. if remote_op and msg.meta?.tc
  331. track_changes_as = msg.meta.user_id
  332. else if !remote_op and @track_changes_as?
  333. track_changes_as = @track_changes_as
  334. @ranges.track_changes = track_changes_as?
  335. for op in ops
  336. @ranges.applyOp op, { user_id: track_changes_as }
  337. if old_id_seed?
  338. @ranges.setIdSeed(old_id_seed)
  339. if remote_op
  340. # With remote ops, Ace hasn't been updated when we receive this op,
  341. # so defer updating track changes until it has
  342. setTimeout () => @emit "ranges:dirty"
  343. else
  344. @emit "ranges:dirty"
  345. _catchUpRanges: (changes = [], comments = []) ->
  346. # We've just been given the current server's ranges, but need to apply any local ops we have.
  347. # Reset to the server state then apply our local ops again.
  348. @emit "ranges:clear"
  349. @ranges.changes = changes
  350. @ranges.comments = comments
  351. @ranges.track_changes = @doc.track_changes
  352. for op in @doc.getInflightOp() or []
  353. @ranges.setIdSeed(@doc.track_changes_id_seeds.inflight)
  354. @ranges.applyOp(op, { user_id: @track_changes_as })
  355. for op in @doc.getPendingOp() or []
  356. @ranges.setIdSeed(@doc.track_changes_id_seeds.pending)
  357. @ranges.applyOp(op, { user_id: @track_changes_as })
  358. @emit "ranges:redraw"