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 ADDED
@@ -0,0 +1,70 @@
1
+ # pubsub-js-client
2
+
3
+ This is the library to integrate with our new PubSub service.
4
+
5
+ ## Installation
6
+
7
+ Make sure you are running node version `8.17.0` as more modern versions do not work.
8
+
9
+ ```bash
10
+ make build
11
+ ```
12
+
13
+ This generates `dist/pubsub.js` which is in AMD.
14
+
15
+ ## Browser parameters
16
+
17
+ * `pubsub_log_level` Set this to `debug` to see all messages in console logs
18
+ * `force_pubsub_tester` Set this to `false` to disable pubsub_tester messages
19
+
20
+ ## Client API
21
+
22
+ ##### Instantiation
23
+ ```javascript
24
+ let driver = PubsubDriver.getInstance(environment); // environment can be "production" or "darklaunch"
25
+
26
+ // Driver automatically connects to Pubsub on instantiation
27
+
28
+ driver.on("connected", function () { ... });
29
+ // Triggered when the driver first connects
30
+ // Also triggered upon recovering from a disconnect
31
+ driver.on("reconnected", function() { ... });
32
+ // Triggered when the driver recovers from a disconnect
33
+ driver.on("disconnected", function () { ... });
34
+ // Triggered when the driver loses connection to the Pubsub
35
+ // Driver automatically attempts to reconnect and re-listen on topics
36
+ ```
37
+ ##### Methods
38
+ ```javascript
39
+ driver.Listen({
40
+ topic: "topic",
41
+ auth: "auth_token",
42
+ success: function (), // callback when Driver has successfully listened on the topic
43
+ failure: function (err), // callback when there was an error listening - either a permissions error, or a timeout
44
+ message: function (msg) // callback when a message is received on this topic
45
+ });
46
+
47
+ driver.Unlisten({
48
+ topic: "topic",
49
+ success: function (), // callback when the Driver has successfully unlistened on the topic
50
+ failure: function (err), // callback when there was an error unlistening
51
+ message: function (msg) // a reference to the callback used in the initial Listen(), to specify which callback to remove
52
+ })
53
+ ```
54
+
55
+ ##### Example
56
+ ```javascript
57
+ let driver = PubsubDriver.getInstance("production");
58
+ driver.Listen({
59
+ topic: "pubsubtest.123456",
60
+ success: function () { console.log("successfully listened"); },
61
+ failure: function (err) { console.log("error listening: " + err); },
62
+ message: function (msg) { console.log("received message: " + msg); }
63
+ });
64
+ ```
65
+ ```bash
66
+ curl -v -X POST https://pubster.twitch.tv/publish -d '{"topics":["pubsubtest.123456"],"data":"arbitrary string"}'
67
+ ```
68
+
69
+ ##### Troubleshooting
70
+ Try `let driver = PubsubDriver.default.getInstance("production");` (add `.default.`)
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "pubsub-js-client",
3
+ "version": "0.6.1",
4
+ "description": "Pubsub JS Client",
5
+ "main": "src/PubsubDriver.js",
6
+ "files": [
7
+ "src"
8
+ ],
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "devDependencies": {
13
+ "babel-core": "^6.5.1",
14
+ "babel-loader": "^6.2.2",
15
+ "babel-preset-es2015": "^6.5.0",
16
+ "broccoli": "^0.16.3",
17
+ "broccoli-babel-transpiler": "^5.1.1",
18
+ "broccoli-cli": "^1.0.0",
19
+ "broccoli-es6modules": "^0.6.1",
20
+ "broccoli-funnel": "^0.2.3",
21
+ "broccoli-sourcemap-concat": "^0.4.4",
22
+ "jscs": "^1.13.1",
23
+ "jshint": "^2.9.1",
24
+ "webpack": "^1.12.13"
25
+ }
26
+ }
@@ -0,0 +1,182 @@
1
+ import EventsDispatcher from "./events";
2
+ import logging from "./log";
3
+ import util from "./util";
4
+ import MyMap from "./mymap";
5
+
6
+ const NONCE_LENGTH = 30;
7
+ const RESPONSE_TIMEOUT = 30 * 1000; // 30 seconds
8
+ const VERIFY_TIMEOUT = 1 * 1000; // 1 second
9
+ const TWITCH_PROTOCOL = "pubsub";
10
+
11
+ var logger = logging._getLogger("IframeClient");
12
+
13
+ class IframeClient extends EventsDispatcher {
14
+ constructor (opts) {
15
+ super(opts);
16
+ this._parentUrl = opts.parentUrl;
17
+ this._pendingResponses = new MyMap();
18
+ this._listens = new EventsDispatcher();
19
+
20
+ window.addEventListener("message", this.receiveMessage.bind(this), false);
21
+ }
22
+
23
+ connect () {
24
+ window.parent.postMessage({
25
+ twitch_protocol: TWITCH_PROTOCOL,
26
+ type: "connect"
27
+ }, this._parentUrl);
28
+ }
29
+
30
+ verify () {
31
+ window.parent.postMessage({
32
+ twitch_protocol: TWITCH_PROTOCOL,
33
+ type: "verify"
34
+ }, this._parentUrl);
35
+ this._verifyTimeout = setTimeout(this._unverified.bind(this), VERIFY_TIMEOUT);
36
+ }
37
+
38
+ Listen (opts) {
39
+ // opts should include: topic, auth, success, failure, message
40
+ logger.debug("listening on " + opts.topic);
41
+ var nonce = this._generateNonce();
42
+ var msg = {
43
+ twitch_protocol: TWITCH_PROTOCOL,
44
+ type: "LISTEN",
45
+ nonce: nonce,
46
+ data: {
47
+ topics: [opts.topic],
48
+ auth_token: opts.auth
49
+ }
50
+ };
51
+ this._send(nonce, msg, opts);
52
+ }
53
+
54
+ Unlisten (opts) {
55
+ // opts should include: topic, success, failure, message
56
+ logger.debug("unlistening on " + opts.topic + "(" + this._listens.count(opts.topic) + " listeners)");
57
+
58
+ // If there are more than one callbacks waiting on this topic, we can just remove the specified one rather than sending an UNLISTEN
59
+ if (this._listens.count(opts.topic) > 1) {
60
+
61
+ if (opts.message) {
62
+ this._listens.off(opts.topic, opts.message);
63
+ }
64
+
65
+ if (opts.success) {
66
+ opts.success();
67
+ }
68
+
69
+ logger.debug("now have " + this._listens.count(opts.topic) + " listeners");
70
+ return;
71
+ }
72
+
73
+ var nonce = this._generateNonce();
74
+ var msg = {
75
+ twitch_protocol: TWITCH_PROTOCOL,
76
+ type: "UNLISTEN",
77
+ nonce: nonce,
78
+ data: {
79
+ topics: [opts.topic]
80
+ }
81
+ };
82
+ this._send(nonce, msg, opts);
83
+ }
84
+
85
+ _send (nonce, msg, opts) {
86
+ this._pendingResponses.set(nonce, {
87
+ timeout: setTimeout(this._onResponseTimeout.bind(this), RESPONSE_TIMEOUT, nonce),
88
+ topic: opts.topic,
89
+ auth: opts.auth,
90
+ message: msg,
91
+ callbacks: {
92
+ success: opts.success,
93
+ failure: opts.failure,
94
+ message: opts.message
95
+ }
96
+ });
97
+ window.parent.postMessage(msg, this._parentUrl);
98
+ }
99
+
100
+ receiveMessage (event) {
101
+ if (!event.data || event.data.twitch_protocol != TWITCH_PROTOCOL) {
102
+ return;
103
+ }
104
+ logger.debug("Received message: " + JSON.stringify(event.data));
105
+ switch (event.data.type) {
106
+ case "connected":
107
+ this._trigger("connected");
108
+ break;
109
+ case "disconnected":
110
+ this._trigger("disconnected");
111
+ break;
112
+ case "success":
113
+ this.handleResponse(true, event.data);
114
+ break;
115
+ case "failure":
116
+ this.handleResponse(false, event.data);
117
+ break;
118
+ case "message":
119
+ this.handleMessage(event.data.topic, event.data.message);
120
+ break;
121
+ case "verify":
122
+ this._verified();
123
+ break;
124
+ }
125
+ }
126
+
127
+ handleResponse (successful, resp) {
128
+ if (this._pendingResponses.has(resp.nonce)) {
129
+ var responseInfo = this._pendingResponses.get(resp.nonce);
130
+ logger.debug("ResponseInfo: " + JSON.stringify(responseInfo));
131
+ clearTimeout(responseInfo.timeout);
132
+ this._pendingResponses.remove(resp.nonce);
133
+
134
+ if (successful) {
135
+
136
+
137
+ if (responseInfo.callbacks.message) {
138
+ if (responseInfo.message.type === "LISTEN") {
139
+ this._listens.on(responseInfo.topic, responseInfo.callbacks.message, this);
140
+ } else if (responseInfo.message.type === "UNLISTEN") {
141
+ this._listens.off(responseInfo.topic, responseInfo.callbacks.message, this);
142
+ }
143
+ }
144
+
145
+ if (responseInfo.callbacks.success) {
146
+ responseInfo.callbacks.success();
147
+ }
148
+ } else {
149
+ if (responseInfo.callbacks.failure) {
150
+ responseInfo.callbacks.failure(resp.error);
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ handleMessage (topic, msg) {
157
+ logger.debug("received '" + msg + "' on topic " + topic);
158
+ this._listens._trigger(topic, msg);
159
+ }
160
+
161
+ _onResponseTimeout (nonce) {
162
+ logger.debug("response timed out: " + nonce);
163
+ }
164
+
165
+ _verified () {
166
+ logger.debug("Verified");
167
+ clearTimeout(this._verifyTimeout);
168
+ this._trigger("verified");
169
+ }
170
+
171
+ _unverified () {
172
+ window.removeEventListener("message", this.receiveMessage.bind(this), false);
173
+ this._trigger("unverified");
174
+ }
175
+
176
+ // Utility functions
177
+ _generateNonce () {
178
+ return util.generateString(NONCE_LENGTH);
179
+ }
180
+ }
181
+
182
+ export default IframeClient;
@@ -0,0 +1,174 @@
1
+ import logging from "./log";
2
+
3
+ const TWITCH_PROTOCOL = "pubsub";
4
+ const ORPHAN_CHECK_INTERVAL = 10000;
5
+
6
+ var logger = logging._getLogger("IframeHost");
7
+
8
+ class IframeHost {
9
+ constructor (driver) {
10
+ this._driver = driver;
11
+ this._sources = [];
12
+ this._listeners = [];
13
+ this._driver.on("connected", this.handleConnected, this);
14
+ this._driver.on("disconnected", this.handleDisconnected, this);
15
+ window.addEventListener("message", this.receiveMessage.bind(this), false);
16
+
17
+ // Periodically check to see if we have any orphaned listeners
18
+ this._orphanedListenerCheckTimer = setInterval(this._checkOrphanedListeners.bind(this), ORPHAN_CHECK_INTERVAL);
19
+ }
20
+
21
+ destroy () {
22
+ clearInterval(this._orphanedListenerCheckTimer);
23
+ }
24
+
25
+ receiveMessage (event) {
26
+ if (!event.data || event.data.twitch_protocol != TWITCH_PROTOCOL || !event.source) {
27
+ return;
28
+ }
29
+ logger.debug("Received message: " + JSON.stringify(event.data));
30
+ switch (event.data.type) {
31
+ case "LISTEN":
32
+ this.handleListen(event.source, event.data.nonce, event.data.data);
33
+ break;
34
+ case "UNLISTEN":
35
+ this.handleUnlisten(event.source, event.data.nonce, event.data.data);
36
+ break;
37
+ case "connect":
38
+ this._sources.push(event.source);
39
+ this._driver.connect();
40
+ break;
41
+ case "verify":
42
+ event.source.postMessage({
43
+ twitch_protocol: TWITCH_PROTOCOL,
44
+ type: "verify"
45
+ }, "*");
46
+ break;
47
+ }
48
+ }
49
+
50
+ // Keeps track of which sources are listening to which topics
51
+ // Returns an object containing the source, topic, and onMessage callback
52
+ // If a listener already exists for the specified source and topic, returns null
53
+ _pushListener (source, topic) {
54
+ // If we've already got a listener for this source and topic we don't need to create another one
55
+ for (var i = 0; i < this._listeners.length; i++) {
56
+ if (this._listeners[i].source === source && this._listeners[i].topic === topic) {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ var listener = {
62
+ source: source,
63
+ topic: topic,
64
+ message: function (msg) {
65
+ source.postMessage({twitch_protocol: TWITCH_PROTOCOL, type: "message", topic: topic, message: msg}, "*");
66
+ }
67
+ };
68
+
69
+ this._listeners.push(listener);
70
+
71
+ return listener;
72
+ }
73
+
74
+ // Untracks and returns the listener for the given source and topic
75
+ // If no listener is found for the specified source and topic, returns null
76
+ _popListener (source, topic) {
77
+ for (var i = 0; i < this._listeners.length; i++) {
78
+ if (this._listeners[i].source === source && this._listeners[i].topic === topic) {
79
+ return this._listeners.splice(i, 1)[0];
80
+ }
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ // Checks to see if any of the registered listeners have a source that has been deleted
87
+ // Calls Unlisten on any listeners that are found
88
+ _checkOrphanedListeners () {
89
+ for (var i = 0; i < this._listeners.length; i++) {
90
+ // source is a reference to the window that sent the postMessage
91
+ if (this._listeners[i].source.closed) {
92
+ this._cleanUpOrphanedListener(this._listeners.splice(i--, 1)[0]);
93
+ }
94
+ }
95
+ }
96
+
97
+ // Call Unlisten for the supplied listener with local logging as callback because the listener passed in has no source
98
+ _cleanUpOrphanedListener (listener) {
99
+ logger.debug("Cleaning up orphaned listener for topic: " + listener.topic);
100
+ this._driver.Unlisten({
101
+ topic: listener.topic,
102
+ success: (function () {
103
+ logger.debug("Success when cleaning up orphaned listener for topic: " + listener.topic);
104
+ }),
105
+ failure: (function (err) {
106
+ logger.debug("Error when cleaning up orphaned listener for topic: " + listener.topic + " Error: " + err);
107
+ }),
108
+ message: listener.message
109
+ });
110
+ }
111
+
112
+ handleListen (source, nonce, data) {
113
+ var listener = this._pushListener(source, data.topics[0]);
114
+
115
+ // We already have a listener for this source and topic so just send back the success message
116
+ if (!listener) {
117
+ source.postMessage({twitch_protocol: TWITCH_PROTOCOL, type: "success", nonce: nonce}, "*");
118
+ return;
119
+ }
120
+
121
+ this._driver.Listen({
122
+ topic: listener.topic,
123
+ auth: data.auth_token,
124
+ success: (function () {
125
+ source.postMessage({twitch_protocol: TWITCH_PROTOCOL, type: "success", nonce: nonce}, "*");
126
+ }),
127
+ failure: (function (err) {
128
+ source.postMessage({twitch_protocol: TWITCH_PROTOCOL, type: "failure", nonce: nonce, error: err}, "*");
129
+ }),
130
+ message: listener.message
131
+ });
132
+ }
133
+
134
+ handleUnlisten (source, nonce, data) {
135
+ var listener = this._popListener(source, data.topics[0]);
136
+
137
+ if (!listener) {
138
+ logger.debug("Failed to unlisten, could not find listener for topic " + data.topics[0]);
139
+ return;
140
+ }
141
+
142
+ this._driver.Unlisten({
143
+ topic: listener.topic,
144
+ auth: data.auth_token,
145
+ success: (function () {
146
+ source.postMessage({twitch_protocol: TWITCH_PROTOCOL, type: "success", nonce: nonce}, "*");
147
+ }),
148
+ failure: (function (err) {
149
+ source.postMessage({twitch_protocol: TWITCH_PROTOCOL, type: "failure", nonce: nonce, error: err}, "*");
150
+ }),
151
+ message: listener.message
152
+ });
153
+ }
154
+
155
+ handleConnected () {
156
+ for (var i = 0; i < this._sources.length; i++) {
157
+ this._sources[i].postMessage({
158
+ twitch_protocol: TWITCH_PROTOCOL,
159
+ type: "connected"
160
+ }, "*");
161
+ }
162
+ }
163
+
164
+ handleDisconnected () {
165
+ for (var i = 0; i < this._sources.length; i++) {
166
+ this._sources[i].postMessage({
167
+ twitch_protocol: TWITCH_PROTOCOL,
168
+ type: "disconnected"
169
+ }, "*");
170
+ }
171
+ }
172
+ }
173
+
174
+ export default IframeHost;
@@ -0,0 +1,177 @@
1
+
2
+ import EventsDispatcher from "./events";
3
+ import logging from "./log";
4
+ import util from "./util";
5
+ import IframeClient from "./IframeClient";
6
+ import IframeHost from "./IframeHost";
7
+ import WebsocketClient from "./WebsocketClient";
8
+ import PubsubTest from "./PubsubTest";
9
+
10
+ var logger = logging._getLogger("PubsubDriver");
11
+
12
+ const SOCKET_CLOSED_RECONNECT_TIME = 1 * 1000; // 1 second
13
+ const RESPONSE_TIMEOUT = 30 * 1000; // 30 seconds
14
+ const ERR_RESPONSE_TIMEOUT = "response timeout";
15
+ const NONCE_LENGTH = 30;
16
+ const DEFER_TEST_TIME = 30 * 1000; // 30 seconds
17
+
18
+ const twitchUrlRegexp = /^https?:\/\/([\w-]+\.)*twitch\.(tv|tech)(:\d+)?\/.*$/;
19
+
20
+ var pctTester = 0.0;
21
+
22
+ class PubsubDriver extends EventsDispatcher {
23
+
24
+ constructor (env) {
25
+ // opts should include: environment
26
+ super(env);
27
+
28
+ this._env = util.urlParams.pubsub_environment || env;
29
+
30
+ this._clientReady = false;
31
+ this._hasDisconnected = false;
32
+ this._queuedRequests = [];
33
+
34
+ this._numDisconnects = 0;
35
+
36
+ if (util.inIframe() && twitchUrlRegexp.test(document.referrer)) {
37
+ // check parent location, if ok, create iframe with specified domain
38
+ logger.debug("Driver is in an iframe");
39
+ this._client = new IframeClient({
40
+ parentUrl: document.referrer
41
+ });
42
+ this._clientType = "iframe-verified";
43
+ } else {
44
+ logger.debug("Driver is not in an iframe");
45
+ this._client = new WebsocketClient({
46
+ env: this._env
47
+ });
48
+ this._iframeHost = new IframeHost(this._client);
49
+ this._clientType = "ws";
50
+ }
51
+
52
+ // Pubsub Tester
53
+ if (util.urlParams.force_pubsub_tester === "true") {
54
+ pctTester = 1.0;
55
+ } else if (util.urlParams.force_pubsub_tester === "false") {
56
+ pctTester = 0.0;
57
+ }
58
+ if (Math.random() < pctTester) {
59
+ // Defer starting the test to avoid wasting resources during page load.
60
+ window.setTimeout(this.runTest.bind(this), DEFER_TEST_TIME);
61
+ }
62
+
63
+ this._client.on("unverified", this._clientUnverified, this);
64
+ this._client.on("verified", this._clientVerified, this);
65
+ this._client.verify();
66
+ }
67
+
68
+ runTest () {
69
+ this._tester = new PubsubTest({
70
+ env: this._env,
71
+ driver: this
72
+ });
73
+ }
74
+
75
+ connect () {
76
+ // return this._client.connect();
77
+ }
78
+
79
+ Listen (opts) {
80
+ if (this._clientReady) {
81
+ this._client.Listen(opts);
82
+ } else {
83
+ this._queuedRequests.push({type: "LISTEN", opts: opts});
84
+ }
85
+ }
86
+
87
+ Unlisten (opts) {
88
+ if (this._clientReady) {
89
+ this._client.Unlisten(opts);
90
+ } else {
91
+ this._queuedRequests.push({type: "UNLISTEN", opts: opts});
92
+ }
93
+ }
94
+
95
+ simulateReceivedMessage(topic, message) {
96
+ const msg = {
97
+ data: {message, topic},
98
+ type: "MESSAGE-SIMULATED"
99
+ };
100
+ this._client._onMessage(msg);
101
+ }
102
+
103
+ _flushQueuedRequests () {
104
+ logger.debug("Flushing " + this._queuedRequests.length + " queued requests");
105
+ while (this._queuedRequests.length > 0) {
106
+ var req = this._queuedRequests.shift();
107
+ switch (req.type) {
108
+ case "LISTEN":
109
+ this._client.Listen(req.opts);
110
+ break;
111
+ case "UNLISTEN":
112
+ this._client.Unlisten(req.opts);
113
+ break;
114
+ }
115
+ }
116
+ }
117
+
118
+ _clientConnected () {
119
+ logger.debug("Client connected");
120
+ this._client.on("disconnected", this._clientDisconnected, this);
121
+ this._trigger("connected");
122
+ if (this._hasDisconnected) {
123
+ this._trigger("reconnected");
124
+ }
125
+ this._clientReady = true;
126
+ this._flushQueuedRequests();
127
+ }
128
+
129
+ _clientDisconnected () {
130
+ logger.debug("Client disconnected");
131
+ this._trigger("disconnected");
132
+ this._clientReady = false;
133
+ this._numDisconnects += 1;
134
+ this._hasDisconnected = true;
135
+ }
136
+
137
+ _clientVerified () {
138
+ logger.debug("Client verified (type = " + this._clientType + ")");
139
+ this._client.on("connected", this._clientConnected, this);
140
+ this._client.connect();
141
+ }
142
+
143
+ _clientUnverified () {
144
+ // only triggered by iframe clients
145
+ logger.debug("Unverified IframeClient");
146
+ this._client.off("verified", this._clientVerified, this);
147
+ this._client.off("unverified", this._clientUnverified, this);
148
+
149
+ this._client = new WebsocketClient({
150
+ env: this._env
151
+ });
152
+ this._clientType = "iframe-unverified";
153
+
154
+ this._client.on("unverified", this._clientUnverified, this);
155
+ this._client.on("verified", this._clientVerified, this);
156
+ this._client.verify();
157
+ }
158
+ }
159
+
160
+ window.__Twitch__pubsubInstances = window.__Twitch__pubsubInstances || {
161
+ production: null,
162
+ staging: null,
163
+ darklaunch: null
164
+ };
165
+
166
+ function getInstance(env) {
167
+ if (env !== "production" && env !== "staging" && env !== "darklaunch") {
168
+ throw "Invalid Pubsub instance environment";
169
+ }
170
+ if (window.__Twitch__pubsubInstances[env] === null) {
171
+ // create driver
172
+ window.__Twitch__pubsubInstances[env] = new PubsubDriver(env);
173
+ }
174
+ return window.__Twitch__pubsubInstances[env];
175
+ }
176
+
177
+ export default {getInstance};