1 | var Promise = require('bluebird');
|
2 | var logger = require('debug');
|
3 | var debug = logger('socket.io-rpc');
|
4 | var lldebug = logger('socket.io-rpc:low-level');
|
5 | var _ = require('lodash');
|
6 | var path = require('path');
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | function createServer(ioP, expApp) {
|
13 |
|
14 | var runDate = new Date();
|
15 | var io;
|
16 |
|
17 |
|
18 | var channelTemplates = {};
|
19 | var channelTemplatesSize = 0;
|
20 | var clientKnownChannels = {};
|
21 |
|
22 |
|
23 | var deferreds = [];
|
24 | var serverChannels = {};
|
25 | var clientChannels = {};
|
26 | var getClientChannel = function (id, name) {
|
27 | if (!clientChannels.hasOwnProperty(id)) {
|
28 | lldebug("creating an object for client channels for ID " + id);
|
29 | clientChannels[id] = {};
|
30 | }
|
31 | if (!clientChannels[id].hasOwnProperty(name)) {
|
32 | lldebug("creating a client channel for client with ID " + id + " and channel name " + name);
|
33 | clientChannels[id][name] = {};
|
34 | }
|
35 | return clientChannels[id][name];
|
36 | };
|
37 |
|
38 | var getChannelNames = function () {
|
39 | var names = [];
|
40 | for(var channel in serverChannels){
|
41 | names.push(channel);
|
42 | }
|
43 | return names;
|
44 | };
|
45 |
|
46 |
|
47 | |
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | var RpcChannel = function(name, toExpose) {
|
54 | this.fns = toExpose;
|
55 |
|
56 | |
57 |
|
58 |
|
59 | this.fnNames = [];
|
60 |
|
61 | for (var fnName in toExpose) {
|
62 | this.fnNames.push(fnName);
|
63 | }
|
64 | Object.freeze(toExpose);
|
65 |
|
66 |
|
67 | for (var tplId in channelTemplates) {
|
68 | if (_.isEqual(channelTemplates[tplId], this.fnNames)) {
|
69 | this.tplId = Number(tplId);
|
70 | }
|
71 | }
|
72 |
|
73 | if (!_.isNumber(this.tplId)) {
|
74 | var index = channelTemplatesSize + 1;
|
75 | channelTemplates[index] = this.fnNames;
|
76 | channelTemplatesSize = index;
|
77 | this.tplId = index;
|
78 | }
|
79 |
|
80 | this._socket = io.of('/rpc/' + name);
|
81 |
|
82 | this._socket.on('connection', function(socket) {
|
83 | var invocationRes = function(data) {
|
84 | if (toExpose.hasOwnProperty(data.fnName) && typeof toExpose[data.fnName] === 'function') {
|
85 | var retVal;
|
86 | try {
|
87 | retVal = toExpose[data.fnName].apply(socket, data.args);
|
88 | } catch (e) {
|
89 |
|
90 | console.error('RPC method ' + data.fnName + ' on channel ' + name + ': ', e);
|
91 | retVal = e;
|
92 | }
|
93 |
|
94 | if (retVal && typeof retVal.then === 'function') {
|
95 | retVal.then(function (asyncRetVal) {
|
96 | socket.emit('resolve', { Id: data.Id, value: asyncRetVal });
|
97 | }, function (error) {
|
98 | if (error instanceof Error) {
|
99 | error = error.toString();
|
100 | }
|
101 | socket.emit('reject', { Id: data.Id, reason: error });
|
102 |
|
103 | });
|
104 | } else {
|
105 |
|
106 | if (retVal instanceof Error) {
|
107 | socket.emit('reject', { Id: data.Id, reason: retVal.toString() });
|
108 | } else {
|
109 | socket.emit('resolve', { Id: data.Id, value: retVal });
|
110 | }
|
111 | }
|
112 |
|
113 | } else {
|
114 | socket.emit('reject', {Id: data.Id, reason: 'no such function has been exposed: ' + data.fnName });
|
115 | }
|
116 | };
|
117 |
|
118 | socket.on('call', invocationRes);
|
119 |
|
120 | });
|
121 |
|
122 | return this;
|
123 | };
|
124 |
|
125 | var invocationCounter = 0;
|
126 | var endCounter = 0;
|
127 | var callToClientEnded = function (Id) {
|
128 | if (deferreds[Id]) {
|
129 | delete deferreds[Id];
|
130 | endCounter++;
|
131 | if (endCounter == invocationCounter) {
|
132 | invocationCounter = 0;
|
133 | endCounter = 0;
|
134 | }
|
135 | } else {
|
136 |
|
137 | console.error("Deferred Id " + Id + " was resolved/rejected more than once, this should not occur.");
|
138 | }
|
139 | };
|
140 |
|
141 | var rpcInstance = {
|
142 | |
143 |
|
144 |
|
145 | get channels() {
|
146 | return serverChannels;
|
147 | },
|
148 | |
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | expose: function (name, toExpose, urlForModule) {
|
156 | if (!urlForModule) {
|
157 | urlForModule = '/rpc/' + name + '.js';
|
158 | }
|
159 | var fnNames = Object.keys(toExpose);
|
160 |
|
161 | expApp.get(urlForModule, function (req, res){
|
162 | res.type('application/javascript; charset=utf-8');
|
163 | var fullUrl = "'" + req.protocol + '://' + req.get('host') + "'";
|
164 |
|
165 | var clSideScript = 'var fns = ' + JSON.stringify(fnNames) + '\n' + '' +
|
166 | 'var chnl = require("rpc/export-channel")("' + name + '", fns, ' + fullUrl + ') \n' +
|
167 | 'module.exports = chnl;';
|
168 | res.send(clSideScript);
|
169 | res.end();
|
170 | });
|
171 |
|
172 | if (serverChannels[name]) {
|
173 | console.warn("This channel name(" + name + ") is already exposed-ignoring the command.");
|
174 | } else {
|
175 | if (toExpose.rpcProps) {
|
176 | throw new Error('Failed to expose channel, rpcProps property is reserved for socket and rpc stuff');
|
177 | }
|
178 | serverChannels[name] = new RpcChannel(name, toExpose);
|
179 | }
|
180 | return rpcInstance;
|
181 | },
|
182 | |
183 |
|
184 |
|
185 |
|
186 |
|
187 | exposeFile: function (mPath) {
|
188 | var stack = new Error().stack;
|
189 |
|
190 | var callerFile = stack.split('\n')[2].match(/\(.*\)/g)[0];
|
191 | callerFile = callerFile.substr(1, callerFile.indexOf('.js') + 2);
|
192 |
|
193 | var absToModule = path.join(path.dirname(callerFile), mPath);
|
194 | var toModule = path.relative('./', absToModule).split(path.sep);
|
195 | var exposedObj = require(absToModule);
|
196 |
|
197 | var url = '/' + toModule.join('/') + '.js';
|
198 |
|
199 | return this.expose(mPath, exposedObj, url);
|
200 | },
|
201 | |
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | loadClientChannel: function (socket, name) {
|
208 | var channel = getClientChannel(socket.id, name);
|
209 |
|
210 | |
211 |
|
212 |
|
213 | if (!channel.dfd) {
|
214 | channel.dfd = Promise.defer();
|
215 |
|
216 | socket.on('disconnect', function onDisconnect() {
|
217 | var err = function () {
|
218 | throw new Error('Client channel disconnected, this channel is not available anymore')
|
219 | };
|
220 | for (var method in channel.fns) {
|
221 | channel.fns[method] = err;
|
222 | }
|
223 | debug("disconnected clc " + name);
|
224 | });
|
225 | }
|
226 | return channel.dfd.promise;
|
227 | }
|
228 | };
|
229 |
|
230 | |
231 |
|
232 |
|
233 |
|
234 | var absPath = function(rel) {
|
235 | return path.join(__dirname, rel);
|
236 | };
|
237 |
|
238 | var fileMap = {
|
239 | '/rpc/client.js': 'node_modules/socket.io-rpc-client/client.js',
|
240 | '/rpc/rpc-client.js': 'node_modules/socket.io-rpc-client/socket.io-rpc-client.js',
|
241 | '/rpc/export-channel.js': 'node_modules/socket.io-rpc-client/export-channel.js',
|
242 | '/rpc/rpc-client-angular.js': 'node_modules/socket.io-rpc-client/socket.io-rpc-client-angular.js'
|
243 | };
|
244 |
|
245 | Object.keys(fileMap).forEach(function (serverPath){
|
246 | fileMap[serverPath] = absPath(fileMap[serverPath]);
|
247 | expApp.get(serverPath, function(req, res) {
|
248 | res.sendFile(fileMap[serverPath]);
|
249 | });
|
250 | });
|
251 |
|
252 | io = ioP;
|
253 |
|
254 | io.sockets.on('connection', function (socket) {
|
255 | clientKnownChannels[socket.id] = [];
|
256 |
|
257 | var timeoutId;
|
258 |
|
259 | socket.on('disconnect', function() {
|
260 | timeoutId = setTimeout(function () {
|
261 | debug("cleaning client channels of " + socket.id);
|
262 | delete clientChannels[socket.id];
|
263 | delete clientKnownChannels[socket.id];
|
264 | }, 300000);
|
265 | });
|
266 |
|
267 | socket.on('reconnect', function () {
|
268 | if (timeoutId) {
|
269 | clearTimeout(timeoutId);
|
270 | }
|
271 | var thisClientChnls = clientChannels[socket.id];
|
272 | if (!thisClientChnls) {
|
273 |
|
274 | socket.emit('reexposeChannels');
|
275 | } else {
|
276 | var index = thisClientChnls.length;
|
277 | while(index--) {
|
278 | socket.emit('clientChannelCreated', thisClientChnls[index].name);
|
279 | }
|
280 | }
|
281 | })
|
282 | });
|
283 |
|
284 | var authProcess = function (data, callback, socket) {
|
285 | if (serverChannels.hasOwnProperty(data.name)) {
|
286 | var authFn = serverChannels[data.name].authFn;
|
287 | if (typeof authFn === 'function') {
|
288 | serverChannels[data.name].authFn.call(socket, data.handshake, callback);
|
289 | } else {
|
290 | callback(true);
|
291 | }
|
292 | } else {
|
293 | socket.emit('channelDoesNotExist', {name: data.name});
|
294 | }
|
295 | };
|
296 |
|
297 | rpcInstance.masterChannel = io.of('/rpc-master')
|
298 | .on('connection', function (socket) {
|
299 |
|
300 | socket.on('load channel', function (data) {
|
301 | var callback = function (authorized) {
|
302 | if (authorized) {
|
303 | var channel = serverChannels[data.name];
|
304 |
|
305 | if (data.cachedDate && data.cachedDate > runDate) {
|
306 | socket.emit('channelFns', {name: data.name});
|
307 | } else {
|
308 | var channelFnPayload;
|
309 |
|
310 | if (clientKnownChannels[socket.id].indexOf(channel.tplId) !== -1) {
|
311 | channelFnPayload = {name: data.name, tplId: channel.tplId};
|
312 | } else {
|
313 | channelFnPayload = {name: data.name, fnNames: channel.fnNames, tplId: channel.tplId};
|
314 | clientKnownChannels[socket.id].push(channel.tplId);
|
315 | }
|
316 |
|
317 | socket.emit('channelFns', channelFnPayload);
|
318 | }
|
319 | } else {
|
320 | socket.emit('AuthorizationFailed', data.name);
|
321 | }
|
322 | };
|
323 | authProcess(data, callback, socket);
|
324 | });
|
325 |
|
326 | socket.on('load channelList', function () {
|
327 | socket.emit('channels', { list: getChannelNames() });
|
328 | });
|
329 |
|
330 | socket.on('exposeChannel', function (data) {
|
331 | var clId = socket.id;
|
332 | debug("client with id " + clId +" exposed rpc channel " + data.name);
|
333 | var channel = getClientChannel(clId, data.name);
|
334 | channel.dfd = channel.dfd || Promise.defer();
|
335 |
|
336 | channel.fns = channel.fns || {};
|
337 | channel.socket = io.of('/rpcC-'+data.name + '/' + socket.id);
|
338 | data.fns.forEach(function (fnName) {
|
339 | channel.fns[fnName] = function () {
|
340 | invocationCounter++;
|
341 | channel.socket.emit('call',
|
342 | {Id: invocationCounter, fnName: fnName, args: Array.prototype.slice.call(arguments, 0)}
|
343 | );
|
344 | deferreds[invocationCounter] = Promise.defer();
|
345 | return deferreds[invocationCounter].promise;
|
346 | };
|
347 | });
|
348 | channel.socket.on('connection', function (socket) {
|
349 |
|
350 | socket.on('resolve', function (data) {
|
351 | deferreds[data.Id].resolve(data.value);
|
352 | callToClientEnded(data.Id);
|
353 | });
|
354 | socket.on('reject', function (data) {
|
355 | deferreds[data.Id].reject(data.reason);
|
356 | callToClientEnded(data.Id);
|
357 | });
|
358 |
|
359 | socket.on('reconnect', function () {
|
360 |
|
361 |
|
362 | debug("reconnected to client chnl " + data.name);
|
363 | channel.dfd.resolve(channel.fns);
|
364 |
|
365 | });
|
366 |
|
367 | debug("client connected to its own rpc channel " + data.name);
|
368 | channel.dfd.resolve(channel.fns);
|
369 |
|
370 | });
|
371 |
|
372 | socket.emit('clientChannelCreated', data.name);
|
373 |
|
374 | });
|
375 |
|
376 | socket.emit('serverRunDate', runDate);
|
377 | }
|
378 | );
|
379 |
|
380 | return rpcInstance;
|
381 | }
|
382 |
|
383 | createServer.client = require('socket.io-rpc-client');
|
384 |
|
385 | module.exports = createServer;
|