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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734 | 1
1
1
1
1
1
1
1
105
105
105
105
105
105
105
105
105
220
24
24
10
7
69
69
69
3
3
2
1
1
69
69
69
5
5
7
4
4
74
74
109
38
71
138
68
2
57
57
31
31
31
1
30
30
37
37
23
23
23
23
23
1
22
22
22
22
22
22
68
68
68
101
33
33
68
37
19
36
67
67
67
74
7
7
67
34
34
34
5
5
5
5
34
69
69
126
57
69
37
37
37
31
30
36
36
18
18
18
18
18
2
1
17
1
16
15
13
15
2
15
13
35
22
16
25
25
106
106
106
24
24
24
24
24
24
24
24
13
13
13
13
13
13
13
1
12
12
12
12
12
12
1
11
67
67
67
124
57
67
106
106
106
106
106
106
106
106
68
68
68
68
124
33
68
68
98
7
68
68
68
68
68
106
105
105
67
67
105
105
105
105
172
172
52
120
102
102
102
102
1
1
1
102
102
105
105
105
105
105
105
105
105
105
945
840
105
1
1 | var Bitstream = require('./bitstream');
var Connection = require('./connection');
var Group = require('./group');
var Registry = require('./registry');
var Rpc = require('./rpc');
var WasabiError = require('./wasabi_error');
var events = require('events');
/**
* Named and exported function that would otherwise be an IIFE. Used to
* instantiate a second Wasabi module for use in tests (to simulate a remote
* client)
* @method makeWasabi
* @static
*/
function makeWasabi() {
var iota; // for enums
// packet control constants
iota = 0xFFFF;
var WSB_SEPARATOR = iota;
var WSB_SECTION_GHOSTS = --iota;
var WSB_SECTION_REMOVED_GHOSTS = --iota;
var WSB_SECTION_UPDATES = --iota;
var WSB_SECTION_RPC = --iota;
var WSB_PACKET_STOP = --iota;
/**
* Facade class for interacting with Wasabi.
*
* Note that Wasabi implements the Node.js `events.EventEmitter` interface
* for event handling, allowing use of `on`, `once`, `removeListener` and
* friends. See the related [Node.js events.EventEmitter docs](http://nodejs.org/api/events.html#events_class_events_eventemitter) for
* event handling methods.
*
* @class Wasabi
*/
var Wasabi = {
Bitstream: Bitstream,
Connection: Connection,
Registry: Registry,
Rpc: Rpc,
makeWasabi: makeWasabi,
servers: [],
clients: [],
_rpcQueue: [],
/**
* Register a class with Wasabi, allowing it to transmit instances of
* this class through a Connection
* @method addClass
* @param {Function} klass The constructor of the class to add
*/
addClass: function (klass) {
this.registry.addClass(klass, this);
},
/**
* Register an instance of a klass, which can then be sent to
* connected clients as needed (based on the results of their
* `scopeCallback`s).
*
* *Note: This method should only be called manually on
* authoritative peers (i.e. server-side).* Wasabi clients will
* automatically add instances to the Registry when their ghosts
* are unpacked
* @method addObject
* @param {NetObject} obj The object to add to the registry
* @param {Number} serial The serial number to assign to this object. If
* falsy, the nextSerialNumber will be used
*/
addObject: function (obj) {
this.registry.addObject(obj);
obj.wsbInstance = this;
},
/**
* Unregister an instance of a klass
* @method removeObject
* @param {mixed} arg Either a NetObject or a serial number to be
* removed from the registry
*/
removeObject: function (arg) {
this.registry.removeObject(arg);
},
/**
* Create an RPC from the supplied procedure and serialize functions.
* @method mkRpc
* @param {Function} fn The local function to call when the RPC is
* invoked on a remote host
* @param {Function} opt_serialize An optional serialize function
* describing the arguments used by this RPC
* @return {Function} The function you should call remotely to invoke
* the RPC on a connection
*/
mkRpc: function (fn, opt_serialize) {
return this.registry.mkRpc(false, fn, opt_serialize, this);
},
/**
* Attach to a server connected through the socket object
* @method addServer
* @param {Socket} sock The socket object used to communicate with the
* new server
* @return {Connection} The newly created Connection object
*/
addServer: function (sock) {
var conn = new Wasabi.Connection(sock, true, false);
this.servers.push(conn);
return conn;
},
/**
* Remove a server by its socket object. `sock` must be strictly equal
* (`===`) to the original socket.
* @method removeServer
* @param {Socket} sock The socket object originally passed to addServer
*/
removeServer: function (sock) {
var i;
for (i = 0; i < this.servers.length; i++) {
if (this.servers[i]._socket === sock) {
this.servers.splice(i, 1);
return;
}
}
},
/**
* Attach a client connected through the given socket object. Currently
* this must be a WebSocket or socket.io socket, or something that is
* API compatible (i.e. has an `onmessage` callback and a `send`
* method).
* @method addClient
* @param {Socket} client The socket object used to communicate with the
* new client
* @param {Function} scopeCallback See {{#crossLink
* "Connection"}}{{/crossLink}}
* @return {Connection} The newly created Connection object
*/
addClient: function (client, scopeCallback) {
var conn = new Wasabi.Connection(client, false, true, scopeCallback);
this.clients.push(conn);
return conn;
},
/**
* Remove a client by its socket object. `sock` must be strictly equal
* (`===`) to the original socket.
* @method removeClient
* @param {Socket} sock The socket object originally passed to addClient
*/
removeClient: function (sock) {
var i;
for (i = 0; i < this.clients.length; i++) {
if (this.clients[i]._socket === sock) {
this.clients.splice(i, 1);
return;
}
}
},
/**
* Process the incoming and outgoing data for all connected clients and
* servers. This is typically called in your game's update loop
* @method processConnections
*/
processConnections: function () {
var k;
// process server connections
for (k in this.servers) {
if (this.servers.hasOwnProperty(k)) {
this._processConnection(this.servers[k]);
}
}
// process client connections
for (k in this.clients) {
if (this.clients.hasOwnProperty(k)) {
this._processConnection(this.clients[k]);
}
}
},
/**
* Create a new visibility group
* @method createGroup
* @return {Group} The new group
*/
createGroup: function () {
return new Group();
},
/**
* Packs update data for `obj` into `bs`
* @method _packUpdate
* @param {Object} obj The object to pack
* @param {BitStream} bs The bitstream to pack into
* @private
*/
_packUpdate: function (obj, bs) {
bs.writeUInt(obj.wsbSerialNumber, 16);
bs.pack(obj);
},
/**
* Unpacks update data from `bs`
* @method _unpackUpdate
* @param {BitStream} bs The bitstream to unpack from
* @private
*/
_unpackUpdate: function (bs) {
var serial = bs.readUInt(16);
var obj = this.registry.getObject(serial);
if (!obj) {
throw new WasabiError('Received update for unknown object ' + serial);
}
bs.unpack(obj);
return obj;
},
/**
* Packs data needed to instantiate a replicated version of obj
* @method _packGhost
* @param {Object} obj The object to pack
* @param {BitStream} bs The bitstream to pack into
* @private
*/
_packGhost: function (obj, bs) {
bs.writeUInt(this.registry.hash(obj.constructor), 16);
bs.writeUInt(obj.wsbSerialNumber, 16);
},
/**
* Unpacks a newly replicated object from Bitstream
* @method _unpackGhost
* @param {Bitstream} bs The target bitstream
* @private
*/
_unpackGhost: function (bs) {
var hash = bs.readUInt(16);
var T = this.registry.getClass(hash);
var serial = bs.readUInt(16);
var obj;
if (!T) {
throw new WasabiError('Received ghost for unknown class with hash ' + hash);
}
obj = new T();
obj.wsbInstance = this;
obj.wsbIsGhost = true;
this.registry.addObject(obj, serial);
/**
* Fired client-side when a ghost (the remote counterpart) of an
* object is created. This occurs when the scope callback for this
* client (on the server) returns an object when it did not
* previously.
*
* The `obj` will be a newly created instance of the class every
* time this event is emitted, even when subsequent emissions refer
* to the same server-side instance. That is, Wasabi created a brand
* new object every time it creates a ghost.
*
* Note that this event can be emitted multiple times per object, if
* object comes in and out of scope.
*
* @event clientGhostCreate
* @param {Object} obj The newly created ghost
*/
this.emit('clientGhostCreate', obj);
return obj;
},
/**
* Packs ghosts for needed objects into `bs`
* @method _packGhosts
* @private
* @param {Array} objects An Array or map of objects to pack ghosts for
* @param {Bitstream} bs The target Bitstream
*/
_packGhosts: function (objects, bs) {
var i;
var obj;
for (i in objects) {
if (objects.hasOwnProperty(i)) {
obj = objects[i];
this._packGhost(obj, bs);
}
}
bs.writeUInt(WSB_SEPARATOR, 16);
},
/**
* Unpack all needed ghosts from `bs`
* @method _unpackGhosts
* @private
* @param {Bitstream} bs The source Bitstream
*/
_unpackGhosts: function (bs) {
while (bs.peekUInt(16) !== WSB_SEPARATOR) {
this._unpackGhost(bs);
}
// burn off the separator
bs.readUInt(16);
},
/**
* Packs removed ghosts for `objects` into `bs`
* @method _packRemovedGhosts
* @private
* @param {Object} objects An Array or map of objects to remove
* @param {Bitstream} bs The target Bitstream
*/
_packRemovedGhosts: function (objects, bs) {
var k;
var obj;
for (k in objects) {
if (objects.hasOwnProperty(k)) {
obj = objects[k];
bs.writeUInt(obj.wsbSerialNumber, 16);
}
}
bs.writeUInt(WSB_SEPARATOR, 16);
},
/**
* Unpack all removed ghosts from bs. An object with its ghost unpacked
* in this way will be removed from the local Wasabi's registry
* @method _unpackRemovedGhosts
* @private
* @param {Bitstream} bs The source Bitstream
*/
_unpackRemovedGhosts: function (bs) {
var serial;
var obj;
while (bs.peekUInt(16) !== WSB_SEPARATOR) {
serial = bs.readUInt(16);
obj = this.registry.getObject(serial);
/**
* Fired client-side when a ghost (the remote counterpart) of an
* object is about to be destroyed. This occurs when the scope
* callback for this client (on the server) does not return the
* object after it did previously.
*
* Although Wasabi can not acutally "destroy" the object (since
* JavaScript has no destructors), the particular instance will
* never be referred to be Wasabi again.
*
* Note that this event can be emitted multiple times per
* object, if object comes in and out of scope.
*
* @event clientGhostDestroy
* @param {Object} obj The ghost which is about to be destroyed
*/
this.emit('clientGhostDestroy', obj);
this.removeObject(serial);
}
// burn off the separator
bs.readUInt(16);
},
/**
* Pack the given list of object update data into bs
* @method _packUpdates
* @private
* @param {Object} list An Array or map of objects to pack updates for
* @param {Bitstream} bs The target Bitstream
*/
_packUpdates: function (list, bs) {
var k;
for (k in list) {
if (list.hasOwnProperty(k)) {
this._packUpdate(list[k], bs);
}
}
bs.writeUInt(WSB_SEPARATOR, 16);
},
/**
* Unpack the given list of objects (with update data) from bs
* @method _unpackUpdates
* @private
* @param {Bitstream} bs The source Bitstream
*/
_unpackUpdates: function (bs) {
var list = [];
var obj;
while (bs.peekUInt(16) !== WSB_SEPARATOR) {
obj = this._unpackUpdate(bs);
list.push(obj);
}
// burn off the separator
bs.readUInt(16);
return list;
},
/**
* Pack an RPC invocation to the appropriate connections
* @method _invokeRpc
* @private
* @param {Rpc} rpc the rpc to invoke
* @param {NetObject} obj the obj to use as the context the invocation,
* or false for static invocations
* @param {Array} args the arguments to the rpc, followed by an optional
* list of connections to emit the invocation to. If no connections
* are specified, the invocation is emitted to all connections
*/
_invokeRpc: function (rpc, obj, args) {
var i;
var k;
var invocation;
// Extract connection list from supplied args
var conns = args.splice(rpc._fn.length, args.length - rpc._fn.length);
// Note that RPCs expect exactly the number of arguments specified
// in the original function's definition, so any arguments passed
// after that must be Connections to send the invocation on
for (i = 0; i < conns.length; i++) {
if (!(conns[i] instanceof Connection)) {
throw new WasabiError('Expected connection but got ' + conns[i] + '. Did you pass too many arguments to ' + rpc._fn.wasabiFnName + '?');
}
}
// check for argument underflow
if (args.length < rpc._fn.length) {
throw new WasabiError('Too few arguments passed to ' + rpc._fn.wasabiFnName);
}
// if no Connections are specified, send the invocation to either
// servers, clients, or both, depending on the RPC's definition (see
// the Rpc constructor for details)
if (conns.length === 0) {
// process server connections
if (rpc._toServer) {
for (k in this.servers) {
if (this.servers.hasOwnProperty(k)) {
conns.push(this.servers[k]);
}
}
}
// process client connections
if (rpc._toClient) {
for (k in this.clients) {
if (this.clients.hasOwnProperty(k)) {
conns.push(this.clients[k]);
}
}
}
}
// add the invocation to the proper connections' rpc queues
for (i = 0; i < conns.length; i++) {
invocation = {
rpc: rpc,
args: args,
obj: obj,
bs: conns[i]._sendBitstream
};
conns[i]._rpcQueue.push(invocation);
}
},
/**
* Pack all RPC invocations in the specified `Connection`'s queue.
* @method _packRpcs
* @private
* @param {Connection} conn The connection to pack RPC invocations for
*/
_packRpcs: function (conn) {
var i;
var invocation;
for (i = 0; i < conn._rpcQueue.length; i++) {
invocation = conn._rpcQueue[i];
conn._sendBitstream.writeUInt(WSB_SECTION_RPC, 16);
this._packRpc(invocation.rpc, invocation.args, invocation.obj, invocation.bs);
}
},
/**
* Pack a call to a registered RP and the supplied arguments into bs
* @method _packRpc
* @private
* @param {Rpc} rpc The RPC to pack
* @param {Array} args The arguments to be serialized into this
* invocation
* @param {NetObject} obj The NetObject to apply the RPC to (or falsy
* for "static" RPC invocation
* @param {Bitstream} bs The target Bitstream
*/
_packRpc: function (rpc, args, obj, bs) {
rpc._populateKeys(args);
bs.writeUInt(obj ? obj.wsbSerialNumber : 0, 16);
bs.writeUInt(this.registry.hash(rpc._klass, rpc._fn), 16);
args.serialize = rpc._serialize;
bs.pack(args);
},
/**
* Unpack and execute a call to a registered RP using the supplied
* arguments from bs
* @method _unpackRpc
* @private
* @param {Bitstream} bs The source Bitstream
* @param {Connection} conn The connection this RPC was invoked from
*/
_unpackRpc: function (bs, conn) {
var serialNumber = bs.readUInt(16);
var hash = bs.readUInt(16);
var obj = this.registry.getObject(serialNumber);
var rpc;
var args;
// look up the Rpc by the hash
rpc = this.registry.getRpc(hash);
if (!rpc) {
throw new WasabiError('Unknown RPC with hash ' + hash);
}
// unpack the arguments
args = [];
args.serialize = rpc._serialize;
bs.unpack(args);
rpc._populateIndexes(args);
// add the connection this invocation was received through to the
// argument list
args.push(conn);
if (serialNumber && !obj) {
// a serial number was specified, but the object wasn't found
// this can happen in normal operation if a server removes an
// object in the same frame that a client calls an RPC on it
return;
}
// invoke the real function
rpc._fn.apply(obj, args);
},
/**
* Returns a clone of the registry's object table. used as a fallback
* when no _scopeCallback is specified for a connection
* @method _getAllObjects
* @private
*/
_getAllObjects: function () {
var result = {};
var k;
for (k in this.registry.objects) {
if (this.registry.objects.hasOwnProperty(k)) {
result[k] = this.registry.objects[k];
}
}
return result;
},
/**
* Receive, process, and transmit data as needed for this connection
* @method _processConnection
* @private
*/
_processConnection: function (conn) {
var k;
var data;
var newObjects;
var newlyVisibleObjects;
var newlyInvisibleObjects;
var oldObjects;
var section;
// connections with ghostTo set (i.e. clients)
if (conn._ghostTo) {
// get list of objects which are visible this frame
// if the connection has no groups attached, all objects are visible
newObjects = conn._groups.length ? conn.getObjectsInGroups() : this._getAllObjects();
// list of objects which were visible last frame
oldObjects = conn._visibleObjects;
// an object in newObjects, but not in oldObjects must be newly
// visible this frame
newlyVisibleObjects = {};
for (k in newObjects) {
if (newObjects.hasOwnProperty(k) && oldObjects[k] === undefined) {
newlyVisibleObjects[k] = newObjects[k];
}
}
// an object in oldObjects, but not in newObjects must be newly
// invisible this frame
newlyInvisibleObjects = {};
for (k in oldObjects) {
if (oldObjects.hasOwnProperty(k) && newObjects[k] === undefined) {
newlyInvisibleObjects[k] = oldObjects[k];
}
}
// set the connection's visible object collection
conn._visibleObjects = newObjects;
// pack ghosts for newly visible objects
conn._sendBitstream.writeUInt(WSB_SECTION_GHOSTS, 16);
this._packGhosts(newlyVisibleObjects, conn._sendBitstream);
// pack updates for all objects visible this frame
conn._sendBitstream.writeUInt(WSB_SECTION_UPDATES, 16);
this._packUpdates(newObjects, conn._sendBitstream);
}
// pack all rpc invocations sent to this connection
this._packRpcs(conn);
conn._rpcQueue = [];
if (conn._ghostTo) {
// pack ghost removals for newly invisible objects
conn._sendBitstream.writeUInt(WSB_SECTION_REMOVED_GHOSTS, 16);
this._packRemovedGhosts(newlyInvisibleObjects, conn._sendBitstream);
}
// write a packet terminator
conn._sendBitstream.writeUInt(WSB_PACKET_STOP, 16);
// now we'll process the incoming data on this connection
conn._receiveBitstream._index = 0;
/**
* Fired before Wasabi processes incoming data. Useful for
* measuring data transmission statistics.
*
* Note that this event fires during `processConnections`, and is
* not meant to replace the `onmessage` handler for typical
* WebSockets or socket.io sockets.
*
* @event receive
* @param {Connection} conn The connection being processed
* @param {String} data The data being received over the connection
*/
this.emit('receive', conn, conn._receiveBitstream.toChars());
while (conn._receiveBitstream.bitsLeft() > 0) {
section = conn._receiveBitstream.readUInt(16);
if (section === WSB_PACKET_STOP) {
// when a packet is terminated we must consume
// the bit padding from Bitstream#fromChars via
// the Bitstream#align method
conn._receiveBitstream.align();
} else {
// otherwise invoke the appropriate unpack
// function via the _sectionMap
this._sectionMap[section].call(this, conn._receiveBitstream, conn);
}
}
/**
* Fired before Wasabi sends data over a connection. Useful for
* measuring data transmission statistics.
*
* @event send
* @param {Connection} conn The connection being processed
* @param {String} data The data being sent over the connection
*/
data = conn._sendBitstream.toChars();
this.emit('send', conn, data);
try {
// send the actual data
conn._socket.send(data);
} catch (e) {
/**
* Fired when Wasabi receives an error while sending data over a
* connection. Note that Wasabi will remove the connection from
* its list of clients and servers immediately after emitting
* this event.
*
* An event is used in order to give user code a chance to react
* to the error without interupting the processing of other
* connections within the same `processConnections` call.
*
* @event sendError
* @param {Connection} conn The connection which generated the
* error.
* @param {Error} e The original error
*/
this.emit('sendError', conn, e);
this.removeClient(conn._socket);
this.removeServer(conn._socket);
}
// clear the bit streams
conn._sendBitstream.empty();
conn._receiveBitstream.empty();
}
};
// a simple section marker -> method map
Wasabi._sectionMap = {};
Wasabi._sectionMap[WSB_SECTION_GHOSTS] = Wasabi._unpackGhosts;
Wasabi._sectionMap[WSB_SECTION_REMOVED_GHOSTS] = Wasabi._unpackRemovedGhosts;
Wasabi._sectionMap[WSB_SECTION_UPDATES] = Wasabi._unpackUpdates;
Wasabi._sectionMap[WSB_SECTION_RPC] = Wasabi._unpackRpc;
Wasabi.registry = new Registry();
// mixin a Node event emitter
events.EventEmitter.call(Wasabi);
var k;
for (k in events.EventEmitter.prototype) {
if (events.EventEmitter.prototype.hasOwnProperty(k)) {
Wasabi[k] = events.EventEmitter.prototype[k];
}
}
return Wasabi;
}
var Wasabi = makeWasabi();
module.exports = Wasabi; |