forked from abner/for-legacy-web
278 lines
7.4 KiB
TypeScript
278 lines
7.4 KiB
TypeScript
import { action, computed, makeAutoObservable } from "mobx";
|
|
import { API, Client } from "revolt.js";
|
|
|
|
import { state } from "../../mobx/State";
|
|
|
|
import { __thisIsAHack } from "../../context/intermediate/Intermediate";
|
|
|
|
import { modalController } from "../modals/ModalController";
|
|
|
|
/**
|
|
* Current lifecycle state
|
|
*/
|
|
type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline";
|
|
|
|
/**
|
|
* Possible transitions between states
|
|
*/
|
|
type Transition =
|
|
| {
|
|
action: "LOGIN";
|
|
apiUrl?: string;
|
|
session: SessionPrivate;
|
|
configuration?: API.RevoltConfig;
|
|
|
|
knowledge: "new" | "existing";
|
|
}
|
|
| {
|
|
action:
|
|
| "SUCCESS"
|
|
| "DISCONNECT"
|
|
| "RETRY"
|
|
| "LOGOUT"
|
|
| "ONLINE"
|
|
| "OFFLINE";
|
|
};
|
|
|
|
/**
|
|
* Client lifecycle finite state machine
|
|
*/
|
|
export default class Session {
|
|
state: State = window.navigator.onLine ? "Ready" : "Offline";
|
|
user_id: string | null = null;
|
|
client: Client | null = null;
|
|
|
|
/**
|
|
* Create a new Session
|
|
*/
|
|
constructor() {
|
|
makeAutoObservable(this);
|
|
|
|
this.onDropped = this.onDropped.bind(this);
|
|
this.onReady = this.onReady.bind(this);
|
|
this.onOnline = this.onOnline.bind(this);
|
|
this.onOffline = this.onOffline.bind(this);
|
|
|
|
window.addEventListener("online", this.onOnline);
|
|
window.addEventListener("offline", this.onOffline);
|
|
}
|
|
|
|
/**
|
|
* Initiate logout and destroy client
|
|
*/
|
|
@action destroy() {
|
|
if (this.client) {
|
|
this.client.logout(false);
|
|
this.state = "Ready";
|
|
this.client = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when user's browser signals it is online
|
|
*/
|
|
private onOnline() {
|
|
this.emit({
|
|
action: "ONLINE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called when user's browser signals it is offline
|
|
*/
|
|
private onOffline() {
|
|
this.emit({
|
|
action: "OFFLINE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called when the client signals it has disconnected
|
|
*/
|
|
private onDropped() {
|
|
this.emit({
|
|
action: "DISCONNECT",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called when the client signals it has received the Ready packet
|
|
*/
|
|
private onReady() {
|
|
this.emit({
|
|
action: "SUCCESS",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new Revolt.js Client for this Session
|
|
* @param apiUrl Optionally specify an API URL
|
|
*/
|
|
private createClient(apiUrl?: string) {
|
|
this.client = new Client({
|
|
unreads: true,
|
|
autoReconnect: false,
|
|
onPongTimeout: "EXIT",
|
|
apiURL: apiUrl ?? import.meta.env.VITE_API_URL,
|
|
});
|
|
|
|
this.client.addListener("dropped", this.onDropped);
|
|
this.client.addListener("ready", this.onReady);
|
|
}
|
|
|
|
/**
|
|
* Destroy the client including any listeners.
|
|
*/
|
|
private destroyClient() {
|
|
this.client!.removeAllListeners();
|
|
this.client!.logout();
|
|
this.user_id = null;
|
|
this.client = null;
|
|
}
|
|
|
|
/**
|
|
* Ensure we are in one of the given states
|
|
* @param state Possible states
|
|
*/
|
|
private assert(...state: State[]) {
|
|
let found = false;
|
|
for (const target of state) {
|
|
if (this.state === target) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
throw `State must be ${state} in order to transition! (currently ${this.state})`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Continue logging in provided onboarding is successful
|
|
* @param data Transition Data
|
|
*/
|
|
private async continueLogin(data: Transition & { action: "LOGIN" }) {
|
|
try {
|
|
await this.client!.useExistingSession(data.session);
|
|
this.user_id = this.client!.user!._id;
|
|
state.auth.setSession(data.session);
|
|
} catch (err) {
|
|
this.state = "Ready";
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transition to a new state by a certain action
|
|
* @param data Transition Data
|
|
*/
|
|
@action async emit(data: Transition) {
|
|
console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data);
|
|
|
|
switch (data.action) {
|
|
// Login with session
|
|
case "LOGIN": {
|
|
this.assert("Ready");
|
|
this.state = "Connecting";
|
|
this.createClient(data.apiUrl);
|
|
|
|
if (data.configuration) {
|
|
this.client!.configuration = data.configuration;
|
|
}
|
|
|
|
if (data.knowledge === "new") {
|
|
await this.client!.fetchConfiguration();
|
|
this.client!.session = data.session;
|
|
(this.client! as any).$updateHeaders();
|
|
|
|
const { onboarding } = await this.client!.api.get(
|
|
"/onboard/hello",
|
|
);
|
|
|
|
if (onboarding) {
|
|
modalController.push({
|
|
type: "onboarding",
|
|
callback: async (username: string) =>
|
|
this.client!.completeOnboarding(
|
|
{ username },
|
|
false,
|
|
).then(() => this.continueLogin(data)),
|
|
});
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.continueLogin(data);
|
|
|
|
break;
|
|
}
|
|
// Ready successfully received
|
|
case "SUCCESS": {
|
|
this.assert("Connecting");
|
|
this.state = "Online";
|
|
break;
|
|
}
|
|
// Client got disconnected
|
|
case "DISCONNECT": {
|
|
if (navigator.onLine) {
|
|
this.assert("Online");
|
|
this.state = "Disconnected";
|
|
|
|
setTimeout(() => {
|
|
// Check we are still disconnected before retrying.
|
|
if (this.state === "Disconnected") {
|
|
this.emit({
|
|
action: "RETRY",
|
|
});
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
break;
|
|
}
|
|
// We should try reconnecting
|
|
case "RETRY": {
|
|
this.assert("Disconnected");
|
|
this.client!.websocket.connect();
|
|
this.state = "Connecting";
|
|
break;
|
|
}
|
|
// User instructed logout
|
|
case "LOGOUT": {
|
|
this.assert("Connecting", "Online", "Disconnected");
|
|
this.state = "Ready";
|
|
this.destroyClient();
|
|
break;
|
|
}
|
|
// Browser went offline
|
|
case "OFFLINE": {
|
|
this.state = "Offline";
|
|
break;
|
|
}
|
|
// Browser went online
|
|
case "ONLINE": {
|
|
this.assert("Offline");
|
|
if (this.client) {
|
|
this.state = "Disconnected";
|
|
this.emit({
|
|
action: "RETRY",
|
|
});
|
|
} else {
|
|
this.state = "Ready";
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether we are ready to render.
|
|
* @returns Boolean
|
|
*/
|
|
@computed get ready() {
|
|
return !!this.client?.user;
|
|
}
|
|
}
|