refactor: event manager (#25565)

This commit is contained in:
Jason Rasmussen 2026-01-29 08:52:18 -05:00 committed by GitHub
parent 8db61d341f
commit 10b53b525d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 84 additions and 68 deletions

View file

@ -5,7 +5,7 @@ export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolea
const zoomInstance = createZoomImageWheel(node, { maxZoom: 10, initialState: assetViewerManager.zoomState }); const zoomInstance = createZoomImageWheel(node, { maxZoom: 10, initialState: assetViewerManager.zoomState });
const unsubscribes = [ const unsubscribes = [
assetViewerManager.on('ZoomChange', (state) => zoomInstance.setState(state)), assetViewerManager.on({ ZoomChange: (state) => zoomInstance.setState(state) }),
zoomInstance.subscribe(({ state }) => assetViewerManager.onZoomChange(state)), zoomInstance.subscribe(({ state }) => assetViewerManager.onZoomChange(state)),
]; ];

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { assetViewerManager, type Events } from '$lib/managers/asset-viewer-manager.svelte'; import { assetViewerManager, type Events } from '$lib/managers/asset-viewer-manager.svelte';
import type { EventCallback } from '$lib/utils/base-event-manager.svelte'; import type { EventCallback, EventMap } from '$lib/utils/base-event-manager.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
type Props = { type Props = {
@ -10,22 +10,17 @@
const props: Props = $props(); const props: Props = $props();
onMount(() => { onMount(() => {
const unsubscribes: Array<() => void> = []; const events: EventMap<Events> = {};
for (const name of Object.keys(props)) { for (const [name, listener] of Object.entries(props)) {
if (listener) {
const event = name.slice(2) as keyof Events; const event = name.slice(2) as keyof Events;
const listener = props[name as keyof Props] as EventCallback<Events, typeof event> | undefined; events[event] = listener as EventCallback<Events, typeof event>;
if (!listener) { }
continue;
} }
unsubscribes.push(assetViewerManager.on(event, listener)); return assetViewerManager.on(events);
}
return () => {
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
};
}); });
</script> </script>
const event = name.slice(2) as keyof Events;

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { eventManager, type Events } from '$lib/managers/event-manager.svelte'; import { eventManager, type Events } from '$lib/managers/event-manager.svelte';
import type { EventCallback, EventMap } from '$lib/utils/base-event-manager.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
type Props = { type Props = {
@ -9,25 +10,15 @@
const props: Props = $props(); const props: Props = $props();
onMount(() => { onMount(() => {
const unsubscribes: Array<() => void> = []; const events: EventMap<Events> = {};
for (const name of Object.keys(props)) { for (const [name, listener] of Object.entries(props)) {
if (listener) {
const event = name.slice(2) as keyof Events; const event = name.slice(2) as keyof Events;
const listener = props[name as keyof Props]; events[event] = listener as EventCallback<Events, typeof event>;
}
if (!listener) {
continue;
} }
const args = [event, listener as (...args: Events[typeof event]) => void] as const; return eventManager.on(events);
unsubscribes.push(eventManager.on(...args));
}
return () => {
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
};
}); });
</script> </script>

View file

@ -37,9 +37,11 @@ class AssetCacheManager {
#ocrCache = new AsyncCache<AssetOcrResponseDto[]>(); #ocrCache = new AsyncCache<AssetOcrResponseDto[]>();
constructor() { constructor() {
eventManager.on('AssetEditsApplied', () => { eventManager.on({
AssetEditsApplied: () => {
this.#assetCache.clear(); this.#assetCache.clear();
this.#ocrCache.clear(); this.#ocrCache.clear();
},
}); });
} }

View file

@ -59,7 +59,9 @@ class CastManager {
// Add other cast destinations here (ie FCast) // Add other cast destinations here (ie FCast)
]; ];
eventManager.on('AppInit', () => void this.initialize()); eventManager.on({
AppInit: () => void this.initialize(),
});
} }
private async initialize() { private async initialize() {

View file

@ -5,7 +5,9 @@ class FeatureFlagsManager {
#value?: ServerFeaturesDto = $state(); #value?: ServerFeaturesDto = $state();
constructor() { constructor() {
eventManager.on('SystemConfigUpdate', () => void this.#loadFeatureFlags()); eventManager.on({
SystemConfigUpdate: () => void this.#loadFeatureFlags(),
});
} }
async init() { async init() {

View file

@ -4,7 +4,9 @@ import { lang } from '$lib/stores/preferences.store';
class LanguageManager { class LanguageManager {
constructor() { constructor() {
eventManager.on('AppInit', () => lang.subscribe((lang) => this.setLanguage(lang))); eventManager.on({
AppInit: () => lang.subscribe((lang) => this.setLanguage(lang)),
});
} }
rtl = $state(false); rtl = $state(false);

View file

@ -19,7 +19,9 @@ export class QueueManager {
} }
constructor() { constructor() {
eventManager.on('QueueUpdate', () => this.refresh()); eventManager.on({
QueueUpdate: () => this.refresh(),
});
} }
listen() { listen() {

View file

@ -5,7 +5,9 @@ class ReleaseManager {
value = $state<ReleaseEvent | undefined>(); value = $state<ReleaseEvent | undefined>();
constructor() { constructor() {
eventManager.on('ReleaseEvent', (event) => (this.value = event)); eventManager.on({
ReleaseEvent: (event) => (this.value = event),
});
} }
} }

View file

@ -5,7 +5,9 @@ class ServerConfigManager {
#value?: ServerConfigDto = $state(); #value?: ServerConfigDto = $state();
constructor() { constructor() {
eventManager.on('SystemConfigUpdate', () => this.loadServerConfig()); eventManager.on({
SystemConfigUpdate: () => this.loadServerConfig(),
});
} }
async init() { async init() {

View file

@ -7,7 +7,9 @@ class SystemConfigManager {
#defaultValue?: SystemConfigDto = $state(); #defaultValue?: SystemConfigDto = $state();
constructor() { constructor() {
eventManager.on('SystemConfigUpdate', (config) => (this.#value = config)); eventManager.on({
SystemConfigUpdate: (config) => (this.#value = config),
});
} }
async init() { async init() {

View file

@ -37,7 +37,9 @@ class ThemeManager {
isDark = $derived(this.value === Theme.DARK); isDark = $derived(this.value === Theme.DARK);
constructor() { constructor() {
eventManager.on('AppInit', () => this.#onAppInit()); eventManager.on({
AppInit: () => this.#onAppInit(),
});
} }
setSystem(system: boolean) { setSystem(system: boolean) {

View file

@ -111,9 +111,11 @@ export class TimelineManager extends VirtualScrollManager {
constructor() { constructor() {
super(); super();
const onAssetUpdate = (asset: AssetResponseDto) => this.upsertAssets([toTimelineAsset(asset)]); this.#unsubscribes.push(
eventManager.on({
this.#unsubscribes.push(eventManager.on('AssetUpdate', onAssetUpdate)); AssetUpdate: (asset: AssetResponseDto) => this.upsertAssets([toTimelineAsset(asset)]),
}),
);
} }
override get scrollTop(): number { override get scrollTop(): number {

View file

@ -6,7 +6,7 @@ class UploadManager {
mediaTypes = $state<ServerMediaTypesResponseDto>({ image: [], sidecar: [], video: [] }); mediaTypes = $state<ServerMediaTypesResponseDto>({ image: [], sidecar: [], video: [] });
constructor() { constructor() {
eventManager.onMany({ eventManager.on({
AppInit: () => this.#loadExtensions(), AppInit: () => this.#loadExtensions(),
AuthLogout: () => this.reset(), AuthLogout: () => this.reset(),
}); });

View file

@ -19,7 +19,9 @@ class FoldersStore {
private assets = $state<AssetCache>({}); private assets = $state<AssetCache>({});
constructor() { constructor() {
eventManager.on('AuthLogout', () => this.clearCache()); eventManager.on({
AuthLogout: () => this.clearCache(),
});
} }
async fetchTree(): Promise<TreeNode> { async fetchTree(): Promise<TreeNode> {

View file

@ -25,7 +25,7 @@ class MemoryStoreSvelte {
#loading: Promise<void> | undefined; #loading: Promise<void> | undefined;
constructor() { constructor() {
eventManager.onMany({ eventManager.on({
AuthLogout: () => this.clearCache(), AuthLogout: () => this.clearCache(),
AuthUserLoaded: () => this.initialize(), AuthUserLoaded: () => this.initialize(),
}); });

View file

@ -8,7 +8,7 @@ class NotificationStore {
notifications = $state<NotificationDto[]>([]); notifications = $state<NotificationDto[]>([]);
constructor() { constructor() {
eventManager.onMany({ eventManager.on({
AuthLogin: () => this.refresh(), AuthLogin: () => this.refresh(),
AuthLogout: () => this.clear(), AuthLogout: () => this.clear(),
}); });

View file

@ -5,7 +5,9 @@ class SearchStore {
isSearchEnabled = $state(false); isSearchEnabled = $state(false);
constructor() { constructor() {
eventManager.on('AuthLogout', () => this.clearCache()); eventManager.on({
AuthLogout: () => this.clearCache(),
});
} }
clearCache() { clearCache() {

View file

@ -16,4 +16,6 @@ export const resetSavedUser = () => {
purchaseStore.setPurchaseStatus(false); purchaseStore.setPurchaseStatus(false);
}; };
eventManager.on('AuthLogout', () => resetSavedUser()); eventManager.on({
AuthLogout: () => resetSavedUser(),
});

View file

@ -26,4 +26,6 @@ const reset = () => {
Object.assign(userInteraction, defaultUserInteraction); Object.assign(userInteraction, defaultUserInteraction);
}; };
eventManager.on('AuthLogout', () => reset()); eventManager.on({
AuthLogout: () => reset(),
});

View file

@ -1,8 +1,9 @@
type EventMap = Record<string, unknown[]>; type EventsBase = Record<string, unknown[]>;
type PromiseLike<T> = Promise<T> | T; type PromiseLike<T> = Promise<T> | T;
export type EventCallback<E extends EventMap, T extends keyof E> = (...args: E[T]) => PromiseLike<unknown>; export type EventMap<E extends EventsBase> = { [K in keyof E]?: EventCallback<E, K> };
export type EventItem<E extends EventMap, T extends keyof E = keyof E> = { export type EventCallback<E extends EventsBase, T extends keyof E> = (...args: E[T]) => PromiseLike<unknown>;
export type EventItem<E extends EventsBase, T extends keyof E = keyof E> = {
id: number; id: number;
event: T; event: T;
callback: EventCallback<E, T>; callback: EventCallback<E, T>;
@ -13,10 +14,22 @@ const nextId = () => count++;
const noop = () => {}; const noop = () => {};
export class BaseEventManager<Events extends EventMap> { export class BaseEventManager<Events extends EventsBase> {
#callbacks: EventItem<Events>[] = $state([]); #callbacks: EventItem<Events>[] = $state([]);
on<T extends keyof Events>(event: T, callback?: EventCallback<Events, T>) { on(subscriptions: EventMap<Events>): () => void {
const cleanups = Object.entries(subscriptions).map(([event, callback]) =>
this.#onEvent(event as keyof Events, callback as EventCallback<Events, keyof Events>),
);
return () => {
for (const cleanup of cleanups) {
cleanup();
}
};
}
#onEvent<T extends keyof Events>(event: T, callback?: EventCallback<Events, T>) {
if (!callback) { if (!callback) {
return noop; return noop;
} }
@ -30,17 +43,6 @@ export class BaseEventManager<Events extends EventMap> {
}; };
} }
onMany(subscriptions: { [T in keyof Events]?: EventCallback<Events, T> }) {
const cleanups = Object.entries(subscriptions).map(([event, callback]) =>
this.on(event as keyof Events, callback as EventCallback<Events, keyof Events>),
);
return () => {
for (const cleanup of cleanups) {
cleanup();
}
};
}
emit<T extends keyof Events>(event: T, ...params: Events[T]) { emit<T extends keyof Events>(event: T, ...params: Events[T]) {
const listeners = this.getListeners(event); const listeners = this.getListeners(event);
for (const listener of listeners) { for (const listener of listeners) {