message_bus 2.2.4 → 3.3.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.

Potentially problematic release.


This version of message_bus might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93e3bc28f1411da6037721acabab32fa504542bb68238afe6671f10bfccfa194
4
- data.tar.gz: b25d3013be8f3a0efa180db29de0ac6a55e5ce08ea87f80c912f046b9548c821
3
+ metadata.gz: 659722c1ea3a6a805dbe81099ee2848a11ab7ebffaf8612d119f5062fd740337
4
+ data.tar.gz: 4b187421f5fb4bdff177d117b059c05314305729885f5ec520127ad827db5369
5
5
  SHA512:
6
- metadata.gz: c74493de8f9f2dc5305457696988d7f07dce56642a8796d3757a0e5bf4e14f6e9b011ed8bd9255e0d3dc197c290bbdafedcf47e2519f09ca085ec86f33848508
7
- data.tar.gz: e4778edb5d540ef011ad29916dd8e4a374b04fb5dcd57ef80542b9f0b0985f5887609bf67b071455ebd15e59386eb0abc30698d015a4585f66a4d7afd217fc05
6
+ metadata.gz: c345409c8c9c1624b079d28ca166b962f9d64bd79159fc085055df8cb760545372732fd5cf509061b55eb0a427e8ffd7018238a7b49c44f27f4e942d575f0504
7
+ data.tar.gz: d92fb533dea19da315c210f7ac96a1029cf2d1709fee3dd919032f221b557cff9ebb3d2556285e5e0c6018ebc4b227ed178fd8658711c5a9ffacf19aeb47edff
@@ -1 +1,9 @@
1
- inherit_from: https://raw.githubusercontent.com/discourse/discourse/master/.rubocop.yml
1
+ inherit_gem:
2
+ rubocop-discourse: .rubocop.yml
3
+
4
+ AllCops:
5
+ Exclude:
6
+ - 'examples/**/*'
7
+
8
+ RSpec:
9
+ Enabled: false
@@ -1,9 +1,9 @@
1
1
  before_install: gem install bundler
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3
5
4
  - 2.4
6
5
  - 2.5
6
+ - 2.6
7
7
  gemfile:
8
8
  - Gemfile
9
9
  addons:
data/CHANGELOG CHANGED
@@ -1,3 +1,36 @@
1
+ 09-06-2020
2
+
3
+ - Version 3.3.1
4
+
5
+ - FIX: Disconnect Redis conn when rescuing errors in global subscribe.
6
+ - FIX: `MessageBus::Backends::Redis#global_subscribe` not closing Redis connections.
7
+
8
+ 15-05-2020
9
+
10
+ - Version 3.3.0
11
+
12
+ - FEATURE: `MessageBus.base_route=` to alter the route that message bus will listen on.
13
+
14
+ 07-05-2020
15
+
16
+ - Version 3.2.0
17
+
18
+ - FIX: compatability with Rails 6.0.3, note: apps without ActionDispatch::Flash may stop working after this upgrade
19
+ to correct this disable middleware injection with `config.skip_message_bus_middleware = true` and configure middleware by hand with `app.middleware.use(MessageBus::Rack::Middleware)`
20
+
21
+ 28-04-2020
22
+
23
+ - Version 3.1.0
24
+
25
+ - FEATURE: `MessageBus#register_client_message_filter` to register a custom filter so that messages can be inspected and filtered away from clients.
26
+
27
+ 27-04-2020
28
+
29
+ - Version 3.0.0
30
+
31
+ - Drop support for Ruby 2.3
32
+ - FIX: Don't publish message to intersection of `user_ids` and `group_ids` - instead use the union, this is a behavior change, hence a new major release.
33
+
1
34
  26-03-2020
2
35
 
3
36
  - Version 2.2.4
data/Gemfile CHANGED
@@ -20,8 +20,11 @@ group :test, :development do
20
20
  gem 'byebug'
21
21
  end
22
22
 
23
+ group :development do
24
+ gem 'yard'
25
+ gem 'rubocop-discourse', require: false
26
+ gem 'rubocop-rspec', require: false
27
+ end
28
+
23
29
  gem 'rack'
24
30
  gem 'concurrent-ruby' # for distributed-cache
25
-
26
- gem 'rubocop'
27
- gem 'yard'
data/README.md CHANGED
@@ -74,32 +74,40 @@ id = MessageBus.last_id("/channel")
74
74
  MessageBus.backlog "/channel", id
75
75
  ```
76
76
 
77
- ```ruby
78
- # messages can be targetted at particular users or groups
79
- MessageBus.publish "/channel", "hello", user_ids: [1,2,3], group_ids: [4,5,6]
77
+ ### Targetted messages
78
+
79
+ Messages can be targetted to particular clients by supplying the `client_ids` option when publishing a message.
80
80
 
81
- # messages can be targetted at particular clients (using MessageBus.clientId)
82
- MessageBus.publish "/channel", "hello", client_ids: ["XXX","YYY"]
81
+ ```ruby
82
+ MessageBus.publish "/channel", "hello", client_ids: ["XXX", "YYY"] # (using MessageBus.clientId)
83
+ ```
83
84
 
