-
Notifications
You must be signed in to change notification settings - Fork 456
/
connection.coffee
167 lines (133 loc) · 4.81 KB
/
connection.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# 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