Compare commits

...

1 Commits

Author SHA1 Message Date
midzelis
58e169b969 refactor: reimplement websocket-support as a mixin 2025-11-21 14:06:25 +00:00
4 changed files with 95 additions and 91 deletions

View File

@@ -13,7 +13,12 @@
import { isIntersecting } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset, TimelineManagerOptions, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
import {
ExtendedTimelineManager,
type TimelineAsset,
type TimelineManagerOptions,
type ViewportTopMonth,
} from '$lib/managers/timeline-manager/types';
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@@ -90,7 +95,7 @@
onThumbnailClick,
}: Props = $props();
timelineManager = new TimelineManager();
timelineManager = new ExtendedTimelineManager();
onDestroy(() => timelineManager.destroy());
$effect(() => options && void timelineManager.updateOptions(options));

View File

@@ -1,85 +1,91 @@
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { PendingChange, TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { GenericTimeManager, PendingChange, TimelineAsset } from '$lib/managers/timeline-manager/types';
import { websocketEvents } from '$lib/stores/websocket';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { throttle } from 'lodash-es';
import type { Unsubscriber } from 'svelte/store';
export class WebsocketSupport {
#pendingChanges: PendingChange[] = [];
#unsubscribers: Unsubscriber[] = [];
#timelineManager: TimelineManager;
export function HasWebsocket<T extends GenericTimeManager>(timelineManager: T) {
return class extends timelineManager {
#pendingChanges: PendingChange[] = [];
#unsubscribers: Unsubscriber[] = [];
#processPendingChanges = throttle(() => {
const { add, update, remove } = this.#getPendingChangeBatches();
if (add.length > 0) {
this.#timelineManager.upsertAssets(add);
}
if (update.length > 0) {
this.#timelineManager.upsertAssets(update);
}
if (remove.length > 0) {
this.#timelineManager.removeAssets(remove);
}
this.#pendingChanges = [];
}, 2500);
#processPendingChanges = throttle(() => {
const { add, update, remove } = this.#getPendingChangeBatches();
if (add.length > 0) {
this.upsertAssets(add);
}
if (update.length > 0) {
this.upsertAssets(update);
}
if (remove.length > 0) {
this.removeAssets(remove);
}
this.#pendingChanges = [];
}, 2500);
constructor(timeineManager: TimelineManager) {
this.#timelineManager = timeineManager;
}
connectWebsocketEvents() {
this.#unsubscribers.push(
websocketEvents.on('on_upload_success', (asset) =>
this.#addPendingChanges({ type: 'add', values: [toTimelineAsset(asset)] }),
),
websocketEvents.on('on_asset_trash', (ids) => this.#addPendingChanges({ type: 'trash', values: ids })),
websocketEvents.on('on_asset_update', (asset) =>
this.#addPendingChanges({ type: 'update', values: [toTimelineAsset(asset)] }),
),
websocketEvents.on('on_asset_delete', (id: string) => this.#addPendingChanges({ type: 'delete', values: [id] })),
);
}
disconnectWebsocketEvents() {
for (const unsubscribe of this.#unsubscribers) {
unsubscribe();
}
this.#unsubscribers = [];
}
#addPendingChanges(...changes: PendingChange[]) {
this.#pendingChanges.push(...changes);
this.#processPendingChanges();
}
#getPendingChangeBatches() {
const batch: {
add: TimelineAsset[];
update: TimelineAsset[];
remove: string[];
} = {
add: [],
update: [],
remove: [],
};
for (const { type, values } of this.#pendingChanges) {
switch (type) {
case 'add': {
batch.add.push(...values);
break;
override connect(): void {
super.connect();
if (this.#unsubscribers.length !== 0) {
if (import.meta.env.DEV) {
throw new Error('Websocket already connected');
}
case 'update': {
batch.update.push(...values);
break;
}
case 'delete':
case 'trash': {
batch.remove.push(...values);
break;
return;
}
this.#unsubscribers.push(
websocketEvents.on('on_upload_success', (asset) =>
this.#addPendingChanges({ type: 'add', values: [toTimelineAsset(asset)] }),
),
websocketEvents.on('on_asset_trash', (ids) => this.#addPendingChanges({ type: 'trash', values: ids })),
websocketEvents.on('on_asset_update', (asset) =>
this.#addPendingChanges({ type: 'update', values: [toTimelineAsset(asset)] }),
),
websocketEvents.on('on_asset_delete', (id: string) =>
this.#addPendingChanges({ type: 'delete', values: [id] }),
),
);
}
override disconnect(): void {
for (const unsubscribe of this.#unsubscribers) {
unsubscribe();
}
this.#unsubscribers = [];
super.disconnect();
}
#addPendingChanges(...changes: PendingChange[]) {
this.#pendingChanges.push(...changes);
this.#processPendingChanges();
}
#getPendingChangeBatches() {
const batch: {
add: TimelineAsset[];
update: TimelineAsset[];
remove: string[];
} = {
add: [],
update: [],
remove: [],
};
for (const { type, values } of this.#pendingChanges) {
switch (type) {
case 'add': {
batch.add.push(...values);
break;
}
case 'update': {
batch.update.push(...values);
break;
}
case 'delete':
case 'trash': {
batch.remove.push(...values);
break;
}
}
}
return batch;
}
return batch;
}
};
}

View File

@@ -15,7 +15,6 @@ import {
getMonthGroupByDate,
retrieveRange as retrieveRangeUtil,
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
import { CancellableTask } from '$lib/utils/cancellable-task';
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
@@ -84,7 +83,6 @@ export class TimelineManager extends VirtualScrollManager {
);
static #INIT_OPTIONS = {};
#websocketSupport: WebsocketSupport | undefined;
#options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS;
#updatingIntersections = false;
#scrollableElement: HTMLElement | undefined = $state();
@@ -140,21 +138,9 @@ export class TimelineManager extends VirtualScrollManager {
}
}
connect() {
if (this.#websocketSupport) {
throw new Error('TimelineManager already connected');
}
this.#websocketSupport = new WebsocketSupport(this);
this.#websocketSupport.connectWebsocketEvents();
}
connect() {}
disconnect() {
if (!this.#websocketSupport) {
return;
}
this.#websocketSupport.disconnectWebsocketEvents();
this.#websocketSupport = undefined;
}
disconnect() {}
#calculateMonthBottomViewportRatio(month: MonthGroup | undefined) {
if (!month) {

View File

@@ -1,3 +1,5 @@
import { HasWebsocket } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineDate, TimelineDateTime, TimelineYearMonth } from '$lib/utils/timeline-util';
import type { AssetStackResponseDto, AssetVisibility } from '@immich/sdk';
@@ -96,3 +98,8 @@ export interface UpdateGeometryOptions {
invalidateHeight: boolean;
noDefer?: boolean;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type GenericConstructor<T = {}> = new (...args: any[]) => T;
export type GenericTimeManager = GenericConstructor<TimelineManager>;
export const ExtendedTimelineManager = HasWebsocket(TimelineManager);