message_bus 3.3.7 → 4.1.0
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 +1 -8
- data/.github/workflows/ci.yml +50 -20
- data/.prettierrc +1 -0
- data/CHANGELOG +99 -46
- data/README.md +31 -53
- data/Rakefile +22 -22
- data/docker-compose.yml +1 -1
- data/lib/message_bus/backends/base.rb +14 -0
- data/lib/message_bus/backends/memory.rb +13 -0
- data/lib/message_bus/backends/postgres.rb +55 -22
- data/lib/message_bus/backends/redis.rb +17 -1
- data/lib/message_bus/client.rb +26 -22
- data/lib/message_bus/distributed_cache.rb +1 -0
- data/lib/message_bus/rack/middleware.rb +0 -6
- data/lib/message_bus/rack/thin_ext.rb +1 -0
- data/lib/message_bus/version.rb +1 -1
- data/lib/message_bus.rb +53 -71
- data/message_bus.gemspec +4 -3
- 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 +20 -3
- 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 +21 -10
- data/spec/lib/message_bus/rack/middleware_spec.rb +2 -49
- data/spec/lib/message_bus/timer_thread_spec.rb +1 -5
- data/spec/lib/message_bus_spec.rb +12 -3
- data/spec/performance/backlog.rb +80 -0
- data/spec/performance/publish.rb +4 -4
- data/spec/spec_helper.rb +1 -1
- data/vendor/assets/javascripts/message-bus-ajax.js +38 -0
- data/vendor/assets/javascripts/message-bus.js +549 -0
- metadata +8 -31
- data/assets/application.jsx +0 -121
- data/assets/babel.min.js +0 -25
- data/assets/react-dom.js +0 -19851
- data/assets/react.js +0 -3029
- data/examples/diagnostics/Gemfile +0 -6
- data/examples/diagnostics/config.ru +0 -22
- data/lib/message_bus/diagnostics.rb +0 -62
- data/lib/message_bus/rack/diagnostics.rb +0 -120
data/spec/performance/publish.rb
CHANGED
@@ -32,8 +32,8 @@ benchmark_subscription_no_trimming = lambda do |bm, backend|
|
|
32
32
|
bus = MessageBus::Instance.new
|
33
33
|
bus.configure(test_config_for_backend(backend))
|
34
34
|
|
35
|
-
bus.
|
36
|
-
bus.
|
35
|
+
bus.backend_instance.max_backlog_size = iterations
|
36
|
+
bus.backend_instance.max_global_backlog_size = iterations
|
37
37
|
|
38
38
|
messages_received = 0
|
39
39
|
bus.after_fork
|
@@ -58,8 +58,8 @@ benchmark_subscription_with_trimming = lambda do |bm, backend|
|
|
58
58
|
bus = MessageBus::Instance.new
|
59
59
|
bus.configure(test_config_for_backend(backend))
|
60
60
|
|
61
|
-
bus.
|
62
|
-
bus.
|
61
|
+
bus.backend_instance.max_backlog_size = (iterations / 10)
|
62
|
+
bus.backend_instance.max_global_backlog_size = (iterations / 10)
|
63
63
|
|
64
64
|
messages_received = 0
|
65
65
|
bus.after_fork
|
data/spec/spec_helper.rb
CHANGED
@@ -14,7 +14,7 @@ require_relative "helpers"
|
|
14
14
|
CURRENT_BACKEND = (ENV['MESSAGE_BUS_BACKEND'] || :redis).to_sym
|
15
15
|
|
16
16
|
require "message_bus/backends/#{CURRENT_BACKEND}"
|
17
|
-
|
17
|
+
BACKEND_CLASS = MessageBus::BACKENDS.fetch(CURRENT_BACKEND)
|
18
18
|
|
19
19
|
puts "Running with backend: #{CURRENT_BACKEND}"
|
20
20
|
|
@@ -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
|
+
});
|