Skip to content

Commit 7a1d35c

Browse files
committed
refactor(auth): create onBeforeLogout and onAfterLogout callbacks
This greatly simplifies the timing of the cleanups performed when the current user logs out of the application Signed-off-by: Fernando Fernández <ferferga@hotmail.com>
1 parent ad8cc2c commit 7a1d35c

File tree

7 files changed

+120
-71
lines changed

7 files changed

+120
-71
lines changed

frontend/src/plugins/remote/auth.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import SDK, { useOneTimeAPI } from './sdk/sdk-utils';
1212
import { isAxiosError, isNil, sealed } from '@/utils/validation';
1313
import { i18n } from '@/plugins/i18n';
1414
import { useSnackbar } from '@/composables/use-snackbar';
15-
import { CommonStore } from '@/store/super/common-store';
15+
import { BaseState } from '@/store/super/base-state';
1616

1717
export interface ServerInfo extends BetterOmit<PublicSystemInfo, 'LocalAddress'> {
1818
PublicAddress: string;
@@ -34,7 +34,12 @@ interface AuthState {
3434
}
3535

3636
@sealed
37-
class RemotePluginAuth extends CommonStore<AuthState> {
37+
class RemotePluginAuth extends BaseState<AuthState> {
38+
private readonly _callbacks = {
39+
beforeLogout: [] as MaybePromise<void>[],
40+
afterLogout: [] as MaybePromise<void>[]
41+
};
42+
3843
public readonly servers = computed(() => this._state.value.servers);
3944
public readonly currentServer = computed(() => this._state.value.servers[this._state.value.currentServerIndex]);
4045
public readonly currentUser = computed(() => this._state.value.users[this._state.value.currentUserIndex]);
@@ -104,6 +109,18 @@ class RemotePluginAuth extends CommonStore<AuthState> {
104109
};
105110
};
106111

112+
private readonly _runCallbacks = async (callbacks: MaybePromise<void>[]) =>
113+
await Promise.allSettled(callbacks.map(fn => fn()));
114+
115+
/**
116+
* Runs the passed function before logging out the user
117+
*/
118+
public readonly onBeforeLogout = (fn: MaybePromise<void>) =>
119+
this._callbacks.beforeLogout.push(fn);
120+
121+
public readonly onAfterLogout = (fn: MaybePromise<void>) =>
122+
this._callbacks.afterLogout.push(fn);
123+
107124
/**
108125
* Connects to a server
109126
*
@@ -240,9 +257,17 @@ class RemotePluginAuth extends CommonStore<AuthState> {
240257
*/
241258
public readonly logoutCurrentUser = async (skipRequest = false): Promise<void> => {
242259
if (!isNil(this.currentUser.value) && !isNil(this.currentServer.value)) {
260+
await this._runCallbacks(this._callbacks.beforeLogout);
243261
await this.logoutUser(this.currentUser.value, this.currentServer.value, skipRequest);
244262

245263
this._state.value.currentUserIndex = -1;
264+
/**
265+
* We need this so the callbacks are run after all the dependencies are updated
266+
* (i.e the page component is routed to index).
267+
*/
268+
globalThis.requestAnimationFrame(() =>
269+
globalThis.setTimeout(() => void this._runCallbacks(this._callbacks.afterLogout))
270+
);
246271
}
247272
};
248273

frontend/src/plugins/workers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import type { ICanvasDrawer } from './canvas-drawer.worker';
66
import CanvasDrawer from './canvas-drawer.worker?worker';
77
import type { IGenericWorker } from './generic.worker';
88
import GenericWorker from './generic.worker?worker';
9+
import { remote } from '@/plugins/remote';
910

1011
/**
1112
* A worker for decoding blurhash strings into pixels
1213
*/
1314
export const blurhashDecoder = wrap<IBlurhashDecoder>(new BlurhashDecoder());
1415

16+
remote.auth.onAfterLogout(async () => await blurhashDecoder.clearCache());
17+
1518
/**
1619
* A worker for drawing canvas offscreen. The canvas must be transferred like this:
1720
* ```ts

frontend/src/store/api.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -210,17 +210,7 @@ class ApiStore {
210210
}
211211
);
212212

213-
watch(remote.auth.currentUser,
214-
() => {
215-
globalThis.requestAnimationFrame(() => {
216-
globalThis.setTimeout(() => {
217-
if (!remote.auth.currentUser.value) {
218-
this._clear();
219-
}
220-
});
221-
});
222-
}, { flush: 'post' }
223-
);
213+
remote.auth.onAfterLogout(this._clear);
224214
}
225215
}
226216

frontend/src/store/playback-manager.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ class PlaybackManagerStore extends CommonStore<PlaybackManagerState> {
340340
* Report playback stopped to the server. Used by the "Now playing" statistics in other clients.
341341
*/
342342
private readonly _reportPlaybackStopped = async (
343-
itemId: string,
343+
itemId = this.currentItemId.value,
344344
sessionId = this._state.value.playSessionId,
345345
currentTime = this.currentTime.value
346346
): Promise<void> => {
@@ -989,14 +989,13 @@ class PlaybackManagerStore extends CommonStore<PlaybackManagerState> {
989989
});
990990

991991
/**
992-
* Dispose on logout
992+
* Report playback stop before logging out
993993
*/
994-
watch(remote.auth.currentUser,
995-
() => {
996-
if (isNil(remote.auth.currentUser.value)) {
997-
this.stop();
998-
}
999-
}, { flush: 'post' }
994+
remote.auth.onBeforeLogout(
995+
async () => {
996+
await this._reportPlaybackStopped(this.currentItemId.value);
997+
this._reset();
998+
}
1000999
);
10011000
}
10021001
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* This class provides the base class functionality for stores that contains
3+
* reactive state. It provides a default state, a way to reset the state, and
4+
* persistence options.
5+
*
6+
* This class is intended to be used by plugins and other abstract classes. It
7+
* should not be used by stores (check CommonStore for that).
8+
*/
9+
import { useStorage } from '@vueuse/core';
10+
import { ref, type Ref } from 'vue';
11+
import type { UnknownRecord } from 'type-fest';
12+
import { mergeExcludingUnknown } from '@/utils/data-manipulation';
13+
import { isNil } from '@/utils/validation';
14+
15+
export interface BaseStateParams<T> {
16+
defaultState: () => T;
17+
/**
18+
* Key to be used as an identifier
19+
*/
20+
storeKey: string;
21+
persistenceType?: 'localStorage' | 'sessionStorage';
22+
}
23+
24+
export abstract class BaseState<
25+
T extends object = UnknownRecord,
26+
/**
27+
* State properties that should be exposed to consumers
28+
* of this class. If not provided, all properties
29+
* will be private.
30+
* Exposed properties are also writable.
31+
*/
32+
K extends keyof T = never
33+
> {
34+
private readonly _defaultState;
35+
protected readonly _storeKey;
36+
protected readonly _state: Ref<T>;
37+
/**
38+
* Same as _state, but we use the type system to define which properties
39+
* we want to have accessible to consumers of the extended class.
40+
*/
41+
public readonly state: Ref<Pick<T, K>>;
42+
43+
protected readonly _reset = () => {
44+
this._state.value = this._defaultState();
45+
};
46+
47+
protected constructor({
48+
defaultState,
49+
storeKey,
50+
persistenceType
51+
}: BaseStateParams<T>
52+
) {
53+
this._storeKey = storeKey;
54+
this._defaultState = defaultState;
55+
56+
this._state = isNil(persistenceType) || isNil(storeKey)
57+
? ref(this._defaultState()) as Ref<T>
58+
: useStorage(storeKey, this._defaultState(), globalThis[persistenceType], {
59+
mergeDefaults: (storageValue, defaults) =>
60+
mergeExcludingUnknown(storageValue, defaults)
61+
});
62+
this.state = this._state;
63+
}
64+
}
Lines changed: 16 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { useStorage } from '@vueuse/core';
2-
import { ref, watch, type Ref } from 'vue';
1+
/**
2+
* CommonStore is a base class for all stores. It extends from BaseState,
3+
* providing also a way to reset the state on logout automatically.
4+
*
5+
* This class is intended to be used by stores. It
6+
* should not be used by plugins (check BaseState for that)
7+
* since it has a dependency on the auth plugin.
8+
*/
39
import type { UnknownRecord } from 'type-fest';
4-
import { mergeExcludingUnknown } from '@/utils/data-manipulation';
5-
import { isFunc, isNil } from '@/utils/validation';
10+
import { isBool } from '@/utils/validation';
11+
import { remote } from '@/plugins/remote';
12+
import { BaseState, type BaseStateParams } from '@/store/super/base-state';
613

7-
export interface CommonStoreParams<T> {
8-
defaultState: () => T;
9-
/**
10-
* Key to be used as an identifier
11-
*/
12-
storeKey: string;
13-
persistenceType?: 'localStorage' | 'sessionStorage';
14-
resetOnLogout?: boolean | (() => void);
14+
export interface CommonStoreParams<T> extends BaseStateParams<T> {
15+
resetOnLogout?: boolean | MaybePromise<T>;
1516
}
1617

1718
export abstract class CommonStore<
@@ -23,53 +24,18 @@ export abstract class CommonStore<
2324
* Exposed properties are also writable.
2425
*/
2526
K extends keyof T = never
26-
> {
27-
private readonly _defaultState;
28-
protected readonly _storeKey;
29-
protected readonly _state: Ref<T>;
30-
/**
31-
* Same as _state, but we use the type system to define which properties
32-
* we want to have accessible to consumers of the extended class.
33-
*/
34-
public readonly state: Ref<Pick<T, K>>;
35-
36-
protected readonly _reset = (): void => {
37-
Object.assign(this._state.value, this._defaultState());
38-
};
39-
27+
> extends BaseState<T, K> {
4028
protected constructor({
4129
defaultState,
4230
storeKey,
4331
persistenceType,
4432
resetOnLogout
4533
}: CommonStoreParams<T>
4634
) {
47-
this._storeKey = storeKey;
48-
this._defaultState = defaultState;
49-
50-
this._state = isNil(persistenceType) || isNil(storeKey)
51-
? ref(this._defaultState()) as Ref<T>
52-
: useStorage(storeKey, this._defaultState(), globalThis[persistenceType], {
53-
mergeDefaults: (storageValue, defaults) =>
54-
mergeExcludingUnknown(storageValue, defaults)
55-
});
56-
this.state = this._state;
35+
super({ defaultState, storeKey, persistenceType });
5736

5837
if (resetOnLogout) {
59-
// eslint-disable-next-line sonarjs/no-async-constructor
60-
void (async () => {
61-
const { remote } = await import('@/plugins/remote');
62-
63-
watch(remote.auth.currentUser,
64-
() => {
65-
if (!remote.auth.currentUser.value) {
66-
const funcToRun = isFunc(resetOnLogout) ? resetOnLogout : this._reset;
67-
68-
funcToRun();
69-
}
70-
}, { flush: 'post' }
71-
);
72-
})();
38+
remote.auth.onAfterLogout(isBool(resetOnLogout) ? this._reset : resetOnLogout);
7339
}
7440
}
7541
}

frontend/types/global/util.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ type BetterOmit<T, K extends keyof never> = T extends Record<never, never>
1212
* Sets a type as nullish
1313
*/
1414
type Nullish<T> = T | null | undefined;
15+
16+
type MaybePromise<T> = (() => Promise<T>) | (() => T);

0 commit comments

Comments
 (0)