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 | 1
1
1
3403
3403
3403
16
3387
3355
32
32
29
3
32
32
1
106
106
106
106
106
106
1
1249
1249
1249
1249
984
984
1249
13075
13075
1249
223
223
223
223
223
223
223
1
222
222
1
221
221
221
3462
947
947
947
947
937
10
947
947
958
958
958
1
957
2
955
955
955
955
18
18
15
3
48
48
48
10
10
5
5
6
5
5
62
25
15
1 | var Rpc = require('./rpc');
var WasabiError = require('./wasabi_error');
/**
* Extract the name of a function using .wsbFnName, .name, or parsing .toString()
*/
function _parseFunctionName(fn) {
var match;
var name;
if (typeof fn !== 'function') {
return false;
}
if (typeof fn.wsbFnName === 'string') {
return fn.wsbFnName;
}
/**
* Another apparent toString parsing hack that isn't.
* Function.prototype.toString is specified to return a string
* representation of the function with the syntax of a function declaration
* since ECMAScript 1 (15.3.4.2)
*/
match = /function\s*(\w*)/.exec(fn.toString());
if (match && match[1] && match[1].length > 0) {
name = match[1];
} else {
name = fn.name;
}
fn.wsbFnName = name;
return fn.wsbFnName;
}
/**
* Manages the registration of classes for consistent
* serialization/unserialization
* @class Registry
* @constructor
*/
function Registry() {
// hash <-> klass
this.klassToHash = {};
this.hashToKlass = {};
// hash <-> RPC
this.rpcToHash = {};
this.hashToRpc = {};
// objects by serial number
this.objects = {};
this.nextSerialNumber = 1;
}
Registry.prototype = {
constructor: Registry,
/**
* Return a unique hash from one or two functions suitable for entering into
* the registry's klass or rpc tables. Note that the supplied functions must
* have a valid `name` property
* @method hash
* @param {Function} fn1 The first function to hash
* @param {Function} fn2 The (optional) second function to hash
* @return {Number} The XOR hash of the characters of
* klass.prototype.constructor.name
*/
hash: function (fn1, fn2) {
var result = 0;
var name = _parseFunctionName(fn1);
var i;
if (fn2) {
var name2 = _parseFunctionName(fn2);
name += '.' + name2;
}
// compute a 16 bit hash using wrap-around bitshifting and XOR
for (i = 0; i < name.length; i++) {
// save the lowest bit, and wrap it around to the front, and shift
// all other bits down by 1, filling with zeros from the left
result = ((result & 1) << 15) + (result >>> 1);
// then XOR the current character into the hash
result ^= name.charCodeAt(i);
}
return result;
},
/**
* 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
* @param {Wasabi} instance The Wasabi instance to invoke the RPC through
*/
addClass: function (klass, instance) {
var k;
var fn;
var keyOfRealFunction;
var hash;
var args;
var name = _parseFunctionName(klass);
// wasabi requires the `name` property to be set and valid
if (typeof name !== 'string' || name.length < 1) {
throw new WasabiError('Attempt to add anonymous class. Give it a name with "function NAME () { ... }"');
}
hash = this.hash(klass);
// detect hash collisions (library error) or class redefinition (user error)
if (this.hashToKlass[hash] !== undefined) {
throw new WasabiError('Invalid attempt to redefine class ' + klass.name + ' with hash ' + hash);
}
// add the klass to the hash map
this.klassToHash[klass] = hash;
this.hashToKlass[hash] = klass;
// register this class's RPCs
for (k in klass.prototype) {
// RPCs are functions starting with rpc, c2s, or s2c, and not ending
// with Args
if (typeof klass.prototype[k] === 'function' && !(/Args$/.test(k)) && /^(rpc|c2s|s2c)/.test(k)) {
fn = klass.prototype[k];
// find the Args function if it exists (for rpcFoo this would be
// rpcFooArgs)
args = klass.prototype[k + 'Args'];
// if this class was already added to a different Wasabi
// instance, we'll use the real method instead of the
// replacement we create later in this function. This usually
// only happens in the test suite
keyOfRealFunction = 'wsbReal_' + k;
if (klass.prototype[keyOfRealFunction] !== undefined) {
fn = klass.prototype[keyOfRealFunction];
} else {
klass.prototype[keyOfRealFunction] = fn;
}
// use property name if the function name can't be found
fn.wsbFnName = _parseFunctionName(fn) || k;
// replace the definition on the klass's prototype with the
// invocation stub generated by mkRpc
klass.prototype[k] = this.mkRpc(klass, fn, args, instance);
}
}
},
/**
* Create an RPC from the supplied procedure function and serialize
* function. `instance` must be a {{#crossLink "Wasabi"}}{{/crossLink}}
* instance
* @method mkRpc
* @param {Function} klass The klass this rpc is associated with, or `false`
* for static RPCs
* @param {Function} fn The local function to call when the RPC is invoked
* on a remote host
* @param {Function} serialize A serialize function describing the arguments
* used by this RPC
* @param {Wasabi} instance The Wasabi instance to invoke this RPC through
* @return {Function} The function you should call remotely to invoke the
* RPC on a connection
*/
mkRpc: function (klass, fn, serialize, instance) {
var hash = this.hash(klass, fn);
var rpc;
// detect anonymous static RPCs
if (typeof fn.wsbFnName !== 'string' || fn.wsbFnName.length < 1) {
throw new WasabiError('Attempt to add anonymous RPC. Give it a name with "function NAME () { ... }"');
}
// detect hash collisions (library error) or class redefinition (user error)
if (this.hashToRpc[hash] !== undefined) {
throw new WasabiError('Invalid attempt to redefine RPC ' + (klass ? klass.name + '#' : '') + fn.name + ' with hash ' + hash);
}
// create a new RPC definition
rpc = new Rpc(fn, klass, serialize);
// update the hash <-> rpc mapping
this.rpcToHash[rpc] = hash;
this.hashToRpc[hash] = rpc;
// if klass is truthy, create a method RPC. `this` will refer to the
// object which the RPC is invoked on locally, and we should send the
// invocation through instance which the object belongs to. otherwise
// the method is static, and we should send the RPC through the wasabi
// instance it was defined on
return function () {
var args = Array.prototype.slice.call(arguments);
if (klass) {
this.wsbInstance._invokeRpc(rpc, this, args);
} else {
instance._invokeRpc(rpc, false, args);
}
};
},
/**
* Register an instance of a klass
* @method addObject
* @param {NetObject} obj The object to add to the registry
* @param {Nunmber} serial The serial number to assign to this object. If
* falsy, the nextSerialNumber will be used
*/
addObject: function (obj, serial) {
obj.wsbSerialNumber = serial || this.nextSerialNumber;
this.nextSerialNumber += 1;
this.objects[obj.wsbSerialNumber] = obj;
},
/**
* Remove an instance of a klass
* @method removeObject
* @param {NetObject|number} arg The object or serial number of an object to
* remove from the registry
*/
removeObject: function (arg) {
var k;
if (typeof arg === 'number') {
delete this.objects[arg];
} else {
for (k in this.objects) {
if (this.objects.hasOwnProperty(k) && this.objects[k] === arg) {
delete this.objects[k];
return;
}
}
}
},
/**
* Get an instance of a klass by serial number
* @method getObject
*/
getObject: function (serial) {
return this.objects[serial];
},
/**
* Get the function/constructor/klass represented by the given hash
* @method getClass
*/
getClass: function (hash) {
return this.hashToKlass[hash];
},
/**
* get the RPC function associated with the hash
* @method getRpc
*/
getRpc: function (hash) {
return this.hashToRpc[hash];
}
};
module.exports = Registry; |