message_bus 2.2.3 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -1
  3. data/.travis.yml +1 -1
  4. data/CHANGELOG +39 -0
  5. data/Gemfile +8 -3
  6. data/Guardfile +1 -0
  7. data/README.md +62 -11
  8. data/Rakefile +5 -0
  9. data/assets/message-bus.js +64 -78
  10. data/examples/bench/config.ru +1 -0
  11. data/examples/bench/puma.rb +1 -0
  12. data/examples/bench/unicorn.conf.rb +1 -0
  13. data/examples/chat/Gemfile +1 -0
  14. data/examples/chat/chat.rb +2 -0
  15. data/examples/chat/config.ru +2 -0
  16. data/examples/diagnostics/Gemfile +1 -0
  17. data/examples/diagnostics/config.ru +1 -0
  18. data/examples/minimal/Gemfile +1 -0
  19. data/examples/minimal/config.ru +1 -0
  20. data/lib/message_bus.rb +33 -0
  21. data/lib/message_bus/backends/redis.rb +11 -8
  22. data/lib/message_bus/client.rb +36 -8
  23. data/lib/message_bus/diagnostics.rb +1 -1
  24. data/lib/message_bus/em_ext.rb +1 -0
  25. data/lib/message_bus/http_client.rb +2 -1
  26. data/lib/message_bus/http_client/channel.rb +1 -0
  27. data/lib/message_bus/rack/diagnostics.rb +5 -4
  28. data/lib/message_bus/rack/middleware.rb +8 -4
  29. data/lib/message_bus/rails/railtie.rb +15 -13
  30. data/lib/message_bus/version.rb +1 -1
  31. data/package.json +20 -0
  32. data/spec/assets/message-bus.spec.js +0 -9
  33. data/spec/assets/support/jasmine_helper.rb +1 -0
  34. data/spec/fixtures/test/Gemfile +1 -0
  35. data/spec/fixtures/test/config.ru +1 -0
  36. data/spec/helpers.rb +1 -0
  37. data/spec/integration/http_client_spec.rb +2 -0
  38. data/spec/lib/fake_async_middleware.rb +3 -2
  39. data/spec/lib/message_bus/assets/asset_encoding_spec.rb +1 -0
  40. data/spec/lib/message_bus/backend_spec.rb +2 -0
  41. data/spec/lib/message_bus/client_spec.rb +208 -23
  42. data/spec/lib/message_bus/connection_manager_spec.rb +3 -1
  43. data/spec/lib/message_bus/distributed_cache_spec.rb +2 -0
  44. data/spec/lib/message_bus/multi_process_spec.rb +2 -0
  45. data/spec/lib/message_bus/rack/middleware_spec.rb +63 -0
  46. data/spec/lib/message_bus/timer_thread_spec.rb +2 -0
  47. data/spec/lib/message_bus_spec.rb +34 -0
  48. data/spec/performance/publish.rb +2 -0
  49. data/spec/spec_helper.rb +3 -1
  50. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46de79e771fac3fb31c3edc55b0866842b6dc108df65c5bcbd7d1062568a6279
4
- data.tar.gz: 201cb983ab710b1fe487f69105a13f0291873bf94c8a5e16f1749993a22ddc74
3
+ metadata.gz: 659722c1ea3a6a805dbe81099ee2848a11ab7ebffaf8612d119f5062fd740337
4
+ data.tar.gz: 4b187421f5fb4bdff177d117b059c05314305729885f5ec520127ad827db5369
5
5
  SHA512:
6
- metadata.gz: 633808d11a69d4f6e7c8e53f4805c6bff2d5cd5e57b4c38a166af355e8c6cd2e423c6b27cb0f861597cda6b65dedb0c2bf46edfe5b7fada98b79e8f1bed7d38a
7
- data.tar.gz: d38cb6e67c791a9e1e3dcaef8764517e47a363fa4045aed35eedff84cdbdf2a7da85bf0e55a2f00c4c877612bf46ce3dbaea430786afb6dd903ae090b4144638
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,42 @@
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
+
34
+ 26-03-2020
35
+
36
+ - Version 2.2.4
37
+
38
+ - FEATURE: shouldLongPollCallback optional setting which allows overriding decision about long polling
39
+
1
40
  18-10-2019
2
41
 
3
42
  - Version 2.2.3
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  source 'https://rubygems.org'
2
3
 
3
4
  # Specify your gem's dependencies in message_bus.gemspec
@@ -6,6 +7,7 @@ gemspec
6
7
  group :test do
7
8
  gem 'minitest'
8
9
  gem 'minitest-hooks'
10
+ gem 'minitest-global_expectations'
9
11
  gem 'rake'
10
12
  gem 'http_parser.rb'
11
13
  gem 'thin'
@@ -18,8 +20,11 @@ group :test, :development do
18
20
  gem 'byebug'
19
21
  end
20
22
 
