support multiple connections to the worker

Also removes the IRCClient message-passing-style API since the service
worker will mutualize connections and thus interpret stuff like
"connect" differently.

This is getting messy, there is a real need to refactor and extend the
"IRC client state" so that

1. There is less state in the shoutbox page, instead more should come
   from the IRC client state (which updates pretty frequently)
2. The protocol is cleaner

Because currently if you disconnect from one page the others don't
necessarily show the proper tabs, etc.
This commit is contained in:
Lephenixnoir 2023-09-05 22:14:45 +02:00
parent e127e92bd3
commit ece78efd9c
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
5 changed files with 98 additions and 49 deletions

View File

@ -19,6 +19,8 @@ body {
--shoutbox-message-author-color: #777777;
--shoutbox-message-bg1: #ffffff;
--shoutbox-message-bg2: #f8f8f8;
--shoutbox-error-bg: #dd5648;
--shoutbox-error-fg: white;
}
/* Shoutbox-only style */
@ -107,6 +109,13 @@ body {
flex-flow: column;
justify-content: space-evenly;
}
#v5shoutbox .channels .login-form .error {
display: none;
background: var(--shoutbox-error-bg);
color: var(--shoutbox-error-fg);
padding: 6px 10px;
text-align: center;
}
#v5shoutbox .channel {
overflow-x: auto;

View File

