mongodb-livedata-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +63 -0
  2. package/dist/livedata_server.js +9 -0
  3. package/dist/meteor/binary-heap/max_heap.js +186 -0
  4. package/dist/meteor/binary-heap/min_heap.js +17 -0
  5. package/dist/meteor/binary-heap/min_max_heap.js +48 -0
  6. package/dist/meteor/callback-hook/hook.js +78 -0
  7. package/dist/meteor/ddp/crossbar.js +136 -0
  8. package/dist/meteor/ddp/heartbeat.js +77 -0
  9. package/dist/meteor/ddp/livedata_server.js +403 -0
  10. package/dist/meteor/ddp/method-invocation.js +72 -0
  11. package/dist/meteor/ddp/random-stream.js +100 -0
  12. package/dist/meteor/ddp/session-collection-view.js +106 -0
  13. package/dist/meteor/ddp/session-document-view.js +82 -0
  14. package/dist/meteor/ddp/session.js +570 -0
  15. package/dist/meteor/ddp/stream_server.js +181 -0
  16. package/dist/meteor/ddp/subscription.js +347 -0
  17. package/dist/meteor/ddp/utils.js +104 -0
  18. package/dist/meteor/ddp/writefence.js +111 -0
  19. package/dist/meteor/diff-sequence/diff.js +257 -0
  20. package/dist/meteor/ejson/ejson.js +569 -0
  21. package/dist/meteor/ejson/stringify.js +119 -0
  22. package/dist/meteor/ejson/utils.js +42 -0
  23. package/dist/meteor/id-map/id_map.js +92 -0
  24. package/dist/meteor/mongo/caching_change_observer.js +94 -0
  25. package/dist/meteor/mongo/doc_fetcher.js +53 -0
  26. package/dist/meteor/mongo/geojson_utils.js +41 -0
  27. package/dist/meteor/mongo/live_connection.js +264 -0
  28. package/dist/meteor/mongo/live_cursor.js +57 -0
  29. package/dist/meteor/mongo/minimongo_common.js +2002 -0
  30. package/dist/meteor/mongo/minimongo_matcher.js +217 -0
  31. package/dist/meteor/mongo/minimongo_sorter.js +268 -0
  32. package/dist/meteor/mongo/observe_driver_utils.js +73 -0
  33. package/dist/meteor/mongo/observe_multiplexer.js +228 -0
  34. package/dist/meteor/mongo/oplog-observe-driver.js +919 -0
  35. package/dist/meteor/mongo/oplog_tailing.js +352 -0
  36. package/dist/meteor/mongo/oplog_v2_converter.js +126 -0
  37. package/dist/meteor/mongo/polling_observe_driver.js +195 -0
  38. package/dist/meteor/mongo/synchronous-cursor.js +261 -0
  39. package/dist/meteor/mongo/synchronous-queue.js +110 -0
  40. package/dist/meteor/ordered-dict/ordered_dict.js +198 -0
  41. package/dist/meteor/random/AbstractRandomGenerator.js +92 -0
  42. package/dist/meteor/random/AleaRandomGenerator.js +90 -0
  43. package/dist/meteor/random/NodeRandomGenerator.js +42 -0
  44. package/dist/meteor/random/createAleaGenerator.js +32 -0
  45. package/dist/meteor/random/createRandom.js +22 -0
  46. package/dist/meteor/random/main.js +12 -0
  47. package/livedata_server.ts +3 -0
  48. package/meteor/LICENSE +28 -0
  49. package/meteor/binary-heap/max_heap.ts +225 -0
  50. package/meteor/binary-heap/min_heap.ts +15 -0
  51. package/meteor/binary-heap/min_max_heap.ts +53 -0
  52. package/meteor/callback-hook/hook.ts +85 -0
  53. package/meteor/ddp/crossbar.ts +148 -0
  54. package/meteor/ddp/heartbeat.ts +97 -0
  55. package/meteor/ddp/livedata_server.ts +473 -0
  56. package/meteor/ddp/method-invocation.ts +86 -0
  57. package/meteor/ddp/random-stream.ts +102 -0
  58. package/meteor/ddp/session-collection-view.ts +119 -0
  59. package/meteor/ddp/session-document-view.ts +92 -0
  60. package/meteor/ddp/session.ts +708 -0
  61. package/meteor/ddp/stream_server.ts +204 -0
  62. package/meteor/ddp/subscription.ts +392 -0
  63. package/meteor/ddp/utils.ts +119 -0
  64. package/meteor/ddp/writefence.ts +130 -0
  65. package/meteor/diff-sequence/diff.ts +295 -0
  66. package/meteor/ejson/ejson.ts +601 -0
  67. package/meteor/ejson/stringify.ts +122 -0
  68. package/meteor/ejson/utils.ts +38 -0
  69. package/meteor/id-map/id_map.ts +84 -0
  70. package/meteor/mongo/caching_change_observer.ts +120 -0
  71. package/meteor/mongo/doc_fetcher.ts +52 -0
  72. package/meteor/mongo/geojson_utils.ts +42 -0
  73. package/meteor/mongo/live_connection.ts +302 -0
  74. package/meteor/mongo/live_cursor.ts +79 -0
  75. package/meteor/mongo/minimongo_common.ts +2440 -0
  76. package/meteor/mongo/minimongo_matcher.ts +275 -0
  77. package/meteor/mongo/minimongo_sorter.ts +331 -0
  78. package/meteor/mongo/observe_driver_utils.ts +79 -0
  79. package/meteor/mongo/observe_multiplexer.ts +256 -0
  80. package/meteor/mongo/oplog-observe-driver.ts +1049 -0
  81. package/meteor/mongo/oplog_tailing.ts +414 -0
  82. package/meteor/mongo/oplog_v2_converter.ts +124 -0
  83. package/meteor/mongo/polling_observe_driver.ts +247 -0
  84. package/meteor/mongo/synchronous-cursor.ts +293 -0
  85. package/meteor/mongo/synchronous-queue.ts +119 -0
  86. package/meteor/ordered-dict/ordered_dict.ts +229 -0
  87. package/meteor/random/AbstractRandomGenerator.ts +99 -0
  88. package/meteor/random/AleaRandomGenerator.ts +96 -0
  89. package/meteor/random/NodeRandomGenerator.ts +37 -0
  90. package/meteor/random/createAleaGenerator.ts +31 -0
  91. package/meteor/random/createRandom.ts +19 -0
  92. package/meteor/random/main.ts +8 -0
  93. package/package.json +30 -0
  94. package/tsconfig.json +10 -0
