diff --git a/web/package-lock.json b/web/package-lock.json
index f521c97bfeef3..0177175c30237 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -29,6 +29,7 @@
"devDependencies": {
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
+ "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.6",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
diff --git a/web/package.json b/web/package.json
index d41b8b9ce7496..35eac08ddac30 100644
--- a/web/package.json
+++ b/web/package.json
@@ -24,6 +24,7 @@
"devDependencies": {
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
+ "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.6",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index 252119a0aa938..97d3e2dfea701 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -6,7 +6,7 @@
import { locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.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 { getAssetFilename } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/utils/autogrow';
@@ -30,7 +30,7 @@
mdiPencil,
} from '@mdi/js';
import { DateTime } from 'luxon';
- import { createEventDispatcher, onDestroy } from 'svelte';
+ import { createEventDispatcher, onMount } from 'svelte';
import { slide } from 'svelte/transition';
import { asByteUnitString } from '../../utils/byte-units';
import { handleError } from '../../utils/handle-error';
@@ -91,14 +91,12 @@
$: people = asset.people || [];
$: showingHiddenPeople = false;
- const unsubscribe = websocketStore.onAssetUpdate.subscribe((assetUpdate) => {
- if (assetUpdate && assetUpdate.id === asset.id) {
- asset = assetUpdate;
- }
- });
-
- onDestroy(() => {
- unsubscribe();
+ onMount(() => {
+ return websocketEvents.on('on_asset_update', (assetUpdate) => {
+ if (assetUpdate.id === asset.id) {
+ asset = assetUpdate;
+ }
+ });
});
const dispatch = createEventDispatcher<{
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte
index a34ee322f6b39..91a9587998fb1 100644
--- a/web/src/lib/components/faces-page/person-side-panel.svelte
+++ b/web/src/lib/components/faces-page/person-side-panel.svelte
@@ -2,7 +2,7 @@
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
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 { handleError } from '$lib/utils/handle-error';
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
@@ -49,32 +49,12 @@
let loaderLoadingDoneTimeout: NodeJS.Timeout;
let automaticRefreshTimeout: NodeJS.Timeout;
- const { onPersonThumbnail } = websocketStore;
const dispatch = createEventDispatcher<{
close: void;
refresh: void;
}>();
- // Reset value
- $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 () => {
+ async function loadPeople() {
const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
try {
const { people } = await getAllPeople({ withHidden: true });
@@ -88,6 +68,25 @@
clearTimeout(timeout);
}
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 => {
diff --git a/web/src/lib/components/shared-components/update-panel.svelte b/web/src/lib/components/shared-components/update-panel.svelte
index 13cbeecd7edfe..c566bf0e79a4c 100644
--- a/web/src/lib/components/shared-components/update-panel.svelte
+++ b/web/src/lib/components/shared-components/update-panel.svelte
@@ -1,15 +1,18 @@
diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte
index 93ba1d7a4b67e..1c58a37286d0b 100644
--- a/web/src/lib/components/shared-components/version-announcement-box.svelte
+++ b/web/src/lib/components/shared-components/version-announcement-box.svelte
@@ -6,13 +6,13 @@
let showModal = false;
- const { onRelease } = websocketStore;
+ const { release } = websocketStore;
const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
- $: releaseVersion = $onRelease && semverToName($onRelease.releaseVersion);
- $: serverVersion = $onRelease && semverToName($onRelease.serverVersion);
- $: $onRelease?.isAvailable && handleRelease();
+ $: releaseVersion = $release && semverToName($release.releaseVersion);
+ $: serverVersion = $release && semverToName($release.serverVersion);
+ $: $release?.isAvailable && handleRelease();
const onAcknowledge = () => {
localStorage.setItem('appVersion', releaseVersion);
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts
index 3000a64b06592..f6f22197030bc 100644
--- a/web/src/lib/stores/assets.store.ts
+++ b/web/src/lib/stores/assets.store.ts
@@ -4,7 +4,7 @@ import { throttle } from 'lodash-es';
import { DateTime } from 'luxon';
import { writable, type Unsubscriber } from 'svelte/store';
import { handleError } from '../utils/handle-error';
-import { websocketStore } from './websocket';
+import { websocketEvents } from './websocket';
export enum BucketPosition {
Above = 'above',
@@ -96,22 +96,14 @@ export class AssetStore {
connect() {
this.unsubscribers.push(
- websocketStore.onUploadSuccess.subscribe((value) => {
- if (value) {
- this.addPendingChanges({ type: 'add', value });
- }
+ websocketEvents.on('on_upload_success', (asset) => {
+ this.addPendingChanges({ type: 'add', value: asset });
}),
-
- websocketStore.onAssetTrash.subscribe((ids) => {
- if (ids) {
- this.addPendingChanges(...ids.map((id) => ({ type: 'trash', value: id }) as PendingChange));
- }
+ websocketEvents.on('on_asset_trash', (ids) => {
+ this.addPendingChanges(...ids.map((id): TrashAsset => ({ type: 'trash', value: id })));
}),
-
- websocketStore.onAssetDelete.subscribe((value) => {
- if (value) {
- this.addPendingChanges({ type: 'delete', value });
- }
+ websocketEvents.on('on_asset_delete', (id: string) => {
+ this.addPendingChanges({ type: 'delete', value: id });
}),
);
}
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts
index 71ba4354f02e9..33748f35fe5bc 100644
--- a/web/src/lib/stores/websocket.ts
+++ b/web/src/lib/stores/websocket.ts
@@ -1,7 +1,7 @@
+import { createEventEmitter } from '$lib/utils/eventemitter';
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
import { io, type Socket } from 'socket.io-client';
import { get, writable } from 'svelte/store';
-import { loadConfig } from './server-config.store';
import { user } from './user.store';
export interface ReleaseEvent {
@@ -10,58 +10,54 @@ export interface ReleaseEvent {
serverVersion: ServerVersionResponseDto;
releaseVersion: ServerVersionResponseDto;
}
+export interface Events {
+ on_upload_success: (asset: AssetResponseDto) => void;
+ on_asset_delete: (assetId: string) => void;
+ on_asset_trash: (assetIds: string[]) => void;
+ on_asset_update: (asset: AssetResponseDto) => void;
+ on_asset_hidden: (assetId: string) => void;
+ on_asset_restore: (assetIds: string[]) => void;
+ on_person_thumbnail: (personId: string) => void;
+ on_server_version: (serverVersion: ServerVersionResponseDto) => void;
+ on_config_update: () => void;
+ on_new_release: (newRelase: ReleaseEvent) => void;
+}
+
+const websocket: Socket = io('', {
+ path: '/api/socket.io',
+ transports: ['websocket'],
+ reconnection: true,
+ forceNew: true,
+ autoConnect: false,
+});
export const websocketStore = {
- onUploadSuccess: writable(),
- onAssetDelete: writable(),
- onAssetTrash: writable(),
- onAssetUpdate: writable(),
- onPersonThumbnail: writable(),
- serverVersion: writable(),
connected: writable(false),
- onRelease: writable(),
+ serverVersion: writable(),
+ release: writable(),
};
-let websocket: Socket | null = null;
+export const websocketEvents = createEventEmitter(websocket);
+
+websocket
+ .on('connect', () => websocketStore.connected.set(true))
+ .on('disconnect', () => websocketStore.connected.set(false))
+ .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
+ .on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion))
+ .on('connect_error', (e) => console.log('Websocket Connect Error', e));
export const openWebsocketConnection = async () => {
try {
- if (websocket) {
- return;
- }
-
if (!get(user)) {
return;
}
- websocket = io('', {
- path: '/api/socket.io',
- transports: ['websocket'],
- reconnection: true,
- forceNew: true,
- autoConnect: true,
- });
-
- websocket
- .on('connect', () => websocketStore.connected.set(true))
- .on('disconnect', () => websocketStore.connected.set(false))
- // .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(data))
- .on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(data))
- .on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(data))
- .on('on_asset_update', (data) => websocketStore.onAssetUpdate.set(data))
- .on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(data))
- .on('on_server_version', (data) => websocketStore.serverVersion.set(data))
- .on('on_config_update', () => loadConfig())
- .on('on_new_release', (data) => websocketStore.onRelease.set(data))
- .on('error', (e) => console.log('Websocket Error', e));
+ websocket.connect();
} catch (error) {
console.log('Cannot connect to websocket', error);
}
};
export const closeWebsocketConnection = () => {
- if (websocket) {
- websocket.close();
- }
- websocket = null;
+ websocket.disconnect();
};
diff --git a/web/src/lib/utils/eventemitter.ts b/web/src/lib/utils/eventemitter.ts
new file mode 100644
index 0000000000000..35d8eecf8738e
--- /dev/null
+++ b/web/src/lib/utils/eventemitter.ts
@@ -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,
+>(socket: Socket) {
+ function on>(
+ ev: Ev,
+ listener: ReservedOrUserListener,
+ ) {
+ socket.on(ev, listener);
+ return () => {
+ socket.off(ev, listener);
+ };
+ }
+
+ function once>(
+ ev: Ev,
+ listener: ReservedOrUserListener,
+ ) {
+ socket.once(ev, listener);
+ return () => {
+ socket.off(ev, listener);
+ };
+ }
+
+ function off>(
+ ev: Ev,
+ listener: ReservedOrUserListener,
+ ) {
+ socket.off(ev, listener);
+ }
+
+ return { on, once, off };
+}
diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte
index babaca1069b21..64381677f0dd7 100644
--- a/web/src/routes/(user)/people/[personId]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/+page.svelte
@@ -30,7 +30,7 @@
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.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 { clickOutside } from '$lib/utils/click-outside';
import { handleError } from '$lib/utils/handle-error';
@@ -68,7 +68,6 @@
});
const assetInteractionStore = createAssetInteractionStore();
const { selectedAssets, isMultiSelectState } = assetInteractionStore;
- const { onPersonThumbnail } = websocketStore;
let viewMode: ViewMode = ViewMode.VIEW_ASSETS;
let isEditingName = false;
@@ -119,8 +118,6 @@
$: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
$: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
- $: $onPersonThumbnail === data.person.id &&
- (thumbnailData = getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`);
$: {
if (people) {
@@ -138,6 +135,12 @@
if (action == 'merge') {
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) => {