forked from devs/v5shoutbox
service worker setup
This commit is contained in:
parent
ca3730b64e
commit
f8e765018e
122
v5shoutbox.js
122
v5shoutbox.js
|
@ -1,69 +1,95 @@
|
|||
/*
|
||||
# Planète Casio v5 shoutbox
|
||||
|
||||
This script contains all of the client-side logic for the Planète Casio
|
||||
shoutbox. Unlike the legacy v4 shoutbox which was a poll-based HTTP service,
|
||||
this shoutbox is run by an IRC server and this is mostly an IRC client.
|
||||
This script contains all of the interface logic for the Planète Casio shoutbox.
|
||||
It mostly sets up a service worker to use the IRC client and then manages the
|
||||
DOM.
|
||||
|
||||
Support for IRC features is fairly limited as the shoutbox has never been a
|
||||
very fancy chat. For full functionality, use a standard IRC client.
|
||||
The shoutbox as a whole is divided into three modules:
|
||||
- v5shoutbox.js (this file), handling the interface and coordinating pieces.
|
||||
- v5shoutbox_irc.js, which implements the IRC client.
|
||||
- v5shoutbox_worker.js, which is the service worker running the IRC client.
|
||||
|
||||
The one unusual feature in this program is the interaction with the Planète
|
||||
Casio website. This solves an issue of permissions that is difficult to manage
|
||||
with pure IRC. The expectation for the shoutbox is that:
|
||||
- Anyone can read but only community members can write;
|
||||
- It should load immediately when embedded into website pages and allow logged
|
||||
in users to read and write messages with no further interactions.
|
||||
## The service worker
|
||||
|
||||
In the v4's cursed design, the shoutbox was part of the website API, so users
|
||||
logged into their Planète Casio account would be recognized by the shoutbox
|
||||
server (which is just the website) and authenticated by their session cookie.
|
||||
This doesn't work with the IRC server, since the connections are very different
|
||||
and cookies are not a reliable method of authentication. With a standard IRC
|
||||
client, users would need to type in their password to connect to the chat
|
||||
despite already being logged in to the website.
|
||||
One key aspect of the shoutbox is that it tries to run the IRC client in a
|
||||
_service worker_, which is a web worker than runs in the browser's background
|
||||
and can interact with multiple pages on the same site [1]. The purpose of the
|
||||
worker is to share a single connection to the Planète Casio IRC server between
|
||||
all open tabs, avoiding the redundancy of updating each shoutbox separately via
|
||||
different connections.
|
||||
|
||||
The chosen solution is as follows:
|
||||
- Every embedded shoutbox connects to the IRC server as a read-only guest;
|
||||
- Sending a message is done via a website request. The website identifies
|
||||
logged in users by their session cookies and forwards the messages to the
|
||||
IRC server locally;
|
||||
- Messages from the website to the IRC server are attributed to their original
|
||||
authors by whichever nick/plugin trick works best.
|
||||
Because service workers remain active in the background and can be used by
|
||||
multiple pages at once, loading and updating them takes a bit of effort. This
|
||||
results in the complex but well-designed _service worker life cycle_ which is
|
||||
explained very nicely in [2]. Here, we skip the waiting phase by having all
|
||||
versions of the worker claim active shoutboxes because interactions between the
|
||||
shoutbox and the worker are limited in scope and it's reasonably easy to handle
|
||||
the switch.
|
||||
|
||||
Hence, this client supports different connection modes depending on the
|
||||
authentication setup: normal IRC (with password), PCv4, PCv5.
|
||||
(WIP at time of writing)
|
||||
[1] https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
|
||||
[2] https://web.dev/service-worker-lifecycle/
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import * as IRC from "./v5shoutbox_irc.js";
|
||||
|
||||
const registerServiceWorker = async () => {
|
||||
if(!("serviceWorker" in navigator)) {
|
||||
// TODO: Use previous, local IRC client
|
||||
console.error("Service workers not supported, shoutbox not available.");
|
||||
return;
|
||||
/* IRC-backed chat running in a shared context (service worker) if possible,
|
||||
with a default to a local client. */
|
||||
class SharedChat {
|
||||
|
||||
/* 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. */
|
||||
async registerServiceWorker() {
|
||||
if(!("serviceWorker" in navigator)) {
|
||||
console.warn("No service workers, shoutbox will use per-tab IRC client");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Call the update method once if the worker was already there */
|
||||
if(navigator.serviceWorker.controller !== null)
|
||||
this.onControllerChanged();
|
||||
|
||||
/* Then call it after first install and every update */
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
this.onControllerChanged();
|
||||
});
|
||||
|
||||
return 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;
|
||||
});
|
||||
};
|
||||
|
||||
/* 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() {
|
||||
console.log("onControllerChanged!");
|
||||
console.log(navigator.serviceWorker);
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register(
|
||||
'v5shoutbox_worker.js', { scope: "./" });
|
||||
async init() {
|
||||
const ok = await this.registerServiceWorker(() => {
|
||||
});
|
||||
console.log("SharedChat service worker registration:", ok);
|
||||
|
||||
if (registration.installing)
|
||||
console.log('Service worker installing');
|
||||
else if (registration.waiting)
|
||||
console.log('Service worker installed');
|
||||
else if (registration.active)
|
||||
console.log('Service worker active');
|
||||
|
||||
console.log(registration);
|
||||
} catch (error) {
|
||||
console.error(`Registration failed with ${error}`);
|
||||
// if(ok)
|
||||
// this.irc = null;
|
||||
// else
|
||||
// this.irc = new IRC.Client("wss://irc.planet-casio.com:443", undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
registerServiceWorker();
|
||||
let sc = new SharedChat();
|
||||
sc.init();
|
||||
|
||||
let irc = new IRC.Client("wss://irc.planet-casio.com:443", undefined);
|
||||
|
||||
|
|
|
@ -1,6 +1,53 @@
|
|||
/* WebSocket IRC with HTTP-based website interface. */
|
||||
/*
|
||||
# WebSocket IRC with HTTP-based website interface
|
||||
|
||||
class Message {
|
||||
This script is the v5 shoutbox IRC client. Unlike the legacy v4 shoutbox which
|
||||
was a poll-based HTTP service, this shoutbox is run by an IRC server, hence the
|
||||
shoutbox is powered by this IRC client.
|
||||
|
||||
Support for IRC features is fairly limited as the shoutbox has never been a
|
||||
very fancy chat. For full functionality, use a standard IRC client.
|
||||
|
||||
|
||||
## Posting via HTTP to use the Planète Casio session cookie
|
||||
|
||||
The one unusual feature in this program is the interaction with the Planète
|
||||
Casio website. This solves an issue of permissions that is difficult to manage
|
||||
with pure IRC. The expectation for the shoutbox is that:
|
||||
- Anyone can read but only community members can write;
|
||||
- It should load immediately when embedded into website pages and allow logged
|
||||
in users to read and write messages with no further interactions.
|
||||
|
||||
In the v4's cursed design, the shoutbox was part of the website API, so users
|
||||
logged into their Planète Casio account would be recognized by the shoutbox
|
||||
server (which is just the website) and authenticated by their session cookie.
|
||||
This doesn't work with the IRC server, since the connections are very different
|
||||
and cookies are not a reliable method of authentication. With a standard IRC
|
||||
client, users would need to type in their password to connect to the chat
|
||||
despite already being logged in to the website.
|
||||
|
||||
The chosen solution is as follows:
|
||||
- Every embedded shoutbox connects to the IRC server as a read-only guest;
|
||||
- Sending a message is done via a website request. The website identifies
|
||||
logged in users by their session cookies and forwards the messages to the
|
||||
IRC server locally;
|
||||
- Messages from the website to the IRC server are attributed to their original
|
||||
authors by whichever nick/plugin trick works best.
|
||||
|
||||
(WIP at time of writing)
|
||||
|
||||
|
||||
## Message protocol
|
||||
|
||||
This IRC client usually runs inside of a service worker to that it can be
|
||||
shared between any number of open Planète Casio tabs. Thus, communication
|
||||
between this client and the user interface uses a message-based interface,
|
||||
which is intended to work like RPC.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
class IRCMessage {
|
||||
/* Regex for parsing messages */
|
||||
static #rMessage = (function(){
|
||||
const rTags = /(?:@([^ ]+) +)?/;
|
||||
|
@ -13,7 +60,7 @@ class Message {
|
|||
})();
|
||||
|
||||
constructor(text) {
|
||||
const matches = Message.#rMessage.exec(text);
|
||||
const matches = IRCMessage.#rMessage.exec(text);
|
||||
if(matches === undefined)
|
||||
return;
|
||||
|
||||
|
@ -142,12 +189,12 @@ export class Client {
|
|||
|
||||
ws.onmessage = function(net) {
|
||||
this.log("[<] " + net.data);
|
||||
let msg = new Message(net.data);
|
||||
let msg = new IRCMessage(net.data);
|
||||
if(msg.command === undefined) {
|
||||
this.log("[v5shoutbox] invalid message");
|
||||
return;
|
||||
}
|
||||
this.processMessage(msg);
|
||||
this.processIRCMessage(msg);
|
||||
}.bind(this);
|
||||
|
||||
ws.onclose = function() {
|
||||
|
@ -180,7 +227,7 @@ export class Client {
|
|||
this.send("USER", [this.username, "*", "0", this.username]);
|
||||
}
|
||||
|
||||
processMessage(msg) {
|
||||
processIRCMessage(msg) {
|
||||
if(msg.command === "PING") {
|
||||
this.send("PONG", msg.args);
|
||||
return;
|
||||
|
|
|
@ -1,15 +1,34 @@
|
|||
self.addEventListener("activate", (e) => {
|
||||
console.log("[worker] activating!");
|
||||
"use strict";
|
||||
|
||||
function log(...args) {
|
||||
const time = new Date().toISOString();
|
||||
console.log(`[${time}]`, ...args);
|
||||
}
|
||||
|
||||
/* Activate eagerly as soon as the worker gets updated. Normally, when a worker
|
||||
is updated, the previous version still gets to control new pages until all
|
||||
of its controlled pages are closed simultaneously. We skip this, so new
|
||||
pages will get the latest shoutbox service worker as soon as possible. */
|
||||
self.addEventListener("install", (e) => {
|
||||
log("installing!");
|
||||
/* This promise is guarantee OK to ignore by API */
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("install", (e) => {
|
||||
console.log("[worker] installing!");
|
||||
/* As soon as the worker activates, we claim existing clients. This means that
|
||||
not only new pages will use the updated worker, but running ones will also
|
||||
transition to it, giving us transparent background updates. We handle
|
||||
protocol differences manually; if the main script cannot handle the new
|
||||
shoutbox version it will simply ask for a reload. Doing this allows us to
|
||||
take control of the shoutbox without a page refresh when it is loaded for
|
||||
the first time. */
|
||||
self.addEventListener("activate", (e) => {
|
||||
log("activating!");
|
||||
e.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener("message", (e) => {
|
||||
console.log("[worker] message!");
|
||||
console.log(e);
|
||||
console.log(self);
|
||||
// if(e.data[0] == "!")
|
||||
// postMessage(e.data.substring(1));
|
||||
log(e);
|
||||
if(e.data[0] == "!")
|
||||
postMessage(e.data.substring(1));
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue