diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index 3012ba35..cc8b9449 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -10,6 +10,9 @@ import Auth from "../../mobx/stores/Auth"; import { modalController } from "../modals/ModalController"; import Session from "./Session"; +/** + * Controls the lifecycles of clients + */ class ClientController { /** * API client @@ -36,6 +39,7 @@ class ClientController { apiURL: import.meta.env.VITE_API_URL, }); + // ! FIXME: loop until success infinitely this.apiClient .fetchConfiguration() .then(() => (this.configuration = this.apiClient.configuration!)); @@ -70,26 +74,51 @@ class ClientController { this.pickNextSession(); } + /** + * Get the currently selected session + * @returns Active Session + */ @computed getActiveSession() { return this.sessions.get(this.current!); } + /** + * Get an unauthenticated instance of the Revolt.js Client + * @returns API Client + */ @computed getAnonymousClient() { return this.apiClient; } + /** + * Get the next available client (either from session or API) + * @returns Revolt.js Client + */ @computed getAvailableClient() { return this.getActiveSession()?.client ?? this.apiClient; } + /** + * Fetch server configuration + * @returns Server Configuration + */ @computed getServerConfig() { return this.configuration; } + /** + * Check whether we are logged in right now + * @returns Whether we are logged in + */ @computed isLoggedIn() { return this.current === null; } + /** + * Start a new client lifecycle + * @param entry Session Information + * @param knowledge Whether the session is new or existing + */ @action addSession( entry: { session: SessionPrivate; apiUrl?: string }, knowledge: "new" | "existing", @@ -119,6 +148,10 @@ class ClientController { this.pickNextSession(); } + /** + * Login given a set of credentials + * @param credentials Credentials + */ async login(credentials: API.DataLogin) { const browser = detect(); @@ -181,35 +214,19 @@ class ClientController { } } + // Start client lifecycle this.addSession( { session, }, "new", ); - - /*const s = session; - - client.session = session; - (client as any).$updateHeaders(); - - async function login() { - state.auth.setSession(s); - } - - const { onboarding } = await client.api.get("/onboard/hello"); - - if (onboarding) { - openScreen({ - id: "onboarding", - callback: async (username: string) => - client.completeOnboarding({ username }, false).then(login), - }); - } else { - login(); - }*/ } + /** + * Log out of a specific user session + * @param user_id Target User ID + */ @action logout(user_id: string) { const session = this.sessions.get(user_id); if (session) { @@ -223,12 +240,19 @@ class ClientController { } } + /** + * Logout of the current session + */ @action logoutCurrent() { if (this.current) { this.logout(this.current); } } + /** + * Switch to another user session + * @param user_id Target User ID + */ @action switchAccount(user_id: string) { this.current = user_id; } diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index d171613b..dcd705ca 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -5,8 +5,14 @@ import { state } from "../../mobx/State"; import { __thisIsAHack } from "../../context/intermediate/Intermediate"; +/** + * Current lifecycle state + */ type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; +/** + * Possible transitions between states + */ type Transition = | { action: "LOGIN"; @@ -26,11 +32,17 @@ type Transition = | "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); @@ -44,7 +56,7 @@ export default class Session { } /** - * Initiate logout and destroy client. + * Initiate logout and destroy client */ @action destroy() { if (this.client) { @@ -54,30 +66,46 @@ export default class Session { } } + /** + * 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, @@ -90,12 +118,20 @@ export default class Session { 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) { @@ -110,6 +146,10 @@ export default class Session { } } + /** + * 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); @@ -121,6 +161,10 @@ export default class Session { } } + /** + * 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);