@@ -0,0 +1,708 @@
1
+ import { MethodInvocation } from "./method-invocation";
2
+ import { _WriteFence } from "./writefence";
3
+ import DoubleEndedQueue from "double-ended-queue";
4
+ import { Random } from "../random/main";
5
+ import { StreamServerSocket } from "./stream_server";
6
+ import { DDP, ddpError, DDPServer, wrapInternalException } from "./livedata_server";
7
+ import { Heartbeat } from "./heartbeat";
8
+ import { stringifyDDP } from "./utils";
9
+ import { DiffSequence } from "../diff-sequence/diff";
10
+ import { SessionCollectionView } from "./session-collection-view";
11
+ import { Subscription } from "./subscription";
12
+
13
+ export interface SessionConnectionHandle {
14
+ id: string;
15
+ close: Function;
16
+ onClose: Function;
17
+ clientAddress: any;
18
+ httpHeaders: Record<string, any>
19
+ }
20
+
21
+ interface DDPMessage {
22
+ msg: string;
23
+ id: string;
24
+ version?: string;
25
+ name?: string;
26
+ method?: string;
27
+ params?: any[]
28
+ randomSeed?: any;
29
+ }
30
+
31
+ export class DDPSession {
32
+ public id: string;
33
+ public server: any;
34
+ public inQueue: DoubleEndedQueue<any>;
35
+ public userId: string | null;
36
+ public connectionHandle: SessionConnectionHandle;
37
+ public _dontStartNewUniversalSubs: boolean;
38
+ public _socketUrl: string;
39
+
40
+ private version: string;
41
+ private socket: StreamServerSocket;
42
+ private initialized: boolean;
43
+ private workerRunning: boolean;
44
+ private _namedSubs: Map<string, any>;
45
+ private _universalSubs: any[];
46
+ private collectionViews: Map<string, any>;
47
+ private _isSending: boolean;
48
+ private _pendingReady: any[];
49
+ private _closeCallbacks: Function[];
50
+ private _respondToPings: boolean;
51
+ private heartbeat: Heartbeat;
52
+
53
+ constructor(server: DDPServer, version: string, socket: StreamServerSocket, options) {
54
+ var self = this;
55
+ self.id = Random.id();
56
+
57
+ self.server = server;
58
+ self.version = version;
59
+
60
+ self.initialized = false;
61
+ self.socket = socket;
62
+
63
+ // Set to null when the session is destroyed. Multiple places below
64
+ // use this to determine if the session is alive or not.
65
+ self.inQueue = new DoubleEndedQueue();
66
+
67
+ self.workerRunning = false;
68
+
69
+ // Sub objects for active subscriptions
70
+ self._namedSubs = new Map();
71
+ self._universalSubs = [];
72
+
73
+ self.userId = null;
74
+
75
+ self.collectionViews = new Map();
76
+
77
+ // Set this to false to not send messages when collectionViews are
78
+ // modified. This is done when rerunning subs in _setUserId and those messages
79
+ // are calculated via a diff instead.
80
+ self._isSending = true;
81
+
82
+ // If this is true, don't start a newly-created universal publisher on this
83
+ // session. The session will take care of starting it when appropriate.
84
+ self._dontStartNewUniversalSubs = false;
85
+
86
+ // When we are rerunning subscriptions, any ready messages
87
+ // we want to buffer up for when we are done rerunning subscriptions
88
+ self._pendingReady = [];
89
+
90
+ // List of callbacks to call when this connection is closed.
91
+ self._closeCallbacks = [];
92
+
93
+
94
+ // XXX HACK: If a sockjs connection, save off the URL. This is
95
+ // temporary and will go away in the near future.
96
+ self._socketUrl = socket.url;
97
+
98
+ // Allow tests to disable responding to pings.
99
+ self._respondToPings = options.respondToPings;
100
+
101
+ // This object is the public interface to the session. In the public
102
+ // API, it is called the `connection` object. Internally we call it
103
+ // a `connectionHandle` to avoid ambiguity.
104
+ self.connectionHandle = {
105
+ id: self.id,
106
+ close: function () {
107
+ self.close();
108
+ },
109
+ onClose: function (cb) {
110
+ if (self.inQueue) {
111
+ self._closeCallbacks.push(cb);
112
+ } else {
113
+ setImmediate(cb);
114
+ }
115
+ },
116
+ clientAddress: self._clientAddress(),
117
+ httpHeaders: self.socket.headers
118
+ };
119
+
120
+ self.send({ msg: 'connected', session: self.id });
121
+
122
+ // On initial connect, spin up all the universal publishers.
123
+ setImmediate(() => this.startUniversalSubs());
124
+
125
+ if (version !== 'pre1' && options.heartbeatInterval !== 0) {
126
+ // We no longer need the low level timeout because we have heartbeats.
127
+ socket.setWebsocketTimeout(0);
128
+
129
+ self.heartbeat = new Heartbeat({
130
+ heartbeatInterval: options.heartbeatInterval,
131
+ heartbeatTimeout: options.heartbeatTimeout,
132
+ onTimeout: function () {
133
+ self.close();
134
+ },
135
+ sendPing: function () {
136
+ self.send({ msg: 'ping' });
137
+ }
138
+ });
139
+ self.heartbeat.start();
140
+ }
141
+
142
+ }
143
+
144
+ sendReady(subscriptionIds) {
145
+ var self = this;
146
+ if (self._isSending)
147
+ self.send({ msg: "ready", subs: subscriptionIds });
148
+ else {
149
+ for (const subscriptionId of subscriptionIds) {
150
+ self._pendingReady.push(subscriptionId);
151
+ }
152
+ }
153
+ }
154
+
155
+ _canSend(collectionName) {
156
+ return this._isSending || !this.server.getPublicationStrategy(collectionName).useCollectionView;
157
+ }
158
+
159
+
160
+ sendAdded(collectionName: string, id: string, fields: Record<string, any>) {
161
+ if (this._canSend(collectionName))
162
+ this.send({ msg: "added", collection: collectionName, id, fields });
163
+ }
164
+
165
+ sendChanged(collectionName: string, id: string, fields: Record<string, any>) {
166
+ if (fields == null || Object.keys(fields).length === 0)
167
+ return;
168
+
169
+ if (this._canSend(collectionName)) {
170
+ this.send({
171
+ msg: "changed",
172
+ collection: collectionName,
173
+ id,
174
+ fields
175
+ });
176
+ }
177
+ }
178
+
179
+ sendRemoved(collectionName: string, id: string) {
180
+ if (this._canSend(collectionName))
181
+ this.send({ msg: "removed", collection: collectionName, id });
182
+ }
183
+
184
+ getSendCallbacks() {
185
+ return {
186
+ added: this.sendAdded.bind(this),
187
+ changed: this.sendChanged.bind(this),
188
+ removed: this.sendRemoved.bind(this)
189
+ };
190
+ }
191
+
192
+ getCollectionView(collectionName) {
193
+ var self = this;
194
+ var ret = self.collectionViews.get(collectionName);
195
+ if (!ret) {
196
+ ret = new SessionCollectionView(collectionName, self.getSendCallbacks());
197
+ self.collectionViews.set(collectionName, ret);
198
+ }
199
+ return ret;
200
+ }
201
+
202
+ added(subscriptionHandle, collectionName, id, fields) {
203
+ if (this.server.getPublicationStrategy(collectionName).useCollectionView) {
204
+ const view = this.getCollectionView(collectionName);
205
+ view.added(subscriptionHandle, id, fields);
206
+ } else {
207
+ this.sendAdded(collectionName, id, fields);
208
+ }
209
+ }
210
+
211
+ removed(subscriptionHandle, collectionName, id) {
212
+ if (this.server.getPublicationStrategy(collectionName).useCollectionView) {
213
+ const view = this.getCollectionView(collectionName);
214
+ view.removed(subscriptionHandle, id);
215
+ if (view.isEmpty()) {
216
+ this.collectionViews.delete(collectionName);
217
+ }
218
+ } else {
219
+ this.sendRemoved(collectionName, id);
220
+ }
221
+ }
222
+
223
+ changed(subscriptionHandle, collectionName, id, fields) {
224
+ if (this.server.getPublicationStrategy(collectionName).useCollectionView) {
225
+ const view = this.getCollectionView(collectionName);
226
+ view.changed(subscriptionHandle, id, fields);
227
+ } else {
228
+ this.sendChanged(collectionName, id, fields);
229
+ }
230
+ }
231
+
232
+ startUniversalSubs() {
233
+ // Make a shallow copy of the set of universal handlers and start them. If
234
+ // additional universal publishers start while we're running them (due to
235
+ // yielding), they will run separately as part of Server.publish.
236
+ var handlers = [ ...this.server.universal_publish_handlers ];
237
+ for (const handler of handlers) {
238
+ this._startSubscription(handler);
239
+ }
240
+ }
241
+
242
+ // Destroy this session and unregister it at the server.
243
+ close() {
244
+ var self = this;
245
+
246
+ // Destroy this session, even if it's not registered at the
247
+ // server. Stop all processing and tear everything down. If a socket
248
+ // was attached, close it.
249
+
250
+ // Already destroyed.
251
+ if (!self.inQueue)
252
+ return;
253
+
254
+ // Drop the merge box data immediately.
255
+ self.inQueue = null;
256
+ self.collectionViews = new Map();
257
+
258
+ if (self.heartbeat) {
259
+ self.heartbeat.stop();
260
+ self.heartbeat = null;
261
+ }
262
+
263
+ if (self.socket) {
264
+ self.socket.close();
265
+ self.socket._meteorSession = null;
266
+ }
267
+
268
+ setImmediate(function () {
269
+ // Stop callbacks can yield, so we defer this on close.
270
+ // sub._isDeactivated() detects that we set inQueue to null and
271
+ // treats it as semi-deactivated (it will ignore incoming callbacks, etc).
272
+ self._deactivateAllSubscriptions();
273
+
274
+ // Defer calling the close callbacks, so that the caller closing
275
+ // the session isn't waiting for all the callbacks to complete.
276
+ for (const callback of self._closeCallbacks) {
277
+ callback();
278
+ }
279
+ });
280
+
281
+ // Unregister the session.
282
+ self.server._removeSession(self);
283
+ }
284
+
285
+ // Send a message (doing nothing if no socket is connected right now).
286
+ // It should be a JSON object (it will be stringified).
287
+ send(msg) {
288
+ var self = this;
289
+ if (self.socket) {
290
+ self.socket.send(stringifyDDP(msg));
291
+ }
292
+ }
293
+
294
+ // Send a connection error.
295
+ sendError(reason, offendingMessage) {
296
+ var self = this;
297
+ var msg = { msg: 'error', reason, offendingMessage };
298
+ self.send(msg);
299
+ }
300
+
301
+ // Process 'msg' as an incoming message. As a guard against
302
+ // race conditions during reconnection, ignore the message if
303
+ // 'socket' is not the currently connected socket.
304
+ //
305
+ // We run the messages from the client one at a time, in the order
306
+ // given by the client. The message handler is passed an idempotent
307
+ // function 'unblock' which it may call to allow other messages to
308
+ // begin running in parallel in another fiber (for example, a method
309
+ // that wants to yield). Otherwise, it is automatically unblocked
310
+ // when it returns.
311
+ //
312
+ // Actually, we don't have to 'totally order' the messages in this
313
+ // way, but it's the easiest thing that's correct. (unsub needs to
314
+ // be ordered against sub, methods need to be ordered against each
315
+ // other).
316
+ async processMessage(msg_in: DDPMessage) {
317
+ var self = this;
318
+ if (!self.inQueue) // we have been destroyed.
319
+ return;
320
+
321
+ // Respond to ping and pong messages immediately without queuing.
322
+ // If the negotiated DDP version is "pre1" which didn't support
323
+ // pings, preserve the "pre1" behavior of responding with a "bad
324
+ // request" for the unknown messages.
325
+ //
326
+ // Any message counts as receiving a pong, as it demonstrates that
327
+ // the client is still alive.
328
+ if (self.heartbeat) {
329
+ self.heartbeat.messageReceived();
330
+ }
331
+
332
+ if (self.version !== 'pre1' && msg_in.msg === 'ping') {
333
+ if (self._respondToPings)
334
+ self.send({ msg: "pong", id: msg_in.id });
335
+ return;
336
+ }
337
+ if (self.version !== 'pre1' && msg_in.msg === 'pong') {
338
+ // Since everything is a pong, there is nothing to do
339
+ return;
340
+ }
341
+
342
+ self.inQueue.push(msg_in);
343
+ if (self.workerRunning)
344
+ return;
345
+ self.workerRunning = true;
346
+
347
+ while (self.inQueue && self.inQueue.length > 0) {
348
+ var msg = self.inQueue.shift();
349
+
350
+ self.server.onMessageHook.each(callback => {
351
+ callback(msg, self);
352
+ return true;
353
+ });
354
+
355
+ if (self.protocol_handlers.hasOwnProperty(msg.msg))
356
+ await self.protocol_handlers[msg.msg].call(self, msg);
357
+ else
358
+ self.sendError('Bad request', msg);
359
+
360
+ }
361
+ self.workerRunning = false;
362
+
363
+ }
364
+
365
+ private protocol_handlers = {
366
+ sub: async function (msg: DDPMessage) {
367
+ var self = this;
368
+
369
+ // reject malformed messages
370
+ if (typeof (msg.id) !== "string" ||
371
+ typeof (msg.name) !== "string" ||
372
+ (('params' in msg) && !(msg.params instanceof Array))) {
373
+ self.sendError("Malformed subscription", msg);
374
+ return;
375
+ }
376
+
377
+ if (!self.server.publish_handlers[msg.name]) {
378
+ self.send({
379
+ msg: 'nosub', id: msg.id,
380
+ error: ddpError(404, `Subscription '${msg.name}' not found`)
381
+ });
382
+ return;
383
+ }
384
+
385
+ if (self._namedSubs.has(msg.id))
386
+ // subs are idempotent, or rather, they are ignored if a sub
387
+ // with that id already exists. this is important during
388
+ // reconnect.
389
+ return;
390
+
391
+ // XXX It'd be much better if we had generic hooks where any package can
392
+ // hook into subscription handling, but in the mean while we special case
393
+ // ddp-rate-limiter package. This is also done for weak requirements to
394
+ // add the ddp-rate-limiter package in case we don't have Accounts. A
395
+ // user trying to use the ddp-rate-limiter must explicitly require it.
396
+ /* if (Package['ddp-rate-limiter']) {
397
+ var DDPRateLimiter = Package['ddp-rate-limiter'].DDPRateLimiter;
398
+ var rateLimiterInput = {
399
+ userId: self.userId,
400
+ clientAddress: self.connectionHandle.clientAddress,
401
+ type: "subscription",
402
+ name: msg.name,
403
+ connectionId: self.id
404
+ };
405
+
406
+ DDPRateLimiter._increment(rateLimiterInput);
407
+ var rateLimitResult = DDPRateLimiter._check(rateLimiterInput);
408
+ if (!rateLimitResult.allowed) {
409
+ self.send({
410
+ msg: 'nosub', id: msg.id,
411
+ error: ddpError(
412
+ 'too-many-requests',
413
+ DDPRateLimiter.getErrorMessage(rateLimitResult),
414
+ {timeToReset: rateLimitResult.timeToReset})
415
+ });
416
+ return;
417
+ }
418
+ }*/
419
+
420
+ var handler = self.server.publish_handlers[msg.name];
421
+
422
+ self._startSubscription(handler, msg.id, msg.params, msg.name);
423
+ },
424
+
425
+ unsub: async function (msg: DDPMessage) {
426
+ var self = this;
427
+
428
+ self._stopSubscription(msg.id);
429
+ },
430
+
431
+ method: async function (msg: DDPMessage) {
432
+ var self = this;
433
+
434
+ // Reject malformed messages.
435
+ // For now, we silently ignore unknown attributes,
436
+ // for forwards compatibility.
437
+ if (typeof (msg.id) !== "string" ||
438
+ typeof (msg.method) !== "string" ||
439
+ (('params' in msg) && !(msg.params instanceof Array)) ||
440
+ (('randomSeed' in msg) && (typeof msg.randomSeed !== "string"))) {
441
+ self.sendError("Malformed method invocation", msg);
442
+ return;
443
+ }
444
+
445
+ var randomSeed = msg.randomSeed || null;
446
+
447
+ // Set up to mark the method as satisfied once all observers
448
+ // (and subscriptions) have reacted to any writes that were
449
+ // done.
450
+ var fence = new _WriteFence();
451
+ fence.onAllCommitted(function () {
452
+ // Retire the fence so that future writes are allowed.
453
+ // This means that callbacks like timers are free to use
454
+ // the fence, and if they fire before it's armed (for
455
+ // example, because the method waits for them) their
456
+ // writes will be included in the fence.
457
+ fence.retire();
458
+ self.send({ msg: 'updated', methods: [msg.id] });
459
+ });
460
+
461
+ // Find the handler
462
+ var handler = self.server.method_handlers[msg.method];
463
+ if (!handler) {
464
+ self.send({ msg: 'result', id: msg.id, error: ddpError(404, `Method '${msg.method}' not found`) });
465
+ fence.arm();
466
+ return;
467
+ }
468
+
469
+ var setUserId = function (userId) {
470
+ self._setUserId(userId);
471
+ };
472
+
473
+ var invocation = new MethodInvocation({
474
+ isSimulation: false,
475
+ userId: self.userId,
476
+ setUserId: setUserId,
477
+ connection: self.connectionHandle,
478
+ randomSeed: randomSeed
479
+ });
480
+
481
+ const oldInvocation = DDP._CurrentMethodInvocation;
482
+ const oldFence = _WriteFence._CurrentWriteFence;
483
+
484
+ DDP._CurrentMethodInvocation = invocation;
485
+ _WriteFence._CurrentWriteFence = fence;
486
+
487
+ function finish() {
488
+ DDP._CurrentMethodInvocation = oldInvocation;
489
+ _WriteFence._CurrentWriteFence = oldFence;
490
+ fence.arm();
491
+ }
492
+
493
+ const payload = {
494
+ msg: "result",
495
+ id: msg.id,
496
+ result: undefined,
497
+ error: undefined
498
+ };
499
+
500
+ handler.apply(invocation, msg.params).then((result) => {
501
+ finish();
502
+ if (result !== undefined) {
503
+ payload.result = result;
504
+ }
505
+ self.send(payload);
506
+ }, (exception) => {
507
+ finish();
508
+ payload.error = wrapInternalException(
509
+ exception,
510
+ `while invoking method '${msg.method}'`
511
+ );
512
+ self.send(payload);
513
+ });
514
+ }
515
+ }
516
+
517
+ _eachSub(f) {
518
+ var self = this;
519
+ self._namedSubs.forEach(f);
520
+ self._universalSubs.forEach(f);
521
+ }
522
+
523
+ _diffCollectionViews(beforeCVs) {
524
+ var self = this;
525
+ DiffSequence.diffMaps(beforeCVs, self.collectionViews, {
526
+ both: function (collectionName, leftValue, rightValue) {
527
+ rightValue.diff(leftValue);
528
+ },
529
+ rightOnly: function (collectionName, rightValue) {
530
+ rightValue.documents.forEach(function (docView, id) {
531
+ self.sendAdded(collectionName, id, docView.getFields());
532
+ });
533
+ },
534
+ leftOnly: function (collectionName, leftValue) {
535
+ leftValue.documents.forEach(function (doc, id) {
536
+ self.sendRemoved(collectionName, id);
537
+ });
538
+ }
539
+ });
540
+ }
541
+
542
+ // Sets the current user id in all appropriate contexts and reruns
543
+ // all subscriptions
544
+ _setUserId(userId) {
545
+ var self = this;
546
+
547
+ if (userId !== null && typeof userId !== "string")
548
+ throw new Error("setUserId must be called on string or null, not " +
549
+ typeof userId);
550
+
551
+ // Prevent newly-created universal subscriptions from being added to our
552
+ // session. They will be found below when we call startUniversalSubs.
553
+ //
554
+ // (We don't have to worry about named subscriptions, because we only add
555
+ // them when we process a 'sub' message. We are currently processing a
556
+ // 'method' message, and the method did not unblock, because it is illegal
557
+ // to call setUserId after unblock. Thus we cannot be concurrently adding a
558
+ // new named subscription).
559
+ self._dontStartNewUniversalSubs = true;
560
+
561
+ // Prevent current subs from updating our collectionViews and call their
562
+ // stop callbacks. This may yield.
563
+ self._eachSub(function (sub) {
564
+ sub._deactivate();
565
+ });
566
+
567
+ // All subs should now be deactivated. Stop sending messages to the client,
568
+ // save the state of the published collections, reset to an empty view, and
569
+ // update the userId.
570
+ self._isSending = false;
571
+ var beforeCVs = self.collectionViews;
572
+ self.collectionViews = new Map();
573
+ self.userId = userId;
574
+
575
+ // _setUserId is normally called from a Meteor method with
576
+ // DDP._CurrentMethodInvocation set. But DDP._CurrentMethodInvocation is not
577
+ // expected to be set inside a publish function, so we temporary unset it.
578
+ // Inside a publish function DDP._CurrentPublicationInvocation is set.
579
+ const oldInvocation = DDP._CurrentMethodInvocation;
580
+ DDP._CurrentMethodInvocation = undefined;
581
+ try {
582
+ // Save the old named subs, and reset to having no subscriptions.
583
+ var oldNamedSubs = self._namedSubs;
584
+ self._namedSubs = new Map();
585
+ self._universalSubs = [];
586
+
587
+ oldNamedSubs.forEach(function (sub, subscriptionId) {
588
+ var newSub = sub._recreate();
589
+ self._namedSubs.set(subscriptionId, newSub);
590
+ // nb: if the handler throws or calls this.error(), it will in fact
591
+ // immediately send its 'nosub'. This is OK, though.
592
+ newSub._runHandler();
593
+ });
594
+
595
+ // Allow newly-created universal subs to be started on our connection in
596
+ // parallel with the ones we're spinning up here, and spin up universal
597
+ // subs.
598
+ self._dontStartNewUniversalSubs = false;
599
+ self.startUniversalSubs();
600
+ } finally {
601
+ DDP._CurrentMethodInvocation = oldInvocation;
602
+ }
603
+
604
+ // Start sending messages again, beginning with the diff from the previous
605
+ // state of the world to the current state. No yields are allowed during
606
+ // this diff, so that other changes cannot interleave.
607
+ self._isSending = true;
608
+ self._diffCollectionViews(beforeCVs);
609
+ if (!self._pendingReady || self._pendingReady.length === 0) {
610
+ self.sendReady(self._pendingReady);
611
+ self._pendingReady = [];
612
+ }
613
+ }
614
+
615
+ _startSubscription(handler, subId?, params?, name?) {
616
+ var self = this;
617
+
618
+ var sub = new Subscription(self, handler, subId, params, name);
619
+
620
+ if (subId)
621
+ self._namedSubs.set(subId, sub);
622
+ else
623
+ self._universalSubs.push(sub);
624
+
625
+ sub._runHandler();
626
+ }
627
+
628
+ // Tear down specified subscription
629
+ _stopSubscription(subId, error?) {
630
+ var self = this;
631
+
632
+ var subName = null;
633
+ if (subId) {
634
+ var maybeSub = self._namedSubs.get(subId);
635
+ if (maybeSub) {
636
+ subName = maybeSub._name;
637
+ maybeSub._removeAllDocuments();
638
+ maybeSub._deactivate();
639
+ self._namedSubs.delete(subId);
640
+ }
641
+ }
642
+
643
+ var response = { msg: 'nosub', id: subId, error: undefined };
644
+
645
+ if (error) {
646
+ response.error = wrapInternalException(
647
+ error,
648
+ subName ? ("from sub " + subName + " id " + subId)
649
+ : ("from sub id " + subId));
650
+ }
651
+
652
+ self.send(response);
653
+ }
654
+
655
+ // Tear down all subscriptions. Note that this does NOT send removed or nosub
656
+ // messages, since we assume the client is gone.
657
+ _deactivateAllSubscriptions() {
658
+ var self = this;
659
+
660
+ self._namedSubs.forEach(function (sub, id) {
661
+ sub._deactivate();
662
+ });
663
+ self._namedSubs = new Map();
664
+
665
+ self._universalSubs.forEach(function (sub) {
666
+ sub._deactivate();
667
+ });
668
+ self._universalSubs = [];
669
+ }
670
+
671
+ // Determine the remote client's IP address, based on the
672
+ // HTTP_FORWARDED_COUNT environment variable representing how many
673
+ // proxies the server is behind.
674
+ _clientAddress() {
675
+ var self = this;
676
+
677
+ // For the reported client address for a connection to be correct,
678
+ // the developer must set the HTTP_FORWARDED_COUNT environment
679
+ // variable to an integer representing the number of hops they
680
+ // expect in the `x-forwarded-for` header. E.g., set to "1" if the
681
+ // server is behind one proxy.
682
+ //
683
+ // This could be computed once at startup instead of every time.
684
+ var httpForwardedCount = parseInt(process.env['HTTP_FORWARDED_COUNT']) || 0;
685
+
686
+ if (httpForwardedCount === 0)
687
+ return self.socket.remoteAddress;
688
+
689
+ let forwardedFor: string | string[] = self.socket.headers["x-forwarded-for"];
690
+ if (typeof forwardedFor !== "string")
691
+ return null;
692
+ forwardedFor = forwardedFor.trim().split(/\s*,\s*/);
693
+
694
+ // Typically the first value in the `x-forwarded-for` header is
695
+ // the original IP address of the client connecting to the first
696
+ // proxy. However, the end user can easily spoof the header, in
697
+ // which case the first value(s) will be the fake IP address from
698
+ // the user pretending to be a proxy reporting the original IP
699
+ // address value. By counting HTTP_FORWARDED_COUNT back from the
700
+ // end of the list, we ensure that we get the IP address being
701
+ // reported by *our* first proxy.
702
+
703
+ if (httpForwardedCount < 0 || httpForwardedCount > forwardedFor.length)
704
+ return null;
705
+
706
+ return forwardedFor[forwardedFor.length - httpForwardedCount];
707
+ }
708
+ }