84
- # message bus determines the user ids and groups based on env
85
+ By configuring the `user_id_lookup` and `group_ids_lookup` options with a Proc or Lambda which will be called with a [Rack specification environment](https://github.com/rack/rack/blob/master/SPEC.rdoc#the-environment-), messages can be targetted to particular clients users or groups by supplying either the `user_ids` or `group_ids` options when publishing a message.
85
86
 
87
+ ```ruby
86
88
  MessageBus.configure(user_id_lookup: proc do |env|
87
89
  # this lookup occurs on JS-client poolings, so that server can retrieve backlog
88
90
  # for the client considering/matching/filtering user_ids set on published messages
89
91
  # if user_id is not set on publish time, any user_id returned here will receive the message
90
-
91
92
  # return the user id here
92
93
  end)
93
94
 
95
+ # Target user_ids when publishing a message
96
+ MessageBus.publish "/channel", "hello", user_ids: [1, 2, 3]
97
+
94
98
  MessageBus.configure(group_ids_lookup: proc do |env|
95
99
  # return the group ids the user belongs to
96
100
  # can be nil or []
97
101
  end)
98
102
 
99
- # example of message bus to set user_ids from an initializer in Rails and Devise:
103
+ # Target group_ids when publishing a message
104
+ MessageBus.publish "/channel", "hello", group_ids: [1, 2, 3]
105
+
106
+ # example of MessageBus to set user_ids from an initializer in Rails and Devise:
100
107
  # config/inializers/message_bus.rb
101
108
  MessageBus.user_id_lookup do |env|
102
109
  req = Rack::Request.new(env)
110
+
103
111
  if req.session && req.session["warden.user.user.key"] && req.session["warden.user.user.key"][0][0]
104
112
  user = User.find(req.session["warden.user.user.key"][0][0])
105
113
  user.id
@@ -107,6 +115,36 @@ MessageBus.user_id_lookup do |env|
107
115
  end
108
116
  ```
109
117
 
118
+ If both `user_ids` and `group_ids` options are supplied when publishing a message, the message will be targetted at clients with lookup return values that matches on either the `user_ids` **or** the `group_ids` options.
119
+
120
+ ```ruby
121
+ MessageBus.publish "/channel", "hello", user_ids: [1, 2, 3], group_ids: [1, 2, 3]
122
+ ```
123
+
124
+ If the `client_ids` option is supplied with either the `user_ids` or `group_ids` options when publising a message, the `client_ids` option will be applied unconditionally and messages will be filtered further using `user_id` or `group_id` clauses.
125
+
126
+ ```ruby
127
+ MessageBus.publish "/channel", "hello", client_ids: ["XXX", "YYY"], user_ids: [1, 2, 3], group_ids: [1, 2, 3]
128
+ ```
129
+
130
+ Passing `nil` or `[]` to either `client_ids`, `user_ids` or `group_ids` is equivalent to allowing all values on each option.
131
+
132
+ ### Filtering Client Messages
133
+
134
+ Custom client message filters can be registered via `MessageBus#register_client_message_filter`. This can be useful for filtering away messages from the client based on the message's payload.
135
+
136
+ For example, ensuring that only messages seen by the server in the last 20 seconds are published to the client:
137
+
138
+ ```
139
+ MessageBus.register_client_message_filter('/test') do |message|
140
+ (Time.now.to_i - message.data[:published_at]) <= 20
141
+ end
142
+
143
+ MessageBus.publish('/test/5', { data: "somedata", published_at: Time.now.to_i })
144
+ ```
145
+
146
+ ### Error handling
147
+
110
148
  ```ruby
111
149
  MessageBus.configure(on_middleware_error: proc do |env, e|
112
150
  # If you wish to add special handling based on error
@@ -281,7 +319,7 @@ minPollInterval|100|When polling requests succeed, this is the minimum amount of
281
319
  maxPollInterval|180000|If request to the server start failing, MessageBus will backoff, this is the upper limit of the backoff.
282
320
  alwaysLongPoll|false|For debugging you may want to disable the "is browser in background" check and always long-poll
283
321
  shouldLongPollCallback|undefined|A callback returning true or false that determines if we should long-poll or not, if unset ignore and simply depend on window visibility.
284
- baseUrl|/|If message bus is mounted at a sub-path or different domain, you may configure it to perform requests there
322
+ baseUrl|/|If message bus is mounted at a sub-path or different domain, you may configure it to perform requests there. See `MessageBus.base_route=` on how to configure the MessageBus server to listen on a sub-path.
285
323
  ajax|$.ajax falling back to XMLHttpRequest|MessageBus will first attempt to use jQuery and then fallback to a plain XMLHttpRequest version that's contained in the `message-bus-ajax.js` file. `message-bus-ajax.js` must be loaded after `message-bus.js` for it to be used. You may override this option with a function that implements an ajax request by some other means
286
324
  headers|{}|Extra headers to be include with requests. Properties and values of object must be valid values for HTTP Headers, i.e. no spaces or control characters.
287
325
  minHiddenPollInterval|1500|Time to wait between poll requests performed by background or hidden tabs and windows, shared state via localStorage
@@ -303,8 +341,6 @@ enableChunkedEncoding|true|Allows streaming of message bus data over the HTTP co
303
341
 
304
342
  `MessageBus.status()` : Returns status (started, paused, stopped)
305
343
 
306
- `MessageBus.noConflict()` : Removes MessageBus from the global namespace by replacing it with whatever was present before MessageBus was loaded. Returns a reference to the MessageBus object.
307
-
308
344
  `MessageBus.diagnostics()` : Returns a log that may be used for diagnostics on the status of message bus.
309
345
 
310
346
  #### Ruby
data/Rakefile CHANGED
@@ -92,3 +92,7 @@ end
92
92
 
93
93
  desc "Run all tests, link checks and confirms documentation compiles without error"
94
94
  task default: [:spec, :rubocop, :test_doc]
95
+
96
+ Rake::Task['release'].enhance do
97
+ sh "npm publish"
98
+ end
@@ -1,55 +1,52 @@
1
1
  /*jshint bitwise: false*/
2
- (function(global, document, undefined) {
2
+
3
+ (function (root, factory) {
4
+ if (typeof define === 'function' && define.amd) {
5
+ // AMD. Register as an anonymous module.
6
+ define([], function (b) {
7
+ // Also create a global in case some scripts
8
+ // that are loaded still are looking for
9
+ // a global even when an AMD loader is in use.
10
+ return (root.MessageBus = factory());
11
+ });
12
+ } else {
13
+ // Browser globals
14
+ root.MessageBus = factory();
15
+ }
16
+ }(typeof self !== 'undefined' ? self : this, function () {
3
17
  "use strict";
4
- var previousMessageBus = global.MessageBus;
5
18
 
6
19
  // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
7
- var callbacks,
8
- clientId,
9
- failCount,
10
- shouldLongPoll,
11
- queue,
12
- responseCallbacks,
13
- uniqueId,
14
- baseUrl;
15
- var me,
16
- started,
17
- stopped,
18
- longPoller,
19
- pollTimeout,
20
- paused,
21
- later,
22
- jQuery,
23
- interval,
24
- chunkedBackoff;
25
- var delayPollTimeout;
26
-
27
- var ajaxInProgress = false;
28
-
29
- uniqueId = function() {
20
+ var uniqueId = function() {
30
21
  return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function(c) {
31
- var r, v;
32
- r = (Math.random() * 16) | 0;
33
- v = c === "x" ? r : (r & 0x3) | 0x8;
22
+ var r = (Math.random() * 16) | 0;
23
+ var v = c === "x" ? r : (r & 0x3) | 0x8;
34
24
  return v.toString(16);
35
25
  });
36
26
  };
37
27
 
38
- clientId = uniqueId();
39
- responseCallbacks = {};
40
- callbacks = [];
41
- queue = [];
42
- interval = null;
43
- failCount = 0;
44
- baseUrl = "/";
45
- paused = false;
46
- later = [];
47
- chunkedBackoff = 0;
48
- jQuery = global.jQuery;
49
- var hiddenProperty;
50
-
51
- (function() {
28
+ var me;
29
+ var delayPollTimeout;
30
+ var ajaxInProgress = false;
31
+ var started = false;
32
+ var clientId = uniqueId();
33
+ var callbacks = [];
34
+ var queue = [];
35
+ var interval = null;
36
+ var failCount = 0;
37
+ var baseUrl = "/";
38
+ var paused = false;
39
+ var later = [];
40
+ var chunkedBackoff = 0;
41
+ var stopped;
42
+ var pollTimeout = null;
43
+ var totalAjaxFailures = 0;
44
+ var totalAjaxCalls = 0;
45
+ var lastAjax;
46
+
47
+ var isHidden = (function() {
52
48
  var prefixes = ["", "webkit", "ms", "moz"];
49
+ var hiddenProperty;
53
50
  for (var i = 0; i < prefixes.length; i++) {
54
51
  var prefix = prefixes[i];
55
52
  var check = prefix + (prefix === "" ? "hidden" : "Hidden");
@@ -57,15 +54,15 @@
57
54
  hiddenProperty = check;
58
55
  }
59
56
  }
60
- })();
61
57
 
62
- var isHidden = function() {
63
- if (hiddenProperty !== undefined) {
64
- return document[hiddenProperty];
65
- } else {
66
- return !document.hasFocus;
67
- }
68
- };
58
+ return function() {
59
+ if (hiddenProperty !== undefined) {
60
+ return document[hiddenProperty];
61
+ } else {
62
+ return !document.hasFocus;
63
+ }
64
+ };
65
+ })();
69
66
 
70
67
  var hasLocalStorage = (function() {
71
68
  try {
@@ -98,24 +95,19 @@
98
95
  return me.enableChunkedEncoding && hasonprogress;
99
96
  };
100
97
 
101
- shouldLongPoll = function() {
98
+ var shouldLongPoll = function() {
102
99
  return (
103
100
  me.alwaysLongPoll ||
104
101
  (me.shouldLongPollCallback ? me.shouldLongPollCallback() : !isHidden())
105
102
  );
106
103
  };
107
104
 
108
- var totalAjaxFailures = 0;
109
- var totalAjaxCalls = 0;
110
- var lastAjax;
111
-
112
105
  var processMessages = function(messages) {
113
106
  var gotData = false;
114
- if (!messages) return false; // server unexpectedly closed connection
107
+ if ((!messages) || (messages.length === 0)) { return false; }
115
108
 
116
109
  for (var i = 0; i < messages.length; i++) {
117
110
  var message = messages[i];
118
- gotData = true;
119
111
  for (var j = 0; j < callbacks.length; j++) {
120
112
  var callback = callbacks[j];
121
113
  if (callback.channel === message.channel) {
@@ -141,7 +133,7 @@
141
133
  }
142
134
  }
143
135
 
144
- return gotData;
136
+ return true;
145
137
  };
146
138
 
147
139
  var reqSuccess = function(messages) {
@@ -158,7 +150,7 @@
158
150
  return false;
159
151
  };
160
152
 
161
- longPoller = function(poll, data) {
153
+ var longPoller = function(poll, data) {
162
154
  if (ajaxInProgress) {
163
155
  // never allow concurrent ajax reqs
164
156
  return;
@@ -180,9 +172,7 @@
180
172
  chunked = false;
181
173
  }
182
174
 
183
- var headers = {
184
- "X-SILENCE-LOGGER": "true"
185
- };
175
+ var headers = { "X-SILENCE-LOGGER": "true" };
186
176
  for (var name in me.headers) {
187
177
  headers[name] = me.headers[name];
188
178
  }
@@ -383,11 +373,7 @@
383
373
  shouldLongPollCallback: undefined,
384
374
  baseUrl: baseUrl,
385
375
  headers: {},
386
- ajax: jQuery && jQuery.ajax,
387
- noConflict: function() {
388
- global.MessageBus = global.MessageBus.previousMessageBus;
389
- return this;
390
- },
376
+ ajax: typeof jQuery !== "undefined" && jQuery.ajax,
391
377
  diagnostics: function() {
392
378
  console.log("Stopped: " + stopped + " Started: " + started);
393
379
  console.log("Current callbacks");
@@ -429,15 +415,11 @@
429
415
 
430
416
  // Start polling
431
417
  start: function() {
432
- var poll;
433
-
434
418
  if (started) return;
435
419
  started = true;
436
420
  stopped = false;
437
421
 
438
- poll = function() {
439
- var data;
440
-
422
+ var poll = function() {
441
423
  if (stopped) {
442
424
  return;
443
425
  }
@@ -452,7 +434,7 @@
452
434
  return;
453
435
  }
454
436
 
455
- data = {};
437
+ var data = {};
456
438
  for (var i = 0; i < callbacks.length; i++) {
457
439
  data[callbacks[i].channel] = callbacks[i].last_id;
458
440
  }
@@ -466,7 +448,7 @@
466
448
 
467
449
  // monitor visibility, issue a new long poll when the page shows
468
450
  if (document.addEventListener && "hidden" in document) {
469
- me.visibilityEvent = global.document.addEventListener(
451
+ me.visibilityEvent = document.addEventListener(
470
452
  "visibilitychange",
471
453
  function() {
472
454
  if (!document.hidden && !me.longPoll && pollTimeout) {
@@ -532,7 +514,7 @@
532
514
  // TODO allow for globbing in the middle of a channel name
533
515
  // like /something/*/something
534
516
  // at the moment we only support globbing /something/*
535
- var glob;
517
+ var glob = false;
536
518
  if (channel.indexOf("*", channel.length - 1) !== -1) {
537
519
  channel = channel.substr(0, channel.length - 1);
538
520
  glob = true;
@@ -567,5 +549,5 @@
567
549
  return removed;
568
550
  }
569
551
  };
570
- global.MessageBus = me;
571
- })(window, document);
552
+ return me;
553
+ }));
@@ -149,6 +149,19 @@ module MessageBus::Implementation
149
149
  @config[:long_polling_interval] || 25 * 1000
150
150
  end
151
151
 
152
+ # @param [String] route Message bus will listen to requests on this route.
153
+ # @return [void]
154
+ def base_route=(route)
155
+ configure(base_route: route.gsub(Regexp.new('\A(?!/)|(?<!/)\Z|//+'), "/"))
156
+ end
157
+
158
+ # @return [String] the route that message bus will respond to. If not
159
+ # explicitly set, defaults to "/". Requests to "#{base_route}message-bus/*" will be handled
160
+ # by the message bus server.
161
+ def base_route
162
+ @config[:base_route] || "/"
163
+ end
164
+
152
165
  # @return [Boolean] whether the bus is disabled or not
153
166
  def off?
154
167
  @off
@@ -554,6 +567,26 @@ module MessageBus::Implementation
554
567
  @config[:keepalive_interval] || 60
555
568
  end
556
569
 
570
+ # Registers a client message filter that allows messages to be filtered from the client.
571
+ #
572
+ # @param [String,Regexp] channel_prefix channel prefix to match against a message's channel
573
+ #
574
+ # @yieldparam [MessageBus::Message] message published to the channel that matched the prefix provided
575
+ # @yieldreturn [Boolean] whether the message should be published to the client
576
+ # @return [void]
577
+ def register_client_message_filter(channel_prefix, &blk)
578
+ if blk
579
+ configure(client_message_filters: []) if !@config[:client_message_filters]
580
+ @config[:client_message_filters] << [channel_prefix, blk]
581
+ end
582
+ end
583
+
584
+ # @return [Array] returns a hash of message filters that have been registered
585
+ def client_message_filters
586
+ configure(client_message_filters: []) if !@config[:client_message_filters]
587
+ @config[:client_message_filters]
588
+ end
589
+
557
590
  private
558
591
 
559
592
  ENCODE_SITE_TOKEN = "$|$"
@@ -251,11 +251,11 @@ LUA
251
251
 
252
252
  # (see Base#global_unsubscribe)
253
253
  def global_unsubscribe
254
- if @redis_global
255
- # new connection to avoid deadlock
256
- new_redis_connection.publish(redis_channel_name, UNSUB_MESSAGE)
257
- @redis_global.disconnect
258
- @redis_global = nil
254
+ begin
255
+ new_redis = new_redis_connection
256
+ new_redis.publish(redis_channel_name, UNSUB_MESSAGE)
257
+ ensure
258
+ new_redis&.disconnect!
259
259
  end
260
260
  end
261
261
 
@@ -278,13 +278,13 @@ LUA
278
278
  end
279
279
 
280
280
  begin
281
- @redis_global = new_redis_connection
281
+ global_redis = new_redis_connection
282
282
 
283
283
  if highest_id
284
284
  clear_backlog.call(&blk)
285
285
  end
286
286
 
287
- @redis_global.subscribe(redis_channel_name) do |on|
287
+ global_redis.subscribe(redis_channel_name) do |on|
288
288
  on.subscribe do
289
289
  if highest_id
290
290
  clear_backlog.call(&blk)
@@ -298,7 +298,7 @@ LUA
298
298
 
299
299
  on.message do |_c, m|
300
300
  if m == UNSUB_MESSAGE
301
- @redis_global.unsubscribe
301
+ global_redis.unsubscribe
302
302
  return
303
303
  end
304
304
  m = MessageBus::Message.decode m
@@ -320,7 +320,10 @@ LUA
320
320
  rescue => error
321
321
  @logger.warn "#{error} subscribe failed, reconnecting in 1 second. Call stack #{error.backtrace}"
322
322
  sleep 1
323
+ global_redis&.disconnect!
323
324
  retry
325
+ ensure
326
+ global_redis&.disconnect!
324
327
  end
325
328
  end
326
329
 
@@ -128,15 +128,43 @@ class MessageBus::Client
128
128
  # @return [Boolean] whether or not the client has permission to receive the
129
129
  # passed message
130
130
  def allowed?(msg)
131
- allowed = !msg.user_ids || msg.user_ids.include?(self.user_id)
132
- allowed &&= !msg.client_ids || msg.client_ids.include?(self.client_id)
133
- allowed && (
134
- msg.group_ids.nil? ||
135
- msg.group_ids.length == 0 ||
136
- (
137
- msg.group_ids - self.group_ids
131
+ client_allowed = !msg.client_ids || msg.client_ids.length == 0 || msg.client_ids.include?(self.client_id)
132
+
133
+ user_allowed = false
134
+ group_allowed = false
135
+
136
+ # this is an inconsistency we should fix anyway, publishing `user_ids: nil` should work same as groups
137
+ has_users = msg.user_ids && msg.user_ids.length > 0
138
+ has_groups = msg.group_ids && msg.group_ids.length > 0
139
+
140
+ if has_users
141
+ user_allowed = msg.user_ids.include?(self.user_id)
142
+ end
143
+
144
+ if has_groups
145
+ group_allowed = (
146
+ msg.group_ids - (self.group_ids || [])
138
147
  ).length < msg.group_ids.length
139
- )
148
+ end
149
+
150
+ has_permission = client_allowed && (user_allowed || group_allowed || (!has_users && !has_groups))
151
+
152
+ return has_permission if !has_permission
153
+
154
+ filters_allowed = true
155
+
156
+ len = @bus.client_message_filters.length
157
+ while len > 0
158
+ len -= 1
159
+ channel_prefix, blk = @bus.client_message_filters[len]
160
+
161
+ if msg.channel.start_with?(channel_prefix)
162
+ filters_allowed = blk.call(msg)
163
+ break if !filters_allowed
164
+ end
165
+ end
166
+
167
+ filters_allowed
140
168
  end
141
169
 
142
170
  # @return [Array<MessageBus::Message>] the set of messages the client is due
@@ -15,7 +15,6 @@ class MessageBus::Diagnostics
15
15
  # process to process comms
16
16
  bus.subscribe('/_diagnostics/hup') do |msg|
17
17
  if Process.pid == msg.data["pid"] && hostname == msg.data["hostname"]
18
- $shutdown = true
19
18
  sleep 4
20
19
  Process.kill("HUP", $$)
21
20
  end
@@ -16,9 +16,9 @@ class MessageBus::Rack::Diagnostics
16
16
  # Process an HTTP request from a subscriber client
17
17
  # @param [Rack::Request::Env] env the request environment
18
18
  def call(env)
19
- return @app.call(env) unless env['PATH_INFO'].start_with? '/message-bus/_diagnostics'
19
+ return @app.call(env) unless env['PATH_INFO'].start_with? "#{@bus.base_route}message-bus/_diagnostics"
20
20
 
21
- route = env['PATH_INFO'].split('/message-bus/_diagnostics')[1]
21
+ route = env['PATH_INFO'].split("#{@bus.base_route}message-bus/_diagnostics")[1]
22
22
 
23
23
  if @bus.is_admin_lookup.nil? || !@bus.is_admin_lookup.call(env)
24
24
  return [403, {}, ['not allowed']]
@@ -45,7 +45,7 @@ class MessageBus::Rack::Diagnostics
45
45
  return [200, { 'Content-Type' => 'application/javascript;charset=UTF-8' }, [content]]
46
46
  end
47
47
 
48
- return [404, {}, ['not found']]
48
+ [404, {}, ['not found']]
49
49
  end
50
50
 
51
51
  private
@@ -92,6 +92,7 @@ class MessageBus::Rack::Diagnostics
92
92
  </body>
93
93
  </html>
94
94
  HTML
95
- return [200, { "content-type" => "text/html;" }, [html]]
95
+
96
+ [200, { "content-type" => "text/html;" }, [html]]
96
97
  end
97
98
  end
@@ -39,6 +39,10 @@ class MessageBus::Rack::Middleware
39
39
  @bus = config[:message_bus] || MessageBus
40
40
  @connection_manager = MessageBus::ConnectionManager.new(@bus)
41
41
  @started_listener = false
42
+ @base_route = "#{@bus.base_route}message-bus/"
43
+ @base_route_length = @base_route.length
44
+ @diagnostics_route = "#{@base_route}_diagnostics"
45
+ @broadcast_route = "#{@base_route}broadcast"
42
46
  start_listener unless @bus.off?
43
47
  end
44
48
 
@@ -54,7 +58,7 @@ class MessageBus::Rack::Middleware
54
58
  # Process an HTTP request from a subscriber client
55
59
  # @param [Rack::Request::Env] env the request environment
56
60
  def call(env)
57
- return @app.call(env) unless env['PATH_INFO'] =~ /^\/message-bus\//
61
+ return @app.call(env) unless env['PATH_INFO'].start_with? @base_route
58
62
 
59
63
  handle_request(env)
60
64
  end
@@ -63,18 +67,18 @@ class MessageBus::Rack::Middleware
63
67
 
64
68
  def handle_request(env)
65
69
  # special debug/test route
66
- if @bus.allow_broadcast? && env['PATH_INFO'] == '/message-bus/broadcast'
70
+ if @bus.allow_broadcast? && env['PATH_INFO'] == @broadcast_route
67
71
  parsed = Rack::Request.new(env)
68
72
  @bus.publish parsed["channel"], parsed["data"]
69
73
  return [200, { "Content-Type" => "text/html" }, ["sent"]]
70
74
  end
71
75
 
72
- if env['PATH_INFO'].start_with? '/message-bus/_diagnostics'
76
+ if env['PATH_INFO'].start_with? @diagnostics_route
73
77
  diags = MessageBus::Rack::Diagnostics.new(@app, message_bus: @bus)
74
78
  return diags.call(env)
75
79
  end
76
80
 
77
- client_id = env['PATH_INFO'].split("/")[2]
81
+ client_id = env['PATH_INFO'][@base_route_length..-1].split("/")[0]
78
82
  return [404, {}, ["not found"]] unless client_id
79
83
 
80
84
  user_id = @bus.user_id_lookup.call(env) if @bus.user_id_lookup
@@ -11,27 +11,29 @@ class MessageBus::Rails::Railtie < ::Rails::Railtie
11
11
  # the Rails app is configured that might be ActionDispatch::Session::CookieStore, or potentially
12
12
  # ActionDispatch::Session::ActiveRecordStore.
13
13
  #
14
- # To handle either case, we insert it before ActionDispatch::Flash.
15
- #
16
- # For APIs or apps that have ActionDispatch::Flash deleted from the middleware
17
- # stack we just push MessageBus to the bottom.
18
- if api_only?(app.config) || flash_middleware_deleted?(app.middleware)
19
- app.middleware.use(MessageBus::Rack::Middleware)
20
- else
21
- app.middleware.insert_before(ActionDispatch::Flash, MessageBus::Rack::Middleware)
14
+ # given https://github.com/rails/rails/commit/fedde239dcee256b417dc9bcfe5fef603bf0d952#diff-533a9a9cc17a8a899cb830626089e5f9
15
+ # there is no way of walking the stack for operations
16
+ if !skip_middleware?(app.config)
17
+ if api_only?(app.config)
18
+ app.middleware.use(MessageBus::Rack::Middleware)
19
+ else
20
+ app.middleware.insert_before(ActionDispatch::Flash, MessageBus::Rack::Middleware)
21
+ end
22
22
  end
23
23
 
24
24
  MessageBus.logger = Rails.logger
25
25
  end
26
26
 
27
+ def skip_middleware?(config)
28
+ return false if !config.respond_to?(:skip_message_bus_middleware)
29
+
30
+ config.skip_message_bus_middleware
31
+ end
32
+
27
33
  def api_only?(config)
28
- return false unless config.respond_to?(:api_only)
34
+ return false if !config.respond_to?(:api_only)
29
35
 
30
36
  config.api_only
31
37
  end
32
38
 
33
- def flash_middleware_deleted?(middleware)
34
- ops = middleware.instance_variable_get(:@operations)
35
- ops.any? { |m| m[0] == :delete && m[1].include?(ActionDispatch::Flash) }
36
- end
37
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MessageBus
4
- VERSION = "2.2.4"
4
+ VERSION = "3.3.1"
5
5
  end
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "message-bus-client",
3
+ "version": "3.3.0",
4
+ "description": "A message bus client in Javascript",
5
+ "main": "assets/message-bus.js",
6
+ "keywords": "es6, modules",
7
+ "files": ["assets/message-bus.js"],
8
+ "jsnext:main": "assets/message-bus.js",
9
+ "module": "assets/message-bus.js",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/discourse/message_bus.git"
13
+ },
14
+ "author": "Sam Saffron, Robin Ward",
15
+ "license": "MIT",
16
+ "bugs": {
17
+ "url": "https://github.com/discourse/message_bus/issues"
18
+ },
19
+ "homepage": "https://github.com/discourse/message_bus#readme"
20
+ }
@@ -75,15 +75,6 @@ describe("Messagebus", function() {
75
75
  })
76
76
  });
77
77
 
78
- it('removes itself from root namespace when noConflict is called', function(){
79
- expect(window.MessageBus).not.toBeUndefined();
80
- var mb = window.MessageBus;
81
- expect(mb).toEqual(window.MessageBus.noConflict());
82
- expect(window.MessageBus).toBeUndefined();
83
- // reset it so afterEach has something to work on
84
- window.MessageBus = mb;
85
- });
86
-
87
78
  it('respects minPollInterval setting with defaults', function(){
88
79
  expect(MessageBus.minPollInterval).toEqual(100);
89
80
  MessageBus.minPollInterval = 1000;
@@ -169,37 +169,220 @@ describe MessageBus::Client do
169
169
  log[0].data.must_equal 'world'
170
170
  end
171
171
 
172
- it "allows only client_id in list if message contains client_ids" do
173
- @message = MessageBus::Message.new(1, 2, '/test', 'hello')
174
- @message.client_ids = ["1", "2"]
175
- @client.client_id = "2"
176
- @client.allowed?(@message).must_equal true
177
-
178
- @client.client_id = "3"
179
- @client.allowed?(@message).must_equal false
180
- end
181
-
182
- describe "targetted at group" do
183
- before do
172
+ describe '#allowed?' do
173
+ it "allows only client_id in list if message contains client_ids" do
184
174
  @message = MessageBus::Message.new(1, 2, '/test', 'hello')
185
- @message.group_ids = [1, 2, 3]
186
- end
175
+ @message.client_ids = ["1", "2"]
176
+ @client.client_id = "2"
177
+ @client.allowed?(@message).must_equal true
187
178
 
188
- it "denies users that are not members of group" do
189
- @client.group_ids = [77, 0, 10]
179
+ @client.client_id = "3"
190
180
  @client.allowed?(@message).must_equal false
191
- end
192
181
 
193
- it "allows users that are members of group" do
194
- @client.group_ids = [1, 2, 3]
182
+ @message.client_ids = []
183
+
184
+ @client.client_id = "3"
195
185
  @client.allowed?(@message).must_equal true
196
- end
197
186
 
198
- it "allows all users if groups not set" do
199
- @message.group_ids = nil
200
- @client.group_ids = [77, 0, 10]
187
+ @message.client_ids = nil
188
+
189
+ @client.client_id = "3"
201
190
  @client.allowed?(@message).must_equal true
202
191
  end
192
+
193
+ describe 'targetted at user' do
194
+ before do
195
+ @message = MessageBus::Message.new(1, 2, '/test', 'hello')
196
+ @message.user_ids = [1, 2, 3]
197
+ end
198
+
199
+ it "allows client with user_id that is included in message's user_ids" do
200
+ @client.user_id = 1
201
+ @client.allowed?(@message).must_equal(true)
202
+ end
203
+
204
+ it "denies client with user_id that is not included in message's user_ids" do
205
+ @client.user_id = 4
206
+ @client.allowed?(@message).must_equal(false)
207
+ end
208
+
209
+ it "denies client with nil user_id" do
210
+ @client.user_id = nil
211
+
212
+ @client.allowed?(@message).must_equal(false)
213
+ end
214
+
215
+ it "allows client if message's user_ids is not set" do
216
+ @message.user_ids = nil
217
+ @client.user_id = 4
218
+ @client.allowed?(@message).must_equal(true)
219
+ end
220
+
221
+ it "allows client if message's user_ids is empty" do
222
+ @message.user_ids = []
223
+ @client.user_id = 4
224
+ @client.allowed?(@message).must_equal(true)
225
+ end
226
+
227
+ it "allows client with client_id that is included in message's client_ids" do
228
+ @message.client_ids = ["1", "2"]
229
+ @client.client_id = "1"
230
+ @client.user_id = 1
231
+
232
+ @client.allowed?(@message).must_equal(true)
233
+ end
234
+
235
+ it "denies client with client_id that is not included in message's client_ids" do
236
+ @message.client_ids = ["1", "2"]
237
+ @client.client_id = "3"
238
+ @client.user_id = 1
239
+
240
+ @client.allowed?(@message).must_equal(false)
241
+ end
242
+ end
243
+
244
+ describe "targetted at group" do
245
+ before do
246
+ @message = MessageBus::Message.new(1, 2, '/test', 'hello')
247
+ @message.group_ids = [1, 2, 3]
248
+ end
249
+
250
+ it "denies client that are not members of group" do
251
+ @client.group_ids = [77, 0, 10]
252
+ @client.allowed?(@message).must_equal false
253
+ end
254
+
255
+ it 'denies client with nil group_ids' do
256
+ @client.group_ids = nil
257
+ @client.allowed?(@message).must_equal false
258
+ end
259
+
260
+ it "allows client that are members of group" do
261
+ @client.group_ids = [1, 2, 3]
262
+ @client.allowed?(@message).must_equal true
263
+ end
264
+
265
+ it "allows any client if message's group_ids is not set" do
266
+ @message.group_ids = nil
267
+ @client.group_ids = [77, 0, 10]
268
+ @client.allowed?(@message).must_equal true
269
+ end
270
+
271
+ it "allows any client if message's group_ids is empty" do
272
+ @message.group_ids = []
273
+ @client.group_ids = [77, 0, 10]
274
+ @client.allowed?(@message).must_equal true
275
+ end
276
+
277
+ it "allows client with client_id that is included in message's client_ids" do
278
+ @message.client_ids = ["1", "2"]
279
+ @client.client_id = "1"
280
+ @client.group_ids = [1]
281
+
282
+ @client.allowed?(@message).must_equal(true)
283
+ end
284
+
285
+ it "denies client with client_id that is not included in message's client_ids" do
286
+ @message.client_ids = ["1", "2"]
287
+ @client.client_id = "3"
288
+ @client.group_ids = [1]
289
+
290
+ @client.allowed?(@message).must_equal(false)
291
+ end
292
+ end
293
+
294
+ describe 'targetted at group and user' do
295
+ before do
296
+ @message = MessageBus::Message.new(1, 2, '/test', 'hello')
297
+ @message.group_ids = [1, 2, 3]
298
+ @message.user_ids = [4, 5, 6]
299
+ end
300
+
301
+ it "allows client with user_id that is included in message's user_ids" do
302
+ @client.user_id = 4
303
+ @client.allowed?(@message).must_equal(true)
304
+ end
305
+
306
+ it "denies client with user_id that is not included in message's user_ids" do
307
+ @client.user_id = 1
308
+ @client.allowed?(@message).must_equal(false)
309
+ end
310
+
311
+ it "allows client with group_ids that is included in message's group_ids" do
312
+ @client.group_ids = [1, 0, 3]
313
+ @client.allowed?(@message).must_equal(true)
314
+ end
315
+
316
+ it "denies client with group_ids that is not included in message's group_ids" do
317
+ @client.group_ids = [8, 9, 10]
318
+ @client.allowed?(@message).must_equal(false)
319
+ end
320
+
321
+ it "allows client with allowed client_id and user_id" do
322
+ @message.client_ids = ["1", "2"]
323
+ @client.user_id = 4
324
+ @client.client_id = "2"
325
+
326
+ @client.allowed?(@message).must_equal(true)
327
+ end
328
+
329
+ it "denies client with allowed client_id but disallowed user_id" do
330
+ @message.client_ids = ["1", "2"]
331
+ @client.user_id = 99
332
+ @client.client_id = "2"
333
+
334
+ @client.allowed?(@message).must_equal(false)
335
+ end
336
+
337
+ it "allows client with allowed client_id and group_id" do
338
+ @message.client_ids = ["1", "2"]
339
+ @client.group_ids = [1]
340
+ @client.client_id = "2"
341
+
342
+ @client.allowed?(@message).must_equal(true)
343
+ end
344
+
345
+ it "denies client with allowed client_id but disallowed group_id" do
346
+ @message.client_ids = ["1", "2"]
347
+ @client.group_ids = [99]
348
+ @client.client_id = "2"
349
+
350
+ @client.allowed?(@message).must_equal(false)
351
+ end
352
+ end
353
+
354
+ describe 'when MessageBus#client_message_filters has been configured' do
355
+
356
+ it 'filters messages correctly' do
357
+ message = MessageBus::Message.new(1, 2, '/test/5', 'hello')
358
+ @client.allowed?(message).must_equal(true)
359
+
360
+ @bus.register_client_message_filter('/test') do |m|
361
+ m.data != 'hello'
362
+ end
363
+
364
+ @client.allowed?(message).must_equal(false)
365
+ end
366
+
367
+ it 'filters messages correctly when multiple filters have been configured' do
368
+
369
+ bob_message = MessageBus::Message.new(1, 2, '/test/5', 'bob')
370
+ fred_message = MessageBus::Message.new(1, 2, '/test/5', 'fred')
371
+ random_message = MessageBus::Message.new(1, 2, '/test/77', 'random')
372
+
373
+ @bus.register_client_message_filter('/test') do |message|
374
+ message.data == 'bob' || message.data == 'fred'
375
+ end
376
+
377
+ @bus.register_client_message_filter('/test') do |message|
378
+ message.data == 'fred'
379
+ end
380
+
381
+ @client.allowed?(fred_message).must_equal(true)
382
+ @client.allowed?(bob_message).must_equal(false)
383
+ @client.allowed?(random_message).must_equal(false)
384
+ end
385
+ end
203
386
  end
204
387
  end
205
388
  end
@@ -8,11 +8,13 @@ require 'rack/test'
8
8
  describe MessageBus::Rack::Middleware do
9
9
  include Rack::Test::Methods
10
10
  let(:extra_middleware) { nil }
11
+ let(:base_route) { nil }
11
12
 
12
13
  before do
13
14
  bus = @bus = MessageBus::Instance.new
14
15
  @bus.configure(MESSAGE_BUS_CONFIG)
15
16
  @bus.long_polling_enabled = false
17
+ @bus.base_route = base_route if base_route
16
18
 
17
19
  e_m = extra_middleware
18
20
  builder = Rack::Builder.new {
@@ -44,12 +46,27 @@ describe MessageBus::Rack::Middleware do
44
46
  @bus.long_polling_enabled = true
45
47
  end
46
48
 
49
+ describe "with altered base_route" do
50
+ let(:base_route) { "/base/route/" }
51
+
52
+ it "should respond as normal" do
53
+ post "/base/route/message-bus/ABC?dlp=t", '/foo1' => 0
54
+ @async_middleware.in_async?.must_equal false
55
+ last_response.ok?.must_equal true
56
+ end
57
+ end
58
+
47
59
  it "should respond right away if dlp=t" do
48
60
  post "/message-bus/ABC?dlp=t", '/foo1' => 0
49
61
  @async_middleware.in_async?.must_equal false
50
62
  last_response.ok?.must_equal true
51
63
  end
52
64
 
65
+ it "should respond with a 404 if the client_id is missing" do
66
+ post "/message-bus/?dlp=t", '/foo1' => 0
67
+ last_response.not_found?.must_equal true
68
+ end
69
+
53
70
  it "should respond right away to long polls that are polling on -1 with the last_id" do
54
71
  post "/message-bus/ABC", '/foo' => -1
55
72
  last_response.ok?.must_equal true
@@ -141,6 +158,19 @@ describe MessageBus::Rack::Middleware do
141
158
  last_response.status.must_equal 200
142
159
  end
143
160
 
161
+ describe "with an altered base_route" do
162
+ let(:base_route) { "/base/route/" }
163
+
164
+ it "should get a 200 with html for an authorized user" do
165
+ def @bus.is_admin_lookup
166
+ proc { |_| true }
167
+ end
168
+
169
+ get "/base/route/message-bus/_diagnostics"
170
+ last_response.status.must_equal 200
171
+ end
172
+ end
173
+
144
174
  it "should get the script it asks for" do
145
175
 
146
176
  def @bus.is_admin_lookup
@@ -243,6 +273,38 @@ describe MessageBus::Rack::Middleware do
243
273
  parsed[1]["data"].must_equal "borbs"
244
274
  end
245
275
 
276
+ it "should use the correct client ID" do
277
+ id = @bus.last_id('/foo')
278
+
279
+ client_id = "aBc123"
280
+ @bus.publish("/foo", "msg1", client_ids: [client_id])
281
+ @bus.publish("/foo", "msg2", client_ids: ["not_me#{client_id}"])
282
+
283
+ post "/message-bus/#{client_id}",
284
+ '/foo' => id
285
+
286
+ parsed = JSON.parse(last_response.body)
287
+ parsed.length.must_equal 2
288
+ parsed[0]["data"].must_equal("msg1")
289
+ parsed[1]["data"].wont_equal("msg2")
290
+ end
291
+
292
+ it "should use the correct client ID with additional path" do
293
+ id = @bus.last_id('/foo')
294
+
295
+ client_id = "aBc123"
296
+ @bus.publish("/foo", "msg1", client_ids: [client_id])
297
+ @bus.publish("/foo", "msg2", client_ids: ["not_me#{client_id}"])
298
+
299
+ post "/message-bus/#{client_id}/path/not/needed",
300
+ '/foo' => id
301
+
302
+ parsed = JSON.parse(last_response.body)
303
+ parsed.length.must_equal 2
304
+ parsed[0]["data"].must_equal("msg1")
305
+ parsed[1]["data"].wont_equal("msg2")
306
+ end
307
+
246
308
  it "should have no cross talk" do
247
309
  seq = 0
248
310
  @bus.site_id_lookup do
@@ -37,6 +37,23 @@ describe MessageBus do
37
37
  @bus.after_fork
38
38
  end
39
39
 
40
+ describe "#base_route=" do
41
+ it "adds leading and trailing slashes" do
42
+ @bus.base_route = "my/base/route"
43
+ @bus.base_route.must_equal '/my/base/route/'
44
+ end
45
+
46
+ it "leaves existing leading and trailing slashes" do
47
+ @bus.base_route = "/my/base/route/"
48
+ @bus.base_route.must_equal '/my/base/route/'
49
+ end
50
+
51
+ it "removes duplicate slashes" do
52
+ @bus.base_route = "//my///base/route"
53
+ @bus.base_route.must_equal '/my/base/route/'
54
+ end
55
+ end
56
+
40
57
  it "can subscribe from a point in time" do
41
58
  @bus.publish("/minion", "banana")
42
59
 
@@ -292,4 +309,19 @@ describe MessageBus do
292
309
 
293
310
  data.must_equal(["pre-fork", "from-fork", "continuation"])
294
311
  end
312
+
313
+ describe '#register_client_message_filter' do
314
+ it 'should register the message filter correctly' do
315
+ @bus.register_client_message_filter('/test')
316
+
317
+ @bus.client_message_filters.must_equal([])
318
+
319
+ @bus.register_client_message_filter('/test') { puts "hello world" }
320
+
321
+ channel, blk = @bus.client_message_filters[0]
322
+
323
+ blk.must_respond_to(:call)
324
+ channel.must_equal('/test')
325
+ end
326
+ end
295
327
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: message_bus
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.4
4
+ version: 3.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-26 00:00:00.000000000 Z
11
+ date: 2020-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -113,6 +113,7 @@ files:
113
113
  - lib/message_bus/timer_thread.rb
114
114
  - lib/message_bus/version.rb
115
115
  - message_bus.gemspec
116
+ - package.json
116
117
  - spec/assets/SpecHelper.js
117
118
  - spec/assets/message-bus.spec.js
118
119
  - spec/assets/support/jasmine.yml
@@ -155,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
156
  - !ruby/object:Gem::Version
156
157
  version: '0'
157
158
  requirements: []
158
- rubygems_version: 3.0.3
159
+ rubygems_version: 3.1.2
159
160
  signing_key:
160
161
  specification_version: 4
161
162
  summary: ''