mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-24 06:32:08 -05:00
feat: build finite state machine for sessions
This commit is contained in:
parent
1cfcb20d4d
commit
80f4bb3d98
6 changed files with 235 additions and 28 deletions
49
src/controllers/client/ClientController.tsx
Normal file
49
src/controllers/client/ClientController.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { action, makeAutoObservable, ObservableMap } from "mobx";
|
||||
import type { Nullable } from "revolt.js";
|
||||
|
||||
import Auth from "../../mobx/stores/Auth";
|
||||
|
||||
import Session from "./Session";
|
||||
|
||||
class ClientController {
|
||||
/**
|
||||
* Map of user IDs to sessions
|
||||
*/
|
||||
private sessions: ObservableMap<string, Session>;
|
||||
|
||||
/**
|
||||
* User ID of active session
|
||||
*/
|
||||
private current: Nullable<string>;
|
||||
|
||||
constructor() {
|
||||
this.sessions = new ObservableMap();
|
||||
this.current = null;
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate sessions and start client lifecycles.
|
||||
* @param auth Authentication store
|
||||
*/
|
||||
@action hydrate(auth: Auth) {
|
||||
for (const entry of auth.getAccounts()) {
|
||||
const session = new Session();
|
||||
session.emit({
|
||||
action: "LOGIN",
|
||||
session: entry.session,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getActiveSession() {
|
||||
return this.sessions;
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
return this.current === null;
|
||||
}
|
||||
}
|
||||
|
||||
export const clientController = new ClientController();
|
164
src/controllers/client/Session.tsx
Normal file
164
src/controllers/client/Session.tsx
Normal file
|
@ -0,0 +1,164 @@
|
|||
import { action, makeAutoObservable } from "mobx";
|
||||
import { Client } from "revolt.js";
|
||||
|
||||
type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline";
|
||||
|
||||
type Transition =
|
||||
| {
|
||||
action: "LOGIN";
|
||||
session: SessionPrivate;
|
||||
}
|
||||
| {
|
||||
action:
|
||||
| "SUCCESS"
|
||||
| "DISCONNECT"
|
||||
| "RETRY"
|
||||
| "LOGOUT"
|
||||
| "ONLINE"
|
||||
| "OFFLINE";
|
||||
};
|
||||
|
||||
export default class Session {
|
||||
state: State = window.navigator.onLine ? "Ready" : "Offline";
|
||||
client: Client | null = null;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private onOnline() {
|
||||
this.emit({
|
||||
action: "ONLINE",
|
||||
});
|
||||
}
|
||||
|
||||
private onOffline() {
|
||||
this.emit({
|
||||
action: "OFFLINE",
|
||||
});
|
||||
}
|
||||
|
||||
private onDropped() {
|
||||
this.emit({
|
||||
action: "DISCONNECT",
|
||||
});
|
||||
}
|
||||
|
||||
private onReady() {
|
||||
this.emit({
|
||||
action: "SUCCESS",
|
||||
});
|
||||
}
|
||||
|
||||
private createClient() {
|
||||
this.client = new Client({
|
||||
unreads: true,
|
||||
autoReconnect: false,
|
||||
onPongTimeout: "EXIT",
|
||||
apiURL: import.meta.env.VITE_API_URL,
|
||||
});
|
||||
|
||||
this.client.addListener("dropped", this.onDropped);
|
||||
this.client.addListener("ready", this.onReady);
|
||||
}
|
||||
|
||||
private destroyClient() {
|
||||
this.client!.removeAllListeners();
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
|
||||
@action async emit(data: Transition) {
|
||||
switch (data.action) {
|
||||
// Login with session
|
||||
case "LOGIN": {
|
||||
this.assert("Ready");
|
||||
this.state = "Connecting";
|
||||
this.createClient();
|
||||
|
||||
try {
|
||||
await this.client!.useExistingSession(data.session);
|
||||
} catch (err) {
|
||||
this.state = "Ready";
|
||||
throw err;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
this.emit({
|
||||
action: "RETRY",
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,7 @@ import localforage from "localforage";
|
|||
import { makeAutoObservable, reaction, runInAction } from "mobx";
|
||||
import { Client } from "revolt.js";
|
||||
|
||||
import { reportError } from "../lib/ErrorBoundary";
|
||||
|
||||
import { clientController } from "../controllers/client/ClientController";
|
||||
import Persistent from "./interfaces/Persistent";
|
||||
import Syncable from "./interfaces/Syncable";
|
||||
import Auth from "./stores/Auth";
|
||||
|
@ -24,6 +23,7 @@ import Sync, { Data as DataSync, SyncKeys } from "./stores/Sync";
|
|||
|
||||
export const MIGRATIONS = {
|
||||
REDUX: 1640305719826,
|
||||
MULTI_SERVER_CONFIG: 1656350006152,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -253,6 +253,9 @@ export default class State {
|
|||
|
||||
// Post-hydration, init plugins.
|
||||
this.plugins.init();
|
||||
|
||||
// Push authentication information forwards to client controller.
|
||||
clientController.hydrate(this.auth);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
import { API } from "revolt.js";
|
||||
import { Nullable } from "revolt.js";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
|
@ -13,7 +11,6 @@ interface Account {
|
|||
|
||||
export interface Data {
|
||||
sessions: Record<string, Account>;
|
||||
current?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,14 +19,12 @@ export interface Data {
|
|||
*/
|
||||
export default class Auth implements Store, Persistent<Data> {
|
||||
private sessions: ObservableMap<string, Account>;
|
||||
private current: Nullable<string>;
|
||||
|
||||
/**
|
||||
* Construct new Auth store.
|
||||
*/
|
||||
constructor() {
|
||||
this.sessions = new ObservableMap();
|
||||
this.current = null;
|
||||
|
||||
// Inject session token if it is provided.
|
||||
if (import.meta.env.VITE_SESSION_TOKEN) {
|
||||
|
@ -40,8 +35,6 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
token: import.meta.env.VITE_SESSION_TOKEN as string,
|
||||
},
|
||||
});
|
||||
|
||||
this.current = "0";
|
||||
}
|
||||
|
||||
makeAutoObservable(this);
|
||||
|
@ -54,7 +47,6 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
@action toJSON() {
|
||||
return {
|
||||
sessions: JSON.parse(JSON.stringify(mapToRecord(this.sessions))),
|
||||
current: this.current ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -72,10 +64,6 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
this.sessions.set(id, v[id]),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.current && this.sessions.has(data.current)) {
|
||||
this.current = data.current;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,7 +72,6 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
*/
|
||||
@action setSession(session: Session) {
|
||||
this.sessions.set(session.user_id, { session });
|
||||
this.current = session.user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,34 +79,38 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
* @param user_id User ID tied to session
|
||||
*/
|
||||
@action removeSession(user_id: string) {
|
||||
if (user_id == this.current) {
|
||||
this.current = null;
|
||||
}
|
||||
|
||||
this.sessions.delete(user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all known accounts.
|
||||
* @returns Array of accounts
|
||||
*/
|
||||
@computed getAccounts() {
|
||||
return [...this.sessions.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove current session.
|
||||
*/
|
||||
@action logout() {
|
||||
/*@action logout() {
|
||||
this.current && this.removeSession(this.current);
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Get current session.
|
||||
* @returns Current session
|
||||
*/
|
||||
@computed getSession() {
|
||||
/*@computed getSession() {
|
||||
if (!this.current) return;
|
||||
return this.sessions.get(this.current)!.session;
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Check whether we are currently logged in.
|
||||
* @returns Whether we are logged in
|
||||
*/
|
||||
@computed isLoggedIn() {
|
||||
/*@computed isLoggedIn() {
|
||||
return this.current !== null;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { action, computed, makeAutoObservable } from "mobx";
|
||||
import { API } from "revolt.js";
|
||||
import { Client } from "revolt.js";
|
||||
import { Nullable } from "revolt.js";
|
||||
import { API, Client, Nullable } from "revolt.js";
|
||||
|
||||
import { isDebug } from "../../revision";
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
|
|
2
src/types/revolt-api.d.ts
vendored
2
src/types/revolt-api.d.ts
vendored
|
@ -5,3 +5,5 @@ declare type Session = {
|
|||
name: string;
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
declare type SessionPrivate = Session;
|
||||
|
|
Loading…
Reference in a new issue