Skip to content

Commit a17bd1f

Browse files
committed
BREAKING: feat: rename options; manual connect/disconnect; better errors
wip: mock
1 parent 255e004 commit a17bd1f

File tree

8 files changed

+206
-126
lines changed

8 files changed

+206
-126
lines changed

.env.example

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
CHAINLINK_CLIENT_ID=
22
CHAINLINK_CLIENT_SECRET=
33
CHAINLINK_API_URL=
4-
CHAINLINK_WEBSOCKET_URL=
5-
CHAINLINK_WSS_RECONNECT_ENABLED=
6-
CHAINLINK_WSS_RECONNECT_MAX_ATTEMPTS=
7-
CHAINLINK_WSS_RECONNECT_INTERVAL=
4+
CHAINLINK_WS_URL=
5+
CHAINLINK_WS_RECONNECT_ENABLED=
6+
CHAINLINK_WS_RECONNECT_MAX_ATTEMPTS=
7+
CHAINLINK_WS_RECONNECT_INTERVAL=
8+
CHAINLINK_WS_MOCK_SERVER=

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# 2.0.0
2+
3+
* Automatic and manual reconnection:
4+
* Configuration changes:
5+
* Full URLs now required instead of bare hostnames:
6+
* `hostname` -> `apiUrl`, now requires protocol (i.e. `https://`)
7+
* `wsHostname` -> `wsUrl`, now requires protocol (i.e. `wss://`)
8+
* `http://` and `ws://` supported (for testing only!)
9+
* `clientID` -> `clientId` to match capitalization of other parameters
10+
* Configuration variables in `.env` updated accordingly;, see `.env.example`.
11+
12+
# 1.1.0
13+
14+
* Allow only `GET|HEAD|OPTIONS` in `generateHeaders`
15+
16+
# 1.0.1
17+
18+
* Harden `generateHeaders`
19+
20+
# 1.0.0
21+
22+
* Initial release

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ Use your authentication credentials to instantiate the default export of the mod
2828
import ChainlinkDatastreamsConsumer from '@hackbg/chainlink-datastreams-consumer'
2929

3030
const api = new ChainlinkDatastreamsConsumer({
31-
hostname: '...',
32-
wsHostname: '...',
33-
clientID: '...',
31+
apiUrl: '...',
32+
wsUrl: '...',
33+
clientId: '...',
3434
clientSecret: '...',
3535
})
3636
```
@@ -72,11 +72,15 @@ Use the instance's `on`, `once` and `off` methods to set up event handling.
7272
import ChainlinkDatastreamsConsumer from '@hackbg/chainlink-datastreams-consumer'
7373

7474
const api = new ChainlinkDatastreamsConsumer({
75-
hostname: '...',
76-
wsHostname: '...',
77-
clientID: '...',
75+
apiUrl: '...',
76+
wsUrl: '...',
77+
clientId: '...',
7878
clientSecret: '...',
79-
feeds: [ '0x...', '0x...', '0x...', ]
79+
feeds: [
80+
'0x...',
81+
'0x...',
82+
'0x...',
83+
]
8084
}).on('report', report => {
8185
console.log(report)
8286
})

index.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ export type ReconnectOptions = {
77
};
88
export default class ChainlinkDataStreamsConsumer {
99
constructor(args: {
10-
clientID?: string;
10+
clientId?: string;
1111
clientSecret?: string;
12-
hostname?: string;
13-
wsHostname?: string;
12+
apiUrl?: string;
13+
wsUrl?: string;
1414
feeds?: string[];
1515
reconnect?: boolean|ReconnectOptions;
1616
});
@@ -29,6 +29,8 @@ export default class ChainlinkDataStreamsConsumer {
2929

3030
unsubscribeFrom(feeds: string | string[]): Promise<WebSocket | null>;
3131

32+
connect(): Promise<void>;
33+
3234
disconnect(): void;
3335

3436
feeds: Set<string>;

index.js

Lines changed: 110 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,30 @@ class EventEmitter {
3838

3939
export default class ChainlinkDataStreamsConsumer extends EventEmitter {
4040
constructor(options = {}) {
41-
const { hostname, wsHostname, clientID, clientSecret, feeds } = options;
41+
if ('clientID' in options) {
42+
throw new Error(
43+
'Deprecated: options.clientID is now options.clientId '+
44+
'to match capitalization of other parameters.'
45+
)
46+
}
47+
if ('hostname' in options) {
48+
throw new Error(
49+
'Deprecated: options.hostname is now options.apiUrl and requires protocol.'
50+
)
51+
}
52+
if ('wsHostname' in options) {
53+
throw new Error(
54+
'Deprecated: options.wsHostname is now options.wsUrl and requires protocol.'
55+
)
56+
}
57+
const { apiUrl, wsUrl, clientId, clientSecret, feeds } = options;
4258
let { reconnect = {} } = options;
4359
if (typeof reconnect === 'boolean') reconnect = { enabled: reconnect };
4460
reconnect.enabled ??= true;
4561
reconnect.maxAttempts ??= 1000;
4662
reconnect.interval ??= 100;
4763
super();
48-
Object.assign(this, { hostname, wsHostname, clientID, reconnect });
64+
Object.assign(this, { apiUrl, wsUrl, clientId, reconnect });
4965
this.setClientSecret(clientSecret);
5066
this.manuallyDisconnected = false;
5167
this.setConnectedFeeds(feeds);
@@ -73,12 +89,12 @@ export default class ChainlinkDataStreamsConsumer extends EventEmitter {
7389
}).then(Report.fromBulkAPIResponse);
7490

7591
async fetch(path, params = {}) {
76-
if (!this.hostname) {
92+
if (!this.apiUrl) {
7793
throw new Error(
7894
'Hostname was not passed to ChainlinkDataStreamsConsumer constructor.',
7995
);
8096
}
81-
const url = new URL(path, `https://${this.hostname}`);
97+
const url = new URL(path, this.apiUrl);
8298
url.search = new URLSearchParams(params).toString();
8399
const headers = this.generateHeaders('GET', path, url.search);
84100
const response = await fetch(url, { headers });
@@ -87,7 +103,7 @@ export default class ChainlinkDataStreamsConsumer extends EventEmitter {
87103
}
88104

