message_bus 4.2.0 → 4.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.
- checksums.yaml +4 -4
- data/.eslintrc.js +9 -0
- data/.github/workflows/ci.yml +4 -4
- data/CHANGELOG +16 -0
- data/Dockerfile +2 -2
- data/README.md +1 -1
- data/assets/message-bus.js +37 -22
- data/lib/message_bus/backends/redis.rb +23 -1
- data/lib/message_bus/version.rb +1 -1
- data/lib/message_bus.rb +8 -3
- data/package-lock.json +541 -430
- data/package.json +1 -1
- data/spec/assets/SpecHelper.js +10 -4
- data/spec/assets/message-bus.spec.js +70 -1
- data/spec/lib/message_bus/backend_spec.rb +0 -7
- data/spec/lib/message_bus_spec.rb +52 -20
- data/vendor/assets/javascripts/message-bus.js +37 -22
- metadata +2 -2
data/package.json
CHANGED
data/spec/assets/SpecHelper.js
CHANGED
@@ -33,8 +33,7 @@ beforeEach(function () {
|
|
33
33
|
MockedXMLHttpRequest.prototype.send = function(){
|
34
34
|
this.readyState = 4
|
35
35
|
this.responseText = encodeChunks(this, spec.responseChunks);
|
36
|
-
this.
|
37
|
-
this.status = 200;
|
36
|
+
this.status = spec.responseStatus;
|
38
37
|
if (this.onprogress){ this.onprogress(); }
|
39
38
|
this.onreadystatechange()
|
40
39
|
}
|
@@ -53,8 +52,8 @@ beforeEach(function () {
|
|
53
52
|
this.headers[k] = v;
|
54
53
|
}
|
55
54
|
|
56
|
-
MockedXMLHttpRequest.prototype.getResponseHeader = function(){
|
57
|
-
return
|
55
|
+
MockedXMLHttpRequest.prototype.getResponseHeader = function(headerName){
|
56
|
+
return spec.responseHeaders[headerName];
|
58
57
|
}
|
59
58
|
|
60
59
|
MessageBus.xhrImplementation = MockedXMLHttpRequest
|
@@ -64,12 +63,19 @@ beforeEach(function () {
|
|
64
63
|
{channel: '/test', data: {password: 'MessageBusRocks!'}}
|
65
64
|
];
|
66
65
|
|
66
|
+
this.responseStatus = 200;
|
67
|
+
this.responseHeaders = {
|
68
|
+
"Content-Type": 'text/plain; charset=utf-8',
|
69
|
+
};
|
70
|
+
|
67
71
|
MessageBus.start();
|
68
72
|
});
|
69
73
|
|
70
74
|
afterEach(function(){
|
71
75
|
MessageBus.stop()
|
72
76
|
MessageBus.callbacks.splice(0, MessageBus.callbacks.length)
|
77
|
+
MessageBus.shouldLongPollCallback = null;
|
78
|
+
MessageBus.enableChunkedEncoding = true;
|
73
79
|
});
|
74
80
|
|
75
81
|
window.testMB = function(description, testFn, path, data){
|
@@ -1,7 +1,11 @@
|
|
1
|
+
/* eslint-env es2022 */
|
1
2
|
/* global describe, it, spyOn, MessageBus, expect, jasmine, testMB */
|
2
3
|
|
3
|
-
|
4
|
+
function approxEqual(valueOne, valueTwo) {
|
5
|
+
return Math.abs(valueOne - valueTwo) < 500;
|
6
|
+
}
|
4
7
|
|
8
|
+
describe("Messagebus", function () {
|
5
9
|
it("submits change requests", function(done){
|
6
10
|
spyOn(this.MockedXMLHttpRequest.prototype, 'send').and.callThrough();
|
7
11
|
var spec = this;
|
@@ -97,4 +101,69 @@ describe("Messagebus", function() {
|
|
97
101
|
})
|
98
102
|
});
|
99
103
|
|
104
|
+
it("respects Retry-After response header when larger than 15 seconds", async function () {
|
105
|
+
spyOn(this.MockedXMLHttpRequest.prototype, "send").and.callThrough();
|
106
|
+
spyOn(window, "setTimeout").and.callThrough();
|
107
|
+
|
108
|
+
this.responseStatus = 429;
|
109
|
+
this.responseHeaders["Retry-After"] = "23";
|
110
|
+
|
111
|
+
await new Promise((resolve) => MessageBus.subscribe("/test", resolve));
|
112
|
+
|
113
|
+
const nextPollScheduledIn = window.setTimeout.calls.mostRecent().args[1];
|
114
|
+
expect(nextPollScheduledIn).toEqual(23000);
|
115
|
+
});
|
116
|
+
|
117
|
+
it("retries after 15s for lower retry-after values", async function () {
|
118
|
+
spyOn(this.MockedXMLHttpRequest.prototype, "send").and.callThrough();
|
119
|
+
spyOn(window, "setTimeout").and.callThrough();
|
120
|
+
|
121
|
+
this.responseStatus = 429;
|
122
|
+
this.responseHeaders["Retry-After"] = "13";
|
123
|
+
|
124
|
+
await new Promise((resolve) => MessageBus.subscribe("/test", resolve));
|
125
|
+
|
126
|
+
const nextPollScheduledIn = window.setTimeout.calls.mostRecent().args[1];
|
127
|
+
expect(nextPollScheduledIn).toEqual(15000);
|
128
|
+
});
|
129
|
+
|
130
|
+
it("waits for callbackInterval after receiving data in chunked long-poll mode", async function () {
|
131
|
+
// The callbackInterval is equal to the length of the server response in chunked long-poll mode, so
|
132
|
+
// this ultimately ends up being a continuous stream of requests
|
133
|
+
|
134
|
+
spyOn(this.MockedXMLHttpRequest.prototype, "send").and.callThrough();
|
135
|
+
spyOn(window, "setTimeout").and.callThrough();
|
136
|
+
|
137
|
+
await new Promise((resolve) => MessageBus.subscribe("/test", resolve));
|
138
|
+
|
139
|
+
const nextPollScheduledIn = window.setTimeout.calls.mostRecent().args[1];
|
140
|
+
expect(
|
141
|
+
approxEqual(nextPollScheduledIn, MessageBus.callbackInterval)
|
142
|
+
).toEqual(true);
|
143
|
+
});
|
144
|
+
|
145
|
+
it("waits for backgroundCallbackInterval after receiving data in non-long-poll mode", async function () {
|
146
|
+
spyOn(this.MockedXMLHttpRequest.prototype, "send").and.callThrough();
|
147
|
+
spyOn(window, "setTimeout").and.callThrough();
|
148
|
+
MessageBus.shouldLongPollCallback = () => false;
|
149
|
+
MessageBus.enableChunkedEncoding = false;
|
150
|
+
|
151
|
+
await new Promise((resolve) => MessageBus.subscribe("/test", resolve));
|
152
|
+
|
153
|
+
const nextPollScheduledIn = window.setTimeout.calls.mostRecent().args[1];
|
154
|
+
expect(
|
155
|
+
approxEqual(nextPollScheduledIn, MessageBus.backgroundCallbackInterval)
|
156
|
+
).toEqual(true);
|
157
|
+
});
|
158
|
+
|
159
|
+
it("re-polls immediately after receiving data in non-chunked long-poll mode", async function () {
|
160
|
+
spyOn(this.MockedXMLHttpRequest.prototype, "send").and.callThrough();
|
161
|
+
spyOn(window, "setTimeout").and.callThrough();
|
162
|
+
MessageBus.enableChunkedEncoding = false;
|
163
|
+
|
164
|
+
await new Promise((resolve) => MessageBus.subscribe("/test", resolve));
|
165
|
+
|
166
|
+
const nextPollScheduledIn = window.setTimeout.calls.mostRecent().args[1];
|
167
|
+
expect(nextPollScheduledIn).toEqual(MessageBus.minPollInterval);
|
168
|
+
});
|
100
169
|
});
|
@@ -392,11 +392,4 @@ describe BACKEND_CLASS do
|
|
392
392
|
got.map { |m| m.data }.must_equal ["12"]
|
393
393
|
end
|
394
394
|
|
395
|
-
it 'should not lose redis config' do
|
396
|
-
test_only :redis
|
397
|
-
redis_config = { connector: Redis::Client::Connector }
|
398
|
-
@bus.instance_variable_set(:@redis_config, redis_config)
|
399
|
-
@bus.send(:new_redis_connection)
|
400
|
-
expect(@bus.instance_variable_get(:@redis_config)[:connector]).must_equal Redis::Client::Connector
|
401
|
-
end
|
402
395
|
end
|
@@ -17,26 +17,6 @@ describe MessageBus do
|
|
17
17
|
@bus.destroy
|
18
18
|
end
|
19
19
|
|
20
|
-
it "can be turned off" do
|
21
|
-
@bus.off
|
22
|
-
|
23
|
-
@bus.off?.must_equal true
|
24
|
-
end
|
25
|
-
|
26
|
-
it "can call destroy multiple times" do
|
27
|
-
@bus.destroy
|
28
|
-
@bus.destroy
|
29
|
-
@bus.destroy
|
30
|
-
end
|
31
|
-
|
32
|
-
it "can be turned on after destroy" do
|
33
|
-
@bus.destroy
|
34
|
-
|
35
|
-
@bus.on
|
36
|
-
|
37
|
-
@bus.after_fork
|
38
|
-
end
|
39
|
-
|
40
20
|
it "destroying immediately after `after_fork` does not lock" do
|
41
21
|
10.times do
|
42
22
|
@bus.on
|
@@ -216,6 +196,58 @@ describe MessageBus do
|
|
216
196
|
@bus.backlog("/chuck").map { |i| i.data }.to_a.must_equal ['foo']
|
217
197
|
end
|
218
198
|
|
199
|
+
it "can be turned off" do
|
200
|
+
@bus.off
|
201
|
+
|
202
|
+
@bus.off?.must_equal true
|
203
|
+
|
204
|
+
@bus.publish("/chuck", "norris")
|
205
|
+
|
206
|
+
@bus.backlog("/chuck").to_a.must_equal []
|
207
|
+
end
|
208
|
+
|
209
|
+
it "can be turned off only for subscriptions" do
|
210
|
+
@bus.off(disable_publish: false)
|
211
|
+
|
212
|
+
@bus.off?.must_equal true
|
213
|
+
|
214
|
+
data = []
|
215
|
+
|
216
|
+
@bus.subscribe("/chuck") do |msg|
|
217
|
+
data << msg.data
|
218
|
+
end
|
219
|
+
|
220
|
+
@bus.publish("/chuck", "norris")
|
221
|
+
|
222
|
+
@bus.on
|
223
|
+
|
224
|
+
@bus.subscribe("/chuck") do |msg|
|
225
|
+
data << msg.data
|
226
|
+
end
|
227
|
+
|
228
|
+
@bus.publish("/chuck", "berry")
|
229
|
+
|
230
|
+
wait_for(2000) { data.length > 0 }
|
231
|
+
|
232
|
+
data.must_equal ["berry"]
|
233
|
+
|
234
|
+
@bus.backlog("/chuck").map(&:data).to_a.must_equal ["norris", "berry"]
|
235
|
+
end
|
236
|
+
|
237
|
+
it "can call destroy multiple times" do
|
238
|
+
@bus.destroy
|
239
|
+
@bus.destroy
|
240
|
+
@bus.destroy
|
241
|
+
end
|
242
|
+
|
243
|
+
it "can be turned on after destroy" do
|
244
|
+
@bus.destroy
|
245
|
+
|
246
|
+
@bus.on
|
247
|
+
|
248
|
+
@bus.after_fork
|
249
|
+
end
|
250
|
+
|
219
251
|
it "allows you to look up last_message" do
|
220
252
|
@bus.publish("/bob", "dylan")
|
221
253
|
@bus.publish("/bob", "marley")
|
@@ -40,7 +40,7 @@
|
|
40
40
|
var pollTimeout = null;
|
41
41
|
var totalAjaxFailures = 0;
|
42
42
|
var totalAjaxCalls = 0;
|
43
|
-
var
|
43
|
+
var lastAjaxStartedAt;
|
44
44
|
|
45
45
|
var isHidden = (function () {
|
46
46
|
var prefixes = ["", "webkit", "ms", "moz"];
|
@@ -156,11 +156,11 @@
|
|
156
156
|
}
|
157
157
|
|
158
158
|
var gotData = false;
|
159
|
-
var
|
159
|
+
var abortedByClient = false;
|
160
160
|
var rateLimited = false;
|
161
161
|
var rateLimitedSeconds;
|
162
162
|
|
163
|
-
|
163
|
+
lastAjaxStartedAt = new Date();
|
164
164
|
totalAjaxCalls += 1;
|
165
165
|
data.__seq = totalAjaxCalls;
|
166
166
|
|
@@ -281,7 +281,7 @@
|
|
281
281
|
rateLimitedSeconds = tryAfter;
|
282
282
|
rateLimited = true;
|
283
283
|
} else if (textStatus === "abort") {
|
284
|
-
|
284
|
+
abortedByClient = true;
|
285
285
|
} else {
|
286
286
|
failCount += 1;
|
287
287
|
totalAjaxFailures += 1;
|
@@ -290,27 +290,40 @@
|
|
290
290
|
complete: function () {
|
291
291
|
ajaxInProgress = false;
|
292
292
|
|
293
|
-
var
|
293
|
+
var inLongPollingMode = shouldLongPoll();
|
294
|
+
var startNextRequestAfter;
|
294
295
|
try {
|
295
296
|
if (rateLimited) {
|
296
|
-
|
297
|
-
|
298
|
-
|
297
|
+
// Respect `Retry-After` header
|
298
|
+
startNextRequestAfter = Math.max(
|
299
|
+
me.minPollInterval,
|
300
|
+
rateLimitedSeconds * 1000
|
301
|
+
);
|
302
|
+
} else if (abortedByClient) {
|
303
|
+
// Immediately trigger another poll
|
304
|
+
startNextRequestAfter = me.minPollInterval;
|
305
|
+
} else if (failCount > 2) {
|
306
|
+
// Linear backoff up to maxPollInterval
|
307
|
+
startNextRequestAfter = Math.min(
|
308
|
+
me.callbackInterval * failCount,
|
309
|
+
me.maxPollInterval
|
310
|
+
);
|
311
|
+
} else if (inLongPollingMode && gotData) {
|
312
|
+
// Immediately trigger another poll
|
313
|
+
startNextRequestAfter = me.minPollInterval;
|
299
314
|
} else {
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
if (interval > me.maxPollInterval) {
|
307
|
-
interval = me.maxPollInterval;
|
308
|
-
}
|
315
|
+
// Trigger next poll N seconds after the last one **started**
|
316
|
+
var targetRequestInterval = inLongPollingMode
|
317
|
+
? me.callbackInterval
|
318
|
+
: me.backgroundCallbackInterval;
|
319
|
+
|
320
|
+
var elapsedSinceLastAjaxStarted = new Date() - lastAjaxStartedAt;
|
309
321
|
|
310
|
-
|
322
|
+
startNextRequestAfter =
|
323
|
+
targetRequestInterval - elapsedSinceLastAjaxStarted;
|
311
324
|
|
312
|
-
if (
|
313
|
-
|
325
|
+
if (startNextRequestAfter < 100) {
|
326
|
+
startNextRequestAfter = 100;
|
314
327
|
}
|
315
328
|
}
|
316
329
|
} catch (e) {
|
@@ -328,7 +341,7 @@
|
|
328
341
|
pollTimeout = setTimeout(function () {
|
329
342
|
pollTimeout = null;
|
330
343
|
poll();
|
331
|
-
},
|
344
|
+
}, startNextRequestAfter);
|
332
345
|
}
|
333
346
|
|
334
347
|
me.longPoll = null;
|
@@ -367,7 +380,9 @@
|
|
367
380
|
totalAjaxFailures
|
368
381
|
);
|
369
382
|
console.log(
|
370
|
-
"Last ajax call: " +
|
383
|
+
"Last ajax call: " +
|
384
|
+
(new Date() - lastAjaxStartedAt) / 1000 +
|
385
|
+
" seconds ago"
|
371
386
|
);
|
372
387
|
},
|
373
388
|
|
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: 4.
|
4
|
+
version: 4.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:
|
11
|
+
date: 2023-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|