@ -41,10 +41,7 @@ class IRCClientState {
this.state = IRCState.DISCONNECTED;
this.channels = [];
}
else {
this.state = protocol_state_object.state;
this.channels = protocol_state_object.channels;
}
else Object.assign(this, protocol_state_object);
}
/* Connection status */
@ -52,13 +49,13 @@ class IRCClientState {
return this.state !== IRCState.DISCONNECTED;
}
isRunning() {
return this.state === IRCState.READY;
return this.state === IRCState.RUNNING;
}
stateString() {
switch(this.state) {
case IRCState.DISCONNECTED: return "Disconnected";
case IRCState.CONNECTING: return "Connecting...";
case IRCState.READY: return "Connected";
case IRCState.RUNNING: return "Connected";
}
}
}
@ -97,16 +94,26 @@ class SharedChat {
this.onMessage(e.data);
});
return navigator.serviceWorker.register(
/* Register the service worker */
const reg = await navigator.serviceWorker.register(
"v5shoutbox_worker.js", { scope: "./" })
.then((registration) => {
console.log(registration);
return true;
})
.catch((error) => {
console.error(`Service worker registration failed with ${error}`);
return false;
return null;
});
if(reg === null)
return false;
/* Once registered, wait for it to be ready, and then check whether it's
controlling us; if not, fall back to the local mode */
console.log("Service worker registration succeeded:", reg);
await navigator.serviceWorker.ready;
if(navigator.serviceWorker.controller === null) {
console.warn("No controller (maybe Shift-refresh was used?)");
return false;
}
return true;
};
/* Handler called when the page is taken over by a service worker. Usually
@ -140,30 +147,29 @@ class SharedChat {
}
}
/** API for sending messages to the IRC client **/
/** API for using the IRC client **/
/* Send a message to the IRC client */
remoteCall(e) {
connect(login, password) {
if(this.irc == null)
navigator.serviceWorker.controller.postMessage({
type: "remote_call",
rpc: e,
type: "connect",
login: login,
password: password,
});
else
this.irc.remoteCall(e);
this.irc.connect(login, password);
}
connect(login, password) { this.remoteCall({
type: "connect",
login: login,
password: password,
})}
post(channel, message) { this.remoteCall({
type: "post",
channel: channel,
message: message,
})}
post(channel, message) {
if(this.irc == null)
navigator.serviceWorker.controller.postMessage({
type: "post",
channel: channel,
message: message,
});
else
this.irc.postMessage(channel, message);
}
/** Handlers for getting messages from the IRC client **/
@ -203,6 +209,7 @@ let shoutbox = new function() {
this.eLoginForm = root.querySelector(".login-form");
this.eLogin = root.querySelector(".login");
this.ePassword = root.querySelector(".password");
this.eAuthenticationError = root.querySelector(".error");
/* Elements of the shoutbox form */
this.eShoutboxForm = root.querySelector(".shoutbox-form");
this.eMessage = root.querySelector(".message-input");
@ -258,8 +265,22 @@ let shoutbox = new function() {
console.log(e);
this.refreshView();
}
else if(e.type == "welcome") {
for(const [channel, info] of this.clientState.channels) {
this.createChannel(channel);
/* If we are connected, select a channel */
if(!this.isOnRemoteChannel())
this.selectChannel(channel);
}
this.refreshView();
}
else if(e.type == "authenticated") {
this.ePassword.value = "";
this.eAuthenticationError.style.display = "none";
}
else if(e.type == "authentication_failed") {
this.ePassword.value = "";
this.eAuthenticationError.style.display = "block";
}
else if(e.type == "new_message") {
this.onNewMessageCallback(e.channel, e.date, e.author, e.message);

View File

@ -88,8 +88,8 @@ const IRCState = Object.freeze({
DISCONNECTED: 0,
/* We just opened a socket to the IRC server and have yet to register. */
CONNECTING: 1,
/* We are authenticated and ready to use the server. */
READY: 2,
/* We are authenticated and using the server. */
RUNNING: 2,
});
/* https://stackoverflow.com/questions/30106476 */
@ -143,6 +143,9 @@ class IRCClient {
onAuthenticated() {
this.sendMessage({ type: "authenticated" });
}
onAuthenticationFailed() {
this.sendMessage({ type: "authentication_failed" });
}
onNewMessage(channel, date, author, message) {
this.sendMessage({
type: "new_message",
@ -156,15 +159,6 @@ class IRCClient {
this.sendMessage({ type: "channel_changed", channel: channel });
}
/*** Message-based actions ***/
remoteCall(e) {
if(e.type == "connect")
this.connect(e.login, e.password);
else if(e.type == "post")
this.postMessage(e.channel, e.message);
}
/*** Connection management ***/
setState(state) {
@ -284,11 +278,25 @@ class IRCClient {
this.#ws.send("AUTH " + this.username + ":" + this.#password);
}
if(msg.command == 433 || msg.command == 451) {
/* These are messy situations to be in, if this happens just disconnect
so we can at least start from a blank state */
this.log("[v5shoutbox] Nickname already in use / Not registered!");
this.#ws.close();
this.setState(IRCState.DISCONNECTED);
}
if(msg.command == 904) {
this.log("[v5shoutbox] Authentication failed!");
this.#ws.close();
this.setState(IRCState.DISCONNECTED);
this.onAuthenticationFailed();
}
if(msg.command === 900) {
this.log("[v5shoutbox] Authenticated.");
if(this.caps.includes("sasl"))
this.#sendRaw("CAP END");
this.setState(IRCState.READY);
this.setState(IRCState.RUNNING);
this.#password = undefined;
this.onAuthenticated();

View File

@ -30,7 +30,7 @@ self.addEventListener("activate", (e) => {
log("activating!");
irc = new IRCClient("wss://irc.planet-casio.com:443", undefined);
irc.onMessage = (msg) => {
console.log("message from irc server", msg);
log(msg.type == "log" ? msg.text : msg);
clients.forEach(c => c.postMessage(msg));
};
@ -39,17 +39,27 @@ self.addEventListener("activate", (e) => {
});
self.addEventListener("message", (e) => {
log(e);
if(e.data.type == "remote_call")
irc.remoteCall(e.data.rpc);
if(e.data.type == "connect") {
log("<connect call, not shown>");
irc.connect(e.data.login, e.data.password);
}
else if(e.data.type == "post") {
log(e);
irc.postMessage(e.data.channel, e.data.message);
}
else if(e.data.type == "handshake") {
clients.push(e.source);
console.log(`New client: ${e.source.id} (total ${clients.length})`);
console.log("All clients:", clients);
log(`New client: ${e.source.id} (total ${clients.length})`);
log("All clients:", clients);
e.source.postMessage({
type: "welcome",
// TODO: This is copied from IRC.sendMessage
state: { state: irc.state, channels: irc.channels }
});
}
else if(e.data.type == "leave") {
clients = clients.filter(c => c.id != e.source.id);
console.log(`Client left: ${e.source.id} (remains ${clients.length})`);
console.log("All clients:", clients);
log(`Client left: ${e.source.id} (remains ${clients.length})`);
log("All clients:", clients);
}
});

View File

@ -9,6 +9,7 @@
<div class="channels">
<div data-channel="\login">
<form class="login-form form">
<div class="error">Authentication failed, please try again.</div>
<input type="text" class="login" placeholder="Utilisateur" />
<input type="password" class="password" placeholder="Mot de passe" />
<input type="submit" class="connect bg-ok" value="Connect!" />