overleaf/services/web/public/coffee/editor/sharejs/client/connection.coffee
2014-02-12 10:23:40 +00:00

167 lines
4.8 KiB
CoffeeScript

# A Connection wraps a persistant BC connection to a sharejs server.
#
# This class implements the client side of the protocol defined here:
# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
#
# The equivalent server code is in src/server/browserchannel.coffee.
#
# This file is a bit of a mess. I'm dreadfully sorry about that. It passes all the tests,
# so I have hope that its *correct* even if its not clean.
#
# Most of Connection exists to support the open() method, which creates a new document
# reference.
if WEB?
types = exports.types
throw new Error 'Must load browserchannel before this library' unless window.BCSocket
{BCSocket} = window
else
types = require '../types'
{BCSocket} = require 'browserchannel'
Doc = require('./doc').Doc
class Connection
constructor: (host) ->
# Map of docname -> doc
@docs = {}
# States:
# - 'connecting': The connection is being established
# - 'handshaking': The connection has been established, but we don't have the auth ID yet
# - 'ok': We have connected and recieved our client ID. Ready for data.
# - 'disconnected': The connection is closed, but it will not reconnect automatically.
# - 'stopped': The connection is closed, and will not reconnect.
@state = 'connecting'
@socket = new BCSocket host, reconnect:true
@socket.onmessage = (msg) =>
if msg.auth is null
# Auth failed.
@lastError = msg.error # 'forbidden'
@disconnect()
return @emit 'connect failed', msg.error
else if msg.auth
# Our very own client id.
@id = msg.auth
@setState 'ok'
return
docName = msg.doc
if docName isnt undefined
@lastReceivedDoc = docName
else
msg.doc = docName = @lastReceivedDoc
if @docs[docName]
@docs[docName]._onMessage msg
else
console?.error 'Unhandled message', msg
@connected = false
@socket.onclose = (reason) =>
#console.warn 'onclose', reason
@setState 'disconnected', reason
if reason in ['Closed', 'Stopped by server']
@setState 'stopped', @lastError or reason
@socket.onerror = (e) =>
#console.warn 'onerror', e
@emit 'error', e
@socket.onopen = =>
#console.warn 'onopen'
@lastError = @lastReceivedDoc = @lastSentDoc = null
@setState 'handshaking'
@socket.onconnecting = =>
#console.warn 'connecting'
@setState 'connecting'
setState: (state, data) ->
return if @state is state
@state = state
delete @id if state is 'disconnected'
@emit state, data
# Documents could just subscribe to the state change events, but there's less state to
# clean up when you close a document if I just notify the doucments directly.
for docName, doc of @docs
doc._connectionStateChanged state, data
send: (data) ->
docName = data.doc
if docName is @lastSentDoc
delete data.doc
else
@lastSentDoc = docName
#console.warn 'c->s', data
@socket.send data
disconnect: ->
# This will call @socket.onclose(), which in turn will emit the 'disconnected' event.
#console.warn 'calling close on the socket'
@socket.close()
# *** Doc management
makeDoc: (name, data, callback) ->
throw new Error("Doc #{name} already open") if @docs[name]
doc = new Doc(@, name, data)
@docs[name] = doc
doc.open (error) =>
delete @docs[name] if error
callback error, (doc unless error)
# Open a document that already exists
# callback(error, doc)
openExisting: (docName, callback) ->
return callback 'connection closed' if @state is 'stopped'
return callback null, @docs[docName] if @docs[docName]
doc = @makeDoc docName, {}, callback
# Open a document. It will be created if it doesn't already exist.
# Callback is passed a document or an error
# type is either a type name (eg 'text' or 'simple') or the actual type object.
# Types must be supported by the server.
# callback(error, doc)
open: (docName, type, callback) ->
return callback 'connection closed' if @state is 'stopped'
if typeof type is 'function'
callback = type
type = 'text'
callback ||= ->
type = types[type] if typeof type is 'string'
throw new Error "OT code for document type missing" unless type
throw new Error 'Server-generated random doc names are not currently supported' unless docName?
if @docs[docName]
doc = @docs[docName]
if doc.type == type
callback null, doc
else
callback 'Type mismatch', doc
return
@makeDoc docName, {create:true, type:type.name}, callback
# Not currently working.
# create: (type, callback) ->
# open null, type, callback
# Make connections event emitters.
unless WEB?
MicroEvent = require './microevent'
MicroEvent.mixin Connection
exports.Connection = Connection