refactor(web): websocket events (#7152)

This commit is contained in:
Michel Heusschen 2024-02-16 21:43:40 +01:00 committed by GitHub
parent bbf7a54c65
commit c84c0bae6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 134 additions and 99 deletions

1
web/package-lock.json generated
View File

@ -29,6 +29,7 @@
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.0.0", "@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1", "@floating-ui/dom": "^1.5.1",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.6", "@sveltejs/kit": "^2.0.6",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",

View File

@ -24,6 +24,7 @@
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.0.0", "@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1", "@floating-ui/dom": "^1.5.1",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.6", "@sveltejs/kit": "^2.0.6",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",

View File

@ -6,7 +6,7 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { websocketStore } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAssetFilename } from '$lib/utils/asset-utils'; import { getAssetFilename } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/utils/autogrow'; import { autoGrowHeight } from '$lib/utils/autogrow';
@ -30,7 +30,7 @@
mdiPencil, mdiPencil,
} from '@mdi/js'; } from '@mdi/js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { createEventDispatcher, onDestroy } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { asByteUnitString } from '../../utils/byte-units'; import { asByteUnitString } from '../../utils/byte-units';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
@ -91,14 +91,12 @@
$: people = asset.people || []; $: people = asset.people || [];
$: showingHiddenPeople = false; $: showingHiddenPeople = false;
const unsubscribe = websocketStore.onAssetUpdate.subscribe((assetUpdate) => { onMount(() => {
if (assetUpdate && assetUpdate.id === asset.id) { return websocketEvents.on('on_asset_update', (assetUpdate) => {
if (assetUpdate.id === asset.id) {
asset = assetUpdate; asset = assetUpdate;
} }
}); });
onDestroy(() => {
unsubscribe();
}); });
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{

View File

@ -2,7 +2,7 @@
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { timeBeforeShowLoadingSpinner } from '$lib/constants'; import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { boundingBoxesArray } from '$lib/stores/people.store'; import { boundingBoxesArray } from '$lib/stores/people.store';
import { websocketStore } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getPersonNameWithHiddenValue } from '$lib/utils/person'; import { getPersonNameWithHiddenValue } from '$lib/utils/person';
@ -49,32 +49,12 @@
let loaderLoadingDoneTimeout: NodeJS.Timeout; let loaderLoadingDoneTimeout: NodeJS.Timeout;
let automaticRefreshTimeout: NodeJS.Timeout; let automaticRefreshTimeout: NodeJS.Timeout;
const { onPersonThumbnail } = websocketStore;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
close: void; close: void;
refresh: void; refresh: void;
}>(); }>();
// Reset value async function loadPeople() {
$onPersonThumbnail = '';
$: {
if ($onPersonThumbnail) {
numberOfAssetFaceGenerated.push($onPersonThumbnail);
if (
isEqual(numberOfAssetFaceGenerated, numberOfPersonToCreate) &&
loaderLoadingDoneTimeout &&
automaticRefreshTimeout &&
selectedPersonToCreate.filter((person) => person !== null).length === numberOfPersonToCreate.length
) {
clearTimeout(loaderLoadingDoneTimeout);
clearTimeout(automaticRefreshTimeout);
dispatch('refresh');
}
}
}
onMount(async () => {
const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner); const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
try { try {
const { people } = await getAllPeople({ withHidden: true }); const { people } = await getAllPeople({ withHidden: true });
@ -88,6 +68,25 @@
clearTimeout(timeout); clearTimeout(timeout);
} }
isShowLoadingPeople = false; isShowLoadingPeople = false;
}
const onPersonThumbnail = (personId: string) => {
numberOfAssetFaceGenerated.push(personId);
if (
isEqual(numberOfAssetFaceGenerated, numberOfPersonToCreate) &&
loaderLoadingDoneTimeout &&
automaticRefreshTimeout &&
selectedPersonToCreate.filter((person) => person !== null).length === numberOfPersonToCreate.length
) {
clearTimeout(loaderLoadingDoneTimeout);
clearTimeout(automaticRefreshTimeout);
dispatch('refresh');
}
};
onMount(() => {
loadPeople();
return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
}); });
const isEqual = (a: string[], b: string[]): boolean => { const isEqual = (a: string[], b: string[]): boolean => {

View File

@ -1,15 +1,18 @@
<script lang="ts"> <script lang="ts">
import { websocketStore } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import type { AssetStore } from '$lib/stores/assets.store'; import type { AssetStore } from '$lib/stores/assets.store';
import { onMount } from 'svelte';
export let assetStore: AssetStore | null; export let assetStore: AssetStore | null;
websocketStore.onAssetUpdate.subscribe((asset) => { onMount(() => {
if (asset && asset.originalFileName && assetStore) { return websocketEvents.on('on_asset_update', (asset) => {
if (asset.originalFileName && assetStore) {
assetStore.updateAsset(asset, true); assetStore.updateAsset(asset, true);
assetStore.removeAsset(asset.id); // Update timeline assetStore.removeAsset(asset.id); // Update timeline
assetStore.addAsset(asset); assetStore.addAsset(asset);
} }
}); });
});
</script> </script>

View File

@ -6,13 +6,13 @@
let showModal = false; let showModal = false;
const { onRelease } = websocketStore; const { release } = websocketStore;
const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`; const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
$: releaseVersion = $onRelease && semverToName($onRelease.releaseVersion); $: releaseVersion = $release && semverToName($release.releaseVersion);
$: serverVersion = $onRelease && semverToName($onRelease.serverVersion); $: serverVersion = $release && semverToName($release.serverVersion);
$: $onRelease?.isAvailable && handleRelease(); $: $release?.isAvailable && handleRelease();
const onAcknowledge = () => { const onAcknowledge = () => {
localStorage.setItem('appVersion', releaseVersion); localStorage.setItem('appVersion', releaseVersion);

View File

@ -4,7 +4,7 @@ import { throttle } from 'lodash-es';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { writable, type Unsubscriber } from 'svelte/store'; import { writable, type Unsubscriber } from 'svelte/store';
import { handleError } from '../utils/handle-error'; import { handleError } from '../utils/handle-error';
import { websocketStore } from './websocket'; import { websocketEvents } from './websocket';
export enum BucketPosition { export enum BucketPosition {
Above = 'above', Above = 'above',
@ -96,22 +96,14 @@ export class AssetStore {
connect() { connect() {
this.unsubscribers.push( this.unsubscribers.push(
websocketStore.onUploadSuccess.subscribe((value) => { websocketEvents.on('on_upload_success', (asset) => {
if (value) { this.addPendingChanges({ type: 'add', value: asset });
this.addPendingChanges({ type: 'add', value });
}
}), }),
websocketEvents.on('on_asset_trash', (ids) => {
websocketStore.onAssetTrash.subscribe((ids) => { this.addPendingChanges(...ids.map((id): TrashAsset => ({ type: 'trash', value: id })));
if (ids) {
this.addPendingChanges(...ids.map((id) => ({ type: 'trash', value: id }) as PendingChange));
}
}), }),
websocketEvents.on('on_asset_delete', (id: string) => {
websocketStore.onAssetDelete.subscribe((value) => { this.addPendingChanges({ type: 'delete', value: id });
if (value) {
this.addPendingChanges({ type: 'delete', value });
}
}), }),
); );
} }

View File

@ -1,7 +1,7 @@
import { createEventEmitter } from '$lib/utils/eventemitter';
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk'; import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
import { io, type Socket } from 'socket.io-client'; import { io, type Socket } from 'socket.io-client';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import { loadConfig } from './server-config.store';
import { user } from './user.store'; import { user } from './user.store';
export interface ReleaseEvent { export interface ReleaseEvent {
@ -10,58 +10,54 @@ export interface ReleaseEvent {
serverVersion: ServerVersionResponseDto; serverVersion: ServerVersionResponseDto;
releaseVersion: ServerVersionResponseDto; releaseVersion: ServerVersionResponseDto;
} }
export interface Events {
export const websocketStore = { on_upload_success: (asset: AssetResponseDto) => void;
onUploadSuccess: writable<AssetResponseDto>(), on_asset_delete: (assetId: string) => void;
onAssetDelete: writable<string>(), on_asset_trash: (assetIds: string[]) => void;
onAssetTrash: writable<string[]>(), on_asset_update: (asset: AssetResponseDto) => void;
onAssetUpdate: writable<AssetResponseDto>(), on_asset_hidden: (assetId: string) => void;
onPersonThumbnail: writable<string>(), on_asset_restore: (assetIds: string[]) => void;
serverVersion: writable<ServerVersionResponseDto>(), on_person_thumbnail: (personId: string) => void;
connected: writable<boolean>(false), on_server_version: (serverVersion: ServerVersionResponseDto) => void;
onRelease: writable<ReleaseEvent>(), on_config_update: () => void;
}; on_new_release: (newRelase: ReleaseEvent) => void;
let websocket: Socket | null = null;
export const openWebsocketConnection = async () => {
try {
if (websocket) {
return;
} }
if (!get(user)) { const websocket: Socket<Events> = io('', {
return;
}
websocket = io('', {
path: '/api/socket.io', path: '/api/socket.io',
transports: ['websocket'], transports: ['websocket'],
reconnection: true, reconnection: true,
forceNew: true, forceNew: true,
autoConnect: true, autoConnect: false,
}); });
export const websocketStore = {
connected: writable<boolean>(false),
serverVersion: writable<ServerVersionResponseDto>(),
release: writable<ReleaseEvent>(),
};
export const websocketEvents = createEventEmitter(websocket);
websocket websocket
.on('connect', () => websocketStore.connected.set(true)) .on('connect', () => websocketStore.connected.set(true))
.on('disconnect', () => websocketStore.connected.set(false)) .on('disconnect', () => websocketStore.connected.set(false))
// .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(data)) .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
.on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(data)) .on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion))
.on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(data)) .on('connect_error', (e) => console.log('Websocket Connect Error', e));
.on('on_asset_update', (data) => websocketStore.onAssetUpdate.set(data))
.on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(data)) export const openWebsocketConnection = async () => {
.on('on_server_version', (data) => websocketStore.serverVersion.set(data)) try {
.on('on_config_update', () => loadConfig()) if (!get(user)) {
.on('on_new_release', (data) => websocketStore.onRelease.set(data)) return;
.on('error', (e) => console.log('Websocket Error', e)); }
websocket.connect();
} catch (error) { } catch (error) {
console.log('Cannot connect to websocket', error); console.log('Cannot connect to websocket', error);
} }
}; };
export const closeWebsocketConnection = () => { export const closeWebsocketConnection = () => {
if (websocket) { websocket.disconnect();
websocket.close();
}
websocket = null;
}; };

View File

@ -0,0 +1,42 @@
import type {
DefaultEventsMap,
EventsMap,
ReservedOrUserEventNames,
ReservedOrUserListener,
} from '@socket.io/component-emitter';
import type { Socket } from 'socket.io-client';
export function createEventEmitter<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ReservedEvents extends EventsMap = NonNullable<unknown>,
>(socket: Socket<ListenEvents, EmitEvents>) {
function on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>,
) {
socket.on(ev, listener);
return () => {
socket.off(ev, listener);
};
}
function once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>,
) {
socket.once(ev, listener);
return () => {
socket.off(ev, listener);
};
}
function off<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>,
) {
socket.off(ev, listener);
}
return { on, once, off };
}

View File

@ -30,7 +30,7 @@
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store'; import { AssetStore } from '$lib/stores/assets.store';
import { websocketStore } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { clickOutside } from '$lib/utils/click-outside'; import { clickOutside } from '$lib/utils/click-outside';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@ -68,7 +68,6 @@
}); });
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { selectedAssets, isMultiSelectState } = assetInteractionStore; const { selectedAssets, isMultiSelectState } = assetInteractionStore;
const { onPersonThumbnail } = websocketStore;
let viewMode: ViewMode = ViewMode.VIEW_ASSETS; let viewMode: ViewMode = ViewMode.VIEW_ASSETS;
let isEditingName = false; let isEditingName = false;
@ -119,8 +118,6 @@
$: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived); $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
$: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
$: $onPersonThumbnail === data.person.id &&
(thumbnailData = getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`);
$: { $: {
if (people) { if (people) {
@ -138,6 +135,12 @@
if (action == 'merge') { if (action == 'merge') {
viewMode = ViewMode.MERGE_PEOPLE; viewMode = ViewMode.MERGE_PEOPLE;
} }
return websocketEvents.on('on_person_thumbnail', (personId: string) => {
if (data.person.id === personId) {
thumbnailData = getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`;
}
});
}); });
const handleKeyboardPress = (event: KeyboardEvent) => { const handleKeyboardPress = (event: KeyboardEvent) => {