89105
generateHeaders(method, path, search, timestamp = +new Date()) {
90-
if (!this.clientID) {
106+
if (!this.clientId) {
91107
throw new Error(
92108
'Client ID was not passed to ChainlinkDataStreamsConsumer constructor',
93109
);
@@ -141,7 +157,7 @@ export default class ChainlinkDataStreamsConsumer extends EventEmitter {
141157
method,
142158
`${path}${search}`,
143159
base16.encode(sha256.create().update('').digest()).toLowerCase(),
144-
this.clientID,
160+
this.clientId,
145161
String(timestamp),
146162
];
147163

@@ -161,36 +177,41 @@ export default class ChainlinkDataStreamsConsumer extends EventEmitter {
161177
.toLowerCase();
162178

163179
return {
164-
Authorization: this.clientID,
180+
Authorization: this.clientId,
165181
'X-Authorization-Timestamp': timestamp.toString(),
166182
'X-Authorization-Signature-SHA256': signature,
167183
};
168184
}
169185

170186
connect = () => {
171187
this.manuallyDisconnected = false;
172-
this.connectImpl()
188+
return this.connectImpl();
173189
}
174190

175191
disconnect = () => {
176192
this.manuallyDisconnected = true;
177-
this.disconnectImpl()
193+
return this.disconnectImpl();
178194
}
179195

180-
disconnectImpl = () => {
196+
disconnectImpl = () => new Promise((resolve, reject)=>{
181197
const { ws } = this;
182198
if (ws) {
183199
ws.off('message', this.decodeAndEmit);
184200
if (ws.readyState === WebSocket.CONNECTING) {
185-
ws.on('open', () => ws.close());
201+
ws.on('open', () => {
202+
ws.close();
203+
resolve();
204+
});
186205
} else if (ws.readyState === WebSocket.OPEN) {
187206
ws.close();
207+
resolve();
188208
}
189209
this.ws = null;
190210
} else {
191211
console.warn('Already disconnected.');
212+
resolve();
192213
}
193-
};
214+
});
194215

195216
decodeAndEmit = (message) => {
196217
if (this.listeners['report']) {
@@ -229,9 +250,10 @@ export default class ChainlinkDataStreamsConsumer extends EventEmitter {
229250
};
230251

231252
setConnectedFeeds = (feeds) => {
232-
if (!this.wsHostname) {
253+
console.debug('Connecting to feeds:', feeds||'[]');
254+
if (!this.wsUrl) {
233255
throw new Error(
234-
'WebSocket hostname was not passed to ChainlinkDataStreamsConsumer constructor.',
256+
'WebSocket URL was not passed to ChainlinkDataStreamsConsumer constructor.',
235257
);
236258
}
237259
feeds = feeds || [];
@@ -256,81 +278,83 @@ export default class ChainlinkDataStreamsConsumer extends EventEmitter {
256278
return Promise.resolve()
257279
}
258280

259-
connectImpl = () => {
260-
return new Promise((resolve, reject)=>{
261-
const feeds = this.feeds;
262-
if (feeds.size < 1) {
263-
console.debug('No feeds enabled, disconnecting. Use setConnectedFeeds to connect.')
264-
if (this.ws) this.disconnectImpl();
265-
return resolve();
281+
connectImpl = () => new Promise((resolve, reject)=>{
282+
const feeds = this.feeds;
283+
if (feeds.size < 1) {
284+
if (this.ws) {
285+
console.debug('No feeds enabled, disconnecting. Set feeds to connect.')
286+
this.disconnectImpl();
287+
} else {
288+
console.debug('No feeds enabled, not connecting. Set feeds to connect.')
266289
}
267-
if (feeds.size > 0) {
268-
const path = '/api/v1/ws';
269-
const search = new URLSearchParams({
270-
feedIDs: [...feeds].join(','),
271-
}).toString();
272-
const url = Object.assign(new URL(path, `wss://${this.wsHostname}`), {
273-
search,
274-
});
275-
const headers = this.generateHeaders('GET', path, search);
276-
if (this.ws) this.disconnectImpl();
277-
const ws = (this.ws = new WebSocket(url.toString(), { headers }));
278-
const onerror = (error) => {
279-
unbind();
290+
return resolve();
291+
}
292+
if (feeds.size > 0) {
293+
const path = '/api/v1/ws';
294+
const search = new URLSearchParams({
295+
feedIDs: [...feeds].join(','),
296+
}).toString();
297+
const url = Object.assign(new URL(path, this.wsUrl), {
298+
search,
299+
});
300+
const headers = this.generateHeaders('GET', path, search);
301+
if (this.ws) this.disconnectImpl();
302+
const ws = (this.ws = new WebSocket(url.toString(), { headers }));
303+
const onerror = (error) => {
304+
unbind();
305+
resolve();
306+
};
307+
const onopen = () => {
308+
unbind();
309+
// reset reconnect attempts on successful connection
310+
this.reconnect.attempts = 0;
311+
resolve(this.ws);
312+
};
313+
const unbind = () => {
314+
ws.off('error', onerror);
315+
ws.off('open', onopen);
316+
};
317+
const onclose = () => {
318+
unbind();
319+
if (!this.reconnect?.enabled) {
320+
console.debug(
321+
'Socket closed. Reconnect not enabled, will not reconnect. ' +
322+
'Use connect() to reconnect.'
323+
);
280324
resolve();
281-
};
282-
const onopen = () => {
283-
unbind();
284-
// reset reconnect attempts on successful connection
285-
this.reconnect.attempts = 0;
286-
resolve(this.ws);
287-
};
288-
const unbind = () => {
289-
ws.off('error', onerror);
290-
ws.off('open', onopen);
291-
};
292-
const onclose = () => {
293-
unbind();
294-
if (!this.reconnect?.enabled) {
295-
console.debug(
296-
'Socket closed. Reconnect not enabled, will not reconnect. ' +
297-
'Use connect() to reconnect.'
298-
);
299-
resolve();
300-
return;
301-
}
302-
if (this.manuallyDisconnected) {
303-
console.debug(
304-
'Socket closed. Manually disconnected, will not reconnect. ' +
305-
'Use connect() to reconnect.'
306-
);
307-
resolve();
308-
return;
309-
}
310-
if (this.reconnect.attempts < this.reconnect.maxAttempts) {
311-
this.reconnect.attempts++;
312-
console.log(
313-
`Reconnecting attempt #${this.reconnect.attempts}/${this.reconnect.maxAttempts}`,
314-
`in ${this.reconnect.interval}ms...`,
315-
);
316-
setTimeout(() => {
317-
this.connectImpl();
318-
}, this.reconnect.interval);
319-
} else {
320-
const error =
321-
`Max reconnect attempts (${this.reconnect.maxAttempts}) reached. Giving up.`
322-
console.error(error);
323-
return reject(new Error(error))
324-
}
325+
return;
326+
}
327+
if (this.manuallyDisconnected) {
328+
console.debug(
329+
'Socket closed. Manually disconnected, will not reconnect. ' +
330+
'Use connect() to reconnect.'
331+
);
325332
resolve();
326-
};
327-
ws.on('error', onerror);
328-
ws.on('open', onopen);
329-
ws.on('close', onclose);
330-
ws.on('message', this.decodeAndEmit);
331-
}
332-
})
333-
}
333+
return;
334+
}
335+
if (this.reconnect.attempts < this.reconnect.maxAttempts) {
336+
this.reconnect.attempts++;
337+
console.log(
338+
`Reconnecting attempt #${this.reconnect.attempts}/${this.reconnect.maxAttempts}`,
339+
`in ${this.reconnect.interval}ms...`,
340+
);
341+
setTimeout(() => {
342+
this.connectImpl();
343+
}, this.reconnect.interval);
344+
} else {
345+
const error =
346+
`Max reconnect attempts (${this.reconnect.maxAttempts}) reached. Giving up.`
347+
console.error(error);
348+
return reject(new Error(error))
349+
}
350+
resolve();
351+
};
352+
ws.on('error', onerror);
353+
ws.on('open', onopen);
354+
ws.on('close', onclose);
355+
ws.on('message', this.decodeAndEmit);
356+
}
357+
})
334358
}
335359

336360
const compareSets = (xs, ys) =>

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"main": "index.js",
66
"types": "index.d.ts",
7-
"version": "1.1.0",
7+
"version": "2.0.0",
88
"license": "BSD-3-Clause-No-Military-License",
99
"dependencies": {
1010
"@noble/hashes": "^1.3.2",

0 commit comments

Comments
 (0)