message_bus 3.3.7 → 3.3.8
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +48 -20
- data/.prettierrc +1 -0
- data/lib/message_bus/backends/base.rb +5 -0
- data/lib/message_bus/backends/memory.rb +6 -0
- data/lib/message_bus/backends/postgres.rb +20 -13
- data/lib/message_bus/backends/redis.rb +6 -1
- data/lib/message_bus/client.rb +3 -0
- data/lib/message_bus/distributed_cache.rb +1 -0
- data/lib/message_bus/rack/thin_ext.rb +1 -0
- data/lib/message_bus/version.rb +1 -1
- data/lib/message_bus.rb +5 -0
- data/message_bus.gemspec +4 -1
- data/package.json +2 -5
- data/spec/helpers.rb +6 -1
- data/spec/lib/fake_async_middleware.rb +1 -0
- data/spec/lib/message_bus/backend_spec.rb +1 -0
- data/spec/lib/message_bus/client_spec.rb +1 -0
- data/spec/lib/message_bus/connection_manager_spec.rb +4 -0
- data/spec/lib/message_bus/multi_process_spec.rb +18 -8
- data/spec/lib/message_bus/rack/middleware_spec.rb +2 -1
- data/spec/lib/message_bus_spec.rb +2 -1
- data/vendor/assets/javascripts/message-bus-ajax.js +38 -0
- data/vendor/assets/javascripts/message-bus.js +549 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b45c0d65f5a82c9465f620059d2125ebca09ee5619a9094fcbc309a0e179a983
|
4
|
+
data.tar.gz: b691c40ae2360ae661aa83f83e9647cb0223a70a6ca536f285db21ce791a3ae3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2bb5db140d0fc8bbee5df6e00f57560b1e399c7cac86d825f9841c4c9d8bc805ec8f9ab2ea8ba0ad066c53039a44b5044ecef7299a74f942b3a52cd426ecbef6
|
7
|
+
data.tar.gz: 66fdc2a3c483d1b775b3963b71bdc422edcdf16233cb47aa2ec99d3a6946e6e0f6f6ca917db12e5d528de6dfb39cc494923e2abe380ca0c75d64d92f6f167d7e
|
data/.github/workflows/ci.yml
CHANGED
@@ -1,16 +1,10 @@
|
|
1
|
-
name:
|
1
|
+
name: CI
|
2
2
|
|
3
3
|
on:
|
4
|
-
pull_request:
|
5
4
|
push:
|
6
5
|
branches:
|
7
6
|
- main
|
8
|
-
|
9
|
-
env:
|
10
|
-
PGHOST: localhost
|
11
|
-
PGPORT: 5432
|
12
|
-
PGPASSWORD: postgres
|
13
|
-
PGUSER: postgres
|
7
|
+
pull_request:
|
14
8
|
|
15
9
|
jobs:
|
16
10
|
build:
|
@@ -18,10 +12,22 @@ jobs:
|
|
18
12
|
name: Ruby ${{ matrix.ruby }} (redis ${{ matrix.redis }})
|
19
13
|
timeout-minutes: 10
|
20
14
|
|
15
|
+
env:
|
16
|
+
PGHOST: localhost
|
17
|
+
PGPASSWORD: postgres
|
18
|
+
PGUSER: postgres
|
19
|
+
|
20
|
+
strategy:
|
21
|
+
fail-fast: false
|
22
|
+
matrix:
|
23
|
+
ruby: [2.6, 2.7, 3.0]
|
24
|
+
redis: [5, 6]
|
25
|
+
|
21
26
|
services:
|
22
27
|
postgres:
|
23
|
-
image: postgres:14
|
28
|
+
image: postgres:14
|
24
29
|
env:
|
30
|
+
POSTGRES_DB: message_bus_test
|
25
31
|
POSTGRES_PASSWORD: postgres
|
26
32
|
ports:
|
27
33
|
- 5432:5432
|
@@ -36,19 +42,13 @@ jobs:
|
|
36
42
|
--health-timeout 5s
|
37
43
|
--health-retries 5
|
38
44
|
|
39
|
-
strategy:
|
40
|
-
fail-fast: false
|
41
|
-
matrix:
|
42
|
-
ruby: ["3.0", "2.7", "2.6"]
|
43
|
-
redis: ["5", "6"]
|
44
|
-
|
45
45
|
steps:
|
46
46
|
- uses: actions/checkout@v2
|
47
47
|
|
48
48
|
- uses: ruby/setup-ruby@v1
|
49
49
|
with:
|
50
50
|
ruby-version: ${{ matrix.ruby }}
|
51
|
-
bundler-cache: true
|
51
|
+
bundler-cache: true
|
52
52
|
|
53
53
|
- name: Set up Node.js
|
54
54
|
uses: actions/setup-node@v2
|
@@ -59,13 +59,41 @@ jobs:
|
|
59
59
|
- name: Setup npm
|
60
60
|
run: npm install
|
61
61
|
|
62
|
-
- name: Create Database
|
63
|
-
run: |
|
64
|
-
createdb message_bus_test
|
65
|
-
|
66
62
|
- name: Tests
|
67
63
|
run: bundle exec rake
|
68
64
|
timeout-minutes: 3
|
69
65
|
|
70
66
|
- name: Linting
|
71
67
|
run: npx eslint .
|
68
|
+
|
69
|
+
publish:
|
70
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
71
|
+
needs: build
|
72
|
+
runs-on: ubuntu-latest
|
73
|
+
|
74
|
+
steps:
|
75
|
+
- uses: actions/checkout@v2
|
76
|
+
|
77
|
+
- name: Release gem
|
78
|
+
uses: discourse/publish-rubygems-action@v2
|
79
|
+
id: publish-gem
|
80
|
+
env:
|
81
|
+
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
82
|
+
GIT_EMAIL: team@discourse.org
|
83
|
+
GIT_NAME: discoursebot
|
84
|
+
|
85
|
+
- name: Update package version
|
86
|
+
if: steps.publish-gem.outputs.new_version == 'true'
|
87
|
+
run: |
|
88
|
+
VERSION=$(ruby -r './lib/message_bus/version' -e 'puts MessageBus::VERSION')
|
89
|
+
sed -i "s/0.0.0-version-placeholder/$VERSION/" package.json
|
90
|
+
git config --global user.email "ci@ci.invalid"
|
91
|
+
git config --global user.name "Discourse CI"
|
92
|
+
git add package.json
|
93
|
+
git commit -m 'bump'
|
94
|
+
|
95
|
+
- name: Publish package
|
96
|
+
uses: JS-DevTools/npm-publish@v1
|
97
|
+
if: steps.publish-gem.outputs.new_version == 'true'
|
98
|
+
with:
|
99
|
+
token: ${{ secrets.NPM_TOKEN }}
|
data/.prettierrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
{}
|
@@ -84,6 +84,11 @@ module MessageBus
|
|
84
84
|
raise ConcreteClassMustImplementError
|
85
85
|
end
|
86
86
|
|
87
|
+
# Closes all open connections to the storage.
|
88
|
+
def destroy
|
89
|
+
raise ConcreteClassMustImplementError
|
90
|
+
end
|
91
|
+
|
87
92
|
# Deletes all backlogs and their data. Does not delete non-backlog data that message_bus may persist, depending on the concrete backend implementation. Use with extreme caution.
|
88
93
|
# @abstract
|
89
94
|
def expire_all_backlogs!
|
@@ -212,6 +212,12 @@ module MessageBus
|
|
212
212
|
client.reset!
|
213
213
|
end
|
214
214
|
|
215
|
+
# No-op; this backend doesn't maintain any storage connections.
|
216
|
+
# (see Base#destroy)
|
217
|
+
def destroy
|
218
|
+
nil
|
219
|
+
end
|
220
|
+
|
215
221
|
# (see Base#expire_all_backlogs!)
|
216
222
|
def expire_all_backlogs!
|
217
223
|
client.expire_all_backlogs!
|
@@ -86,10 +86,12 @@ module MessageBus
|
|
86
86
|
hold { |conn| exec_prepared(conn, 'get_message', [channel, id]) { |r| r.getvalue(0, 0) } }
|
87
87
|
end
|
88
88
|
|
89
|
-
def
|
89
|
+
def after_fork
|
90
90
|
sync do
|
91
|
-
@
|
91
|
+
@pid = Process.pid
|
92
|
+
INHERITED_CONNECTIONS.concat(@available)
|
92
93
|
@available.clear
|
94
|
+
@listening_on.clear
|
93
95
|
end
|
94
96
|
end
|
95
97
|
|
@@ -101,6 +103,13 @@ module MessageBus
|
|
101
103
|
end
|
102
104
|
end
|
103
105
|
|
106
|
+
def destroy
|
107
|
+
sync do
|
108
|
+
@available.each(&:close)
|
109
|
+
@available.clear
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
104
113
|
# use with extreme care, will nuke all of the data
|
105
114
|
def expire_all_backlogs!
|
106
115
|
reset!
|
@@ -174,11 +183,7 @@ module MessageBus
|
|
174
183
|
def hold
|
175
184
|
current_pid = Process.pid
|
176
185
|
if current_pid != @pid
|
177
|
-
|
178
|
-
sync do
|
179
|
-
INHERITED_CONNECTIONS.concat(@available)
|
180
|
-
@available.clear
|
181
|
-
end
|
186
|
+
after_fork
|
182
187
|
end
|
183
188
|
|
184
189
|
if conn = sync { @allocated[Thread.current] }
|
@@ -253,12 +258,13 @@ module MessageBus
|
|
253
258
|
# after 7 days inactive backlogs will be removed
|
254
259
|
@max_backlog_age = 604800
|
255
260
|
@clear_every = config[:clear_every] || 1
|
261
|
+
@mutex = Mutex.new
|
256
262
|
end
|
257
263
|
|
258
264
|
# Reconnects to Postgres; used after a process fork, typically triggered by a forking webserver
|
259
265
|
# @see Base#after_fork
|
260
266
|
def after_fork
|
261
|
-
client.
|
267
|
+
client.after_fork
|
262
268
|
end
|
263
269
|
|
264
270
|
# (see Base#reset!)
|
@@ -266,6 +272,11 @@ module MessageBus
|
|
266
272
|
client.reset!
|
267
273
|
end
|
268
274
|
|
275
|
+
# (see Base#destroy)
|
276
|
+
def destroy
|
277
|
+
client.destroy
|
278
|
+
end
|
279
|
+
|
269
280
|
# (see Base#expire_all_backlogs!)
|
270
281
|
def expire_all_backlogs!
|
271
282
|
client.expire_all_backlogs!
|
@@ -401,11 +412,7 @@ module MessageBus
|
|
401
412
|
private
|
402
413
|
|
403
414
|
def client
|
404
|
-
@client ||=
|
405
|
-
end
|
406
|
-
|
407
|
-
def new_connection
|
408
|
-
Client.new(@config)
|
415
|
+
@client || @mutex.synchronize { @client ||= Client.new(@config) }
|
409
416
|
end
|
410
417
|
|
411
418
|
def postgresql_channel_name
|
@@ -65,7 +65,7 @@ module MessageBus
|
|
65
65
|
# Reconnects to Redis; used after a process fork, typically triggered by a forking webserver
|
66
66
|
# @see Base#after_fork
|
67
67
|
def after_fork
|
68
|
-
pub_redis
|
68
|
+
@pub_redis&.disconnect!
|
69
69
|
end
|
70
70
|
|
71
71
|
# (see Base#reset!)
|
@@ -75,6 +75,11 @@ module MessageBus
|
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
+
# (see Base#destroy)
|
79
|
+
def destroy
|
80
|
+
@pub_redis&.disconnect!
|
81
|
+
end
|
82
|
+
|
78
83
|
# Deletes all backlogs and their data. Does not delete ID pointers, so new publications will get IDs that continue from the last publication before the expiry. Use with extreme caution.
|
79
84
|
# @see Base#expire_all_backlogs!
|
80
85
|
def expire_all_backlogs!
|
data/lib/message_bus/client.rb
CHANGED
data/lib/message_bus/version.rb
CHANGED
data/lib/message_bus.rb
CHANGED
@@ -41,6 +41,10 @@ module MessageBus::Implementation
|
|
41
41
|
def initialize
|
42
42
|
@config = {}
|
43
43
|
@mutex = Synchronizer.new
|
44
|
+
@off = false
|
45
|
+
@destroyed = false
|
46
|
+
@timer_thread = nil
|
47
|
+
@subscriber_thread = nil
|
44
48
|
end
|
45
49
|
|
46
50
|
# @param [Boolean] val whether or not to cache static assets for the diagnostics pages
|
@@ -533,6 +537,7 @@ module MessageBus::Implementation
|
|
533
537
|
return if @destroyed
|
534
538
|
|
535
539
|
reliable_pub_sub.global_unsubscribe
|
540
|
+
reliable_pub_sub.destroy
|
536
541
|
|
537
542
|
@mutex.synchronize do
|
538
543
|
return if @destroyed
|
data/message_bus.gemspec
CHANGED
@@ -9,7 +9,8 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.summary = %q{}
|
10
10
|
gem.homepage = "https://github.com/discourse/message_bus"
|
11
11
|
gem.license = "MIT"
|
12
|
-
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.files = `git ls-files`.split($\) +
|
13
|
+
["vendor/assets/javascripts/message-bus.js", "vendor/assets/javascripts/message-bus-ajax.js"]
|
13
14
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
14
15
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
16
|
gem.name = "message_bus"
|
@@ -19,9 +20,11 @@ Gem::Specification.new do |gem|
|
|
19
20
|
|
20
21
|
gem.add_runtime_dependency 'rack', '>= 1.1.3'
|
21
22
|
|
23
|
+
# Optional runtime dependencies
|
22
24
|
gem.add_development_dependency 'redis'
|
23
25
|
gem.add_development_dependency 'pg'
|
24
26
|
gem.add_development_dependency 'concurrent-ruby' # for distributed-cache
|
27
|
+
|
25
28
|
gem.add_development_dependency 'minitest'
|
26
29
|
gem.add_development_dependency 'minitest-hooks'
|
27
30
|
gem.add_development_dependency 'minitest-global_expectations'
|
data/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "message-bus-client",
|
3
|
-
"version": "
|
3
|
+
"version": "0.0.0-version-placeholder",
|
4
4
|
"description": "A message bus client in Javascript",
|
5
5
|
"main": "assets/message-bus.js",
|
6
6
|
"keywords": [
|
@@ -12,10 +12,7 @@
|
|
12
12
|
],
|
13
13
|
"jsnext:main": "assets/message-bus.js",
|
14
14
|
"module": "assets/message-bus.js",
|
15
|
-
"repository":
|
16
|
-
"type": "git",
|
17
|
-
"url": "git+https://github.com/discourse/message_bus.git"
|
18
|
-
},
|
15
|
+
"repository": "https://github.com/discourse/message_bus",
|
19
16
|
"author": "Sam Saffron, Robin Ward",
|
20
17
|
"license": "MIT",
|
21
18
|
"bugs": {
|
data/spec/helpers.rb
CHANGED
@@ -25,7 +25,12 @@ def test_config_for_backend(backend)
|
|
25
25
|
when :redis
|
26
26
|
config[:url] = ENV['REDISURL']
|
27
27
|
when :postgres
|
28
|
-
config[:backend_options] = {
|
28
|
+
config[:backend_options] = {
|
29
|
+
host: ENV['PGHOST'],
|
30
|
+
user: ENV['PGUSER'] || ENV['USER'],
|
31
|
+
password: ENV['PGPASSWORD'],
|
32
|
+
dbname: ENV['PGDATABASE'] || 'message_bus_test'
|
33
|
+
}
|
29
34
|
end
|
30
35
|
config
|
31
36
|
end
|
@@ -18,13 +18,15 @@ describe PUB_SUB_CLASS do
|
|
18
18
|
|
19
19
|
def work_it
|
20
20
|
bus = new_bus
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
bus.subscribe("/echo", 0) do |msg|
|
22
|
+
if msg.data == "done"
|
23
|
+
bus.global_unsubscribe
|
24
|
+
else
|
25
|
+
bus.publish("/response", "#{msg.data}-#{Process.pid.to_s}")
|
26
|
+
end
|
26
27
|
end
|
27
28
|
ensure
|
29
|
+
bus.destroy
|
28
30
|
exit!(0)
|
29
31
|
end
|
30
32
|
|
@@ -44,12 +46,14 @@ describe PUB_SUB_CLASS do
|
|
44
46
|
test_never :memory
|
45
47
|
skip("previous error") if self.class.error?
|
46
48
|
GC.start
|
47
|
-
new_bus
|
49
|
+
bus = new_bus
|
50
|
+
bus.reset!
|
51
|
+
|
48
52
|
begin
|
49
53
|
pids = (1..10).map { spawn_child }
|
50
54
|
expected_responses = pids.map { |x| (0...10).map { |i| "0#{i}-#{x}" } }.flatten
|
51
55
|
unexpected_responses = []
|
52
|
-
|
56
|
+
|
53
57
|
t = Thread.new do
|
54
58
|
bus.subscribe("/response", 0) do |msg|
|
55
59
|
if expected_responses.include?(msg.data)
|
@@ -59,10 +63,14 @@ describe PUB_SUB_CLASS do
|
|
59
63
|
end
|
60
64
|
end
|
61
65
|
end
|
66
|
+
|
62
67
|
10.times { |i| bus.publish("/echo", "0#{i}") }
|
63
|
-
|
68
|
+
|
69
|
+
wait_for(2000) do
|
64
70
|
expected_responses.empty?
|
65
71
|
end
|
72
|
+
|
73
|
+
bus.publish("/echo", "done")
|
66
74
|
bus.global_unsubscribe
|
67
75
|
t.join
|
68
76
|
|
@@ -81,7 +89,9 @@ describe PUB_SUB_CLASS do
|
|
81
89
|
Process.wait(pid)
|
82
90
|
end
|
83
91
|
end
|
92
|
+
|
84
93
|
bus.global_unsubscribe
|
94
|
+
bus.destroy
|
85
95
|
end
|
86
96
|
end
|
87
97
|
end
|
@@ -83,7 +83,7 @@ describe MessageBus::Rack::Middleware do
|
|
83
83
|
{ "FOO" => "BAR" }
|
84
84
|
end
|
85
85
|
|
86
|
-
Thread.new do
|
86
|
+
t = Thread.new do
|
87
87
|
wait_for(2000) { middleware.in_async? }
|
88
88
|
bus.publish "/foo", "םוֹלשָׁ"
|
89
89
|
end
|
@@ -96,6 +96,7 @@ describe MessageBus::Rack::Middleware do
|
|
96
96
|
parsed[0]["data"].must_equal "םוֹלשָׁ"
|
97
97
|
|
98
98
|
last_response.headers["FOO"].must_equal "BAR"
|
99
|
+
t.join
|
99
100
|
end
|
100
101
|
|
101
102
|
it "should timeout within its alloted slot" do
|
@@ -0,0 +1,38 @@
|
|
1
|
+
// A bare-bones implementation of $.ajax that MessageBus will use
|
2
|
+
// as a fallback if jQuery is not present
|
3
|
+
//
|
4
|
+
// Only implements methods & options used by MessageBus
|
5
|
+
(function(global) {
|
6
|
+
'use strict';
|
7
|
+
if (!global.MessageBus){
|
8
|
+
throw new Error("MessageBus must be loaded before the ajax adapter");
|
9
|
+
}
|
10
|
+
|
11
|
+
global.MessageBus.ajax = function(options){
|
12
|
+
var XHRImpl = (global.MessageBus && global.MessageBus.xhrImplementation) || global.XMLHttpRequest;
|
13
|
+
var xhr = new XHRImpl();
|
14
|
+
xhr.dataType = options.dataType;
|
15
|
+
xhr.open('POST', options.url);
|
16
|
+
for (var name in options.headers){
|
17
|
+
xhr.setRequestHeader(name, options.headers[name]);
|
18
|
+
}
|
19
|
+
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
20
|
+
if (options.messageBus.chunked){
|
21
|
+
options.messageBus.onProgressListener(xhr);
|
22
|
+
}
|
23
|
+
xhr.onreadystatechange = function(){
|
24
|
+
if (xhr.readyState === 4){
|
25
|
+
var status = xhr.status;
|
26
|
+
if (status >= 200 && status < 300 || status === 304){
|
27
|
+
options.success(xhr.responseText);
|
28
|
+
} else {
|
29
|
+
options.error(xhr, xhr.statusText);
|
30
|
+
}
|
31
|
+
options.complete();
|
32
|
+
}
|
33
|
+
}
|
34
|
+
xhr.send(new URLSearchParams(options.data).toString());
|
35
|
+
return xhr;
|
36
|
+
};
|
37
|
+
|
38
|
+
})(window);
|
@@ -0,0 +1,549 @@
|
|
1
|
+
/*global define, jQuery*/
|
2
|
+
|
3
|
+
(function (root, factory) {
|
4
|
+
if (typeof define === "function" && define.amd) {
|
5
|
+
// AMD. Register as an anonymous module.
|
6
|
+
define([], function () {
|
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 () {
|
17
|
+
"use strict";
|
18
|
+
|
19
|
+
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
|
20
|
+
var uniqueId = function () {
|
21
|
+
return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
22
|
+
var r = (Math.random() * 16) | 0;
|
23
|
+
var v = c === "x" ? r : (r & 0x3) | 0x8;
|
24
|
+
return v.toString(16);
|
25
|
+
});
|
26
|
+
};
|
27
|
+
|
28
|
+
var me;
|
29
|
+
var delayPollTimeout;
|
30
|
+
var ajaxInProgress = false;
|
31
|
+
var started = false;
|
32
|
+
var clientId = uniqueId();
|
33
|
+
var callbacks = [];
|
34
|
+
var failCount = 0;
|
35
|
+
var baseUrl = "/";
|
36
|
+
var paused = false;
|
37
|
+
var later = [];
|
38
|
+
var chunkedBackoff = 0;
|
39
|
+
var stopped;
|
40
|
+
var pollTimeout = null;
|
41
|
+
var totalAjaxFailures = 0;
|
42
|
+
var totalAjaxCalls = 0;
|
43
|
+
var lastAjax;
|
44
|
+
|
45
|
+
var isHidden = (function () {
|
46
|
+
var prefixes = ["", "webkit", "ms", "moz"];
|
47
|
+
var hiddenProperty;
|
48
|
+
for (var i = 0; i < prefixes.length; i++) {
|
49
|
+
var prefix = prefixes[i];
|
50
|
+
var check = prefix + (prefix === "" ? "hidden" : "Hidden");
|
51
|
+
if (document[check] !== undefined) {
|
52
|
+
hiddenProperty = check;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
return function () {
|
57
|
+
if (hiddenProperty !== undefined) {
|
58
|
+
return document[hiddenProperty];
|
59
|
+
} else {
|
60
|
+
return !document.hasFocus;
|
61
|
+
}
|
62
|
+
};
|
63
|
+
})();
|
64
|
+
|
65
|
+
var hasLocalStorage = (function () {
|
66
|
+
try {
|
67
|
+
localStorage.setItem("mbTestLocalStorage", Date.now());
|
68
|
+
localStorage.removeItem("mbTestLocalStorage");
|
69
|
+
return true;
|
70
|
+
} catch (e) {
|
71
|
+
return false;
|
72
|
+
}
|
73
|
+
})();
|
74
|
+
|
75
|
+
var updateLastAjax = function () {
|
76
|
+
if (hasLocalStorage) {
|
77
|
+
localStorage.setItem("__mbLastAjax", Date.now());
|
78
|
+
}
|
79
|
+
};
|
80
|
+
|
81
|
+
var hiddenTabShouldWait = function () {
|
82
|
+
if (hasLocalStorage && isHidden()) {
|
83
|
+
var lastAjaxCall = parseInt(localStorage.getItem("__mbLastAjax"), 10);
|
84
|
+
var deltaAjax = Date.now() - lastAjaxCall;
|
85
|
+
|
86
|
+
return deltaAjax >= 0 && deltaAjax < me.minHiddenPollInterval;
|
87
|
+
}
|
88
|
+
return false;
|
89
|
+
};
|
90
|
+
|
91
|
+
var hasonprogress = new XMLHttpRequest().onprogress === null;
|
92
|
+
var allowChunked = function () {
|
93
|
+
return me.enableChunkedEncoding && hasonprogress;
|
94
|
+
};
|
95
|
+
|
96
|
+
var shouldLongPoll = function () {
|
97
|
+
return (
|
98
|
+
me.alwaysLongPoll ||
|
99
|
+
(me.shouldLongPollCallback ? me.shouldLongPollCallback() : !isHidden())
|
100
|
+
);
|
101
|
+
};
|
102
|
+
|
103
|
+
var processMessages = function (messages) {
|
104
|
+
if (!messages || messages.length === 0) {
|
105
|
+
return false;
|
106
|
+
}
|
107
|
+
|
108
|
+
for (var i = 0; i < messages.length; i++) {
|
109
|
+
var message = messages[i];
|
110
|
+
for (var j = 0; j < callbacks.length; j++) {
|
111
|
+
var callback = callbacks[j];
|
112
|
+
if (callback.channel === message.channel) {
|
113
|
+
callback.last_id = message.message_id;
|
114
|
+
try {
|
115
|
+
callback.func(message.data, message.global_id, message.message_id);
|
116
|
+
} catch (e) {
|
117
|
+
if (console.log) {
|
118
|
+
console.log(
|
119
|
+
"MESSAGE BUS FAIL: callback " +
|
120
|
+
callback.channel +
|
121
|
+
" caused exception " +
|
122
|
+
e.stack
|
123
|
+
);
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
if (message.channel === "/__status") {
|
128
|
+
if (message.data[callback.channel] !== undefined) {
|
129
|
+
callback.last_id = message.data[callback.channel];
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
return true;
|
136
|
+
};
|
137
|
+
|
138
|
+
var reqSuccess = function (messages) {
|
139
|
+
failCount = 0;
|
140
|
+
if (paused) {
|
141
|
+
if (messages) {
|
142
|
+
for (var i = 0; i < messages.length; i++) {
|
143
|
+
later.push(messages[i]);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
} else {
|
147
|
+
return processMessages(messages);
|
148
|
+
}
|
149
|
+
return false;
|
150
|
+
};
|
151
|
+
|
152
|
+
var longPoller = function (poll, data) {
|
153
|
+
if (ajaxInProgress) {
|
154
|
+
// never allow concurrent ajax reqs
|
155
|
+
return;
|
156
|
+
}
|
157
|
+
|
158
|
+
var gotData = false;
|
159
|
+
var aborted = false;
|
160
|
+
var rateLimited = false;
|
161
|
+
var rateLimitedSeconds;
|
162
|
+
|
163
|
+
lastAjax = new Date();
|
164
|
+
totalAjaxCalls += 1;
|
165
|
+
data.__seq = totalAjaxCalls;
|
166
|
+
|
167
|
+
var longPoll = shouldLongPoll() && me.enableLongPolling;
|
168
|
+
var chunked = longPoll && allowChunked();
|
169
|
+
if (chunkedBackoff > 0) {
|
170
|
+
chunkedBackoff--;
|
171
|
+
chunked = false;
|
172
|
+
}
|
173
|
+
|
174
|
+
var headers = { "X-SILENCE-LOGGER": "true" };
|
175
|
+
for (var name in me.headers) {
|
176
|
+
headers[name] = me.headers[name];
|
177
|
+
}
|
178
|
+
|
179
|
+
if (!chunked) {
|
180
|
+
headers["Dont-Chunk"] = "true";
|
181
|
+
}
|
182
|
+
|
183
|
+
var dataType = chunked ? "text" : "json";
|
184
|
+
|
185
|
+
var handle_progress = function (payload, position) {
|
186
|
+
var separator = "\r\n|\r\n";
|
187
|
+
var endChunk = payload.indexOf(separator, position);
|
188
|
+
|
189
|
+
if (endChunk === -1) {
|
190
|
+
return position;
|
191
|
+
}
|
192
|
+
|
193
|
+
var chunk = payload.substring(position, endChunk);
|
194
|
+
chunk = chunk.replace(/\r\n\|\|\r\n/g, separator);
|
195
|
+
|
196
|
+
try {
|
197
|
+
reqSuccess(JSON.parse(chunk));
|
198
|
+
} catch (e) {
|
199
|
+
if (console.log) {
|
200
|
+
console.log("FAILED TO PARSE CHUNKED REPLY");
|
201
|
+
console.log(data);
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
return handle_progress(payload, endChunk + separator.length);
|
206
|
+
};
|
207
|
+
|
208
|
+
var disableChunked = function () {
|
209
|
+
if (me.longPoll) {
|
210
|
+
me.longPoll.abort();
|
211
|
+
chunkedBackoff = 30;
|
212
|
+
}
|
213
|
+
};
|
214
|
+
|
215
|
+
if (!me.ajax) {
|
216
|
+
throw new Error("Either jQuery or the ajax adapter must be loaded");
|
217
|
+
}
|
218
|
+
|
219
|
+
updateLastAjax();
|
220
|
+
|
221
|
+
ajaxInProgress = true;
|
222
|
+
var req = me.ajax({
|
223
|
+
url:
|
224
|
+
me.baseUrl +
|
225
|
+
"message-bus/" +
|
226
|
+
me.clientId +
|
227
|
+
"/poll" +
|
228
|
+
(!longPoll ? "?dlp=t" : ""),
|
229
|
+
data: data,
|
230
|
+
async: true,
|
231
|
+
dataType: dataType,
|
232
|
+
type: "POST",
|
233
|
+
headers: headers,
|
234
|
+
messageBus: {
|
235
|
+
chunked: chunked,
|
236
|
+
onProgressListener: function (xhr) {
|
237
|
+
var position = 0;
|
238
|
+
// if it takes longer than 3000 ms to get first chunk, we have some proxy
|
239
|
+
// this is messing with us, so just backoff from using chunked for now
|
240
|
+
var chunkedTimeout = setTimeout(disableChunked, 3000);
|
241
|
+
return (xhr.onprogress = function () {
|
242
|
+
clearTimeout(chunkedTimeout);
|
243
|
+
if (
|
244
|
+
xhr.getResponseHeader("Content-Type") ===
|
245
|
+
"application/json; charset=utf-8"
|
246
|
+
) {
|
247
|
+
chunked = false; // not chunked, we are sending json back
|
248
|
+
} else {
|
249
|
+
position = handle_progress(xhr.responseText, position);
|
250
|
+
}
|
251
|
+
});
|
252
|
+
},
|
253
|
+
},
|
254
|
+
xhr: function () {
|
255
|
+
var xhr = jQuery.ajaxSettings.xhr();
|
256
|
+
if (!chunked) {
|
257
|
+
return xhr;
|
258
|
+
}
|
259
|
+
this.messageBus.onProgressListener(xhr);
|
260
|
+
return xhr;
|
261
|
+
},
|
262
|
+
success: function (messages) {
|
263
|
+
if (!chunked) {
|
264
|
+
// we may have requested text so jQuery will not parse
|
265
|
+
if (typeof messages === "string") {
|
266
|
+
messages = JSON.parse(messages);
|
267
|
+
}
|
268
|
+
gotData = reqSuccess(messages);
|
269
|
+
}
|
270
|
+
},
|
271
|
+
error: function (xhr, textStatus) {
|
272
|
+
if (xhr.status === 429) {
|
273
|
+
var tryAfter =
|
274
|
+
parseInt(
|
275
|
+
xhr.getResponseHeader && xhr.getResponseHeader("Retry-After")
|
276
|
+
) || 0;
|
277
|
+
tryAfter = tryAfter || 0;
|
278
|
+
if (tryAfter < 15) {
|
279
|
+
tryAfter = 15;
|
280
|
+
}
|
281
|
+
rateLimitedSeconds = tryAfter;
|
282
|
+
rateLimited = true;
|
283
|
+
} else if (textStatus === "abort") {
|
284
|
+
aborted = true;
|
285
|
+
} else {
|
286
|
+
failCount += 1;
|
287
|
+
totalAjaxFailures += 1;
|
288
|
+
}
|
289
|
+
},
|
290
|
+
complete: function () {
|
291
|
+
ajaxInProgress = false;
|
292
|
+
|
293
|
+
var interval;
|
294
|
+
try {
|
295
|
+
if (rateLimited) {
|
296
|
+
interval = Math.max(me.minPollInterval, rateLimitedSeconds * 1000);
|
297
|
+
} else if (gotData || aborted) {
|
298
|
+
interval = me.minPollInterval;
|
299
|
+
} else {
|
300
|
+
interval = me.callbackInterval;
|
301
|
+
if (failCount > 2) {
|
302
|
+
interval = interval * failCount;
|
303
|
+
} else if (!shouldLongPoll()) {
|
304
|
+
interval = me.backgroundCallbackInterval;
|
305
|
+
}
|
306
|
+
if (interval > me.maxPollInterval) {
|
307
|
+
interval = me.maxPollInterval;
|
308
|
+
}
|
309
|
+
|
310
|
+
interval -= new Date() - lastAjax;
|
311
|
+
|
312
|
+
if (interval < 100) {
|
313
|
+
interval = 100;
|
314
|
+
}
|
315
|
+
}
|
316
|
+
} catch (e) {
|
317
|
+
if (console.log && e.message) {
|
318
|
+
console.log("MESSAGE BUS FAIL: " + e.message);
|
319
|
+
}
|
320
|
+
}
|
321
|
+
|
322
|
+
if (pollTimeout) {
|
323
|
+
clearTimeout(pollTimeout);
|
324
|
+
pollTimeout = null;
|
325
|
+
}
|
326
|
+
|
327
|
+
if (started) {
|
328
|
+
pollTimeout = setTimeout(function () {
|
329
|
+
pollTimeout = null;
|
330
|
+
poll();
|
331
|
+
}, interval);
|
332
|
+
}
|
333
|
+
|
334
|
+
me.longPoll = null;
|
335
|
+
},
|
336
|
+
});
|
337
|
+
|
338
|
+
return req;
|
339
|
+
};
|
340
|
+
|
341
|
+
me = {
|
342
|
+
/* shared between all tabs */
|
343
|
+
minHiddenPollInterval: 1500,
|
344
|
+
enableChunkedEncoding: true,
|
345
|
+
enableLongPolling: true,
|
346
|
+
callbackInterval: 15000,
|
347
|
+
backgroundCallbackInterval: 60000,
|
348
|
+
minPollInterval: 100,
|
349
|
+
maxPollInterval: 3 * 60 * 1000,
|
350
|
+
callbacks: callbacks,
|
351
|
+
clientId: clientId,
|
352
|
+
alwaysLongPoll: false,
|
353
|
+
shouldLongPollCallback: undefined,
|
354
|
+
baseUrl: baseUrl,
|
355
|
+
headers: {},
|
356
|
+
ajax: typeof jQuery !== "undefined" && jQuery.ajax,
|
357
|
+
diagnostics: function () {
|
358
|
+
console.log("Stopped: " + stopped + " Started: " + started);
|
359
|
+
console.log("Current callbacks");
|
360
|
+
console.log(callbacks);
|
361
|
+
console.log(
|
362
|
+
"Total ajax calls: " +
|
363
|
+
totalAjaxCalls +
|
364
|
+
" Recent failure count: " +
|
365
|
+
failCount +
|
366
|
+
" Total failures: " +
|
367
|
+
totalAjaxFailures
|
368
|
+
);
|
369
|
+
console.log(
|
370
|
+
"Last ajax call: " + (new Date() - lastAjax) / 1000 + " seconds ago"
|
371
|
+
);
|
372
|
+
},
|
373
|
+
|
374
|
+
pause: function () {
|
375
|
+
paused = true;
|
376
|
+
},
|
377
|
+
|
378
|
+
resume: function () {
|
379
|
+
paused = false;
|
380
|
+
processMessages(later);
|
381
|
+
later = [];
|
382
|
+
},
|
383
|
+
|
384
|
+
stop: function () {
|
385
|
+
stopped = true;
|
386
|
+
started = false;
|
387
|
+
if (delayPollTimeout) {
|
388
|
+
clearTimeout(delayPollTimeout);
|
389
|
+
delayPollTimeout = null;
|
390
|
+
}
|
391
|
+
if (pollTimeout) {
|
392
|
+
clearTimeout(pollTimeout);
|
393
|
+
pollTimeout = null;
|
394
|
+
}
|
395
|
+
if (me.longPoll) {
|
396
|
+
me.longPoll.abort();
|
397
|
+
}
|
398
|
+
if (me.onVisibilityChange) {
|
399
|
+
document.removeEventListener("visibilitychange", me.onVisibilityChange);
|
400
|
+
me.onVisibilityChange = null;
|
401
|
+
}
|
402
|
+
},
|
403
|
+
|
404
|
+
// Start polling
|
405
|
+
start: function () {
|
406
|
+
if (started) return;
|
407
|
+
started = true;
|
408
|
+
stopped = false;
|
409
|
+
|
410
|
+
var poll = function () {
|
411
|
+
if (stopped) {
|
412
|
+
return;
|
413
|
+
}
|
414
|
+
|
415
|
+
if (callbacks.length === 0 || hiddenTabShouldWait()) {
|
416
|
+
if (!delayPollTimeout) {
|
417
|
+
delayPollTimeout = setTimeout(function () {
|
418
|
+
delayPollTimeout = null;
|
419
|
+
poll();
|
420
|
+
}, parseInt(500 + Math.random() * 500));
|
421
|
+
}
|
422
|
+
return;
|
423
|
+
}
|
424
|
+
|
425
|
+
var data = {};
|
426
|
+
for (var i = 0; i < callbacks.length; i++) {
|
427
|
+
data[callbacks[i].channel] = callbacks[i].last_id;
|
428
|
+
}
|
429
|
+
|
430
|
+
// could possibly already be started
|
431
|
+
// notice the delay timeout above
|
432
|
+
if (!me.longPoll) {
|
433
|
+
me.longPoll = longPoller(poll, data);
|
434
|
+
}
|
435
|
+
};
|
436
|
+
|
437
|
+
// monitor visibility, issue a new long poll when the page shows
|
438
|
+
if (document.addEventListener && "hidden" in document) {
|
439
|
+
me.onVisibilityChange = function () {
|
440
|
+
if (
|
441
|
+
!document.hidden &&
|
442
|
+
!me.longPoll &&
|
443
|
+
(pollTimeout || delayPollTimeout)
|
444
|
+
) {
|
445
|
+
clearTimeout(pollTimeout);
|
446
|
+
clearTimeout(delayPollTimeout);
|
447
|
+
|
448
|
+
delayPollTimeout = null;
|
449
|
+
pollTimeout = null;
|
450
|
+
poll();
|
451
|
+
}
|
452
|
+
};
|
453
|
+
|
454
|
+
document.addEventListener("visibilitychange", me.onVisibilityChange);
|
455
|
+
}
|
456
|
+
|
457
|
+
poll();
|
458
|
+
},
|
459
|
+
|
460
|
+
status: function () {
|
461
|
+
if (paused) {
|
462
|
+
return "paused";
|
463
|
+
} else if (started) {
|
464
|
+
return "started";
|
465
|
+
} else if (stopped) {
|
466
|
+
return "stopped";
|
467
|
+
} else {
|
468
|
+
throw "Cannot determine current status";
|
469
|
+
}
|
470
|
+
},
|
471
|
+
|
472
|
+
// Subscribe to a channel
|
473
|
+
// if lastId is 0 or larger, it will recieve messages AFTER that id
|
474
|
+
// if lastId is negative it will perform lookbehind
|
475
|
+
// -1 will subscribe to all new messages
|
476
|
+
// -2 will recieve last message + all new messages
|
477
|
+
// -3 will recieve last 2 messages + all new messages
|
478
|
+
// if undefined will default to -1
|
479
|
+
subscribe: function (channel, func, lastId) {
|
480
|
+
if (!started && !stopped) {
|
481
|
+
me.start();
|
482
|
+
}
|
483
|
+
|
484
|
+
if (lastId === null || typeof lastId === "undefined") {
|
485
|
+
lastId = -1;
|
486
|
+
} else if (typeof lastId !== "number") {
|
487
|
+
throw (
|
488
|
+
"lastId has type " + typeof lastId + " but a number was expected."
|
489
|
+
);
|
490
|
+
}
|
491
|
+
|
492
|
+
if (typeof channel !== "string") {
|
493
|
+
throw "Channel name must be a string!";
|
494
|
+
}
|
495
|
+
|
496
|
+
callbacks.push({
|
497
|
+
channel: channel,
|
498
|
+
func: func,
|
499
|
+
last_id: lastId,
|
500
|
+
});
|
501
|
+
if (me.longPoll) {
|
502
|
+
me.longPoll.abort();
|
503
|
+
}
|
504
|
+
|
505
|
+
return func;
|
506
|
+
},
|
507
|
+
|
508
|
+
// Unsubscribe from a channel
|
509
|
+
unsubscribe: function (channel, func) {
|
510
|
+
// TODO allow for globbing in the middle of a channel name
|
511
|
+
// like /something/*/something
|
512
|
+
// at the moment we only support globbing /something/*
|
513
|
+
var glob = false;
|
514
|
+
if (channel.indexOf("*", channel.length - 1) !== -1) {
|
515
|
+
channel = channel.substr(0, channel.length - 1);
|
516
|
+
glob = true;
|
517
|
+
}
|
518
|
+
|
519
|
+
var removed = false;
|
520
|
+
|
521
|
+
for (var i = callbacks.length - 1; i >= 0; i--) {
|
522
|
+
var callback = callbacks[i];
|
523
|
+
var keep;
|
524
|
+
|
525
|
+
if (glob) {
|
526
|
+
keep = callback.channel.substr(0, channel.length) !== channel;
|
527
|
+
} else {
|
528
|
+
keep = callback.channel !== channel;
|
529
|
+
}
|
530
|
+
|
531
|
+
if (!keep && func && callback.func !== func) {
|
532
|
+
keep = true;
|
533
|
+
}
|
534
|
+
|
535
|
+
if (!keep) {
|
536
|
+
callbacks.splice(i, 1);
|
537
|
+
removed = true;
|
538
|
+
}
|
539
|
+
}
|
540
|
+
|
541
|
+
if (removed && me.longPoll) {
|
542
|
+
me.longPoll.abort();
|
543
|
+
}
|
544
|
+
|
545
|
+
return removed;
|
546
|
+
},
|
547
|
+
};
|
548
|
+
return me;
|
549
|
+
});
|
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: 3.3.
|
4
|
+
version: 3.3.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Saffron
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -272,6 +272,7 @@ files:
|
|
272
272
|
- ".eslintrc.js"
|
273
273
|
- ".github/workflows/ci.yml"
|
274
274
|
- ".gitignore"
|
275
|
+
- ".prettierrc"
|
275
276
|
- ".rubocop.yml"
|
276
277
|
- CHANGELOG
|
277
278
|
- DEV.md
|
@@ -355,6 +356,8 @@ files:
|
|
355
356
|
- spec/spec_helper.rb
|
356
357
|
- spec/support/jasmine-browser.json
|
357
358
|
- vendor/assets/javascripts/.gitignore
|
359
|
+
- vendor/assets/javascripts/message-bus-ajax.js
|
360
|
+
- vendor/assets/javascripts/message-bus.js
|
358
361
|
homepage: https://github.com/discourse/message_bus
|
359
362
|
licenses:
|
360
363
|
- MIT
|
@@ -374,7 +377,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
374
377
|
- !ruby/object:Gem::Version
|
375
378
|
version: '0'
|
376
379
|
requirements: []
|
377
|
-
rubygems_version: 3.
|
380
|
+
rubygems_version: 3.1.6
|
378
381
|
signing_key:
|
379
382
|
specification_version: 4
|
380
383
|
summary: ''
|