23
+ group :development do
24
+ gem 'yard'
25
+ gem 'rubocop-discourse', require: false
26
+ gem 'rubocop-rspec', require: false
27
+ end
28
+
21
29
  gem 'rack'
22
30
  gem 'concurrent-ruby' # for distributed-cache
23
-
24
- gem 'rubocop'
25
- gem 'yard'
data/Guardfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # A sample Guardfile
2
3
  # More info at https://github.com/guard/guard#readme
3
4
 
data/README.md CHANGED
@@ -8,6 +8,8 @@ Since long-polling is implemented using Rack Hijack and Thin::Async, all common
8
8
 
9
9
  MessageBus is implemented as Rack middleware and can be used by any Rails / Sinatra or pure Rack application.
10
10
 
11
+ Read the generated docs: <https://www.rubydoc.info/gems/message_bus>
12
+
11
13
  ## Try it out!
12
14
 
13
15
  Live chat demo per [examples/chat](https://github.com/SamSaffron/message_bus/tree/master/examples/chat) is at:
@@ -72,32 +74,40 @@ id = MessageBus.last_id("/channel")
72
74
  MessageBus.backlog "/channel", id
73
75
  ```
74
76
 
75
- ```ruby
76
- # messages can be targetted at particular users or groups
77
- 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.
78
80
 
79
- # messages can be targetted at particular clients (using MessageBus.clientId)
80
- MessageBus.publish "/channel", "hello", client_ids: ["XXX","YYY"]
81
+ ```ruby
82
+ MessageBus.publish "/channel", "hello", client_ids: ["XXX", "YYY"] # (using MessageBus.clientId)
83
+ ```
81
84
 
82
- # 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.
83
86
 
87
+ ```ruby
84
88
  MessageBus.configure(user_id_lookup: proc do |env|
85
89
  # this lookup occurs on JS-client poolings, so that server can retrieve backlog
86
90
  # for the client considering/matching/filtering user_ids set on published messages
87
91
  # if user_id is not set on publish time, any user_id returned here will receive the message
88
-
89
92
  # return the user id here
90
93
  end)
91
94
 
95
+ # Target user_ids when publishing a message
96
+ MessageBus.publish "/channel", "hello", user_ids: [1, 2, 3]
97
+
92
98
  MessageBus.configure(group_ids_lookup: proc do |env|
93
99
  # return the group ids the user belongs to
94
100
  # can be nil or []
95
101
  end)
96
102
 
97
- # 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:
98
107
  # config/inializers/message_bus.rb
99
108
  MessageBus.user_id_lookup do |env|
100
109
  req = Rack::Request.new(env)
110
+
101
111
  if req.session && req.session["warden.user.user.key"] && req.session["warden.user.user.key"][0][0]
102
112
  user = User.find(req.session["warden.user.user.key"][0][0])
103
113
  user.id
@@ -105,6 +115,36 @@ MessageBus.user_id_lookup do |env|
105
115
  end
106
116
  ```
107
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
+
108
148
  ```ruby
109
149
  MessageBus.configure(on_middleware_error: proc do |env, e|
110
150
  # If you wish to add special handling based on error
@@ -278,7 +318,8 @@ backgroundCallbackInterval|60000|Interval to poll when long polling is disabled
278
318
  minPollInterval|100|When polling requests succeed, this is the minimum amount of time to wait before making the next request.
279
319
  maxPollInterval|180000|If request to the server start failing, MessageBus will backoff, this is the upper limit of the backoff.
280
320
  alwaysLongPoll|false|For debugging you may want to disable the "is browser in background" check and always long-poll
281
- baseUrl|/|If message bus is mounted at a sub-path or different domain, you may configure it to perform requests there
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.
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.
282
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
283
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.
284
325
  minHiddenPollInterval|1500|Time to wait between poll requests performed by background or hidden tabs and windows, shared state via localStorage
@@ -300,8 +341,6 @@ enableChunkedEncoding|true|Allows streaming of message bus data over the HTTP co
300
341
 
301
342
  `MessageBus.status()` : Returns status (started, paused, stopped)
302
343
 
303
- `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.
304
-
305
344
  `MessageBus.diagnostics()` : Returns a log that may be used for diagnostics on the status of message bus.
306
345
 
307
346
  #### Ruby
@@ -561,6 +600,18 @@ MessageBus.configure(on_middleware_error: proc do |env, e|
561
600
  end)
562
601
  ```
563
602
 
603
+ ### Adding extra response headers
604
+
605
+ In e.g. `config/initializers/message_bus.rb`:
606
+
607
+ ```ruby
608
+ MessageBus.extra_response_headers_lookup do |env|
609
+ [
610
+ ["Access-Control-Allow-Origin", "http://example.com:3000"],
611
+ ]
612
+ end
613
+ ```
614
+
564
615
  ## How it works
565
616
 
566
617
  MessageBus provides durable messaging following the publish-subscribe (pubsub) pattern to subscribers who track their own subscriptions. Durability is by virtue of the persistence of messages in backlogs stored in the selected backend implementation (Redis, Postgres, etc) which can be queried up until a configurable expiry. Subscribers must keep track of the ID of the last message they processed, and request only more-recent messages in subsequent connections.
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rubygems'
2
3
  require 'rake/testtask'
3
4
  require 'bundler'
@@ -91,3 +92,7 @@ end
91
92
 
92
93
  desc "Run all tests, link checks and confirms documentation compiles without error"
93
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,21 +95,19 @@
98
95
  return me.enableChunkedEncoding && hasonprogress;
99
96
  };
100
97
 
101
- shouldLongPoll = function() {
102
- return me.alwaysLongPoll || !isHidden();
98
+ var shouldLongPoll = function() {
99
+ return (
100
+ me.alwaysLongPoll ||
101
+ (me.shouldLongPollCallback ? me.shouldLongPollCallback() : !isHidden())
102
+ );
103
103
  };
104
104
 
105
- var totalAjaxFailures = 0;
106
- var totalAjaxCalls = 0;
107
- var lastAjax;
108
-
109
105
  var processMessages = function(messages) {
110
106
  var gotData = false;
111
- if (!messages) return false; // server unexpectedly closed connection
107
+ if ((!messages) || (messages.length === 0)) { return false; }
112
108
 
113
109
  for (var i = 0; i < messages.length; i++) {
114
110
  var message = messages[i];
115
- gotData = true;
116
111
  for (var j = 0; j < callbacks.length; j++) {
117
112
  var callback = callbacks[j];
118
113
  if (callback.channel === message.channel) {
@@ -138,7 +133,7 @@
138
133
  }
139
134
  }
140
135
 
141
- return gotData;
136
+ return true;
142
137
  };
143
138
 
144
139
  var reqSuccess = function(messages) {
@@ -155,7 +150,7 @@
155
150
  return false;
156
151
  };
157
152
 
158
- longPoller = function(poll, data) {
153
+ var longPoller = function(poll, data) {
159
154
  if (ajaxInProgress) {
160
155
  // never allow concurrent ajax reqs
161
156
  return;
@@ -177,9 +172,7 @@
177
172
  chunked = false;
178
173
  }
179
174
 
180
- var headers = {
181
- "X-SILENCE-LOGGER": "true"
182
- };
175
+ var headers = { "X-SILENCE-LOGGER": "true" };
183
176
  for (var name in me.headers) {
184
177
  headers[name] = me.headers[name];
185
178
  }
@@ -377,13 +370,10 @@
377
370
  callbacks: callbacks,
378
371
  clientId: clientId,
379
372
  alwaysLongPoll: false,
373
+ shouldLongPollCallback: undefined,
380
374
  baseUrl: baseUrl,
381
375
  headers: {},
382
- ajax: jQuery && jQuery.ajax,
383
- noConflict: function() {
384
- global.MessageBus = global.MessageBus.previousMessageBus;
385
- return this;
386
- },
376
+ ajax: typeof jQuery !== "undefined" && jQuery.ajax,
387
377
  diagnostics: function() {
388
378
  console.log("Stopped: " + stopped + " Started: " + started);
389
379
  console.log("Current callbacks");
@@ -425,15 +415,11 @@
425
415
 
426
416
  // Start polling
427
417
  start: function() {
428
- var poll;
429
-
430
418
  if (started) return;
431
419
  started = true;
432
420
  stopped = false;
433
421
 
434
- poll = function() {
435
- var data;
436
-
422
+ var poll = function() {
437
423
  if (stopped) {
438
424
  return;
439
425
  }
@@ -448,7 +434,7 @@
448
434
  return;
449
435
  }
450
436
 
451
- data = {};
437
+ var data = {};
452
438
  for (var i = 0; i < callbacks.length; i++) {
453
439
  data[callbacks[i].channel] = callbacks[i].last_id;
454
440
  }
@@ -462,7 +448,7 @@
462
448
 
463
449
  // monitor visibility, issue a new long poll when the page shows
464
450
  if (document.addEventListener && "hidden" in document) {
465
- me.visibilityEvent = global.document.addEventListener(
451
+ me.visibilityEvent = document.addEventListener(
466
452
  "visibilitychange",
467
453
  function() {
468
454
  if (!document.hidden && !me.longPoll && pollTimeout) {
@@ -528,7 +514,7 @@
528
514
  // TODO allow for globbing in the middle of a channel name
529
515
  // like /something/*/something
530
516
  // at the moment we only support globbing /something/*
531
- var glob;
517
+ var glob = false;
532
518
  if (channel.indexOf("*", channel.length - 1) !== -1) {
533
519
  channel = channel.substr(0, channel.length - 1);
534
520
  glob = true;
@@ -563,5 +549,5 @@
563
549
  return removed;
564
550
  }
565
551
  };
566
- global.MessageBus = me;
567
- })(window, document);
552
+ return me;
553
+ }));