chore(doc): document client controller

This commit is contained in:
Paul Makles 2022-06-29 14:49:48 +01:00
parent 31220db8fe
commit 0e86f19da2
2 changed files with 90 additions and 22 deletions

View file

@ -10,6 +10,9 @@ import Auth from "../../mobx/stores/Auth";
import { modalController } from "../modals/ModalController"; import { modalController } from "../modals/ModalController";
import Session from "./Session"; import Session from "./Session";
/**
* Controls the lifecycles of clients
*/
class ClientController { class ClientController {
/** /**
* API client * API client
@ -36,6 +39,7 @@ class ClientController {
apiURL: import.meta.env.VITE_API_URL, apiURL: import.meta.env.VITE_API_URL,
}); });
// ! FIXME: loop until success infinitely
this.apiClient this.apiClient
.fetchConfiguration() .fetchConfiguration()
.then(() => (this.configuration = this.apiClient.configuration!)); .then(() => (this.configuration = this.apiClient.configuration!));
@ -70,26 +74,51 @@ class ClientController {
this.pickNextSession(); this.pickNextSession();
} }
/**
* Get the currently selected session
* @returns Active Session
*/
@computed getActiveSession() { @computed getActiveSession() {
return this.sessions.get(this.current!); return this.sessions.get(this.current!);
} }
/**
* Get an unauthenticated instance of the Revolt.js Client
* @returns API Client
*/
@computed getAnonymousClient() { @computed getAnonymousClient() {
return this.apiClient; return this.apiClient;
} }
/**
* Get the next available client (either from session or API)
* @returns Revolt.js Client
*/
@computed getAvailableClient() { @computed getAvailableClient() {
return this.getActiveSession()?.client ?? this.apiClient; return this.getActiveSession()?.client ?? this.apiClient;
} }
/**
* Fetch server configuration
* @returns Server Configuration
*/
@computed getServerConfig() { @computed getServerConfig() {
return this.configuration; return this.configuration;
} }
/**
* Check whether we are logged in right now
* @returns Whether we are logged in
*/
@computed isLoggedIn() { @computed isLoggedIn() {
return this.current === null; return this.current === null;
} }
/**
* Start a new client lifecycle
* @param entry Session Information
* @param knowledge Whether the session is new or existing
*/
@action addSession( @action addSession(
entry: { session: SessionPrivate; apiUrl?: string }, entry: { session: SessionPrivate; apiUrl?: string },
knowledge: "new" | "existing", knowledge: "new" | "existing",
@ -119,6 +148,10 @@ class ClientController {
this.pickNextSession(); this.pickNextSession();
} }
/**
* Login given a set of credentials
* @param credentials Credentials
*/
async login(credentials: API.DataLogin) { async login(credentials: API.DataLogin) {
const browser = detect(); const browser = detect();
@ -181,35 +214,19 @@ class ClientController {
} }
} }
// Start client lifecycle
this.addSession( this.addSession(
{ {
session, session,
}, },
"new", "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) { @action logout(user_id: string) {
const session = this.sessions.get(user_id); const session = this.sessions.get(user_id);
if (session) { if (session) {
@ -223,12 +240,19 @@ class ClientController {
} }
} }
/**
* Logout of the current session
*/
@action logoutCurrent() { @action logoutCurrent() {
if (this.current) { if (this.current) {
this.logout(this.current); this.logout(this.current);
} }
} }
/**
* Switch to another user session
* @param user_id Target User ID
*/
@action switchAccount(user_id: string) { @action switchAccount(user_id: string) {
this.current = user_id; this.current = user_id;
} }

View file

@ -5,8 +5,14 @@ import { state } from "../../mobx/State";
import { __thisIsAHack } from "../../context/intermediate/Intermediate"; import { __thisIsAHack } from "../../context/intermediate/Intermediate";
/**
* Current lifecycle state
*/
type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline";
/**
* Possible transitions between states
*/
type Transition = type Transition =
| { | {
action: "LOGIN"; action: "LOGIN";
@ -26,11 +32,17 @@ type Transition =
| "OFFLINE"; | "OFFLINE";
}; };
/**
* Client lifecycle finite state machine
*/
export default class Session { export default class Session {
state: State = window.navigator.onLine ? "Ready" : "Offline"; state: State = window.navigator.onLine ? "Ready" : "Offline";
user_id: string | null = null; user_id: string | null = null;
client: Client | null = null; client: Client | null = null;
/**
* Create a new Session
*/
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
@ -44,7 +56,7 @@ export default class Session {
} }
/** /**
* Initiate logout and destroy client. * Initiate logout and destroy client
*/ */
@action destroy() { @action destroy() {
if (this.client) { if (this.client) {
@ -54,30 +66,46 @@ export default class Session {
} }
} }
/**
* Called when user's browser signals it is online
*/
private onOnline() { private onOnline() {
this.emit({ this.emit({
action: "ONLINE", action: "ONLINE",
}); });
} }
/**
* Called when user's browser signals it is offline
*/
private onOffline() { private onOffline() {
this.emit({ this.emit({
action: "OFFLINE", action: "OFFLINE",
}); });
} }
/**
* Called when the client signals it has disconnected
*/
private onDropped() { private onDropped() {
this.emit({ this.emit({
action: "DISCONNECT", action: "DISCONNECT",
}); });
} }
/**
* Called when the client signals it has received the Ready packet
*/
private onReady() { private onReady() {
this.emit({ this.emit({
action: "SUCCESS", action: "SUCCESS",
}); });
} }
/**
* Create a new Revolt.js Client for this Session
* @param apiUrl Optionally specify an API URL
*/
private createClient(apiUrl?: string) { private createClient(apiUrl?: string) {
this.client = new Client({ this.client = new Client({
unreads: true, unreads: true,
@ -90,12 +118,20 @@ export default class Session {
this.client.addListener("ready", this.onReady); this.client.addListener("ready", this.onReady);
} }
/**
* Destroy the client including any listeners.
*/
private destroyClient() { private destroyClient() {
this.client!.removeAllListeners(); this.client!.removeAllListeners();
this.client!.logout();
this.user_id = null; this.user_id = null;
this.client = null; this.client = null;
} }
/**
* Ensure we are in one of the given states
* @param state Possible states
*/
private assert(...state: State[]) { private assert(...state: State[]) {
let found = false; let found = false;
for (const target of state) { 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" }) { private async continueLogin(data: Transition & { action: "LOGIN" }) {
try { try {
await this.client!.useExistingSession(data.session); 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) { @action async emit(data: Transition) {
console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data); console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data);