socketio.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. # This implements the socketio-based network API for ShareJS.
  2. #
  3. # This is the frontend used by the javascript socket implementation.
  4. #
  5. # See documentation for this protocol is in doc/protocol.md
  6. # Tests are in test/socketio.coffee
  7. #
  8. # This code will be removed in a future version of sharejs because socket.io is
  9. # too buggy.
  10. socketio = require 'socket.io'
  11. util = require 'util'
  12. hat = require 'hat'
  13. p = ->#util.debug
  14. i = ->#util.inspect
  15. # Attach the streaming protocol to the supplied http.Server.
  16. #
  17. # Options = {}
  18. exports.attach = (server, createClient, options) ->
  19. io = socketio.listen server
  20. io.configure ->
  21. io.set 'log level', 1
  22. for option of options
  23. io.set option, options[option]
  24. authClient = (handshakeData, callback) ->
  25. data =
  26. headers: handshakeData.headers
  27. remoteAddress: handshakeData.address.address
  28. secure: handshakeData.secure
  29. createClient data, (error, client) ->
  30. if error
  31. # Its important that we don't pass the error message to the client here - leaving it as null
  32. # will ensure the client recieves the normal 'not_authorized' message and thus
  33. # emits 'connect_failed' instead of 'error'
  34. callback null, false
  35. else
  36. handshakeData.client = client
  37. callback null, true
  38. io.of('/sjs').authorization(authClient).on 'connection', (socket) ->
  39. client = socket.handshake.client
  40. # There seems to be a bug in socket.io where socket.request isn't set sometimes.
  41. p "New socket connected from #{socket.request.socket.remoteAddress} with id #{socket.id}" if socket.request?
  42. lastSentDoc = null
  43. lastReceivedDoc = null
  44. # Map from docName -> {listener:fn, queue:[msg], busy:bool}
  45. docState = {}
  46. closed = false
  47. # Send a message to the socket.
  48. # msg _must_ have the doc:DOCNAME property set. We'll remove it if its the same as lastReceivedDoc.
  49. send = (msg) ->
  50. if msg.doc == lastSentDoc
  51. delete msg.doc
  52. else
  53. lastSentDoc = msg.doc
  54. p "Sending #{i msg}"
  55. socket.json.send msg
  56. # Open the given document name, at the requested version.
  57. # callback(error, version)
  58. open = (docName, version, callback) ->
  59. callback 'Doc already opened' if docState[docName].listener?
  60. p "Registering listener on #{docName} by #{socket.id} at #{version}"
  61. # This passes op events to the client
  62. docState[docName].listener = listener = (opData) ->
  63. throw new Error 'Consistency violation - doc listener invalid' unless docState[docName].listener == listener
  64. p "listener doc:#{docName} opdata:#{i opData} v:#{version}"
  65. # Skip the op if this socket sent it.
  66. return if opData.meta?.source == client.id
  67. opMsg =
  68. doc: docName
  69. op: opData.op
  70. v: opData.v
  71. meta: opData.meta
  72. send opMsg
  73. # Tell the socket the doc is open at the requested version
  74. client.listen docName, version, listener, (error, v) ->
  75. delete docState[docName].listener if error
  76. callback error, v
  77. # Close the named document.
  78. # callback([error])
  79. close = (docName, callback) ->
  80. p "Closing #{docName}"
  81. listener = docState[docName].listener
  82. return callback 'Doc already closed' unless listener?
  83. client.removeListener docName
  84. docState[docName].listener = null
  85. callback()
  86. # Handles messages with any combination of the open:true, create:true and snapshot:null parameters
  87. handleOpenCreateSnapshot = (query, finished) ->
  88. docName = query.doc
  89. msg = doc:docName
  90. callback = (error) ->
  91. if error
  92. close(docName) if msg.open == true
  93. msg.open = false if query.open == true
  94. msg.snapshot = null if query.snapshot != undefined
  95. delete msg.create
  96. msg.error = error
  97. send msg
  98. finished()
  99. return callback 'No docName specified' unless query.doc?
  100. if query.create == true
  101. if typeof query.type != 'string'
  102. return callback 'create:true requires type specified'
  103. if query.meta != undefined
  104. unless typeof query.meta == 'object' and Array.isArray(query.meta) == false
  105. return callback 'meta must be an object'
  106. docData = undefined
  107. # Technically, we don't need a snapshot if the user called create but not open or createSnapshot,
  108. # but no clients do that yet anyway.
  109. #
  110. # It might be nice to add a 'createOrGet()' method to model / db manager. But most
  111. # of the time clients are opening an existing document rather than creating a new one anyway.
  112. ###
  113. model.clientGetSnapshot client, query.doc, (error, data) ->
  114. maybeCreate = (callback) ->
  115. if query.create and error is 'Document does not exist'
  116. model.clientCreate client, docName, query.type, query.meta or {}, callback
  117. else
  118. callback error, data
  119. maybeCreate (error, data) ->
  120. if query.create
  121. msg.create = !!error
  122. if error is 'Document already exists'
  123. msg.create = false
  124. else if error and (!msg.create or error isnt 'Document already exists')
  125. # This is the real final callback, to say an error has occurred.
  126. return callback error
  127. else if query.create or query.snapshot is null
  128. if query.snapshot isnt null
  129. ###
  130. # This is implemented with a series of cascading methods for each different type of
  131. # thing this method can handle. This would be so much nicer with an async library. Welcome to
  132. # callback hell.
  133. step1Create = ->
  134. return step2Snapshot() if query.create != true
  135. # The document obviously already exists if we have a snapshot.
  136. if docData
  137. msg.create = false
  138. step2Snapshot()
  139. else
  140. client.create docName, query.type, query.meta || {}, (error) ->
  141. if error is 'Document already exists'
  142. # We've called getSnapshot (-> null), then createClient (-> already exists). Its possible
  143. # another client has called createClient first.
  144. client.getSnapshot docName, (error, data) ->
  145. return callback error if error
  146. docData = data
  147. msg.create = false
  148. step2Snapshot()
  149. else if error
  150. callback error
  151. else
  152. msg.create = !error
  153. step2Snapshot()
  154. # The socket requested a document snapshot
  155. step2Snapshot = ->
  156. # Skip inserting a snapshot if the document was just created.
  157. if query.snapshot != null or msg.create == true
  158. step3Open()
  159. return
  160. if docData
  161. msg.v = docData.v
  162. msg.type = docData.type.name unless query.type == docData.type.name
  163. msg.snapshot = docData.snapshot
  164. else
  165. return callback 'Document does not exist'
  166. step3Open()
  167. # Attempt to open a document with a given name. Version is optional.
  168. # callback(opened at version) or callback(null, errormessage)
  169. step3Open = ->
  170. return callback() if query.open != true
  171. # Verify the type matches
  172. return callback 'Type mismatch' if query.type and docData and query.type != docData.type.name
  173. open docName, query.v, (error, version) ->
  174. return callback error if error
  175. # + Should fail if the type is wrong.
  176. p "Opened #{docName} at #{version} by #{socket.id}"
  177. msg.open = true
  178. msg.v = version
  179. callback()
  180. if query.snapshot == null or (query.open == true and query.type)
  181. client.getSnapshot query.doc, (error, data) ->
  182. return callback error if error and error != 'Document does not exist'
  183. docData = data
  184. step1Create()
  185. else
  186. step1Create()
  187. # The socket closes a document
  188. handleClose = (query, callback) ->
  189. close query.doc, (error) ->
  190. if error
  191. # An error closing still results in the doc being closed.
  192. send {doc:query.doc, open:false, error:error}
  193. else
  194. send {doc:query.doc, open:false}
  195. callback()
  196. # We received an op from the socket
  197. handleOp = (query, callback) ->
  198. throw new Error 'No docName specified' unless query.doc?
  199. throw new Error 'No version specified' unless query.v? or (query.meta?.path? and query.meta?.value?)
  200. op_data = {v:query.v, op:query.op}
  201. op_data.meta = query.meta || {}
  202. op_data.meta.source = socket.id
  203. client.submitOp query.doc, op_data, (error, appliedVersion) ->
  204. msg = if error
  205. p "Sending error to socket: #{error}"
  206. {doc:query.doc, v:null, error:error}
  207. else
  208. {doc:query.doc, v:appliedVersion}
  209. p "sending #{i msg}"
  210. send msg
  211. callback()
  212. flush = (state) ->
  213. return if state.busy || state.queue.length == 0
  214. state.busy = true
  215. query = state.queue.shift()
  216. callback = ->
  217. state.busy = false
  218. flush state
  219. p "processing query #{i query}"
  220. try
  221. if query.open == false
  222. handleClose query, callback
  223. else if query.open != undefined or query.snapshot != undefined or query.create
  224. # You can open, request a snapshot and create all in the same
  225. # request. They're all handled together.
  226. handleOpenCreateSnapshot query, callback
  227. else if query.op? or query.meta? # The socket is applying an op.
  228. handleOp query, callback
  229. else
  230. util.debug "Unknown message received: #{util.inspect query}"
  231. catch error
  232. util.debug error.stack
  233. # ... And disconnect the socket?
  234. callback()
  235. # And now the actual message handler.
  236. messageListener = (query) ->
  237. p "Server recieved message #{i query}"
  238. # There seems to be a bug in socket.io where messages are detected
  239. # after the client disconnects.
  240. if closed
  241. console.warn "WARNING: received query from socket after the socket disconnected."
  242. console.warn socket
  243. return
  244. try
  245. query = JSON.parse query if typeof(query) == 'string'
  246. if query.doc == null
  247. lastReceivedDoc = null
  248. query.doc = hat()
  249. else if query.doc != undefined
  250. lastReceivedDoc = query.doc
  251. else
  252. throw new Error 'msg.doc missing. Probably the client reconnected without telling us - this is a socket.io bug.' unless lastReceivedDoc
  253. query.doc = lastReceivedDoc
  254. catch error
  255. util.debug error.stack
  256. return
  257. docState[query.doc] ||= {listener:null, queue:[], busy:no}
  258. docState[query.doc].queue.push query
  259. flush docState[query.doc]
  260. socket.on 'message', messageListener
  261. socket.on 'disconnect', ->
  262. p "socket #{socket.id} disconnected"
  263. closed = true
  264. for docName, state of docState
  265. state.busy = true
  266. state.queue = []
  267. client.removeListener docName if state.listener?
  268. socket.removeListener 'message', messageListener
  269. docState = {}
  270. server