doc.coffee 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. unless WEB?
  2. types = require '../types'
  3. if WEB?
  4. exports.extendDoc = (name, fn) ->
  5. Doc::[name] = fn
  6. # A Doc is a client's view on a sharejs document.
  7. #
  8. # Documents are created by calling Connection.open().
  9. #
  10. # Documents are event emitters - use doc.on(eventname, fn) to subscribe.
  11. #
  12. # Documents get mixed in with their type's API methods. So, you can .insert('foo', 0) into
  13. # a text document and stuff like that.
  14. #
  15. # Events:
  16. # - remoteop (op)
  17. # - changed (op)
  18. # - acknowledge (op)
  19. # - error
  20. # - open, closing, closed. 'closing' is not guaranteed to fire before closed.
  21. class Doc
  22. # connection is a Connection object.
  23. # name is the documents' docName.
  24. # data can optionally contain known document data, and initial open() call arguments:
  25. # {v[erson], snapshot={...}, type, create=true/false/undefined}
  26. # callback will be called once the document is first opened.
  27. constructor: (@connection, @name, openData) ->
  28. # Any of these can be null / undefined at this stage.
  29. openData ||= {}
  30. @version = openData.v
  31. @snapshot = openData.snaphot
  32. @_setType openData.type if openData.type
  33. @state = 'closed'
  34. @autoOpen = false
  35. # Has the document already been created?
  36. @_create = openData.create
  37. # The op that is currently roundtripping to the server, or null.
  38. #
  39. # When the connection reconnects, the inflight op is resubmitted.
  40. @inflightOp = null
  41. @inflightCallbacks = []
  42. # The auth ids which the client has previously used to attempt to send inflightOp. This is
  43. # usually empty.
  44. @inflightSubmittedIds = []
  45. # All ops that are waiting for the server to acknowledge @inflightOp
  46. @pendingOp = null
  47. @pendingCallbacks = []
  48. # Some recent ops, incase submitOp is called with an old op version number.
  49. @serverOps = {}
  50. # Transform a server op by a client op, and vice versa.
  51. _xf: (client, server) ->
  52. if @type.transformX
  53. @type.transformX(client, server)
  54. else
  55. client_ = @type.transform client, server, 'left'
  56. server_ = @type.transform server, client, 'right'
  57. return [client_, server_]
  58. _otApply: (docOp, isRemote) ->
  59. oldSnapshot = @snapshot
  60. @snapshot = @type.apply(@snapshot, docOp)
  61. # Its important that these event handlers are called with oldSnapshot.
  62. # The reason is that the OT type APIs might need to access the snapshots to
  63. # determine information about the received op.
  64. @emit 'change', docOp, oldSnapshot
  65. @emit 'remoteop', docOp, oldSnapshot if isRemote
  66. _connectionStateChanged: (state, data) ->
  67. switch state
  68. when 'disconnected'
  69. @state = 'closed'
  70. # This is used by the server to make sure that when an op is resubmitted it
  71. # doesn't end up getting applied twice.
  72. @inflightSubmittedIds.push @connection.id if @inflightOp
  73. @emit 'closed'
  74. when 'ok' # Might be able to do this when we're connecting... that would save a roundtrip.
  75. @open() if @autoOpen
  76. when 'stopped'
  77. @_openCallback? data
  78. @emit state, data
  79. _setType: (type) ->
  80. if typeof type is 'string'
  81. type = types[type]
  82. throw new Error 'Support for types without compose() is not implemented' unless type and type.compose
  83. @type = type
  84. if type.api
  85. this[k] = v for k, v of type.api
  86. @_register?()
  87. else
  88. @provides = {}
  89. _onMessage: (msg) ->
  90. #console.warn 's->c', msg
  91. if msg.open == true
  92. # The document has been successfully opened.
  93. @state = 'open'
  94. @_create = false # Don't try and create the document again next time open() is called.
  95. unless @created?
  96. @created = !!msg.create
  97. @_setType msg.type if msg.type
  98. if msg.create
  99. @created = true
  100. @snapshot = @type.create()
  101. else
  102. @created = false unless @created is true
  103. @snapshot = msg.snapshot if msg.snapshot isnt undefined
  104. @version = msg.v if msg.v?
  105. # Resend any previously queued operation.
  106. if @inflightOp
  107. response =
  108. doc: @name
  109. op: @inflightOp
  110. v: @version
  111. response.dupIfSource = @inflightSubmittedIds if @inflightSubmittedIds.length
  112. @connection.send response
  113. else
  114. @flush()
  115. @emit 'open'
  116. @_openCallback? null
  117. else if msg.open == false
  118. # The document has either been closed, or an open request has failed.
  119. if msg.error
  120. # An error occurred opening the document.
  121. console?.error "Could not open document: #{msg.error}"
  122. @emit 'error', msg.error
  123. @_openCallback? msg.error
  124. @state = 'closed'
  125. @emit 'closed'
  126. @_closeCallback?()
  127. @_closeCallback = null
  128. else if msg.op is null and error is 'Op already submitted'
  129. # We've tried to resend an op to the server, which has already been received successfully. Do nothing.
  130. # The op will be confirmed normally when we get the op itself was echoed back from the server
  131. # (handled below).
  132. else if (msg.op is undefined and msg.v isnt undefined) or (msg.op and msg.meta.source in @inflightSubmittedIds)
  133. # Our inflight op has been acknowledged.
  134. oldInflightOp = @inflightOp
  135. @inflightOp = null
  136. @inflightSubmittedIds.length = 0
  137. error = msg.error
  138. if error
  139. # The server has rejected an op from the client for some reason.
  140. # We'll send the error message to the user and roll back the change.
  141. #
  142. # If the server isn't going to allow edits anyway, we should probably
  143. # figure out some way to flag that (readonly:true in the open request?)
  144. if @type.invert
  145. undo = @type.invert oldInflightOp
  146. # Now we have to transform the undo operation by any server ops & pending ops
  147. if @pendingOp
  148. [@pendingOp, undo] = @_xf @pendingOp, undo
  149. # ... and apply it locally, reverting the changes.
  150. #
  151. # This call will also call @emit 'remoteop'. I'm still not 100% sure about this
  152. # functionality, because its really a local op. Basically, the problem is that
  153. # if the client's op is rejected by the server, the editor window should update
  154. # to reflect the undo.
  155. @_otApply undo, true
  156. else
  157. @emit 'error', "Op apply failed (#{error}) and the op could not be reverted"
  158. callback error for callback in @inflightCallbacks
  159. else
  160. # The op applied successfully.
  161. throw new Error('Invalid version from server') unless msg.v == @version
  162. @serverOps[@version] = oldInflightOp
  163. @version++
  164. @emit 'acknowledge', oldInflightOp
  165. callback null, oldInflightOp for callback in @inflightCallbacks
  166. # Send the next op.
  167. @flush()
  168. else if msg.op
  169. # We got a new op from the server.
  170. # msg is {doc:, op:, v:}
  171. # There is a bug in socket.io (produced on firefox 3.6) which causes messages
  172. # to be duplicated sometimes.
  173. # We'll just silently drop subsequent messages.
  174. return if msg.v < @version
  175. return @emit 'error', "Expected docName '#{@name}' but got #{msg.doc}" unless msg.doc == @name
  176. return @emit 'error', "Expected version #{@version} but got #{msg.v}" unless msg.v == @version
  177. # p "if: #{i @inflightOp} pending: #{i @pendingOp} doc '#{@snapshot}' op: #{i msg.op}"
  178. op = msg.op
  179. @serverOps[@version] = op
  180. docOp = op
  181. if @inflightOp != null
  182. [@inflightOp, docOp] = @_xf @inflightOp, docOp
  183. if @pendingOp != null
  184. [@pendingOp, docOp] = @_xf @pendingOp, docOp
  185. @version++
  186. # Finally, apply the op to @snapshot and trigger any event listeners
  187. @_otApply docOp, true
  188. else if msg.meta
  189. {path, value} = msg.meta
  190. switch path?[0]
  191. when 'shout'
  192. return @emit 'shout', value
  193. else
  194. console?.warn 'Unhandled meta op:', msg
  195. else
  196. console?.warn 'Unhandled document message:', msg
  197. # Send ops to the server, if appropriate.
  198. #
  199. # Only one op can be in-flight at a time, so if an op is already on its way then
  200. # this method does nothing.
  201. flush: =>
  202. return unless @connection.state == 'ok' and @inflightOp == null and @pendingOp != null
  203. # Rotate null -> pending -> inflight
  204. @inflightOp = @pendingOp
  205. @inflightCallbacks = @pendingCallbacks
  206. @pendingOp = null
  207. @pendingCallbacks = []
  208. @connection.send {doc:@name, op:@inflightOp, v:@version}
  209. # Submit an op to the server. The op maybe held for a little while before being sent, as only one
  210. # op can be inflight at any time.
  211. submitOp: (op, callback) ->
  212. op = @type.normalize(op) if @type.normalize?
  213. # If this throws an exception, no changes should have been made to the doc
  214. @snapshot = @type.apply @snapshot, op
  215. if @pendingOp != null
  216. @pendingOp = @type.compose(@pendingOp, op)
  217. else
  218. @pendingOp = op
  219. @pendingCallbacks.push callback if callback
  220. @emit 'change', op
  221. # A timeout is used so if the user sends multiple ops at the same time, they'll be composed
  222. # & sent together.
  223. setTimeout @flush, 0
  224. shout: (msg) =>
  225. # Meta ops don't have to queue, they can go direct. Good/bad idea?
  226. @connection.send {doc:@name, meta: { path: ['shout'], value: msg } }
  227. # Open a document. The document starts closed.
  228. open: (callback) ->
  229. @autoOpen = true
  230. return unless @state is 'closed'
  231. message =
  232. doc: @name
  233. open: true
  234. message.snapshot = null if @snapshot is undefined
  235. message.type = @type.name if @type
  236. message.v = @version if @version?
  237. message.create = true if @_create
  238. @connection.send message
  239. @state = 'opening'
  240. @_openCallback = (error) =>
  241. @_openCallback = null
  242. callback? error
  243. # Close a document.
  244. close: (callback) ->
  245. @autoOpen = false
  246. return callback?() if @state is 'closed'
  247. @connection.send {doc:@name, open:false}
  248. # Should this happen immediately or when we get open:false back from the server?
  249. @state = 'closed'
  250. @emit 'closing'
  251. @_closeCallback = callback
  252. # Make documents event emitters
  253. unless WEB?
  254. MicroEvent = require './microevent'
  255. MicroEvent.mixin Doc
  256. exports.Doc = Doc