Skip to content
This repository
Browse code

Rewrote the entire client to use browserchannel.

  • Loading branch information...
commit 692865cd25b2d51d6ceb9d2c76424f2a86a2bdc3 1 parent 4279098
Joseph Gentle authored
4  examples/_wiki/wiki.html.mu
@@ -16,7 +16,7 @@
16 16
     <div id="editor">{{{content}}}</div>
17 17
     <script src="/lib/markdown/showdown.js" type="text/javascript"></script>
18 18
     <script src="/lib/ace/ace.js" type="text/javascript" charset="utf-8"></script>
19  
-    <script src="/socket.io/socket.io.js"></script>
  19
+    <script src="/channel/bcsocket.js"></script>
20 20
     <script src="/share/share.js"></script>
21 21
     <script src="/share/ace.js"></script>
22 22
     <script>
@@ -33,7 +33,7 @@ window.onload = function() {
33 33
   // sharejs.open('{{{docName}}}', function(doc, error) {
34 34
   //   ...
35 35
 
36  
-  var connection = new sharejs.Connection('http://' + window.location.hostname + ':' + 8000 + '/sjs');
  36
+  var connection = new sharejs.Connection('http://' + window.location.hostname + ':' + 8000 + '/channel');
37 37
 
38 38
   connection.open('{{{docName}}}', function(error, doc) {
39 39
     if (error) {
2  examples/ace/index.html
@@ -14,7 +14,7 @@
14 14
 		<div id="editor">Connecting...</div>
15 15
 		<script src="/lib/ace/ace.js" type="text/javascript" charset="utf-8"></script>
16 16
 		<script src="/lib/ace/mode-coffee.js" type="text/javascript" charset="utf-8"></script>
17  
-		<script src="/socket.io/socket.io.js"></script>
  17
+    <script src="/channel/bcsocket.js"></script>
18 18
 		<script src="/share/share.js"></script>
19 19
 		<script src="/share/ace.js"></script>
20 20
 		<script>
2  examples/clobber-ace.html
@@ -15,7 +15,7 @@
15 15
 		<div id="editor">Some content. The document will be replaced with this contents when its opened.</div>
16 16
 
17 17
 		<script src="/lib/ace/ace.js" type="text/javascript" charset="utf-8"></script>
18  
-		<script src="/socket.io/socket.io.js"></script>
  18
+    <script src="/channel/bcsocket.js"></script>
19 19
 		<script src="/share/share.js"></script>
20 20
 		<script src="/share/ace.js"></script>
21 21
 
2  examples/code.html
@@ -17,7 +17,7 @@
17 17
     <script src="/lib/ace/ace.js"></script>
18 18
     <script src="/lib/ace/mode-coffee.js"></script>
19 19
     <script src="/lib/ace/theme-idle_fingers.js"></script>
20  
-    <script src="/socket.io/socket.io.js"></script>
  20
+    <script src="/channel/bcsocket.js"></script>
21 21
     <script src="/share/share.js"></script>
22 22
     <script src="/share/ace.js"></script>
23 23
     <script>
2  examples/demos.html
@@ -120,7 +120,7 @@
120 120
           <p>When you open this page, it creates a new empty editing pad with a random name (pad:XXXX).
121 121
           You can share the URL with someone else and edit with them.
122 122
           </p>
123  
-          <p><a class="btn primary" href="static/html">Edit a new pad &raquo;</a></p>
  123
+          <p><a class="btn primary" href="pad/">Edit a new pad &raquo;</a></p>
124 124
         </div>
125 125
         
126 126
         <div class="span6 demo">
2  examples/hello-ace.html
@@ -15,7 +15,7 @@
15 15
 		<div id="editor"></div>
16 16
 
17 17
 		<script src="/lib/ace/ace.js" type="text/javascript" charset="utf-8"></script>
18  
-		<script src="/socket.io/socket.io.js"></script>
  18
+    <script src="/channel/bcsocket.js"></script>
19 19
 		<script src="/share/share.js"></script>
20 20
 		<script src="/share/ace.js"></script>
21 21
 
2  examples/hello-node.js
@@ -3,7 +3,7 @@
3 3
 
4 4
 var client = require('..').client;
5 5
 
6  
-client.open('hello', 'text', 'http://localhost:8000/sjs', function(error, doc) {
  6
+client.open('hello', 'text', 'http://localhost:8000/channel', function(error, doc) {
7 7
 	doc.insert('Hi there\n', 0);
8 8
 	
9 9
 	console.log(doc.snapshot);
2  examples/hello-readonly.html
@@ -10,7 +10,7 @@
10 10
 			<div id='text' class='content'></div>
11 11
 		</div>
12 12
 
13  
-		<script src="/socket.io/socket.io.js"></script>
  13
+    <script src="/channel/bcsocket.js"></script>
14 14
 		<script src="/share/share.js"></script>
15 15
 
16 16
 		<script>
2  examples/hello-stream.js
@@ -3,7 +3,7 @@
3 3
 
4 4
 var client = require('..').client;
5 5
 
6  
-client.open('hello', 'text', 'http://localhost:8000/sjs', function(error, doc) {
  6
+client.open('hello', 'text', 'http://localhost:8000/channel', function(error, doc) {
7 7
 	if (error) {
8 8
 		throw error;
9 9
 	}
2  examples/hello-tp2.html
@@ -15,7 +15,7 @@
15 15
 		<div id="editor"></div>
16 16
 
17 17
 		<script src="/lib/ace/ace.js" type="text/javascript" charset="utf-8"></script>
18  
-		<script src="/socket.io/socket.io.js"></script>
  18
+    <script src="/channel/bcsocket.js"></script>
19 19
 		<script src="/share/share.js"></script>
20 20
 		<script src="/share/text-tp2.uncompressed.js"></script>
21 21
 		<script src="/share/ace.js"></script>
2  examples/hex.html
@@ -58,7 +58,7 @@
58 58
 			}
59 59
 		</style>
60 60
 		<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
61  
-		<script src="/socket.io/socket.io.js"></script>
  61
+    <script src="/channel/bcsocket.js"></script>
62 62
 		<script src="/share/share.js"></script>
63 63
 		<script src="/share/json.js"></script>
64 64
 	</head>
3  examples/index.html
@@ -191,7 +191,8 @@
191 191
 
192 192
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
193 193
 <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>
194  
-<script src="/socket.io/socket.io.js"></script>
  194
+<!--<script src="/socket.io/socket.io.js"></script>-->
  195
+<script src="/channel/bcsocket.js"></script>
195 196
 <script src="/share/share.js"></script>
196 197
 <script src="/share/textarea.js"></script>
197 198
 <script src="/lib/prettify.js"></script>
2  examples/pad/pad.html
@@ -20,7 +20,7 @@
20 20
 	<body>
21 21
 		<div id="editor">Connecting...</div>
22 22
 		<script src="/lib/ace/ace.js" type="text/javascript" charset="utf-8"></script>
23  
-		<script src="/socket.io/socket.io.js"></script>
  23
+    <script src="/channel/bcsocket.js"></script>
24 24
 		<script src="/share/share.js"></script>
25 25
 		<script src="/share/ace.js"></script>
26 26
 		<script>
2  examples/readonly/html.html
@@ -6,7 +6,7 @@
6 6
 		<div id="container">
7 7
 			<div id='text' class='content'></div>
8 8
 		</div>
9  
-		<script src="/socket.io/socket.io.js" type="text/javascript"></script>
  9
+    <script src="/channel/bcsocket.js"></script>
10 10
 		<script src="/share/share.js" type="text/javascript"></script>
11 11
 		<script type="text/javascript">
12 12
 
2  examples/readonly/markdown.html
@@ -7,7 +7,7 @@
7 7
 		<div id="container">
8 8
 			<div id='text' class='content'></div>
9 9
 		</div>
10  
-		<script src="/socket.io/socket.io.js" type="text/javascript"></script>
  10
+    <script src="/channel/bcsocket.js"></script>
11 11
 		<script src="../lib/markdown/showdown.js" type="text/javascript"></script>
12 12
 		<script src="/share/share.js" type="text/javascript"></script>
13 13
 		<script type="text/javascript">
2  examples/sharefile.coffee
@@ -8,7 +8,7 @@ fs = require('fs')
8 8
 argv = require('optimist')
9 9
 	.usage('Usage: $0 -d docname [--url URL] [-f filename]')
10 10
 	.default('d', 'hello')
11  
-	.default('url', 'http://localhost:8000/sjs')
  11
+	.default('url', 'http://localhost:8000/channel')
12 12
 	.argv
13 13
 
14 14
 filename = argv.f || argv.d
2  examples/textarea.html
@@ -18,7 +18,7 @@
18 18
 		</div>
19 19
 		<textarea id="editor" disabled>Loading...</textarea>
20 20
 
21  
-		<script src="/socket.io/socket.io.js"></script>
  21
+    <script src="/channel/bcsocket.js"></script>
22 22
 		<script src="/share/share.js"></script>
23 23
 		<script src="/share/textarea.js"></script>
24 24
 		<script>
242  src/client/connection.coffee
... ...
@@ -1,9 +1,9 @@
1  
-# A Connection manages a socket.io connection to a sharejs server.
  1
+# A Connection wraps a persistant BC connection to a sharejs server.
2 2
 #
3 3
 # This class implements the client side of the protocol defined here:
4 4
 # https://github.com/josephg/ShareJS/wiki/Wire-Protocol
5 5
 #
6  
-# The equivalent server code is in src/server/socketio.coffee.
  6
+# The equivalent server code is in src/server/browserchannel.coffee.
7 7
 #
8 8
 # This file is a bit of a mess. I'm dreadfully sorry about that. It passes all the tests,
9 9
 # so I have hope that its *correct* even if its not clean.
@@ -13,148 +13,111 @@
13 13
 
14 14
 if WEB?
15 15
   types ||= exports.types
16  
-  throw new Error 'Must load socket.io before this library' unless window.io
17  
-  io = window.io
  16
+  throw new Error 'Must load browserchannel before this library' unless window.BCSocket
  17
+  {BCSocket} = window
18 18
 else
19 19
   types = require '../types'
20  
-  io = require 'socket.io-client'
  20
+  {BCSocket} = require 'browserchannel'
21 21
   Doc = require('./doc').Doc
22 22
 
23 23
 class Connection
24  
-  constructor: (origin) ->
  24
+  constructor: (host) ->
  25
+    # Map of docname -> doc
25 26
     @docs = {}
26 27
 
27  
-    # Map of docName -> map of type -> function(data, error)
28  
-    #
29  
-    # Once socket.io isn't buggy, this will be rewritten to use socket.io's RPC.
30  
-    @handlers = {}
31  
-
  28
+    # States:
  29
+    # - 'connecting': The connection is being established
  30
+    # - 'handshaking': The connection has been established, but we don't have the auth ID yet
  31
+    # - 'ok': We have connected and recieved our client ID. Ready for data.
  32
+    # - 'disconnected': The connection is closed, but it will not reconnect automatically.
  33
+    # - 'stopped': The connection is closed, and will not reconnect.
32 34
     @state = 'connecting'
  35
+    @socket = new BCSocket host, reconnect:true
  36
+
  37
+    @socket.onmessage = (msg) =>
  38
+      if msg.auth is null
  39
+        # Auth failed.
  40
+        @lastError = msg.error # 'forbidden'
  41
+        @disconnect()
  42
+        return @emit 'connect failed', msg.error
  43
+      else if msg.auth
  44
+        # Our very own client id.
  45
+        @id = msg.auth
  46
+        @setState 'ok'
  47
+        return
  48
+
  49
+      docName = msg.doc
  50
+
  51
+      if docName isnt undefined
  52
+        @lastReceivedDoc = docName
  53
+      else
  54
+        msg.doc = docName = @lastReceivedDoc
33 55
 
34  
-    # We can't reuse connections because the socket.io server doesn't
35  
-    # emit connected events when a new connection comes in. Multiple documents
36  
-    # are already multiplexed over the connection by socket.io anyway, so it
37  
-    # shouldn't matter too much unless you're doing something particularly wacky.
38  
-    @socket = io.connect origin, 'force new connection': true
39  
-
40  
-    @socket.on 'connect', @connected
41  
-    @socket.on 'disconnect', @disconnected
42  
-    @socket.on 'message', @onMessage
43  
-    @socket.on 'connect_failed', (error) =>
44  
-      error = 'forbidden' if error == 'unauthorized' # For consistency with the server
45  
-      @socket = null
46  
-      @emit 'connect failed', error
47  
-      # Cancel all hanging messages
48  
-      for docName, h of @handlers
49  
-        for t, callbacks of h
50  
-          callback error for callback in callbacks
51  
-
52  
-    # This avoids a bug in socket.io-client (v0.7.9) which causes
53  
-    # subsequent connections on the same host to not fire a .connect event
54  
-    #if @socket.socket.connected
55  
-    #  setTimeout (=> @connected()), 0
56  
-
57  
-  disconnected: =>
58  
-    # Start reconnect sequence
59  
-    @emit 'disconnect'
60  
-    @socket = null
61  
-
62  
-  connected: =>
63  
-    # Stop reconnect sequence
64  
-    @emit 'connect'
65  
-
66  
-  # Send the specified message to the server. The server's response will be passed
67  
-  # to callback. If the message is 'open', ops will be sent to follower()
68  
-  #
69  
-  # The callback is optional. It takes (error, data). Data might be missing if the
70  
-  # error was a connection error.
71  
-  send: (msg, callback) ->
72  
-    throw new Error "Cannot send message #{JSON.stringify msg} to a closed connection" if @socket == null
73  
-
74  
-    docName = msg.doc
75  
-
76  
-    if docName == @lastSentDoc
77  
-      delete msg.doc
78  
-    else
79  
-      @lastSentDoc = docName
  56
+      if @docs[docName]
  57
+        @docs[docName]._onMessage msg
  58
+      else
  59
+        console?.error 'Unhandled message', msg
80 60
 
81  
-    @socket.json.send msg
82  
-    
83  
-    if callback
84  
-      type = if msg.open == true then 'open'
85  
-      else if msg.open == false then 'close'
86  
-      else if msg.create then 'create'
87  
-      else if msg.snapshot == null then 'snapshot'
88  
-      else if msg.op then 'op response'
89  
-
90  
-      #cb = (response) =>
91  
-        #  if response.doc == docName
92  
-        #  @removeListener type, cb
93  
-        #  callback response, response.error
94  
-
95  
-      docHandlers = (@handlers[docName] ||= {})
96  
-      callbacks = (docHandlers[type] ||= [])
97  
-      callbacks.push callback
98  
-
99  
-  onMessage: (msg) =>
100  
-    docName = msg.doc
101  
-
102  
-    if docName != undefined
103  
-      @lastReceivedDoc = docName
104  
-    else
105  
-      msg.doc = docName = @lastReceivedDoc
  61
+    @connected = false
  62
+    @socket.onclose = (reason) =>
  63
+      @setState 'disconnected', reason
  64
+      if reason in ['Closed', 'Stopped by server']
  65
+        @setState 'stopped', @lastError or reason
106 66
 
107  
-    @emit 'message', msg
  67
+    @socket.onerror = (e) =>
  68
+      @emit 'error', e
108 69
 
109  
-    # This should probably be rewritten to use socketio's message response stuff instead.
110  
-    # (it was originally written for socket.io 0.6)
111  
-    type = if msg.open == true or (msg.open == false and msg.error) then 'open'
112  
-    else if msg.open == false then 'close'
113  
-    else if msg.snapshot != undefined then 'snapshot'
114  
-    else if msg.create then 'create'
115  
-    else if msg.op then 'op'
116  
-    else if msg.v != undefined then 'op response'
  70
+    @socket.onopen = =>
  71
+      @lastError = null
  72
+      @setState 'handshaking'
117 73
 
118  
-    callbacks = @handlers[docName]?[type]
119  
-    if callbacks
120  
-      delete @handlers[docName][type]
121  
-      c msg.error, msg for c in callbacks
  74
+    @socket.onconnecting = =>
  75
+      @setState 'connecting'
122 76
 
123  
-    if type == 'op'
124  
-      doc = @docs[docName]
125  
-      doc._onOpReceived msg if doc
  77
+  setState: (state, data) ->
  78
+    return if @state is state
  79
+    @state = state
126 80
 
127  
-  makeDoc: (params) ->
128  
-    name = params.doc
129  
-    throw new Error("Doc #{name} already open") if @docs[name]
  81
+    delete @id if state is 'disconnected'
  82
+    @emit state, data
130 83
 
131  
-    type = params.type
132  
-    type = types[type] if typeof type == 'string'
133  
-    doc = new Doc(@, name, params.v, type, params.snapshot)
134  
-    doc.created = !!params.create
135  
-    @docs[name] = doc
  84
+    # Documents could just subscribe to the state change events, but there's less state to
  85
+    # clean up when you close a document if I just notify the doucments directly.
  86
+    for docName, doc of @docs
  87
+      doc._connectionStateChanged state, data
  88
+
  89
+  send: (data) ->
  90
+    docName = data.doc
  91
+
  92
+    if docName is @lastSentDoc
  93
+      delete data.doc
  94
+    else
  95
+      @lastSentDoc = docName
136 96
 
137  
-    doc.on 'closing', =>
138  
-      delete @docs[name]
  97
+    #console.warn 'c->s', data
  98
+    @socket.send data
139 99
 
140  
-    doc
  100
+  disconnect: ->
  101
+    # This will call @socket.onclose(), which in turn will emit the 'disconnected' event.
  102
+    @socket.close()
  103
+
  104
+  # *** Doc management
  105
+ 
  106
+  makeDoc: (name, data, callback) ->
  107
+    throw new Error("Doc #{name} already open") if @docs[name]
  108
+    doc = new Doc(@, name, data)
  109
+    @docs[name] = doc
  110
+
  111
+    doc.open (error) =>
  112
+      delete @docs[name] if error
  113
+      callback error, (doc unless error)
141 114
 
142 115
   # Open a document that already exists
143 116
   # callback(error, doc)
144 117
   openExisting: (docName, callback) ->
145  
-    if @socket == null # The connection is perminantly disconnected
146  
-      callback 'connection closed'
147  
-      return
148  
-
149  
-    return @docs[docName] if @docs[docName]?
150  
-
151  
-    @send {doc:docName, open:true, snapshot:null}, (error, response) =>
152  
-      if error
153  
-        callback error
154  
-      else
155  
-        # response.doc is used instead of docName to allow docName to be null.
156  
-        # In that case, the server generates a random docName to use.
157  
-        callback null, @makeDoc(response)
  118
+    return callback 'connection closed' if @state is 'stopped'
  119
+    return callback null, @docs[docName] if @docs[docName]
  120
+    doc = @makeDoc docName, {}, callback
158 121
 
159 122
   # Open a document. It will be created if it doesn't already exist.
160 123
   # Callback is passed a document or an error
@@ -162,46 +125,33 @@ class Connection
162 125
   # Types must be supported by the server.
163 126
   # callback(error, doc)
164 127
   open: (docName, type, callback) ->
165  
-    if @socket == null # The connection is perminantly disconnected
166  
-      callback 'connection closed'
167  
-      return
  128
+    return callback 'connection closed' if @state is 'stopped'
168 129
 
169  
-    if typeof type == 'function'
  130
+    if typeof type is 'function'
170 131
       callback = type
171 132
       type = 'text'
172 133
 
173 134
     callback ||= ->
174 135
 
175  
-    type = types[type] if typeof type == 'string'
  136
+    type = types[type] if typeof type is 'string'
176 137
 
177 138
     throw new Error "OT code for document type missing" unless type
178 139
 
179  
-    if docName? and @docs[docName]?
  140
+    throw new Error 'Server-generated random doc names are not currently supported' unless docName?
  141
+
  142
+    if @docs[docName]
180 143
       doc = @docs[docName]
181 144
       if doc.type == type
182 145
         callback null, doc
183 146
       else
184 147
         callback 'Type mismatch', doc
185  
-
186 148
       return
187 149
 
188  
-    @send {doc:docName, open:true, create:true, snapshot:null, type:type.name}, (error, response) =>
189  
-      if error
190  
-        callback error
191  
-      else
192  
-        response.snapshot = type.create() unless response.snapshot != undefined
193  
-        response.type = type
194  
-        callback null, @makeDoc(response)
195  
-
196  
-  # To be written. Create a new document with a random name.
197  
-  create: (type, callback) ->
198  
-    open null, type, callback
199  
-
200  
-  disconnect: () ->
201  
-    if @socket
202  
-      @emit 'disconnecting'
203  
-      @socket.disconnect()
204  
-      @socket = null
  150
+    @makeDoc docName, {create:true, type:type.name}, callback
  151
+
  152
+# Not currently working.
  153
+#  create: (type, callback) ->
  154
+#    open null, type, callback
205 155
 
206 156
 # Make connections event emitters.
207 157
 unless WEB?
371  src/client/doc.coffee
... ...
@@ -1,7 +1,9 @@
  1
+unless WEB?
  2
+  types = require '../types'
  3
+
1 4
 # A Doc is a client's view on a sharejs document.
2 5
 #
3  
-# Documents are created by calling Connection.open(). They are only instantiated
4  
-# once the client has the document snapshot.
  6
+# Documents are created by calling Connection.open().
5 7
 #
6 8
 # Documents are event emitters - use doc.on(eventname, fn) to subscribe.
7 9
 #
@@ -11,36 +13,53 @@
11 13
 # Events:
12 14
 #  - remoteop (op)
13 15
 #  - changed (op)
14  
-#
15  
-# connection is a Connection object.
16  
-# name is the documents' docName.
17  
-# version is the version number of the document _on the server_
18  
-# type is the OT type of the document, which defines .compose(), .tranform(), etc.
19  
-# snapshot is the current state of the document.
20  
-
21  
-Doc = (connection, @name, @version, @type, @snapshot) ->
22  
-  throw new Error('Handling types without compose() defined is not currently implemented') unless @type.compose?
23  
-
24  
-  # Gotta figure out a cleaner way to make this work with closure.
25  
-
26  
-  # The op that is currently roundtripping to the server, or null.
27  
-  inflightOp = null
28  
-  inflightCallbacks = []
29  
-
30  
-  # All ops that are waiting for the server to acknowledge @inflightOp
31  
-  pendingOp = null
32  
-  pendingCallbacks = []
33  
-
34  
-  # Some recent ops, incase submitOp is called with an old op version number.
35  
-  serverOps = {}
  16
+#  - error
  17
+#  - open, closing, closed. 'closing' is not guaranteed to fire before closed.
  18
+class Doc
  19
+  # connection is a Connection object.
  20
+  # name is the documents' docName.
  21
+  # data can optionally contain known document data, and initial open() call arguments:
  22
+  # {v[erson], snapshot={...}, type, create=true/false/undefined}
  23
+  # callback will be called once the document is first opened.
  24
+  constructor: (@connection, @name, openData) ->
  25
+    # Any of these can be null / undefined at this stage.
  26
+    openData ||= {}
  27
+    @version = openData.v
  28
+    @snapshot = openData.snaphot
  29
+    @_setType openData.type if openData.type
  30
+
  31
+    @state = 'closed'
  32
+    @autoOpen = false
  33
+
  34
+    # Has the document already been created?
  35
+    @_create = openData.create
  36
+
  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
+
  46
+    # All ops that are waiting for the server to acknowledge @inflightOp
  47
+    @pendingOp = null
  48
+    @pendingCallbacks = []
  49
+
  50
+    # Some recent ops, incase submitOp is called with an old op version number.
  51
+    @serverOps = {}
36 52
 
37 53
   # Transform a server op by a client op, and vice versa.
38  
-  xf = @type.transformX or (client, server) =>
39  
-    client_ = @type.transform client, server, 'left'
40  
-    server_ = @type.transform server, client, 'right'
41  
-    return [client_, server_]
  54
+  _xf: (client, server) ->
  55
+    if @type.transformX
  56
+      @type.transformX(client, server)
  57
+    else
  58
+      client_ = @type.transform client, server, 'left'
  59
+      server_ = @type.transform server, client, 'right'
  60
+      return [client_, server_]
42 61
   
43  
-  otApply = (docOp, isRemote) =>
  62
+  _otApply: (docOp, isRemote) ->
44 63
     oldSnapshot = @snapshot
45 64
     @snapshot = @type.apply(@snapshot, docOp)
46 65
 
@@ -50,126 +69,236 @@ Doc = (connection, @name, @version, @type, @snapshot) ->
50 69
     @emit 'change', docOp, oldSnapshot
51 70
     @emit 'remoteop', docOp, oldSnapshot if isRemote
52 71
   
53  
-  # Send ops to the server, if appropriate.
54  
-  #
55  
-  # Only one op can be in-flight at a time, so if an op is already on its way then
56  
-  # this method does nothing.
57  
-  @flush = =>
58  
-    if inflightOp == null && pendingOp != null
59  
-      # Rotate null -> pending -> inflight, 
60  
-      inflightOp = pendingOp
61  
-      inflightCallbacks = pendingCallbacks
62  
-
63  
-      pendingOp = null
64  
-      pendingCallbacks = []
65  
-
66  
-      connection.send {'doc':@name, 'op':inflightOp, 'v':@version}, (error, response) =>
67  
-        oldInflightOp = inflightOp
68  
-        inflightOp = null
69  
-
70  
-        if error
71  
-          # This will happen if the server rejects edits from the client.
72  
-          # We'll send the error message to the user and roll back the change.
73  
-          #
74  
-          # If the server isn't going to allow edits anyway, we should probably
75  
-          # figure out some way to flag that (readonly:true in the open request?)
76  
-
77  
-          if type.invert
78  
-
79  
-            undo = @type.invert oldInflightOp
80  
-
81  
-            # Now we have to transform the undo operation by any server ops & pending ops
82  
-            if pendingOp
83  
-              [pendingOp, undo] = xf pendingOp, undo
84  
-
85  
-            # ... and apply it locally, reverting the changes.
86  
-            # 
87  
-            # This call will also call @emit 'remoteop'. I'm still not 100% sure about this
88  
-            # functionality, because its really a local op. Basically, the problem is that
89  
-            # if the client's op is rejected by the server, the editor window should update
90  
-            # to reflect the undo.
91  
-            otApply undo, true
92  
-          else
93  
-            throw new Error "Op apply failed (#{response.error}) and the OT type does not define an invert function."
94  
-
95  
-          callback error for callback in inflightCallbacks
96  
-        else
97  
-          throw new Error('Invalid version from server') unless response.v == @version
  72
+  _connectionStateChanged: (state, data) ->
  73
+    switch state
  74
+      when 'disconnected'
  75
+        @state = 'closed'
  76
+        # This is used by the server to make sure that when an op is resubmitted it
  77
+        # doesn't end up getting applied twice.
  78
+        @inflightSubmittedIds.push @connection.id if @inflightOp
98 79
 
99  
-          serverOps[@version] = oldInflightOp
100  
-          @version++
101  
-          callback null, oldInflightOp for callback in inflightCallbacks
  80
+        @emit 'closed'
102 81
 
103  
-        @flush()
  82
+      when 'ok' # Might be able to do this when we're connecting... that would save a roundtrip.
  83
+        @open() if @autoOpen
104 84
 
105  
-  # Internal API
106  
-  # The connection uses this method to notify a document that an op is received from the server.
107  
-  @_onOpReceived = (msg) ->
108  
-    # msg is {doc:, op:, v:}
  85
+      when 'stopped'
  86
+        @_openCallback? data
109 87
 
110  
-    # There is a bug in socket.io (produced on firefox 3.6) which causes messages
111  
-    # to be duplicated sometimes.
112  
-    # We'll just silently drop subsequent messages.
113  
-    return if msg.v < @version
  88
+    @emit state, data
114 89
 
115  
-    throw new Error("Expected docName '#{@name}' but got #{msg.doc}") unless msg.doc == @name
116  
-    throw new Error("Expected version #{@version} but got #{msg.v}") unless msg.v == @version
  90
+  _setType: (type) ->
  91
+    if typeof type is 'string'
  92
+      type = types[type]
117 93
 
118  
-#    p "if: #{i @inflightOp} pending: #{i @pendingOp} doc '#{@snapshot}' op: #{i msg.op}"
  94
+    throw new Error 'Support for types without compose() is not implemented' unless type and type.compose
119 95
 
120  
-    op = msg.op
121  
-    serverOps[@version] = op
  96
+    @type = type
  97
+    if type.api
  98
+      this[k] = v for k, v of type.api
  99
+      @_register?()
  100
+    else
  101
+      @provides = {}
  102
+
  103
+  _onMessage: (msg) ->
  104
+    #console.warn 's->c', msg
  105
+    if msg.open == true
  106
+      # The document has been successfully opened.
  107
+      @state = 'open'
  108
+      @_create = false # Don't try and create the document again next time open() is called.
  109
+      unless @created?
  110
+        @created = !!msg.create
  111
+
  112
+      @_setType msg.type if msg.type
  113
+      if msg.create
  114
+        @created = true
  115
+        @snapshot = @type.create()
  116
+      else
  117
+        @created = false unless @created is true
  118
+        @snapshot = msg.snapshot if msg.snapshot isnt undefined
  119
+
  120
+      @version = msg.v if msg.v?
  121
+
  122
+      # Resend any previously queued operation.
  123
+      if @inflightOp
  124
+        response =
  125
+          doc: @name
  126
+          op: @inflightOp
  127
+          v: @version
  128
+        response.dupIfSource = @inflightSubmittedIds if @inflightSubmittedIds.length
  129
+        @connection.send response
  130
+      else
  131
+        @flush()
122 132
 
123  
-    docOp = op
124  
-    if inflightOp != null
125  
-      [inflightOp, docOp] = xf inflightOp, docOp
126  
-    if pendingOp != null
127  
-      [pendingOp, docOp] = xf pendingOp, docOp
  133
+      @emit 'open'
128 134
       
129  
-    @version++
130  
-    # Finally, apply the op to @snapshot and trigger any event listeners
131  
-    otApply docOp, true
  135
+      @_openCallback? null
  136
+ 
  137
+    else if msg.open == false
  138
+      # The document has either been closed, or an open request has failed.
  139
+      if msg.error
  140
+        # An error occurred opening the document.
  141
+        console?.error "Could not open document: #{msg.error}"
  142
+        @emit 'error', msg.error
  143
+        @_openCallback? msg.error
  144
+
  145
+      @state = 'closed'
  146
+      @emit 'closed'
  147
+
  148
+      @_closeCallback?()
  149
+      @_closeCallback = null
  150
+
  151
+    else if msg.op is null and error is 'Op already submitted'
  152
+      # We've tried to resend an op to the server, which has already been received successfully. Do nothing.
  153
+      # The op will be confirmed normally when we get the op itself was echoed back from the server
  154
+      # (handled below).
  155
+
  156
+    else if (msg.op is undefined and msg.v isnt undefined) or (msg.op and msg.meta.source in @inflightSubmittedIds)
  157
+      # Our inflight op has been acknowledged.
  158
+      oldInflightOp = @inflightOp
  159
+      @inflightOp = null
  160
+      @inflightSubmittedIds.length = 0
  161
+
  162
+      error = msg.error
  163
+      if error
  164
+        # The server has rejected an op from the client for some reason.
  165
+        # We'll send the error message to the user and roll back the change.
  166
+        #
  167
+        # If the server isn't going to allow edits anyway, we should probably
  168
+        # figure out some way to flag that (readonly:true in the open request?)
  169
+
  170
+        if @type.invert
  171
+          undo = @type.invert oldInflightOp
  172
+
  173
+          # Now we have to transform the undo operation by any server ops & pending ops
  174
+          if @pendingOp
  175
+            [@pendingOp, undo] = @_xf @pendingOp, undo
  176
+
  177
+          # ... and apply it locally, reverting the changes.
  178
+          # 
  179
+          # This call will also call @emit 'remoteop'. I'm still not 100% sure about this
  180
+          # functionality, because its really a local op. Basically, the problem is that
  181
+          # if the client's op is rejected by the server, the editor window should update
  182
+          # to reflect the undo.
  183
+          @_otApply undo, true
  184
+        else
  185
+          @emit 'error', "Op apply failed (#{error}) and the op could not be reverted"
  186
+
  187
+        callback error for callback in @inflightCallbacks
  188
+      else
  189
+        # The op applied successfully.
  190
+        throw new Error('Invalid version from server') unless msg.v == @version
  191
+
  192
+        @serverOps[@version] = oldInflightOp
  193
+        @version++
  194
+        callback null, oldInflightOp for callback in @inflightCallbacks
  195
+
  196
+      # Send the next op.
  197
+      @flush()
  198
+
  199
+    else if msg.op
  200
+      # We got a new op from the server.
  201
+      # msg is {doc:, op:, v:}
  202
+
  203
+      # There is a bug in socket.io (produced on firefox 3.6) which causes messages
  204
+      # to be duplicated sometimes.
  205
+      # We'll just silently drop subsequent messages.
  206
+      return if msg.v < @version
  207
+
  208
+      return @emit 'error', "Expected docName '#{@name}' but got #{msg.doc}" unless msg.doc == @name
  209
+      return @emit 'error', "Expected version #{@version} but got #{msg.v}" unless msg.v == @version
  210
+
  211
+  #    p "if: #{i @inflightOp} pending: #{i @pendingOp} doc '#{@snapshot}' op: #{i msg.op}"
  212
+
  213
+      op = msg.op
  214
+      @serverOps[@version] = op
  215
+
  216
+      docOp = op
  217
+      if @inflightOp != null
  218
+        [@inflightOp, docOp] = @_xf @inflightOp, docOp
  219
+      if @pendingOp != null
  220
+        [@pendingOp, docOp] = @_xf @pendingOp, docOp
  221
+        
  222
+      @version++
  223
+      # Finally, apply the op to @snapshot and trigger any event listeners
  224
+      @_otApply docOp, true
  225
+
  226
+    else
  227
+      console?.warn 'Unhandled document message:', msg
  228
+
  229
+
  230
+  # Send ops to the server, if appropriate.
  231
+  #
  232
+  # Only one op can be in-flight at a time, so if an op is already on its way then
  233
+  # this method does nothing.
  234
+  flush: =>
  235
+    return unless @connection.state == 'ok' and @inflightOp == null and @pendingOp != null
  236
+
  237
+    # Rotate null -> pending -> inflight
  238
+    @inflightOp = @pendingOp
  239
+    @inflightCallbacks = @pendingCallbacks
  240
+
  241
+    @pendingOp = null
  242
+    @pendingCallbacks = []
  243
+
  244
+    @connection.send {doc:@name, op:@inflightOp, v:@version}
132 245
 
133 246
   # Submit an op to the server. The op maybe held for a little while before being sent, as only one
134 247
   # op can be inflight at any time.
135  
-  @submitOp = (op, callback) ->
  248
+  submitOp: (op, callback) ->
136 249
     op = @type.normalize(op) if @type.normalize?
137 250
 
138 251
     # If this throws an exception, no changes should have been made to the doc
139 252
     @snapshot = @type.apply @snapshot, op
140 253
 
141  
-    if pendingOp != null
142  
-      pendingOp = @type.compose(pendingOp, op)
  254
+    if @pendingOp != null
  255
+      @pendingOp = @type.compose(@pendingOp, op)
143 256
     else
144  
-      pendingOp = op
  257
+      @pendingOp = op
145 258
 
146  
-    pendingCallbacks.push callback if callback
  259
+    @pendingCallbacks.push callback if callback
147 260
 
148 261
     @emit 'change', op
149 262
 
150 263
     # A timeout is used so if the user sends multiple ops at the same time, they'll be composed
151  
-    # together and sent together.
  264
+    # & sent together.
152 265
     setTimeout @flush, 0
153  
-  
  266
+
  267
+  # Open a document. The document starts closed.
  268
+  open: (callback) ->
  269
+    @autoOpen = true
  270
+    return unless @state is 'closed'
  271
+
  272
+    message =
  273
+      doc: @name
  274
+      open: true
  275
+
  276
+    message.snapshot = null if @snapshot is undefined
  277
+    message.type = @type.name if @type
  278
+    message.v = @version if @version?
  279
+    message.create = true if @_create
  280
+
  281
+    @connection.send message
  282
+
  283
+    @state = 'opening'
  284
+
  285
+    @_openCallback = (error) =>
  286
+      @_openCallback = null
  287
+      callback? error
  288
+
154 289
   # Close a document.
155  
-  # No unit tests for this so far.
156  
-  @close = (callback) ->
157  
-    return callback?() if connection.socket == null
  290
+  close: (callback) ->
  291
+    @autoOpen = false
  292
+    return callback?() if @state is 'closed'
158 293
 
159  
-    connection.send {'doc':@name, open:false}, =>
160  
-      callback?()
161  
-      @emit 'closed'
162  
-      return
163  
-    @emit 'closing'
164  
-  
165  
-  if @type.api
166  
-    this[k] = v for k, v of @type.api
167  
-    @_register?()
168  
-  else
169  
-    @provides = {}
  294
+    @connection.send {doc:@name, open:false}
170 295
 
171  
-  this
  296
+    # Should this happen immediately or when we get open:false back from the server?
  297
+    @state = 'closed'
172 298
 
  299
+    @emit 'closing'
  300
+    @_closeCallback = callback
  301
+ 
173 302
 # Make documents event emitters
174 303
 unless WEB?
175 304
   MicroEvent = require './microevent'
2  src/client/index.coffee
@@ -25,7 +25,7 @@ exports.open = do ->
25 25
   getConnection = (origin) ->
26 26
     if WEB?
27 27
       location = window.location
28  
-      origin ?= "#{location.protocol}//#{location.hostname}/sjs"
  28
+      origin ?= "#{location.protocol}//#{location.host}/channel"
29 29
     
30 30
     unless connections[origin]
31 31
       c = new Connection origin
5  src/server/index.coffee
@@ -42,10 +42,13 @@ create.attach = attach = (server, options, model = createModel(options)) ->
42 42
   # The client frontend doesn't get access to the model at all, to make sure security stuff is
43 43
   # done properly.
44 44
   server.use rest(createClient, options.rest) if options.rest != null
45  
-  socketio.attach(server, createClient, options.socketio or {}) if options.socketio != null
  45
+
  46
+  # Socketio frontend is now disabled by default.
  47
+  socketio.attach(server, createClient, options.socketio or {}) if options.socketio?
46 48
 
47 49
   if options.browserChannel != null
48 50
     options.browserChannel ?= {}
  51
+    #options.browserChannel.base ?= '/sjs'
49 52
     options.browserChannel.server = server
50 53
     server.use browserChannel(createClient, options.browserChannel)
51 54
 
10  test/client.coffee
@@ -17,8 +17,9 @@ genTests = (client) -> testCase
17 17
     @auth = (client, action) -> action.accept()
18 18
 
19 19
     options =
20  
-      socketio: {}
  20
+      socketio: null
21 21
       rest: null
  22
+      browserChannel: {base: '/sjs'}
22 23
       db: {type: 'none'}
23 24
       auth: (client, action) => @auth client, action
24 25
 
@@ -28,14 +29,14 @@ genTests = (client) -> testCase
28 29
     @server.listen =>
29 30
       @port = @server.address().port
30 31
       @c = new client.Connection "http://localhost:#{@port}/sjs"
31  
-      @c.on 'connect', callback
  32
+      @c.on 'ok', callback
32 33
   
33 34
   tearDown: (callback) ->
34 35
     @c.disconnect()
35 36
 
36 37
     @server.on 'close', callback
37 38
     @server.close()
38  
-
  39
+  
39 40
   'open using the bare API': (test) ->
40 41
     client.open @name, 'text', "http://localhost:#{@port}/sjs", (error, doc) =>
41 42
       test.ok doc
@@ -291,7 +292,7 @@ genTests = (client) -> testCase
291 292
     c.on 'connect failed', (error) ->
292 293
       test.strictEqual error, 'forbidden'
293 294
       test.done()
294  
-
  295
+  
295 296
   '(new Connection).open() fails if auth rejects the connection': (test) ->
296 297
     @auth = (client, action) -> action.reject()
297 298
 
@@ -319,6 +320,7 @@ genTests = (client) -> testCase
319 320
     c.open @name, 'text', (error, doc) =>
320 321
       test.fail doc if doc
321 322
       test.strictEqual error, 'forbidden'
  323
+      c.disconnect()
322 324
       test.done()
323 325
 
324 326
   'client.open fails if auth rejects the connection': (test) ->
1  test/helpers/webclient.coffee
@@ -11,6 +11,7 @@ fs = require 'fs'
11 11
 
12 12
 window = {}
13 13
 window.io = require 'socket.io-client'
  14
+window.BCSocket = require('browserchannel').BCSocket
14 15
 
15 16
 for script in ['share', 'json']
16 17
   script = "#{script}.uncompressed" if TEST_UNCOMPRESSED
1  test/microevent.coffee
@@ -91,7 +91,6 @@ tests =
91 91
     @e.emit 'bar'
92 92
 
93 93
 
94  
-
95 94
 # The tests above are run both with a new MicroEvent and with an object with
96 95
 # microevent mixed in.
97 96
 
12  webclient/ace.js
... ...
@@ -1,6 +1,8 @@
1 1
 (function() {
2 2
   var Range, applyToShareJS;
  3
+
3 4
   Range = require("ace/range").Range;
  5
+
4 6
   applyToShareJS = function(editorDoc, delta, doc) {
5 7
     var getStartOffsetPosition, pos, text;
6 8
     getStartOffsetPosition = function(range) {
@@ -33,6 +35,7 @@
33 35
         throw new Error("unknown action: " + delta.action);
34 36
     }
35 37
   };
  38
+
36 39
   window.sharejs.Doc.prototype.attach_ace = function(editor, keepEditorContents) {
37 40
     var check, doc, docListener, editorDoc, editorListener, offsetToPos, suppress;
38 41
     if (!this.provides['text']) {
@@ -62,9 +65,7 @@
62 65
     check();
63 66
     suppress = false;
64 67
     editorListener = function(change) {
65  
-      if (suppress) {
66  
-        return;
67  
-      }
  68
+      if (suppress) return;
68 69
       applyToShareJS(editorDoc, change.data, doc);
69 70
       return check();
70 71
     };
@@ -81,9 +82,7 @@
81 82
       row = 0;
82 83
       for (row = 0, _len = lines.length; row < _len; row++) {
83 84
         line = lines[row];
84  
-        if (offset <= line.length) {
85  
-          break;
86  
-        }
  85
+        if (offset <= line.length) break;
87 86
         offset -= lines[row].length + 1;
88 87
       }
89 88
       return {
@@ -111,4 +110,5 @@
111 110
       return delete doc.detach_ace;
112 111
     };
113 112
   };
  113
+
114 114
 }).call(this);
2  webclient/json.js
... ...
@@ -1 +1 @@
1  
-((function(){var a,b,c,d,e,f,g,h,i=!0,j=Array.prototype.slice,k=window.sharejs;typeof i!="undefined"&&i!==null?g=k.types.text:g=require("./text"),e={},e.name="json",e.create=function(){return null},e.invertComponent=function(a){var b={p:a.p};return a.si!==void 0&&(b.sd=a.si),a.sd!==void 0&&(b.si=a.sd),a.oi!==void 0&&(b.od=a.oi),a.od!==void 0&&(b.oi=a.od),a.li!==void 0&&(b.ld=a.li),a.ld!==void 0&&(b.li=a.ld),a.na!==void 0&&(b.na=-a.na),a.lm!==void 0&&(b.lm=a.p[a.p.length-1],b.p=a.p.slice(0,a.p.length-1).concat([a.lm])),b},e.invert=function(a){var b,c,d,f=a.slice().reverse(),g=[];for(c=0,d=f.length;c<d;c++)b=f[c],g.push(e.invertComponent(b));return g},e.checkValidOp=function(){},d=function(a){return Object.prototype.toString.call(a)==="[object Array]"},e.checkList=function(a){if(!d(a))throw new Error("Referenced element not a list")},e.checkObj=function(a){if(a.constructor!==Object)throw new Error("Referenced element not an object (it was "+JSON.stringify(a)+")")},e.apply=function(a,c){var d,f,g,h,i,j,k,l,m,n,o,p,q;e.checkValidOp(c),c=b(c),f={data:b(a)};try{for(i=0,o=c.length;i<o;i++){d=c[i],l=null,m=null,h=f,j="data",q=d.p;for(n=0,p=q.length;n<p;n++){k=q[n],l=h,m=j,h=h[j],j=k;if(l==null)throw new Error("Path invalid")}if(d.na!==void 0){if(typeof h[j]!="number")throw new Error("Referenced element not a number");h[j]+=d.na}else if(d.si!==void 0){if(typeof h!="string")throw new Error("Referenced element not a string (it was "+JSON.stringify(h)+")");l[m]=h.slice(0,j)+d.si+h.slice(j)}else if(d.sd!==void 0){if(typeof h!="string")throw new Error("Referenced element not a string");if(h.slice(j,j+d.sd.length)!==d.sd)throw new Error("Deleted string does not match");l[m]=h.slice(0,j)+h.slice(j+d.sd.length)}else if(d.li!==void 0&&d.ld!==void 0)e.checkList(h),h[j]=d.li;else if(d.li!==void 0)e.checkList(h),h.splice(j,0,d.li);else if(d.ld!==void 0)e.checkList(h),h.splice(j,1);else if(d.lm!==void 0)e.checkList(h),d.lm!==j&&(g=h[j],h.splice(j,1),h.splice(d.lm,0,g));else if(d.oi!==void 0)e.checkObj(h),h[j]=d.oi;else if(d.od!==void 0)e.checkObj(h),delete h[j];else throw new Error("invalid / missing instruction in op")}}catch(r){throw r}return f.data},e.pathMatches=function(a,b,c){var d,e,f;if(a.length!==b.length)return!1;for(d=0,f=a.length;d<f;d++){e=a[d];if(e!==b[d]&&(!c||d!==a.length-1))return!1}return!0},e.append=function(a,c){var d;return c=b(c),a.length!==0&&e.pathMatches(c.p,(d=a[a.length-1]).p)?d.na!==void 0&&c.na!==void 0?a[a.length-1]={p:d.p,na:d.na+c.na}:d.li!==void 0&&c.li===void 0&&c.ld===d.li?d.ld!==void 0?delete d.li:a.pop():d.od!==void 0&&d.oi===void 0&&c.oi!==void 0&&c.od===void 0?d.oi=c.oi:c.lm!==void 0&&c.p[c.p.length-1]===c.lm?null:a.push(c):a.push(c)},e.compose=function(a,c){var d,f,g,h;e.checkValidOp(a),e.checkValidOp(c),f=b(a);for(g=0,h=c.length;g<h;g++)d=c[g],e.append(f,d);return f},e.normalize=function(a){var b,c,f,g,h=[];d(a)||(a=[a]);for(c=0,f=a.length;c<f;c++)b=a[c],(g=b.p)==null&&(b.p=[]),e.append(h,b);return h},b=function(a){return JSON.parse(JSON.stringify(a))},e.commonPath=function(a,b){var c;a=a.slice(),b=b.slice(),a.unshift("data"),b.unshift("data"),a=a.slice(0,a.length-1),b=b.slice(0,b.length-1);if(b.length===0)return-1;c=0;while(a[c]===b[c]&&c<a.length){c++;if(c===b.length)return c-1}},e.transformComponent=function(a,c,d,f){var h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;c=b(c),c.na!==void 0&&c.p.push(0),d.na!==void 0&&d.p.push(0),h=e.commonPath(c.p,d.p),i=e.commonPath(d.p,c.p),l=c.p.length,p=d.p.length,c.na!==void 0&&c.p.pop(),d.na!==void 0&&d.p.pop();if(d.na)return i!=null&&p>=l&&d.p[i]===c.p[i]&&(c.ld!==void 0?(o=b(d),o.p=o.p.slice(l),c.ld=e.apply(b(c.ld),[o])):c.od!==void 0&&(o=b(d),o.p=o.p.slice(l),c.od=e.apply(b(c.od),[o]))),e.append(a,c),a;i!=null&&p>l&&c.p[i]===d.p[i]&&(c.ld!==void 0?(o=b(d),o.p=o.p.slice(l),c.ld=e.apply(b(c.ld),[o])):c.od!==void 0&&(o=b(d),o.p=o.p.slice(l),c.od=e.apply(b(c.od),[o])));if(h!=null){j=l===p;if(d.na===void 0)if(d.si!==void 0||d.sd!==void 0){if(c.si!==void 0||c.sd!==void 0){if(!j)throw new Error("must be a string?");k=function(a){var b={p:a.p[a.p.length-1]};return a.si?b.i=a.si:b.d=a.sd,b},v=k(c),w=k(d),t=[],g._tc(t,v,w,f);for(y=0,z=t.length;y<z;y++)u=t[y],n={p:c.p.slice(0,h)},n.p.push(u.p),u.i!=null&&(n.si=u.i),u.d!=null&&(n.sd=u.d),e.append(a,n);return a}}else if(d.li!==void 0&&d.ld!==void 0){if(d.p[h]===c.p[h]){if(!j)return a;if(c.ld!==void 0)if(c.li!==void 0&&f==="left")c.ld=b(d.li);else return a}}else if(d.li!==void 0)c.li!==void 0&&c.ld===void 0&&j&&c.p[h]===d.p[h]?f==="right"&&c.p[h]++:d.p[h]<=c.p[h]&&c.p[h]++,c.lm!==void 0&&j&&d.p[h]<=c.lm&&c.lm++;else if(d.ld!==void 0){if(c.lm!==void 0&&j){if(d.p[h]===c.p[h])return a;s=d.p[h],m=c.p[h],x=c.lm,(s<x||s===x&&m<x)&&c.lm--}if(d.p[h]<c.p[h])c.p[h]--;else if(d.p[h]===c.p[h]){if(p<l)return a;if(c.ld!==void 0)if(c.li!==void 0)delete c.ld;else return a}}else if(d.lm!==void 0)if(c.lm!==void 0&&l===p){m=c.p[h],x=c.lm,q=d.p[h],r=d.lm;if(q!==r)if(m===q)if(f==="left")c.p[h]=r,m===x&&(c.lm=r);else return a;else m>q&&c.p[h]--,m>r?c.p[h]++:m===r&&q>r&&(c.p[h]++,m===x&&c.lm++),x>q?c.lm--:x===q&&x>m&&c.lm--,x>r?c.lm++:x===r&&(r>q&&x>m||r<q&&x<m?f==="right"&&c.lm++:x>m?c.lm++:x===q&&c.lm--)}else c.li!==void 0&&c.ld===void 0&&j?(m=d.p[h],x=d.lm,s=c.p[h],s>m&&c.p[h]--,s>x&&c.p[h]++):(m=d.p[h],x=d.lm,s=c.p[h],s===m?c.p[h]=x:(s>m&&c.p[h]--,s>x?c.p[h]++:s===x&&m>x&&c.p[h]++));else if(d.oi!==void 0&&d.od!==void 0){if(c.p[h]===d.p[h]){if(c.oi===void 0||!j)return a;if(f==="right")return a;c.od=d.oi}}else if(d.oi!==void 0){if(c.oi!==void 0&&c.p[h]===d.p[h])if(f==="left")e.append(a,{p:c.p,od:d.oi});else return a}else if(d.od!==void 0&&c.p[h]===d.p[h]){if(!j)return a;if(c.oi!==void 0)delete c.od;else return a}}return e.append(a,c),a},typeof i!="undefined"&&i!==null?(k.types||(k.types={}),k._bt(e,e.transformComponent,e.checkValidOp,e.append),k.types.json=e):(module.exports=e,require("./helpers").bootstrapTransform(e,e.transformComponent,e.checkValidOp,e.append)),typeof i=="undefined"&&(e=require("./json")),c=function(a){return a.length===1&&a[0].constructor===Array?a[0]:a},a=function(){function a(a,b){this.doc=a,this.path=b}return a.prototype.at=function(){var a=1<=arguments.length?j.call(arguments,0):[];return this.doc.at(this.path.concat(c(a)))},a.prototype.get=function(){return this.doc.getAt(this.path)},a.prototype.set=function(a,b){return this.doc.setAt(this.path,a,b)},a.prototype.insert=function(a,b,c){return this.doc.insertAt(this.path,a,b,c)},a.prototype.del=function(a,b,c){return this.doc.deleteTextAt(this.path,b,a,c)},a.prototype.remove=function(a){return this.doc.removeAt(this.path,a)},a.prototype.push=function(a,b){return this.insert(this.get().length,a,b)},a.prototype.move=function(a,b,c){return this.doc.moveAt(this.path,a,b,c)},a.prototype.add=function(a,b){return this.doc.addAt(this.path,a,b)},a.prototype.on=function(a,b){return this.doc.addListener(this.path,a,b)},a.prototype.removeListener=function(a){return this.doc.removeListener(a)},a.prototype.getLength=function(){return this.get().length},a.prototype.getText=function(){return this.get()},a}(),h=function(a,b){var c,d,e,f={data:a},g="data",h=f;for(d=0,e=b.length;d<e;d++){c=b[d],h=h[g],g=c;if(typeof h=="undefined")throw new Error("bad path")}return{elem:h,key:g}},f=function(a,b){var c,d,e;if(a.length!==b.length)return!1;for(d=0,e=a.length;d<e;d++){c=a[d];if(c!==b[d])return!1}return!0},e.api={provides:{json:!0},at:function(){var b=1<=arguments.length?j.call(arguments,0):[];return new a(this,c(b))},get:function(){return this.snapshot},set:function(a,b){return this.setAt([],a,b)},getAt:function(a){var b=h(this.snapshot,a),c=b.elem,d=b.key;return c[d]},setAt:function(a,b,c){var d=h(this.snapshot,a),e=d.elem,f=d.key,g={p:a};if(e.constructor===Array)g.li=b,typeof e[f]!="undefined"&&(g.ld=e[f]);else if(typeof e=="object")g.oi=b,typeof e[f]!="undefined"&&(g.od=e[f]);else throw new Error("bad path");return this.submitOp([g],c)},removeAt:function(a,b){var c,d=h(this.snapshot,a),e=d.elem,f=d.key;if(typeof e[f]=="undefined")throw new Error("no element at that path");c={p:a};if(e.constructor===Array)c.ld=e[f];else if(typeof e=="object")c.od=e[f];else throw new Error("bad path");return this.submitOp([c],b)},insertAt:function(a,b,c,d){var e=h(this.snapshot,a),f=e.elem,g=e.key,i={p:a.concat(b)};return f[g].constructor===Array?i.li=c:typeof f[g]=="string"&&(i.si=c),this.submitOp([i],d)},moveAt:function(a,b,c,d){var e=[{p:a.concat(b),lm:c}];return this.submitOp(e,d)},addAt:function(a,b,c){var d=[{p:a,na:b}];return this.submitOp(d,c)},deleteTextAt:function(a,b,c,d){var e=h(this.snapshot,a),f=e.elem,g=e.key,i=[{p:a.concat(c),sd:f[g].slice(c,c+b)}];return this.submitOp(i,d)},addListener:function(a,b,c){var d={path:a,event:b,cb:c};return this._listeners.push(d),d},removeListener:function(a){var b=this._listeners.indexOf(a);return b<0?!1:(this._listeners.splice(b,1),!0)},_register:function(){return this._listeners=[],this.on("change",function(a){var b,c,d,e,f,g,h,i,j,k,l=[];for(h=0,i=a.length;h<i;h++){b=a[h];if(b.na!==void 0||b.si!==void 0||b.sd!==void 0)continue;f=[],k=this._listeners;for(d=0,j=k.length;d<j;d++){e=k[d],c={p:e.path,na:0},g=this.type.transformComponent([],c,b,"left");if(g.length===0)f.push(d);else if(g.length===1)e.path=g[0].p;else throw new Error("Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components.")}f.sort(function(a,b){return b-a}),l.push(function(){var a,b,c=[];for(a=0,b=f.length;a<b;a++)d=f[a],c.push(this._listeners.splice(d,1));return c}.call(this))}return l}),this.on("remoteop",function(a){var b,c,d,e,g,h,i,j,k,l=[];for(j=0,k=a.length;j<k;j++)b=a[j],h=b.na===void 0?b.p.slice(0,b.p.length-1):b.p,l.push(function(){var a,j,k,l=this._listeners,m=[];for(a=0,j=l.length;a<j;a++)k=l[a],i=k.path,g=k.event,c=k.cb,m.push(function(){if(f(i,h))switch(g){case"insert":if(b.li!==void 0&&b.ld===void 0)return c(b.p[b.p.length-1],b.li);if(b.oi!==void 0&&b.od===void 0)return c(b.p[b.p.length-1],b.oi);if(b.si!==void 0)return c(b.p[b.p.length-1],b.si);break;case"delete":if(b.li===void 0&&b.ld!==void 0)return c(b.p[b.p.length-1],b.ld);if(b.oi===void 0&&b.od!==void 0)return c(b.p[b.p.length-1],b.od);if(b.sd!==void 0)return c(b.p[b.p.length-1],b.sd);break;case"replace":if(b.li!==void 0&&b.ld!==void 0)return c(b.p[b.p.length-1],b.ld,b.li);if(b.oi!==void 0&&b.od!==void 0)return c(b.p[b.p.length-1],b.od,b.oi);break;case"move":if(b.lm!==void 0)return c(b.p[b.p.length-1],b.lm);break;case"add":if(b.na!==void 0)return c(b.na)}else if((e=this.type.commonPath(h,i))!=null&&g==="child op"){if(h.length===i.length)throw new Error("paths match length and have commonality, but aren't equal?");return d=b.p.slice(e+1),c(d,b)}}.call(this));return m}.call(this));return l})}}})).call(this)
  1
+((function(){var a,b,c,d,e,f,g,h,i=!0,j=Array.prototype.slice,k=window.sharejs;typeof i!="undefined"&&i!==null?g=k.types.text:g=require("./text"),e={},e.name="json",e.create=function(){return null},e.invertComponent=function(a){var b={p:a.p};return a.si!==void 0&&(b.sd=a.si),a.sd!==void 0&&(b.si=a.sd),a.oi!==void 0&&(b.od=a.oi),a.od!==void 0&&(b.oi=a.od),a.li!==void 0&&(b.ld=a.li),a.ld!==void 0&&(b.li=a.ld),a.na!==void 0&&(b.na=-a.na),a.lm!==void 0&&(b.lm=a.p[a.p.length-1],b.p=a.p.slice(0,a.p.length-1).concat([a.lm])),b},e.invert=function(a){var b,c,d,f=a.slice().reverse(),g=[];for(c=0,d=f.length;c<d;c++)b=f[c],g.push(e.invertComponent(b));return g},e.checkValidOp=function(){},d=function(a){return Object.prototype.toString.call(a)==="[object Array]"},e.checkList=function(a){if(!d(a))throw new Error("Referenced element not a list")},e.checkObj=function(a){if(a.constructor!==Object)throw new Error("Referenced element not an object (it was "+JSON.stringify(a)+")")},e.apply=function(a,c){var d,f,g,h,i,j,k,l,m,n,o,p,q;e.checkValidOp(c),c=b(c),f={data:b(a)};try{for(i=0,o=c.length;i<o;i++){d=c[i],l=null,m=null,h=f,j="data",q=d.p;for(n=0,p=q.length;n<p;n++){k=q[n],l=h,m=j,h=h[j],j=k;if(l==null)throw new Error("Path invalid")}if(d.na!==void 0){if(typeof h[j]!="number")throw new Error("Referenced element not a number");h[j]+=d.na}else if(d.si!==void 0){if(typeof h!="string")throw new Error("Referenced element not a string (it was "+JSON.stringify(h)+")");l[m]=h.slice(0,j)+d.si+h.slice(j)}else if(d.sd!==void 0){if(typeof h!="string")throw new Error("Referenced element not a string");if(h.slice(j,j+d.sd.length)!==d.sd)throw new Error("Deleted string does not match");l[m]=h.slice(0,j)+h.slice(j+d.sd.length)}else if(d.li!==void 0&&d.ld!==void 0)e.checkList(h),h[j]=d.li;else if(d.li!==void 0)e.checkList(h),h.splice(j,0,d.li);else if(d.ld!==void 0)e.checkList(h),h.splice(j,1);else if(d.lm!==void 0)e.checkList(h),d.lm!==j&&(g=h[j],h.splice(j,1),h.splice(d.lm,0,g));else if(d.oi!==void 0)e.checkObj(h),h[j]=d.oi;else{if(d.od===void 0)throw new Error("invalid / missing instruction in op");e.checkObj(h),delete h[j]}}}catch(r){throw r}return f.data},e.pathMatches=function(a,b,c){var d,e,f;if(a.length!==b.length)return!1;for(d=0,f=a.length;d<f;d++){e=a[d];if(e!==b[d]&&(!c||d!==a.length-1))return!1}return!0},e.append=function(a,c){var d;return c=b(c),a.length!==0&&e.pathMatches(c.p,(d=a[a.length-1]).p)?d.na!==void 0&&c.na!==void 0?a[a.length-1]={p:d.p,na:d.na+c.na}:d.li!==void 0&&c.li===void 0&&c.ld===d.li?d.ld!==void 0?delete d.li:a.pop():d.od!==void 0&&d.oi===void 0&&c.oi!==void 0&&c.od===void 0?d.oi=c.oi:c.lm!==void 0&&c.p[c.p.length-1]===c.lm?null:a.push(c):a.push(c)},e.compose=function(a,c){var d,f,g,h;e.checkValidOp(a),e.checkValidOp(c),f=b(a);for(g=0,h=c.length;g<h;g++)d=c[g],e.append(f,d);return f},e.normalize=function(a){var b,c,f,g=[];d(a)||(a=[a]);for(c=0,f=a.length;c<f;c++)b=a[c],b.p==null&&(b.p=[]),e.append(g,b);return g},b=function(a){return JSON.parse(JSON.stringify(a))},e.commonPath=function(a,b){var c;a=a.slice(),b=b.slice(),a.unshift("data"),b.unshift("data"),a=a.slice(0,a.length-1),b=b.slice(0,b.length-1);if(b.length===0)return-1;c=0;while(a[c]===b[c]&&c<a.length){c++;if(c===b.length)return c-1}},e.transformComponent=function(a,c,d,f){var h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;c=b(c),c.na!==void 0&&c.p.push(0),d.na!==void 0&&d.p.push(0),h=e.commonPath(c.p,d.p),i=e.commonPath(d.p,c.p),l=c.p.length,p=d.p.length,c.na!==void 0&&c.p.pop(),d.na!==void 0&&d.p.pop();if(d.na)return i!=null&&p>=l&&d.p[i]===c.p[i]&&(c.ld!==void 0?(o=b(d),o.p=o.p.slice(l),c.ld=e.apply(b(c.ld),[o])):c.od!==void 0&&(o=b(d),o.p=o.p.slice(l),c.od=e.apply(b(c.od),[o]))),e.append(a,c),a;i!=null&&p>l&&c.p[i]===d.p[i]&&(c.ld!==void 0?(o=b(d),o.p=o.p.slice(l),c.ld=e.apply(b(c.ld),[o])):c.od!==void 0&&(o=b(d),o.p=o.p.slice(l),c.od=e.apply(b(c.od),[o])));if(h!=null){j=l===p;if(d.na===void 0)if(d.si!==void 0||d.sd!==void 0){if(c.si!==void 0||c.sd!==void 0){if(!j)throw new Error("must be a string?");k=function(a){var b={p:a.p[a.p.length-1]};return a.si?b.i=a.si:b.d=a.sd,b},v=k(c),w=k(d),t=[],g._tc(t,v,w,f);for(y=0,z=t.length;y<z;y++)u=t[y],n={p:c.p.slice(0,h)},n.p.push(u.p),u.i!=null&&(n.si=u.i),u.d!=null&&(n.sd=u.d),e.append(a,n);return a}}else if(d.li!==void 0&&d.ld!==void 0){if(d.p[h]===c.p[h]){if(!j)return a;if(c.ld!==void 0){if(c.li===void 0||f!=="left")return a;c.ld=b(d.li)}}}else if(d.li!==void 0)c.li!==void 0&&c.ld===void 0&&j&&c.p[h]===d.p[h]?f==="right"&&c.p[h]++:d.p[h]<=c.p[h]&&c.p[h]++,c.lm!==void 0&&j&&d.p[h]<=c.lm&&c.lm++;else if(d.ld!==void 0){if(c.lm!==void 0&&j){if(d.p[h]===c.p[h])return a;s=d.p[h],m=c.p[h],x=c.lm,(s<x||s===x&&m<x)&&c.lm--}if(d.p[h]<c.p[h])c.p[h]--;else if(d.p[h]===c.p[h]){if(p<l)return a;if(c.ld!==void 0){if(c.li===void 0)return a;delete c.ld}}}else if(d.lm!==void 0)if(c.lm!==void 0&&l===p){m=c.p[h],x=c.lm,q=d.p[h],r=d.lm;if(q!==r)if(m===q){if(f!=="left")return a;c.p[h]=r,m===x&&(c.lm=r)}else m>q&&c.p[h]--,m>r?c.p[h]++:m===r&&q>r&&(c.p[h]++,m===x&&c.lm++),x>q?c.lm--:x===q&&x>m&&c.lm--,x>r?c.lm++:x===r&&(r>q&&x>m||r<q&&x<m?f==="right"&&c.lm++:x>m?c.lm++:x===q&&c.lm--)}else c.li!==void 0&&c.ld===void 0&&j?(m=d.p[h],x=d.lm,s=c.p[h],s>m&&c.p[h]--,s>x&&c.p[h]++):(m=d.p[h],x=d.lm,s=c.p[h],s===m?c.p[h]=x:(s>m&&c.p[h]--,s>x?c.p[h]++:s===x&&m>x&&c.p[h]++));else if(d.oi!==void 0&&d.od!==void 0){if(c.p[h]===d.p[h]){if(c.oi===void 0||!j)return a;if(f==="right")return a;c.od=d.oi}}else if(d.oi!==void 0){if(c.oi!==void 0&&c.p[h]===d.p[h]){if(f!=="left")return a;e.append(a,{p:c.p,od:d.oi})}}else if(d.od!==void 0&&c.p[h]===d.p[h]){if(!j)return a;if(c.oi===void 0)return a;delete c.od}}return e.append(a,c),a},typeof i!="undefined"&&i!==null?(k.types||(k.types={}),k._bt(e,e.transformComponent,e.checkValidOp,e.append),k.types.json=e):(module.exports=e,require("./helpers").bootstrapTransform(e,e.transformComponent,e.checkValidOp,e.append)),typeof i=="undefined"&&(e=require("./json")),c=function(a){return a.length===1&&a[0].constructor===Array?a[0]:a},a=function(){function a(a,b){this.doc=a,this.path=b}return a.prototype.at=function(){var a=1<=arguments.length?j.call(arguments,0):[];return this.doc.at(this.path.concat(c(a)))},a.prototype.get=function(){return this.doc.getAt(this.path)},a.prototype.set=function(a,b){return this.doc.setAt(this.path,a,b)},a.prototype.insert=function(a,b,c){return this.doc.insertAt(this.path,a,b,c)},a.prototype.del=function(a,b,c){return this.doc.deleteTextAt(this.path,b,a,c)},a.prototype.remove=function(a){return this.doc.removeAt(this.path,a)},a.prototype.push=function(a,b){return this.insert(this.get().length,a,b)},a.prototype.move=function(a,b,c){return this.doc.moveAt(this.path,a,b,c)},a.prototype.add=function(a,b){return this.doc.addAt(this.path,a,b)},a.prototype.on=function(a,b){return this.doc.addListener(this.path,a,b)},a.prototype.removeListener=function(a){return this.doc.removeListener(a)},a.prototype.getLength=function(){return this.get().length},a.prototype.getText=function(){return this.get()},a}(),h=function(a,b){var c,d,e,f={data:a},g="data",h=f;for(d=0,e=b.length;d<e;d++){c=b[d],h=h[g],g=c;if(typeof h=="undefined")throw new Error("bad path")}return{elem:h,key:g}},f=function(a,b){var c,d,e;if(a.length!==b.length)return!1;for(d=0,e=a.length;d<e;d++){c=a[d];if(c!==b[d])return!1}return!0},e.api={provides:{json:!0},at:function(){var b=1<=arguments.length?j.call(arguments,0):[];return new a(this,c(b))},get:function(){return this.snapshot},set:function(a,b){return this.setAt([],a,b)},getAt:function(a){var b=h(this.snapshot,a),c=b.elem,d=b.key;return c[d]},setAt:function(a,b,c){var d=h(this.snapshot,a),e=d.elem,f=d.key,g={p:a};if(e.constructor===Array)g.li=b,typeof e[f]!="undefined"&&(g.ld=e[f]);else{if(typeof e!="object")throw new Error("bad path");g.oi=b,typeof e[f]!="undefined"&&(g.od=e[f])}return this.submitOp([g],c)},removeAt:function(a,b){var c,d=h(this.snapshot,a),e=d.elem,f=d.key;if(typeof e[f]=="undefined")throw new Error("no element at that path");c={p:a};if(e.constructor===Array)c.ld=e[f];else{if(typeof e!="object")throw new Error("bad path");c.od=e[f]}return this.submitOp([c],b)},insertAt:function(a,b,c,d){var e=h(this.snapshot,a),f=e.elem,g=e.key,i={p:a.concat(b)};return f[g].constructor===Array?i.li=c:typeof f[g]=="string"&&(i.si=c),this.submitOp([i],d)},moveAt:function(a,b,c,d){var e=[{p:a.concat(b),lm:c}];return this.submitOp(e,d)},addAt:function(a,b,c){var d=[{p:a,na:b}];return this.submitOp(d,c)},deleteTextAt:function(a,b,c,d){var e=h(this.snapshot,a),f=e.elem,g=e.key,i=[{p:a.concat(c),sd:f[g].slice(c,c+b)}];return this.submitOp(i,d)},addListener:function(a,b,c){var d={path:a,event:b,cb:c};return this._listeners.push(d),d},removeListener:function(a){var b=this._listeners.indexOf(a);return b<0?!1:(this._listeners.splice(b,1),!0)},_register:function(){return this._listeners=[],this.on("change",function(a){var b,c,d,e,f,g,h,i,j,k,l=[];for(h=0,i=a.length;h<i;h++){b=a[h];if(b.na!==void 0||b.si!==void 0||b.sd!==void 0)continue;f=[],k=this._listeners;for(d=0,j=k.length;d<j;d++){e=k[d],c={p:e.path,na:0},g=this.type.transformComponent([],c,b,"left");if(g.length===0)f.push(d);else{if(g.length!==1)throw new Error("Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components.");e.path=g[0].p}}f.sort(function(a,b){return b-a}),l.push(function(){var a,b,c=[];for(a=0,b=f.length;a<b;a++)d=f[a],c.push(this._listeners.splice(d,1));return c}.call(this))}return l}),this.on("remoteop",function(a){var b,c,d,e,g,h,i,j,k,l=[];for(j=0,k=a.length;j<k;j++)b=a[j],h=b.na===void 0?b.p.slice(0,b.p.length-1):b.p,l.push(function(){var a,j,k,l,m=this._listeners,n=[];for(a=0,j=m.length;a<j;a++){k=m[a],i=k.path,g=k.event,c=k.cb;if(f(i,h))switch(g){case"insert":b.li!==void 0&&b.ld===void 0?n.push(c(b.p[b.p.length-1],b.li)):b.oi!==void 0&&b.od===void 0?n.push(c(b.p[b.p.length-1],b.oi)):b.si!==void 0?n.push(c(b.p[b.p.length-1],b.si)):n.push(void 0);break;case"delete":b.li===void 0&&b.ld!==void 0?n.push(c(b.p[b.p.length-1],b.ld)):b.oi===void 0&&b.od!==void 0?n.push(c(b.p[b.p.length-1],b.od)):b.sd!==void 0?n.push(c(b.p[b.p.length-1],b.sd)):n.push(void 0);break;case"replace":b.li!==void 0&&b.ld!==void 0?n.push(c(b.p[b.p.length-1],b.ld,b.li)):b.oi!==void 0&&b.od!==void 0?n.push(c(b.p[b.p.length-1],b.od,b.oi)):n.push(void 0);break;case"move":b.lm!==void 0?n.push(c(b.p[b.p.length-1],b.lm)):n.push(void 0);break;case"add":b.na!==void 0?n.push(c(b.na)):n.push(void 0);break;default:n.push(void 0)}else if((e=this.type.commonPath(h,i))!=null)if(g==="child op"){if(h.length===(l=i.length)&&l===e)throw new Error("paths match length and have commonality, but aren't equal?");d=b.p.slice(e+1),n.push(c(d,b))}else n.push(void 0);else n.push(void 0)}return n}.call(this));return l})}}})).call(this)
346  webclient/json.uncompressed.js
@@ -5,51 +5,44 @@
5 5
 */
6 6
 var WEB = true;
7 7
 ;
8  
-  var SubDoc, clone, depath, exports, isArray, json, pathEquals, text, traverse;
9  
-  var __slice = Array.prototype.slice;
  8
+  var SubDoc, clone, depath, exports, isArray, json, pathEquals, text, traverse,
  9
+    __slice = Array.prototype.slice;
  10
+
10 11
   exports = window['sharejs'];
  12
+
11 13
   if (typeof WEB !== "undefined" && WEB !== null) {
12 14
     text = exports.types.text;
13 15
   } else {
14 16
     text = require('./text');
15 17
   }
  18
+
16 19
   json = {};
  20
+
17 21
   json.name = 'json';
  22
+
18 23
   json.create = function() {
19 24
     return null;
20 25
   };
  26
+
21 27
   json.invertComponent = function(c) {
22 28
     var c_;
23 29
     c_ = {
24 30
       p: c.p
25 31
     };
26  
-    if (c.si !== void 0) {
27  
-      c_.sd = c.si;
28  
-    }
29  
-    if (c.sd !== void 0) {
30  
-      c_.si = c.sd;
31  
-    }
32  
-    if (c.oi !== void 0) {
33  
-      c_.od = c.oi;
34  
-    }
35  
-    if (c.od !== void 0) {
36  
-      c_.oi = c.od;
37  
-    }
38  
-    if (c.li !== void 0) {
39  
-      c_.ld = c.li;
40  
-    }
41  
-    if (c.ld !== void 0) {
42  
-      c_.li = c.ld;
43  
-    }
44  
-    if (c.na !== void 0) {
45  
-      c_.na = -c.na;
46  
-    }
  32
+    if (c.si !== void 0) c_.sd = c.si;