track state of connection to worker in main script

This commit is contained in:
Lephenixnoir 2023-10-10 23:23:00 +02:00
parent d189054616
commit bd1b9a369b
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
1 changed files with 87 additions and 49 deletions

View File

@ -65,22 +65,48 @@ class IRCClientState {
callbacks around the message-passing interface of the IRC client. */
class SharedChat {
/* Chat state. This outlines the protocol for communicating with the service
worker when loading pages, refreshing the worker, etc. */
static State = Object.freeze({
/* We are not using a service worker. */
LOCAL: 0,
/* We are waiting for a handshake response after loading the page. */
HANDSHAKE_PAGE: 1,
/* We are waiting for a handshake response after greeting a dynamically-
updated controller */
HANDSHAKE_UPDATE: 2,
/* We are connected to the worker. (The worker itself might not be
connected to the IRC server, though!) */
RUNNING: 3,
});
stateString() {
switch(this.state) {
case SharedChat.State.LOCAL: return "Local IRC client";
case SharedChat.State.HANDSHAKE_PAGE: return "Connecting to worker...";
case SharedChat.State.HANDSHAKE_UPDATE: return "Updating worker...";
case SharedChat.State.RUNNING: return "Running";
default: return `<State ${this.state.toString()}>`
}
}
/* Registers the service worker that runs the IRC client. Waits for
registration to complete and returns true on success, false on error.
If successful, sets up the onControllerChanged callback. */
If successful, sets up the handleControllerChanged callback. */
async registerServiceWorker() {
if(!("serviceWorker" in navigator)) {
console.warn("No service workers, shoutbox will use per-tab IRC client");
this.state = SharedChat.State.LOCAL;
return false;
}
/* Call the update method once if the worker was already there */
if(navigator.serviceWorker.controller !== null)
this.onControllerChanged();
this.handleControllerChanged();
/* Then call it after first install and every update */
navigator.serviceWorker.addEventListener("controllerchange", () => {
this.onControllerChanged();
this.handleControllerChanged();
});
/* Explicitly disconnect before unloading (the worker will also disconnect
@ -91,7 +117,7 @@ class SharedChat {
});
navigator.serviceWorker.addEventListener("message", (e) => {
this.onMessage(e.data);
this.handleMessage(e.data);
});
/* Register the service worker */
@ -101,8 +127,10 @@ class SharedChat {
console.error(`Service worker registration failed with ${error}`);
return null;
});
if(reg === null)
if(reg === null) {
this.state = SharedChat.State.LOCAL;
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 */
@ -111,40 +139,46 @@ class SharedChat {
if(navigator.serviceWorker.controller === null) {
console.warn("No controller (maybe Shift-refresh was used?)");
this.state = SharedChat.State.LOCAL;
return false;
}
this.state = SharedChat.State.HANDSHAKE_PAGE;
return true;
};
/* Handler called when the page is taken over by a service worker. Usually
the worker manages the page immediately when it starts loading, but this
also happens after the first install and during live worker updates. */
onControllerChanged() {
handleControllerChanged() {
console.log("Service worker changed, sending new handshake");
navigator.serviceWorker.controller.postMessage({ type: "handshake" });
this.state = SharedChat.State.HANDSHAKE_UPDATE;
this.onStateChanged();
}
// TODO: Pick up already connected worker
/* getClientState() {
if(this.irc == null) {
this.remoteCall({ type: "get_state" });
return null;
/* Handler called when a message is received from the worker. */
handleMessage(e) {
if(e.type == "welcome") {
this.state = SharedChat.State.RUNNING;
this.onStateChanged();
}
else
return new IRCClientState(this.irc);
} */
this.onMessage(e);
}
async init() {
const ok = await this.registerServiceWorker(() => {
});
const ok = await this.registerServiceWorker();
console.log("SharedChat service worker registration:", ok);
if(ok)
this.irc = null;
else {
this.irc = new IRCClient("wss://irc.planet-casio.com:443", undefined);
this.irc.onMessage = (msg) => this.onMessage(msg);
this.irc.onMessage = (msg) => this.handleMessage(msg);
}
console.log("Initialization finished; state:", this.stateString());
this.onStateChanged();
}
/** API for using the IRC client **/
@ -173,8 +207,10 @@ class SharedChat {
/** Handlers for getting messages from the IRC client **/
/* Handler called when a message is received from the worker. */
/* Notifier called when a message is received from the worker. */
onMessage(e) {}
/* Notifier called when the shared chat's connection state changes. */
onStateChanged() {}
}
/* Shoutbox entry point and DOM manipulation. */
@ -196,7 +232,7 @@ let shoutbox = new function() {
this.init = function(root, chat) {
this.title = document.title;
this.chat = chat;
this.chat.onMessage = (e) => this.onMessage(e);
this.chat.onMessage = (e) => this.handleMessage(e);
/* Channel views */
this.eChannels = root.querySelector(".channels");
@ -243,35 +279,32 @@ let shoutbox = new function() {
shoutbox.focused = false;
});
/* Initial client state */
/* TODO: Pick up an already connected service worker */
/* Initial client state. This will get replaced as soon as we get the
handshake response from the worker */
// TODO: Actually put a loading screen instead
this.clientState = new IRCClientState(null);
console.log(this.clientState);
this.selectChannel("\\login");
this.refreshView();
}
/*** IRC callbacks ***/
this.onMessage = function(e) {
this.handleMessage = function(e) {
/* Update known client state based on info bundled with the message */
if(e.state !== undefined)
if(e.state !== undefined) {
this.clientState = new IRCClientState(e.state);
console.log("Client state changed, now", this.clientState);
}
if(e.type == "log") {
this.log(e.text);
}
else if(e.type == "state_changed") {
console.log(e);
this.refreshView();
}
else if(e.type == "welcome") {
for(const [channel, info] of this.clientState.channels) {
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") {
@ -283,7 +316,7 @@ let shoutbox = new function() {
this.eAuthenticationError.style.display = "block";
}
else if(e.type == "new_message") {
this.onNewMessageCallback(e.channel, e.date, e.author, e.message);
this.handleNewMessage(e.channel, e.date, e.author, e.message);
}
else if(e.type == "channel_changed") {
for(const [channel, info] of this.clientState.channels) {
@ -325,7 +358,10 @@ let shoutbox = new function() {
this.eStatus.insertBefore(code, this.eStatus.childNodes[0]);
}
else {
this.eStatus.textContent = st.stateString();
let str = this.chat.stateString();
if(this.chat.state == SharedChat.State.RUNNING)
str += "|" + st.stateString();
this.eStatus.textContent = str;
}
this.eShoutboxForm.style.display = running ? "flex" : "none";
@ -344,7 +380,7 @@ let shoutbox = new function() {
}
};
this.onNewMessageCallback = function(channel, date, author, message) {
this.handleNewMessage = function(channel, date, author, message) {
shoutbox.addNewMessage(channel, date, author, message);
if(channel === this.channel && !shoutbox.focused) {
@ -384,26 +420,28 @@ let shoutbox = new function() {
this.createChannel = function(channel) {
if(!availableChannels.includes(channel))
return;
if(this.getChannelView(channel) !== undefined)
return;
let view = document.createElement("div");
view.classList.add("channel");
view.dataset.channel = channel;
view.style.display = "none";
this.eChannels.appendChild(view);
if(this.getChannelView(channel) === undefined) {
let view = document.createElement("div");
view.classList.add("channel");
view.dataset.channel = channel;
view.style.display = "none";
this.eChannels.appendChild(view);
let button = document.createElement("a");
button.classList.add("tab");
button.appendChild(document.createTextNode(channel));
button.dataset.channel = channel;
this.addChannelButton(button);
let button = document.createElement("a");
button.classList.add("tab");
button.appendChild(document.createTextNode(channel));
button.dataset.channel = channel;
this.addChannelButton(button);
button.addEventListener("click", () => {
shoutbox.selectChannel(button.dataset.channel);
});
button.addEventListener("click", () => {
shoutbox.selectChannel(button.dataset.channel);
});
}
if(this.channel === "\\login")
/* Automatically select channels when they appear.
TODO: Maybe not if we're looking at the log or config? */
if(!this.isOnRemoteChannel())
shoutbox.selectChannel(channel);
}