pubsub-js-client 0.6.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.
- package/README.md +70 -0
- package/package.json +26 -0
- package/src/IframeClient.js +182 -0
- package/src/IframeHost.js +174 -0
- package/src/PubsubDriver.js +177 -0
- package/src/PubsubSocket.js +186 -0
- package/src/PubsubTest.js +189 -0
- package/src/WebsocketClient.js +383 -0
- package/src/events.js +43 -0
- package/src/log.js +121 -0
- package/src/mymap.js +49 -0
- package/src/util.js +51 -0
@@ -0,0 +1,186 @@
|
|
1
|
+
import EventsDispatcher from "./events";
|
2
|
+
import logging from "./log";
|
3
|
+
import util from "./util";
|
4
|
+
|
5
|
+
var logger = logging._getLogger("PubsubSocket");
|
6
|
+
|
7
|
+
const MAX_CONNECTION_DELAY = 120;
|
8
|
+
|
9
|
+
const ERR_SOCKET_CLOSED = "socket_closed";
|
10
|
+
const ERR_PONG_TIMEOUT = "missed_pong";
|
11
|
+
const ERR_CONNECTION_FAILED = "max_connection_attempts";
|
12
|
+
const ERR_NOT_READY = "not_ready";
|
13
|
+
const ERR_FAILED_SEND = "failed_send";
|
14
|
+
|
15
|
+
const PONG_TIMEOUT = 30 * 1000; // 30 seconds
|
16
|
+
const PING_INTERVAL = 4 * 60 * 1000; // 4 minutes
|
17
|
+
|
18
|
+
/*
|
19
|
+
PubsubSocket is responsible for managing the WebSocket connction to Pubsub Edge, hiding the details of ping/pong, connection retries, and parsing received message types from the PubsubDriver.
|
20
|
+
It retries connecting with exponential backoff if it fails to connect.
|
21
|
+
When it opens, it triggers an "open" event.
|
22
|
+
If it closes unexpectedly, it triggers a "connection_failure" event.
|
23
|
+
It handles pings/pongs, and triggers a "pong_timeout" event if a pong isn't received.
|
24
|
+
If it is intentionally closed, it triggers a "closed" event when it closes.
|
25
|
+
If it receives a RESPONSE type message from the Pubsub, it triggers a "response" event.
|
26
|
+
If it receives a MESSAGE type message from the Pubsub, it triggers a "message" event.
|
27
|
+
If it receives a RECONNECT type message from the Pubsub, it triggers a "reconnect" event.
|
28
|
+
*/
|
29
|
+
|
30
|
+
class PubsubSocket extends EventsDispatcher {
|
31
|
+
constructor (opts) {
|
32
|
+
super(opts);
|
33
|
+
this._opts = opts;
|
34
|
+
this._addr = opts.addr;
|
35
|
+
|
36
|
+
this._connectionAttempts = 0;
|
37
|
+
this._sentPing = this._receivedPong = false;
|
38
|
+
|
39
|
+
this._id = "[" + util.generateString(10) + "] ";
|
40
|
+
|
41
|
+
window.addEventListener("beforeunload", this._beforeUnload.bind(this));
|
42
|
+
}
|
43
|
+
|
44
|
+
connect () {
|
45
|
+
logger.debug(this._id + "connecting to " + this._addr);
|
46
|
+
this._connecting = true;
|
47
|
+
try {
|
48
|
+
this._socket = new WebSocket(this._addr);
|
49
|
+
this._socket.onmessage = this._onMessage.bind(this);
|
50
|
+
this._socket.onerror = this._onError.bind(this);
|
51
|
+
this._socket.onclose = this._onClose.bind(this);
|
52
|
+
this._socket.onopen = this._onOpen.bind(this);
|
53
|
+
} catch (e) {
|
54
|
+
this._trigger("connection_failure");
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
close () {
|
59
|
+
logger.debug(this._id + "closing");
|
60
|
+
this._closing = true;
|
61
|
+
this._clearTimeouts();
|
62
|
+
this._socket.close();
|
63
|
+
}
|
64
|
+
|
65
|
+
send (msg) {
|
66
|
+
logger.debug(this._id + "sending " + JSON.stringify(msg));
|
67
|
+
if (this._isReady()) {
|
68
|
+
this._socket.send(JSON.stringify(msg));
|
69
|
+
} else {
|
70
|
+
this._trigger("error", ERR_NOT_READY);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
_isReady () {
|
75
|
+
logger.debug(this._id + "_isReady called");
|
76
|
+
if (this._socket) {
|
77
|
+
return (this._socket.readyState === WebSocket.OPEN);
|
78
|
+
} else {
|
79
|
+
return false;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
_onMessage (event) {
|
84
|
+
logger.debug(this._id + "received message: " + event.data);
|
85
|
+
try {
|
86
|
+
var msg = JSON.parse(event.data);
|
87
|
+
switch (msg.type) {
|
88
|
+
case "RESPONSE":
|
89
|
+
this._trigger("response", msg);
|
90
|
+
break;
|
91
|
+
case "MESSAGE":
|
92
|
+
this._trigger("message", msg);
|
93
|
+
break;
|
94
|
+
case "PONG":
|
95
|
+
this._receivedPong = true;
|
96
|
+
break;
|
97
|
+
case "RECONNECT":
|
98
|
+
this._trigger("reconnect");
|
99
|
+
break;
|
100
|
+
}
|
101
|
+
} catch (e) {
|
102
|
+
// bad json parse
|
103
|
+
logger.debug("onMessage JSON Parse error: " + e);
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
_onError (event) {
|
108
|
+
// Irrelevant since the _onClose event is about to be triggered
|
109
|
+
logger.debug(this._id + "error: " + JSON.stringify(event));
|
110
|
+
}
|
111
|
+
|
112
|
+
_onClose (event) {
|
113
|
+
logger.debug(this._id + "onClose triggered with code " + event.code + "(closing = " + this._closing + ", connecting = " + this._connecting + ")");
|
114
|
+
this._clearTimeouts();
|
115
|
+
if (this._connecting) {
|
116
|
+
// Failed during connection, retry with exponential backoff
|
117
|
+
var connectionDelay = Math.pow(2, this._connectionAttempts);
|
118
|
+
if (connectionDelay > MAX_CONNECTION_DELAY) {
|
119
|
+
connectionDelay = MAX_CONNECTION_DELAY;
|
120
|
+
}
|
121
|
+
logger.debug(this._id + "reconnecting in " + connectionDelay + " seconds");
|
122
|
+
this._connectionAttempts += 1;
|
123
|
+
this._nextConnectionAttempt = setTimeout(this.connect.bind(this), 1000 * connectionDelay);
|
124
|
+
} else if (this._closing) {
|
125
|
+
// Intentionally closed itself (due to an error), don't send an 'unexpected_closed' error since the relevant error will have already been sent
|
126
|
+
this._closed = true;
|
127
|
+
this._trigger("connection_failure");
|
128
|
+
} else if (this._windowUnloading) {
|
129
|
+
// Closed because the browser window is being refreshed or redirected
|
130
|
+
// Don't trigger anything, whole object is going to be destroyed by the browser anyhow
|
131
|
+
return;
|
132
|
+
} else {
|
133
|
+
// Unintentionally closed, trigger an error so the Driver knows to re-fetch state
|
134
|
+
logger.debug(this._id + "unexpected close");
|
135
|
+
var line = "pubsub-js-client unexpected_close. code: " + event.code + ", reason: " + event.reason + ", wasClean: " + event.wasClean;
|
136
|
+
this._closed = true;
|
137
|
+
this._trigger("connection_failure");
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
_onOpen (event) {
|
142
|
+
logger.debug(this._id + " socket opened");
|
143
|
+
this._connectionAttempts = 0;
|
144
|
+
this._connecting = false;
|
145
|
+
|
146
|
+
this._ping();
|
147
|
+
this._pingInterval = window.setInterval(this._ping.bind(this), PING_INTERVAL);
|
148
|
+
this._trigger("open");
|
149
|
+
}
|
150
|
+
|
151
|
+
_ping () {
|
152
|
+
logger.debug(this._id + "sending PING");
|
153
|
+
try {
|
154
|
+
this._socket.send(JSON.stringify({type: "PING"}));
|
155
|
+
this._sentPing = true;
|
156
|
+
if (this._pongTimeout) {
|
157
|
+
clearTimeout(this._pongTimeout);
|
158
|
+
}
|
159
|
+
this._pongTimeout = setTimeout(this._pongTimedOut.bind(this), PONG_TIMEOUT);
|
160
|
+
} catch (e) {
|
161
|
+
this.close();
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
_pongTimedOut () {
|
166
|
+
if (this._sentPing && !this._receivedPong) {
|
167
|
+
logger.debug(this._id + "Pong timed out!");
|
168
|
+
// Close the socket; this will initiate the reconnection flow automatically
|
169
|
+
this.close();
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
_clearTimeouts () {
|
174
|
+
this._sentPing = this._receivedPong = false;
|
175
|
+
clearTimeout(this._pongTimeout);
|
176
|
+
clearInterval(this._pingInterval);
|
177
|
+
clearTimeout(this._nextConnectionAttempt);
|
178
|
+
}
|
179
|
+
|
180
|
+
_beforeUnload () {
|
181
|
+
this._windowUnloading = true;
|
182
|
+
}
|
183
|
+
|
184
|
+
}
|
185
|
+
|
186
|
+
export default PubsubSocket;
|
@@ -0,0 +1,189 @@
|
|
1
|
+
/* globals $ */
|
2
|
+
|
3
|
+
import util from "./util";
|
4
|
+
import logging from "./log";
|
5
|
+
|
6
|
+
const pubsterAddrProduction = "https://pubster.twitch.tv/publish";
|
7
|
+
const pubsterAddrDarklaunch = "https://pubster-darklaunch.twitch.tv/publish";
|
8
|
+
|
9
|
+
const uniqueKey = "pubsubtest.unique.";
|
10
|
+
const sharedKey = "pubsubtest.shared." + util.randomInt(10); // 10 topics
|
11
|
+
const pctPublishShared = 0.0001;
|
12
|
+
|
13
|
+
const publishIntervalTime = 60 * 1000; // 60 seconds
|
14
|
+
const ajaxTimeout = 30 * 1000; // 30 seconds
|
15
|
+
|
16
|
+
const sampleRateSuccess = 0.1;
|
17
|
+
const sampleRateFailure = 1.0;
|
18
|
+
|
19
|
+
var logger = logging._getLogger("PubsubTest");
|
20
|
+
|
21
|
+
class PubsubTest {
|
22
|
+
constructor (opts) {
|
23
|
+
if (!window.$) {
|
24
|
+
logger.debug("PubsubTest could not be enabled. JQuery is undefined.");
|
25
|
+
return;
|
26
|
+
}
|
27
|
+
|
28
|
+
logger.debug("PubsubTest enabled");
|
29
|
+
this._env = opts.env;
|
30
|
+
this._driver = opts.driver;
|
31
|
+
|
32
|
+
switch (this._env) {
|
33
|
+
case "production":
|
34
|
+
this._addr = pubsterAddrProduction;
|
35
|
+
break;
|
36
|
+
case "darklaunch":
|
37
|
+
this._addr = pubsterAddrDarklaunch;
|
38
|
+
break;
|
39
|
+
default:
|
40
|
+
this._env = "production";
|
41
|
+
this._addr = pubsterAddrProduction;
|
42
|
+
}
|
43
|
+
|
44
|
+
this._statKeys = {
|
45
|
+
uniqueSuccess: "test.unique.success",
|
46
|
+
uniqueFailure: "test.unique.failure",
|
47
|
+
sharedSuccess: "test.shared.success",
|
48
|
+
sharedFailure: "test.shared.failure"
|
49
|
+
};
|
50
|
+
|
51
|
+
this._uniqueKey = uniqueKey + util.generateString(20);
|
52
|
+
this._sharedKey = sharedKey;
|
53
|
+
|
54
|
+
this._listeningUnique = this._listeningShared = false;
|
55
|
+
this.sendListens();
|
56
|
+
}
|
57
|
+
|
58
|
+
sendListens () {
|
59
|
+
this._driver.Listen({
|
60
|
+
topic: this._uniqueKey,
|
61
|
+
auth: "",
|
62
|
+
success: this._gotUniqueOk.bind(this),
|
63
|
+
failure: this._gotUniqueFail.bind(this),
|
64
|
+
message: this._gotUniqueMessage.bind(this)
|
65
|
+
});
|
66
|
+
|
67
|
+
this._driver.Listen({
|
68
|
+
topic: this._sharedKey,
|
69
|
+
auth: "",
|
70
|
+
success: this._gotSharedOk.bind(this),
|
71
|
+
failure: this._gotSharedFail.bind(this),
|
72
|
+
message: this._gotSharedMessage.bind(this)
|
73
|
+
});
|
74
|
+
}
|
75
|
+
|
76
|
+
_gotUniqueOk () {
|
77
|
+
this._listeningUnique = true;
|
78
|
+
if (this._listeningShared) {
|
79
|
+
this.startTesting();
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
_gotUniqueFail (err) {
|
84
|
+
// Ignore
|
85
|
+
}
|
86
|
+
|
87
|
+
_gotSharedOk () {
|
88
|
+
this._listeningShared = true;
|
89
|
+
if (this._listeningUnique) {
|
90
|
+
this.startTesting();
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
_gotSharedFail (err) {
|
95
|
+
// Ignore
|
96
|
+
}
|
97
|
+
|
98
|
+
startTesting() {
|
99
|
+
logger.debug("startTesting");
|
100
|
+
this._driver.on("connected", this.resumeTesting, this);
|
101
|
+
this._driver.on("disconnected", this.stopTesting, this);
|
102
|
+
this.checkAndSend();
|
103
|
+
this._publishInterval = window.setInterval(this.checkAndSend.bind(this), publishIntervalTime);
|
104
|
+
}
|
105
|
+
|
106
|
+
resumeTesting() {
|
107
|
+
logger.debug("resumeTesting");
|
108
|
+
this.checkAndSend();
|
109
|
+
this._publishInterval = window.setInterval(this.checkAndSend.bind(this), publishIntervalTime);
|
110
|
+
}
|
111
|
+
|
112
|
+
stopTesting() {
|
113
|
+
logger.debug("stopTesting");
|
114
|
+
clearInterval(this._publishInterval);
|
115
|
+
this._receivedUniqueMessage = this._sentUniqueMessage = false;
|
116
|
+
this._receivedSharedMessage = this._sentSharedMessage = false;
|
117
|
+
}
|
118
|
+
|
119
|
+
checkAndSend() {
|
120
|
+
logger.debug("checkAndSend: unique: sent = " + this._sentUniqueMessage + ", received = " + this._receivedUniqueMessage);
|
121
|
+
if (!this._receivedUniqueMessage && this._sentUniqueMessage) {
|
122
|
+
// log unique error
|
123
|
+
logger.debug("unique failure");
|
124
|
+
}
|
125
|
+
if (!this._receivedSharedMessage && this._sentSharedMessage) {
|
126
|
+
// log shared error
|
127
|
+
logger.debug("shared failure");
|
128
|
+
}
|
129
|
+
|
130
|
+
this._receivedUniqueMessage = this._sentUniqueMessage = false;
|
131
|
+
this._receivedSharedMessage = this._sentSharedMessage = false;
|
132
|
+
|
133
|
+
this._expectedMessage = util.generateString(30);
|
134
|
+
|
135
|
+
// publish unique message to testPub
|
136
|
+
$.ajax({
|
137
|
+
type: "POST",
|
138
|
+
url: this._addr,
|
139
|
+
contentType: "application/json",
|
140
|
+
timeout: ajaxTimeout,
|
141
|
+
data: JSON.stringify({
|
142
|
+
topics: [this._uniqueKey],
|
143
|
+
data: this._expectedMessage
|
144
|
+
}),
|
145
|
+
success: (function () {
|
146
|
+
logger.debug("unique message sent");
|
147
|
+
this._sentUniqueMessage = true;
|
148
|
+
}).bind(this)
|
149
|
+
});
|
150
|
+
this._sentUniqueMessageTime = util.time.now();
|
151
|
+
|
152
|
+
// potentially publish shared message
|
153
|
+
if (Math.random() < pctPublishShared) {
|
154
|
+
$.ajax({
|
155
|
+
type: "POST",
|
156
|
+
url: this._addr,
|
157
|
+
contentType: "application/json",
|
158
|
+
timeout: ajaxTimeout,
|
159
|
+
data: JSON.stringify({
|
160
|
+
topics: [this._sharedKey],
|
161
|
+
data: this._expectedMessage
|
162
|
+
}),
|
163
|
+
success: (function () {
|
164
|
+
logger.debug("shared message sent");
|
165
|
+
this._sentSharedMessage = true;
|
166
|
+
}).bind(this)
|
167
|
+
});
|
168
|
+
this._sentSharedMessageTime = util.time.now();
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
_gotUniqueMessage (msg) {
|
173
|
+
logger.debug("received unique message: " + msg);
|
174
|
+
if (msg === this._expectedMessage) {
|
175
|
+
var rtt = util.time.now() - this._sentUniqueMessageTime;
|
176
|
+
this._receivedUniqueMessage = true;
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
_gotSharedMessage (msg) {
|
181
|
+
if (msg === this._expectedMessage) {
|
182
|
+
var rtt = util.time.now() - this._sentSharedMessageTime;
|
183
|
+
this._receivedSharedMessage = true;
|
184
|
+
}
|
185
|
+
}
|
186
|
+
|
187
|
+
}
|
188
|
+
|
189
|
+
export default PubsubTest;
|