browserchannel.coffee 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. # This implements the network API for ShareJS.
  2. #
  3. # The wire protocol is speccced out here:
  4. # https://github.com/josephg/ShareJS/wiki/Wire-Protocol
  5. #
  6. # When a client connects the server first authenticates it and sends:
  7. #
  8. # S: {auth:<agent session id>}
  9. # or
  10. # S: {auth:null, error:'forbidden'}
  11. #
  12. # After that, the client can open documents:
  13. #
  14. # C: {doc:'foo', open:true, snapshot:null, create:true, type:'text'}
  15. # S: {doc:'foo', open:true, snapshot:{snapshot:'hi there', v:5, meta:{}}, create:false}
  16. #
  17. # ...
  18. #
  19. # The client can send open requests as soon as the socket has opened - it doesn't need to
  20. # wait for auth.
  21. #
  22. # The wire protocol is documented here:
  23. # https://github.com/josephg/ShareJS/wiki/Wire-Protocol
  24. browserChannel = require('browserchannel').server
  25. util = require 'util'
  26. hat = require 'hat'
  27. syncQueue = require './syncqueue'
  28. # Attach the streaming protocol to the supplied http.Server.
  29. #
  30. # Options = {}
  31. module.exports = (createAgent, options) ->
  32. options or= {}
  33. browserChannel options, (session) ->
  34. #console.log "New BC session from #{session.address} with id #{session.id}"
  35. data =
  36. headers: session.headers
  37. remoteAddress: session.address
  38. # This is the user agent through which a connecting client acts. It is set when the
  39. # session is authenticated. The agent is responsible for making sure client requests are
  40. # properly authorized, and metadata is kept up to date.
  41. agent = null
  42. # To save on network traffic, the agent & server can leave out the docName with each message to mean
  43. # 'same as the last message'
  44. lastSentDoc = null
  45. lastReceivedDoc = null
  46. # Map from docName -> {queue, listener if open}
  47. docState = {}
  48. # We'll only handle one message from each client at a time.
  49. handleMessage = (query) ->
  50. #console.log "Message from #{session.id}", query
  51. error = null
  52. error = 'Invalid docName' unless query.doc is null or typeof query.doc is 'string' or (query.doc is undefined and lastReceivedDoc)
  53. error = "'create' must be true or missing" unless query.create in [true, undefined]
  54. error = "'open' must be true, false or missing" unless query.open in [true, false, undefined]
  55. error = "'snapshot' must be null or missing" unless query.snapshot in [null, undefined]
  56. error = "'type' invalid" unless query.type is undefined or typeof query.type is 'string'
  57. error = "'v' invalid" unless query.v is undefined or (typeof query.v is 'number' and query.v >= 0)
  58. if error
  59. console.warn "Invalid query #{JSON.stringify query} from #{agent.sessionId}: #{error}"
  60. session.abort()
  61. return callback()
  62. # The agent can specify null as the docName to get a random doc name.
  63. if query.doc is null
  64. query.doc = lastReceivedDoc = hat()
  65. else if query.doc != undefined
  66. lastReceivedDoc = query.doc
  67. else
  68. unless lastReceivedDoc
  69. console.warn "msg.doc missing in query #{JSON.stringify query} from #{agent.sessionId}"
  70. # The disconnect handler will be called when we do this, which will clean up the open docs.
  71. return session.abort()
  72. query.doc = lastReceivedDoc
  73. docState[query.doc] or= queue: syncQueue (query, callback) ->
  74. # When the session is closed, we'll nuke docState. When that happens, no more messages
  75. # should be handled.
  76. return callback() unless docState
  77. # Close messages are {open:false}
  78. if query.open == false
  79. handleClose query, callback
  80. # Open messages are {open:true}. There's a lot of shared logic with getting snapshots
  81. # and creating documents. These operations can be done together; and I'll handle them
  82. # together.
  83. else if query.open or query.snapshot is null or query.create
  84. # You can open, request a snapshot and create all in the same
  85. # request. They're all handled together.
  86. handleOpenCreateSnapshot query, callback
  87. # The socket is submitting an op.
  88. else if query.op? or query.meta?.path?
  89. handleOp query, callback
  90. else
  91. console.warn "Invalid query #{JSON.stringify query} from #{agent.sessionId}"
  92. session.abort()
  93. callback()
  94. # ... And add the message to the queue.
  95. docState[query.doc].queue query
  96. # # Some utility methods for message handlers
  97. # Send a message to the socket.
  98. # msg _must_ have the doc:DOCNAME property set. We'll remove it if its the same as lastReceivedDoc.
  99. send = (response) ->
  100. if response.doc is lastSentDoc
  101. delete response.doc
  102. else
  103. lastSentDoc = response.doc
  104. # Its invalid to send a message to a closed session. We'll silently drop messages if the
  105. # session has closed.
  106. if session.state isnt 'closed'
  107. #console.log "Sending", response
  108. session.send response
  109. # Open the given document name, at the requested version.
  110. # callback(error, version)
  111. open = (docName, version, callback) ->
  112. return callback 'Session closed' unless docState
  113. return callback 'Document already open' if docState[docName].listener
  114. #p "Registering listener on #{docName} by #{socket.id} at #{version}"
  115. docState[docName].listener = listener = (opData) ->
  116. throw new Error 'Consistency violation - doc listener invalid' unless docState[docName].listener == listener
  117. #p "listener doc:#{docName} opdata:#{i opData} v:#{version}"
  118. # Skip the op if this socket sent it.
  119. return if opData.meta.source is agent.sessionId
  120. opMsg =
  121. doc: docName
  122. op: opData.op
  123. v: opData.v
  124. meta: opData.meta
  125. send opMsg
  126. # Tell the socket the doc is open at the requested version
  127. agent.listen docName, version, listener, (error, v) ->
  128. delete docState[docName].listener if error
  129. callback error, v
  130. # Close the named document.
  131. # callback([error])
  132. close = (docName, callback) ->
  133. #p "Closing #{docName}"
  134. return callback 'Session closed' unless docState
  135. listener = docState[docName].listener
  136. return callback 'Doc already closed' unless listener?
  137. agent.removeListener docName
  138. delete docState[docName].listener
  139. callback()
  140. # Handles messages with any combination of the open:true, create:true and snapshot:null parameters
  141. handleOpenCreateSnapshot = (query, finished) ->
  142. docName = query.doc
  143. msg = doc:docName
  144. callback = (error) ->
  145. if error
  146. close(docName) if msg.open == true
  147. msg.open = false if query.open == true
  148. msg.snapshot = null if query.snapshot != undefined
  149. delete msg.create
  150. msg.error = error
  151. send msg
  152. finished()
  153. return callback 'No docName specified' unless query.doc?
  154. if query.create == true
  155. if typeof query.type != 'string'
  156. return callback 'create:true requires type specified'
  157. if query.meta != undefined
  158. unless typeof query.meta == 'object' and Array.isArray(query.meta) == false
  159. return callback 'meta must be an object'
  160. docData = undefined
  161. # This is implemented with a series of cascading methods for each different type of
  162. # thing this method can handle. This would be so much nicer with an async library. Welcome to
  163. # callback hell.
  164. step1Create = ->
  165. return step2Snapshot() if query.create != true
  166. # The document obviously already exists if we have a snapshot.
  167. if docData
  168. msg.create = false
  169. step2Snapshot()
  170. else
  171. agent.create docName, query.type, query.meta || {}, (error) ->
  172. if error is 'Document already exists'
  173. # We've called getSnapshot (-> null), then create (-> already exists). Its possible
  174. # another agent has called create() between our getSnapshot and create() calls.
  175. agent.getSnapshot docName, (error, data) ->
  176. return callback error if error
  177. docData = data
  178. msg.create = false
  179. step2Snapshot()
  180. else if error
  181. callback error
  182. else
  183. msg.create = true
  184. step2Snapshot()
  185. # The socket requested a document snapshot
  186. step2Snapshot = ->
  187. # if query.create or query.open or query.snapshot == null
  188. # msg.meta = docData.meta
  189. # Skip inserting a snapshot if the document was just created.
  190. if query.snapshot != null or msg.create == true
  191. step3Open()
  192. return
  193. if docData
  194. msg.v = docData.v
  195. msg.type = docData.type.name unless query.type == docData.type.name
  196. msg.snapshot = docData.snapshot
  197. else
  198. return callback 'Document does not exist'
  199. step3Open()
  200. # Attempt to open a document with a given name. Version is optional.
  201. # callback(opened at version) or callback(null, errormessage)
  202. step3Open = ->
  203. return callback() if query.open != true
  204. # Verify the type matches
  205. return callback 'Type mismatch' if query.type and docData and query.type != docData.type.name
  206. open docName, query.v, (error, version) ->
  207. return callback error if error
  208. # + Should fail if the type is wrong.
  209. #p "Opened #{docName} at #{version} by #{socket.id}"
  210. msg.open = true
  211. msg.v = version
  212. callback()
  213. # Technically, we don't need a snapshot if the user called create but not open or createSnapshot,
  214. # but no clients do that yet anyway.
  215. if query.snapshot == null or query.open == true #and query.type
  216. agent.getSnapshot query.doc, (error, data) ->
  217. return callback error if error and error != 'Document does not exist'
  218. docData = data
  219. step1Create()
  220. else
  221. step1Create()
  222. # The socket closes a document
  223. handleClose = (query, callback) ->
  224. close query.doc, (error) ->
  225. if error
  226. # An error closing still results in the doc being closed.
  227. send {doc:query.doc, open:false, error:error}
  228. else
  229. send {doc:query.doc, open:false}
  230. callback()
  231. # We received an op from the socket
  232. handleOp = (query, callback) ->
  233. # ...
  234. #throw new Error 'No version specified' unless query.v?
  235. opData = {v:query.v, op:query.op, meta:query.meta, dupIfSource:query.dupIfSource}
  236. # If it's a metaOp don't send a response
  237. agent.submitOp query.doc, opData, if (not opData.op? and opData.meta?.path?) then callback else (error, appliedVersion) ->
  238. msg = if error
  239. #p "Sending error to socket: #{error}"
  240. {doc:query.doc, v:null, error:error}
  241. else
  242. {doc:query.doc, v:appliedVersion}
  243. send msg
  244. callback()
  245. # We don't process any messages from the agent until they've authorized. Instead,
  246. # they are stored in this buffer.
  247. buffer = []
  248. session.on 'message', bufferMsg = (msg) -> buffer.push msg
  249. createAgent data, (error, agent_) ->
  250. if error
  251. # The client is not authorized, so they shouldn't try and reconnect.
  252. session.send {auth:null, error}
  253. session.stop()
  254. else
  255. agent = agent_
  256. session.send auth:agent.sessionId
  257. # Ok. Now we can handle all the messages in the buffer. They'll go straight to
  258. # handleMessage from now on.
  259. session.removeListener 'message', bufferMsg
  260. handleMessage msg for msg in buffer
  261. buffer = null
  262. session.on 'message', handleMessage
  263. session.on 'close', ->
  264. return unless agent
  265. #console.log "Client #{agent.sessionId} disconnected"
  266. for docName, {listener} of docState
  267. agent.removeListener docName if listener
  268. docState = null