2023-05-23 21:13:01 +02:00
|
|
|
import {
|
|
|
|
basicAuth,
|
|
|
|
bearerAuth,
|
|
|
|
encodeBase64Url,
|
|
|
|
topicShortUrl,
|
|
|
|
topicUrlWs,
|
|
|
|
} from "./utils";
|
2022-02-24 15:52:49 +01:00
|
|
|
|
2023-03-08 20:51:47 +01:00
|
|
|
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
2022-02-24 15:52:49 +01:00
|
|
|
|
2022-03-11 21:17:12 +01:00
|
|
|
/**
|
|
|
|
* A connection contains a single WebSocket connection for one topic. It handles its connection
|
|
|
|
* status itself, including reconnect attempts and backoff.
|
|
|
|
*
|
|
|
|
* Incoming messages and state changes are forwarded via listeners.
|
|
|
|
*/
|
2022-02-24 02:30:12 +01:00
|
|
|
class Connection {
|
2023-05-23 21:13:01 +02:00
|
|
|
constructor(
|
|
|
|
connectionId,
|
|
|
|
subscriptionId,
|
|
|
|
baseUrl,
|
|
|
|
topic,
|
|
|
|
user,
|
|
|
|
since,
|
|
|
|
onNotification,
|
|
|
|
onStateChanged
|
|
|
|
) {
|
|
|
|
this.connectionId = connectionId;
|
|
|
|
this.subscriptionId = subscriptionId;
|
|
|
|
this.baseUrl = baseUrl;
|
|
|
|
this.topic = topic;
|
|
|
|
this.user = user;
|
|
|
|
this.since = since;
|
|
|
|
this.shortUrl = topicShortUrl(baseUrl, topic);
|
|
|
|
this.onNotification = onNotification;
|
|
|
|
this.onStateChanged = onStateChanged;
|
|
|
|
this.ws = null;
|
|
|
|
this.retryCount = 0;
|
|
|
|
this.retryTimeout = null;
|
|
|
|
}
|
2022-02-26 05:25:04 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
start() {
|
|
|
|
// Don't fetch old messages; we do that as a poll() when adding a subscription;
|
|
|
|
// we don't want to re-trigger the main view re-render potentially hundreds of times.
|
2022-02-26 05:25:04 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
const wsUrl = this.wsUrl();
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`
|
|
|
|
);
|
2022-02-24 02:30:12 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
this.ws = new WebSocket(wsUrl);
|
|
|
|
this.ws.onopen = (event) => {
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`,
|
|
|
|
event
|
|
|
|
);
|
|
|
|
this.retryCount = 0;
|
|
|
|
this.onStateChanged(this.subscriptionId, ConnectionState.Connected);
|
|
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`
|
|
|
|
);
|
|
|
|
try {
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
if (data.event === "open") {
|
|
|
|
return;
|
2022-02-24 02:30:12 +01:00
|
|
|
}
|
2023-05-23 21:13:01 +02:00
|
|
|
const relevantAndValid =
|
|
|
|
data.event === "message" &&
|
|
|
|
"id" in data &&
|
|
|
|
"time" in data &&
|
|
|
|
"message" in data;
|
|
|
|
if (!relevantAndValid) {
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`
|
|
|
|
);
|
|
|
|
return;
|
2022-02-24 15:52:49 +01:00
|
|
|
}
|
2023-05-23 21:13:01 +02:00
|
|
|
this.since = data.id;
|
|
|
|
this.onNotification(this.subscriptionId, data);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.ws.onclose = (event) => {
|
|
|
|
if (event.wasClean) {
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`
|
|
|
|
);
|
2022-02-24 15:52:49 +01:00
|
|
|
this.ws = null;
|
2023-05-23 21:13:01 +02:00
|
|
|
} else {
|
|
|
|
const retrySeconds =
|
|
|
|
retryBackoffSeconds[
|
|
|
|
Math.min(this.retryCount, retryBackoffSeconds.length - 1)
|
|
|
|
];
|
|
|
|
this.retryCount++;
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`
|
|
|
|
);
|
|
|
|
this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000);
|
|
|
|
this.onStateChanged(this.subscriptionId, ConnectionState.Connecting);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.ws.onerror = (event) => {
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`,
|
|
|
|
event
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
console.log(
|
|
|
|
`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`
|
|
|
|
);
|
|
|
|
const socket = this.ws;
|
|
|
|
const retryTimeout = this.retryTimeout;
|
|
|
|
if (socket !== null) {
|
|
|
|
socket.close();
|
2022-02-24 02:30:12 +01:00
|
|
|
}
|
2023-05-23 21:13:01 +02:00
|
|
|
if (retryTimeout !== null) {
|
|
|
|
clearTimeout(retryTimeout);
|
|
|
|
}
|
|
|
|
this.retryTimeout = null;
|
|
|
|
this.ws = null;
|
|
|
|
}
|
2022-02-26 05:25:04 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
wsUrl() {
|
|
|
|
const params = [];
|
|
|
|
if (this.since) {
|
|
|
|
params.push(`since=${this.since}`);
|
2022-02-26 05:25:04 +01:00
|
|
|
}
|
2023-05-23 21:13:01 +02:00
|
|
|
if (this.user) {
|
|
|
|
params.push(`auth=${this.authParam()}`);
|
|
|
|
}
|
|
|
|
const wsUrl = topicUrlWs(this.baseUrl, this.topic);
|
|
|
|
return params.length === 0 ? wsUrl : `${wsUrl}?${params.join("&")}`;
|
|
|
|
}
|
2022-12-27 03:27:07 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
authParam() {
|
|
|
|
if (this.user.password) {
|
|
|
|
return encodeBase64Url(basicAuth(this.user.username, this.user.password));
|
2022-12-27 03:27:07 +01:00
|
|
|
}
|
2023-05-23 21:13:01 +02:00
|
|
|
return encodeBase64Url(bearerAuth(this.user.token));
|
|
|
|
}
|
2022-02-24 02:30:12 +01:00
|
|
|
}
|
|
|
|
|
2022-03-04 17:08:32 +01:00
|
|
|
export class ConnectionState {
|
2023-05-23 21:13:01 +02:00
|
|
|
static Connected = "connected";
|
|
|
|
static Connecting = "connecting";
|
2022-03-04 17:08:32 +01:00
|
|
|
}
|
|
|
|
|
2022-02-24 02:30:12 +01:00
|
|
|
export default Connection;
|